C 언어에서 포인터 배열과 이중 포인터의 관계는 메모리 구조를 이해하는 데 있어 가장 중요한 부분이다.
다음 예시 코드를 바탕으로 메모리의 각 영역(Stack, Read-Only Data)에서 데이터가 어떻게 배치되고 참조되는지 분석해보자.
#include <stdio.h>
void print_pointer2(char **p); // *p[]
// int main(int argc, char *argv[])
int main()
{
char *ch2[] = { "Linux", "Windows", "Unix", "Mac", NULL };
print_pointer2(ch2);
return 0;
}
void print_pointer2(char **p)
{
for(int i=0; p[i] != NULL; i++)
{
printf("%s\n", p[i]);
printf("%s\n", *(p+i));
}
}
1. 메모리 영역별 할당 구조
이 코드의 메모리 구조는 크게 두 영역으로 나뉜다.
문자열 리터럴이 저장되는 정적 데이터 영역(RODATA)과 변수 및 포인터가 생성되는 스택(Stack) 영역이다.
A. 상수 영역 (Read-Only Data Segment)
"Linux", "Windows", "Unix", "Mac" 등 소스 코드에 직접 기입된 문자열 리터럴은 프로그램 실행 시 메모리의 읽기 전용 데이터 영역에 저장된다.
- 각 문자열은 메모리 어딘가에 연속적으로 배치되며, 마지막에는 NULL 문자(\0)이 자동으로 붙는다.
- 예를 들어, "Linux"가 0x100 주소에 있다면, ch2[0]은 이 0x100이라는 주소값을 저장하게 된다.
B. 스택 영역 (Stack Segment)
- ch2 배열: main 함수 내에 선언된 char *ch2[]는 포인터 변수 5개를 담을 수 있는 배열이다. 각 요소는 8바이트(64비트 시스템 기준)의 크기를 가지며, 상수 영역에 있는 문자열들의 시작 주소를 저장한다. 마지막 요소는 NULL로 초기화되어 배열의 끝을 알린다.
- p 매개변수: print_pointer2 함수가 호출될 때 생성되는 char **p는 ch2 배열의 첫 번째 요소의 주소를 복사받는다. 즉, p는 ch2[0]을 가리키는 이중 포인터가 된다.
2. print_pointer2() 함수의 참조 과정
함수 내부에서 사용되는 p[i]와 *(p+i)는 문법적으로 완벽히 동일한 동작을 수행한다.
- p: ch2 배열의 시작 주소(&ch2[0])를 담고 있다.
- p + i: p가 가리키는 지점에서 i만큼 떨어진 위치의 주소를 계산한다. 이때 단위는 sizeof(char *)이다.
- *(p + i) (또는 p[i]): 해당 주소에 저장된 값, 즉 문자열 리터럴의 주소를 가져온다.
- printf("%s", ...): 가져온 주소로 찾아가서 NULL 문자(\0)를 만날 때까지 문자를 출력한다.
printf("%s", ...)의 작동 원리
printf 함수에서 %s 서식 지정자는 인자로 문자열의 시작 주소(char *)를 받도록 설계되어 있다.
- printf는 전달받은 주소로 직접 찾아간다.
- 해당 주소부터 1바이트씩 읽으며 문자를 출력한다.
- 메모리 상에서 \0 (NULL 문자, 0)을 만날 때까지 이 과정을 반복한다.
따라서 printf("%s", p[i]); 문장에서 p[i]가 "Linux"라는 글자 자체를 넘겨주는 것이 아니라, "L이 저장된 위치의 주소값(예: 0x100)"을 넘겨주면 printf가 그 주소를 참조하여 글자를 읽어내는 방식이다.
3. 메모리 참조 관계 도식화
아래 도표는 메모리 주소값과 참조 흐름을 시각화한 것이다. (주소값은 이해를 돕기 위한 예시)

| 변수/표현식 | 메모리 주소 | 저장된 값 (데이터) | 데이터의 의미 |
| p | 0x700 | 0x500 | ch2 배열의 시작 주소 |
| p[0] | 0x500 | 0x100 | "Linux"의 시작 주소 (char *) |
| *(p+1) | 0x508 | 0x110 | "Windows"의 시작 주소 (char *) |
| **p | 0x100 | 'L' | 실제 문자 데이터 (char) |
4. 코드의 핵심 포인트
- 배열 이름의 퇴화(Array Decay): print_pointer2(ch2) 호출 시, 배열 ch2는 그 자체로 전달되는 것이 아니라 첫 번째 요소의 주소인 &ch2[0]으로 변환되어 전달된다. 이 때문에 매개변수 타입이 char **가 된다.
- 포인터 연산의 단위: p + i 연산 시, p가 char *를 가리키는 포인터이므로 주소값은 i * 8byte(64bit 기준, 32bit에서는 4byte)씩 증가한다. 이는 배열의 각 인덱스를 정확히 순회할 수 있게 한다.
- 이중 참조의 구조: p(이중 포인터) -> ch2[i](싱글 포인터) -> 문자열(데이터). 즉, 실제 데이터에 접근하기 위해 두 번의 메모리 점프가 일어난다.
'Journey to Security > C언어' 카테고리의 다른 글
| C언어 조건부 컴파일: #ifdef (0) | 2026.02.24 |
|---|---|
| GDB TUI 모드: 소스 코드를 보며 디버깅하기 (0) | 2026.02.23 |
| C언어 배열명과 포인터의 차이: GDB로 문자열 저장 방식 뜯어보기 (0) | 2026.02.22 |
| C언어 배열 메모리 레이아웃 (feat. GDB) (0) | 2026.02.20 |
| 비트 연산(Bitwise Operation) + 2의 보수 개념 (0) | 2026.02.15 |