C++의 객체(변수) 초기화가 중구난방인 것은 절대 아닙니다. 언제 초기화가 보장되며 언제 그렇지 않은지에 대한 규칙이 명확히 준비되어 있지요. 안타까운 점은 규칙 자체가 아주 조금 복잡하다는 것인데, 필자의 사견이지만 머리에 새겨둘 가치가 있기엔 너무 복잡합니다.


사정이야 어쨌든 가장 좋은 방법은 모든 객체를 사용하기 전에 항상 초기화하는 것입니다. 기본제공 타입으로 만들어진 비멤버 객체에 대해서는 초기화를 손수 해야 하겠습니다. 아래의 예를 보시죠.


1
2
3
4
5
6
7
int x = 0;                                    //int의 직접 초기화
 
const char * text = "A C-style string";        // 포인터의 직접 초기화
                                            // (항목 3도 참조)
 
double d;                                    // 입력 ㅅ트릠에서 읽음으로써
std::cin >> d;                                // "초기화" 
cs


이런 부분을 제외하고 나면, C++ 초기화의 나머지 부분은 생성자로 귀결됩니다. 생성자에서 지킬 규칙은 지극히 간답합니다. 그 객체의 모든 것을 초기화 하자!!


참 지키기도 쉬운 규칙입니다만, 대입(assignment)을 초기화 (initialization)와 헷갈리지 않는 것이 가장 중요하다는 데 따옴표를 달고 싶습니다.


클래스로 한 예를 들어 보죠


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PhoneNumber { ... };
 
class ABEntry {            //ABEntry = "Address Book Entry"
public:
    ABEntry( const std::string& name, const std::string& address,    //생성자 선언
                const std::list<PhoneNumber>& phones );
 
private:
    std::string theName;
    std::string theAddress;
    std::list<PhoneNumber> thePhones;
    int numTimesConsulted;
};
 
ABEntry::ABEntry( const std::string& name, const std::string& address,        //생성자 정의
            const std::list<PhoneNumber>& phones )
{    
    theName = name;                    // 지금음 모두 '대입'을 하고 있습니다.
    theAddress = address;            // '초기화'가 아닙니다.
    thePhones = phones;
    numTimesConsulted = 0;
}
 
cs


여기서 초기화 되고 있는 것이아니라, 어떤 값이 대입되고 있는 것입니다.


ABEntry 생성자를 좀더 멋있게 만들 수 있을까요? 물론입니다. 인상부터 지루하기 짝이 없는 대입문 대신에 멤버 초기화 리스트를 사용하면 됩니다.


1
2
3
4
5
6
7
8
ABEntry::ABEntry( const std::string& name, const std::string& address,        //생성자 정의
            const std::list<PhoneNumber>& phones )
: theName(),                // theName의 기본 ctor를 호출합니다.
  theAddress(),                // theAddress에 대해서도 그렇게 하고요.
  thePhones(),                // thePhones에 대해서도 그렇게 하지만,
  numTimesConsulted(0)        // numTimesConsulted는 명시적으로
{ }                            // 0으로 초기화 합니다.
 
cs


기본제공 타입의 멤버를 초기화 리스트로 넣는 일이 선택이 아니라 의무가 될 때도 있습니다. 상수이거나 참조라로 되어 있는 데이터 멤버의 경우엔 반드시 초기화되어야 합니다. 이것은 아주 중요한데, 상수와 참조자는 대입 자체가 불가능하기 때문입니다.


이 와중에도 변덕스럽지 않은 부분이 하나 있는데, 꼭 알아 두셔야 합니다. 바로 객체를 구성하는 데이터의 초기화 순서입니다. 이 순서는 어떤 컴파일러를 막론하고 항상 똑같습니다.

1. 기본 클래스는 파생 클래스보다 먼저 초기화된다.

2. 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화 된다.

     (어쩌다가 멤버 초기화 리스트에 이들이 넣어진 순서가 다르더라도 초기화 순서는 그대로입니다.)



정적 객체(static object) 초기화

정적 객체의 범주에 들어가는 것들은

1. 우선 전역 객체가 있고

2. 네임스페이스 유효범위에서 정의된 객체

3. 클래스 안에서 static으로 선언된 객체

4. 그리고 파일 유효범위에서 static으로 정의된 객체

이렇게 다섯종류가 되겠습니다.


이들 중 함수 안에 있는 정적 객체는 지역 정적 객체(local static obejct)라고 하고, 나머지는 비지역 정적 객체(non-local static object)라고 합니다.


이 다섯 종류의 객체, 합쳐서 정적 객체는 프로그램이 끝날 때 자동으로 소멸됩니다. 다시 말해, main()함수의 실행이 끝날 때 정적 객체의 소멸자가 호출된다는 이야기죠.


정적객체에 대한 문제는 별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 '정해져 있지 않다'라는 사실 입니다.

예제를 하나 준비했습니다.


1
2
3
4
5
6
7
8
class FileSystem{                    // 여러분의 라이브러리에 포함된 클래스
public:
    ...
    std::size_t numDisks() const;    // 많고 많은 멤버 함수들 중 하나
    ...
};
extern FileSystem tfs;                // 사용자가 쓰게 될 객체
                                    // "tfs" = "the file system"
cs


이 클래스로 만든 객체가 초기화되기(생성자 호출되기) 전에 그 객체를 사용한다는 것은 대재앙을 일으키는 셈이겠지요


1
2
3
4
5
6
7
8
9
10
11
12
13
class Directory {
public:
    Directory( params );
    ...
};
 
Directory::Directory( params )
{
    ...
    stdLLsize_t disks = tfs.numDisks();    //tfs 객체를 여기서 사용함
    ...
}
 
cs


어차피 가정이니, 한 발짝 더 나아가 봅시다. 이제는 이 사용자가 Directory 클래스를 사용해서 임시 파일을 담는 디렉토리 객체 하나를 생성하기로 마음먹습니다.


1
2
Directory tempDir( params );        // 임시 파일을 담는 디렉토리
 
cs


tfs가 tempDir보다 먼저 초기화되지 않으면, tempDir의 생성자는 tfs가 초기화되지도 않았는데 tfs를 사용하려고 하겠지요. 그렇게 되면 대재앙.


어쨌든 tempDir전에 tfs가 초기화되게 만들고 싶은데, 이 당면 목표를 어떻게 달성할 수 있을까요?


단언컨데 안 됩니다. 또 말씀드릴까요? 서로 다른 번역 단위에 정의된 비지역 정적 객체들 사이의 상대적인 초기화 순서는 정해져 있지 않습니다.

한 가지 다행스러운 사실은 설계에 약간의 변화만 살짝 주면 이 문제를 사전에 봉쇄할 수 있다는 점입니다. 방법도 간단합니다. 비지역 정적 객체를 하나씩 맡는 함수를 준비하고 이 안에 각 객체를 넣는 것입니다. 함수 속에서도 이들은 정적 객체로 선언하고, 그 함수에서는 이들에 대한 참조자를 반환하게 만듭니다. 사용자 쪽에서는 비지역 정적 객체를 지접 참조하는 과거의 폐딴을 버리고 이제는 함수 호출로 대신합니다. 자, 정리하면 '비지역 정적 객체'가 '지역 정적 객체'로 바뀐 것입니다.(디자인 패턴에 관심이 많은 분이라면 이것이 단일체 패턴(singleton pattern)의 전형적인 구현양식임을 바로 알 수 있겠지요)


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
class FileSystem { ... };            // 이전과 다를 것 없는 클래스
 
FileSystem& tfs()                    // tfs 객체를 이 함수로 대신합니다. 이 함수는
{                                    // 클래스 안에 정적 멤버로 들어가도 됩니다.
 
    static FileSystem fs;            // 지역 정적 객체를 정의하고 초기화합니다.
    return fs;                        // 이 객체에 대한 참조자를 반환합니다.
}
 
class Directory { ... };            // 역시 이전과 다를 것 없는 클래스
 
Directory::Directory( params )        // 이전과 동일합니다. tfs의 참조자였던 것이
{                                    // 지금은 tfs()로 바뀌었다는 것만 다릅니다.
    ...
    std::size_t disks = tfs().numDisks();
    ...
}
 
Directory& tempDir()                // tempDir 객체를 이 함수로 대신합니다. 이 함수는
{                                    // Directory 클래스의 정적 멤버로 들어가도 됩니다.
    
    static Directory td;            // 지역 정적 객체를 정의/초기화합니다.
    return td;                        // 이 객체에 대한 참조자를 반환합니다.
}
 
cs



정리하죠. 어떤 객체가 초기화되기 전에 그 객체를 사용하는 일이 생기지 않도록 하려면 딱 세가지만 기억해 두고 실천하면 됩니다.

1. 멤버가 아닌 기본제공 타입 객체는 여러분 손으로 직접 초기화하세요.

2. 객체의 모든 부분에 대한 초기화에는 멤버 초기화 리스트를 사용합니다.

3. 별개의 번역 단위에 정의된 비지역 정적 객체에 영향을 끼치는 불확실한 초기화 순서를 염두에 두고 이러한 불확실성을 피해서 프로그램을 설계해야 합니다.



이것만은 잊지 말자!

* 기본제공 타입의 객체는 직접 손으로 초기화합니다. 경우에 따라 저절로 되기도 하고 안되기도 하기 때문입니다.

* 생성자에서는, 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화하지 말고 멤버 초기화 리스트를 즐겨 사용합시다. 그리고 초기화 리스트에 데이터멤버를 나열할 때는 클래스에 각 데이터 멤버가 선언된 순서와 똑같이 나열합시다.

* 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 합니다.

   비지역 정적 객체를 지역 정적 객체로 바꾸면 됩니다.


+ Recent posts