const 포인터와 포인터 상수
const가 가리키는 값과 포인터 자체 중 무엇을 제한하는지 세 선언으로 구분하고 함수 매개변수에 적용합니다.
포인터를 실제 코드에서 안전하게 사용하려면 const가 포인터에 붙는 위치에 따라 의미가 어떻게 달라지는지 정확히 이해해야 합니다.
많은 초급자들이 const int*, int* const, const int* const를 헷갈리며, 이 혼동이 버그로 이어지는 경우가 많습니다.
이 장에서는 세 가지 선언을 명확히 구분하고, 함수 매개변수 설계에서 어떤 형태를 선택해야 하는지 실전 기준으로 정리하겠습니다.
먼저 const가 값과 주소 중 무엇을 잠그는지 세 선언을 나란히 비교합니다.
POINTEE ACCESS · POINTER OBJECT · TOP-LEVEL CONST
const는 위치에 따라 대상 접근과 포인터 재지정을 제한한다
선언은 대상 수정과 p 재지정을 따로 묻습니다. 대상 타입의 const는 이 경로를 제한합니다. 다른 비-const 별칭의 변경은 막지 않습니다. 객체 전체 불변도 아닙니다.
| 선언 | 대상 수정? | p 재지정? | 정확한 의미 |
|---|---|---|---|
const T* p | 아니요 | 예 | p가 제공하는 경로는 읽기 전용이고, p는 다른 T를 가리킬 수 있습니다. |
T* const p | 예 | 아니요 | 대상은 수정할 수 있지만, 이 포인터 객체는 초기화 뒤 다른 주소로 재지정할 수 없습니다. |
const T* const p | 아니요 | 아니요 | 이 경로로는 대상을 수정할 수 없고, 포인터 객체도 재지정할 수 없습니다. |
const T* p
대상 수정: 아니요 · 재지정: 예. p 경로는 읽기 전용이며, 다른 비-const 별칭의 변경은 막지 않습니다.
T* const p
대상 수정: 예 · 재지정: 아니요. 대상 수정만 가능합니다.
const T* const p
대상 수정: 아니요 · 재지정: 아니요. 두 제한이 독립적으로 함께 적용됩니다.
void inspect(T* const p)
최상위 const는 내부 복사본 p만 고정합니다. 함수 타입에는 이 한정이 남지 않습니다. 호출자 포인터와 대상 접근은 바뀌지 않습니다. 읽기 전용에는 const T*가 필요합니다.
오른쪽→왼쪽 읽기는 문법 규칙이 아닌 출발점이다
변수 이름부터 찾습니다. const가 포인터 쪽인지 대상 타입 쪽인지 봅니다. 함수 포인터 등은 괄호와 선언자 구조를 봅니다.
const가 가리키는 대상에 붙는 경우 (const int*)
const int* ptr는 ptr가 제공하는 접근 경로로 가리키는 값을 수정할 수 없다는 의미입니다.
- 포인터 자체(
ptr)는 다른 주소를 가리키도록 변경할 수 있습니다. - 하지만
*ptr을 통해 값을 바꾸는 것은 금지됩니다.
여기서 const는 이 접근 경로를 제한합니다. 같은 객체를 가리키는 다른 non-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)은 수정할 수 있습니다.
#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수정 불가
#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는 constint* const p->p는 const 포인터const int* const p-> const 포인터가 const int를 가리킴
흔히 말하는 “오른쪽에서 왼쪽으로 읽기”는 이 확인을 돕는 휴리스틱입니다. 함수 포인터처럼 선언이 복잡해지면 괄호와 선언자 구조가 실제 문법 기준입니다.
함수 매개변수 설계에서의 선택 기준
포인터를 매개변수로 받을 때는 “함수가 무엇을 바꿀 수 있어야 하는가?”를 기준으로 결정합니다.
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; // 허용
}값으로 전달한 포인터 매개변수의 최상위 const는 함수 안의 복사본 p에만 적용됩니다. 함수 타입과 호출자 계약에서는 T* 매개변수와 구별되지 않으며, 호출자의 포인터나 대상 자체를 고정하지 않습니다.
실무에서는 API 사용자 입장에서 명확한 의도를 전달하기 위해 읽기 전용은 const, 수정 가능은 non-const를 엄격히 구분하는 것이 중요합니다.
포인터와 참조 중 무엇을 쓸까?
포인터와 참조 매개변수는 대상이 항상 존재하는지, null을 허용하는지, 읽기 전용인지, 호출자가 소유권을 넘기는지를 기준으로 선택합니다.
PRESENCE · MUTABILITY · LIFETIME · OWNERSHIP
존재·수정 우선, 수명·소유권 별도
필수 대상에는 참조를, nullptr가 의미 있으면 포인터를 먼저 검토합니다. 이어 이 경로에서 원본을 수정할지에 따라 const를 고릅니다.
존재 계약
필수 대상을 표현할지, 없음 상태를 허용할지 결정합니다.
접근 권한
이 함수 경로에서 원본을 수정할지 읽기만 할지 결정합니다.
수명과 소유권
호출 중 유효성, 소유자, 해제 시점은 별도 계약입니다.
| 존재 계약 | 대상 수정? | 형태 | 호출자에게 보이는 의미 |
|---|---|---|---|
| 반드시 존재 | 예 | T& | 유효한 대상을 받아 원본을 수정합니다. 참조 자체에는 정상적인 null 상태가 없습니다. |
| 반드시 존재 | 아니요 | const T& | 유효한 대상을 받아 이 참조 경로로는 읽기만 합니다. |
| 없음 허용 | 예 | T* | nullptr를 검사한 뒤 대상이 있으면 수정합니다. 포인터라는 사실만으로 비-null을 보장하지 않습니다. |
| 없음 허용 | 아니요 | const T* | nullptr를 검사한 뒤 대상이 있으면 이 경로로 읽기만 합니다. |
T&
대상은 반드시 존재하고, 함수는 이 경로로 원본을 수정합니다.
const T&
대상은 반드시 존재하고, 함수는 이 경로로 읽기만 합니다.
T*
nullptr를 허용하며, 대상이 있으면 수정할 수 있습니다.
const T*
nullptr를 허용하며, 대상이 있으면 이 경로로 읽습니다.
참조의 비-null 성질과 수명 유효성은 다른 조건이다
정상적으로 형성된 참조에는 null 상태가 없지만, 참조 대상이 먼저 파괴되면 참조는 댕글링할 수 있습니다. 포인터도 비-null인 것만으로 유효한 객체를 가리킨다고 보장되지 않습니다.
원시 포인터와 참조 자체는 소유권 이전을 표현하지 않는다
빌린 접근은 수명 규칙을 문서화합니다. 소유권 이전·공유에는 값이나 스마트 포인터를 씁니다. 연속 범위는 길이를 담는 std::span 같은 뷰를 검토합니다.
둘 다 원본 객체를 다룰 수 있지만, 용도가 다릅니다.
- 참조(
T&,const T&): 정상적으로 형성된 참조에는 null 상태가 없으므로, 호출 시점에 유효한 객체가 필요합니다. - 포인터(
T*,const T*): null(nullptr)을 표현할 수 있어 “없음” 상태를 전달하기 쉽습니다.
즉, “항상 대상이 존재한다”면 참조가 더 단순하고, “대상이 없을 수도 있다”면 포인터가 더 적절합니다. 다만 참조가 null이 아니라는 사실은 대상이 영원히 유효하다는 뜻이 아니며, 대상이 먼저 파괴되면 댕글링할 수 있습니다.
const int*: 대상 값 수정 금지int* const: 포인터 재지정 금지const int* const: 둘 다 금지- 함수 설계에서는
const를 통해 읽기/수정 의도를 명확히 드러내는 것이 핵심입니다.
원시 포인터와 참조는 그 자체로 소유권 이전을 표현하지 않습니다. 소유권을 넘기거나 공유해야 한다면 값 또는 스마트 포인터처럼 그 계약을 담는 타입을 별도로 선택해야 합니다.
const 포인터 선언은 기호를 외우기보다 값이 잠기는지, 주소가 잠기는지, 둘 다 잠기는지를 한 번에 확인하는 습관이 가장 실용적입니다.