본문 바로가기

상수와 리터럴

const 상수와 정수·실수·문자·문자열 리터럴의 차이를 이해하고 값의 의미와 타입을 코드에 명확히 표현합니다.

이번 장에서는 이렇게 변하지 않는 값들을 좀 더 심도 있게 다루겠습니다.

바로 상수(Constants)와 리터럴(Literals)입니다.

상수와 리터럴은 프로그램 내에서 고정된 값을 표현하는 방식입니다.

이들을 정확히 이해하는 것은 코드의 가독성과 안정성을 높이는 데 매우 중요합니다.


상수(Constants)의 이해

상수는 이름, 타입, 범위를 함께 보며 값이 바뀌지 않아야 하는 의도를 확인합니다.

우리는 이미 const 키워드를 사용하여 상수를 선언하는 방법을 간략하게 살펴보았습니다.

이 절의 const 상수는 이름과 타입을 가진 객체이며, 초기화한 뒤 그 객체에 새 값을 대입할 수 없습니다. 초깃값을 실행 중 다른 변수나 함수 결과에서 얻는 경우도 있으므로, const라는 이유만으로 모든 값이 컴파일 시점에 결정된다고 가정하지 않습니다.

왜 상수를 사용하는가?

상수를 사용하는 주된 이유는 다음과 같습니다.

의미 부여 및 가독성 향상: 3.141592라는 숫자만 보면 그것이 원주율인지, 다른 어떤 값인지 알기 어렵습니다.

하지만 const double PI = 3.141592;라고 선언하면 PI라는 이름 자체가 그 숫자의 의미를 명확하게 전달하여 코드의 가독성을 높입니다.

오류 방지: 실수로 값을 변경하려는 시도를 컴파일 시점에서 방지할 수 있습니다.

예를 들어, 원주율 PI의 값을 실수로 변경하려 한다면 컴파일러가 오류를 발생시켜 알려줄 것입니다.

이는 프로그램의 안정성을 높이는 데 기여합니다.

유지보수 용이성: 만약 프로그램의 여러 곳에서 동일한 고정 값을 사용하고 있다면, 이 값을 상수로 선언해두고 사용하는 것이 좋습니다.

나중에 이 값을 변경해야 할 때, 상수의 정의만 한 번 변경하면 모든 사용처에 자동으로 반영되므로 유지보수가 매우 편리해집니다.

상수를 선언하는 방법
  • const 키워드: 가장 일반적이고 권장되는 방법입니다.

    변수 선언 시 타입 앞에 const를 붙여 상수로 만듭니다.

    아래 기본 타입 const 객체는 정의할 때 초깃값이 필요합니다.

    const 상수 선언 예시
    const double PI = 3.1415926535; // 원주율
    const int MAX_USERS = 100;     // 최대 사용자 수
    const char NEWLINE = '\n';     // 개행 문자

    const를 사용하면 컴파일러가 해당 값이 변경되지 않음을 알기 때문에 최적화를 수행하는 데도 도움이 될 수 있습니다.

  • #define 전처리기 지시자 (레거시 방식): #define은 C++보다는 C 언어에서 상수를 정의할 때 많이 사용되던 방식입니다.

    이는 전처리 단계에서 매크로 이름에 해당하는 토큰을 치환합니다. 같은 글자가 든 문자열 안까지 찾아 바꾸는 문자열 검색은 아닙니다.

    #define 매크로 예시
    #define PI_MACRO 3.1415926535 // 세미콜론을 붙이지 않습니다!
    #define MAX_BUFFER_SIZE 1024
    const와 #define의 차이점
    • 타입 안전성: const는 타입 정보를 가지므로 컴파일러가 타입 체크를 수행하여 오류를 방지할 수 있습니다. 매크로 이름 자체에는 C++ 타입이 없지만, 치환된 표현식은 이후 컴파일에서 타입 검사를 받습니다.
    • 디버깅: const 상수는 디버거에서 심볼 정보로 확인 가능하지만, #define 매크로는 컴파일 전에 사라지므로 디버깅이 어렵습니다.
    • 스코프: const 상수는 변수와 동일하게 스코프(유효 범위)를 가질 수 있어 특정 블록이나 함수 내에서만 유효하게 만들 수 있습니다. 매크로는 C++ 블록 범위를 따르지 않고 #undef되거나 전처리 번역 단위가 끝날 때까지 적용됩니다.
    • 메모리 사용: const 상수는 실제 메모리 공간을 차지할 수 있지만, 컴파일러 최적화에 따라 레지스터에 저장되거나 직접 값이 삽입되어 메모리를 차지하지 않을 수도 있습니다. 매크로 이름 자체는 객체가 아니지만, 치환 결과가 생성하는 객체나 데이터는 메모리를 사용할 수 있습니다. 이러한 이유로 현대 C++에서는 #define을 사용하여 상수를 정의하는 대신 const 키워드를 사용하는 것을 강력히 권장합니다.
  • enum (열거형): enum은 주로 관련된 정수 값들을 의미 있는 이름으로 묶을 때 사용됩니다.

    상수의 집합을 정의하는 데 유용합니다.

    enum 예시
    enum Color { RED, GREEN, BLUE }; // RED는 0, GREEN은 1, BLUE는 2의 값을 가짐 (기본적으로 0부터 시작)
    enum State { IDLE = 0, RUNNING = 1, PAUSED = 2, STOPPED = 3 }; // 특정 값 지정 가능
    // 사용 예시
    Color myColor = RED;
    if (myColor == RED) {
        // ...
    }

    enum에 대해서는 나중에 더 자세히 다룰 기회가 있을 것입니다.


리터럴(Literals)의 이해

리터럴은 소스 코드에 직접적으로 표현되는 고정된 값 자체를 의미합니다.

소스에 10이나 "Hi"처럼 직접 쓴 표기입니다. 이 표기는 변수의 초깃값으로도 사용할 수 있습니다. int score = 10;에서는 score가 객체의 이름이고 10이 리터럴입니다.

리터럴은 특정 데이터 타입을 가집니다.

리터럴의 종류
정수 리터럴 (Integer Literals)
  • 십진수: 우리가 가장 흔히 사용하는 형태입니다. (예: 10, 123, 5). -5는 정수 리터럴 5에 단항 - 연산자를 적용한 식입니다
  • 이진수 (Binary): C++14부터 0b 또는 0B 접두사를 사용하여 이진수를 표현할 수 있습니다. (예: 0b1010은 십진수 10)
  • 팔진수 (Octal): 0 접두사를 사용하여 팔진수를 표현합니다. (예: 012는 십진수 10)
  • 십육진수 (Hexadecimal): 0x 또는 0X 접두사를 사용하여 십육진수를 표현합니다. (예: 0xA는 십진수 10)

타입 접미사: 정수 리터럴 뒤에 특정 접미사를 붙여 타입을 명시할 수 있습니다.

  • U 또는 u: unsigned (부호 없는) 정수. (예: 100U)
  • L 또는 l: long 정수. (예: 100L)
  • LL 또는 ll: long long 정수. (예: 100LL)
  • UL, ull 등 조합도 가능합니다.
정수 리터럴 예시
int dec = 10;
int bin = 0b1010; // C++14
int oct = 012;
int hex = 0xA;

long long veryBig = 123456789012345LL;
unsigned int flags = 0xFFU; // 십육진수 255 (unsigned int)

부동 소수점 리터럴 (Floating-Point Literals): 소수점이나 지수 표기(e 또는 E)를 포함한 숫자입니다.

기본적으로 double 타입으로 간주됩니다.

타입 접미사
  • F 또는 f: float 타입. (예: 3.14F)
  • L 또는 l: long double 타입. (예: 3.14L)
부동 소수점 리터럴 예시
double dval = 3.14;
float fval = 3.14F;
long double ldval = 3.14L;

double sci_notation = 6.02e23; // 6.02 * 10^23

문자 리터럴 (Character Literals) 작은따옴표('')로 묶인 단일 문자입니다.

기본적으로 char 타입입니다.

접미사/접두사
  • L: wchar_t (와이드 문자). (예: L'가')
  • u: char16_t (UTF-16 문자). (예: u'글')
  • U: char32_t (UTF-32 문자). (예: U'자')

이스케이프 시퀀스 (Escape Sequences): 특정 특수 문자는 백슬래시(\)와 함께 조합하여 표현합니다.

  • \n: 줄 바꿈 (개행)
  • \t: 탭
  • \\: 백슬래시 자체
  • \': 작은따옴표
  • \": 큰따옴표
  • \0: 널 문자 (문자열의 끝을 나타내는 데 사용)
문자 리터럴 예시
char ch = 'A';
char newline = '\n'; // 줄 바꿈 문자
char backslash = '\\'; // 백슬래시 문자

문자열 리터럴 (String Literals) 큰따옴표("")로 묶인 일련의 문자 시퀀스입니다.

일반 문자열 리터럴 "Hi"는 끝의 널 문자를 포함한 const char[3] 배열입니다. H, i, \0의 세 코드 단위를 담으며, std::string 객체와는 별개입니다.

접두사
  • L: wchar_t 문자열. (예: L"안녕하세요")
  • u: char16_t 문자열. (예: u"Hi")
  • U: char32_t 문자열. (예: U"Hello")
  • R: 원시 문자열 (Raw String Literal). 이스케이프 시퀀스를 해석하지 않고 문자 그대로를 사용합니다. 파일 경로 등을 표현할 때 유용합니다. (예: R"(C:\new\folder)"는 C:\new\folder 그대로 해석)
문자열 리터럴 예시
const char* message = "Hello, C++!"; // C-스타일 문자열
std::string name = "나 혼자 C++"; // C++ 표준 라이브러리의 string 타입

// 원시 문자열 리터럴
const char* path = R"(C:\Users\Admin\Documents\my_file.txt)";
std::cout << path << std::endl; // 출력: C:\Users\Admin\Documents\my_file.txt

std::string은 C++ 표준 라이브러리에서 제공하는 문자열 클래스로, C-스타일 문자열보다 유연하고 안전한 문자열 처리를 제공합니다.

이에 대해서는 나중에 별도의 장에서 자세히 다룰 것입니다.

논리 리터럴 (Boolean Literals) 논리 값을 직접 표현하는 리터럴은 두 가지뿐입니다.

  • true: 참
  • false: 거짓
논리 리터럴 예시
bool isComplete = true;
bool isValid = false;
포인터 리터럴 (Pointer Literal)
  • nullptr: C++11부터 도입된 포인터 리터럴입니다. 아무것도 가리키지 않는 널 포인터를 명시적으로 나타냅니다. C에서는 NULL이나 0을 사용하기도 했지만, nullptr은 타입 안전성을 제공하여 더 바람직합니다.
포인터 리터럴 예시
int* ptr = nullptr; // 아무것도 가리키지 않는 정수형 포인터

리터럴 상수 (Literal Constants)

리터럴은 그 자체로 상수입니다.

10, 3.14, 'A', "Hello"와 같은 값들은 프로그램 내에서 고정된 값을 가지며 변경할 수 없습니다.

이들은 이름이 없는 상수라고 볼 수 있습니다.

우리가 변수를 선언하고 const 키워드로 상수를 만드는 것은 이렇게 이름 없는 리터럴에 의미 있는 이름을 부여하여 코드의 가독성과 유지보수성을 높이기 위함입니다.

상수와 리터럴 예시
#include <iostream>
#include <string> // std::string을 사용하기 위해 포함

int main() {
    // 상수 선언 (이름 있는 상수)
    const int PROGRAM_VERSION = 2;
    const double DISCOUNT_RATE = 0.15; // 15% 할인
    const std::string WELCOME_MESSAGE = "환영합니다!";

    // 리터럴 직접 사용 (이름 없는 상수)
    int score = 95; // 95는 정수 리터럴
    double tax = score * 0.05; // 0.05는 부동 소수점 리터럴

    std::cout << WELCOME_MESSAGE << std::endl;
    std::cout << "프로그램 버전: " << PROGRAM_VERSION << std::endl;
    std::cout << "할인율: " << DISCOUNT_RATE * 100 << "%" << std::endl;
    std::cout << "원래 점수: " << score << std::endl;
    std::cout << "세금: " << tax << std::endl;

    // 문자 리터럴과 이스케이프 시퀀스
    char tabChar = '\t';
    std::cout << "이것은" << tabChar << "탭 문자입니다." << std::endl;
    std::cout << "큰따옴표(\")와 백슬래시(\\)를 사용할 수 있습니다." << std::endl;

    return 0;
}

앞에서 다룬 const, 매크로, 열거형의 이름·타입·범위 차이를 비교합니다.

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++의 타입·범위를 가진 객체처럼 취급하지 않습니다.

다음 그림은 리터럴 중 정수, 부동소수점, 문자·문자열, nullptr의 표기 기준을 묶습니다. 논리 리터럴 true와 false는 앞의 예제처럼 bool 값을 나타냅니다.

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와 정수의 진법·접미사·범위를 함께 확인합니다.

예시 같은 글자가 보여도 문자와 문자열은 크기와 타입이 다릅니다. 다음 완전한 프로그램은 문자열 리터럴의 끝 문자까지 포함해 크기를 확인합니다.

문자 하나와 문자열 배열 크기 확인
#include <iostream>

int main() {
    std::cout << sizeof('A') << ' ' << sizeof("A") << ' '
              << sizeof("Hi") << '\n';
}

출력은 1 2 3입니다. C++의 'A'는 char 한 칸이고, "A"와 "Hi"는 끝의 \0까지 포함한 배열입니다. 여기서 크기의 단위는 C++ 바이트입니다. 출력되는 글자의 개수와 저장 공간의 크기를 같은 값으로 읽지 마세요. 이 예의 기본 문자와 달리 한글 등은 인코딩에 따라 여러 코드 단위를 사용할 수 있습니다.

표기를 먼저 판단한다. 012와 12의 값은 같은가? sizeof("")는 0인가 1인가? 3.14F를 double 변수에 넣으면 처음부터 double 리터럴을 사용한 것과 항상 같은 정밀도인가? 앞의 접두사·접미사·끝 문자를 근거로 답한 뒤 확인합니다.

표기별 근거 확인

012는 8진수이므로 십진수 10이고 12와 다릅니다. 빈 문자열도 끝의 널 문자 한 칸이 있어 크기는 1입니다. 3.14F는 먼저 float 값이 되므로 이후 double에 저장해도 그때 잃은 정밀도가 복원되지 않습니다. 실제 저장 표현은 구현에 따라 확인하지만 접미사가 정한 최초 타입은 구분해야 합니다.

문자열 배열과 끝 문자, raw 표기의 규칙은 C++ 작업 초안의 문자열 리터럴 규칙을 참고할 수 있습니다. 이 절은 현재 예시에 필요한 규칙만 사용하며 모든 리터럴 접미사나 인코딩 조합을 다루지는 않습니다.