포인터 (Pointer)

한눈에 보기

다른 변수의 메모리 주소를 저장하는 포인터의 개념과 역참조, 포인터 산술, 배열과의 관계, 이중 포인터와 함수 포인터, 그리고 널 포인터와 댕글링 포인터 같은 오류 상황까지 정리한다.

최종 업데이트 2026.09.17

포인터(Pointer)는 다른 변수의 메모리 주소를 값으로 저장하는 변수다. C 언어에서 포인터는 함수 간에 데이터를 참조로 전달하고, 동적 할당된 메모리를 다루며, 배열과 문자열을 효율적으로 순회하는 핵심 수단이다. 주소 연산자와 역참조 연산자의 의미, 포인터 산술의 규칙, 그리고 잘못된 포인터 사용이 일으키는 오류를 함께 이해해야 한다.

포인터의 기본 개념

모든 변수는 메모리 어딘가에 저장되며 그 위치는 주소로 식별된다. 주소 연산자(Address-of Operator) &는 변수의 주소를 얻고, 역참조 연산자(Dereference Operator) *는 포인터가 가리키는 주소에 저장된 값에 접근한다. 포인터 자체도 변수이므로 메모리 공간을 차지하는데, 그 크기는 가리키는 타입과 무관하게 주소를 담을 수 있는 크기로 고정된다. 64비트 환경에서는 보통 8바이트다.

#include <stdio.h>

int main(void) {
    int num = 42;
    int *p = &num;      /* p는 num의 주소를 저장한다 */

    printf("%d\n", *p); /* 42, 역참조로 값에 접근 */
    *p = 100;           /* num이 100으로 바뀐다 */
    printf("%d\n", num);
    return 0;
}

포인터의 타입은 가리키는 대상의 해석 방법을 결정한다. int 포인터는 해당 주소부터 sizeof(int) 바이트를 정수로 해석하겠다는 약속이며, 포인터 산술의 단위도 이 타입 크기로 정해진다.

포인터와 함수, 배열

C의 함수 호출은 값에 의한 전달(Call by Value)이므로 인자를 복사해서 넘긴다. 함수 안에서 호출자의 변수를 바꾸려면 주소를 넘겨 포인터로 접근해야 하며, 이것이 주소에 의한 전달(Call by Address)이다. scanf가 변수 앞에 &를 요구하는 이유도 입력값을 써 넣을 주소가 필요하기 때문이다.

void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int arr[3] = {10, 20, 30};
int *p = arr;               /* 배열 이름은 첫 원소의 주소로 변환된다 */
printf("%d\n", *(p + 2));   /* 30, arr[2]와 같다 */

배열 이름은 대부분의 문맥에서 첫 원소를 가리키는 포인터로 변환(Decay)되며, arr[i]는 *(arr + i)와 동일하게 정의된다. 포인터에 정수를 더하면 바이트 단위가 아니라 타입 크기 단위로 이동한다는 점이 핵심이다. int 포인터에 1을 더하면 주소는 4가 증가한다. 다만 배열과 포인터가 완전히 같은 것은 아니다. sizeof(arr)는 배열 전체 크기를 주지만 sizeof(p)는 포인터 크기만 주고, 함수 매개변수로 선언한 배열은 실제로는 포인터로 취급된다.

이중 포인터, void 포인터, 함수 포인터

이중 포인터(Double Pointer)는 포인터의 주소를 저장하는 포인터로, 함수 안에서 호출자의 포인터 자체를 바꿔야 할 때 쓴다. 예를 들어 함수 내부에서 malloc으로 할당한 주소를 호출자의 포인터에 넣어 주려면 int **를 받아야 한다. void 포인터(void *)는 타입 정보가 없는 순수한 주소로, malloc의 반환 타입처럼 임의 타입을 담는 범용 통로 역할을 하며 역참조하려면 구체적 타입으로 캐스팅해야 한다. 함수 포인터(Function Pointer)는 함수의 시작 주소를 저장해 qsort의 비교 함수처럼 동작을 인자로 전달하는 콜백(Callback) 구현에 쓰인다.

void alloc_int(int **out, int n) {
    *out = (int *)malloc(sizeof(int) * n);
}

int cmp(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}
/* qsort(arr, n, sizeof(int), cmp); 처럼 함수 포인터로 전달한다 */

주의해야 할 포인터 오류

초기화하지 않은 포인터는 쓰레기 주소를 담고 있어 역참조하면 정의되지 않은 동작(Undefined Behavior)이 발생한다. 널 포인터(NULL Pointer)는 아무것도 가리키지 않음을 명시하는 값으로, 역참조하면 대부분의 환경에서 세그멘테이션 폴트(Segmentation Fault)로 즉시 죽기 때문에 오히려 쓰레기 주소보다 안전한 실패를 만든다. 해제된 메모리나 반환된 함수의 지역 변수를 가리키는 댕글링 포인터(Dangling Pointer)는 접근 시점의 메모리 상태에 따라 증상이 달라져 디버깅이 어렵다. 함수가 지역 변수의 주소를 반환하면 안 되는 이유가 바로 스택 프레임이 사라지면서 댕글링 포인터가 되기 때문이다.

상황포인터 상태역참조 결과
초기화 안 함쓰레기 주소정의되지 않은 동작
NULL 대입널 포인터대부분 즉시 크래시로 발견이 쉬움
free 이후댕글링 포인터오염된 값 읽기, 간헐적 오류
유효한 객체정상의도한 값 접근

C++에서는 이런 위험을 줄이기 위해 소유권을 표현하는 std::unique_ptr, std::shared_ptr 같은 스마트 포인터(Smart Pointer)와 참조(Reference)를 우선 사용하고, 날 것의 포인터(Raw Pointer)는 소유하지 않는 관찰 용도로 제한하는 것이 모던 C++(Modern C++)의 관례다.

면접에서 이렇게 물어본다

  • 포인터란 무엇이며, 64비트 환경에서 포인터 변수의 크기는 왜 타입과 무관하게 같은가?
  • 배열과 포인터의 공통점과 차이점을 sizeof 동작을 포함해 설명해 보라.
  • 포인터에 1을 더하면 주소가 얼마나 증가하는가? 그 이유는 무엇인가?
  • 이중 포인터는 언제 필요한가? 함수에서 malloc 결과를 호출자에게 전달하는 예로 설명해 보라.
  • 댕글링 포인터가 생기는 대표적인 상황 두 가지와 예방 방법을 말해 보라.
C / C++ 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기