Computer/ETC Updated: 2014. 3. 20. 16:54 hwaya.

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

반응형
 
int x;

어떤 상황에서는 x의 값이 확실치 초기화되지만(0으로), 또 다른 상황에서는 그것이 보장되지 않는다.

 
class Point{
int x, y;
};
...
Point p;
이렇게 했을 때, p의 데이터 멤버 역시 어떤 상황에서는 초기화가 보장되지만 어쩔때는 또 안된다.
초기화되지 않은 값을 읽도록 내버려 두면 정의되지 않은 동작이 그대로 흘러 나오게 된다.
어떤 플랫폼의 경우에는 미초기화 객체를 읽기만 해도 프로그램이 서 버리기도 합니다. 대체적인 경우에는 적당히 무작위 비트의 값을 읽고 객체의 내부가 이상한 값을 갖게 된다.
 
C++의 객체가 초기화가 중구난방인 것은 절대 아니다. 언제 초기화가 보장되며 언제 그렇지 않은지에 대한 규칙이 명확히 준비되어 있다. 안타까운점은 복잡하다.;;;
 
가장좋은 방법은 모든 객체를 사용하기전에 항상 초기화하는 것.
기본제공 타입으로 만들어진 비멤버 객체에 대해서는 초기화를 손수 해야된다.
 
int x = 0;
const char * text = "A C-style string";
 
double d;
std::cin >> d;
이런부분을 제외하고 나면, C++ 초기화의 나머지 부분은 생성자로 귀결된다.
그 객체의 모든 것을 초기화하자!
 

지키기 쉬운 규칙입니다만, 대입(assignment)을 초기화(initialization)와 헷갈리지 않는것이 중요합니다.

 

class PhoneNumber { ... };

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++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 한다고 명기되어 있습니다.

위 생성자를 살짝 보면 theName, theAddress, thePhones는 초기화되는것이 아니라, 어떤 값이 대입되고 있는 것입니다.

초기화는 진작;;;

그럼 numTimesConsulted도 미리 초기화 되었을까?

그런것은 아니다, 기본제공 타입의 데이터 멤버이기 때문에 기본제공 타입의 경우에는 (생성자 안에서) 대입되기 전에 초기화되리란 보장은 없다.

 

그럼 다시,

대입문 대신에 멤버초기화 리스트를 사용하면 된다.

 

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)

: theName(name),

  theAddress(address),                        // 모두 초기화되고 있다.

  thePhones(phones),

  numTimesConsulted(0)

{}                                                            // 생성자 본문엔 아무것도 들어가 있지 않다.

 

데이터 멤버에 사용자가 원하는 값을 주고 시작한다는 점에서는 똑같지만, 방금 만든 생성자는 앞의 것보다 더 효율적일 가능성이 크다.대입만 사용한 버전의 경우 theName, theAddress 및 thePhones에 대해 기본 생성자를 호출해서 초기화를 미리 해 놓은 후에 생성자에서 곧바로 새로운 값을 대입하고 있습니다. 따라서 먼저 호출된 기존 생성자에서 해 놓은 초기화는 아깝게도 그냥 헛짓이 되고 말았습니다. 멤버 초기화 리스트를 사용하면 피해 갈 수 있습니다.초기화 리스트에 들어가는 인자는 바로 데이터 멤버에 대한 생성자에 의해 초기화되고, theAddress는 address로부터 역시 복사 생성자에 의해 초기화되며, thePhones도 동일한 과정을 거쳐 phones로부터 초기화된다.대부분의 데이터타입에 대해서는, 기본 생성자 호출 후에 복사 대입 연산자를 연달아 호출하는 이전의 방법보다 복사 생성자를 한 번 호출하는 쪽이 더 효율적이다.
앞서 말한 '대부분의 타입'에 포함되지 않는 타입이 numTimesConsulted와 같은 기본제공 타입이다.기본제공 타입의 객체는 초기화와 대입에 걸리는 비용의 차이가 없지만, 역시 멤버 초기화 리스트에 모두 넣어 주는 쪽이 가장 쫗다.또, 데이터 멤버를 기존 생성자로 초기화하고 싶을 때도 멤버 초기화 리스트를 사용하는 습관을...

 

 

1. 기본클래스는 파생 클래스보다 먼저 초기화되고,2. 클래스 데이터 멤버는 그들의 선언된 순서대로 초기화됩니다.


정적 객체(static object)는 자신이 생성된 시점부터 프로그램이 끝날때까지 살아 있는 객체를 일컫습니다.스택객체 및 힙 기반 객체는 애초부터 정적객체가 될수 없다.정적객체의 범주에 들어가는 것들은,1. 전역 객체2, 네임스페이스 유효범위에서 정의된 객체3. 클래스 안에서 static으로 선언된 객체4. 함수 안에서 static으로 선언된 객체5. 파일 유효범위에서 static으로 정의된 객체

이들 중 함수 안에 있는 정적객체는 지역정적객체(local static object), 나머지는 비지역정적객체(non-local static object)라고 한다. 다시말해, main() 함수의 실행이 끝날 때 정적 객체의 소멸자가 호출된다.

 

번역단위(translation unit)는 컴파일을 통해 하나의 목적 파일(object file)을 만드는 바탕이 되는 소스 코드를 일컫습니다.

번역은 소스의 언어를 기계어로 옮긴다는 의미겠지요

기본적으로는 소스 파일 하나가 되는데, 그 파일이 #include 하는 파일까지 합쳐서 하나의 번역 단위가 됩니다.

 

별도로 컴파일된 소스 파일이 두개 이상 있으며 각 소스 파일에 비지역 정적 객체(전역 객체, 네임스페이스에 있는 객체, 클래스 혹은 파일에 있는 정적 객체)가 한 개 이상 들어 있는 경우에 어떻게 되느냐 하는....??

한쪽 번역 단위에 있는 비정적 객체의 초기화가 진행되면서 다른쪽 번역 단위에 있는 비지역 정적 객체가 사용되는데, 불행히도 이 객체가 초기화되어 있지 않을지도 모른다는거~

별개의 번역단위에서 정의된 비지역 정적 객체들의 초기화 순서는 '정해져 있지 않다' 라는 사실

 

파일시스템을 나타내는 FileSystem이라는 클래스가 있다고 가정합시다. 이 클래스는 주변의 모든 파일을 단일 파일 시스템처럼 보이게끔 하니까, 아마도 이런 단일 파일 시스템을 나타내는 특수한 객체가 전역 유효범위 혹은 네임스페이스 유효범위에 들어 있어야 하겠지요.

 

class FileSystem {                                // 라이브러리에 포함된 클래스

public:

...

std::size_t numDisks() const;           // 많고 많은 멤버 함수들 중 하나

...

};

 

extern FileSystem tfs;                            // 사용자가 쓰게 될 객체, tfs = the file system

 

FileSystem 객체가 그리 가볍지 않다는 점은 분위기로 보아 확실하므로, 이 클래스로 만든 객체가 초기화되기(생성자 호출전) 전에 그 객체를 사용한다는 것은 대재앙을 일으키는 셈.

 

이제 이 객체의 사용자 쪽으로 초점을 돌려, 파일 시스템 내의 디렉토리를 나타내는 클래스를 사용자가 만들었다고 가정해 보죠 이 클래스는 tfs를 사용하는게 자연스러울 것입니다.

 

class Directory {

public:

Directory(params);

...

};

Directory::Directory(params)

{

...

std::size_t disks = tfs.numDisks();    // tfs 객체를 여기서 사용

...

}

 

어차피 가정이니, (그래 가정이니...)

한 발짝 더 나아가 봅시다. 이제는 이 사용자가 Directory 클래스를 사용해서 임시 파일을 담는 디렉토리 객체 하나를 생성하기로 마음 먹습니다.

 

Dirctory tempDir(params);    // 임시 파일을 담는 디렉토리

 

정적 객체의 초기화 순서 때문에 문제가 심각해질 수도 있는 상황이 드디어 눈앞에 나타났습니다. (오~~~~+_+)

tfs가 tempDir보다 먼저 초기화 되지 않으면, tempDir의 생성자는 tfs가 초기화되지도 않았는데 tfs를 사용하려고 하겠지요?

(따딴!)

그러나 tfs와 tempDir은 제작자도 다르고 만들어진 시기도 다른데다가 소재지(소스파일)도 다릅니다. 다시 말해 이들은, 다른 번역 단위 안에서 정의된 비지역 정적 객체입니다. 어쨌든 tempDir 전에 tfs가 초기화되게 만들고 싶은데, ..... 달성할수 있을까?

 

단언컨대 안됩니다. (베가 아이언~)

서로다른 번역 단위에 정의된 비지역 정적 객체들 사이의 상대적인 초기화 순서는 정해져 있지 않습니다.

흥4

우앙!!! 우짜노~ C++ 썅!!!!

 

한가지 다행스러운 사실은 설계에 약간의 변화만 주면 이 문제를 사전에 봉쇄할 수 있다는 점입니다.

비지역 정적 객체를 하나씩 맡는 함수를 준비하고 이안에 각 객체를 넣는 것.

함수 속에서도 이들은 정적 객체로 선언하고, 그 함수에서는 이들에 대한 참조자를 반환하게 만듭니다. 사용자 쪽에서는 비지역 정적 객체를 직접 참조하는 과거의 폐단을 버리고 이제는 함수 호출로 대신합니다. 

자, 정리하면 '비지역 정적 객체'가 '지역 정적객체'로 바뀐 것입니다.

[이쯤 되면, 디자인 패턴에 관심이 많은 분이라면 이것이 단일체 패턴의 전형적인 구현양식임을 바로 알수 있겠지요]

<난 몰랐음 ;;; ㅠ_ㅠ>

 

지역 정적 객체는 함수 호출 중에 그 객체의 정의에 최초로 닿았을 때 초기화되도록 만들어져 있다.

이것은 C++에서 보장하는 사실이며, 위의 방법은 바로 그 사실을 이용한 것입니다. 따라서 비지역 정적객체를 직접 접근하지 않고 지역 정적 객체에 대한 참조자를 맞추어 주어야 하겠습니다. (이런거였군... 사용은 하고 있었는데 ㅋㅋ)

그리고 보너스~ 비지역 정적 객체 대신에 준비한 '지역 객체 참조자 반환' 함수를 호출할 일이 없다면 해당 객체의 생성/소멸 비용도 생기지 않게 막아야 합니다. 진짜 비지역 정적 객체의 경우엔 막고 자시고 할 일도 없습니다만.

 

class FileSystem { ... };

FileSystem& tfs()

{

static FileSystem fs;

return fs;

}

 

class Directory { ... };

Directory::Directory( params )

{

...

std::size_t disks = tfs().numDisks();

...

}

 

Directory& tempDir()

{

static Directory td;

return td;

}

tfs와 tempDir 대신에 tfs()와 tempDir()을 참조하는 것으로 바뀐 게 전부.

즉, 정적객체 자체를 직접 사용하지 않고 그 객체에 대한 참조자를 반환하는 함수를 사용하고 있는 것이죠잉~

 

이 기법을 도입하면서 출현한 '참조자 반환' 함수는 어느 경우이든 복잡하게 구현될 일이 없습니다.

첫 번째 줄에서 지역 정적 객체를 정의/초기화하고, 두번째 줄에서 그 객체의 참조자를 반환하면 끝입니다.

함수 본문이 지극히 단순하다 보니, 인라인해도 좋겠다는 생각도 듭니다. 특히 이 함수의 호출빈도가 잦다면 더욱....

하지만 다른쪽에서 생각해 보면 이런 문제도 있습니다.

참조자 반환함수는 내부적으로 정적 객체를 쓰기 때문에, 다중스레드 시스템에서 동작에 장애가 생길 수도 있다는 거죠 다중 스레드, 말이 나왔으니 말인데, 혹시나 이것이 돌아가는 프로그램이라면 비상수 정적 객체(지역객체이든 비지역객체이든)는 온갖 골칫거리의 시한폭탄이라고 보시면됩니다. (맞아맞아 ..... 짱나~)

골칫거리를 다루는 한가지 방법으로, 프로그램이 다중스레드로 돌입하기 전의 시동 단계에서 참조자 반환 함수를 전부 손으로 호출해 줄 수 있습니다. 이렇게 하면 초기화에 관계된 경쟁상태가 없어집니다.

 

물론 초기화 순서 문제를 방지하기 위해 이처럼 참조자 반환 함수를 사용하는 아이디어는 여러분이 객체들의 초기화 순서를 제대로 맞춰둔다는 전제조건이 뒷받침되어 있어야 말이됩니다.

어쩌구 저쩌구...

 

첫째, 멤버가 아닌 기본제공 타임 객체는 여러분 손으로 직접 초기화하세요

둘째, 객체의 모든 부분에 대한 초기화에는 멤버 초기화 리스트를 사용합니다.

셋째, 별개의 번역 단위에 정의된 비지역 정적 객체에 영향을 끼치는 불확실한 초기화 순서를 염두에 두고 이러한 불확실성을 피해서 프로그램을 설계해야 합니다.

 

뭐 이런내용... Bye~

 

 


이펙티브C++(EFFECTIVE C++)-3/E

저자
스콧 마이어스 지음
출판사
ADDISON WESLEY | 2006-05-25 출간
카테고리
컴퓨터/IT
책소개
-
가격비교 글쓴이 평점  

 

 

 

반응형

'Computer > ETC' 카테고리의 다른 글

리틀엔디안(little endian)&빅엔디안(big endian)  (0) 2015.08.26
VS2013, 플러그인 없애고 싶은데...  (0) 2014.04.25
운영체제의 주요 구성 기술  (0) 2014.03.27
Thread(스레드)  (0) 2014.03.27
운영체제의 개요  (0) 2014.03.12
회선  (0) 2013.05.07
포인트 처리_XOR연산, 룩업테이블  (0) 2013.05.07
블러링 & 샤프닝  (0) 2013.05.07

Table of Contents


    EIGHTBOX
    EIGHTBOX
    hwaya.

    programmer

    🍋일상의 소소한 순간들부터 큰 꿈까지, 호기심을 만족시킬 다양한 카테고리를 담은 블로그 입니다. 그리고, 소소한 행동에 감동하며 기뻐하고 하루하루에 감사하는 사람🌵

    Today Yesterday Total
    최신글