초기화
초기화 (Initialization)
변수의 초기화(initialization) 는 그 변수가 만들어질 때(construction) 초기 값을 부여해 주는 일이에요. 초기 값은 선언자(declarator)의 이니셜라이저 부분이나 new 표현식에서 제공돼요. 또 함수 호출 중에도 일어나요 — 함수 매개변수와 함수 반환 값도 초기화되거든요.
출처: cppreference
본문
이니셜라이저 (Initializers)
각 선언자에 대해 이니셜라이저(있으면)는 다음 중 하나예요.
| 형태 | 종류 |
|---|---|
= expression |
(1) |
= {}, = { initializer-list }, = { designated-initializer-list } |
(2) |
( expression-list ), ( initializer-list ) |
(3) |
{}, { initializer-list }, { designated-initializer-list } |
(4) (C++11부터) |
- (1) 복사 초기화(copy-initialization) 문법
- (2) 집계 초기화(aggregate initialization) 문법(C++11까지) / 리스트 초기화(list-initialization) 문법(C++11부터)
- (3) 직접 초기화(direct-initialization) 문법
- (4) 리스트 초기화 문법
initializer-list는 이니셜라이저 절(initializer clause)들의 쉼표로 구분된 목록이에요. designated-initializer-list는 지정된 이니셜라이저 절들의 목록이고요. 이니셜라이저 절은 expression(1), {}(2), { initializer-list }(3), { designated-initializer-list }(4, C++20부터) 중 하나예요. (2)~(4) 문법을 통틀어 중괄호로 감싼 이니셜라이저 목록(brace-enclosed initializer list) 이라고 불러요.
이니셜라이저 의미론 (Initializer semantics)
- 객체에 이니셜라이저가 지정되지 않으면 그 객체는 기본 초기화(default-initialized)돼요. 참조에 이니셜라이저가 지정되지 않으면 프로그램은 규칙 위반이에요.
- 객체에 지정된 이니셜라이저가
()이면(선언자에서는 문법 제약 때문에 나타날 수 없어요) 그 객체는 값 초기화(value-initialized)돼요. 참조에 지정된 이니셜라이저가()이면 규칙 위반이에요.
이니셜라이저의 의미는 이래요.
- 초기화되는 대상이 참조면 참조 초기화를 봐요.
- 그 외에는 초기화 대상이 객체예요. 객체의 타입을
T라고 할 때:- (1) 문법이면 객체는 복사 초기화돼요.
- (2) 문법이면:
T가 집계(aggregate) 타입이면 집계 초기화가 적용돼요.T가 스칼라 타입이면T x = { a };는T x = a;와 동등해요.- 그 외에는 규칙 위반이에요. (C++11까지)
- (2) 또는 (4) 문법이면 객체는 리스트 초기화돼요. (C++11부터)
- (3) 문법이면 객체는 직접 초기화돼요.
#include <string>
std::string s1; // default-initialization
std::string s2(); // NOT an initialization!
// actually declares a function "s2"
// with no parameter and returns std::string
std::string s3 = "hello"; // copy-initialization
std::string s4("hello"); // direct-initialization
std::string s5{'a'}; // list-initialization (since C++11)
char a[3] = {'a', 'b'}; // aggregate initialization
// (part of list initialization since C++11)
char& c = a[0]; // reference initialization
비지역 변수 (Non-local variables)
정적 저장 기간(static storage duration)을 가진 모든 비지역 변수는 프로그램 시작 시, main 함수가 실행되기 전에 초기화의 일부로 초기화돼요 (아래에서 설명할 지연되는 경우는 제외). 스레드 지역 저장 기간(thread-local storage duration)을 가진 모든 비지역 변수는 스레드 시작 시, 스레드 함수 실행 전에 초기화돼요. 이 두 부류의 변수 모두 초기화가 두 단계로 나눠져 일어나요.
정적 초기화 (Static initialization)
정적 초기화에는 두 가지 형태가 있어요.
- 가능하면 상수 초기화(constant initialization)가 적용돼요.
- 그렇지 않으면 비지역 정적·스레드 지역 변수는 0 초기화(zero-initialized)돼요.
실제로는:
- 상수 초기화는 보통 컴파일 시점에 적용돼요. 미리 계산된 객체 표현이 프로그램 이미지의 일부로 저장돼요. 컴파일러가 그렇게 하지 않으면, 초기화가 어떤 동적 초기화보다 먼저 일어난다는 것을 보장해야 해요.
- 0 초기화될 변수들은 프로그램 이미지의
.bss세그먼트에 놓여요. 이 세그먼트는 디스크 공간을 차지하지 않고, 프로그램 로드 시 OS가 0으로 채워요.
동적 초기화 (Dynamic initialization)
모든 정적 초기화가 끝난 뒤, 다음 상황에서 비지역 변수의 동적 초기화가 일어나요.
- 순서 없는(unordered) 동적 초기화 — 명시적으로 특수화되지 않은 (정적/스레드 지역) 클래스 템플릿 정적 데이터 멤버와 변수 템플릿(C++14부터)에만 적용돼요. 이런 정적 변수의 초기화는 다른 모든 동적 초기화와 상대적으로 순서가 결정되지 않게(indeterminately) 이어져요. 단 변수가 초기화되기 전에 프로그램이 스레드를 시작하면 그 초기화는 순서가 없게(unsequenced) 돼요(C++17부터). 스레드 지역 변수의 초기화는 다른 모든 동적 초기화와 순서가 없어요.
- 부분적으로 순서가 있는(partially-ordered) 동적 초기화 — 암시적 또는 명시적으로 인스턴스화된 특수화가 아닌 모든 인라인 변수에 적용돼요. 부분적으로 순서가 있는 변수
V가 모든 번역 단위에서 순서 있는 또는 부분적으로 순서 있는 변수W보다 먼저 정의되면,V의 초기화는W의 초기화보다 먼저 일어나도록 이어져요 (프로그램이 스레드를 시작하면 happens-before 관계). (C++17부터) - 순서 있는(ordered) 동적 초기화 — 그 외 모든 비지역 변수에 적용돼요. 단일 번역 단위 안에서는 이 변수들의 초기화가 소스 코드에 정의가 나타난 정확한 순서대로 항상 이어져요. 서로 다른 번역 단위의 정적 변수 초기화는 서로 순서가 결정되지 않게 이어지고, 스레드 지역 변수는 순서가 없게 이어져요.
정적 또는 스레드 저장 기간의 비지역 변수 초기화가 예외로 빠져나가면 std::terminate가 호출돼요.
조기 동적 초기화 (Early dynamic initialization)
컴파일러는 다음 두 조건이 모두 참이면 동적으로 초기화되는 변수를 정적 초기화의 일부로(본질적으로 컴파일 시점에) 초기화하는 것이 허용돼요.
- 초기화의 동적 버전이 그 초기화 이전의 다른 네임스페이스 범위 객체의 값을 바꾸지 않는다.
- 초기화의 정적 버전이, 정적으로 초기화될 필요가 없는 모든 변수를 동적으로 초기화했을 때 동적 초기화가 만들어 낼 값과 같은 값을 초기화되는 변수에 만든다.
위 규칙 때문에, 어떤 객체 o1의 초기화가 네임스페이스 범위 객체 o2를 참조하는데 o2는 잠재적으로 동적 초기화가 필요하면서 같은 번역 단위에 나중에 정의된다면, 사용되는 o2의 값이 완전히 초기화된 o2의 값일지(컴파일러가 o2의 초기화를 컴파일 시점으로 끌어올렸을 때), 아니면 그저 0 초기화된 o2의 값일지가 지정되지 않아요.
inline double fd() { return 1.0; }
extern double d1;
double d2 = d1; // unspecified:
// dynamically initialized to 0.0 if d1 is dynamically initialized, or
// dynamically initialized to 1.0 if d1 is statically initialized, or
// statically initialized to 0.0 (because that would be its value
// if both variables were dynamically initialized)
double d1 = fd(); // may be initialized statically or dynamically to 1.0
지연된 동적 초기화 (Deferred dynamic initialization)
동적 초기화가 (정적 변수라면) main 함수의 첫 문장보다, (스레드 지역 변수라면) 스레드의 초기 함수보다 먼저 happens-before 되는지, 아니면 그 뒤로 지연되는지는 구현 정의예요.
비인라인 변수(C++17부터)의 초기화가 main/스레드 함수의 첫 문장 뒤로 지연되면, 그 초기화는 초기화될 변수와 같은 번역 단위에 정의된 정적/스레드 저장 기간 변수의 첫 odr-사용보다 먼저 일어나요. 어떤 번역 단위에서도 변수나 함수가 odr-사용되지 않으면 그 번역 단위에 정의된 비지역 변수는 영영 초기화되지 않을 수 있어요 (이건 주문형 동적 라이브러리의 동작을 모델링해요). 그러나 번역 단위에서 어떤 것이든 odr-사용되는 한, 초기화나 파괴에 부수 효과가 있는 모든 비지역 변수는 프로그램에서 쓰이지 않더라도 초기화돼요.
인라인 변수의 초기화가 지연되면, 그 특정 변수의 첫 odr-사용보다 먼저 일어나요. (C++17부터)
// ============
// == File 1 ==
#include "a.h"
#include "b.h"
B b;
A::A() { b.Use(); }
// ============
// == File 2 ==
#include "a.h"
A a;
// ============
// == File 3 ==
#include "a.h"
#include "b.h"
extern A a;
extern B b;
int main()
{
a.Use();
b.Use();
}
// If a is initialized before main is entered, b may still be uninitialized
// at the point where A::A() uses it (because dynamic initialization is
// indeterminately sequenced across translation units)
// If a is initialized at some point after the first statement of main (which odr-uses
// a function defined in File 1, forcing its dynamic initialization to run),
// then b will be initialized prior to its use in A::A
정적 지역 변수 (Static local variables)
(블록 범위) 정적·스레드 지역 변수의 초기화는 정적 블록 변수를 봐요.
외부 또는 내부 링키지(linkage)를 가진 변수의 블록 범위 선언에는 이니셜라이저가 허용되지 않아요. 그런 선언은 extern과 함께 나타나야 하고 정의가 될 수 없어요.
클래스 멤버 (Class members)
비정적 데이터 멤버는 멤버 이니셜라이저 목록(member initializer list)이나 기본 멤버 이니셜라이저(default member initializer)로 초기화할 수 있어요.
참고 (Notes)
비지역 변수의 파괴 순서는 std::exit에서 설명돼요.
결함 보고 (Defect reports)
동작 변경 결함 보고로 주요하게 조정된 내용은 이래요. 클래스 템플릿 정적 데이터 멤버 초기화 순서가 명시적 특수화를 제외하면 순서 없음으로 지정됨(CWG 270), 정적 저장 기간 비지역 참조가 항상 동적 초기화보다 먼저 초기화됨(CWG 441), 블록 범위 extern 변수 선언이 정의가 될 수 없도록 금지됨(CWG 1415), 이니셜라이저 안 함수 인자 평가가 초기화의 일부로 간주됨(CWG 2599) 등이에요.