Effective C++ 3rd Edition. Scott Meyers.
C++ 프로그래머의 필독서라고 불리는 Effective C++을 읽고 중요한 내용을 정리한 글 입니다.
Item4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자.
초기화되지 않은 값을 읽도록 내버려 두면 해당 객체에 쓰레기 값이 들어가 있게되어 프로그램이 내가 의도한대로 흘러가지 않을 가능성이 매우 크다. 따라서 초기화가 중요하다.
int x;
class Point
{
int x, y;
};
Point p;
이런식으로 객체의 값을 명시적으로 초기화 해주지 않았다면, 어떤 상황에서는 초기화가 보장되지만, 또 어떤 경우에서는 안된다. 이것만 보면 C++의 객체 초기화가 중구난방인 것처럼 보이겠지만, 그런 것은 절대 아니다. 언제 초기화가 보장되고, 또 언제는 그렇지 않은지에 대한 규칙이 명확히 정의되어있다. 하지만, 이러한 규칙자체가 조금 복잡하기 때문에(필자는 머리에 새겨둘 가치가 있다기엔 너무 복잡하다고 말한다.) 가장 좋은 방법은 모든 객체를 사용하기 전에 항상 초기화 하는 것이다.
🔸 기본제공 타입의 초기화
기본제공 타입(Primitive Type)으로 만들어진 비멤버 객체에 대해서는 아래와 같이 초기화를 손수 해주어야 한다.
int x = 0; // int를 직접 초기화
const char *text = "A C-style string"; // 포인터의 직접 초기화
double d; // 입력 스트림에서 읽음으로써
std::cin >> d; // 초기화 수행
🔸 데이터 멤버의 초기화
이런 부분을 제외하면, C++ 초기화의 나머지 부분은 생성자로 귀결되게 된다. 생성자에서 지킬 규칙은 간단하다. 그 객체의 모든 것을 초기화하자!
지키기 쉬운 규칙이지만, "대입(assignment)"을 "초기화(initialization)"와 헷갈리지 않는 것이 매우 중요하다.
class PhoneNumber
{
...
};
class ABEntry
{ // ABEntry = “Address Book Entry”
public:
ABEntry(const std::string &name, const std::string &address,
const std::list<PhoneNumber> &phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string &name, const std::string &address,
const std::list<PhoneNumber> &phones)
{
theName = name; // 이것들은 모두 "대입"이다.
theAddress = address; // "초기화"가 아니다.
thePhones = phones;
numTimesConsulted = 0;
}
이렇게 하면 ABEntry 객체는 우리가 의도했던 값을 가지게될 것이다. 하지만, C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자가 실행되기 이전에 초기화되어야 한다고 명시되어 있다, 위에서 한 것은 초기화를 하고 있는 것이 아니라 어떤 값이 대입되고 있는 것이다. 초기화는 ABEntry에 생성자에 진입하기 전에 데이터 멤버들의 기본 생성자가 이미 호출되었다. 기본 제공 타입의 경우는 대입되기 전에 초기화되리란 보장은 없다.
대입을 통해 값을 넣게되면, 각 데이터 멤버들의 기본 생성자를 호출해서 초기화를 미리 해놓은 후 생성자에서 대입을 통해 새로운 값을 넣게된다. 이렇게되면 기본생성자 초기화 후 대입하기 때문에 기본생성자로 초기화한게 아무 의미없게 된다. 따라서, 비효율적이다.
그렇다면 생성자가 실행되기 이전에 초기화를 하는 방법은 무엇일까? 바로 멤버 초기화 리스트를 사용하면 된다.
ABEntry::ABEntry(const std::string &name, const std::string &address,
const std::list<PhoneNumber> &phones)
: theName(name),
theAddress(address), // 모두 초기화 되고 있다.
thePhones(phones),
numTimesConsulted(0)
{
}
이런식으로 초기화 리스트를 사용하면, 대입없이 객체들에 의도한 값을 넣을 수 있게된다. 이렇게 사용하는 경우가 대부분의 데이터 타입에 대해서 효율적이다. 추가적인 대입의 작업이 실행되지 않기 때문이다.
기본제공 타입에 대해서는 초기화와 대입에 걸리는 비용의 차이가 없지만, 역시나 초기화 리스트에 넣어주는 쪽이 좋다. 또 데이터 멤버를 기본 생성자로 초기화 하고싶을 때도 멤버 초기화 리스트를 사용하는 습관을 들이자. 이런 습관을 들여야 어떤 멤버를 리스트에서 빼먹었을 때, 어떤 멤버가 초기와되지 않을 수 있다는 사실을 끌고 가야하는 부담이 없어지게 된다.
ABEntry::ABEntry()
: theName(),
theAddress(),
thePhones(),
numTimesConsulted(0)
{
}
기본제공 타입의 멤버를 초기화해야하는게 의무인 상황도 있는데, 멤버가 상수나 참조자일 경우에는 대입이 불가능 하기때문에 초기화를 무조건 해주어야한다. 하지만 이런 경우를 생각할 필요도 없이 멤버 초기화 리스트를 항상 사용하는 편이 더 쉬울 것이다.
데이터의 초기화 순서는 기본 클래스는 파생 클래스보다 먼저 초기화되고, 클래스 데이터 멤버는 선언된 순서대로 초기화된다. 따라서, 코드 가독성이나, 버그를 피하자는 의미에서 초기화 리스트에 넣는 멤버들의 순서도 선언한 순서와 동일하게 맞추어 주자.
🔸 비지역 정적 객체의 초기화
비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다. 이게 무슨말인지 모르겠으니 단어 하나하나씩 이해해보자.
정적 객체 : 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체
- 전역 객체
- 네임스페이스 유효범위에서 정의된 객체
- 클래스 안에서 static으로 선언된 객체
- 함수 안에서 static으로 선언된 객체
- 파일 유효범위에서 static으로 정의된 객체
이들 중 함수 안에 있는 정적 객체를 지역 정적 객체, 나머지는 비지역 정적 객체라고 한다.
번역 단위 : 컴파일을 통해 하나의 목적 파일을 만드는 바탕이 되는 소스 코드. 일반적으로 소스파일 + include하는 파일들
여기서 문제는 별도로 컴파일된 소스 파일이 두 개 이상 있으며 각 소스 파일에 비지역 정적 객체가 한 개 이상 들어 있는 경우에 한쪽 번역 단위에 있는 비정적 객체의 초기화가 진행되면서 다른 쪽 번역 단위에 있는 비지역 정적 객체가 사용되는데, 이때 두 비지역 정적 객체 사이의 초기화 순서는 정해져있지않다.
//소스코드 f
class FileSystem
{
public:
... std::size_t numDisks() const;
};
extern FileSystem tfs; // 쓰게될 객체
//소스코드 d
class Directory
{
public:
Directory(params);
};
Directory::Directory(params)
{
std::size_t disks = tfs.numDisks(); // tfs를 사용한다.
}
Directory tempDir(params);
이러한 상황에서 tempDir을 초기화하는 시점에 tfs가 초기화되지 않을 수도 있다는 뜻이다. 두 비지역 정적 객체들 사이의 초기화 순서는 정해져있지 않기 때문이다. 따라서 이러한 상황을 해결하기위해 비지역 정적 객체를 하나씩 맡는 함수를 준비하고, 이안에 각 객체를 넣어서, 해결할 수 있다. 함수 속에서 이들을 정적 객체로 선언하고 그 함수에서는 이들에 대한 레퍼런스를 반환하게 만든다. 이렇게되면 비지역 정적 객체가 지역 정적 객체로 바뀌게 된 것이다.
class FileSystem
{
...
};
FileSystem &tfs() // tfs객체를 이 함수로 대신한다.
{
static FileSystem fs; // 지역 정적 객체를 정의하고 초기화
return fs; // 레퍼런스를 반환
}
class Directory
{
...
};
Directory::Directory(params)
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory &tempDir() //tempDir객체를 이 함수로 대신한다.
{
static Directory td(params); // 지역 정적 객체를 정의하고 초기화
return td; // 레퍼런스를 반환
}
이런 식으로 정적 객체 자체를 직접 사용하지 않고 그 객체에 대한 참조자를 반환하는 함수를 사용하는 것이다. 이 안에 정적 객체는 지역 정적 객체이므로 위의 문제가 발생하지 않을 것이다.
🔸멀티스레드 환경에서는...
참조자 반환 함수는 내부적으로 정적 객체를 쓰기 때문에, 다중스레드 시스템에서는 동작에 장애가 생길 수도 있다. 하지만 다중스레드로 돌입하기전에 참조자 반환 함수를 손으로 모두 호출해주면 초기화에 관계된 경쟁 상태가 없어진다.
💡 핵심!
🔸 기본제공 타입의 객체는 직접 손으로 초기화하자. 경우에 따라 저절로 되기도하고 안되기도 하기 때문
🔸 생성자에서는 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화하지 말고 멤버 초기화
리스트를 즐겨 사용하자. 그리고 초기화 리스트에 데이터 멤버를 나열할 때는 클래스에 각 데이터 멤버가 선언된 순
서와 똑같이 나열하자.
🔸 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문자는 피해서 설계해야 한다. 비지역 정적객체를 지역 정적
객체로 바꾸면 된다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 6. 컴파일러가 만들어낸 함수가 필요없으면 확실히 이들의 사용을 금해버리자 (0) | 2022.11.10 |
---|---|
[Effective C++] 5. C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (0) | 2022.11.10 |
[Effective C++] 3. 낌새만 보이면 const를 들이대 보자! (0) | 2022.11.09 |
[Effective C++] 2. #define을 쓰려거든 const, enum, inline을 떠올리자. (0) | 2022.11.08 |
[Effective C++] 1. C++을 언어들의 연합체로 바라보자. (2) | 2022.11.08 |
게임개발자를 꿈꾸는 대학생의 개발 공부 블로그
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!