C언어 - 포인터 이해하기

1. C언어 포인터

  • C언어의 포인터(pointer)는 메모리 주소를 저장하는 변수이다.
    • 데이터가 있는 "위치"(주소)를 저장한다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    const char *pstr = "Hello World!";

    char *ptr = NULL;
    const size_t len = strlen(pstr);

    ptr = malloc(len + 1);
    memset(ptr, 0x00, len + 1);

    memcpy(ptr, pstr, len);
    printf("%p[%p] → %s\n", &ptr, ptr, ptr);

    free(ptr);

    return 0;
}
  • ptr 포인터 변수는 Stack 영역에 존재한다.
  • malloc을 사용하여 Heap 영역에 메모리 공간을 할당하고, 그 주소를 Stack 영역ptr 변수에 저장한다.
  • memcpy를 통해 "Hello World!" 문자열 데이터를 Heap 영역의 할당된 공간에 복사한다.
0x7ffffc12fd50[0x5a14a05396b0] → Hello World!
  • &ptr 은 포인터 변수 자체의 주소이고, ptr 변수에 저장된 주소에 문자열 데이터가 저장된다.

포인터는 데이터가 저장된 메모리 주소를 저장한다.

포인터는 데이터를 직접 저장하지 않고, 데이터가 저장된 메모리 주소를 저장한다.

1.1. Stack vs. Heap

Stack vs. Heap

1.1.1. Stack

  • 함수 호출 시 생성되는 지역 변수, 매개변수, 반환 주소 등을 저장하는 메모리 영역이다.
  • LIFO(Last-In First-Out) 구조로 동작한다.
    • 나중에 들어온 데이터가 먼저 나간다
  • 함수가 시작되면 메모리 생성, 종료되면 자동 해제된다.
  • 컴파일 시 크기가 결정되고 연속된 메모리 사용한다.
    • 과도하게 사용하면 Stack Overflow 가 발생할 수 있다.
    • Windows는 기본 1MB, Linux는 기본 8MB의 스택을 각 스레드마다 할당한다.

1.1.2. Heap

  • 프로그램 실행 중 동적으로 할당하는 메모리 영역이다.
  • 개발자가 직접 할당하고 해제해야 한다.
    • malloc(), calloc(), realloc(), free() 함수 사용
  • 메모리에 할당하기 위해 시스템 호출이 발생한다.
  • 실행 중인 런타임에 크기를 결정할 수 있다.

1.2. 실행과정

1.2.1. malloc

  • 내부 메모리 관리 정보(Header)를 생성하고 메모리를 할당한다.
ptr = malloc(len + 1);  // len=12, malloc(13)

Allocates: 16 (header) + 13 (data) = 29 bytes

[Heap after malloc]
0x5a14a05396a0 (allocation start)
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ HEADER: size=13, flags=P(1:allocated)         │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
0x5a14a05396b0 (malloc return value)
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│??│??│??│??│??│??│??│??│??│??│??│??│??│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

1.2.2. memset

  • 할당된 메모리를 초기화한다.
memset(ptr, 0x00, len + 1);

Initialize a total of 13 bytes with zero

[Heap after memset]
0x5a14a05396b0
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│00│00│00│00│00│00│00│00│00│00│00│00│00│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

1.2.3. memcpy

  • 주어진 길이만큼 데이터를 복사한다.
memcpy(ptr, pstr, len);

Copy 12 bytes from pstr

[Heap after memcpy]
0x5a14a05396b0
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│H │e │l │l │o │  │W │o │r │l │d │! │00│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

1.2.4. free

  • 내부 메모리 관리 정보(Header)의 상태를 변경하여 메모리를 재사용 가능하게 만든다.
 free(ptr);

Free the allocated memory

[Heap after free]
0x5a14a05396a0
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ HEADER: size=13, flags=P(0:deallocated)       │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
0x5a14a05396b0
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│H │e │l │l │o │  │W │o │r │l │d │! │00│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

1.3. 동적 메모리 할당

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void exam1(const char *pstr)
{
    const size_t len = strlen(pstr);

    char *p = malloc(len + 1);

    memset(p, 0x00, len + 1);
    strncpy(p, pstr, len);

    printf("%s\n", p);

    free(p);
}

void exam2(const char *pstr)
{
    const size_t len = strlen(pstr);

    char *p = calloc(len + 1, sizeof(char));

    strncpy(p, pstr, len);

    printf("%s\n", p);

    free(p);
}

void exam3(const char *pstr)
{
    const size_t len  = strlen(pstr);
    const size_t unit = 4;

    size_t block = unit;

    char *p = malloc(block);

    size_t n = 0;
    for (size_t i = 0; i < len; i++) {
        if (n + 1 >= block) {
            block += unit;
            p = realloc(p, block);
        }
        p[n++] = pstr[i];
    }
    p[n] = '\0';

    printf("%s\n", p);

    free(p);
}

int main()
{
    char *pstr = "01234567890123456789";

    exam1(pstr);
    exam2(pstr);
    exam3(pstr);

    return 0;
}
  • main의 입력 문자열: "01234567890123456789" (길이 20, NULL 포함 필요 버퍼 21)
  • exam1: malloc + memset + strncpy
  • exam2: calloc + strncpy
  • exam3: malloc 후 문자 추가 중 realloc 반복 확장

1.3.1. malloc

  • 요청한 바이트 수만큼 메모리를 할당한다.
  • 초기화되지 않은 메모리(garbage value)를 포함할 수 있다.
[Heap after malloc]
0x5a14a05396a0 (allocation start)
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ HEADER: size=21, flags=P(1:allocated)         │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
0x5a14a05396b0 (malloc return value)
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│??│??│??│??│??│??│??│??│??│??│??│??│??│??│??│??│??│??│??│??│??│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

1.3.2. calloc

  • 요청한 바이트 수만큼 메모리를 할당하고 전체를 0으로 초기화한다.
[Heap after calloc]
0x5a14a05397a0 (allocation start)
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ HEADER: size=21, flags=P(1:allocated)         │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
0x5a14a05397b0 (calloc return value)
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│00│00│00│00│00│00│00│00│00│00│00│00│00│00│00│00│00│00│00│00│00│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

1.3.3. realloc

  • 이미 할당된 메모리의 크기를 변경한다.
    • 최종 크기를 미리 모르는 동적 버퍼 확장에 적합하다.
  • 같은 주소를 유지할 수도 있고 새 주소로 이동할 수도 있다.
    • realloc으로 할당하는 메모리 공간은 연속되는 공간이어야 한다.
    • 기존 공간 뒤로 확장될 수 있지만, 공간이 부족한 경우에는 새로운 위치로 이동될 수 있다.
[Step A: initial malloc(4)]
0x5a14a05398a0 (allocation start)
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ HEADER: size=4, flags=P(1:allocated)          │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
0x5a14a05398b0 (malloc return value)
┌──┬──┬──┬──┐
│??│??│??│??│
└──┴──┴──┴──┘

[Step B: realloc to 8]
0x5a14a05399a0 (new allocation start)
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ HEADER: size=8, flags=P(1:allocated)          │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
0x5a14a05399b0 (realloc return value)
┌──┬──┬──┬──┬──┬──┬──┬──┐
│..│..│..│..│??│??│??│??│
└──┴──┴──┴──┴──┴──┴──┴──┘

...

[Step F: realloc to 24 (final capacity)]
0x5a14a0539da0 (new allocation start)
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ HEADER: size=24, flags=P(1:allocated)         │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
0x5a14a0539db0 (realloc return value)
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│..│..│..│..│..│..│..│..│..│..│..│..│..│..│..│..│..│..│..│..│??│??│??│??│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

1.4. strcpy vs. memcpy

  • strcpy는 NULL 문자('\0')가 나올 때까지 문자열을 복사하는 함수이다.
  • memcpy는 지정한 크기만큼 메모리를 그대로 복사하는 함수이다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    char str[] = {'H', 'e', 'l', 'l', 'o', '\0', 'W', 'o', 'r', 'l', 'd', '!', '\0'};
    printf("str=%s\n", str);

    char *ptr1 = NULL;
    ptr1 = malloc(sizeof(str));
    memset(ptr1, 0x00, sizeof(str));

    strcpy(ptr1, str);
    printf("ptr1=%s\n", ptr1+6);

    free(ptr1);

    char *ptr2 = NULL;
    ptr2 = malloc(sizeof(str));
    memset(ptr2, 0x00, sizeof(str));

    memcpy(ptr2, str, sizeof(str));
    printf("ptr2=%s\n", ptr2+6);

    free(ptr2);

    return 0;
}
str=Hello
ptr1=
ptr2=World!
  • printf("str=%s\n", str);
    • %s는 첫 번째 '\0'에서 출력을 멈추게 되어 "Hello"만 출력된다.
  • strcpy(ptr1, str);
    • strcpy는 첫 번째 '\0'까지 문자열을 복사하므로 "Hello\0"까지만 ptr1에 저장된다.
    • 따라서 ptr1 + 6은 '\0' 바로 다음을 가리키며, 빈 문자열을 출력한다.
  • memcpy(ptr2, str, sizeof(str));
    • memcpy는 지정한 크기만큼 메모리를 그대로 복사하므로 "Hello\0World!\0" 전체가 복사된다.
    • 따라서 ptr2 + 6 은 "World!"를 출력한다.

※ 설명에 사용한 코드 예제에서는 단순화를 위해 malloc, calloc, realloc의 반환값에 대한 NULL 검사를 생략하였다.
그러나 실제 개발 환경에서는 메모리 할당 실패 가능성을 항상 고려해야 하므로, 반환값이 NULL인지 반드시 확인하는 방어 코드가 필요하다.