본문 바로가기
서적 정리/Effective C++

4.객체를 사용하기 전에 반드시 그 객체를 초기화하자

by 민돌이2 2021. 12. 10.

학교에서도 들었던 말이다. 변수나 객체를 선언할 때 같이 초기화를 해야 효율이 좋다. 또한 초기화를 하지 않으면 쓰레기 값이 들어가는 경우가 있어 에러를 유발할 수 있으니 항상 선언할 떄 초기화하는 습관을 들이라고

 

C++에서는 초기화시기가 중구난방인 것은 아니다. 언제 초기화가 보장되며 언제 그렇지 않은지에 대한 규칙이 명확히 준비되어 있다. 하지만 많이 복잡하여 외울 가치는 미미하다고 한다.

C++의 C부분 예를 들어 int나 float같이 C에서 내려온 것들은 초기화에 런타임 비용이 소모될 수 있는 상황이라면 초기화된다는 보장이 없다. 배열(C++의 C부분)은 각 원소가 확실히 초기화 된다는 보장이 없으나, vector(C++의 STL)는 각 원소가 확실히 초기화된다는 보장이 있다.

일일히 외울 필요 없이 초기화하는 습관이 있다면 신경쓰지 않아도 되는 부분이기에 초기화하는 습관들 들이자.

 

초기화라고 생각했던 것이 알고보니 대입이였다.

이번 챕터에서 가장 놀랐던 부분이다. 포인터 변수같은 경우엔 NULL로 초기화 했지만 int 나 bool 같은 경우 어차피 생성자에서 초기화하는데 의미 선언할 때 초기화할 이유가 있나? 했지만 내가 했던 생성자에서의 초기화는 대입이였다.

class PhoneNumber {  };

class ABEntry
{
public:
	ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
	{
		theName = name; //초기화x 대입o
		theAddress = address; //초기화x 대입o
		thePhones = phones; //초기화x 대입o
		numTimesConsulted = 0; //초기화x 대입o
	}

private:
	string theName;
	string theAddress;
	list<PhoneNumber> thePhones;
	int numTimesConsulted;
};

초기화라고 생각했지만, 대입하고 있었던 것이다. 솔직히 기능자체에는 문제가 없다고 생각하지만, C++규칙에 의하면 어떤 객체든 그 객체의 멤버 변수는 생성자의 본문이 실행되기 전에 초기화되어야 한다.

대입문 대신 멤버 초기화 리스트를 사용하여야 한다.

class PhoneNumber {  };

class ABEntry
{
public:
	ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
		: theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0)
	{ }

private:
	string theName;
	string theAddress;
	list<PhoneNumber> thePhones;
	int numTimesConsulted;
};

멤버 변수를 개발자가 원하는 값을 주고 생성한다는 점에서 같지만, 이전 것보다 효율적일 가능성이 크다. 초기화 리스트에 들어가는 인자는 바로 멤버 변수에 대한 생성자의 인자로 쓰이기 때문이다. 이전 방법처럼 생성자 호출 후에 복사 대입 연산자를 연달아 호출하는 것보다 복사 생성자를 한 번 호출하는 쪽이 효율적이기 때문이다. theName은 name으로 부터 복사생성자에 의해 초기화된다. theAddress, thePhones역시 복사생성자에 의해 초기화된다.

상수를 초기화 하는 경우에는 초기화 리스트를 선택이 아닌 의무가 된다. 상수와 참조자는 대입 자체가 불가능 하기 때문이다(5장 참고).

 

C++데이터의 초기화 순서

기본 클래스는 파생 클래스보다 먼저 초기화된다(12장 참고).

부모 클래스가 생성되고 뭔지 알아야 상속받는 자식 클래스가 부모 클래스에서 무엇을 상속받았는지 이해할 수 있는 것은 당연한 이치.

 

클래스 멤버 변수는 그들이 선언된 순서대로 초기화된다.

위에 ABEntry클래스에서는 theName, theAddress, thePhones, numTimesConsulted순으로 초기화 된다. 초기화 리스트에서 순서가 다르더라도 초기화 순서는 그대로 이기에 컴파일자체는 되나, 혹시나 모를 에러를 예방하기 위해서 선언 순서대로 초기화하는 습관들 들이자.

 

정적 객체(Static Object)

생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체. 스택 및 힙 기반 객체는 애초에 정적 객체가 될 수 없다(*주 1).

정적 객체의 범주에 들어가는 것은

1.우선 전역 객체가 있다

2.네임스페이스 유효범위에서 정의된 객체

3.클래스 안에서 static으로 선언된 객체

4.함수 안에서 static으로 선언된 객체

5.파일 유효범위에서 static으로 정의된 객체

위 5가지 정적 객체중 4번 함수 안에 있는 정적 객체는 지역 정적 객체(local static object)라고 하고 나머지는 비지역 정적 객체(non-local static object)라고 한다.

 

별도로 컴파일된 소스 파일이 두 개 이상일때, 각 소스 파일에 비지역 정적 객체(정적 객체 종류1, 2, 3, 5)가 한개 이상 있는 경우 문제가 발생한다. 한쪽 번역 단위(*주 2)에 있는 비정적 객체가 초기화가 진행 되면서 다른쪽 번역 단위에 있는 비지역 정적 개체가 사용될 때 다른쪽 번역 단위에 있는 객체가 초기화되지 않았을지 모른다는 점(C++ 데이터의 초기화 순서에서 보면 초기화가 보장되어 있지 않기 때문)

쉽게 생각해서 파일 A, B가 있을 때 A를 초기화 하기 위해서 B가 필요한데 B가 완료되어있지 않다면 A는 초기화 할 수가 없다.

좀 더 간단하게 비유해보자면 멀티스레딩의 문제점 생각해봐라

class FileSystem
{
public:
	size_t numDisk() const;

};

extern FileSystem tfs; //사용자가 사용하게 될 객체

 

class Directory
{
public:
	Directory()
	{
		size_t disk = tfs.numDisk(); //tfs 객체 사용처. 만약 초기화가 안되어있다면?
	}
};

설계에 약간의 변화만 주면 문제를 컴파일 순서에 구애받지않을 수 있다.

비지역 정적 객체를 하나씩 맡는 함수를 준비하고 이 함수에 각 객체를 넣는 것이다. 함수 속에서 정적 객체로 선언하고, 그 함수에서는 정적 객체에 대한 참조자를 반환하게 한다. 사용자 쪽에서는 비지역 정적 객체를 직접 참조하는 방식에서 함수 호출로 대신한다. 싱글톤 패턴(Singleton Pattern)방식 느낌이다.

class FileSystem
{
public:
	size_t numDisk() const;
    
};

FileSystem& tfs()
{
	static FileSystem fs;
    
	return fs;
}

 

class Directory
{
public:
	Directory()
	{
		size_t disk = tfs().numDisk(); //tfs.numDisk()에서 tfs().numDisk로 변경
	}
};

 

이것만은 잊지 말자

기본제공 타입의 객체는 직접 손으로 초기화한다(초기화 보장 x).

생성자에서는 멤버 변수에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화(대입) 하지말고 멤버 초기화 리스트를 상용하자.

초기화 리스트에 멤버 변수를 나열할 떄는 클래스 각 멤버 변수가 선언된 순서와 똑같이 하자.

여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 한다(비지역 정적 객체를 지역 정적 객체로 바꾸기).

 

(*주 1) 할당되는 메모리 구조

코드 영역 : 코드가 저장됨, CPU는 코드영역에 저장된 명령문을 수행

데이터 영역 : 초기값이 있는 전역 변수, 정적(static) 변수

BSS : 초기값이 없는 전역 변수, 배열, static
힙 영역 : 동적 할당

스택 영역 : 지역 변수, 매개 변수

힙, 스택 영역은 runtime에 크기 결정하고, 코드, 데이터, BSS 영역은 컴파일시에 크기 결정

 

(*주 2)번역 단위(translation unit)

컴파일을 통해 하나의 목적 파일(object file)을 만드는 바탕이 되는 소스코드

여기서 번역이란 소스의 언어(개발자의 코드)를 기계어로 옮긴다는 의미가 된다.

파일이 #include하는 파일들까지 합쳐서 하나의 변역 단위가 된다.

728x90

댓글