포인터 (Pointer)
포인터(Pointer)란, 메모리의 주소를 저장하는 변수입니다. 이는 프로그래밍에서 중요한 역할을 하는데, 메모리의 특정 위치를 참조하고 그 위치에 저장된 값을 얻는 것을 역참조라고 합니다.
포인터를 사용하게 되면, 데이터를 복사하지 않기 때문에 데이터 구조를 효율적으로 순회하거나, 메모리를 직접 조작할 수 있어 훤씬 빠르고 메모리를 효율적으로 사용할 수 있습니다.
- 참조: 메모리의 특정 위치를 참조합니다. (메모리 주소를 참조하는 것으로 주소를 가져옵니다.)
- 역참조: 포인터에 저장된 메모리 주소로 가서 값을 가져오는 것.
사실, 한글로 참조라는 단어 뜻을 살펴보면 포인터의 참조와 역참조라는 단어가 헷갈립니다.
영어로 포인터 참조는 pointer referencing이라고 하는데, 여기에서 reference의 뜻은 어떤 대상을 언급하거나 말하기, 참고, 가리키기 정도로 볼 수 있을 것 같습니다.
따라서, 참조(referencing)는 변수가 가지고 있는 메모리 주소를 언급해주는 것(가져오는 것) 정도로 생각합니다.
즉, 포인터 변수가 어떤 메모리 주소를 참조(참고)하라고 메모리 주소를 넘겨 준다. 는 뜻으로 생각합니다.
포인터의 역참조 또한 영어 단어로 보자면, dereferencing의 “de-“ 접두사는 어떤 것을 제거하거나 반대의 행위를 나타내는 데,
역참조(dereferencing)를 생각해보면, 반대로 이미 가지고 있는 메모리 주소를 가지고 메모리를 찾아간 것? 사용한 것? 정도의 뜻이라고 생각합니다.
포인터에 대해서 좀 더 쉽게 이해할 수 있도록 게임에 빗대어 설명하자면, 포인터는 마치 포탈과 같습니다.
출구에 해당하는 주소를 저장하고, 어디에서든 입구를 열어 해당 장소로 이동 할 수 있습니다.
실제 게임에서는 그렇지 않지만, 만약 포탈의 크기를 정할 수 있다면… 공간의 크기를 넘어 다른 공간을 침범해버릴 수 있으니 크기를 잘 조절하는게 좋습니다. (1층과 2층을 같이 열어버리면 버그입니다!)
데이터형과 포인터
포인터의 크기
포인터의 크기는 운영체제의 주소 값 크기에 따라 달라질 수 있습니다.
32비트 운영체제에서는 4바이트, 64비트 운영체제에서는 8바이트를 차지합니다.
데이터형
다만, 같은 운영체제에서 포인터는 모두 같은 크기를 갖기에 포인터를 선언할 때는 해당 포인터가 가리키는 데이터의 타입을 명시해야 합니다.
이는 데이터형이 일치하지 않을 경우, 다른 메모리 영역을 침범할 위험이 있기 때문입니다.
(int의 경우 4byte씩 읽겠지만, long long의 경우에는 8byte씩 읽어야 합니다.)
주소 연산자와 간접 참조 연산자
&
(주소 연산자, Address-of): 변수의 이름 앞에 사용하여, 해당 변수의 주소 값을 반환합니다.- ‘&’기호는 앰퍼샌드(ampersand)라고 읽으며, 번지 연산자라고도 불립니다.
*
(간접 참조 연산자, Indirection, dereferencing): 포인터의 이름이나 주소 앞에 사용하여, 포인터에 저장된 주소로 가서 값을 반환합니다.- ‘*‘기호는 역참조 연산자로 에스크리터(asterisk operator)라고도 불립니다.
포인터 선언
포인터를 선언할 때, 포인터 변수를 연속적으로 선언하고 싶다면 주의해야 하는 점이 있습니다.
1
2
3
int* p1, p2; // p1은 포인터, p2는 일반 int 변수
*p1 = 10; // 간접 참조 연산자 사용
위와 같이 선언하면, p1은 포인터 변수가 되지만, p2는 일반 int형 변수가 됩니다.
- 포인터 선언시 변수 앞에서 간접 참조 연산자(
*
)를 붙여야 합니다. - 포인터 이름 앞에 간접 참조 연산자(
*
)를 사용해서 값 위치에 접근할 수 있습니다.
포인터의 종류
1. Pointers to objects: 객체에 대한 포인터
이 포인터는 해당 데이터 타입의 크기만큼의 메모리 영역을 가리키며, 이 영역에 데이터를 읽고 쓸 수 있습니다.
1
2
int x = 10;
int* p = &x; // 'p'는 int 타입 객체를 가리킴
2. Pointers to void: void
에 대한 포인터
void
포인터는 특정 타입을 지정하지 않은 포인터입니다.
이 포인터는 메모리의 어떤 위치라도 가리킬 수 있지만, 이 자체로는 역참조를 할 수 없습니다. 즉, 어떤 타입의 데이터가 저장되어 있는지 알 수 없으므로 데이터를 읽거나 쓸 수 없습니다.
1
2
3
int n = 1;
void* p = &n;
cout << *p; // 문제 발생
하지만, 형변환을 통해 우리가 원하는 자료형으로 사용할 수 있습니다.
1
2
3
int n = 1;
void* p = &n;
cout << *(int*)p;
이를 잘 사용하면, 함수 설계시 어떤 자료형으로 반환해야 할 지 모르는 함수에서 사용자가 원하는 자료형으로 사용할 수 있게끔 설계할 수 있습니다.
대표적으로 malloc
함수가 그런 함수에 해당합니다. malloc
함수는 필요한 크기만큼 메모리를 할당해주고, 사용자가 사용할 때, 원하는 자료형의 크기만큼 나눠서 사용할 수 있게 합니다.
1
void* __cdecl malloc(_In_ _CRT_GUARDOVERFLOW size_t _Size);
3. Pointers to functions: 함수에 대한 포인터
이 포인터는 함수를 가리킵니다. 함수 포인터는 특정한 시그니처를 가진 함수의 주소를 저장할 수 있습니다.
이를 통해 함수를 변수처럼 다룰 수 있어, 콜백 함수나 함수 테이블 등을 구현할 때 유용합니다.
1
2
3
void myFunction(int x) {}
void (*funcPtr)(int) = myFunction; // 함수 포인터
포인터의 산술 연산(증가, 감소)
포인터는 증가(++
), 감소(--
), 덧셈(+
), 뺄셈(-
) 등의 연산이 가능합니다.
포인터의 산술 연산은 자료형의 크기만큼 주소를 더하거나 뺄 수 있는데, 예를 들어, arr[2]
는 *(arr + 2)
와 동일합니다.