[C++] 항목 4: 객체를 사용하기 전에 반드시 그 객체를 초기화하자
카테고리: cpp
태그: cpp
이 글은 아래의 책을 정리하였습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 1. C++에 왔으면 C++의 법을 따릅시다
👉🏻 항목 4: 객체를 사용하기 전에 반드시 그 객체를 초기화하자
1. 초기화
int x;
class Point {
int x, y;
};
Point p;
객체를 초기화하지 않으면 객체를 읽을 때 정의되지 않은 동작이 나올 수 있다.
C++의 C 부분만을 사용하고 있으며, 초기화에 런타임 비용이 소모될 수 있는 상황이라면, 값이 초기화된다는 보장이 없다.
예를들어, 배열은 초기화된다는 보장이 없지만 vector는 초기화된다는 보장이 있다.
int x = 0;
const char * text = "A C-style string";
double d;
cin >> d;
위의 코드는 모두 기본 제공 타입이며, 비멤버 객체이다.
C언어 부분만을 사용하고 있으므로, 이들에 대해선 초기화를 손수해주어야한다.
1.1. 초기화 리스트
class PhoneNumber { ... };
class ABEntry {
public:
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones);
private:
string theName;
string theAddress;
list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(string& name, string& address, list<PhoneNumber>& phones) {
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
위의 코드에선 기본 생성자(암시적 생성자)를 먼저 실행한 뒤, 매개 변수가 포함된 파생 생성자를 실행한다.
string, address, phones 변수는 사용자 정의 타입이며 기본 생성자가 존재하기 때문이다.
기본 생성자를 통한 초기화는 이미 지나갔으며, 이후에 대입을 진행해주고 있다.
더 나아가 생각해보면 초기화가 진행될 때, 기본 제공 타입인 numTimesConsulted가 초기화되었을 것이란 보장이 없다.
ABEntry::ABEntry()
: theName(),
theAddress(),
thePhones(),
numTimesConsulted()
{}
ABEntry::ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
: theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{}
이를 해결하기 위해 초기화 리스트를 사용할 수 있다.
초기화 리스트를 사용하면 복사 생성자(대입)를 한번만 사용하여 더 효율적이다.
이러한 방식을 사용하면, 기본 생성자 호출 후, 복사 생성자 호출이라는 비효율적인 방식을 해결할 수 있다.
생성자가 호출되는 순서는 다음과 같다.
theName → theAddress → thePhones → numTImesConsulted
이는 선언된 순서대로 초기화된다는 것을 뜻한다.
1. 기본 클래스는 파생 클래스보다 먼저 초기화된다.
2. 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화된다.
1.2. 정적 객체의 초기화
비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.
이것이 무엇을 뜻할까? 아래에서 알아보자.
1. 전역 객체
2. 네임스페이스 유효범위에서 정의된 객체
3. 클래스 안에서 static으로 선언된 객체
4. 함수 안에서 static으로 선언된 객체
5. 파일 유효범위에서 static으로 정의된 객체
정적 객체의 범주에는 위 5개가 있다.
이들 중, 함수 안에 있는 정적 객체는 지역 정적 객체라고 하고, 나머지는 비지역 정적 객체라고 한다.
이 5개는 프로그램이 끝날때, 즉 main() 함수 실행이 끝날 때 정적 객체의 소멸자가 호출된다.
번역 단위는 컴파일을 통해 하나의 목적 파일(.o 파일)을 만드는 바탕이 되는 소스 코드를 말한다.
기본적으로 소스 파일 하나가 되며, #include하는 파일들까지 합쳐 하나의 번역 단위가 된다.
문제는 다음의 상황에서 벌어진다.
별도로 컴파일된 소스 파일이 두 개 이상 있으며, 각 소스 파일에 비지역 정적 객체가 한개 이상 있다.
한쪽 번역 단위에서 비지역 정적 객체의 초기화가 진행될 때, 다른 쪽에 있는 비지역 정적 객체가 사용된다면 어떻게 될까?
별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 정해져 있지 않다.
그러므로, 이런 상황에선 초기화가 되어있지 않을 수 있다.
이러한 상황을 해결하기 위해서, 싱글톤 패턴 방식을 사용할 수 있다.
어떻게 할수 있는지 아래에서 살펴보자.
class FileSystem { ... };
// tfs 객체를 이 함수로 대신한다.
// 지역 정적 객체이며, 참조자를 반환한다.
FileSystem& tfs() {
static FileSystem fs;
return fs;
}
class Directory { ... };
Directory::Directory(params) {
...
size_t disks = tfs().numDisks();
...
}
// Directory 객체를 이 함수로 대신한다.
// 지역 정적 객체이며, 참조자를 반환한다.
Directory& tempDir() {
static Directory td;
return td;
}
1. 비지역 정적 객체를 지역 정적 객체로 바꾸었다.
2. 유일한 객체를 생성하고 반환한다.
이 점을 통해 싱글톤 패턴이라는 점을 알 수 있다.
단일 객체를 원할 때 생성하고 참조할 수 있어지므로, 완전히 제어할 수 있어지므로 이전의 문제를 해결할 수 있다.
단, 주의 할 점이 있는데 다중스레드 환경에서는 경쟁 상태(race condition)라는 문제가 생길 수 있다.
예를 들어 객체 B를 초기화할 때, 객체 A가 먼저 초기화되어야 하는데, A의 초기화가 B의 초기화에 의존한다면?
데드락 상태(이도저도 못하는 상태)에 빠져버릴 수 있으므로 주의해야한다.
이를 방지하기 위해 개발자가 객체들의 초기화 순서를 제대로 맞춰두어야 한다.
🧐 정리
1. 멤버가 아닌 기본제공 타입 객체는 직접 초기화하자.
2. 멤버 초기화 리스트를 사용하자.
3. 비지역 정적 객체에 영향을 끼치는 불확실한 초기화 순서를 고려하며 설계하자.
댓글남기기