본문 바로가기

상수와 리터럴

값에 이름을 붙이는 const와 constexpr, #define, 열거형의 차이를 비교하고, 정수와 실수, 문자, 문자열, 논리, 포인터 리터럴의 표기가 그 값의 타입을 어떻게 정하는지 접두사와 접미사, 이스케이프 시퀀스, 원시 문자열과 함께 익힙니다.

코드에 3.141592나 100 같은 숫자를 그대로 적으면, 읽는 사람은 그 숫자가 원주율인지 최대 사용자 수인지 알기 어렵고, 같은 값을 여러 곳에 적었다면 바꿀 때 하나라도 빠뜨리기 쉽습니다. 그래서 바뀌지 않는 값에는 이름을 붙이고, 코드에 직접 적는 값은 그 표기가 어떤 타입이 되는지를 알고 씁니다.

이 글은 값에 이름을 붙이는 네 가지 방법인 const, constexpr, #define, 열거형을 비교하고, 코드에 직접 적는 값인 리터럴의 종류와 그 표기가 정하는 타입을 정리합니다.

값에 이름 붙이기

이름 있는 상수를 쓰면 PI라는 이름이 숫자의 뜻을 알려 주고, 실수로 값을 바꾸는 코드를 컴파일러가 막으며, 값을 바꿀 때 정의 한 곳만 고치면 됩니다. C++에는 이렇게 값에 이름을 붙이는 방법이 여러 가지 있고, 각각 타입과 범위, 값이 정해지는 시점이 다릅니다.

C++ · NAMED VALUES

이름 있는 객체·전처리 치환·열거형은 역할이 다르다

이름을 붙인다는 공통점보다 타입, 범위, 검증 책임을 기준으로 선택합니다.

타입과 범위가 필요한 값

const int MAX_LIVES = 3;

const 객체는 타입과 C++ 범위를 가집니다. 초기값은 실행 중 계산된 값일 수도 있어, 항상 컴파일 시점 상수를 뜻하지 않습니다.

전처리 치환 이름

#define MAX_BUFFER_SIZE 1024

사용 전에 텍스트를 치환합니다. C++ 이름 범위를 따르지 않으며, 치환된 C++ 표현식에는 타입 검사가 적용됩니다.

관련 상태에 이름 붙이기

enum Color {
  RED, GREEN, BLUE
};

열거형과 열거자 RED=0, GREEN=1, BLUE=2를 선언합니다. 외부 입력이 유효한 상태인지는 따로 검증합니다.

객체의 불변성에는 const, 관련 상태 집합에는 enum을 검토합니다. 전처리 치환을 C++의 타입·범위를 가진 객체처럼 취급하지 않습니다.

const는 앞 글에서 본 것처럼 초기화한 뒤 바꿀 수 없는 객체를 만듭니다. const 객체는 타입이 있고, 선언한 블록 안에서만 쓰이는 범위를 가집니다. 다만 초깃값은 실행 중에 정해질 수도 있어서, const int limit = input;처럼 사용자가 입력한 값으로도 초기화할 수 있습니다. const는 바꿀 수 없다는 뜻일 뿐, 컴파일할 때 값이 정해진다는 뜻은 아닙니다.

컴파일할 때 값이 정해져야 하는 상수에는 C++11에 들어온 constexpr을 씁니다. constexpr 변수는 컴파일 시간에 계산할 수 있는 상수 표현식(constant expression)으로 초기화해야 하며, 그래서 배열의 크기처럼 컴파일러가 미리 알아야 하는 곳에 쓸 수 있습니다.

constexpr int MAX_USERS = 100;
constexpr double PI = 3.1415926535;
constexpr double AREA = PI * 2.0 * 2.0;
int users[MAX_USERS];

int input = 0;
std::cin >> input;
const int limit = input;
// constexpr int bad = input;

AREA는 다른 constexpr 상수로만 계산하므로 컴파일할 때 값이 정해집니다. 마지막 줄의 주석을 풀면, input의 값은 실행할 때에야 알 수 있으므로 상수 표현식이 아니라는 컴파일 오류가 납니다. 실행 중에 정해지는 값은 const로, 컴파일할 때 정해져야 하는 값은 constexpr로 구별합니다.

#define은 C에서 상수를 만들던 방법으로, 전처리 단계에서 그 이름의 토큰을 적어 둔 내용으로 바꿔 넣습니다. 끝에 세미콜론을 붙이지 않으며, 붙이면 세미콜론까지 함께 바뀌어 들어가 char buffer[MAX_BUFFER_SIZE];가 char buffer[1024;];가 되는 것처럼 매크로 바로 뒤에 ]나 ) 같은 다른 토큰이 이어지는 곳에서 오류가 납니다. 매크로는 C++의 이름 범위를 따르지 않고, 정의한 뒤로 #undef를 만나거나 컴파일하는 소스 파일이 끝날 때까지 어디서나 바뀝니다. 헤더에서 정의한 매크로도 그 헤더를 포함한 소스 파일의 나머지 전체에서 바뀌므로 같은 이름의 변수나 함수와 부딪히기 쉽고, 컴파일 전에 사라져 디버거에서 그 이름을 볼 수도 없습니다. 그래서 C++에서는 상수에 #define 대신 const나 constexpr을 씁니다.

#define MAX_BUFFER_SIZE 1024

열거형(enumeration)은 서로 관련된 정수 상수에 이름을 붙여 하나의 타입으로 묶습니다. 값을 적지 않으면 첫 열거자가 0이 되고 하나씩 늘어납니다.

enum Color { RED, GREEN, BLUE };
enum State { IDLE = 0, RUNNING = 1, PAUSED = 2, STOPPED = 3 };

Color myColor = RED;

RED, GREEN, BLUE는 각각 0, 1, 2이며, State처럼 값을 직접 정할 수도 있습니다. C++11부터는 열거자의 이름이 밖으로 새지 않고 정수로 저절로 바뀌지도 않는 enum class가 있어서 새 코드에서는 그쪽을 많이 씁니다. enum class Light { Red, Yellow, Green };의 열거자는 Light::Yellow처럼 타입 이름을 붙여 쓰고, 정수가 필요하면 다음 글에서 볼 static_cast<int>(Light::Yellow)처럼 직접 바꿉니다. 열거형으로 상태를 정해 두어도, 파일이나 사용자에게서 들어온 정수가 그중 하나인지는 따로 검사해야 합니다.

리터럴과 그 타입

리터럴(literal)은 10, 3.14, 'A', "Hi"처럼 코드에 직접 적은 값입니다. int score = 10;에서 score는 객체의 이름이고 10이 리터럴이며, 리터럴은 이름 없는 상수여서 그 값을 바꿀 수 없습니다. 모든 리터럴은 타입을 가지며, 그 타입은 표기의 모양과 접두사, 접미사로 정해집니다.

C++ · LITERAL TYPES

리터럴의 표기가 타입과 값 표현을 정한다

정수·실수·문자·문자열·논리·null 포인터를 실제 표기와 함께 구분합니다.

정수

10 → int · 10u → unsigned int

진법·접미사에 해당하는 후보 중 값을 표현하는 첫 타입을 선택합니다. 모든 정수 리터럴이 int인 것은 아닙니다.

부동 소수점

3.14 → double · 3.14f → float

L 접미사는 long double입니다. 리터럴의 타입과 저장할 객체 타입은 별도로 확인합니다.

문자

'A' → char · u'A' → char16_t

prefix가 문자 타입과 인코딩을 정합니다. 여기서는 한 코드 단위로 표현되는 문자 예시를 비교합니다.

문자열

"Hi" → const char[3]

끝의 null 문자를 포함한 배열이며 정적 저장 기간을 가집니다. raw 표기는 이스케이프 해석 방식을 바꾸며 std::string 객체를 만들지는 않습니다.

논리

true · false → bool

조건 상태를 표현하는 두 리터럴입니다. 정수 1·0으로 변환할 수 있다는 사실과 고유한 타입을 구분합니다.

null 포인터

nullptr → std::nullptr_t

포인터 타입 자체가 아닌 고유한 타입입니다. 포인터로 변환하면 해당 타입의 null 포인터 값을 만듭니다.

-5는 리터럴 5에 단항 -를 적용한 식입니다. 문자·문자열 prefix와 정수의 진법·접미사·범위를 함께 확인합니다.

정수 리터럴

정수 리터럴은 네 가지 진법으로 적을 수 있습니다.

표기진법예십진수 값
접두사 없음십진수1010
0b 또는 0B이진수, C++14부터0b101010
0팔진수01210
0x 또는 0X십육진수0xA10

0으로 시작하는 수는 팔진수라는 점에 주의합니다. 012는 12가 아니라 10이며, 09는 팔진수에 9가 없으므로 오류입니다. C++14부터는 1'000'000처럼 작은따옴표로 자릿수를 나누어 읽기 쉽게 적을 수 있고, 작은따옴표는 값에 영향을 주지 않습니다. -5는 리터럴이 아니라, 리터럴 5에 부호를 바꾸는 단항 - 연산자를 적용한 식입니다.

접미사가 없는 십진 정수 리터럴은 int, long, long long 가운데 그 값을 담을 수 있는 첫 타입이 됩니다. 그래서 32비트 int를 쓰는 구현에서 2147483648은 int가 아니라 long이나 long long이 됩니다. 접미사 U나 u는 부호 없는 타입을, L이나 l은 long을, LL이나 ll은 long long을 뜻하며, UL이나 ULL처럼 함께 쓸 수도 있습니다. 소문자 l은 숫자 1과 헷갈리므로 대문자 L을 씁니다.

long long veryBig = 123456789012345LL;
unsigned int flags = 0xFFU;
int million = 1'000'000;

부동 소수점 리터럴

소수점이나 지수 표기를 포함한 수는 부동 소수점 리터럴이며, 접미사가 없으면 double입니다. 지수 표기 6.02e23은 6.02 × 10²³을 뜻합니다. 접미사 F나 f는 float, L이나 l은 long double을 뜻합니다.

double dval = 3.14;
float fval = 3.14F;
long double ldval = 3.14L;
double avogadro = 6.02e23;

리터럴의 타입과 그 값을 담는 변수의 타입은 별개입니다. double d = 3.14F;는 먼저 float의 정밀도로 반올림한 값을 double에 담으므로, 처음부터 double 리터럴 3.14를 담은 것과 값이 다릅니다.

문자 리터럴

작은따옴표로 감싼 문자 하나는 문자 리터럴이며 타입은 char입니다. 앞에 u를 붙이면 char16_t, U를 붙이면 char32_t, L을 붙이면 wchar_t, C++20부터 u8을 붙이면 char8_t가 됩니다.

줄 바꿈처럼 그대로 적을 수 없는 문자는 백슬래시로 시작하는 이스케이프 시퀀스(escape sequence)로 적습니다.

이스케이프뜻
\n줄 바꿈
\t탭
\\백슬래시
\'작은따옴표
\"큰따옴표
\0값이 0인 널 문자

문자열 리터럴

큰따옴표로 감싼 문자들은 문자열 리터럴이며, 끝에 널 문자 \0이 하나 더 붙은 const char 배열입니다. "Hi"는 H, i, \0의 세 칸이어서 타입이 const char[3]이며, 뒤의 장에서 다룰 std::string 객체와는 다른 것입니다. 문자 리터럴과 같은 접두사 u8, u, U, L로 다른 문자 타입의 문자열을 만들 수 있습니다.

R"(...)" 모양의 원시 문자열 리터럴(raw string literal)은 괄호 안의 내용을 이스케이프로 해석하지 않고 그대로 씁니다. 그래서 백슬래시가 많은 Windows의 경로나 정규 표현식을 적을 때 편합니다. 내용에 )"가 들어 있다면 R"x(...)x"처럼 괄호 바깥에 구분 문자를 더해 끝을 표시합니다.

const char* message = "Hello, C++!";
const char* path = R"(C:\Users\Admin\Documents\my_file.txt)";

path를 출력하면 C:\Users\Admin\Documents\my_file.txt가 그대로 나옵니다. 원시 문자열을 쓰지 않았다면 "C:\\Users\\Admin..."처럼 백슬래시마다 두 번씩 적어야 합니다.

논리 리터럴과 포인터 리터럴

bool 타입의 리터럴은 true와 false 두 가지뿐입니다. nullptr은 C++11에 들어온 포인터 리터럴로, 아무것도 가리키지 않는 널 포인터를 나타내며 타입은 std::nullptr_t입니다. 예전 코드의 NULL이나 0은 정수로도 해석되어 함수를 고를 때 엉뚱한 쪽이 선택될 수 있으므로, C++에서는 널 포인터를 nullptr로 적습니다.

리터럴과 상수를 함께 쓰기

constants_and_literals.cpp
#include <iostream>
#include <string>

int main() {
    const int PROGRAM_VERSION = 2;
    const double DISCOUNT_RATE = 0.15;
    const std::string WELCOME_MESSAGE = "Welcome!";

    int score = 95;
    double tax = score * 0.05;

    std::cout << WELCOME_MESSAGE << "\n";
    std::cout << "version: " << PROGRAM_VERSION << "\n";
    std::cout << "discount: " << DISCOUNT_RATE * 100 << "%\n";
    std::cout << "score: " << score << ", tax: " << tax << "\n";
    std::cout << "quote(\") and backslash(\\)\n";
    std::cout << sizeof('A') << ' ' << sizeof("A") << ' ' << sizeof("Hi") << '\n';
}
Welcome!
version: 2
discount: 15%
score: 95, tax: 4.75
quote(") and backslash(\)
1 2 3

0.15 * 100과 95 * 0.05는 이진 부동 소수점에서 정확한 값이 아니지만, 반올림한 결과가 가장 가까운 double인 15와 4.75가 되어 그대로 출력됩니다. 이스케이프 시퀀스 \"와 \\는 출력에서 큰따옴표와 백슬래시 하나가 됩니다. 마지막 줄의 sizeof('A')는 char 하나의 크기 1이고, "A"와 "Hi"는 끝의 널 문자를 포함해 각각 2칸과 3칸입니다.

정리

  • 값에 이름을 붙이면 뜻이 드러나고, 실수로 바꾸는 코드를 막으며, 바꿀 때 한 곳만 고치면 됩니다.
  • const는 바꿀 수 없는 객체이고 실행 중의 값으로도 초기화할 수 있으며, constexpr은 컴파일할 때 정해지는 상수 표현식으로 초기화해야 합니다.
  • #define은 타입과 이름 범위가 없는 전처리 치환이므로, C++에서는 상수에 const나 constexpr을 씁니다.
  • 정수 리터럴은 접두사로 진법을, 접미사로 타입을 정하며, 0으로 시작하면 팔진수이고 접미사가 없으면 값을 담을 수 있는 첫 타입이 됩니다.
  • 부동 소수점 리터럴은 기본이 double이고, 문자 리터럴은 char이며, 문자열 리터럴은 끝에 널 문자가 붙은 const char 배열입니다.
  • 원시 문자열은 이스케이프를 해석하지 않고, 널 포인터는 nullptr로 적습니다.

연습 문제

1. 같아 보이는 숫자

012와 12는 같은 값인가요? 0b1100과 0xC는 십진수로 얼마인가요?

해설 보기

012는 0으로 시작하는 팔진수여서 십진수 10이므로 12와 다릅니다. 0b1100은 이진수로 8 + 4 = 12이고, 0xC도 십육진수로 12이므로 둘은 같은 값입니다.

2. 빈 문자열의 크기

sizeof("")는 0인가요, 1인가요? sizeof("Hello")는 얼마인가요?

해설 보기

빈 문자열도 끝의 널 문자 한 칸을 가지므로 sizeof("")는 1입니다. "Hello"는 다섯 글자와 널 문자로 6칸이어서 sizeof("Hello")는 6입니다. 문자열의 길이와 배열의 크기는 널 문자 하나만큼 다릅니다.

3. const와 constexpr

다음 코드에서 a와 b를 선언한 두 줄 가운데 컴파일되지 않는 것은 무엇인가요? 컴파일되는 줄의 변수로 int arr[N];처럼 배열의 크기를 정할 수 있나요?

int count = 10;
const int a = count;
constexpr int b = count;
해설 보기

count는 const도 constexpr도 아닌 보통의 변수여서 그 값은 상수 표현식이 아니므로, constexpr int b = count;가 컴파일되지 않습니다. a는 바꿀 수 없을 뿐 컴파일할 때 값이 정해진 상수가 아니므로, 배열의 크기로 쓸 수 없습니다. 배열의 크기에는 constexpr int N = 10;처럼 컴파일할 때 정해지는 상수를 씁니다. 다만 GCC는 함수 안의 int arr[a];를 가변 길이 배열(VLA)이라는 확장 기능으로 받아들이고 -Wall과 -Wextra로도 경고하지 않으므로, 직접 확인할 때는 -pedantic-errors를 더해 표준을 벗어난 코드를 오류로 봅니다. 이 확장은 1차원 배열에서 다시 다룹니다.

다음 글 형변환에서는 한 타입의 값을 다른 타입으로 바꾸는 암시적 변환과 명시적 변환, 그리고 static_cast 같은 C++의 변환 연산자를 다룹니다.