[Effective C++] Item 4: 객체를 사용하기 전에 반드시 그 객체를 초기화하자
Item 4: 객체를 사용하기 전에 반드시 그 객체를 초기화하자
객체의 값을 초기화하는 데 있어서 C++의 행보 많은 변화가 있었다. int x;
의 경우에는 확실하게 0으로 초기화되지만 다른 경우에는 초기화가 보장되지 않는다.
class Point {
int x, y;
}
Point p;
이렇게 사용하는 경우 p
의 데이터 멤버 역시 어떤 상황에서는 초기화가 보장되지만 어쩔 때는 안되는 경우가 있다. 만약 사용자가 초기화되지 않은 값을 사용한다면 프로그램이 원하지 않는 동작으로 흘러가거나 프로그램이 멈춰버리는 경우도 종종 있다.
C++의 초기화가 항상 중구난방인 것은 아니지만 복잡한 면이 많다. 이를 조금 정리한다면 다음과 같다.
C++의 C부분만을 쓰고 있으며 런타임 비용이 소모될 수 있는 상황이라면 값이 초기화된다는 보장이 없다. 그렇지만 C가 아닌 부분으로 발을 걸치게 되면 상황이 달라진다. 예를 들어 C++의 Vector나 String 같은 클래스를 사용한다면 값이 초기화된다는 보장이 있다.
이러한 특성이 매우 넓게 퍼져 있기 때문에 가장 쉽고 좋은 방법은 괄호와 같이 모든 객체에 사용하기 전 포기화를 해주는 것이다.
int x = 0;
const char* text = "Hello";
double d;
std::cin >> d;
위의 초기화 코드를 제외하면 대부분은 C++ 초기화의 나머지 부분은 생성자로 귀결된다. 생성자 지킬 규칙은 지극히 간단하다. 그 객체의 모든 것을 초기화하면 된다.
생성자 초기화 리스트
다만, 대입(assignment)을 초기화(initialization)와 혼동해서는 안된다. 많은 사람들이 생성자에서 값을 초기화한다고 생각하지만, 이는 초기화가 아닌 대입이다.
실제로 C#에서도 동일한 내용의 Item이 존재하며 default로 초기화 된 뒤 생성자의 대입 연산이 호출된다. 명확한 초기화를 하고 싶다면 초기화 리스트를 사용하자.
c++의 멤버 초기화 리스트를 사용하면 기본 초기화 이후 대입되는 헛짓거리를 줄일 수 있다. 실제로도 훨씬 더 효율적이기도 하다. 또한 기본 생성자로 초기화하고 싶을 때도 멤버 초기화 리스트를 사용하는 습관을 가지자. 빼먹을 가능성이 생각보다 크게 굴러올 수 있다.
초기화 리스트의 의무적 사용
또한 선택이 아닌 의무가 될 때도 있는데, 바로 상수이거나 참조자로 되어 있는 데이터 멤버의 경우엔 반드시 초기화되어야 한다. 사실 당연한 이야기인 상수와 참조는 대입 자체가 불가능하기 때문에 이를 깊게 이해하는 것도 좋지만 프로그래밍 할때는 쉽게 모두 초기화 리스트를 사용하는 것이 좋다.
c++ 초기화 순서
C++에서 초기화 순서는 항상 고정적이기 때문에 잘 알아두는 것이 좋다. 기본 클래스는 파생 클래스보다 먼저 초기화되고, 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화된다. 초기화 리스트의 넣어진 순서가 다르더라도 초기화 순서는 그대로이지만, 편의를 위해 초기화 리스트의 순서를 선언 순서와 일치시키는 것이 좋다.
비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다
정적 객체는 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체를 말한다. 그러니까 스택 객체 및 힙 기반 객체는 애초부터 정적 객체가 될 수 없다. 정적 객체의 범주에 들어가는 것은 다음과 같다. 전역 객체, 네임 스페이스 유효 범위에 선언된 객체, 클래스 안에서 static으로 선언된 객체, 함수 안에서 static으로 선언된 객체, 파일 유효범위에서 static으로 선언된 객체 이렇게 다섯 종류가 있다.
이들 중 함수 안에 있는 정적 객체는 지역 정적 객체라고 하고 나머지 비지역 정적 객체라고 한다. 이 다섯 종류의 객체, 합쳐서 정적 객체는 프로그램이 끝날 때 자동으로 소멸된다. 다시 말해서 main() 함수의 실행이 끝날 때 정적 객체의 소멸자가 호출된다.
번역 단위란, 컴파일을 통해 하나의 목적 파일을 만드는 바탕이 되는 소스 코드를 일컫는다. 여기서 번역은 소스의 언어를 기계어로 옮긴다는 의미겠지만 기본적으로 소스 파일 하나가 되는데, 그 파일이 #include
하는 파일들까지 합쳐서 하나의 번역 단위가 된다.
위 문장를 총 정리한다면 별도로 컴파일된 소스 파일이 두 개 이상 있으며 각 소스 파일에 비지역 정적 객체가 한 개 이상 들어 있는 경우에 어떻게 되느냐에 대한 이야기이다. 한쪽 번역 단위에 있는 비정적 객체의 초기화가 진행되면서 다른 쪽 번역 단위에 있는 비지역 정적 객체가 사용되는데, 불행히도 이 객체가 초기화되어 있지 않을지도 모른다는 점이다.
또한, 서로 다른 번역 단위에 정의된 비지역 정적 객체들 사이의 상대적인 초기화 순서는 정해져 있지 않다. 비지역 정적 객체들의 초기화에 대해 ‘적절한’ 순서를 결정하기는 매우 어렵다. 이런 문제를 해결하기 위해 함수 호출로 대신하게 되면 이는 결국 싱글톤 패턴의 형태로 나타난다.
정리
어떤 객체가 초기화되기 전에 그 객체를 사용하는 일이 생기지 않도록 하려면 다음 3가지를 기억하자.
- 멤버가 아닌 기본제공 타입 객체는 직접 초기화하자.
- 객체의 모든 부분에 대한 초기화에는 멤버 초기화 리스트를 사용한다.
- 별개의 번역 단위에 정의된 비지역 정적 객체에 영향을 끼치는 불확실한 초기화 순서를 염두에 두고 이러한 불확실성을 피해서 프로그램을 설계한다.
추가
- 기본제공 타입의 객체는 직접 손으로 초기화하자. 경우에 따라 되기도 하고 안되기도 하기 때문이다.
- 생성자에서는, 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화하지 말고 멤버 초기화 리스트를 즐겨 사용한다. 그리고 초기화 리스트에 데이터 멤버를 나열할 때는 클래스에 각 데이터 멤버가 선언된 순서와 똑같이 나열한다.
- 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 한다. 비지역 정적 객체를 지역 정적 객체로 바꾸면 된다.
느낀점
C#과 비슷한 내용이지만 확실히 C++의 깊이가 다른 느낌이다. 여러 패러다임의 각각 초기화를 고려해야 하니 통일성있는 기준을 잘 말해준 것 같다.
댓글남기기