Menu

C의 상수: const, #define, enum

C에서 절대 바뀌지 않는 값에 이름을 붙이는 세 가지 방법 - const 키워드, #define 매크로, enum 상수 - 각각이 실제로 무엇이고 언제 무엇을 골라야 하는지.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

상수는 이름이 붙은 값이자 바뀌지 않겠다는 약속입니다. C는 그 약속을 위한 세 가지 수단을 제공하는데, 내부적으로는 서로 정말 다른 것들입니다. 하나는 변수이고, 하나는 텍스트 치환이며, 하나는 정수 타입입니다.

값에 왜 이름을 붙이나

이 두 줄을 비교해 보세요.

double area = 3.14159 * r * r;
double area = PI * r * r;

두 번째가 더 나은 이유는 세 가지이고, 모두 나중에 드러납니다. 그 숫자가 무엇을 뜻하는지 말해 줍니다. 딱 한 곳에만 나타나므로, 정밀도를 바꾸는 일이 검색이 아니라 한 번의 수정으로 끝납니다. 그리고 PI의 오타는 컴파일 오류이지만, 3.14195의 오타는 틀린 답입니다.

코드 여기저기에 흩어진 이름 없는 숫자를 매직 넘버라고 부릅니다. 이걸 없애는 것은 C 프로그램에 할 수 있는 가장 값싼 개선 중 하나입니다.

const

const 키워드는 변수를 읽기 전용으로 표시합니다.

대입문의 주석을 풀면 컴파일러가 거부합니다. assignment of read-only variable 'PI'. 빌드 시점에 나는 이 오류가 바로 핵심입니다.

const 변수는 진짜 변수입니다. 타입이 있으므로 컴파일러가 사용 방식을 검사합니다. 스코프를 따르므로 함수 안의 const는 밖으로 새지 않습니다. 주소가 있으므로 포인터를 잡을 수 있습니다. 그리고 디버거가 이름과 값을 보여 줄 수 있습니다.

const는 선언하는 자리에서 반드시 초기화해야 합니다. 두 번째 기회는 없습니다.

const int limit;       /* 오류(또는 영원히 쓰레기 값): 나중에 아무것도 대입할 수 없음 */
const int limit = 50;  /* 올바른 방식 */

#define

전처리기를 쓰는 대안은 매크로입니다.

이것은 변수가 아닙니다. 컴파일러가 실행되기 전에 전처리기가 소스에 나오는 모든 PI3.14159265358979라는 문자들로 바꿉니다. 컴파일러는 그 이름을 아예 본 적이 없습니다. #define에 타입이 없고 디버거에 나타나지 않는 이유가 바로 이것입니다.

세미콜론도 =도 없다는 점에 주목하세요. 매크로는 날것의 텍스트이므로, 다음과 같은 흔한 실수는

#define MAX_USERS 100;        /* 세미콜론에 주목 */

int limit = MAX_USERS + 1;    /* 확장 결과: int limit = 100; + 1;  -> 오류 */

정의와는 전혀 다르게 생긴 줄에서 오류를 냅니다. 매크로를 대문자로 쓰는 관례는 바로 치환이 일어나고 있다는 걸 읽는 사람이 알아채게 하려고 존재합니다.

매크로는 관례적으로 UPPER_CASE로 씁니다. 같은 역할을 하는 const 변수도 대개 그 관례를 따릅니다.

enum 상수

세 번째 형태는 이름 붙은 정수들의 집합을 줍니다.

값을 직접 지정하지 않으면 0부터 시작해 하나씩 증가하므로 MON은 0이고 FRI는 4입니다. 이름 없는 enum { ... }은 평범한 정수 상수를 만드는 데 흔히 쓰는 관용구입니다.

enum 상수에는 다른 두 방식에 없는 진짜 장점이 있습니다. 컴파일러가 이해하는 컴파일 타임 정수 상수이면서 디버거에도 보인다는 점입니다. 자세한 이야기는 C의 enum에 있습니다.

무엇을 쓸 것인가

필요한 것선택
타입과 스코프가 있고 디버깅 가능한 상수const
배열 크기, case 라벨, 비트필드 폭enum 또는 #define
문자열 상수#define 또는 const char *
부동소수점 상수const double
서로 관련된 정수 값들의 집합enum
전처리기가 반드시 봐야 하는 것(#if, 인클루드 가드)#define

기본값은 const여야 합니다. 값이 컴파일 전에 존재해야 할 때는 #define을, 컴파일 타임 정수나 이름 붙은 묶음이 필요할 때는 enum을 쓰세요.

이 차이가 정말로 발목을 잡는 곳은 배열 크기입니다.

C에서 const int는 컴파일 타임 상수식이 아니라 읽기 전용 변수입니다. 함수 안에서는 C99가 int local[const_size]를 가변 길이 배열로 만들어 받아들이지만, 파일 스코프나 struct 안, case 라벨 자리에서는 그대로 거부됩니다. enum#define은 어디서나 동작합니다.

(이것은 const int가 실제로 상수식인 C++와의 진짜 차이 중 하나입니다. 두 언어를 오가는 코드가 여기서 걸려 넘어집니다.)

포인터와 함께 쓰는 const

const포인터는 세 가지 방식으로 결합하며, 그 의미는 서로 바꿔 쓸 수 없습니다. 선언은 이름에서 바깥쪽으로 읽으세요.

const char *p;          /* p는 수정하면 안 되는 char들을 가리킴 */
char * const q;         /* q 자체는 다른 곳을 가리킬 수 없음. char는 바꿀 수 있음 */
const char * const r;   /* 포인터도 char도 바꿀 수 없음 */

const char * 형태를 가장 많이 쓰게 됩니다. 문자열을 바꾸지 않고 읽기만 하는 함수 매개변수에 알맞은 타입이기 때문입니다.

읽기만 하는 매개변수에 const를 붙이는 데는 아무 비용도 들지 않고, 컴파일러가 강제하는 방식으로 계약을 문서화해 줍니다. 어디서나 해 둘 만합니다.

문자열 리터럴은 사실상 const입니다

const가 존재하는 이유이기도 한 미묘한 함정입니다.

char *s = "hello";   /* 합법적인 C지만, s는 읽기 전용 메모리를 가리킵니다 */
s[0] = 'H';          /* 미정의 동작 - 보통은 크래시 */

문자열 리터럴은 실행 파일의 읽기 전용 영역에 놓입니다. 그 포인터를 통해 쓰면 대개 세그멘테이션 폴트가 납니다. 포인터를 const char *s = "hello";로 선언하면, 실행 중 크래시가 나게 두는 대신 컴파일러가 그 대입을 거부합니다.

수정 가능한 문자열이 필요하다면 배열에 복사하세요.

자주 묻는 질문

C에서 상수는 어떻게 선언하나요?

선언에 const를 붙이고 값을 주면 됩니다. const double PI = 3.14159;처럼요. 그러면 컴파일러가 그 값에 대입하려는 시도를 전부 거부합니다. 더 오래된 대안은 전처리기 매크로 #define PI 3.14159인데, 이쪽은 텍스트 치환을 합니다.

C에서 const와 #define의 차이는 무엇인가요?

const는 컴파일러가 아는 진짜 타입 있는 변수를 만듭니다. 그래서 타입 검사를 받고, 디버거에 나타나며, 스코프를 따릅니다. #define은 컴파일 전에 이루어지는 순수한 텍스트 치환이라 타입도 스코프도 없고 디버깅 중에 들여다볼 수도 없습니다. 컴파일 타임 상수가 꼭 필요한 자리가 아니라면 const를 쓰세요.

C에서 const 변수를 배열 크기로 쓸 수 있나요?

파일 스코프의 고정 크기 배열에는 쓸 수 없습니다. const int N = 10; int arr[N];은 C99의 가변 길이 배열이 됩니다(함수 안에서는 허용되지만 파일 스코프나 구조체 안에서는 안 됩니다). 진짜 컴파일 타임 크기가 필요하면 #define N 10이나 enum { N = 10 };을 쓰세요.

const char *p는 무슨 뜻인가요?

상수 char를 가리키는 포인터입니다. p가 가리키는 곳은 바꿀 수 있지만, 가리키는 문자는 바꿀 수 없습니다. char * const p는 그 반대로, 수정 가능한 char를 가리키는 상수 포인터입니다. const char * const p는 둘 다 잠급니다. 선언은 이름에서 바깥쪽으로 읽어 나가세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기