컴퓨터공학/Effective C++

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

Pyxis 2025. 1. 19. 18:29

[ Effective C++ ]

 

이 책을 읽는 프로그래머라면, 기본적으로 데이터 영역이 아닌 경우, 변수를 초기화하지 않았을 때 쓰레기값이 들어간다는 것은 숙지하고 있을 것이다.

객체의 경우를 본다면, 생성자에서 대입(assignment)과 초기화(initialization)을 구분하여 사용하기 전에 반드시 객체를 초기화해야 한다.

 

아래 예제를 보자.

class PhoneNumber { ... };

class ABEntry
{    // ABEntry = "Address Book Entry"
public:
	ABEntry(const std::string& name, const std::string& addess, const std::list<PhoneNumber>& phones);
    
private:
    std::string theName;
    std::string theAddress;
    std::list<PhoneNumber>& thePhonwa;
    inr numTimesConsulted;
};

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
{
    theName = name;         // !! : 지금은 모두 '대입'을 하고 있습니다.
    theAddress = address;   // !! : '초기화'가 아닙니다.
    thePhones = phones;	    // !! :
    numTimesConsulted = 0;  // !! :
}

 

이미 초기화(initialization)는 끝났고, 대입(assignment)되고 있는 것.

- 예외로, 마지막에 대입되는 numTimesConsulted는 기본제공 타입이기 때문에 대입되기 전에 초기화되리란 보장은 없다.

 

대신 초기화 리스트를 사용하자.

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
 : theName(name), theAdress(address), thePhones(phones), numTimesConsulted(0)
{ // 이제 모두 초기화되고 있음.
	// 이제 생성자 본문엔 아무것도 들어가 있지 않게 된다.	 
}

 

- 무슨 차이일까?

대입(assignment)만 사용한 버전의 경우 theName, theAddress, thePhones에 대해 기본 생성자를 호출해서 초기화를 미리 해 놓은 후에, 생성자에서 또 다시 새로운 값을 대입하고 있다.  컴파일러는 초기화 리스트에 아무것도 없어 '기본 생성자'를 자동으로 호출해준 것이다.

초기화 리스트를 사용한 경우에는, 생성자의 본문이 실행되기 전에 복사 생성자를 이용해서 theName, theAddress, thePhones가 초기화 된 것이다.

numTimesConsulted같은 기본제공 타입이거나, 기본 생성자로 초기화하고 싶을 때에도(생성자 인자로 아무것도 주지 않으면 된다!) 초기화 리스트를 이용해서 초기화한다면 일관성이 생길 것이다.

ABEntry::ABEntry()
 : theName(), theAddress(), thePhones(), numTimesConsulted(0)
 {} // 3가지 객체는 기본 생성자를 이용하여 초기화.

 

(이 책은 C++11가 나오기 전에 쓰였으므로 저자는 선언하며 초기화하는 것을 고려하지 않고 있습니다.)  

또한 필수적으로 초기화 리스트를 호출해야하는 경우도 존재하는데, 상수(const)와 참조자(ref)는 대입 자체가 불가능하기 때문이다.

 

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

[1] 기본 클래스는 파생(자식) 클래스보다 먼저 초기화된다.

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

 

* 초기화 리스트에 초기화되는 순서가 다르더라도 초기화 순서는 '선언된 순서'이다.

→ 이것또한 일관성을 위해 선언 순서와 동일하게 맞춰주자.

 

[ 비지역 정적 객체(non-local static object)의 초기화 순서는 개별 번역 단위(different translation units)에서 정해진다 ]

정적 객체(static object)는 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체를 의미한다. 스택 객체, 힙 기반 객체는 정적 객체가될 수 없는데,

(1) 전역 객체

(2) namespace 유효범위에서 정의된 객체

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

(4) 함수안에서 static으로 선언된 객체

(5) 파일 유효범위에서 static으로 선언된 객체

의 5종류가 해당된다.

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

정적 객체는 프로그램이 끝날 때 소멸되는데, 즉 main() 함수의 실행이 끝날 때 정적 객체의 소멸자가 호출된다는 의미이다.

 

번역 단위(translation unit) : 컴파일을 통해 하나의 목적 파일(object file)을 만드는 바탕이 되는 소스 코드

 

[ 까다로운 문제 ]

class FileSystem // 라이브러리에 포함된 클래스
{
public:
    ...
    std::size_t numDisks() const; // 많고 많은 멤버 함수들 중 하나
    ...
};

extern FileSystem tfs; // 사용자가 쓰게 될 객체, "tfs" = "the file system"

// ======================== //

class Directory // 라이브러리의 사용자가 만든 클래스
{
public:
    Dircctory ( params );
    ...
};

Directory::Direcoty( params )
{
    ...
    std::size_t disks = tfs.numDisks(); // tfs 객체를 여기서 사용함
    ...
}

// ======================== //

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

 

tfs가 tempDir보다 먼저 초기화되지 않으면, tempDir의 생성자는 tfs가 초기화되지도 않았는데 tfs를 사용하려고 한다.

그러나 tfs와 tempDir은, 제작자도 다르고 만들어진 시기도 다를뿐더러, 소스 파일도 다르다.

"다른 번역 단위 안에서 정의된 비지역 정적 객체"가 되는 것.

→ tempDir전에 tfs가 초기화되게 만들고싶다.

 

원칙적으로는 '다른 번역 단위에 정의된 비지역 정적 객체'들 사이의 상대적인 초기화 순서는 '정의되어 있지 않으므로', 다르게 접근해야한다.

비지역 정적 객체를 선언한 뒤, 참조자로 반환하는 함수를 각각 만들어서, 함수 호출로 대신한다면 '지역 정적 객체'로 변경되어 초기화 순서를 맞춰주면 된다.

→ 객체의 정의에 최초로 닿았을 때 초기화하도록 되어있는 C++이 보장하는 규칙이며, 이를 위해 Singleton pattern 이용.

 

FileSystem& tfs()
{
    static FileSystem fs; // 이 함수는 Directory 클래스의 정적 멤버함수로 정의해도 된다.
    return fs;
}

Directory::Directory( params )
}
    ...
    std::size_t disks = tfs().numDisks();
    ...
}

Directory& tempDir()
{
    static Directoy td; // 이 함수는 Directory 클래스의 정적 멤버함수로 정의해도 된다.
    return td;
}

 

여기서 발생가능한 문제는, 내부에서 정적 객체(static object)를 포함하기 때문에, 멀티쓰레드 시스템에서 문제가될 수 있다. 이 경우, 멀티쓰레드로 진입하기 전에 이를 호출하여 초기화에 관련하여 race condition을 없앨 수 있다.

→ static 변수는 data 영역에 올라가기 때문.

 

[ 정리 ]

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

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

- 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 합니다. 비지역 정적 객체를 (singleton pattern을 이용해 함수 호출시 정의될 때 초기화되도록 하여) 지역 정적 객체로 바꾸면 됩니다.