04: 객체를 사용하기 전에 반드시 그 객체를 초기화하자
초기화 되지 않은 값을 읽는다면 의도되지 않은 동작이 그대로 흘러나오게 된다. 이런 방법을 막기 위해서 가장 좋은 방법은 모든 객체를 사용하기 전에 항상 초기화하는 것이다.
int x = 0; // 직접 초기화
const char* text = "Hello"; // 직접 초기화
double d;
std::cin >> d; // 입력 스트림에서 읽음으로써 초기화 수행
위의 부분을 제외하고는 C++의 초기화는 생성자에서 이루어진다.
대입과 초기화를 헷갈리지 말자
class PhoneNumver {};
class ABEntry
{
public:
ABEntry(const string& name, const string address, const list<PhoneNumver>& phones);
private:
string theName;
string theAddress;
list<PhoneNumver> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const string& name, const string address, const list<PhoneNumver>& phones)
{
theName = name; // 아래는 모두 초기화가 아닌 대입이다.
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 한다고 명시되어있다. 위의 ABEntry의 생성자는 초기화되고 있는 것이 아니라, 어떤 값이 대입되고 있는 것이다. 초기화는 진작 지나갔다. 기본제공 타입의 데이터 멤버인 numTimesConsulted의 경우에는 (생성자 안에서) 대입되기 전에 초기화되리란 보장이 없어 미리 초기화 되고 대입된건지는 알 수 없다.
대입문 대신에 멤버 초기화 리스트를 사용하자
ABEntry::ABEntry(const string& name, const string address, const list<PhoneNumver>& phones)
: theName(name), // 이제 모두 초기화되고 있다.
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{}
앞에서 만든 생성자보다 더 효율적일 가능성이 크다. 대입만 사용한 버전의 경우 theName, theAddress, thePhones의 기본 생성자를 호출해서 초기화를 미리 해 놓은 후에 생성자에서 곧바로 새로운 값을 대입하고 있다. 즉, 먼저 호출된 기본 생성자는 헛짓거리가 된다.
초기화 리스트에 들어가는 인자는 바로 데이터 멤버에 대한 생성자의 인자로 쓰이기 때문에 이 문제를 피해 갈 수 있다. 지금의 경우엔 theName, theAddress, thePhones는 복사 생성자에 의해 초기화된다. 복사 생성자를 한 번 호출하는 쪽이 더 효율적이다.
기본 제공 타입의 객체는 초기화와 대입에 걸리는 비용의 차이는 없지만, 역시 멤버 초기화 리스트에 모두 넣어 주는 쪽이 가장 좋다.
ABEntry::ABEntry(const string& name, const string address, const list<PhoneNumver>& phones)
: theName(), // 기본 생성자로 초기화
theAddress(),
thePhones(),
numTimesConsulted(0) // 명시적으로 초기화
{}
데이터 멤버를 기본 생성자로 초기화되고 싶을 때도 멤버 초기화 리스트를 사용하는 습관을 들이자. 어쩌다가 리스트에서 어떤 멤버를 빼먹었을 때, 어떤 멤버가 초기화되지 않을 수 있다는 사실을 끌고 가야 하는 부담이 없어진다. numTimesConsulted가 멤버 초기화 리스트에서 빠졌다면 기본제공 타입이니까, 이것이 초기화될지 안 될지 장담을 못한다.
기본 제공 타입의 멤버를 초기화 리스트로 넣는 일이 선택이 아니라 의무가 될 때도 있다. 상수이거나 참조자로 되어 있는 데이터 멤버의 경우엔 반드시 초기화되어야 한다. 상수와 참조자는 대입이 불가능하기 때문이다.
현장에서 쓰이는 클래스들 중 상당수가 여러 개의 생성자를 갖고 있다. 각 생성자마다 멤버 초기화 리스트가 붙어 있을 것이다. 이렇게 중복이 심한 경우에는 이들에 대한 대입 연산을 하나의(대개 private 멤버) 함수에 몰아넣고, 모든 생성자에서 이 함수를 호출하게 하는 방법도 있다. 데이터 멤버의 진짜 초기값을 파일에서 읽어온다든지 데이터베이스에서 찾아오는 경우에 특히 유용하다. 그래도 초기화 리스트를 통한게 아무래도 좋다.
객체를 구성하는 데이터의 초기화 순서
- 기본 클래스는 파생 클래스보다 먼저 초기화된다.
- 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화된다.
혼동을 피하기 위해 멤버 초기화 리스트에 넣는 멤버들의 순서도 클래스에 선언한 순서와 동일하게 맞춰 주도록 하자.
비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.
정적 객체의 범주를 보자.
- 전역 객체
- 네임스페이스 유효범위에서 정의된 객체
- 클래스 안에서 static으로 선언된 객체
- 함수 안에서 static으로 선언된 객체
- 파일 유효범위에서 static으로 정의된 객체
이들 중에서 함수 안에서 static으로 선언된 객체는 지역 정적 객체(local static object)라고 하고(함수에 대해서 지역성을 가지므로), 나머지는 비지역 정적객체(non-local static object)라 한다.
번역 단위(translation unit)는 컴파일을 통해 하나의 목적 파일을 만드는 바탕이 되는 소스 코드이다.
문제는 한쪽 번역 단위에 있는 비정적 객체의 초기화가 진행되면서 다른 쪽 번역 단위에 있는 비지역 정적 객체가 사용되는데, 불행히도 이 (다른 쪽 번역 단위에 있는) 객체가 초기화되어 있지 않을지도 모른다는 점이다.
class FileSystem // 라이브러리에 포함된 클래스
{
public:
size_t numDisks() const;
};
extern FileSystem tfs; // 사용자가 쓰게 될 객체
class Directory // 라이브러리의 사용자가 만든 클래스
{
public:
Directory(params);
};
Directory::Directory(params)
{
size_t disks = tfs.numDisks(); // tfs를 여기서 사용
}
Directory tempDir(params);
tfs가 tempDir보다 먼저 초기화되지 않으면, tempDir의 생성자는 tfs가 초기화되지도 않았는데 tfs를 사용하려고 할 것이다.
이런 문제점을 해결할 방법은 없다. 하지만 설계에 약간의 변화를 주면 문제를 사전에 봉쇄할 수 있다. 비지역 정적 객체를 하나씩 맡는 함수를 준비하고 이 안에 각 객체를 넣는 것이다. 함수 속에서도 이들은 정적 객체로 선언하고, 그 함수에서는 이들에 대한 참조자를 반환하게 만든다. 정리하면, 비지역 정적 객체가 지역 정적 객체로 바뀐 것이다.
지역 정적 객체는 함수 호출 중에 그 객체의 정의에 최초로 닿았을 때, 초기화되도록 만들어져 있다.
class FileSystem {};
FileSystem& tfs() // tfs 객체를 이 함수로 대체
{
static FileSystem fs;
return fs;
}
class Directory {};
Directory::Directory(params)
{
size_t disks = tfs().numDisks();
}
Directory& tempDir() // tempDir 객체를 이 함수로 대체
{
static Directory td;
return td;
}
정적 객체 자체를 직접 사용하지 않고 그 객체에 대한 참조자를 반환하는 함수를 사용하고 있다.
참조자 반환 함수는 내부적으로 정적 객체를 쓰기 때문에, 다중스레드 시스템에서는 동작에 장애가 생길 수도 있다. 다중 스레드에서 비상수 정적 객체(지역 객체이든 비지역 객체이든)는 온갖 골칫거리의 시한폭탄이다. 골칫거리를 다루는 한 가지 방법으로는, 프로그램이 다중스레드로 돌입하기 전의 시동 단계에서 참조자 반환 함수를 전부 손으로 호출해 줄 수 있다.
이것만은 잊지 말자!
- 기본제공 타입의 객체는 직접 손으로 초기화하자. 경우에 따라 저절로 되기도 하고 안 되기도 하기 때문이다.
- 생성자에서는, 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화하지 말고 멤버 초기화 리스트를 즐겨 사용합시다. 그리고 초기화 리스트에 데이터 멤버를 나열할 때는 클래스에 각 데이터 멤버가 선언된 순서와 똑같이 나열하자.
- 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 한다. 비지역 정적 객체를 지역 정적 객체로 바꾸면 된다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 06: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2023.10.02 |
---|---|
[Effective C++] 05: C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (1) | 2023.10.02 |
[Effective C++] 03: 낌새만 보이면 const를 들이대 보자! (0) | 2023.09.28 |
[Effective C++] 02: #define을 쓰려거든 const, enum, inline을 떠올리자 (0) | 2023.09.28 |
[Effective C++] 01: C++를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2023.09.28 |
04: 객체를 사용하기 전에 반드시 그 객체를 초기화하자
초기화 되지 않은 값을 읽는다면 의도되지 않은 동작이 그대로 흘러나오게 된다. 이런 방법을 막기 위해서 가장 좋은 방법은 모든 객체를 사용하기 전에 항상 초기화하는 것이다.
int x = 0; // 직접 초기화
const char* text = "Hello"; // 직접 초기화
double d;
std::cin >> d; // 입력 스트림에서 읽음으로써 초기화 수행
위의 부분을 제외하고는 C++의 초기화는 생성자에서 이루어진다.
대입과 초기화를 헷갈리지 말자
class PhoneNumver {};
class ABEntry
{
public:
ABEntry(const string& name, const string address, const list<PhoneNumver>& phones);
private:
string theName;
string theAddress;
list<PhoneNumver> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const string& name, const string address, const list<PhoneNumver>& phones)
{
theName = name; // 아래는 모두 초기화가 아닌 대입이다.
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 한다고 명시되어있다. 위의 ABEntry의 생성자는 초기화되고 있는 것이 아니라, 어떤 값이 대입되고 있는 것이다. 초기화는 진작 지나갔다. 기본제공 타입의 데이터 멤버인 numTimesConsulted의 경우에는 (생성자 안에서) 대입되기 전에 초기화되리란 보장이 없어 미리 초기화 되고 대입된건지는 알 수 없다.
대입문 대신에 멤버 초기화 리스트를 사용하자
ABEntry::ABEntry(const string& name, const string address, const list<PhoneNumver>& phones)
: theName(name), // 이제 모두 초기화되고 있다.
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{}
앞에서 만든 생성자보다 더 효율적일 가능성이 크다. 대입만 사용한 버전의 경우 theName, theAddress, thePhones의 기본 생성자를 호출해서 초기화를 미리 해 놓은 후에 생성자에서 곧바로 새로운 값을 대입하고 있다. 즉, 먼저 호출된 기본 생성자는 헛짓거리가 된다.
초기화 리스트에 들어가는 인자는 바로 데이터 멤버에 대한 생성자의 인자로 쓰이기 때문에 이 문제를 피해 갈 수 있다. 지금의 경우엔 theName, theAddress, thePhones는 복사 생성자에 의해 초기화된다. 복사 생성자를 한 번 호출하는 쪽이 더 효율적이다.
기본 제공 타입의 객체는 초기화와 대입에 걸리는 비용의 차이는 없지만, 역시 멤버 초기화 리스트에 모두 넣어 주는 쪽이 가장 좋다.
ABEntry::ABEntry(const string& name, const string address, const list<PhoneNumver>& phones)
: theName(), // 기본 생성자로 초기화
theAddress(),
thePhones(),
numTimesConsulted(0) // 명시적으로 초기화
{}
데이터 멤버를 기본 생성자로 초기화되고 싶을 때도 멤버 초기화 리스트를 사용하는 습관을 들이자. 어쩌다가 리스트에서 어떤 멤버를 빼먹었을 때, 어떤 멤버가 초기화되지 않을 수 있다는 사실을 끌고 가야 하는 부담이 없어진다. numTimesConsulted가 멤버 초기화 리스트에서 빠졌다면 기본제공 타입이니까, 이것이 초기화될지 안 될지 장담을 못한다.
기본 제공 타입의 멤버를 초기화 리스트로 넣는 일이 선택이 아니라 의무가 될 때도 있다. 상수이거나 참조자로 되어 있는 데이터 멤버의 경우엔 반드시 초기화되어야 한다. 상수와 참조자는 대입이 불가능하기 때문이다.
현장에서 쓰이는 클래스들 중 상당수가 여러 개의 생성자를 갖고 있다. 각 생성자마다 멤버 초기화 리스트가 붙어 있을 것이다. 이렇게 중복이 심한 경우에는 이들에 대한 대입 연산을 하나의(대개 private 멤버) 함수에 몰아넣고, 모든 생성자에서 이 함수를 호출하게 하는 방법도 있다. 데이터 멤버의 진짜 초기값을 파일에서 읽어온다든지 데이터베이스에서 찾아오는 경우에 특히 유용하다. 그래도 초기화 리스트를 통한게 아무래도 좋다.
객체를 구성하는 데이터의 초기화 순서
- 기본 클래스는 파생 클래스보다 먼저 초기화된다.
- 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화된다.
혼동을 피하기 위해 멤버 초기화 리스트에 넣는 멤버들의 순서도 클래스에 선언한 순서와 동일하게 맞춰 주도록 하자.
비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.
정적 객체의 범주를 보자.
- 전역 객체
- 네임스페이스 유효범위에서 정의된 객체
- 클래스 안에서 static으로 선언된 객체
- 함수 안에서 static으로 선언된 객체
- 파일 유효범위에서 static으로 정의된 객체
이들 중에서 함수 안에서 static으로 선언된 객체는 지역 정적 객체(local static object)라고 하고(함수에 대해서 지역성을 가지므로), 나머지는 비지역 정적객체(non-local static object)라 한다.
번역 단위(translation unit)는 컴파일을 통해 하나의 목적 파일을 만드는 바탕이 되는 소스 코드이다.
문제는 한쪽 번역 단위에 있는 비정적 객체의 초기화가 진행되면서 다른 쪽 번역 단위에 있는 비지역 정적 객체가 사용되는데, 불행히도 이 (다른 쪽 번역 단위에 있는) 객체가 초기화되어 있지 않을지도 모른다는 점이다.
class FileSystem // 라이브러리에 포함된 클래스
{
public:
size_t numDisks() const;
};
extern FileSystem tfs; // 사용자가 쓰게 될 객체
class Directory // 라이브러리의 사용자가 만든 클래스
{
public:
Directory(params);
};
Directory::Directory(params)
{
size_t disks = tfs.numDisks(); // tfs를 여기서 사용
}
Directory tempDir(params);
tfs가 tempDir보다 먼저 초기화되지 않으면, tempDir의 생성자는 tfs가 초기화되지도 않았는데 tfs를 사용하려고 할 것이다.
이런 문제점을 해결할 방법은 없다. 하지만 설계에 약간의 변화를 주면 문제를 사전에 봉쇄할 수 있다. 비지역 정적 객체를 하나씩 맡는 함수를 준비하고 이 안에 각 객체를 넣는 것이다. 함수 속에서도 이들은 정적 객체로 선언하고, 그 함수에서는 이들에 대한 참조자를 반환하게 만든다. 정리하면, 비지역 정적 객체가 지역 정적 객체로 바뀐 것이다.
지역 정적 객체는 함수 호출 중에 그 객체의 정의에 최초로 닿았을 때, 초기화되도록 만들어져 있다.
class FileSystem {};
FileSystem& tfs() // tfs 객체를 이 함수로 대체
{
static FileSystem fs;
return fs;
}
class Directory {};
Directory::Directory(params)
{
size_t disks = tfs().numDisks();
}
Directory& tempDir() // tempDir 객체를 이 함수로 대체
{
static Directory td;
return td;
}
정적 객체 자체를 직접 사용하지 않고 그 객체에 대한 참조자를 반환하는 함수를 사용하고 있다.
참조자 반환 함수는 내부적으로 정적 객체를 쓰기 때문에, 다중스레드 시스템에서는 동작에 장애가 생길 수도 있다. 다중 스레드에서 비상수 정적 객체(지역 객체이든 비지역 객체이든)는 온갖 골칫거리의 시한폭탄이다. 골칫거리를 다루는 한 가지 방법으로는, 프로그램이 다중스레드로 돌입하기 전의 시동 단계에서 참조자 반환 함수를 전부 손으로 호출해 줄 수 있다.
이것만은 잊지 말자!
- 기본제공 타입의 객체는 직접 손으로 초기화하자. 경우에 따라 저절로 되기도 하고 안 되기도 하기 때문이다.
- 생성자에서는, 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화하지 말고 멤버 초기화 리스트를 즐겨 사용합시다. 그리고 초기화 리스트에 데이터 멤버를 나열할 때는 클래스에 각 데이터 멤버가 선언된 순서와 똑같이 나열하자.
- 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 한다. 비지역 정적 객체를 지역 정적 객체로 바꾸면 된다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 06: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2023.10.02 |
---|---|
[Effective C++] 05: C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (1) | 2023.10.02 |
[Effective C++] 03: 낌새만 보이면 const를 들이대 보자! (0) | 2023.09.28 |
[Effective C++] 02: #define을 쓰려거든 const, enum, inline을 떠올리자 (0) | 2023.09.28 |
[Effective C++] 01: C++를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2023.09.28 |