Journey to Security/C언어

C언어 3중 포인터와 문자열 배열의 메모리 구조

Cordilog 2026. 2. 24. 19:00

포인터의 차수가 높아질수록 참조 관계를 머릿속으로 시각화하기 어려워진다.

아래 예시 코드를 통해 3중 포인터(char ***pp)가 메모리상에서 어떻게 실제 문자열 데이터에 접근하는지, 참조 관계와 구조를 살펴보자.

 

코드 

void print_pointer3(char ***p);
int main()
{
   
    char *ch2[] = { "Linux", "Windows", "Unix", "Mac", NULL };
    char **p;
    p = &ch2[0];     // *p = ch2[0] 

    print_pointer3(&p);

    return 0;
}

void print_pointer3(char ***pp)  // **p[]
{
    for(int i=0; (*pp)[i] != NULL; i++)
    {
        printf("%s\n", (*pp)[i]);
        printf("%s\n", *(*pp+i)); // *(p+0)
    }
}

 

1. 데이터의 배치와 메모리 영역

위 코드에서 사용된 요소들은 각각 다음과 같은 메모리 영역에 위치한다.

  • 문자열 리터럴 ("Linux", "Windows" 등): 이들은 Read-Only Data(ROData) 영역에 저장된다. 프로그램 실행 내내 유지되며 수정이 불가능한 영역이다.
  • 포인터 배열 ch2[]: main 함수의 지역 변수로 Stack 영역에 할당된다. 각 요소는 ROData에 있는 문자열들의 시작 주소를 담고 있다.
  • 포인터 변수 p: 역시 Stack 영역에 할당된 8바이트(64비트 시스템 기준) 변수다.
  • 함수 인자 pp: print_pointer3가 호출될 때 생성되는 Stack 변수로, p의 주소값을 복사받는다.

Stack (스택 영역)

  • 특징: 후입선출(LIFO) 방식으로 관리되며, 함수 호출 시 생성되고 반환 시 소멸한다.
  • 배치: main 함수의 프레임이 먼저 생성되어 ch2 배열과 포인터 p가 자리를 잡는다. 이후 print_pointer3가 호출되면서 그 위에 pp 매개변수가 쌓인다.
  • 주소 방향: 메모리의 높은 주소에서 낮은 주소 방향으로 할당된다.

Data / ROData (데이터 영역)

  • 특징: 프로그램 시작 시 할당되며 종료 시까지 유지된다.
  • 배치: "Linux", "Windows" 등의 실제 문자열 데이터가 여기에 저장된다. ch2[i] 포인터들은 스택에 존재하면서 이 ROData 영역의 주소값을 가리키고 있는 형태다.

Text (텍스트 영역)

  • 특징: CPU가 실행할 실제 코드(명령어)가 들어있다.
  • 배치: 우리가 작성한 printf 호출 로직이나 포인터 연산 로직 자체가 바이너리 형태로 존재한다.

 

2. 코드 구조 및 포인터 변수

위 코드의 핵심은 main 함수에서 정의된 포인터 배열 ch2와 이를 가리키는 포인터 p, 그리고 이를 다시 참조하는 3중 포인터 pp의 관계에 있다.

  • char *ch2[] : 문자열 상수들의 주소를 저장하는 포인터 배열이다. 각 요소는 char * 타입이며, 마지막은 루프의 종료 조건으로 사용될 NULL로 채워져 있다.
  • char **p : ch2 배열의 첫 번째 요소를 가리키는 더블 포인터다. p = &ch2[0]를 통해 p는 ch2[0]의 주소값을 가진다.
  • char ***pp : print_pointer3 함수의 매개변수로, p의 주소(&p)를 받는다. 즉, 3중 포인터가 된다.

 

2. 메모리 구조 분석

이 코드의 메모리 참조 단계는 총 4단계의 계층 구조를 형성한다.

  1. Level 1 (pp) : print_pointer3 함수의 지역 변수로 주소를 저장하는 3중 포인터 변수
    : main 스택에 있는 더블 포인터 p의 메모리 주소를 들고 있다.
  2. Level 2 (*pp = p) : 1차 역참조
    : p가 저장하고 있는 값, 즉 포인터 배열 ch2의 첫 번째 요소(ch2[0]) 주소를 들고 있다.
  3. Level 3 (pp = *p = ch2[i]) : 2차 역참조
    *pp가 가리키는 곳으로 한 번 더 이동해서 만난 대상, 즉 ch2[i]다.
    값: ch2 배열의 각 요소에 들어있는 문자열 상수의 시작 주소를 들고 있다.
  4. Level 4 (***pp = p = ch2[i][0]) : 3차 역참조
    **pp에 적힌 주소로 마지막으로 이동해서 만난 실제 데이터다.
    값: 실제 메모리에 저장된 문자 데이터('L', 'i', 'n', 'u', 'x' 등)이다.

3. 함수 내부의 역참조 메커니즘

print_pointer3 내부에서 사용하는 두 가지 표현식을 살펴보자.

  1. (*pp)[i]
    • *pp를 먼저 수행하여 p가 저장하고 있는 값, 즉 &ch2[0]을 얻는다.
    • 거기에 인덱스 i를 적용한다. 이는 p를 배열처럼 사용하여 p[i]를 참조하는 것과 같다.
    • 결과적으로 ch2[i]에 담긴 문자열의 주소를 반환한다.
  2. *(*pp + i)
    • *pp는 p의 값인 &ch2[0]이다.
    • + i는 포인터 연산에 의해 i * sizeof(char*) 만큼 주소를 이동시킨다.
    • 최종적으로 *(p + i)와 같으며, 이는 p[i]와 동일한 의미를 갖는다.

 

4. 메모리 배치