C++은 어떤 경우에는 초기화를 해주고 어떤 경우에는 초기화를 해주지 않는 것 처럼 보인다.
그러다가 결국 초기화되지 않은 멤버에 접근하면 프로그램이 종료되거나 다른 행동을 하고 만다.
물론 C++ 은 정해진 규칙에 따라서 초기화를 수행하는데 복잡한 규칙을 가지고 있을 뿐이다.
가장 좋은 방법은 모든 객체를 사용하기 전에 항상 초기화 하는 것이다.
int x = 0;
const char* text = "A C-style string"; //포인터 직접 초기화
double d;
std::cin >> d; // 입력 스트림으로 읽으면서 초기화
기본 제공 타입으로 만들어진 비멤버 객체에 대해서는 위의 예시와 같이 수행하면 됩니다.
그렇다면 남은 것은 생성자를 통해서 초기화를 수행하면 되는데 규칙을 알아봅시다.
위의 규칙만 지킨다면 간단합니다.
주의할 점은 대입(assignment)을 초기화(initialization)와 헷갈리지 않아야합니다.
예를 들어서 클래스의 생성자가 다음과 같이 있다고 합시다. (선언은 이미 되어있다고 생각)
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
{
theName = name; // 이것은 모두 '대입'입니다.
theAddress = address; // '초기화'가 아닙니다.
thePhones = phones;
numTimesConsulted = 0; //int
}
위의 예시는 결국 '대입'입니다. 초기화는 이미 수행되고 난 이후의 일이라는 것입니다.
ABEntry 생성자에 진입하기도 전에 theName, theAddress, thePhones의 기본 생성자는 호출되었습니다.
numTimeConsulted는 기본제공 타입이기에 대입 전에 초기화가 꼭 보장되는 것은 아니기에 확실치는 않습니다.
저희는 초기화를 위해서 멤버 초기화 리스트를 사용하면 됩니다.
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
:theName(name),
theAddress(address), // 이것은 모두 '초기화' 되고 있습니다.
thePhones(phones),
numTimesConsulted(0)
{}
위의 예시는 초기화 시점에서 전달한다는 점에서 차이가 큽니다.
이는 복사 생성자에 의해서 초기화가 된다는 것이고, 초기화 리스트를 사용하지 않으면 복사 대입 연산자에 의해서 생성자 이후 불리는 것과는 비용차이가 있습니다.
물론 괄호를 비워놓으면 기본 생성자가 호출되지만, 그래도 항상 초기화 리스트에 작성하는 습관을 들이는 것이 중요합니다.
만일 기본제공 타입을 사용 시에 초기화되지 않는 등의 미정의 행동을 유발할 수 있기 때문입니다.
또한, 초기화 리스트에 상수이거나 참조인 경우에는 반드시 초기화 해주어야 합니다.
왜냐하면 대입 자체가 불가능하기에 초기화 단계에서 꼭 지정해주어야 하기 떄문입니다.
위 두 규칙은 어느 컴파일러건 동일하니 숙지해놓는 것이 좋습니다. 선언되 순서이기에 초기화 리스트의 순서는 상관이 없다는 점도 명심해야 합니다.
정적 객체는 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아있는 객체 입니다.
당연히 스택, 힙 기반 객체는 애초부터 정적 객체가 될 수 없는 겁니다.
정적 객체의 범주에 속하는 것은
가 있습니다.
이들 중 함수 안에 있는 정적 객체만이 지역 정적 객체이고 나머지는 비지역 정적 객체 입니다.
번역 단위는 컴파일을 통해 하나의 목적 파일을 만드는 바탕이되는 소스 코드를 말합니다.
번역은 소스의 언어를 기계어로 옮긴다는 의미입니다. 기본적으로는 소스 파일 하나가 되는데 이 것이 #include하는 파일까지 합쳐서 하나의 번역 단위가 되는 겁니다.
그래서 위의 규칙의 의미는 별도로 컴파일된 소스 파일이 2개 이상 있으며 각 소스 파일에 비지역 정적 객체가 한 개 이상 들어있는 경우에는 어떻게 되느냐는 설명입니다.
문제는 한쪽 번역 단위에서 비정적 객체의 초기화가 진행되면서 다른 쪽 번역 단위에서 비지역 정적 객체를 사용하는데 이게 초기화가 안 되었을 수도 있다는 말입니다.
class FileSystem{ // 라이브러리에 포함된 클래스
public :
...
std::size_t numDisks() const; // 많은 멤버 함수들 중 하나
...
};
extern FileSystem tfs; // 사용자가 쓰게 될 객체
// "tfs" = "the file system"
사용자 코드
class Directory{
public:
Directory( params );
...
};
Directory::Directory( params )
{
...
std::size_t disks = tfs.numDisks(); // tfs 사용
...
}
여기서 멈추지 않고 사용자가 임시 파일을 담는 디렉토리 객체까지 하나 생성한다고 가정합시다.
Directory tempDir( parms );
만약 tfs가 tempDir보다 먼저 초기화되지 않았다면 어떻게 될까요?
tfs와 tempDir는 다른 번역 단위 안에서 정의된 비지역 정적 객체입니다.
그럼 어떻게 초기화 순서를 보장할 수 있을까요??
없습니다.
초기화 순서에 대한 보장은 어렵기에 다른 해결책을 사용해야합니다.
class Fielsystem{...} //이전과 동일
FileSystem& tfs() // 지역 정적 객체를 정의하고 초기화, 이 객체에 대한 참조자를 반환
{
static FileSystem fs;
return fs;
}
class Directory { ... };
Directory::Directory(params)
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir() // 지역 정적 객체를 정의하고 초기화, 이 객체에 대한 참조자를 반환
{
static Directory td;
return td;
}
정적 객체를 직접 사용하지 않고 그 객체에 대한 참조자를 반환하는 함수로 활용을 합니다.
이것은 '비지역 정적 객체'를 '지역 정적 객체'로 수정한 것 뿐입니다.
지역 정적 객체는 함수 호출 중에 그 객체의 정의에 최초로 닿을 때, 초기화되도록 만들어져 있기에 가능한 방법입니다.