복합 리터럴
복합 리터럴 (Compound literal)
구조체나 배열처럼 다루기 번거로운 타입의 객체를 그 자리에서 바로 만들고 싶을 때가 있어요. 복합 리터럴(compound literal) 은 지정한 타입의 이름 없는 객체를 그 자리에서 생성해 주는 문법이에요. 이 페이지에서 복합 리터럴이 어떻게 생겼고, 어떤 특성을 가지는지 살펴볼게요.
출처: cppreference
본문
구문 (Syntax)
( storage-class-specifiers (optional)(since C23) type ) { initializer-list } (since C99)
( storage-class-specifiers (optional)(since C23) type ) { initializer-list , } (since C99)
( storage-class-specifiers (optional) type ) { } (since C23)
여기서
| 항목 | 설명 |
|---|---|
storage-class-specifiers |
(since C23) constexpr, static, register, thread_local만 담을 수 있는 저장 클래스 지정자 목록 |
type |
완전 객체 타입(complete object type)이나 크기를 알 수 없는 배열을 지정하는 타입 이름. 단 VLA는 아님 |
initializer-list |
type 타입의 객체 초기화에 적합한 초기화 목록 |
설명 (Explanation)
복합 리터럴 표현식은 type이 지정하는 타입의 이름 없는 객체를 만들고, initializer-list가 지정한 대로 초기화해요. 지정 초기화(designated initializers)도 허용돼요.
복합 리터럴의 타입은 지정한 type이 돼요. 다만 type이 크기를 알 수 없는 배열이라면 그 크기는 배열 초기화에서처럼 초기화 목록으로부터 추론돼요.
복합 리터럴의 값 범주(value category)는 lvalue예요. 그래서 그 주소를 취할 수 있어요.
| 복합 리터럴이 평가되는 이름 없는 객체는 파일 스코프에서 나타나면 정적 저장 기간(static storage duration)을, 블록 스코프에서 나타나면 자동 저장 기간(automatic storage duration)을 가져요(이 경우 객체의 수명은 둘러싸는 블록의 끝에서 끝나요). | (until C23) |
복합 리터럴이 함수 본문 밖과 어떤 매개변수 목록 밖에서 평가되면 파일 스코프와 연관돼요. 그 외에는 둘러싸는 블록과 연관돼요. 이 연관에 따라 저장 클래스 지정자(빈 목록일 수 있음), 타입 이름, 초기화 목록은 각각 파일 스코프나 블록 스코프에서 아래 형태의 객체 정의에 유효한 지정자여야 해요: storage-class-specifiers typeof( type ) ID = { initializer-list } ; 여기서 ID는 전체 프로그램에서 고유한 식별자예요. 복합 리터럴은 그 값·타입·저장 기간·다른 속성이 위 정의 문법으로 준 것과 같은 이름 없는 객체를 제공해요. 저장 기간이 자동이면 그 이름 없는 객체 인스턴스의 수명은 둘러싸는 블록의 현재 실행이에요. 저장 클래스 지정자에 constexpr, static, register, thread_local 외의 지정자가 있으면 동작은 정의되지 않아요. |
(since C23) |
주의 (Notes)
const 한정 타입의 복합 리터럴은 저장 공간을 공유할 수 있어요. const 한정 문자나 와이드 문자 배열 타입의 복합 리터럴은 문자열 리터럴과 저장 공간을 공유할 수 있어요.
&(const int){7} == &(const int){7} // 1일 수도 0일 수도 있음, 불특정(unspecified)
(const char []){"abc"} == "abc" // 1일 수도 0일 수도 있음, 불특정
각 복합 리터럴은 그 스코프에서 단 하나의 객체만 만들어요:
#include <assert.h>
int main(void)
{
struct S
{
int i;
}
*p = 0, *q;
int j = 0;
again:
q = p,
p = &((struct S){ j++ }); // 타입 S의 이름 없는 객체를 만들고,
// j가 예전에 보유하던 값으로 초기화한 뒤
// 이 이름 없는 객체의 주소를 포인터 p에 대입
if (j < 2)
goto again; // 주의: 루프를 썼다면 여기서 스코프가 끝나,
// 복합 리터럴의 수명도 끝나 p가 댕글링 포인터가 됨
assert(p == q && q->i == 1);
}
복합 리터럴은 이름이 없기 때문에 자기 자신을 참조할 수 없어요(이름 있는 구조체는 자기 자신을 가리키는 포인터를 포함할 수 있어요).
복합 리터럴의 문법은 캐스트(cast)와 비슷해 보이지만 중요한 차이가 있어요. 캐스트는 비-lvalue 표현식인 반면, 복합 리터럴은 lvalue라는 점이에요.
예제 (Example)
#include <stdio.h>
int *p = (int[]){2, 4}; // 타입 int[2]의 이름 없는 정적 배열을 만들고
// 배열을 {2, 4} 값으로 초기화
// 배열의 첫 요소를 가리키는 포인터 p를 만듦
const float *pc = (const float []){1e0, 1e1, 1e2}; // 읽기 전용 복합 리터럴
struct point {double x,y;};
int main(void)
{
int n = 2, *p = &n;
p = (int [2]){*p}; // 타입 int[2]의 이름 없는 자동 배열을 만들고
// 첫 요소를 *p가 예전에 보유하던 값으로 초기화
// 두 번째 요소를 0으로 초기화
// 첫 요소의 주소를 p에 저장
void drawline1(struct point from, struct point to);
void drawline2(struct point *from, struct point *to);
drawline1(
(struct point){.x=1, .y=1}, // 블록 스코프의 구조체 두 개를 만들어
(struct point){.x=3, .y=4}); // 값으로 전달하며 drawline1을 호출
drawline2(
&(struct point){.x=1, .y=1}, // 블록 스코프의 구조체 두 개를 만들어
&(struct point){.x=3, .y=4}); // 그 주소를 전달하며 drawline2를 호출
}
void drawline1(struct point from, struct point to)
{
printf("drawline1: `from` @ %p {%.2f, %.2f}, `to` @ %p {%.2f, %.2f}\n",
(void*)&from, from.x, from.y, (void*)&to, to.x, to.y);
}
void drawline2(struct point *from, struct point *to)
{
printf("drawline2: `from` @ %p {%.2f, %.2f}, `to` @ %p {%.2f, %.2f}\n",
(void*)from, from->x, from->y, (void*)to, to->x, to->y);
}
가능한 출력:
drawline1: `from` @ 0x7ffd24facea0 {1.00, 1.00}, `to` @ 0x7ffd24face90 {3.00, 4.00}
drawline2: `from` @ 0x7ffd24facec0 {1.00, 1.00}, `to` @ 0x7ffd24faced0 {3.00, 4.00}
더 알아보기
- 배열 초기화 (array initialization) — 배열의 크기 추론 규칙
- 캐스트 연산자 (cast operators) — 복합 리터럴과의 차이
- 지정 초기화 (designated initializers) — 복합 리터럴에서 쓰는 초기화 문법