Journey to Security/C언어

C 포인터 배열과 이중 포인터의 메모리 구조

Cordilog 2026. 2. 23. 18:28

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 배열의 첫 번째 요소의 주소를 복사받는다. 즉, pch2[0]을 가리키는 이중 포인터가 된다.

 

2. print_pointer2() 함수의 참조 과정

함수 내부에서 사용되는 p[i]*(p+i)는 문법적으로 완벽히 동일한 동작을 수행한다.

  1. p: ch2 배열의 시작 주소(&ch2[0])를 담고 있다.
  2. p + i: p가 가리키는 지점에서 i만큼 떨어진 위치의 주소를 계산한다. 이때 단위는 sizeof(char *)이다.
  3. *(p + i) (또는 p[i]): 해당 주소에 저장된 값, 즉 문자열 리터럴의 주소를 가져온다.
  4. 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 연산 시, pchar *를 가리키는 포인터이므로 주소값은 i * 8byte(64bit 기준, 32bit에서는 4byte)씩 증가한다. 이는 배열의 각 인덱스를 정확히 순회할 수 있게 한다.
  • 이중 참조의 구조: p(이중 포인터) -> ch2[i](싱글 포인터) -> 문자열(데이터). 즉, 실제 데이터에 접근하기 위해 두 번의 메모리 점프가 일어난다.