Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

로또

4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자. 본문

책/Effective C++

4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자.

아롱로또 2023. 11. 1. 03:02

다루는 내용

C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 한다.

-> 멤버 초기화 리스트의 사용

 

비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.

-> 초기화되지 않은 비지역 정적 객체를 사용할 위험이 있다.

 

다양한 초기화 방법

int x = 0; // int의 직접 초기화
const char* text = "Hello World"; // 포인터의 직접 초기화

double d;
cin >> d; // 입력을 통한 초기화 수행

 

 

위처럼 변수의 선언과 동시에 초기화를 하는 방법, 입력을 받아 초기화를 하는 방법이 있다.

그 외의 초기화 방법은 모두 생성자로 귀결된다.

 

대입

class PhoneNumber{ ... };

class ABEntry{ // Address Book Entry
public:
    ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones){
        the_name = name; // 대입
        the_address = address; // 대입
        the_phones = phones; // 대입
        num_time_consulted = 0; // 대입
    }

private:
    string the_name;
    string the_address;
    list<PhoneNumber> the_phones;
    int num_time_consulted;
};

C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 한다.

하지만 위에서는 생성자의 본문에서 각 멤버 변수들이 대입되고 있다. 초기화는 그 전에 이미 끝난 것이다.

즉, ABEntry 생성자에 진입하기 전에 the_name, the_address, the_phones의 기본 생성자가 호출되었다는 의미이다.

멤버 변수 num_time_consulted의 경우 기본 제공 타입이므로 생성자 안에서 대입되기 전에 초기화된다는 보장이 없다.

 

멤버 초기화 리스트

class PhoneNumber{ ... };

class ABEntry{
public:
    ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
    :   the_name(name), // 멤버 초기화 리스트를 이용한 초기화
        the_address(address),
        the_phones(phones),
        num_time_consulted(0)
    {}

private:
    string the_name;
    string the_address;
    list<PhoneNumber> the_phones;
    int num_time_consulted;
};

 

생성자 본문에서 값을 대입하는 경우 일어나는 일은 다음과 같다.

  1. the_name, the_address, the_phones의 기본 생성자 호출
  2. name, address, phones의 값 대입 (1에서 생성자에 의해 초기화된 값이 버려진다.)

멤버 초기화 리스트를 이용하는 경우 일어나는 일은 다음과 같다.

  1. the_name, the_address, the_phones는 각각 name, address, phones를 인자로 하는 복사 생성자 호출

일반적으로 기본 생성자 호출 후 복사 대입 연산자를 연달아 호출하는 것보다, 복사 생성자를 한 번 호출하는 것이 더 효율적이다.

 

또한, 상수나 참조자의 경우에는 초기화 때를 제외하고는 대입 연산 자체가 불가능하므로 멤버 초기화 리스트를 사용해야 할 것이다.

 

객체를 구성하는 데이터의 초기화 순서

  1. 기본 클래스는 파생 클래스보다 먼저 초기화된다.
  2. 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화된다.

2에 의해 멤버 초기화 리스트를 작성하는 순서와는 관계없이 초기화 순서는 선언된 순서대로이다.

 

 

비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.

정적 객체(static object)는 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아있는 객체를 의미하며, 다음과 같다.

  1. 전역 객체
  2. namespace 유효 범위에서 정의된 객체
  3. class 안에서 static으로 정의된 객체
  4. 함수 안에서 static으로 선언된 객체
  5. 파일 유효범위에서 static으로 선언된 객체

이들은 모두 main()이 종료될 때 정적 객체의 소멸자가 호출된다.

이 중, 함수 안에서 static으로 선언된 객체는 함수에 대해서 지역성을 가지므로 지역 정적 객체이고, 나머지는 모두 비지역 정적 객체이다.

 

번역 단위(translation unit)는 컴파일을 통해 하나의 목적 파일을 만드는 바탕이 되는 소스 코드를 일컫는다. 기본적으로는 소스 파일 하나이지만 해당 파일이 include하는 파일까지 합쳐서 하나의 번역 단위가 된다.

 

문제가 되는 상황

별도로 컴파일된 소스 파일 A와 B가 있다. (A, B는 다른 번역 단위)

 

각 소스 파일에 비지역 정적 객체(non-local static object)가 한 개 이상 들어있다.(비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해짐.)

 

이 때, A 번역 단위에 있는 비정적 객체(non-static object)의 초기화가 진행되면서 B 번역 단위에 있는 비지역 정적 객체가 사용된다.

 

결국, 비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 진행되므로 B 번역 단위에 있는 비지역 정적 객체가 초기화 되어있지 않을 수도 있다.

 

// File A
class FileSystem{
public:
    ...
    size_t NumDisks() const;
    ...
};

extern FileSystem tfs; // 사용자가 사용하게 될 전역 객체
// File B
class Directory{
public:
    Directory(params){
        ...
        disks = tfs.NumDisks(); // 아직 초기화되지 않을 수 있는 A번역 단위의 전역 객체 사용
        ...
    }
    ...
};

...
Directory temp_dir(params); // Directory 생성자 호출
...

위와 같은 코드에서 아직 tfs가 초기화되지 않았는데 temp_dir을 생성하면서 tfs를 사용할 수도 있다는 것이다.

 

해결 방법

class FileSystem{
public:
    ...
    size_t NumDisks() const;
    ...
};

FileSystem& tfs(){
    static FileSystem fs; // 지역 정적 객체를 정의, 초기화한다.
    return fs; // 지역 정적 객체의 참조자를 반환한다.
}

class Directory{
public:
    Directory(params){
        ...
        disks = tfs.NumDisks();
        ...
    }
    ...
};

Directory& TempDir(){
    static Directory td; // 지역 정적 객체를 정의, 초기화한다.
    return td; // 지역 정적 객체의 참조자를 반환한다.
}

지역 정적 객체를 이용한다.

지역 정적 객체는 함수 호출 중에 그 객체의 정의에 최초로 닿았을 때 초기화되도록 C++에서 보장하므로, 앞서 등장한 초기화 순서 문제를 해결할 수 있다.

 

 

이것만은 잊지 말자!

  • 기본제공 타입의 객체는 직접 손으로 초기화하자. 경우에 따라 저절로 되기도, 안되기도 한다.
  • 생성자에서는 대입이 아니라 멤버 초기화 리스트를 사용하자. 또한 초기화 리스트에 데이터 멤버를 나열할 때, 클래스에 데이터 멤버가 선언된 순서와 똑같이 나열하자.
  • 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계하자. 비지역 정적 객체를 지역 정적 객체로 바꾸자.