"이 식별자를 통해서는 값을 바꿀 수 없다"고 컴파일러에 알리는 한정자.
const는 "상수"가 아니라 "읽기 전용"이다
const volatile uint32_t *status; // 내가 못 바꿀 뿐, 값은 변한다
정확한 의미는 "이 경로로는 쓰기 금지" 다. 값 자체가 불변이라는 보장이 아니다.
포인터에서의 위치 — 면접 단골
const char *p; // ① 가리키는 값이 const. *p = 'a' ✗, p++ ○
char const *p; // ① 과 완전히 동일
char *const p; // ② 포인터가 const. *p = 'a' ○, p++ ✗
const char *const p; // ③ 둘 다 const
읽는 요령 — 오른쪽에서 왼쪽으로, *를 만나면 "포인터"라고 읽는다.
char * const p
↑ 오른쪽부터: p는 → const → 포인터 → char 를 가리키는
= "p는 char를 가리키는 상수 포인터"
const char * p
↑ p는 → 포인터 → char → const 를 가리키는
= "p는 상수 char를 가리키는 포인터"
const가 *의 왼쪽이면 값, 오른쪽이면 포인터라고 기억하면 빠르다.
임베디드에서 const가 특별한 이유 — 어디에 배치되는가
const uint8_t table[256] = { … }; // .rodata 섹션
uint8_t buffer[256]; // .bss 섹션
| 섹션 | 위치 | 비고 |
|---|---|---|
.rodata | 플래시(ROM) | RAM을 전혀 쓰지 않는다 |
.data | 플래시 → RAM 복사 | 초기값이 있는 변수 |
.bss | RAM (0으로 초기화) | 초기값 없는 변수 |
RAM이 20KB뿐인 MCU에서 256바이트 룩업 테이블을 const 없이 선언하면
RAM을 256바이트 잡아먹는다. const를 붙이면 플래시에만 존재한다.
폰트 데이터, 사인 테이블, 문자열 메시지, 설정 기본값 —
바뀌지 않는 큰 데이터에 const를 빠뜨리는 것이 임베디드에서
RAM이 모자라는 흔한 원인이다.
// 특히 이 차이가 크다
const char *msg[] = { "OK", "FAIL" }; // 포인터 배열은 RAM에!
const char *const msg[] = { "OK", "FAIL" }; // 포인터까지 ROM으로
함수 인자에서 — 계약을 표현한다
void print(const char *s); // "이 함수는 문자열을 수정하지 않는다"
size_t strlen(const char *s); // 표준 라이브러리도 이렇게 선언
호출자에게 약속을 제공하고, 컴파일러가 그 약속을 검사해 준다. 문자열 리터럴을 넘길 수 있는 것도 이 덕분이다.
#define과의 비교
#define | const | |
|---|---|---|
| 처리 시점 | 전처리 (단순 치환) | 컴파일 |
| 타입 검사 | 없음 | 있다 |
| 디버거에서 | 안 보인다 | 심볼로 보인다 |
| 메모리 | 차지 안 함 | .rodata 차지(최적화로 사라지기도) |
| 배열 크기로 | 가능 | C에서는 불가(C++은 가능) |
#define SIZE 10
const int size = 10;
int arr[SIZE]; // ○
int arr2[size]; // C에서는 VLA. 전역에서는 컴파일 에러
타입 검사와 디버깅 때문에 const가 권장되지만, 배열 크기·전처리 조건에는
#define(또는 enum)을 써야 한다.
const를 벗겨내는 것은 미정의 동작
const int x = 10;
int *p = (int *)&x;
*p = 20; // 미정의 동작 — x가 ROM에 있으면 HardFault
컴파일은 될 수 있지만, .rodata가 플래시에 있는 MCU에서는
쓰기 시도가 버스 폴트로 이어진다.