[Effective C++] #4 객체의 초기화
Scott Meyers의 "Effective C++"를 통해, C++ 구현에 필요한 개념들을 이해하고, 기록하기 위함입니다. 해당 항목은 1장 'C++ 기본', 항목 4 "객체를 사용하기 전에 반드시 그 객체를 초기화하자"에 해당하는 내용입니다.
초기화
int x;
double x;
class Point {...};
Point p;
*********************
int x = 0;
double x = 0;
위 예제 코드를 살펴보겠습니다. C++의 경우, 어떤 상황에서는 변수의 값이 초기화되기도, 그렇지 않기도 합니다. 대부분의 경우, 무작위 비트의 값을 읽어 객체의 내부가 알 수 없는 이상한 값을 갖게 되죠. C++의 vector의 원소는 확실히 초기화되는 반면, 일반 배열은 원소가 초기화된다는 보장이 없습니다. 이처럼 불확실한 결과에 기대지 않고, 확실하게 항상 초기화하는 습관이 중요합니다.
대입 vs 초기화
class ABEntry {
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber> numbers);
private:
std::string _name;
std::string _addr;
std::list<PhoneNumber> _phoneNumber;
int numTimesConsulted;
};
ABEntry::ABEntry (const std::string& name, const std::string& address,
const std::list<PhoneNumber> numbers)
{
_name = name;
_addr = address;
_phoneNumber = numbers; // 초기화 X , 대입 O
numTimesConsulted = 0;
}
C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 한다고 명기되어있습니다. 위 코드를 살펴보겠습니다. ABEntry의 생성자는 데이터 멤버들을 초기화 하는 것이 아니라, 값을 대입하고 있죠. 그렇다면 데이터 멤버들을 확실하게 초기화하는 방법은 무엇일까요?
멤버 초기화 리스트
ABEntry::ABEntry (const std::string& name, const std::string& address,
const std::list<PhoneNumber> numbers)
: _name(name),
_addr(address),
_phoneNumber(numbers),
numTimesConsulted(0)
{}
데이터 멤버를 확실하게 초기화하기위해 우리는 멤버 초기화 리스트를 사용합니다. 대입 방법과 비슷하게 데이터 멤버에게 값을 주고 시작하는 것 같지만, 분명 다른 점이 존재합니다. 대입하는 방법은 데이터 멤버에 대해 기본 생성자를 호출해서 미리 초기화한 후에, 생성자 본문 안에서 다시 새로운 값을 대입하도록 되어 있습니다. 기본 생성자 호출 -> 생성자에서 시작 값을 대입, 이러한 작업은 효율적이지 못합니다. 반면, 멤버 초기화 리스트는 데이터 멤버들의 기본 생성자 호출에 들어갈 인자들을 주고 시작합니다. 훨씬 효율적이겠죠. 초기 값을 주고 싶지 않을 때, 우리는 매개변수를 비워 놓으면 됩니다. 부가적으로, "초기화 순서" 또한 존재합니다. 첫 번째는 기본 클래스, 즉 부모 클래스는 자식 클래스들보다 먼저 초기화해야 합니다. 그리고, 멤버 초기화 리스트 작성 시 데이터 멤버를 순서대로 작성하는 것도 좋겠습니다.
불확실한 결과에 기대기 보다, 멤버 초기화 리스트를 사용하여 확실하게 초기화하자
비지역 정적 객체
// 소스파일1.cpp 생성 시기 2020.02.11
class FileClass {
public:
...
std::size_t numDist() const;
...
};
extern FileClass f;
*******************
// 소스파일2.cpp 생성 시기 2021.03.09
class Directory{
public:
Directory(...){
...
std::size_t disks = f.numDisks // FileClass 객체의 멤버 함수를 호출합니다.
}
}
Directory d (...);
먼저 "정적" 객체란 자신이 생성된 시점부터 프로그램이 끝나는 시점까지 살아 있는 객체를 의미합니다. 다양한 종류의 정적 객체가 존재하며, 크게 두 가지로 "지역 정적 객체"와 "비지역 정적 객체"입니다. 이 두 가지 정적 객체는 "유효 범위"에 따른 분류입니다. 지역성을 갖느냐 갖지 않느냐의 차이죠. 책에서 제기한 문제점은 이겁니다. 별도로 컴파일된 소스 파일이 두 개 이상 있고, 각 소스 파일의 비지역 정적 객체들은 초기화 순서가 정해져 있지 않습니다. 서로 다른 소스 파일에서 어느 비지역 객체가 먼저 초기화될지 알 수 없다는 것이죠. 위 예제 코드를 살펴보겠습니다. 서로 다른 시기에, 그리고 서로 다른 소스파일에 작성되어 있는 FileClass 객체와 Direcotry 객체입니다. DIrectory 클래스의 생성자가 본문 안에 FileClass 객체의 멤버 함수를 호출하고 있죠. 이들은 다른 번역 단위(소스 코드가 이루는 목적 파일) 안에 존재하는 비지역 정적 객체들입니다. 만약, FileClass 객체 "f"가 Directory 객체 "d"보다 먼저 초기화되어있지 않다면, 큰 문제가 발생하겠죠. 이렇게 비지역 정적 객체들 간의 무질서한 초기화 순서 문제는 어떻게 해결할 수 있을까요?
싱글톤 패턴, 비지역 정적 객체 -> 지역 정적 객체 변환
// 소스코드1.cpp
class FileClass {...};
FileClass& f()
{
static FileCalss _f;
retunr _f;
}
// 소스코드2.cpp
class Directory {
public:
Direcotry(){
...
std::size_t disks = _f().numDisks(); //_f.numDisks()에서 _f().numDisks()로 변환
}
};
Direcotry& d()
{
static Directory _d;
return _d;
}
번역 단위별 비지역 정적 객체들간의 무질서한 초기화 순서 문제를 해결하기 위해 우리는 "비지역 정적 객체"를 담당하는 개별적인 함수들을 사용합니다. 이 함수들은 각각의 비지역 정적 객체들의 참조자를 반환합니다. 결국, "비지역 정적 객체"를 "지역 정적 객체"로 바꾸는것이죠. 지역 정적 객체의 경우, 함수 호출 중에 컴파일러가 그 객체의 정의에 최초로 닿았을 때 초기화되겠죠. 더불어, 사용자는 정적 객체 자체를 사용하기보다, 그 객체에 대한 참조자를 반환하는 함수를 사용하게 됩니다. 물론, 소스 코드 작성 시 각 객체의 초기화 순서를 맞추어 주어야겠죠. 이러한 가정을 전제로, 안전하게 정적 객체들의 초기화 순서를 제어할 수 있게 됩니다.
직접 초기화, 멤버 초기화 리스트, 그리고 비지역 정적 객체들 간의 무질서한 초기화 순서 제어가 중요합니다.
'언어 > Effective C++' 카테고리의 다른 글
[Effective C++] #6 사용자 정의 기본 멤버 함수 (0) | 2022.01.07 |
---|---|
[Effective C++] #5 생성자, 소멸자, 복사 생성자, 복사 대입 연산자 (0) | 2022.01.06 |
[Effective C++] #3_const 사용 (0) | 2022.01.05 |
[Effective C++] #2_#define, 매크로 사용의 대안 (0) | 2022.01.03 |
[Effective C++] #33_오버라이딩 문제 (0) | 2022.01.03 |