wn42
코딩이랑 이것저것
wn42
전체 방문자
오늘
어제
  • 분류 전체보기 (113)
    • 프로그래머스 (23)
      • LV1 (11)
      • LV2 (1)
      • LV3 (3)
      • 연습 (8)
    • 딥러닝 공부 (0)
      • 머신러닝&딥러닝 이론 (0)
    • 임베디드 (17)
      • Adventure Design (1)
      • 센서기반모바일로봇 (5)
      • ROS (9)
      • Google Coral (2)
    • C++ (38)
      • C++ 기초 (34)
      • 자료구조 및 알고리즘 (4)
    • Python (14)
      • 기본 파이썬 문법 (6)
      • Python 기초 (8)
    • 빅데이터 (9)
      • 빅데이터 첫걸음 시작하기(국비지원) (5)
      • 빅데이터 공부 (4)
    • 알고리즘 공부 (2)
      • 기본 알고리즘 (2)
    • 전자공학 (10)
      • 반도체 공정 (3)
      • 무선데이터통신 (7)
      • 반도체공학 (0)
    • C# (0)
      • C# 기본 (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • google coral
  • 정렬
  • Queue
  • 반복문
  • stl
  • 프로그래머스
  • 내일배움카드
  • 패스트캠퍼스
  • K디지털크레딧
  • ROS
  • 소멸자
  • 스택
  • 인스턴스
  • 딥러닝
  • 조건문
  • 데이터분석 인강
  • c++
  • 변수
  • Python
  • 데이터분석
  • 큐
  • 클래스
  • 빅데이터 첫걸음 시작하기
  • 스택/큐
  • 노드
  • 빅데이터
  • numpy
  • 바이트디그리
  • 파이썬
  • 상속

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
wn42

코딩이랑 이것저것

[C++] 포인터(Pointer) 기초
C++/C++ 기초

[C++] 포인터(Pointer) 기초

2022. 12. 23. 16:35

 포인터(Pointer) 

다른 변수, 혹은 그 변수의 메모리 공간주소(시작 주소값)를 가리키는 변수

배열 a의 시작주소를 가리키는 포인터 변수 pa

 

포인터 선언

포인터를 선언하는 방식은 2가지가 있으며, 아래와 같다. 둘 중 무엇을 쓰더라도 같은 동작을 한다.

  • (포인터에 주소값이 저장되는 데이터의 Type) *(포인터 변수 명);    # asterisk(*)가 변수 명 앞
  • (포인터에 주소값이 저장되는 데이터의 Type)* (포인터 변수 명);    # asterisk(*)가 타입 뒤
  • 포인터에 타입이 있는 이유는 컴퓨터에 메모리 공간을 얼마나 할당해야 할지 알려주기 위함이다.
int *ptr;
int* ptr;

 

& 연산자

  • 데이터의 주소값을 알고 싶을 때 사용하는 연산자
  • '&변수명' 의 형태로 사용된다. 
#include <iostream>

int main() {
	int a = 7;
	
	int* p1 = &a;
	int *p2 = &a;

	std::cout << &a << std::endl;
	std::cout << p1 << std::endl;
	std::cout << p2 << std::endl;

	return 0;
}
0093FCA0
0093FCA0
0093FCA0

> 모두 동일한 주소값을 가리키고 있음을 알 수 있다.

 

* 연산자

  • 포인터가 보관하고 있는 주소값에 대응하는 데이터를 가져올 때 사용하는 연산자이다.
  • 'int *p = &a;'로 포인터를 선언하면, *p는 변수 a 자체를 의미하게 된다.
#include <iostream>

int main() {
	int a = 7;
	
	int* p1 = &a;
	int *p2 = &a;

	std::cout << "a의 값: " << a << std::endl;
	std::cout << "p1이 가리키는 데이터: " << *p1 << std::endl;
	std::cout << "p2이 가리키는 데이터: " << *p2 << std::endl;

	return 0;
}
a의 값: 7
p1이 가리키는 데이터: 7
p2이 가리키는 데이터: 7

 

 

포인터를 사용하는 이유

포인터를 사용하는 이유는 다양하다.

  • 메모리 주소를 직접 참조하므로 다양한 자료형 변수에 대한 접근 및 조작이 용이하다.
  • Call by Reference 방식을 이용할 수 있다. ▶ 전역변수의 사용을 억제
  • 배열, 구조체 등 복잡하고 다양한 자료구조의 데이터에 쉽게 접근하고 조작할 수 있다.
  • 메모리의 동적 할당(힙 영역)을 가능하게 한다.
  • 이외에도 포인터를 사용하는 이유는 다양하다.

 

포인터 값 변경

  • 포인터는 변수이기 때문에 값을 변경할 수 있다.

 

포인터에 저장된 주소값 바꾸기

#include <iostream>

int main() {
	int a = 7, b = 10;

	int* ptr;

	ptr = &a;
	std::cout << "ptr이 가리키는 데이터: " << *ptr << std::endl;

	ptr = &b;
	std::cout << "ptr이 가리키는 데이터: " << *ptr << std::endl;

	return 0;
}
ptr이 가리키는 데이터: 7
ptr이 가리키는 데이터: 10

 

포인터가 가리키는 데이터의 값 바꾸기

  • *p는 가리키는 변수 그 자체이므로, 포인터를 이용하여 가리키는 변수의 값을 변경할 수 있다.
#include <iostream>

int main() {
	int a = 7;
	int *ptr = &a;

	std::cout << "a의 값(변경 전): " << a << std::endl;

	*ptr = 20;
	std::cout << "a의 값(변경 후): " << a << std::endl;

	return 0;
}
a의 값(변경 전): 7
a의 값(변경 후): 20

 

 

이중 포인터

출처: C++ Pointer To Pointer (Double Pointer) - GeeksforGeeks

  • 포인터는 포인터를 가리킬 수 있다.
  • asterisk(*)를 2개 연속하여 사용하여 선언한다. EX) int **p;
  • 'int **ptr2 = &ptr1;' 으로 포인터 변수 ptr2를 초기화하면, ptr2는 포인터 변수 ptr1의 주소값을 저장한다.
  • asterisk(*)를 2개 연속하여 사용하면 원본 데이터의 값을 출력할 수 있다.

 

이중 포인터 선언

#include <iostream>

int main() {
	int a = 7;
	int *ptr = &a;		// a의 주소를 담는 ptr 선언
	int** pptr = &ptr;  // ptr의 주소를 담는 pptr 선언

	std::cout << "a의 값: " << a << std::endl;
	std::cout << "ptr이 가리키는 값: " << *ptr << std::endl;
	std::cout << "pptr이 가리키는 값: " << **pptr << std::endl;
	std::cout << std::endl;

	std::cout << "a의 주소값: " << &a << std::endl;
	std::cout << "ptr이 담고있는 주소값: " << ptr << std::endl;
	std::cout << "*pptr이 가리키는 값: " << *pptr << std::endl;
	std::cout << "포인터 변수 ptr의 주소값: " << &ptr << std::endl;
	std::cout << "pptr이 담고있는 주소값: " << pptr << std::endl;
	std::cout << "포인터 변수 pptr의 주소값: " << &pptr << std::endl;

	return 0;
}
a의 값: 7
ptr이 가리키는 값: 7
pptr이 가리키는 값: 7

a의 주소값: 0076FAE8
ptr이 담고있는 주소값: 0076FAE8
*pptr이 가리키는 값: 0076FAE8
포인터 변수 ptr의 주소값: 0076FADC
pptr이 담고있는 주소값: 0076FADC
포인터 변수 pptr의 주소값: 0076FAD0
  • &a와 ptr, *pptr은 모두 같은 주소값을 가리킨다.
  • pptr은 포인터변수 ptr이 저장된 메모리 주소값을 담고 있다.
  • **pptr은 변수 a 그 자체를 나타낸다.

 

이중 포인터를 사용하는 이유

  • 값을 전부 넘기는 것보다는 값을 가리키는 주소값을 넘기는 것이 리소스와 처리시간 측면에서 효율적이다.
  • 특히나 영상 등의 2차원 배열은 데이터 크기가 매우 크므로, 값을 직접 넘기는 경우(Call by Value) 처리시간이 길어지고, 리소스 낭비가 심해진다.
  • 따라서 이러한 경우에는 이중 포인터를 이용하여 주소를 넘기는(Call by Reference) 방식으로 처리하는 것이 좋다.

 

 

상수 포인터

  • 포인터도 변수이기 때문에 const 키워드를 이용하여 상수 처리할 수 있다.

 

const datatype*

  • const datatype*는 포인터 변수가 가리키는 변수의 값을 포인터 변수로는 변경할 수 없도록 한다.
  • 포인터 변수는 저장된 메모리 주소를 다른 변수의 메모리 주소로 옮길 수는 있어도, 포인터 변수를 이용하여 저장된 메모리 주소에 해당하는 변수의 값은 변경할 수 없다.

 

예제 1) ptr로 가리키는 변수 값 변경하기 (오류 발생)

#include <iostream>

int main() {
	int a = 7;
	const int *ptr = &a;

	// ptr이 가리키는 값 변경하기
	*ptr = 3;

	return 0;
}

  • 포인터 변수 ptr은 변수 a를 가리키며, const 키워드가 붙어있기 때문에 ptr가 가리키는 변수 a의 값은 변경할 수 없다.

 

예제 2) ptr이 가리키는 변수를 직접 변경하기 (오류 미발생)

#include <iostream>

int main() {
	int a = 7;
	const int* ptr = &a;

	std::cout << "a의 값(변경 전): " << *ptr << std::endl;
	std::cout << "ptr 주소값(변경 후): " << ptr << std::endl;

	// a의 값 변경하기
	a = 10;
	std::cout << "a의 값(변경 후): " << *ptr << std::endl;
	std::cout << "ptr 주소값(변경 후): " << ptr << std::endl;
	
	return 0;
}
a의 값(변경 전): 7
ptr 주소값(변경 후): 00D3FD6C
a의 값(변경 후): 10
ptr 주소값(변경 후): 00D3FD6C
  • 변수 a 자체는 const가 아니기 때문에 변경하더라도 오류가 발생하지 않는다. (변수 a의 값을 변경하더라도 변수 a가 저장된 메모리 주소는 바뀌지 않는다.)
  • 변수 a를 가리키는 포인터 ptr을 통해서 a의 값을 변경하는 것은 불가능하지만, 변수 a 자체를 직접적으로 변경하는 것은 가능하다.

 

예제 3) ptr의 값(담고 있는 주소값) 변경하기 (오류 미발생)

#include <iostream>

int main() {
	int a = 7;
	int b = 10;
	const int* ptr = &a;

	std::cout << "ptr에 담긴 값(변경 전): " << *ptr << std::endl;
	std::cout << "ptr 주소값(변경 전): " << ptr << std::endl;

	// ptr가 담고 있는 주소값 변경하기
	ptr = &b;
	std::cout << "ptr에 담긴 값(변경 후): " << *ptr << std::endl;
	std::cout << "ptr 주소값(변경 후): " << ptr << std::endl;

	return 0;
}
ptr에 담긴 값(변경 전): 7
ptr 주소값(변경 전): 010FFBE8
ptr에 담긴 값(변경 후): 10
ptr 주소값(변경 후): 010FFBDC
  • 변경이 불가능한 것은 ptr이 가리키는 변수의 주소값이다.
  • 따라서 ptr이 가리키는 변수 자체를 변경하는 것은 오류가 발생하지 않는다. (변수들의 주소는 그대로 유지)

 

datatype* const

  • datatype* const는 포인터 변수의 값을 변경할 수 없게 한다.
  • 즉, 한번 포인터 변수의 값을 초기화하면, 다른 변수를 가리키도록 변경할 수 없다.

 

예제 1) ptr의 값(담고 있는 주소값) 변경하기 > 다른 변수 가리키기 (오류 발생)

#include <iostream>

int main() {
	int a = 7, b = 10;
	int* const ptr = &a;

	// ptr가 담고 있는 주소값 변경하기(다른 변수 가리키기)
	ptr = &b;

	return 0;
}

  • 변수 a와 변수 b의 데이터는 서로 다른 주소값에 매칭되어 있다.
  • ptr이 담고 있는 주소값은 변경할 수 없기 때문에 변수 b의 주소값으로 변경할 시 오류가 발생한다.

 

예제 2) ptr이 가리키는 변수를 직접 변경하기 (오류 미발생)

#include <iostream>

int main() {
	int a = 7;
	int* const ptr = &a;

	std::cout << "a의 값(변경 전): " << *ptr << std::endl;
	std::cout << "ptr이 담고있는 주소값: " << ptr << std::endl;

	// a의 값 변경하기
	a = 10;
	std::cout << "a의 값(변경 후): " << *ptr << std::endl;
	std::cout << "ptr이 담고있는 주소값: " << ptr << std::endl;

	return 0;
}
a의 값(변경 전): 7
ptr이 담고있는 주소값: 00CFFBAC
a의 값(변경 후): 10
ptr이 담고있는 주소값: 00CFFBAC
  • ptr의 값(가리키는 변수의 주소값) 자체가 변하지 않기 때문에 오류가 발생하지 않는다.

 

예제 3) ptr로 가리키는 변수 값 변경하기 (오류 미발생)

#include <iostream>

int main() {
	int a = 7;
	int* const ptr = &a;

	std::cout << "a의 값(변경 전): " << a << std::endl;
	std::cout << "ptr이 담고있는 주소값: " << ptr << std::endl;

	// ptr이 가리키는 값 변경하기
	*ptr = 10;
	std::cout << "a의 값(변경 후): " << a << std::endl;
	std::cout << "ptr이 담고있는 주소값: " << ptr << std::endl;

	return 0;
}
a의 값(변경 전): 7
ptr이 담고있는 주소값: 0135FE38
a의 값(변경 후): 10
ptr이 담고있는 주소값: 0135FE38
  • 포인터 변수 ptr로 변수 a의 값을 변경하더라도 주소값 자체가 바뀌는 것이 아니므로 오류가 발생하지 않는다.

 

 

포인터 연산

  • 포인터는 변수이기 때문에 덧셈과 뺄셈 연산이 가능하다.

 

포인터 덧셈

예제 1) 포인터에 상수 더하기 & 증감연산자 사용  (오류 미발생)

#include <iostream>

int main() {
	int a;
	int* ptr = &a;

	std::cout << "ptr의 값: " << ptr << std::endl;
	std::cout << "(ptr + 1)의 값: " << ptr + 1 << std::endl;

	ptr++;
	std::cout << "ptr의 값(증감 후): " << ptr << std::endl;

	return 0;
}
ptr의 값: 00EFFD94
(ptr + 1)의 값: 00EFFD98
ptr의 값(증감 후): 00EFFD98
  • 덧셈이 가능하므로, 당연하게도 증감연산자 사용이 가능하다.
  • ptr이 4byte 메모리 크기를 할당하는 int를 가리키는 포인터 변수이므로, 해당 메모리 크기만큼 주소가 증가한다.

 

예제 2) 포인터 + 포인터 (오류 발생)

#include <iostream>

int main() {
	int a, b;
	int* ptr1 = &a;
	int* ptr2 = &b;

	int* ptr = ptr1 + ptr2;

	return 0;
}

  • 포인터끼리의 덧셈은 허용하지 않는다.
  • 포인터끼리 더하는 경우, 의미없는 프로그램의 지점을 가리키기 때문에 할 이유가 없다.
  • 반면, 포인터끼리 빼는 경우는 허용한다. 포인터끼리 뺄셈을 진행함으로써, 포인터 사이에 데이터가 몇 개가 있는지 확인할 수 있다. 이처럼 의미가 있는 연산이기 때문에 허용된다.

 

포인터 뺄셈

예제 1) 포인터에 상수 빼기 & 증감연산자 사용  (오류 미발생)

#include <iostream>

int main() {
	int a;
	int* ptr = &a;

	std::cout << "ptr의 값: " << ptr << std::endl;
	std::cout << "(ptr - 1)의 값: " << ptr - 1 << std::endl;

	ptr--;
	std::cout << "ptr의 값(증감 후): " << ptr << std::endl;

	return 0;
}
ptr의 값: 0053FD44
(ptr - 1)의 값: 0053FD40
ptr의 값(증감 후): 0053FD40
  • 뺄셈 역시 가능하며, 증감연산자 사용도 가능하다.
  • 포인터 변수가 가리키는 데이터형의 크기만큼 주소값이 변경된다.

 

예제 2) 포인터 - 포인터 (오류 미발생)

#include <iostream>

int main() {
	int* ptr1 = (int*)1000;
	int* ptr2 = (int*)1012;

	int count;
	count = ptr2 - ptr1;
	std::cout << "ptr2와 ptr1 사이에 존재하는 데이터의 개수: " << count << std::endl;

	return 0;
}
ptr2와 ptr1 사이에 존재하는 데이터의 개수: 3
  • 포인터끼리의 덧셈과는 달리, 포인터끼리의 뺄셈은 가능하다.
  • 포인터끼리의 뺄셈 연산 자체는 데이터의 개수를 세는 기능을 수행하므로 따로 sizeof(int)로 나누는 과정을 진행하지 않아도 된다.
  • ptr2 - ptr1 = (1012 - 1000) / sizeof(int)

 

포인터 대입

  • 포인터끼리의 대입이 가능하다.
#include <iostream>

int main() {
	int a;
	int* ptr1 = &a;
	*ptr1 = 10;

	int* ptr2;
	ptr2 = ptr1;

	std::cout << "ptr1이 가리키는 값: " << *ptr1 << std::endl;
	std::cout << "ptr2이 가리키는 값: " << *ptr2 << std::endl;

	return 0;
}
ptr1이 가리키는 값: 10
ptr2이 가리키는 값: 10

 

    'C++/C++ 기초' 카테고리의 다른 글
    • [C++] 함수(Function)
    • [C++] 포인터와 배열
    • [C++] 문자열(String)
    • [C++] 배열(Array)
    wn42
    wn42
    코딩이랑 이것저것 하는 블로그

    티스토리툴바