본문 바로가기
프로그래밍/C언어

C언어 포인터 이야기

by 페이지다운 2021. 10. 7.
반응형

*헷갈릴까봐 말해두지만, 여기서 자료형과 타입은 같은 말이다. 혼용하고 있으나 같은 뜻이다.

오늘은 포인터에 관한 이야기이다.

입문자들은 포인터에 관한 막연한 불안감을 갖는 경우가 많은데, 이는 소문에 의한 거품에 가깝다. 정말 별것도 아니고 그냥 직관적으로 받아들이고 쓰면 된다. 특히 나는 몇몇 오해를 불러일으킬 수 있는 표현법에 대한 설명을 해주고자 한다.

일단 포인터가 무엇인가? 포인터는 메모리의 특정 위치를 가리키는 변수이다. 여기서 가리킨다는 것은 결국 포인터 변수가 가리키고자 하는 변수의 주소값을 갖는다는 의미이다.

백문이 불여일견, 직접 코드를 보자.

int value = 10;
int* p = &value;

대부분 입문자들은 이 코드를 int형 변수 value와 value를 가리키는 포인터 p라고 말할 것이다.

그러나 이는 코드의 반만을 표현한 소리이다. 그럼 이 코드를 완벽하게 표현하는 문장은 무엇일까?

int형 변수 value와 value를 가리키는 int형 포인터 타입 변수 p

뭔가 말이 복잡하다. 특히 사람들이 헷갈리는 부분은 포인터 타입 변수가 무엇인가에 관한 것일거라고 생각한다.

보통 포인터를 표현하는 방법은 두 가지가 있다.

int value = 10;
int *p = &value;

int value = 10;
int* p = &value;

가 있다.

일반적으로는 차이가 없기 때문에 원하는 것으로 골라 쓰라고 한다. 하지만 나는 전자를 권장하지 않는다. 나는 후자를 강력하게 권한다.

이 둘의 차이는 포인터를 바라보는 관점의 차이에서 기인한다. 전자는 변수가 int형 변수의 포인터인 것에 초점을 맞춘다면 후자는 변수가 int형 변수의 포인터 타입인 것에 초점을 맞춘다.

내가 왜 전자의 표현법을 경계하냐면, 자료형과 포인터는 별개가 아니기 때문이다.

그럼 일단 자료형이 뭔지부터 확실하게 하고 가야할 것 같다.

위키백과의 자료형에 대한 정의는 여러 종류의 데이터를 식별하는 분류라고 하고 있다.

우리는 C에서 변수를 선언할 때 "자료형 변수이름"과 같이 선언한다.

즉, 내가 주장하는 바는 int*를 int와 * 가 아닌 int* 라는 하나의 자료형으로 보자는 것이다.

달리 말해 포인터는 해당 타입의 포인터 타입로 보아야 한다.

char*과 int*는 담는 정보의 종류는 같지만 분류는 완전히 다르다는 것이다. *는 중요한 것이 아니다. char*인 것과 int*인 것이 중요할 뿐이다.

상식적으로 생각해도 그렇다. char*가 가리키는 변수는 해당 위치로부터 1바이트 단위로 접근할 것이고, int*가 가리키는 변수는 해당 위치로부터 4바이트 단위로 접근할 것이다. 완전히 다른 타입인 것이다.

아직 감이 안 잡힐 수도 있다. 타입에 근거한 표현법의 위력을 보자. 이제부터는 이중 포인터다.

int **pp = &p;

가 있고

int** pp = &p;

가 있다.

아까와 같은 해석을 적용해 보면 전자는 int형 변수의 이중 포인터에 초점을 맞춘 것이고, 후자는 int형 포인터 타입 변수의 포인터에 초점을 맞춘 것이다.

차이를 모르겠다고?

그럼 이렇게 해석해 보아라.

(int*)* pp = &p;

훨씬 직관적으로 와닫지 않는가? int*를 하나의 타입으로 보았을 때의 결과이다. 우리는 포인터형 변수를 선언할 때 우측에 한 차원 낮은 타입의 변수가 온다는 점에서 int* 형 변수가 나와야 함을 아주 쉽게 예측할 수 있다. 우리는 굳이 int와 *를 따로 해석할 필요가 없다. 만약 포인터 그 자체에 집중한다면 "단일 포인터를 가리키는 이중 포인터인데 int를 곁들인" 같은 괴상한 해석을 하게 될 것이다.

int에 대한 이중 포인터int*를 가리키는 포인터 중 뭐가 더 해석이 용이한지는 자명하다.

즉 결론적으로, 포인터는 일반 타입과 따로 노는 별개의 문법이 아니라 새로운 타입 그 자체이다. 실제로 컴파일러가 해석하는 원리도 이에 기반한다. 이중 포인터에서 1차원 포인터로 캐스팅을 할 때 경고가 나오는 것도 차원이 안 맞아서 따위의 이유가 아니라 애초에 타입이 맞지 않기 때문이다.

정리하면
int*는 int를 가리키는 포인터 타입
int**는 int를 가리키는 포인터 타입의 포인터 타입 → (int*)*
int***는 int를 가리키는 포인터 타입의 포인터 타입의 포인터 타입 → ((int*)*)* → (int**)*
와 같이 볼 수 있는 것이다.

즉 애스터리스크(*)가 하나 들러붙을 때마다 새로운 타입이 탄생한다는 말이다.

누구는 말할 수 있다. int*와 int** 모두 같이 메모리를 가리키는 타입인데 왜 구분함?

이에 대한 대답은 위에서 이미 언급한 char*와 int* 간의 차이와 완벽하게 동일하다. int**의 경우에는 포인터의 포인터이므로 포인터의 크기인 8바이트 단위로 접근하겠지만(64비트 기준) int*의 경우에는 int형 변수에 대한 포인터이므로 int의 크기인 4바이트 단위로 접근할 것이다.

이런 기술적인 이유도 있지만, 결국에는 자료형의 의의를 생각해 본다면 개발자를 위한 것이다.

int**를 본다면 int*를 가리키는 포인터 타입임을 알 수 있을 것이고 int***를 본다면 int**를 가리키는 포인터 타입임을 알 수 있다. 아무리 고차 포인터로 올라가도 우리는 맨 아래까지 볼 필요가 없다. 현재 타입과 한 차원 아래의 포인터 타입만 보면 된다.

그 외 - 배열과 구조체


사실상 포인터가 가장 많이 쓰이는 경우는 배열과 구조체라고 할 수 있다. 하지만 우리는 이제 이것들을 어렵지 않게 이해할 수 있다.

int의 배열 → int*
int의 배열의 배열(2차원 배열) → int*의 배열 → (int*)*

구조체 Struct가 있다고 가정할 때 (typedef로 별칭을 정의한 경우)
Struct의 배열 → Struct*
Struct의 배열의 배열(2차원 배열) → Struct*의 배열 → (Struct*)*

다만 2차원 배열을 꼭 이중 포인터로 사용할 필요는 없다. 1차원 배열을 통해서도 2차원 배열을 이용할 수 있다. 이중 포인터를 사용하는 경우는 동적 할당을 이용해 반복문으로 2차원 배열을 순회하며 1차원 배열의 주소를 할당한 경우이다. 

구조체 배열의 경우에도 구조체를 동적 할당을 통해 배열을 순회하면서 할당한 경우에는 이중 포인터를 이용할 수 있다. 구조체가 아니라 원시 타입도 가능하겠지만 굳이 그럴 이유는 없을 것이다. 물론 이 경우에는 이중 포인터를 사용하지만 1차원 배열이다.

정리


마지막으로 이 글의 모든 내용을 한마디로 압축하겠다.

포인터는 하나의 타입이다


일단 최대한 입문자가 이해할 수 있도록 쉽게 표현하기 위해 노력했으나 잘은 모르겠다. 이해되지 않는 부분이 있다면 적극적으로 물어보시라. 최대한 성심성의껏 대답해 드리도록 하겠다.


반응형

'프로그래밍 > C언어' 카테고리의 다른 글

C로 만든 워드 서치 학습지 생성기  (0) 2021.10.08
C로 만든 슈팅 게임  (0) 2021.10.08

댓글