이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 71p ~ 79p
요약
항목 4: 객체를 사용하기 전에 반드시 그 객체를 초기화하자
- C++에서 초기화되지 않은 값을 읽도록 내버려 두면 정의되지 않은 동작이 발생한다.
- C++의 객체(변수) 초기화 규칙이 분명히 준비되어 있지만, 규칙 자체가 복잡하다.
- 일반적인 사항을 정리하면 다음과 같다.
- C++의 C 부분만을 사용하며 초기화에 런타임 비용이 소모될 수 있는 상황이라면 값이 초기화된다는 보장이 없다.
- 그러나 C 부분이 아닌 다른 부분을 사용하면 상황이 달라지는 경우가 있다.
- 배열을 사용하면(C++의 C 부분) 원소가 확실히 초기화된다는 보장이 없지만, vector(C++의 STL 부분)을 사용하면 이를 보장해준다.
- 따라서 가장 좋은 방법은 모든 객체를 사용하기 전에 항상 초기화하는 것이다.
기본제공 타입인 비멤버 객체
- 기본제공 타입으로 만들어진 비멤버 객체에 대해서는 직접 초기화를 해야 한다.
int x = 0; // int의 직접 초기화
const char* text = "A C-Style string"; // 포인터의 직접 초기화
double d; // 입력 스트림에서 읽음으로 써 초기화 진행
std::cin >> d;
멤버 객체
- 이런 부분을 제외하고 나면, C++ 초기화의 나머지 부분은 생성자로 귀결된다.
- 생성자에서 지킬 규칙은 해당 객체의 모든 것을 초기화하는 것이다.
- 생성자에서 대입과 초기화를 헷갈리지 말아야 한다.
class PhoneNumber {
};
// Address Book Entry
class ABEntry {
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++ 규칙에 의하면 어떤 객체든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 하기 때문에 멤버 초기화 리스트를 사용해주자.
numTimesConsulted
는 기본제공 타입이기 때문에 생성자에서 대입되기 전에 초기화된다는 보장이 없다.
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{} // 초기화, 복사 생성자가 호출됨
- 초기화 리스트에 들어가는 인자는 바로 데이터 멤버에 대한 생성자의 인자로 사용된다. 따라서 생성자가 호출되기 전에 데이터 멤버에 생성자가 호출되는 헛짓거리를 방지할 수 있다.
- 기본제공 타입의 객체는 초기화와 대입에 걸리는 비용 차이가 없지만, 멤버 초기화 리스트에 넣어주는 것이 가독성 측면에서 좋다.
- 상수이거나 레퍼런스 형식의 기본제공 타입 데이터 멤버는 반드시 초기화해야 한다.
- 기본 생성자로 초기화하고 싶으면 초기화 리스트에 인자를 아무것도 사용하지 않으면 된다.
- 컴파일러는 데이터 멤버가 사용자 정의 타입이면 자동으로 기본 생성자를 호출해준다.
- 하지만 초기화를 한 곳에서 진행함으로 프로그래머의 실수를 줄일 수 있다.
- 만약 클래스의 크기가 커져서 여러 개의 생성자를 가지고 있고, 각 생성자마다 멤버 초기화 리스트를 가지고 있으면 코드 가독성이 떨어지게 된다.
- 이러한 경우에는 대입으로도 초기화가 가능한 데이터 멤버들을 초기화 리스트에서 빼내어 별도의 함수로 옮기는 것도 나쁘지 않다.
- C++에서 객체를 구성하는 데이터의 초기화 순서는 다음과 같다.
- 기본 클래스는 파생 클래스보다 먼저 초기화된다.
- 클래스 데이터 멤버는 선언된 순서대로 초기화된다.(멤버 초기화 리스트에 순서를 바꿔 넣어도 선언된 순서로 초기화됨)
비지역 정적 객체
- 비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.
정적 객체(static object)
- 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체
- 스택, 힙 기반 객체는 애초부터 정적 객체가 될 수 없다.
- 정적 객체의 범주에 들어가는 것들
- 전역 객체
- 네임스페이스 유효범위에서 정의된 객체
- 클래스 안에서 static으로 선언된 객체
- 함수 안에서 static으로 선언된 객체
- 파일 유효범위에서 static으로 정의된 객체
- 정적 객체 중에서 함수 안에 있는 정적 객체는 지역 정적 객체이고, 나머지는 모두 비지역 정적 객체이다.
- 정적 객체는 모두 프로그램이 끝날 때 자동으로 소멸된다.
번역 단위(translation unit)
- 컴파일을 통해 하나의 목적 파일(object file)을 만드는 바탕이되는 소스 코드를 일컫는다.
- 번역의 의미는 소스의 언어를 기계어로 옮긴다는 의미이다.
- 기본적으로는 소스 파일 하나인데, 해당 파일이
#include
하는 파일까지 합쳐서 하나의 번역 단위가 된다.
- 별도로 컴파일된 소스 파일이 두 개 이상 있으며 각 소스 파일에 비지역 정적 객체(전역 객체, 네임스페이스에 있는 객체, 클래스 혹은 파일에 있는 정적 객체)가 한 개 이상 있을 때 초기화 순서는 어떻게 되는가?
- 한쪽 번역 단위에 있는 비지역 정적 객체의 초기화가 진행되면서 다른쪽 번역 단위에 있는 비지역 정적 객체가 사용되는데, 다른쪽 비지역 정적 객체가 초기화되어 있지 않다면?'
- 비지역 정적 객체를 하나씩 맡는 함수를 준비하고 이 안에 각 객체를 넣는 방식으로 비지역 정적 객체를 직접 참조하지 않고 지역 정적 객체의 주소값을 리턴하는 함수를 사용한다.(싱글턴 패턴)
- 지역 정적 객체는 함수 호출 중에 그 객체의 정의에 최초로 닿았을 때 초기화되도록 만들어져 있다.
메모
- 멤버가 아닌 기본제공 타입 객체는 손수 초기화한다.
- 객체의 모든 부분에 대한 초기화에는 멤버 초기화 리스트를 사용한다.
- 별개의 번역 단위에 정의된 비지역 정적 객체에 영향을 끼치는 불확실한 초기화 순서를 염두에 두고 프로그램을 설계한다.
- 비지역 정적 객체를 지역 정적 객체로 바꿔 사용하는 대안이 있다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(89p ~ 99p) (1) | 2024.01.12 |
---|---|
이펙티브 C++(80p ~ 89p) (1) | 2024.01.11 |
이펙티브 C++(61p ~ 70p) (0) | 2024.01.09 |
이펙티브 C++(51p ~ 60p) (1) | 2024.01.08 |
이펙티브 C++(35p ~ 50p) (0) | 2024.01.04 |