[Effective C++] 항목 4 : 객체를 사용하기 전에 반드시 그 객체를 초기화하자

Jangmanbo·2023년 2월 7일
0

Effective C++

목록 보기
4/33

C++의 객체 초기화는 언제 보장되고 언제 보장되지 않은지에 대한 규칙이 명확하다.
예를 들어 배열과 같은 기본 제공 타입(C++의 C)은 초기화된다는 보장이 없으나 vector(C++의 STL)는 초기화를 보장한다.

그러나 이 규칙이 복잡하기 때문에 굳이 외우기보다는 모든 객체를 사용 전에 항상 초기화하는 것이 가장 좋은 방법이다.


기본제공 타입 비멤버 객체 초기화

int x = 0;

const char *text = "A C-style string";

double d;
std::cin >> d;

멤버 객체 초기화

클래스 생성자에서 멤버 객체를 초기화할 때는 초기화와 대입을 헷갈리지 않는 것이 중요하다.

// 헤더 파일
class PhoneNumber {...};

class ABEntry {
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)
{
	// 초기화 X, 대입 O
	theName = name;
	theAddress = address;
	thePhones = phones;
	numTimesConsulted = 0;
}

C++에서 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 한다.
따라서 ABEntry의 생성자의 본문에서 theName, theAddress, thePhones는 초기화되고 있는 것이 아니라 대입을 하고 있는 것이다.
세 변수들은 생성자에 진입하기도 전에 이미 기본 생성자가 초기화된 후 복사 대입 연산자를 연달아 호출한 것이다.

참고로 기본제공 타입인 numTimesConsulted는 대입되기 전에 초기화되었을거란 보장이 없다.

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
	:theName(name),		// 멤버 초기화 리스트
	 theAddress(address),
	 thePhones(phones),
	 numTimesConsulted(0)
{}	// 생성자 본문에는 아무것도 없다.

다음과 같이 대입문 대신에 멤버 초기화 리스트를 사용하면 theName, theAddress, thePhones는 복사생성자에 의해 초기화된다.
앞서 기본 생성자와 복사 대입 연산자를 호출한 것보다 훨씬 효율적이다.

기본제공 타입 객체는 초기화와 대입의 비용 차이가 없다.
그러나 기본제공 타입의 멤버가 상수이거나 참조자라면 멤버 초기화 리스트가 필수다. 상수와 참조자는 대입이 불가능하기 때문이다.

이렇게 멤버 초기화 리스트는 대부분의 경우에 대입보다 효율적이며 필수적으로 필요한 경우가 있다.
따라서 생성자를 구현할 때는 항상 멤버 초기화 리스트를 사용하는 습관을 들이는 것이 좋다.

ABEntry::ABEntry()
	:theName(),		// 기본 생성자 호출
	 theAddress(),	// 기본 생성자 호출
	 thePhones(),	// 기본 생성자 호출
	 numTimesConsulted(0)	// numTimesConsulted는 명시적으로 0으로 초기화
{}

초기화 순서

  • 기본 클래스는 파생 클래스보다 먼저 초기화
  • 클래스 데이터 멤버는 선언된 순서대로 초기화
    • theName -> theAddress -> thePhones 순으로 초기화
  • 비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 결정

정적 객체

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

  • 지역 정적 객체
    • 함수 안에서 static으로 선언된 객체
  • 비지역 정적 객체
    • 전역 객체
    • 네임스페이스 유효범위에서 정의된 객체
    • 클래스 안에서 static으로 선언된 객체
    • 파일 유효범위에서 static으로 정의된 객체

번역 단위

컴파일을 통해 하나의 object file을 만드는 바탕이 되는 소스코드
해당 소스코드와 그 파일이 #include한 파일까지가 하나의 번역 단위이다.
참고: C++ 빌드 과정

만약 번역단위A에 있는 비정적 객체의 초기화에 번역단위B의 비정적 객체의 초기화가 사용되는 경우를 생각해보자.
이때 두 비정적 객체 중 어떤 객체가 먼저 초기화될지 알 수 없다. 별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 정해져 있지 않기 때문이다.


예시)
class FileSystem {
public:
	...			// 다른 멤버 함수들
	std::size_t numDisks() const;
};

extern FileSystem tfs;	// 사용자가 쓸 예정인 객체

FileSystem 클래스 객체는 어디서든 접근할 수 있어야 하므로 전역 혹은 네임스페이스 유효범위에 선언하였다. 참고: extern 사용법

// 헤더 파일
class Directory {
public:
	Directory(params);
    ...
};

// 소스 파일
Directory::Directory(params)
{
	...		// 다른 멤버 함수들
    std::size_t disks = tfs.numDisks();
}

Directory는 파일 시스템 내의 디렉토리를 나타내는 클래스이므로 tfs 객체를 사용한다.

Directory tempDir(params);

Directory 클래스를 사용하여 임시 파일을 담는 디렉토리 객체를 하나 생성했을 때 tempDir보다 tfs가 먼저 초기화되지 않았다면, tempDir의 생성자는 tfs가 초기화되지도 않았는데 사용하려고 한 것이다.

이들의 초기화 순서를 어떻게 정할 수 있을까?

// 이전에 만들었던 tfs 객체를 tfs() 함수로 대체
// tfs()는 FileSystem 클래스 내 정적 멤버로도 들어갈 수 있다.
FileSystem& tfs()
{
	static FileSystem fs;	// 지역 정적 객체를 정의 및 초기화
    return fs;				// fs 객체에 대한 참조자 반환
}

// 이전에 만들었던 tempDir 객체를 tempDir() 함수로 대체
// tempDir()는 Directory 클래스 내 정적 멤버로도 들어갈 수 있다.
Directory& tempDir()
{
	static Directory td;	// 지역 정적 객체를 정의 및 초기화
    return td;				// td 객체에 대한 참조자 반환
}

tfs, tempDir 대신 tfs(), tempDir()을 참조하는 것으로 바꾸면 된다.
즉, 정적 객체 자체를 직접 사용하지 않고 그 객체에 대한 참조자를 반환하는 함수를 사용하는 것이다.

이전에 선언했던 tfs, tempDir 객체는 비지역 정적 객체였으므로 초기화 순서를 정할 수 없었다.
그러나 fs, td 객체는 함수 내에서 static으로 선언, 즉 지역 정적 객체로 정의했기 때문에 초기화 순서를 정할 수 있게 되었다.
(지역 정적 객체는 함수 호출 중에 그 객체의 정의에 최초로 닿았을 때 초기화된다.)


※주의: 다중스레드 시스템에서는 정적객체의 사용으로 인해 문제가 생길 수 있다.

정리
1. 기본제공 타입 비멤버 객체는 직접 초기화한다.
2. 객체의 모든 부분에 대한 초기화에는 멤버 초기화 리스트를 사용한다.
3. 비지역 정적 객체의 초기화 순서는 정할 수 없으므로, 지역 정적 객체로 바꾸어 설계한다.

0개의 댓글