본문 바로가기

Effective C++/1. C++에 왔으면 C++ 법을 따릅시다

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

객체의 초기화에 있어서는 C++ 이랬다저랬다 하는 경향이 있다.(맘에 들지 않음)

 

int x;
 

코드가 어떤 상황에서는 0으로 자동 초기화가 되지만, 다른 상황에서는 그렇지 않다는 것이다.

 

class Point{

 

int x, y;

};

 

Point p;

 

이렇게 했을 때에도, p 데이터 맴버(x,y) 역시 어떤 상황에서는 0으로 자동 초기화가 되지만, 다른 상황에서는 안된다. => ????????????

 

초기화되지 않은 값을 만약 그대로 읽으면 C++에서는 그대로 흘러 나오게 된다. 다른 언어에서는 초기화되지 않은 객체를 읽기만 해도 프로그램이 멈추는 경우도 있지만…

 

C++ 객체 초기화가 중구난방인건 절대 아니다. 언제 초기화가 보장되며, 언제 그렇지 않은지에 대한 규칙이 명확하게 있지만, 규칙이 복잡하다는 것이 문제다.

 

일단 가장 기본적인 규칙!!

 

  • C++ C 부분만을 쓰고 있으며
  • 초기화에 런타임 비용이 소모될 있는 상황이라면

 

값이 초기화된다는 보장이 없다.

 

==> C 아닌 부분으로 가면 사정이 달라진다. 예를 들어 배열(C++ C) 원소가 확실히 초기화 된다는 보장이 없지만, vector(C++ STL) 초기화가 보장된다.

 

가장 좋은 방법은 모든 객체를 손수 초기화 하는 것이다. -> built-in type(C++ C) 대해서는 초기화를 손수 해야겠다.

 

기본적인 규칙만 해결한다면, C++ 초기화의 나머지 부분은 모두 생성자로 귀결되기 때문에 지킬 규칙은 간단하다.

 

  • 객체의 모든 것을 초기화 하자!

 

가장 중요한 것은 대입(assignment) 초기화(initialization) 햇갈리지 말자!!!!!!

 

 

 

지금 ABEntry 객체의 생성자를 보면 전부 초기화가 아닌 대입을 하고 있다. 이렇게 하면 ABEntry 우리가 원하던 값을 가지긴 하겠지만, 개운한 방법이 아니다. C++ 규칙에 의하면 어떤 객체이든 객체의 데이터 맴버는 생성자의 본문이 실행되기 전에 초기화되어야 한다고 명기되어 있다.

 

theName, theAddress, thePhones, numTimesConsulted => 전부 초기화가 아닌 대입이다. 초기화할 타이밍은 진작 지나갔다. 정확히 말하면 ABEntry 생성자에 진입하기도 전에 theName, theAddress, thePhones 각각의  기본 생성자(string, list) 의해 초기화되었다.(C++ STL)

 

그럼 buit-in type numTimesConsulted? => C++ C부분이니 초기화가 보장되지 않았다. 그럼 어떻게 할까?? => 대입문 대신 맴버 초기화 리스트를 사용하면 된다.

 

 

 

위는 대입문, 아래는 맴버 초기화 리스트를 사용하였다. 맴버에 사용자가 원하는 값을 주고 시작한다는 점은 똑같지만, 앞의 대입을 사용한 생성자보다 효율이 좋은 이유는 대입버전의 경우는 theName, theAddress, thePhones 각각의 type 대한 기본생성자를 호출해 미리 초기화가 상태에서 대입으로 새로운 값을 넣는 것이다. 초기화 => 대입 두가지 공정이 들어감, 이로 인해 초기화도 의미없는 짓이

 

하지만 초기화리스트버전같은 경우, 초기화 리스트에 들어가는 인자는 바로 데이터 맴버에 대한 생성자의 인자로 쓰이기 때문이다. theName name, theAddress address, thePhones phones 각각 type 생성자에 의해 초기화된다.

 

대입방식 : default constructor 호출 => copy operator=  호출

초기화리스트 방식 : copy constructor 호출

 

대부분의 데이터 타입에 대해선 전자보다 후자의 방식이 훨씬 효율적이다.

 

앞에서 말한 대부분에 해당하지 않는게 numTimesConsulted같은 built-in-type이다. 이것 역시 맴버 초기화 리스트에 넣는 것이 좋다.

 

그리고 데이터 맴버를 copy constructor 아닌 default constructor 초기화 하고 싶을 때도 맴버 초기화 리스트를 사용하는 습관을 들이자.

 

그냥 생성자 인자를 아무것도 안주면 되니까 힘든 것도 아니다. 예를 들어 이런 식으로 구현이 가능하다.

 

 

이걸 보고 이렇게까지 필요가 있나?? 라고 생각할 수도 있다. 어떤 맴버가 맴버 초기화리스트에 들어가지 않았고, 데이터 맴버의 type user-define type이면 컴파일러가 자동으로 맴버에 대해 기본 생성자(default constructor) 호출하기 때문이다. 하지만 기본 생성자이든 다른 생성자이든 데이터 맴버는 모두 초기화 리스트에 항상 올려두는 센스를 정책으로 박아놔야, 어쩌다 초기화 리스트에 어떤 맴버를 빼먹었을 , 맴버가 초기화되지 않을 있다는 사실을 끌고 가야 하는 부담을 있다.

 

이게 먼소리냐? 만약 numTimesConsutled 맴버 초기화 리스트에서 빠졌다고 생각하면, 이게 built-in type이므로 초기화될지 될지 모른다. => 모르면 안됨, 확실히 해놔야됨

 

그리고 built-in type이라도 반드시 초기화리스트에 넣어야 때가 있다. 상수이거나 참조자로 되어 있는 데이터 맴버의 경우 반드시 초기화되어야 한다.

 

 

  why, 상수와 참조자는 대입 자체가 불가능하다. 상수는 대입 불가 인정, 참조자는 ?? 초기화를 반드시

야하는거지, 대입이 안되는건 아니지 않나

 

말인 싶다.. 참조자도 초기화를 해야 대입이 가능하기 때문에 초기화 리스트에 무조건 넣어야 한다.

 

이해가 안되는 부분이 있어 간단한 예를 만들었는데,

 

 

여기서 b int bb 초기화 되는데, 생성자가 종료되면 bb 메모리에서 해제되지만 b 여전히 해제된 메모리를 가리키는 같다.(dangling pointer 발생)

 

 

dangling pointer 대해 error 내지 않는것이 C++ 특징인 같다. 정의되지 않는 행동에 대해서 error 안냄

 

다시 본론으로 돌아와서, built-int type 초기화를 해도 되고 안해도 되는데 또 상수와 참조자는 반드시 초기화를 해야하고.. => 그냥 초기화를 시키는게(모든 맴버를 초기화 리스트에 넣는 것이) 햇갈린다.

 

만약 생성자마다 주렁주렁 매달려있는 초기화리스트가 도저히 맘에 안든다.(중복되니까) 대입으로도 초기화가 가능한 데이터 맴버(주로 built-in type)이라도 초기화리스트에서 빼내고 싶다

=> 별도의 함수(private 함수) 옮기는 것도 나쁘지 않다.

 

 

 

경우에는 데이터 맴버의 진짜 초기값을 파일이나 데이터베이스에서 읽어올 유용하다. 하지만 일반적인 경우라면 초기화 리스트를 통한 초기화가 아무래도 좋겠다.

 

 

C++ 객체 초기화는 변덕스럽지만, 객체를 구성하는 데이터의 초기화 순서는 항상 똑같다.

  1. 기본 클래스는 파생 클래스 보다 먼저 초기화된다.
  2. 클래스 데이터 맴버는 그들이 선언된 순서대로 초기화

 

2번같은 경우 초기화 리스트의 순서를 바꿔도 순서에 상관없이 클래스에서 선언된 순서대로 초기화 된다.(메모리 구조때문인듯?) 하지만 혼동을 피하고, 혹시 모를 버그도 피하자는 의미로 반드시 선언된 순서대로 초기화리스트를 맞추도록 하자.

 

이제 걱정거리가 하나 남았다.

 

  • 비지역(함수 선언이 아닌) 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.

 

비지역 정적 객체 :

 

정적(객체의 유효기간이 프로그램 끝날때까지인)객체의 경우의

  • global 객체
  • namspace 유효범위에서 정의된 객체
  • 클래스 안에서 static 키워드로 선언된 객체
  • 함수 안에서 static 키워드로 선언된 객체
  • 파일 유효범위에서(함수, 클래스 밖에서) static으로 선언된 객체

 

4 빼고 모두 비지역 정적 객체이다. 오직 4번만 지역성이 있는 지역 정적 객체이다.

 

5 모두 정적 객체-> main() 함수의 실행이 끝날 객체의 소멸자가 호출된다.

 

번역 단위 : 번역 뜻이 compile이라 생각하면 편함, 소스파일 하나 + 거기에 include 되는 파일 = 하나의 .o 파일(ojbect file)

 

=> 작금의 문제를 다시 정리하면, 별도로 컴파일된 소스 파일이 두개 이상 있으며(개별 번역단위), 소스 파에 비지역 정적 객체가 이상 들어 있는 경우에 객체의 초기화 순서가 어떻게 되느냐??

 

이게 어떨 문제가 되냐면 예를 들어 내가 어떤 클래스의 생성자에 다른쪽의 번역단위(다른 소스파일) 있는  맴버함수를 사용할려고 하는데, 이때 객체가 초기화 되어있을 수도 있고 아닐 수도 있다.

 

 

 

만약 FileSystem 이라는 어떤 라이브러리에 포함된 클래스에 있는 tfs라는 객체를 사용한다고 하자. 나는 Directory라는 FileSystem 담아내는 클래스를 만들려고 한다. 그럼 클래스는 tfs 사용하는게 자연스러워 보인다. 내가 Directory 클래스를 사용해 임시 파일을 담는 객체 하나를 생성하기로 한다.

 

Directory tempDir();

 

이때 Directory 생성자가 호출되는데, disk = tfs.numDisk() <= 이때 tfs 초기화가 되었을까? numDisk() FileSystem 맴버 변수의 값을 사용하거나 리턴하는 함수라 생각해보면, tfs 초기화 조차 안되었는데 정상적인 값이 나올리가 없다.

 

tfs tempDir 제작자도 다르고 만든 시기도 다르고 소재지(소스파일) 다르다. 어쨋거나 tempDir보다 tfs 먼저 초기화되는걸 확정지을 없을까?

 

상태로는 안된다. 다시 말하지만 서로 다른 번역 단위에 정의되어있는 비지역 정적 객체들 사이의 상대적인 초기화 순서는 정해진게 없다!!!

 

=> 그럼 지역 정적 객체들은 정해져있나??

 

지역 정적 객체는 함수 호출 객체의 정의에 최초로 닿았을 초기화

 

 

 

td = tempDir() => tempDir에서 지역 정적 객체 td 정의 => 객체 초기화 => Directory 생성자 호출 => tfs() => tfs에서 지역 정적 객체 tfs 정의 => 객체 초기화 => FileSystem 생성자 호출 => 초기화된 지역 정적 객체 tfs 참조자 리턴 => tfs.numDisk() 호출 => Directory 생성자 종료 => 참조자 td 리턴

 

  • built-in type 객체는 직접 손으로 초기화 하자. (경우에 따라 저절로 되기도 하고 되기도 하기 때문이다.)
  • 생성자에서는, 데이터 맴버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 맴버를 초기화하는 보다, 맴버 초기화 리스트를 즐겨 사용하자. 그리고 초기화 리스트에 데이터 맴버를 나열 때는 클래스에 데이터 맴버가 선언된 순서와 똑같이 나열하자
  • 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 한다. 비지역 정적 객체를 지역 정적 객체로 바꾸면 된다.