C++ const 완벽 가이드 | "const 정확성" 실전 활용
이 글의 핵심
const 정확성(const-correctness)은 바꾸지 않을 값과 API를 컴파일러로 강제해 버그를 줄이는 C++ 관례입니다. 이 글에서는 const 변수·멤버 함수·포인터 선언과 mutable 조합을 예제로 구분해 설명합니다.
const 변수
const는 “변경 불가”를 표현해 코드 리뷰에서도 권장됩니다. mutable과 함께 쓰면 캐시·락 등 예외적으로 수정 가능한 멤버를 둘 수 있고, 함수 기초·스마트 포인터와 조합해 읽기 전용 인터페이스를 만들 수 있습니다.
const int x = 10;
// x = 20; // 에러: const 변수는 수정 불가
const int y; // 에러: const는 선언 시 초기화 필수
const 함수
class Point {
private:
int x, y;
public:
Point(int x, int y) : x(x), y(y) {}
// const 멤버 함수 (객체 상태 변경 안함)
int getX() const { return x; }
int getY() const { return y; }
// non-const 멤버 함수
void setX(int newX) { x = newX; }
};
int main() {
const Point p(10, 20);
cout << p.getX() << endl; // OK
// p.setX(30); // 에러: const 객체는 non-const 함수 호출 불가
}
const 포인터
const 포인터 패턴
| 선언 | 포인터 수정 | 값 수정 | 읽는 법 |
|---|---|---|---|
const int* ptr | ✅ | ❌ | “포인터 to const int” |
int* const ptr | ❌ | ✅ | “const 포인터 to int” |
const int* const ptr | ❌ | ❌ | “const 포인터 to const int” |
int const* ptr | ✅ | ❌ | const int*와 동일 |
int x = 10;
int y = 20;
// 1. 포인터가 가리키는 값이 const
const int* ptr1 = &x;
// *ptr1 = 20; // 에러
ptr1 = &y; // OK
// 2. 포인터 자체가 const
int* const ptr2 = &x;
*ptr2 = 20; // OK
// ptr2 = &y; // 에러
// 3. 둘 다 const
const int* const ptr3 = &x;
// *ptr3 = 20; // 에러
// ptr3 = &y; // 에러
외우는 법: const를 기준으로 오른쪽이 const
const 포인터 시각화
graph TD
A[const int* ptr] --> B[ptr 변경 가능]
A --> C[*ptr 변경 불가]
D[int* const ptr] --> E[ptr 변경 불가]
D --> F[*ptr 변경 가능]
G[const int* const ptr] --> H[ptr 변경 불가]
G --> I[*ptr 변경 불가]
const 참조
void process(const vector<int>& v) {
// v를 읽기만 함 (복사 없음)
for (int x : v) {
cout << x << " ";
}
// v.push_back(10); // 에러
}
int main() {
vector<int> data = {1, 2, 3};
process(data); // 복사 없이 전달
}
mutable
mutable 사용 시나리오
| 시나리오 | 예시 | 이유 |
|---|---|---|
| 캐싱 | 계산 결과 저장 | 논리적으로 const, 물리적으로 변경 |
| 통계 | 접근 횟수 카운트 | 관찰만 하는 동작 |
| 동기화 | mutable mutex | 락은 논리적 상태 아님 |
| 지연 초기화 | 첫 접근 시 초기화 | 읽기 동작이지만 내부 변경 |
class Cache {
private:
mutable int accessCount; // const 함수에서도 수정 가능
int value;
public:
Cache(int v) : value(v), accessCount(0) {}
int getValue() const {
accessCount++; // OK: mutable
return value;
}
int getAccessCount() const {
return accessCount;
}
};
int main() {
const Cache cache(42);
cout << cache.getValue() << endl; // accessCount 증가
cout << cache.getAccessCount() << endl; // 1
}
실전 예시
예시 1: const 정확성
class String {
private:
char* data;
size_t length;
public:
String(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
~String() {
delete[] data;
}
// const 버전
const char* c_str() const {
return data;
}
// non-const 버전
char* c_str() {
return data;
}
size_t size() const {
return length;
}
};
void print(const String& s) {
cout << s.c_str() << endl; // const 버전 호출
}
예시 2: const와 반복자
#include <vector>
using namespace std;
void process(const vector<int>& v) {
// const_iterator 사용
for (vector<int>::const_iterator it = v.begin();
it != v.end(); ++it) {
cout << *it << " ";
// *it = 10; // 에러
}
// 또는 auto 사용
for (auto it = v.cbegin(); it != v.cend(); ++it) {
cout << *it << " ";
}
}
예시 3: const와 스레드 안전성
#include <mutex>
class ThreadSafeCounter {
private:
mutable mutex mtx; // const 함수에서도 lock 가능
int count;
public:
ThreadSafeCounter() : count(0) {}
void increment() {
lock_guard<mutex> lock(mtx);
count++;
}
int getCount() const {
lock_guard<mutex> lock(mtx); // OK: mutable
return count;
}
};
자주 발생하는 문제
문제 1: const 함수에서 멤버 수정
// ❌ 에러
class Bad {
private:
int x;
public:
void func() const {
x = 10; // 에러!
}
};
// ✅ mutable 사용
class Good {
private:
mutable int x;
public:
void func() const {
x = 10; // OK
}
};
문제 2: const 오버로딩
class Container {
private:
int data[10];
public:
// const 버전
const int& operator const {
return data[index];
}
// non-const 버전
int& operator {
return data[index];
}
};
int main() {
Container c;
c[0] = 10; // non-const 버전
const Container cc;
int x = cc[0]; // const 버전
// cc[0] = 10; // 에러
}
문제 3: const_cast
void legacyFunction(char* str) {
// 레거시 함수 (const 없음)
}
void modernFunction(const char* str) {
// const_cast로 const 제거 (위험!)
legacyFunction(const_cast<char*>(str));
}
// ✅ 더 나은 방법: 래퍼 함수
void safeWrapper(const char* str) {
char* temp = new char[strlen(str) + 1];
strcpy(temp, str);
legacyFunction(temp);
delete[] temp;
}
const 정확성 체크리스트
1. 함수 매개변수
// ❌ 불필요한 복사
void process(vector<int> v) { }
// ✅ const 참조
void process(const vector<int>& v) { }
2. 멤버 함수
class MyClass {
public:
// ❌ const 누락
int getValue() { return value; }
// ✅ const 추가
int getValue() const { return value; }
};
3. 반환 타입
class String {
public:
// ❌ 내부 데이터 노출
char* getData() { return data; }
// ✅ const 반환
const char* getData() const { return data; }
};
컴파일러 최적화
// const는 컴파일러 최적화에 도움
void compute(const int* data, int size) {
// 컴파일러가 data가 변하지 않음을 알고 최적화
for (int i = 0; i < size; i++) {
// ...
}
}
FAQ
Q1: const를 왜 사용하나요?
A:
- 의도 명확화 (이 값은 변하지 않음)
- 버그 방지 (실수로 수정 불가)
- 컴파일러 최적화
- 인터페이스 안전성
Q2: const를 어디에 붙여야 하나요?
A:
- 변하지 않는 변수
- 객체를 수정하지 않는 멤버 함수
- 복사를 피하려는 함수 매개변수
Q3: mutable은 언제 사용하나요?
A:
- 캐싱
- 로깅
- 참조 카운팅
- 뮤텍스
Q4: const_cast는 안전한가요?
A: 위험합니다. 원래 const인 객체를 수정하면 정의되지 않은 동작입니다. 레거시 코드 통합 시에만 사용하세요.
Q5: const와 성능?
A: const 자체는 런타임 오버헤드가 없습니다. 오히려 컴파일러 최적화에 도움이 됩니다.
Q6: const 정확성을 어떻게 시작하나요?
A:
- 새 코드에서 const 습관화
- 멤버 함수에 const 추가
- 함수 매개변수를 const 참조로
- 점진적으로 개선
관련 글: mutable, 스마트 포인터, 함수 기초, 코드 리뷰 체크리스트.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ mutable Keyword | “mutable 키워드” 가이드
- C++ 스마트 포인터 | unique_ptr/shared_ptr “메모리 안전” 가이드
- C++ 함수 | “처음 배우는” 함수 만들기 완벽 가이드 [예제 10개]
- C++ 코드 리뷰 | “체크리스트” 20가지 [실무 필수]
관련 글
- [C++ 클린 코드 기초: const, noexcept, ]로 인터페이스 의도 명확히 하기](/blog/cpp-series-38-1-clean-code-const-noexcept-nodiscard/)
- C++ 코드 리뷰 |
- C++ const 에러 |
- C++ mutable Keyword |
- C++ constexpr 함수와 변수 | 컴파일 타임에 계산하기 [#26-1]