C의 포인터 개념을 처음 배우고 가장 많이하는 실수중에 하나는 int * p;로 포인터을 선언하고 *p;로 포인터가 가리키는 값에 접근한다. 문법적으로 틀린 것은 아니지만 포인터를 초기화할려고 시도했다면 잘못된 동작이다. 또한 대부분의 C 입문서나 대학 교수들은(또는 강사들은) 틀리지는 않지만 잘못된 접근 방식으로 학생들을 가르치기 때문에 초보자가 오개념에 빠지도록 방치한다. 누군가 지식을 검증해줄 사람이 없다면 혼자서 오개념으로부터 빠져나오는 것은 어렵다. 다행히도 지금은 인터넷이 대중화되어있기 때문에 조금만 검색을 해도 곧 답을 찾을 수 있지만 인터넷 자료의 검증을 문제삼을 수도 있고 초보자들은 대부분 검색할려고 시도하는 시간보다 질문글을 짧게 작성하고 답변이 달릴때까지 기다리는 시간이 더 많다는 것이다. 서론이 조금 길었지만 이 글은 입문자들에게 그나마 더 나은 설명을 제공하고자 한다.

먼저 이 글은 몇가지의 장으로 구성되어 있고 전체적으로나 부분적으로든지 어떻게든 길게 설명하진 않는다. 다만 최소한의 일반적인 상식을 가졌다면 이해하기 쉽도록 노력해서 설명해본다.

1. 포인터와 배열의 차이.
아마 가장 궁금해하지 않을까 쉽다. 유감스럽게도 이 장에서는 확실한 해답을 줄 수가 없다. 이 내용은 천천히 중간에 다른 개념들을 설명하면서 이 글 마지막의 포인터의 정리와 같이 요약할 것이다.

2. C의 포인터는 선언만 이해하면 혼동하지 않는다.
만약 어떤 소설에서 저자가 "abc"는 주인공이고 "def"은 나쁜 놈이라고 알려주지 않았고 원래 목적은 "abc"가 정의의 용사고, "def"는 마왕이기 때문에 주인공이 마왕을 해치우려 간다는 간단한 양판소스러운 이야기인데 독자 수준에서는 얼핏 보면 "def"가 나쁜 짓을 많이 했으니까 또 다른 나쁜 놈인 "abc"에게 심판을 받는다. 라고 이해할지도 모른다. "def"가 받는 심판의 결과는 오로지 죽음이기 때문에 "abc"는 결국 "def"를 죽이게 되고 독자는 "용사도 아닌 놈이 마왕을 죽였는데 좀 이상하지 않아?" 라고 생각하겠지. 심지어 저자도 헷갈려서 "abc"가 원래는 용사인데 마왕으로 둔갑할수도 있고 그렇게 되면 소설은 원래 목적을 잃고 산으로 가겠지.

이런 일을 방지하려면 이름에 대해서 설명을 달아주면 끝이지 않을까? "abc"는 정의의 용사다. 라고 정의했으면 미치지 않고서야 중간에 역할이 바뀌지는 않을테니 혼동하지 않을 것이고 만약 잘못 작성해서 "abc"가 갑자기 마왕이 될려고 하는 잘못된 상황을 미리 알 수 있지.

프로그래밍에서도 다르지는 않다. 이름에 대해 설명을 달아주면 모든게 해결이 된다. p를 정수형 포인터로 정의했으면 중간에 정의가 바뀔일은 없으니 굳이 p가 등장할때마다 "이 놈은 내가 정수형 포인터로 정의했어!" 라고 귀찮게 알려줄 필요가 없어진다.

그래서 결론을 내리자면 선언은 이름에 대한 설명을 달아주는 역할을 한다. 그리고 왜 C의 포인터 선언과 사용이 다른지 설명할 수 있게 된다. int * p;는 p가 정수형을 가리키는 포인터라는 설명을 했고 나중에 *를 달지 않아도 p가 포인터라는 사실을 기억하고 있기에 따로 부가 설명을 달아줄 필요가 없다. 참고로 *는 역참조 연산으로 변수가 가지고 있는 주소값에 해당하는 타입을 얻어낸다.

3. 배열의 정의.
배열은 어떤 타입이 중간에 빈공간을 포함하지 않고 가상 메모리 공간에 연속적으로 배치된 형태를 가진다. 어떤 타입이라는 것은 데이터를 저장할 수 있는 모든 데이터형을 말한다. void형과 함수는 데이터를 저장할 수 없으므로 예외이지만 함수의 포인터는 저장할 수 있다. 이외에도 배열은 데이터를 저장하기 때문에 배열을 요소로 가지는 배열이 전혀 이상하지 않다. 이제부터 포인터와의 차이점을 알아보자. 포인터와 다른 몇가지 예외를 가진다.
(1). sizeof의 피연산자로 쓰였을때는 배열의 전체 크기를 바이트 단위로 반환한다.
(2). 포인터는 단항 연산자인 &를 적용했을때 어떤 타입을 가리키는 포인터에 대한 포인터가 되지만 배열은 어떤 배열을 가리키는 포인터가 된다.
(3). 문자형 배열에 문자열로 초기화를 하면 문자열이 복사된다.

3가지 정도의 예외를 제외하고는 배열 이름은 포인터로 변환된다. 즉 포인터의 이름은 특수하며 선언한 함수내에서만 유효하다. 그래서 다른 함수에서 전달받는 배열은 사실 포인터이고 전체 크기를 알 수 없다.

4. 그래서 포인터는 무엇인가?
흔히 교수들은 포인터에 대해 설명을 할때 주소값을 이용해서 개념을 가르친다. 틀리지는 않았지만 C가 추구하는 추상화를 생각하면 옳진 않다. 어떤 사람들은 C가 포터블 어셈블리라고, 기계적이라고 표현하곤 하는데 기계어가 아닌 이상은 모든 언어는 추상적이다. C도 결국 기계어가 아니므로 인간의 말을 그대로 전달하지 않는다.

가장 올바른 포인터의 정의는 어떤 타입을 가리킨다는 것이다. 즉 모든 포인터는 최소한 아무 의미도 없는 타입은 가리킨다. 그렇다면 void를 가리키는 포인터는 타입을 가리키는 포인터는 아니지 않냐고 반문할수도 있는데 void도 염연히 타입이다. 함수의 매개변수와 반환형으로 void를 사용할 수 있다. 다만 void는 데이터를 저장하는 타입은 아니기 때문에 void를 가리키는 포인터는 진짜로 void 형을 역참조할 수는 없고 어떤 데이터형이든 가리키는 범용적인 포인터의 의미로 사용된다. 이 포인터를 실체화하면 포인터 변수에 주소값이 담기는 것일 뿐이지 포인터 자체가 주소값을 담는다고 이해하는 것은 틀리지는 않았지만 목적성으로는 옳지 않다.

다중 포인터라는 것도 있는데 간단히 말해서 포인터를 가리키는 포인터이다. 그런데 포인터는 타입을 가리킨다고 했으므로 어떤 타입의 포인터라는 타입을 가리키는 포인터이다. 다중 포인터는 일단 하나로 취급되지만 각각의 타입들은 분해가 가능하기에 전부 어떤 포인터를 가리키고 최종적으로 마지막에 가리키는 타입의 포인터가 된다.

5. 다차원 배열과 배열 포인터.
2차원 또는 3차원 배열이라는 단어를 많이 들어봤을거라고 생각한다. 그런 사람들이 이 글을 찾아서 왔을 것이고. 이 개념을 설명하기 전에 한가지 요약을 하면 C에는 개념상으로 포인터만 존재한다. 배열도 문법의 일부지만 컴파일 과정 전에 포인터로 변환하는 과정이 있어도 문제는 없다는 뜻이다.

C에서는 사실 다차원 배열이라는 개념이 없다. 배열은 배열을 타입으로 가질 수 있기 때문이다. 그래서 다차원 배열은 개념상으로 배열을 담는 배열이다. 이 논리를 적용하면 다차원 배열에서 첨자가 일부만 사용되었을때 왜 포인터를 반환하는지 알 수 있다. 그것은 포인터가 아니라 원래 배열이 맞고 배열은 몇가지 예외를 제외하면 포인터로 변환되기 때문에 실제로는 포인터로 변환된 배열이다.

그러나 배열은 중간에 빈 공간없이 배치되어야 하므로 다중 포인터로는 접근할 수 없다. 다중 포인터라면 모든 요소가 일차원적으로 배치될 필요가 없기 때문에 난점이 있고 그래서 일반적으로 다차원 배열은 배열의 포인터라는 개념으로 접근한다. 이름 그대로 배열이라는 타입을 가리키는 포인터로 배열의 연속성이라는 특성을 고려한 특수한 포인터이다.

6. 그래서 포인터의 문법은 왜 이렇게 괴상한건가?
C가 하루 아침에 탄생한게 아니다보니 여러 언어들로부터 특징이 이어져 내려왔고 그게 C로 발전한 것이기에 중구난방식으로 확장된 결과물이라고 볼 수도 있겠지만 포인터에 대한 문법 자체는 선언만 이해하면 크게 난해하진 않다. int * p;로 포인터를 선언했기 때문에 나중에 다시 포인터라는 사실을 알려줄 필요가 없기 때문이고 *p = malloc(sizeof(int) * 100); 같은 코드는 필요없다고 이해할 수 있어야 한다.

복잡한 선언문을 읽는 방법은 글이 너무 길어지면 올려지지 않기 때문에 나중에 분할해서 설명하겠음.