문자열 리터럴(String literals)

문자열 리터럴(String literals)

소스 코드에 문자열을 그대로 박아 넣고 싶을 때 쓰는 게 문자열 리터럴이에요. 큰따옴표로 감싼 "hello" 같은 게 그 리터럴이고, 컴파일러가 지정된 문자 배열 타입의 이름 없는 객체를 그 자리에 만들어줘요.

출처: cppreference

본문

문법

" s-char-sequence " (1)
u8" s-char-sequence " (2) (C11부터)
u" s-char-sequence " (3) (C11부터)
U" s-char-sequence " (4) (C11부터)
L" s-char-sequence " (5)

여기서

항목 의미
s-char-sequence - 0개 이상의 문자. 각 문자는 소스 문자 집합의 멀티바이트 문자(", \, 개행 제외)이거나, 이스케이프 시퀀스에서 정의된 문자 이스케이프, 16진 이스케이프, 8진 이스케이프, 또는 유니버설 문자 이름(C99부터)
  1. 문자 문자열 리터럴(character string literal): 리터럴의 타입은 char[N]인데, N은 널 종료자를 포함한 실행 좁은 인코딩(execution narrow encoding)의 코드 단위로 측정한 문자열의 크기예요. 배열의 각 char 요소는 s-char-sequence의 다음 문자에서 실행 문자 집합을 사용해 초기화돼요.
  2. UTF-8 문자열 리터럴: 리터럴의 타입은 char[N](C23 이전)char8_t[N](C23부터)이고, N은 널 종료자를 포함한 UTF-8 코드 단위로 측정한 문자열의 크기예요. 배열의 각 char(C23 이전)char8_t(C23부터) 요소는 s-char-sequence의 다음 멀티바이트 문자에서 UTF-8 인코딩을 사용해 초기화돼요.
  3. 16비트 와이드 문자열 리터럴: 리터럴의 타입은 char16_t[N]이고, N은 널 종료자를 포함한 구현이 정의한 16비트 인코딩(보통 UTF-16)의 코드 단위로 측정한 문자열의 크기예요. 배열의 각 char16_t 요소는 구현이 정의한 로케일에서 mbrtoc16을 실행한 것처럼 초기화돼요.
  4. 32비트 와이드 문자열 리터럴: 리터럴의 타입은 char32_t[N]이고, N은 널 종료자를 포함한 구현이 정의한 32비트 인코딩(보통 UTF-32)의 코드 단위로 측정한 문자열의 크기예요. 배열의 각 char32_t 요소는 구현이 정의한 로케일에서 mbrtoc32를 실행한 것처럼 초기화돼요. (C23 이전)

(C23부터는 3)·4)가 각각 UTF-16, UTF-32 문자열 리터럴을 의미하고, 요소는 UTF-16/UTF-32 인코딩을 사용해 초기화돼요.)

  1. 와이드 문자열 리터럴(wide string literal): 리터럴의 타입은 wchar_t[N]이고, N은 널 종료자를 포함한 실행 와이드 인코딩의 코드 단위로 측정한 문자열의 크기예요. 배열의 각 wchar_t 요소는 구현이 정의한 로케일에서 mbstowcs를 실행한 것처럼 초기화돼요.

설명(Explanation)

먼저, 번역 6단계에서(매크로 확장 후) 인접한 문자열 리터럴(즉 공백으로만 분리된 문자열 리터럴)은 연결돼요.

좁은 문자열 리터럴 두 개, 또는 와이드 문자열 리터럴 두 개만 연결할 수 있었어요. (C99 이전)

한쪽 리터럴에 접두사가 없으면 결과 문자열 리터럴은 접두사가 붙은 리터럴이 지정하는 폭/인코딩을 가져요.

L"Δx = %" PRId16 // at phase 4, PRId16 expands to "d"
                 // at phase 6, L"Δx = %" and "d" form L"Δx = %d"

두 문자열 리터럴이 서로 다른 인코딩 접두사를 가지면 연결은 구현이 정의(implementation-defined)예요. 단, UTF-8 문자열 리터럴과 와이드 문자열 리터럴은 연결할 수 없어요. (C11부터)(C23 이전)

두 문자열 리터럴이 서로 다른 인코딩 접두사를 가지면 연결은 ill-formed예요. (C23부터)

둘째로, 번역 7단계에서 각 문자열 리터럴에 종료 널 문자가 추가되고, 각 리터럴은 문자열 리터럴의 내용에 널 종료자 하나를 더한 것을 담기에 충분한 길이의, 정적 저장 기간을 가진 이름 없는 배열을 초기화해요.

char* p = "\x12" "3"; // creates a static char[3] array holding {'\x12', '3', '\0'}
                      // sets p to point to the first element of the array

문자열 리터럴은 수정할 수 없어요(실제로 .rodata 같은 읽기 전용 메모리에 놓일 수도 있어요). 프로그램이 문자열 리터럴로 형성된 정적 배열을 수정하려 하면 동작은 정의되지 않아요.

char* p = "Hello";
p[1] = 'M'; // Undefined behavior
char a[] = "Hello";
a[1] = 'M'; // OK: a is not a string literal

같은 문자열 리터럴이 메모리의 같은 위치를 가리킬 필요도, 그럴 수 없다는 것도 아니에요. 게다가 겹치는 문자열 리터럴이나 다른 문자열 리터럴의 부분 문자열인 문자열 리터럴은 결합될 수도 있어요.

"def" == 3+"abcdef"; // may be 1 or 0, implementation-defined

주의할 점(Notes)

문자열 리터럴은 반드시 문자열은 아니에요. 문자열 리터럴에 내장 널 문자가 있으면, 하나보다 많은 문자열을 포함하는 배열을 나타내요:

char* p = "abc\0def"; // strlen(p) == 3, but the array has size 8

문자열 리터럴 안에서 16진 이스케이프 뒤에 유효한 16진 숫자가 오면 잘못된 이스케이프 시퀀스로 컴파일이 실패할 수 있는데, 문자열 연결을 우회책으로 쓸 수 있어요:

//char* p = "\xfff"; // error: hex escape sequence out of range
char* p = "\xff""f"; // okay, the literal is char[3] holding {'\xff', 'f', '\0'}

문자열 리터럴은 배열을 초기화하는 데 쓸 수 있고, 배열의 크기가 문자열 리터럴의 크기보다 1 작으면 널 종료자는 무시돼요:

char a1[] = "abc"; // a1 is char[4] holding {'a', 'b', 'c', '\0'}
char a2[4] = "abc"; // a2 is char[4] holding {'a', 'b', 'c', '\0'}
char a3[3] = "abc"; // a3 is char[3] holding {'a', 'b', 'c'}

문자 문자열 리터럴 (1)과 와이드 문자열 리터럴 (5)의 인코딩은 구현이 정의예요. 예를 들어 gcc는 명령줄 옵션 -fexec-charset-fwide-exec-charset으로 이것들을 선택해요.

C11에서는 혼합된 와이드 문자열 리터럴 연결이 허용됐지만, 거의 모든 컴파일러가 그런 연결을 거부해요(알려진 유일한 예외는 SDCC) 그 사용 경험도 알려지지 않았어요. 그래서 혼합 와이드 문자열 리터럴 연결 허용은 C23에서 제거됐어요.

예제(Example)

문자열 리터럴의 여러 종류(좁은 문자열, UTF-8, UTF-16, UTF-32, 와이드)가 각각 어떤 타입과 바이트 배열로 만들어지는지 확인해 볼게요.

#include <inttypes.h>
#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <uchar.h>

int main(void)
{
    char s1[] = "a猫🍌"; // or "a\u732B\U0001F34C"
#if __STDC_VERSION__ >= 202311L
    char8_t
#else
    char
#endif
    s2[] = u8"a猫🍌";
    char16_t s3[] = u"a猫🍌";
    char32_t s4[] = U"a猫🍌";
    wchar_t s5[] = L"a猫🍌";
 
    setlocale(LC_ALL, "en_US.utf8");
    printf("  \"%s\" is a char[%zu] holding     { ", s1, sizeof s1 / sizeof *s1);
    for(size_t n = 0; n < sizeof s1 / sizeof *s1; ++n)
        printf("0x%02X ", +(unsigned char)s1[n]);
    puts("}");
    printf(
#if __STDC_VERSION__ >= 202311L
    "u8\"%s\" is a char8_t[%zu] holding  { "
#else
    "u8\"%s\" is a char[%zu] holding     { "
#endif
, s2, sizeof s2 / sizeof *s2);
    for(size_t n = 0; n < sizeof s2 / sizeof *s2; ++n)
#if __STDC_VERSION__ >= 202311L
       printf("0x%02X ", s2[n]);
#else
       printf("0x%02X ", +(unsigned char)s2[n]);
#endif
    puts("}");
    printf(" u\"a猫🍌\" is a char16_t[%zu] holding { ", sizeof s3 / sizeof *s3);
    for(size_t n = 0; n < sizeof s3 / sizeof *s3; ++n)
       printf("0x%04" PRIXLEAST16" ", s3[n]);
    puts("}");
    printf(" U\"a猫🍌\" is a char32_t[%zu] holding { ", sizeof s4 / sizeof *s4);
    for(size_t n = 0; n < sizeof s4 / sizeof *s4; ++n)
       printf("0x%08" PRIXLEAST32" ", s4[n]);
    puts("}");
    printf(" L\"%ls\" is a wchar_t[%zu] holding  { ", s5, sizeof s5 / sizeof *s5);
    for(size_t n = 0; n < sizeof s5 / sizeof *s5; ++n)
       printf("0x%08X ", (unsigned)s5[n]);
    puts("}");
}

가능한 출력:

  "a猫🍌" is a char[9] holding     { 0x61 0xE7 0x8C 0xAB 0xF0 0x9F 0x8D 0x8C 0x00 }
u8"a猫🍌" is a char[9] holding     { 0x61 0xE7 0x8C 0xAB 0xF0 0x9F 0x8D 0x8C 0x00 }
 u"a猫🍌" is a char16_t[5] holding { 0x0061 0x732B 0xD83C 0xDF4C 0x0000 }
 U"a猫🍌" is a char32_t[4] holding { 0x00000061 0x0000732B 0x0001F34C 0x00000000 }
 L"a猫🍌" is a wchar_t[4] holding  { 0x00000061 0x0000732B 0x0001F34C 0x00000000 }

같은 "a猫🍌" 문자열이라도 리터럴 종류에 따라 배열 타입과 요소 수가 달라져요. 좁은 문자열/UTF-8은 UTF-8의 코드 단위(바이트) 기준이라 9개(0x61, 猫가 3바이트, 🍌가 4바이트, 널)이고, UTF-16은 서로게이트 쌍 때문에 5개, UTF-32와 wchar_t는 코드 포인트당 1개라 4개가 되죠.

더 알아보기