C 포인터

2021년 7월 22일 수정

C 포인터(Pointer)

C 언어를 악명높게 한 개념이 바로 이 포인터다. 이름 처럼 뭔가를 가리키기 위해 사용되는 개념인데 주로 가리키는 대상은 메모리의 위치(주소)다. 과거에는 하드웨어를 직접 액세스하기 위한 목적으로 정적인 메모리 주소를 포인팅하는 경우도 있지만 현대에서 이렇게 쓰는 경우는 없다. 현대에는 메모리 공유나 기본 타입 이외의 고차원적인 데이터를 취급할 때 많이 사용된다.

포인터의 용도

포인터가 주로 쓰이는 분야는 다음과 같다:

  • 메모리 공유: 특정 함수에 넘긴 변수의 값이 함수 내부에서 변경될 수 있다. 위험한 형태이므로 가급적 쓰지 않는 것을 추천하는 패턴이다.
  • 원시 타입(Primitive Type)을 제외한 타입을 매개변수로 전달할 때: C 언어는 저수준(?) 언어이기 때문에 struct 등 복잡한 타입의 값을 복사하는 등의 고차원적인 행동은 하지 않기 때문에 이런 복잡한 타입을 다루려면 어쩔 수 없이 포인터를 쓸 수 밖에 없다.
  • 힙 메모리 사용: 원시 타입에서 점유하는 스택 공간은 사용량에 제한이 있다보니 힙(Heap)에서 메모리를 할당 받아서 사용하려면 반드시 포인터가 필요하다.

    물론 이 외에도 여러 용도가 있을 수 있지만 보통 이런 경우에 많이 볼 수 있다.

포인터 문법

포인터 변수는 특정 타입에 * 를 붙여서 선언한다. 명확하게 하기 위해 타입을 정의할 때 띄어쓰기를 하면 포인터 타입과 일반 타입을 구분할 수 있다.

int a = 0;
int *ptr_a = &a;   /* 1 */
int* ptr_b = &a;   /* 2 */

위 1번과 2번 둘 다 같은 용도의 코드지만 2번 코드가 띄어쓰기 덕분인지 int 포인터 타입 이라고 읽히기 유리한 것 같다. 물론 둘 다 허용되는 문법이며 이상하게 (나를 포함해서) 대부분 1번 방식을 많이 사용한다.

& 는 특정 변수의 포인터를 얻기 위해서 사용한다. 즉 &aa 변수의 레퍼런스 포인터를 얻는 코드다.

위 코드는 결과적으로 ptr_aptr_b 가 동일하게 a가 사용하는 메모리 공간을 가리키게 된다.

하지만 a 가 정수형임에 비해 ptr_aptr_b 는 정수 포인터 타입이다. 포인터 타입은 다른 타입의 크기와는 관계 없이 메모리 어드레스를 포인팅 하기 위한 타입이다. 따라서 넣을 수 있는 데이터의 한계도 원래의 타입과는 전혀 다르다.

포인터 변수의 내용을 바꾸는 것은 굉장히 위험한 일이다.

ptr_a = 1;

아마 최근의 컴파일러라면 위 코드는 컴파일 시 경고를 낼 만한 코드인데, 이 코드는 ptr_a 가 1번 메모리 어드레스를 가리키게 한다. 여기를 읽거나 쓰는 행위는 어떤 결과가 발생할 지 알 수가 없으므로 포인터 변수에 직접 값을 할당하는 행위는 하지 말자.

포인터의 내용을 액세스하기 위해 * 를 사용한다.

*ptr_a = 1;

이 코드는 ptr_a 가 가리키는 곳에 1을 쓴다는 의미다. 따라서 이 코드가 실행되면 a 변수의 값이 1로 바뀐다.

배열과 포인터

포인터는 배열과 매치하기 편한 편이다.

int lst[] = { 0, 1, 2, 3, 4, 5 };
int *ptr_lst = (int *)lst;

이제 포인터를 활용해 이런 짓을 할 수도 있다.

printf("first = %d, second = %d", *ptr_lst, *(ptr_lst + 1));

배열은 연속적인 메모리로 할당된다. 포인터는 연속적인 메모리를 탐색하기에 편하다.

포인터 변수에 증감을 하는 행위는 해당 포인터가 어떤 타입의 포인터냐에 따라 실제로 가리키는 곳이 달라진다.

예를 들어 32비트 정수형 포인터 변수의 값에 1을 더하면 이 포인터가 가리키는 실제 주소는 원래에서 4바이트(32비트)가 증가된 곳이다.

포인터는 과거에는 물리적 메모리 어드레스를 다루기도 했는데 현대의 OS들은 가상 어드레스를 사용한다. 그래서 다른 언어들에서 쓰는 레퍼런스 개념과 비슷하게 생각해도 크게 틀리거나 하지는 않는다.

하지만 레퍼런스와 포인터가 완전히 같은 개념이 아니라는 것은 확실하게 이해하자.