|

C 언어 const 포인터 정리: const int*, int* const, const int* const 쉽게 읽는 법

C 언어 const 포인터 차이를 설명하는 대표 이미지
const int*, int* const, const int* const를 한 번에 구분한다

C 언어 const 포인터는 처음 보면 별표와 const 위치만 조금 바뀐 것처럼 보입니다. 그런데 바로 그 위치 차이 때문에 값이 잠기는지, 포인터가 잠기는지가 완전히 달라집니다.

이 글은 복잡한 선언 이론부터 밀어넣지 않습니다. 먼저 왜 초보자가 헷갈리는지 짚고, 그다음 const int*, int* const, const int* const를 하나씩 분리해서 보겠습니다.


const 포인터가 왜 헷갈릴까

초보자가 흔히 하는 실수는 const를 하나의 스위치처럼 보는 것입니다. 하지만 포인터 선언에는 가리키는 값과 포인터 변수 자체, 이렇게 대상이 둘 있습니다. const가 어디에 붙느냐에 따라 고정되는 대상이 달라집니다.

const int *p;
int * const p;
const int * const p;
  • const int *p → 값을 못 바꾸는 포인터
  • int * const p → 가리키는 위치를 못 바꾸는 포인터
  • const int * const p → 둘 다 못 바꾸는 포인터

포인터 선언에서 const를 읽을 때는 무엇이 고정되는가를 먼저 묻는 편이 가장 안전합니다.


기준은 두 가지

int value = 10;
const int *p1 = &value;
int * const p2 = &value;
const int * const p3 = &value;

이제 질문은 두 개뿐입니다. 첫째, *p로 값을 바꿀 수 있는가. 둘째, p = 다른_주소로 포인터를 옮길 수 있는가. 이 두 질문으로 보면 세 선언이 금방 분리됩니다.


세 가지 선언

const int *p

p는 const int를 가리키는 포인터입니다. 쉽게 말해 p를 통해서는 값을 바꿀 수 없고, p 자체는 다른 주소를 다시 가리킬 수 있습니다.

int a = 10;
int b = 20;
const int *p = &a;

p = &b;   // 가능
// *p = 30; // 불가능

여기서 핵심은 a와 b가 반드시 const 객체여야 한다는 뜻이 아니라, 이 포인터 경로를 통해서는 수정하지 않겠다는 제약이라는 점입니다. 읽기 전용 함수 인자에 자주 쓰이는 이유도 여기에 있습니다.

int * const p

이번에는 p 자체가 const입니다. 즉 p는 한 번 정한 주소를 계속 가리켜야 하고, 그 주소에 있는 값은 수정할 수 있습니다.

int a = 10;
int b = 20;
int * const p = &a;

*p = 30;  // 가능
// p = &b; // 불가능

이 선언은 이 포인터는 꼭 이 대상을 가리켜야 한다는 의도를 드러낼 때 씁니다. 초보자에게는 덜 자주 보이지만, 의미는 아주 분명합니다.

const int * const p

이번에는 둘 다 const입니다. 포인터도 못 옮기고, 그 포인터를 통해 값도 못 바꿉니다. 입문 단계에서는 먼저 앞의 두 선언을 확실히 구분한 뒤 이 선언을 얹는 편이 이해가 쉽습니다.

int a = 10;
const int * const p = &a;

// *p = 30; // 불가능
// p = 다른_주소; // 불가능

읽는 순서

선언을 왼쪽부터 억지로 읽으면 자주 꼬입니다. 포인터 선언은 식별자부터 보고 바깥으로 읽는 습관이 더 안전합니다.

예를 들어 int * const p를 보면 먼저 p를 찾습니다. 그다음 바로 왼쪽의 const를 보고 p 자체가 상수라는 점을 잡습니다. 마지막으로 *와 int를 이어서 보면 int를 가리키는 const 포인터가 됩니다.

같은 방식으로 const int *p를 보면 p는 포인터이고, 그 포인터가 가리키는 대상 쪽이 const int라는 점이 드러납니다. 이 방법은 만능 규칙은 아니지만, 초보자가 const와 * 때문에 틀리는 경우는 크게 줄여 줍니다.

  1. 이름을 먼저 찾는다.
  2. 이름 옆의 *와 const를 본다.
  3. 포인터 자체가 const인지 먼저 판단한다.
  4. 그다음 가리키는 대상 타입이 const인지 본다.
  5. const int *p와 int const *p는 같다는 점을 같이 기억한다.

코드로 비교

#include <stdio.h>

int main(void)
{
    int a = 10;
    int b = 20;

    const int *p1 = &a;
    int * const p2 = &a;
    const int * const p3 = &a;

    p1 = &b;   // 가능
    // *p1 = 11; // 오류

    *p2 = 12;  // 가능
    // p2 = &b; // 오류

    // *p3 = 13; // 오류
    // p3 = &b;  // 오류

    printf("a = %d, b = %d\n", a, b);
    return 0;
}
  • const int *p → 값 수정 불가, 재대입 가능
  • int * const p → 값 수정 가능, 재대입 불가
  • const int * const p → 값 수정 불가, 재대입 불가

결국 const 포인터 문제는 값 변경과 주소 변경을 따로 나눠 보는 문제입니다.


자주 틀리는 점

  • const int *p를 보고 p 자체가 const라고 착각한다.
  • int * const p를 보고 값도 못 바꾼다고 착각한다.
  • const int *p와 int const *p가 다르다고 생각한다.
  • 캐스트로 const를 벗기면 언제나 괜찮다고 생각한다.

특히 마지막 오해는 조심해야 합니다. 원래 const-qualified 객체를 억지로 수정하려 들면 정의되지 않은 동작이 될 수 있습니다. 컴파일이 된다는 이유만으로 안전하다고 보면 안 됩니다.


함수 인자에서 보기

#include <stddef.h>
#include <stdio.h>

void print_numbers(const int *arr, size_t len)
{
    for (size_t i = 0; i < len; ++i)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

이 선언은 arr가 가리키는 값을 이 함수 안에서 바꾸지 않겠다는 뜻입니다. 즉 호출하는 쪽에 이 함수는 읽기만 한다는 의도를 분명하게 전달합니다.

배열 인자와 포인터 인자가 왜 비슷하게 보이는지 아직 헷갈린다면, C 언어 포인터와 배열 차이 글을 같이 보면 감이 훨씬 빨리 잡힙니다. 선언 문법 자체가 낯설다면 C 언어 struct와 typedef 차이 글도 도움이 됩니다.


정리

  1. 포인터 선언에는 값과 포인터 두 대상이 있다.
  2. const int *p는 값을 못 바꾼다.
  3. int * const p는 포인터를 못 옮긴다.
  4. const int * const p는 둘 다 못 바꾼다.
  5. 헷갈리면 이름부터 찾고 무엇이 고정되는지 먼저 본다.

const가 포인터와 만나면 무엇을 보호하려는 선언인가라고 물어보면 대부분 풀립니다.

더 자세한 문법 기준은 cppreference의 const qualifier 문서, pointer declaration 문서, 그리고 선언 읽기 팁은 comp.lang.c FAQ를 참고했습니다.

함께보면 좋은 글