
C언어 이중 포인터는 포인터 기초를 막 넘긴 사람에게 특히 어렵게 느껴지는 주제입니다. 값도 헷갈리고 주소도 헷갈리는데, 그 주소를 또 가리키는 포인터까지 등장하기 때문입니다. 하지만 필요한 장면을 정확히 보면 생각보다 훨씬 덜 낯섭니다.
이번 글에서는 이중 포인터를 단순 문법이 아니라, 포인터 자체를 함수 안에서 바꿔야 할 때 필요한 구조로 설명하겠습니다.

포인터의 포인터라는 말은 무엇을 뜻할까
포인터가 어떤 값의 주소를 저장한다면, 이중 포인터는 그 포인터 변수의 주소를 저장합니다. 즉 실제 값까지 가기 전에 주소를 한 번 더 따라가는 구조입니다.
int value = 10;
int *p = &value;
int **pp = &p;여기서 p는 value를 가리키고, pp는 p를 가리킵니다. 그래서 *pp는 p이고, **pp는 value입니다. 층을 하나씩 따라간다고 생각하면 덜 헷갈립니다.
C 포인터 설명에서도 포인터 선언 안에 또 다른 포인터 declarator가 들어갈 수 있다고 설명합니다. 하지만 실전에서는 문법보다 “무엇을 바꾸려는가”를 먼저 보면 훨씬 쉽습니다.
포인터 하나로 충분한 경우와 부족한 경우
포인터 하나로 충분한 경우는, 포인터가 가리키는 대상의 값만 바꾸면 될 때입니다. 예를 들어 int *p가 가리키는 정수 값을 바꾸는 일은 *p = 100처럼 할 수 있습니다.
하지만 함수 안에서 p 자체를 다른 주소로 바꾸고 싶다면 이야기가 달라집니다. 함수는 인자를 복사해서 받기 때문에, 단순히 int *p를 넘기면 함수 안에서 바꾼 p는 바깥 p에 반영되지 않습니다.
왜 함수 안에서 포인터 자체를 바꾸려면 이중 포인터가 필요할까
void allocate(int **pp) {
*pp = malloc(sizeof(int) * 3);
}
int *arr = NULL;
allocate(&arr);이 예시에서 함수가 바꾸는 것은 배열 원소 값이 아니라 arr 자체입니다. arr가 새로운 메모리 주소를 가리키도록 바꾸는 것이 목적이므로, arr의 주소를 함수에 넘겨야 합니다. 그래서 int **pp가 필요합니다.
즉 이중 포인터는 “포인터가 가리키는 대상”이 아니라 “포인터 변수 그 자체”를 수정하기 위한 통로라고 보면 됩니다.

호출 전후 상태를 글로 따라가 보기
호출 전에는 arr가 NULL입니다. 즉 아직 어떤 배열도 가리키지 않습니다. allocate(&arr)를 호출하면 함수 안의 pp는 arr의 주소를 받습니다. 그리고 *pp = malloc(…)을 수행하면, 결국 arr 안에 새 주소가 들어갑니다.
이 흐름을 말로 풀면 이렇게 됩니다. “값을 바꾼다”가 아니라 “포인터가 담고 있는 주소를 바꾼다.” 바로 이 차이가 이중 포인터의 존재 이유입니다.
문자열 배열이나 argv에서 이중 포인터가 왜 나오나
char **argv 같은 형태도 결국 비슷한 이유로 이해할 수 있습니다. 각 원소가 char *이고, 그 char *들을 여러 개 모아 놓은 배열 또는 목록의 시작점을 다루기 때문입니다.
즉 char **는 “문자 하나를 가리키는 포인터”가 아니라 “문자열을 가리키는 포인터들의 집합을 따라가는 구조”로 보면 됩니다. 이 부분에서 많은 입문자가 int **와 char **를 전혀 다른 괴물처럼 느끼지만, 층 구조는 같습니다.
자주 하는 오해
- 별표가 두 개면 무조건 더 복잡한 값을 저장한다고 생각한다
- 이중 포인터는 특별한 라이브러리나 고급 문법에서만 쓴다고 생각한다
- 값 변경과 포인터 변경을 구분하지 못한다
- malloc 예시를 외우지만 왜 &arr를 넘기는지는 설명하지 못한다
이중 포인터는 고급 트릭이 아니라, 바꿔야 하는 대상이 포인터인지 값인지에 따라 자연스럽게 등장하는 문법입니다.
포인터 기초는 포인터 입문 글, 메모리 수명과 해제 후 문제는 malloc/free 글과 함께 보면 훨씬 잘 이어집니다.
마무리
C언어 이중 포인터를 이해하는 핵심은 포인터가 가리키는 값과 포인터 변수 자체를 구분하는 데 있습니다.
즉 “함수 안에서 바꾸려는 대상이 값인가, 포인터 자체인가”를 먼저 보면 이중 포인터가 필요한 이유가 훨씬 선명해집니다.