구조체·공용체 초기화(Struct and union initialization)

구조체·공용체 초기화(Struct and union initialization)

구조체나 공용체 타입의 객체를 초기화할 때, 이니셜라이저는 멤버들을 위한 중괄호로 감싸고 쉼표로 구분한 이니셜라이저 목록이어야 해요:

= { expression , ... } (1) (C99 이전)
= { designator(선택) expression , ... } (2) (C99부터)
= { } (3) (C23부터)

여기서 designator는 . member 형태의 개별 멤버 지정자와 [ index ] 형태의 배열 지정자의 시퀀스(공백으로 구분하거나 인접)예요.

명시적으로 초기화되지 않은 모든 멤버는 빈 초기화(empty-initialized)돼요.

출처: cppreference

본문

설명(Explanation)

union을 초기화할 때 이니셜라이저 목록은 멤버 하나만 가져야 하고, 지정 이니셜라이저를 쓰지 않으면(C99부터) union의 첫 번째 멤버를 초기화해요:

union { int x; char c[4]; }
  u = {1},           // makes u.x active with value 1
 u2 = { .c={'\1'} }; // makes u2.c active with value {'\1','\0','\0','\0'}

struct를 초기화할 때 목록의 첫 번째 이니셜라이저는 (지정자를 지정하지 않으면)(C99부터) 첫 번째 선언된 멤버를 초기화하고, 지정자가 없는(C99부터) 이후의 모든 이니셜라이저는 이전 표현식이 초기화한 멤버 뒤에 선언된 struct 멤버들을 초기화해요:

struct point {double x,y,z;} p = {1.2, 1.3}; // p.x=1.2, p.y=1.3, p.z=0.0
div_t answer = {.quot = 2, .rem = -1 };      // order of elements in div_t may vary

지정자(designator)는 뒤따르는 이니셜라이저가 그 지정자가 가리키는 struct 멤버를 초기화하게 해요. 초기화는 그다음 선언된 요소부터, 선언 순서대로 앞으로 계속돼요:

struct {int sec,min,hour,day,mon,year;} z
   = {.day=31,12,2014,.sec=30,15,17}; // initializes z to {30,15,17,31,12,2014}

멤버보다 이니셜라이저를 더 많이 제공하면 오류예요.

중첩 초기화(Nested initialization)

struct나 union의 멤버가 배열, struct, union이면, 중괄호 목록의 해당 이니셜라이저는 그 멤버에 유효한 아무 이니셜라이저든 쓸 수 있는데, 단 중괄호를 다음과 같이 생략할 수 있어요:

중첩 이니셜라이저가 여는 중괄호로 시작하면, 닫는 중괄호까지의 전체 중첩 이니셜라이저가 해당 멤버 객체를 초기화해요. 각각의 여는 중괄호는 새 "현재 객체"를 확립해요. 현재 객체의 멤버는 지정자를 쓰지 않으면(C99부터) 자연 순서로 초기화돼요: 배열 요소는 첨자 순서로, struct 멤버는 선언 순서로, union은 첫 번째 선언된 멤버만. 현재 객체 안에서 닫는 중괄호까지 명시적으로 초기화되지 않은 하위 객체는 빈 초기화돼요.

struct example {
    struct addr_t {
       uint32_t port;
    } addr;
    union {
       uint8_t a8[4];
       uint16_t a16[2];
    } in_u;
};
struct example ex = { // start of initializer list for struct example
                     { // start of initializer list for ex.addr
                        80 // initialized struct's only member
                     }, // end of initializer list for ex.addr
                     { // start of initializer-list for ex.in_u
                        {127,0,0,1} // initializes first element of the union
                     } };

중첩 이니셜라이저가 여는 중괄호로 시작하지 않으면, 목록에서 그 멤버 배열·struct·union의 요소·멤버를 채우기에 충분한 만큼만 이니셜라이저를 가져가고, 남은 이니셜라이저는 다음 struct 멤버를 초기화하는 데 남겨져요:

struct example ex = {80, 127, 0, 0, 1}; // 80 initializes ex.addr.port
                                        // 127 initializes ex.in_u.a8[0]
                                        // 0 initializes ex.in_u.a8[1]
                                        // 0 initializes ex.in_u.a8[2]
                                        // 1 initializes ex.in_u.a8[3]

지정자가 중첩되면, 멤버를 위한 지정자는 감싸는 struct/union/배열의 지정자 뒤를 따르는 형태예요. 어떤 중첩된 중괄호 이니셜라이저 목록 안에서도, 가장 바깥쪽 지정자는 현재 객체를 가리키고 현재 객체 안에서 초기화될 하위 객체만 선택해요.

struct example ex2 = { // current object is ex2, designators are for members of example
                       .in_u.a8[0]=127, 0, 0, 1, .addr=80}; 
struct example ex3 = {80, .in_u={ // changes current object to the union ex.in_u
                           127,
                           .a8[2]=1 // this designator refers to the member of in_u
                      } };

어떤 하위 객체가 명시적으로 두 번 초기화되면(지정자를 쓸 때 일어날 수 있어요), 목록에서 나중에 나온 이니셜라이저가 쓰여요(이전 이니셜라이저는 평가되지 않을 수 있어요):

struct {int n;} s = {printf("a\n"), // this may be printed or skipped
                     .n=printf("b\n")}; // always printed

초기화되지 않은 하위 객체들은 암묵적으로 초기화되지만, 하위 객체의 암묵적 초기화는 이니셜라이저 목록에서 더 일찍 나타났던 같은 하위 객체의 명시적 초기화를 절대 덮어쓰지 않아요:

#include <stdio.h>
typedef struct { int k; int l; int a[2]; } T;
typedef struct { int i;  T t; } S;
T x = {.l = 43, .k = 42, .a[1] = 19, .a[0] = 18 };
 // x initialized to {42, 43, {18, 19} }
int main(void)
{
    S l = { 1,          // initializes l.i to 1
           .t = x,      // initializes l.t to {42, 43, {18, 19} }
           .t.l = 41,   // changes l.t to {42, 41, {18, 19} }
           .t.a[1] = 17 // changes l.t to {42, 41, {18, 17} }
          };
    printf("l.t.k is %d\n", l.t.k); // .t = x sets l.t.k to 42 explicitly
                                    // .t.l = 41 would zero out l.t.k implicitly
}

출력:

l.t.k is 42

l.t.k가 42로 유지되는 이유가 바로 위 규칙이에요. .t = xl.t.k를 42로 명시적으로 설정한 뒤, .t.l = 41l.t 전체를 다시 할당하면서 암묵적으로 l.t.k를 0으로 만들 수도 있지만, 명시적 초기화가 암묵적 초기화를 덮어쓰지 않는 규칙 때문에 42가 보존돼요.

그러나 이니셜라이저가 왼쪽 여는 중괄호로 시작하면, 그 현재 객체는 완전히 다시 초기화되고 이전의 명시적 이니셜라이저는 무시돼요:

struct fred { char s[4]; int n; };
struct fred x[ ] = { { { "abc" }, 1 }, // inits x[0] to { {'a','b','c','\0'}, 1 }
                      [0].s[0] = 'q'   // changes x[0] to { {'q','b','c','\0'}, 1 }
                   };
struct fred y[ ] = { { { "abc" }, 1 }, // inits y[0] to { {'a','b','c','\0'}, 1 }
                     [0] = { // current object is now the entire y[0] object
                             .s[0] = 'q' 
                            } // replaces y[0] with { {'q','\0','\0','\0'}, 0 }
                    };

주의할 점(Notes)

이니셜라이저 목록은 뒤에 쉼표를 붙일 수 있고, 이 쉼표는 무시돼요:

struct {double x,y;} p = {1.0,
                         2.0, // trailing comma OK
                         };

C에서는 중괄호로 감싼 이니셜라이저 목록이 비어 있을 수 없어요(C++은 빈 목록을 허용하고, C의 struct는 비어 있을 수 없다는 점에 유의하세요). (C23 이전)

C에서도 C++처럼 이니셜라이저 목록이 비어 있을 수 있어요. (C23부터)

struct {int n;} s = {0}; // OK
struct {int n;} s = {}; // Error until C23: initializer-list cannot be empty
                        // OK since C23: s.n is initialized to 0
struct {} s = {}; // Error: struct cannot be empty

이니셜라이저 목록의 모든 표현식은 임의의 저장 기간의 집합체(aggregate)를 초기화할 때 상수 표현식이어야 했어요. (C99 이전)

다른 모든 초기화와 마찬가지로, 정적 또는 스레드 로컬(thread-local)(C11부터) 저장 기간의 집합체를 초기화할 때는 이니셜라이저 목록의 모든 표현식이 상수 표현식이어야 해요:

static struct {char* p} s = {malloc(1)}; // error

이니셜라이저 안의 부분 표현식의 평가 순서는 무한정 시퀀싱(indeterminately sequenced)돼요(C++11부터의 C++과는 달라요):

int n = 1;
struct {int x,y;} p = {n++, n++}; // unspecified, but well-defined behavior:
                                  // n is incremented twice in arbitrary order
                                  // p equal {1,2} and {2,1} are both valid

예제(Example)

여기서는 멤버 순서가 명시되지 않은 struct를 다룰 때 지정 이니셜라이저가 얼마나 편한지 보여줘요.

#include <stdio.h>
#include <time.h>

int main(void)
{
    char buff[70];
    // designated initializers simplify the use of structs whose
    // order of members is unspecified
    struct tm my_time = { .tm_year=2012-1900, .tm_mon=9, .tm_mday=9,
                          .tm_hour=8, .tm_min=10, .tm_sec=20 };
    strftime(buff, sizeof buff, "%A %c", &my_time);
    puts(buff);
}

가능한 출력:

Sunday Sun Oct  9 08:10:20 2012

struct tm의 멤버 순서가 표준마다 달라질 수 있는데, 지정 이니셜라이저를 쓰면 멤버 순서와 무관하게 이름으로 초기화할 수 있어요.

더 알아보기