본문으로 건너뛰기

안동민 개발노트

본문 시작

const 포인터와 포인터 상수

const가 가리키는 값과 포인터 자체 중 무엇을 제한하는지 세 선언으로 구분하고 함수 매개변수에 적용합니다.

포인터를 실제 코드에서 안전하게 사용하려면 const가 포인터에 붙는 위치에 따라 의미가 어떻게 달라지는지 정확히 이해해야 합니다.

많은 초급자들이 const int*, int* const, const int* const를 헷갈리며, 이 혼동이 버그로 이어지는 경우가 많습니다.

이 장에서는 세 가지 선언을 명확히 구분하고, 함수 매개변수 설계에서 어떤 형태를 선택해야 하는지 실전 기준으로 정리하겠습니다.

먼저 const가 값과 주소 중 무엇을 잠그는지 세 선언을 나란히 비교합니다.

const의 위치는 값이 잠기는지 주소가 잠기는지를 결정한다

포인터 선언을 읽을 때는 “가리키는 값”과 “포인터 변수의 주소값”을 나눠 봅니다. const가 대상 타입에 붙으면 값이 잠기고, 변수 이름 오른쪽에 붙으면 주소 재지정이 잠깁니다.

  1. 값을 보호

    읽기 전용 데이터를 가리킬 때 const int*를 씁니다.

  2. 주소를 고정

    포인터 변수가 다른 대상을 가리키지 못하게 할 때 int* const를 씁니다.

  3. 둘 다 보호

    의도치 않은 값 변경과 재지정을 모두 막습니다.


const가 가리키는 대상에 붙는 경우 (const int*)

const int* ptr포인터가 가리키는 값을 수정할 수 없다는 의미입니다.

  • 포인터 자체(ptr)는 다른 주소를 가리키도록 변경할 수 있습니다.
  • 하지만 *ptr을 통해 값을 바꾸는 것은 금지됩니다.
pointer to const
#include <iostream>

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

    const int* ptr = &a; // int const* ptr 와 동일

    // *ptr = 30; // 컴파일 오류: 가리키는 값 수정 불가
    ptr = &b;     // 가능: 포인터가 다른 대상을 가리키는 것은 허용

    std::cout << "*ptr = " << *ptr << "\n"; // 20
}

포인터 자체가 상수인 경우 (int* const)

int* const ptr포인터 자체가 상수라는 뜻입니다.

  • ptr이 한 번 가리킨 주소는 바꿀 수 없습니다.
  • 대신 가리키는 값(*ptr)은 수정할 수 있습니다.
const pointer
#include <iostream>

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

    int* const ptr = &a;

    *ptr = 30;    // 가능: 대상 값 수정 가능
    // ptr = &b;  // 컴파일 오류: 포인터 자체는 변경 불가

    std::cout << "a = " << a << "\n"; // 30
}

둘 다 상수인 경우 (const int* const)

const int* const ptr는 포인터 자체도 상수, 가리키는 값도 상수입니다.

  • ptr 재지정 불가
  • *ptr 수정 불가
const pointer to const data
#include <iostream>

int main() {
    int value = 42;
    const int* const ptr = &value;

    // *ptr = 100; // 오류
    // ptr = nullptr; // 오류

    std::cout << *ptr << "\n";
}

읽는 요령: 오른쪽에서 왼쪽으로

복잡한 선언은 변수 이름 기준으로 오른쪽에서 왼쪽으로 읽으면 해석이 쉬워집니다.

  • const int* p -> p는 포인터, 가리키는 int는 const
  • int* const p -> p는 const 포인터
  • const int* const p -> const 포인터가 const int를 가리킴
포인터 선언은 변수 이름 p를 기준으로 잠긴 쪽을 찾는다

복잡한 포인터 선언은 문장처럼 외우기보다 p를 중심에 두고 읽습니다. p 오른쪽의 const는 포인터 변수 고정, p 왼쪽 타입의 const는 가리키는 값 고정입니다.

  1. 타입 쪽 const

    const int* p p가 가리키는 int 값을 p를 통해 수정할 수 없습니다.

  2. 이름 쪽 const

    int* const p p 자체를 다른 주소로 다시 묶을 수 없습니다.


함수 매개변수 설계에서의 선택 기준

포인터를 매개변수로 받을 때는 “함수가 무엇을 바꿀 수 있어야 하는가?”를 기준으로 결정합니다.

데이터를 읽기만 할 때

const T* 또는 const T&를 사용합니다.

double getLength(const Vector2* v);
double getLength(const Vector2& v);
데이터를 수정해야 할 때

T* 또는 T&를 사용합니다.

void normalize(Vector2* v);
void normalize(Vector2& v);
포인터 자체 재지정까지 막고 싶을 때

함수 내부 구현 관점에서 T* const를 사용할 수 있습니다.

void updateValue(int* const p) {
    // p = nullptr; // 금지
    *p = 99;       // 허용
}

실무에서는 API 사용자 입장에서 명확한 의도를 전달하기 위해 읽기 전용은 const, 수정 가능은 non-const를 엄격히 구분하는 것이 중요합니다.


포인터와 참조 중 무엇을 쓸까?

포인터와 참조 매개변수는 대상이 항상 존재하는지, null을 허용하는지, 읽기 전용인지, 호출자가 소유권을 넘기는지를 기준으로 선택합니다.

참조는 반드시 있는 대상, 포인터는 없을 수도 있는 대상을 표현한다

포인터와 참조는 둘 다 원본을 다룰 수 있지만 API가 보내는 신호가 다릅니다. null 허용 여부와 수정 가능성을 먼저 정하면 타입 선택이 분명해집니다.

  1. 수정 참조

    T& 대상이 반드시 있고 원본을 바꿉니다.

  2. 읽기 참조

    const T& 큰 객체를 복사하지 않고 읽기만 합니다.

  3. 선택 포인터

    T* null이 의미 있거나 대상을 바꿀 수 있습니다.

  4. 읽기 포인터

    const T* 없을 수도 있는 읽기 대상을 표현합니다.

둘 다 원본 객체를 다룰 수 있지만, 용도가 다릅니다.

  • 참조(T&, const T&): 반드시 유효한 객체가 있어야 하며, null 개념이 없습니다.
  • 포인터(T*, const T*): null(nullptr)을 표현할 수 있어 “없음” 상태를 전달하기 쉽습니다.

즉, “항상 대상이 존재한다”면 참조가 더 단순하고, “대상이 없을 수도 있다”면 포인터가 더 적절합니다.


포인터/참조 선택 기준 정리

const 포인터 선언은 문법보다 "수정 가능한 대상"을 먼저 나누면 실수가 줄어듭니다.

아래 체크카드는 값, 주소, 없음 상태를 기준으로 어떤 형태를 고를지 정리한 것입니다.

수정 가능한 것과 없을 수 있는 것을 먼저 나누면 타입이 정해진다

const 포인터 선언과 참조 선택은 모두 같은 질문으로 정리할 수 있습니다. 값이 바뀌는가, 주소가 바뀌는가, 대상이 없을 수 있는가를 순서대로 확인합니다.

  1. 값을 읽기만 한다

    const T& 대상이 항상 있으면 읽기 참조를 먼저 고려합니다.

  2. 값을 반드시 바꾼다

    T& null 없이 호출자의 원본을 수정합니다.

  3. 없을 수도 있다

    T* null을 의미 있는 상태로 표현합니다.

  4. 주소를 고정한다

    T* const 포인터 변수가 다른 대상으로 바뀌지 못하게 합니다.

  • const int* : 대상 값 수정 금지
  • int* const : 포인터 재지정 금지
  • const int* const : 둘 다 금지
  • 함수 설계에서는 const를 통해 읽기/수정 의도를 명확히 드러내는 것이 핵심입니다.

const 포인터 선언은 기호를 외우기보다 값이 잠기는지, 주소가 잠기는지, 둘 다 잠기는지를 한 번에 확인하는 습관이 가장 실용적입니다.