객체를 사용하기 전에 반드시 그 객체를 초기화 하자!!


이 항목을 위의 한 줄로 끝나는 단원이라해도 무방하다.

이번 장에서는 고맙게도 3가지의 유의점을 콕 찝어 설명해주고 있다.

하나하나 들여다보면서 진행해보도록 하겠다.


1. 멤버가 아닌 기본제공 타입 변수는 직접 초기화하자!


Tip.

배열의 경우 초기화가 런타임에 비용이 소모될 수 있는

 상황이라면 초기화에 대한 보장이 없다.

하지만 vector는 그러한 보장을 가질 수 있다.


초기화 방법

int x = 0;

const char* text = "C_Style";

double d;

cin >> d;


간단하다.

그냥 변수를 생성했다면 바로 초기화해서 사용하자 이런 뜻이다.



2. 객체의 초기화는 초기화 리스트를 이용하자!!


주의점.

" 대입을 초기화와 헷갈리지 않는 것이 가장 중요하다. "


객체의 초기화는 그렇다. 바로 생성자로 귀결된다.

먼저 위에서 언급했던 대입의 경우를 보도록 하자.



C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는

생성자의 본문이 실행되기 전에 초기화되어야 한다라고 명기되어 있다.


위의 예제는 초기화가 아닌 단순 대입이다.

초기화는 단계는 진작에 지나간 셈이다.


여기서 잠깐!

기본 제공 타입인 hp, mp도 미리 초기화 되었을까?

기본 제공 타입의 경우 대입되기 전에 초기화되리란 보장이 없다.


자 그럼 대입이 아닌 멤버 초기화 리스트를 사용해서

대입문 없이 진짜 초기화를 시켜보도록 하겠다.


초기화리스트를 사용하여 깔끔하게 초기화 완료!


대입의 경우 기본 생성자를 호출해서 초기화를 미리 해놓은 후

복사 대입 연산자를 연달아 호출하는 방법이라 하면

초기화 리스트는 복사 생성자 한 번만을 호출하는 방법이다.

당연히 초기화 리스트의 효율이 더 좋다는 뜻이다.

(대부분의 데이터 타입에서는)




대부분의 타입에 포합되지 않는 타입이

앞에서 말한 hp와 같은 기본제공 타입이다.(int)

기본 제공 타입인 경우 초기화나 대입에 드는

비용이 얼마 차이가 나지 않는다고 한다.


하지만!!

데이터 멤버는 초기화 리스트에 모두 올려주는

정책을 지향하며 실수를 방지하는 것도 좋은 방법일 것이다.




기본 제공 타입의 경우에도 초기화 리스트가

의무화 되어지는 경우가 있다. 어떤 경우냐면

바로 const 혹은 참조자로 되어있는 경우이다.

(상수와 참조자는 대입 자체가 불가능)

이 경우 초기화리스트는 선택이 아닌 필수!


아까도 말했지만 기본 제공 타입은

대입과 초기화의 비용이 별반 차이 나지 않는다.

초기화리스트에 주렁주렁 메달려있는 모습이 보기 싫다면


(초기화 해야하는 생성자가 많을 경우 초기화 리스트 또한 늘어나기 때문)


위와 같이 멤버 함수를 통해 초기화 해주는 방법이 있다.




Tip.

객체를 구성하는 데이터의 초기화 순서

1. 기본 클래스는 파생 클래스보다 먼저 초기화 된다.

2. 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화 된다.


따라서 초기화 리스트 순서대로 초기화 되는 것이 아니라

선언된 순서와 같다. 때문에 혼동을 막기 위해

선언 순서와 초기화 순서를 일치 시키도록 하자!



3. 별개의 번역 단위에 정의된 비지역 정적 객체에

영향을 끼치는 불확실한 초기화 순서를 염두해두자!


이게 도대체 뭔 소린가 싶지만 책을

 찬찬히 읽어보며 해석 해보도록 하자.


들어가기에 앞서 용어 정리


정적객체란?

1. 전역 객체

2. 네임스페이스 유효범위 안의 객체

3. 클래서 내의 static 객체

4. 함수 내의 static 객체

5. 파일 유효범위 내의 static 객체


이들 중 함수라는 지역성을 가진 4번만

지역 정적 객체라고 한다.

위의 5가지의 정적객체는

프로그램이 끝날 때 자동 소멸된다.


번역 단위란?

쉽게 말해 하나의 소스 파일

#include하는 파일들까지 합침




==========================문제 제기


두 개의 번역 단위 중 한 곳에서

1. 비정적 객체를 초기화 하려고 보니

2. 다른 번역단위의 비지역 정적 객체를 필요로 한다.

3. 하지만 이 비지역 정적객체는 초기화 되어 있는지 모른다.


별개의 번역단위에서 정의된 비지역 정적 객채들의

초기화 순서는 정해져 있지 않다!!


===========================해결책


1. 비지역 정적 객체를 하나씩 맡는 함수를 준비하고

2. 이 안에 각 객체를 정적 객체로 선언한다.

3. 그리고 함수는 이들에 대한 참조자를 반환한다,


--> 비지역 정적 객체를 직접 참조하는 폐단은 버리고

이제는 함수호출로 대신하게 된다.

비지역 정적 객체 ---> 지역 정적 객체로 바뀌었다.




무슨 말인가??

지역 정적 객체는 함수 호출 중에 그 객체의 정의에

최초로 닿았을 때 객체를 초기화 해버린다는 것이다.

따라서 비지역 정적 객체에 직접 접근하지 않고


함수 내에서 지역 정적객체를 정의하고 초기화 하여

지역 정적 객체에 대한 참조자를 반환하게 만들었다면

그 참조자는 반드시 초기화된 형태라는 것이다.


번역 단위 1


class File {                                  

public:

size_t nimDisks() const;

 }  


File& tf(){                                   

static File f;                      ----> 2. 지역 정적 객체를 정의 하고 초기화

return f                            ----> 3. 이 객체에 대한 참조자를 반환

}    


번역 단위 2


class Dir { 

...

size_t disks = tf().numDiskes();   ---> 1. 함수를 호출

...

}


Dir& tempDir(){

static Dir td;

return rd;

}

--> 결론

객체에 직접 접근하지 않고 함수를 통해 접근하면 된다.


초기화 순서 문제를 방지하기 위해 이처럼 참조자 반환 함수를

사용하는 것은 객체들이 초기화 순서를 제대로 맞춰 둔다는

전제조건이 뒤받침되어 있어야 가능한 말이다.



POINT!!


그냥 무조건 초기화해서 쓰자!!




내용이 조금 길어진 듯하다.

하지만 책에서 초기화에 대한 내용을

매우 강조하고 있다는 점을 보아

한번쯤 제대로 짚고 넘어가야할 부분인 듯 하다.


낌새만 보이면 const를 들이대자!


-> 어떤 값이 불변이어야 한다는 제작자의 의도를 컴파일러 및 다른 프로그래머와 나눌 수 있는 수단.


1. const는 전역 혹은 네임스페이스 유효범위 내의 상수를 선언하는 데 사용할 수 있다.

--> const int Width = 10;


2. 파일, 함수, 블록 유효범위에서 static으로 선언한 변수에도 사용할 수 있다.

--> void SetX(){

static const int count = 1;

}


3. 클래스 내부의 데이터 멤버

-->  class AAA{

private:

const int count;

static const int Size = 10;   --> 객체 내부의 정적 멤버도 당연 적용할 수 있다.

}


4. 포인터

1. const int* aptr;                 --> 포인터로 데이터 조작을 하지 않겠다. 가리키는 대상을 상수화 --> 가리키는 대상은 바꿀 수 있음.

2. int* const aptr;                --> 하나만 가리키겠다. 포인터를 상수화 --> 데이터 조작은 가능

3. const int* const aptr;      --> 가리키는 대상도 바꾸지 못할 뿐더러, 포인터를 통한 데이터 조작까지 허용하지 않겠다.


5. STL의 반복자

STL의 반복자 즉 iterator는 포인터를 본뜬 것이기 때문에, 기본 동작원리가 T* 포인터와 흡사하다.

반복자를 const로 선언하는 것은 T* const 와 같다. --> 즉 포인터를 상수화 하겠다는 말이다.

그렇다면 가리키는 대상을 상수화 하려면 어떻게 해야 할까? --> const_iterator를 쓰면 const T* 와 같이 동작한다.

아래 예제를 보자.


vector<int> vec;


1. const vector<int>::iterator iter = vec.begin();          --> iter는 T* const 처럼 동작. 즉 위의 2번과 같이 포인터를 상수화한 형태

*iter = 10;                                                               -->  따라서 데이터 조작은 가능하다.

++iter;                                                                    --> 하지만 가리키는 대상을 바꾸는 것은 불가능!


2. vector<int>::const_iterator cIter = vec.begin();        --> cIter는 const T* 처럼 동작. 즉 위의 1번과 같이 가리키는 대상을 상수화한 형태

*cIter = 10;                                                             --> 따라서 데이터 조작이 불가능하다.

++cIter;                                                                  --> 가리키는 대상을 바꾸는 것은 가능하다!


6. 함수

ex)

const AAA operator(const AAA& lhs, const AAA& rhs);

AAA a, b, c;

1.   (a*b) = c;  

2.   if(a*b = c)  

---> 1,2번과 같은어처구니 없는 코드를 예방할 수 있다.


상수 멤버 함수


1. 이 함수를 통해서 멤버 변수의 값이 변경되는 것을 허용하지 않겠다!!

2. 상수화된 함수는 상수화되지 않은 함수의 호출을 허용하지 않는다.          --> 상수화 되지 않은 함수 내에서 데이터 조작의 가능성이 있기 때문

3. 또한 멤버 변수의 포인터를 리턴하는 것도 허용하지 않는다.                    --> 반환된 포인터로 조작의 가능성이 있기 때문

--> 따라서 const int* GetPtr () const  --> 이런 형식으로 진행하도록 하자.  4항목의 2번 참고

4. const 키워드에 유무에 따라 함수 오버로딩이 가능하다.


//////////////////// 멤버 함수

void show() const;                    1번

void show();                              2번

//////////////////// 객체 선언

AAA aaa;                

const AAA bbb;     


aaa. show();    --> 비상수 객체 --> 두 개의 show()를 모두 불러올 수 있다. -->우선순위가 높은 비상수 함수를 불러온다. 2번

bbb.show();    --> 상수 객체  --> 오직 상수 함수만을 불러올 수 있다. 1번


5. 클래스의 인터페이스 향상 --> 멤버 변수를 조작할 수 있는 함수는 무엇이고, 조작할 수 없는 함수는 무엇인가? 를 알아야한다.

6. 상수 객체를 사용할 수 있게 된다. --> 객체 전달을 상수 객체에 대한 참조자로 진행하는 것 --> 복사손실 복사비용을 없애주어 코드 효율 UP


7. 상수 객체

1. 상수 멤버 함수만 불러올 수 있다.


실제 프로그래밍에서 상수 객체가 생기는 경우

1. 상수 객체에 대한 포인터 혹은

2.상수 객체에 대한 참조자로 객체가 전달될 때이다.  (위의 6번 참고)


void print (const Player& cp){ ... }  


참고)

const int& GetInt(int& a) { return a; };      1번

int & GetInt(int& a) { return a; }               2번


1번의 경우

int a = 10;

int b = GetInt(a);   --> 가능

int &b = GetInt(a)  ---> 불가능. 데이터 조작의 위험성이 있다. 반환된 레퍼런스로 어떠한 데이터 조작도 할 수 없다.


2번의 경우 모두 가능.


Player& 리턴과

Player 리턴의 차이

--> 함수의 반환 타입이 객체의 참조형식일 경우 리턴된 객체가 그 자리에 반환되게 된다.

--> 참조형식이 아닐 경우 리턴된 객체의 복사본이 오게 된다.



const 키워드는 프로그램을 안정적으로 작성하는데 많은 도움을 준다.

기능적인 측면 말고도 오류를 쉽게 찾아낼 수 있다는 이점까지 줄 수 있다.

따라서 const 키워드는 가급적이면 많이 사용하도록 하자.


const에 대해 상당히 많은 내용이 정리되어 있다.

나름 쉽게 풀어쓴다고 썼지만 포스팅이 아직 어색하여

보기 썩 좋은 모양새는 아닌 듯 하다.


비트수준 상수성, 물리적 상수성 그리고 상수 비상수 멤버의

코드 중복현상을 피하는 방법에 대해서는 다음에 포스팅 하도록 하겠다.

1. 보글 게임 알고리즘


일명 무식하게 풀기라는 완전탐색 알고리즘에 대해 알아보겠다.

모든 경우의 수를 다 생각해보자는 것이다. 제대로 이름 값 하는 놈이라 할 수 있다.

이 알고리즘에서는 (앞으로도 계속일테지만) 재귀 호출이 알고리즘에서 상당히 효율적으로 쓰이게 된다.

들어가기에 앞서 간단한 재귀 호출에 대해서 복습을 하고 가자!!



너무나도 간단한 예제이다.

함수 내에서 자기자신을 호출하고 있다.

위의 경우 for문을 써도 간단하게 해결되는 문제지만

문제의 특성에 따라 적절하게 사용한다면

코딩을 훨씬 간단하게 만들 수 있다.


재귀에 대해 어렴풋이 생각이 났다면 바로 들어가보자.


대충 문제 설명

5X5 보드판에 알파벳이 채워져 있고

1. 시작 위치 (x,y) 와 보드판에서 찾을 단어를 입력한다. (ex. word)

2. 시작 위치부터 해서 상하좌우 대각선까지 인접한 부분으로 다음 단어를 찾게 된다.

3. 한 글자가 두번 사용되어 질 수도 있다.

ex)

q w e r d

p m o f t

b w e a n

y w q s s

h m o c c

-->> 좌표 입력 : 1 0

-->> 찾을 단어 : worord

-->> 단어를 찾았습니다. 



string name("abcdefg");

name.strsub(1);                -->bcdefg   --> 매개변수를 하나 전달하면 그 놈만 빼고 (왜 첫 글자가 1이지?)

name.strsub(0,2);           --> abc        --> 매개변수를 두개 전달하면 그 범위에 있는 놈만 살아남는다.


위의 경우에서 

1번. bool hasWord(int, int, string&);    

2번. bool hasWord(int, int, string);

3번. bool hasWord(int, int, const string&);


hasWord( ....  , _word.substr(1));

2번, 3번은 실행이 되는데 1번이 실행이 안됨.

왜 그런건지 꼭 찾아봐야함!


+ Recent posts