operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자.


class widget{ ... }


widget w;

w=w;


widget w[10];

w[i] = w[j];


*pw1 = *pw2;

위의 경우 자기대입의 소지가 다분하다.


같은 타입으로 만들어진 객체 여러 개를

참조자 혹은 포인터로 물어 놓고 동작하는

코드를 작성할 때는 같은 객체가 사용 될

가능성을 고려하는 것이 바람직하다.




파생 클래스 타입의 객체를 참조하거나 가리키는 용도로

기본 클래스의 참조자나 포인터를 사용할 수도 있다.


class Base { ... };

class Derived : public Base { ... }

void dosomethig ( const Base& rb, Derived* pd );

위의 경우에도 자기대입 연산의 위험이 다분하다.


일반적인 operator= 구현을 보겠다.

위 코드의 문제점은 *this와 rhs가

같은 객체일 경우 문제점이 발생한다.

첫 줄에서 이미 지워진 메모리 영역을

두번 째 줄에서 다시 가리키고 있다.

또한 예외에도 안전하지 않다는 점.




자 이제 이 두문제를 해결해보자.


1. 일치성 검사


위의 operator= 구현 맨 윗줄에

다음과 같은 코드를 삽입합니다.

if(this == &rhs) return *this;

자기 대입인지 확인한 후, 자기 대입이면

아무것도 하지 않고 자기 자신을 리턴한다.




자기 대입의 위험성은 이제 완전히

사라진 듯 보이지만 여전히

예외에 안전하지 않다는 문제점이 있다.

또한 대입을 할 때마다 검사를

해줘야 한다는 비용문제도 생긴다.




'new bitmap' 이 부분에서

동적 할당에 필요한 메모리가 부족하다든지

bitmap 클래스 복사 생성자에서 예외를

던진다든지 해서 메모리 할당이 제대로

진행되지 않을 가능성이 있다.


2. 문장의 순서를 바꿔보자


'new bitmap' 부분에서 예외가 발생하더라도

pb는 원래의 데이터를 가지고 있다.

'new' 연산이 정상적으로 수행되면

그 때 pOrig를 삭제한다.

(pOrig는 pb의 원래 가리키던 곳을 가리키고 있다.)




위 코드는 자기대입 현상 또한 완벽히 처리한다.


그림이 좀 엉성하지만

코드를 보고 조금만 생각하면

충분히 이해가 될 것이다.




예외 안전성과 자기대입 안전성을 동시에

가진 operator=을 구현하는 방법으로

복사 후 맞바꾸기라는 기법이 있는데

이 기법은 책의 중반부에 자세히

설명하고 있기 때문에

다음 포스팅에서 다루도록 하겠다.


POINT!!

1. operator=을 구현할 때, 어떤 객체가 그 자신에

대입되는  경우를 제대로 처리하도록 만들어야 한다.


1) 일치성 검사

2) 문장의 순서 조정

3) 복사 후 맞바꾸기


2. 두 개 이상의 객체에 대해 동작하는 함수가 있다면,

이 함수에 넘겨지는 객체들이 같은 경우에도

정확하게 동작하는지 확인해 봐야한다.


대입 연산자는 *this의 참조자를 반환하게 하자.


윤성우의 열혈강의를 포스팅할 때 잠깐 언급했던 부분이다.

상당히 간결한 내용이므로 핵심만 짚고 후딱 넘어가도록 하겠다.




class AAA{

private:

int x;

int y;

public:

AAA(int _x, int _y) : x(_x), y(_y) {};

AAA& operator=(const AAA& rhs){

x = rhs.x;

y = rhs.y;

return *this;

}

};


int main(){

AAA a(1,2);

AAA b(3,4);

AAA c(5,6);


a = b = c;

}


대입 연산은 우측 연관 연산이다.

즉 ,위의 경우 a = (b = c); 


1. AAA operator=(const AAA& rhs);

2. AAA& operator=(const AAA& rhs);


이번 항목의 제목은 *this의 참조자를 반환하게 하자는 것이다.

그럼 참조자를 반환하지 않으면 어떻게 될까?




1번의 경우 반환되는 값이 복사생성자에 의해 복사되어 리턴된다.

그리고 자기자신이 아닌 복사본이 그 자리에 위치하게 되는 것.

단항연산자를 오버로딩 하는 경우, 참조자를 리턴받지 않으면

++(++a); 이와같은 연산이 불가능하다.

밑줄 친 부분에 자기자신이 아닌, 복사본이 위치하기 때문에

제대로된 연산이 이루어 지지 않는다.


이 부분에 대해서는 C++ 카테고리의

연산자 오버로딩이라는 포스팅을 참고하기 바란다.




POINT!!

대입 연산자 (=, +=, -=, *= 등)는 *this의 참조자를 반환하도록 하자.

객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자.


이번 항목은 시작하기 전에

몇 가지 중요한 부분을

짚고 넘어가도록 하겠다.


1. Base 클래스의 생성자가 호출될 동안에는,

가상 함수는 절대로 Drived 클래스로 내려가지 않는다.


2. Base 클래스 생성자가 돌아가고 있을 시점에

Drived 클래스 데이터 멤버는

초기화된 상태가 아니다.


3. Drived 클래스 객체의 Base 클래스 부분이

생성되는 동안은, 그 객체의 타입은 바로

Base 클래스이다.




사실 위의 내용이

생성자와 소멸자 내에서 왜 가상함수를

넣으면 안되는지에 대한 답변이 되겠다.

자 이제 예제를 보며 찬찬히 짚어보자.


생성될 때마다 로그 기록을 남겨야하는 클래스가 있다.

class Transaction {

public:

Transaction();

virtual void logTransaction() const = 0;

...

};

Transaction::Transaction(){

logTransaction();

}


class BuyTran : public Transaction{

public:

virtual void logTransaction const;

...

};


class SellTran : public Transaction {

public:

virtual void logTransaction() const;

...

};


int main(){

BuyTran b;

}


1. Base 클래스의 성성자가 먼저 호출된다.

2. Drived 클래스의 생성자가 호출된다.

3. Dirved 클래스의 소멸자가 호출된다.

4. Base 클래스의 소멸자가 호출된다.

위와 같은 순서는 잘 알고 있을 것이다.




BuyTran b;

Drived 클래스의 객체로 선언하고 있지만

실제로 호출되는 것은 Base 클래스의

logTransaction()이다.

왜?? 위에 3가지 언급했던 부분을 참고하기 바란다.




다시 정리하자.

Base 클래스가 호출될 동안에는 그 객체의 타입은

Base 클래스이며, 호출되는 가상 함수는 모두

Base 클래스의 것이다.

또한 이 시기에 Drived 클래스 생성자는 호출되지 않으며

자연스레 Drived 클래스의 멤버는 초기화가 되지 않는다.

따라서 가상 함수는 절대로 Drived 클래스로 내려가지 않는다.




다시 예제를 보자.

위의 경우, logTransaction이

순수 가상함수로 선언이 되어 있기 때문에

Base 클래스 생성자에서 호출이 되면

함수의 정의 부분이 없기 때문에 링커 에러가 난다.

(Base 클래스 생성자에서 호출되는 logTransaction은

순수 가상함수로 선언된 자기 자신의 멤버 함수이기 때문)

따라서 오류를 찾기 쉬운 경우이다.




다음 예제를 보자.

class Transaction {

public:

Transaction(){

Init();

}

virtual void logTransaction() const = 0;

...


private:

void Init(){

logTransaction();

}

};


이 코드는 개념적으로 볼 때 위의 코드와 일맥상통하다.

하지만 위의 코드와는 다르게 에러를 발생시키기 않는다. 왜??




위의 logTransaction() 이 순수 가상 함수가 아닌

일반 가상 함수라하면 문제는 더더욱 커진다.

정상적으로 Transaction 객체의 logTransaction()함수가

정상적으로 호출될 것이기 때문이다.

(순수 가상 함수가 아니기 때문에 링커 에러도 없다.)




--> 해결책

생성 중이거나 소물 중인 객체에 대해 생성자나 소멸자에서

가상 함수를 호출하는 코드를 철저히 솎아내자!


하지만 모든 객체가 생성될 때마다

log파일을 만들어야 하는데 이러한 부분의

구현도 만만치 않다. 해결책이 없을까?


--> 해결책

logTransaction을 Transaction 클래스의

비가상 멤버 함수로 바꾸는 것이다.

말로만 해서는 모르겠다. 예제를 보자.


class Transaction {

public:

Transaction(const string& logInfo);

void logTransaction(const string& logIngo) const;

};

Transaction::Transaction(const string& logInfo){

...

logTransaction(logInfo);

}


class BuyTran : public Transaction {

public:

BuyTran (string& _logName ) : Transaction(createLogString (_logName)) {

...

}

private:

static string createLogString (string& _logName);

};


필요한 초기화 정보를 Drived클래스 쪽에서

Base 클래스 쪽으로 올려주도록 만든다.




짚고 넘어가기.

createLogString() 함수는

drived 클래스에서 base클래스의 생성자 쪽으로

값을 넘기기 위한 도우미 함수이다.

static으로 선언하는 이유는

생성이 끝나지 않은 Drived클래스의 멤버를

건드릴 위험성을 배제하기 위함이다.

(static은 객체 생성 전에 이미 생성되어져 있기 때문)




POINT!!

생성자 및 소멸자 안에서 가상 함수를 호출하지 말자!!






예외가 소멸자를 떠나지 못하도록 붙들어 놓자!


그냥 소멸자안에서 예외처리를 하면 안된다는 말인거 같다.

예제를 보며 문제점과 해결방안을 짚어보겠다.


class NPC{

public:

...

~NPC() { ... };

--> 예외를 던지는 소멸자라고 가정

};


void doSomething(){

vector<NPC> n;

...

}


vector 타입의 객체 n은 자신이 소멸될 때

자신이 가지고 있는 NPC객체를 소멸시킬 책임이 있다.

하지만 NPC를 소멸시키는 도중 예외가 발생되었다고 하자.

이 경우 프로그램은 미정의 동작을 보인다.

원인은 바로 예외를 내보내는 소멸자 때문!!

(NPC의 소멸자에서 예외를 내보내고 있다)




무슨 말인지 모르겠다. 다음 예를 보도록 하자.

class DBConnection {

public:

...

static DBConnection create();


void close();  

--> 연결을 닫는 함수. 연결이 실패하면 예외를 던진다고 가정.

--> 즉, 이번 항목 핵심인 예외를 던지는 함수

};


class DBConn{

private:

DBConnection db;

public:

...

~DBConn(){

db.close();

--> 소멸자에서 예외를 던지는 함수를 호출하고 있다.

}    

};


위의 예제는 데이터베이스의 연결을 관리하는

클래스 DBConnection과 그의 자원은 관리하는

클래스 DBConn이다.

여기서 주목할 점은 DBConnection의 멤버 함수

close()는 실패할 경우 예외를 던진다는 점이다.


{

DBConn dbc(DBConnection::create());

...

...

}


--> DBConnection 객체를 생성하고 DBConn으로

넘겨주어 관리를 맡기고 있다.

블록이 끝나면 DBConn 객체가 사라지고

사라지면서 자연스레 close()함수를 불러 연결을 닫게 된다.




하지만 close()는 실패할 경우 예외를 던진다!

때문에 close()를 호출하는 DBConn의 소멸자는

이 예외를 전파할 것이다. --> 문제 발생



해결책

1. 예외가 발생하면 프로그램을 끝내버린다.

~DBConn::~DBConn(){

try{ db.close() }

catch( ... ) {

...

abort();

}

}


-->  close()에서 예외를 던졌다.

                                                                                                          걍 프로그램을 끝내버린다.

                                                                                          미정의 동작으로 가기보다는 그냥 끝내버리겠다는 방법




2. 예외가 발생하면 삼켜버리겠다.

~DBConn::~DBConn(){

try{ db.close() }

catch( ... ) {

...

}

}


--> 그냥 무시하고 진행하겠다는 것.

      무엇이 잘못되었는지 알 수 없다.

      예외를 무시하더라도 그 다음 동작이

      제대로 이루어 진다는 보장이 있을 때 사용.




--> 두가지 모두 좋은 해결책은 아니다.

      사용자가 직접 해결할 수 잇도록 기회를 주도록 하자!


class DBConn{

private:

DBConnection db;

bool closed;

public:

...

void close(){

db.close();

closed = true;

}


~DBConn(){

if(!close)

try{

db.close();

}

catch( ... ){

 ...

}

}

};

--> 사용자가 연결을 직접 닫을 수 있게 close()함수를 제공

--> 즉, 사용자가 함수를 직접 불러 연결을 닫음

--> 따라서 발생하는 예외 또한 사용자가 처리해야함.




--> DBConn 객체가 소멸될 때, 사용자가 이미 close()함수를 불러

연결을 닫은 후라면, 소멸자에서 예외가 발생할 경우가 없어지게 된다.

    --> 연결이 열려있다면 소멸자 내의 db.close() 함수를 호출해서 닫아 준다.

--> 닫다가 실패하면 위의 두가지 방법 중 하나

      예외를 먹던지, 종료를 하던지 한다.




같은 말을 계속 반복하는거 같지만

쉽게 말해서 DBConn 클래스에 close()함수로

DBConnection의 연결을 직접 닫겠다는 것이다.

DBConn의 close()를 호출하기 되면 closed = true가 될 것이고

DBConn의 소멸자에서 예외를 던지지 않게되는 것이다.

--> 쉽게 말해 사용자가 직접 닫아라.



여기서 핵심!!

--> 예외는 소멸자가 아닌 다른 함수에서 비롯되어야 한다.

예외를 던지는 소멸자는 시한폭탄과도 같다.



POINT!!

1. 소멸자에서는 예외가 던져지면 안된다. 

   소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면

   삼키던지 종료하던지 해라.


2. 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 

   사용자가 직접 처리해야 할 경우, 해당 연산을 제공하는 함수는

   반드시 보통의 함수여야 한다. (소멸자 노노)






다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자!


시간 기록을 유지하는 클래스


class TimeKeeper{

public:

TimeKeeper();

~TimeKeeper();

...

};


class AtimicClock : public TimeKeeper { ... };

class WaterClock : public TimeKeeper { ... };

class WristWatch : public TimeKeeper { ... };




시간정보에 접근하기 위한 팩토리함수를 선언

TimeKeeper* getTimeKeeper(){ return new 파생 클래스 };


팩토리 함수의 기존 규약을 그대로 따라간다면 이 함수에서

리턴되는 객체는 힙에 있게 된다. 따라서 삭제를 해줘야함.


TimeKeeper *ptk = getTimeKeeper();

...

...

delete ptk;


문제점


getTimeKeeper()에서 리턴되는 포인터가 파생 클래스 객체에 대한

포인터라는 점과 이 포인터가 가리키는 객체가 삭제될 때는

기본 클래스 포인터를 통해 삭제된 다는 점.


--> 기본 클래스의 소멸자가 비가상 소멸자이기 때문에

자기자신만 지우고 파생클래스는 지우지 않는다




C++의 규정에 의하면, 기본 클래스 포인터를 통해 파생 클래스 객체가

 삭제될 때, 그 기본 클래스에 비가상 소멸자가 들어 있으면

 프로그램 동작은 미정의 사항이라고 되어있다.


머리 아프다 맘 편히 정리하자

가상 함수를 하나라도 가진 기본 클래스라면 가상 소멸자를 가져야 한다!




하지만 기본 클래스로 의도하지 않은 클래스에 대해

 소멸자를 가상으로 선언하는 것은 좋지 않다.

가상 함수 테이블 포인터와 가상 함수 테이블에

 대한 얘기가 나오는데, 이 부분은 나중에 다시 심도있게 다루겠다.

 아무튼 기본 클래스가 아니라면 가상소멸자를 쓰지말자.


중간 정리

가상 소멸자를 선언하는 것은 그 클래스에 가상 함수가 하나라도 들어 있는 경우에만 한정하라.




비가상 소멸자의 예 --> 표준 string 클래스


class sss : public string {

...

};


sss* psss = new sss("Good Morning");

string *ps = psss;

delete ps;             --------> sss의 소멸자가 호출되지 않는다.


STL 컨테이너 타입도 비가상 소멸자이다.

따라서 STL을 상속받아서 나만의 클래스를 만드는 것은 금물!

(자바의 final이나 C#의 sealed 클래스는 파생방지 메커니즘이 따로 있다.)




순수 가상 소멸자

어떤 클래스가 추상 클래스였으면 하는데

 마땅히 순수 가상으로 만들 함수가 없을 때

순수 가상 소멸자를 지정해주는 것도 좋은 방법이다.

(추상 클래스 --> 기본 클래스 목적으로 만들어짐.)


주의!

순수 가상 소멸자는 정의를 꼭 두어야 한다.


class sss{


public:

sss();

virtual ~sss();

}

sss::~sss();




소멸자의 동작 순서

상속 계통 구조에서 가장 말단에 있는 파생 클래스의

소멸자가 먼저 호출 되고 점점 올라오면서 차례로 소멸자가 호출된다


컴파일러는 ~sss의 호출 코드를 만들기 위해 파생 클래스의

소멸자를 사용할 것이므로, 잊지 말고 이 함수의 본문을 준비해 두어야

하는 것이다. 이 부분을 잊는다면 링커 에러!




잊지말기

가상 소멸자를 선언해주는 규칙은 다형성을 가진 기본 클래스,

기본 클래스 인터페이스를 통해 파생 클래스 타입의 조작을 허용하도록

설계된 기본 클래스에서만 적용된다는 사실!




기본 클래스의 인터페이스를 통해 파생클래스의 객체 조작이 허용될 때만 가상 소멸자!!

앞서 말한 string과 stl은 다형성을 가지지 않음

그리고 전전 포스팅에서 uncopyable이라는 복사, 대입 금지 클래스를 만들었었다.

기본 클래스로는 쓰이고 있지만 다형성을 가지지 않는 클래스라 말할 수 있다.

이러한 경우도 가상소멸자 안됨~



POINT!!

다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 한다.

즉, 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 닥치고 가상 소멸자.


기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는

가상 소멸자를 쓰면 안된다!!



컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자!



사본이 나오면 안되는 클래스가 있다.

class Player{ .. }


Player p1;

Player p2;

p1 = p2;

Player p3(p1);


하지만 컴파일러가 만들어낸 함수

기본 복사 생성자, 기본 복사 대입연산자에 의해

사본이 만들어지는 것이 가능하다.


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


class Player {

private:

Player(const Player&);

Player& operator=(const Player&);

}


첫번째 방법. 복사 생성자, 복사 대입연산자를 선언해준다.

(명시적으로 선언이 되었기 때문에 컴파일러는 기본 함수를 만들지않는다.)

또한 접근 지정이 private로 되어 있어 외부 호출을 할 수도 없다.

그리고 정의를 아예 해주지 않는다면 혹시나 모를

friend 선언에 의해 호출이 되더라도 링크 시점에서 에러가 뜬다.


즉, private도 선언하고 정의를 해주지 말자는 것이다.



두번째 방법. 복사 방지를 막는 Base 클래스를 만들자.

class uncopyable{

private:

uncopyable(const uncopyable&);

uncopyable& operator=(const uncopyable&);

}


class Player : private uncopyable { ... }


컴파일러가 생성한 복사 함수는 Base 클래스의 대응 버전을 호출하게 되어있다.

하지만 base 클래스의 복사 함수는 private으로 선언되어있다. (호출 안됨)

또한 링크시점 에러에서 컴파일 시점 에러로 변경된다 (에러를 미리 알 수 있다.)




'C++ 심화' 카테고리의 다른 글

[C++] 소멸자와 예외처리  (0) 2016.12.19
[C++] 가상(virtual) 소멸자  (0) 2016.12.18
[C++] 생성자, 소멸자, 대입 연산자에 주의를 기울이자.  (0) 2016.12.15
[C++] 객체의 초기화  (0) 2016.12.15
[C++] const의 활용  (0) 2016.12.15

C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자.



생성자

새로운 객체를 메모리에 만드는 데 필요한 과정을

제어하고 객체의 초기화를 맡는 함수


소멸자

객체를 없앰과 동시에 그 객체가 메모리에서

적절히 사라질 수 있도록 하는 과정을 제어하는 함수


대입 연산자

기존의 객체에 다른 객체의 값을 줄 때 사용하는 함수


컴파일러가 저절로 선언해 주도록 되어 있는 것들

생성자, 복사 생성자, 복사 대입 연산자, 소멸자

--> 모두 public, inline

기본적인 내용이기 때문에 예시는 생략한다.



1. C++의 참조자는 원래 자신이 참조하고 있는 것 외에 다른 객체를 참조할 수 없다.

(초기화 시에 이미 참조를 결정한다.)

2. const 또한 참조와 마찬가지로 한번 정해진 값을 바꿀 수 없다.


이러한 부분들이 컴파일러에 의해 만들어지는 기본 대입연산자로

처리하기가 상당히 애매한 부분이 될 수 있다.

--> 자신이 직접 대입 연산자를 정의하여 사용해야 한다.


짚고 넘어가기

대입 연산자를 private로 선언할 경우

이 클래스로부터 파생된 클래스는

기본 대입 연산자를 가질 수 없다.



POINT!!


컴파일러는 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를

암시적(디폴트) 으로 만들어 놓을 수 있다.

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


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

이번 장에서는 고맙게도 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에 대해 상당히 많은 내용이 정리되어 있다.

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

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


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

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


가급적 선행 처리자보다 컴파일러를 더 가까이 하자.

#define SIZE 10

--> 컴파일러에겐 보이지 않는다. 전처리기가 컴파일러에게 넘어가기 전에 숫자 상수로 대입해 버린다.

--> 기호 테이블에 들어가지 않는다.

--> 에러가 날 경우 SIZE라 표시 되지 않고 10으로 표시 된다 --> 디버깅이 어려움


해결책

const int Size = 10;

--> 매크로 대신 상수를 쓴다.

--> 컴파일러의 눈에도 보이며, 기호테이블에 들어간다.

--> 등장 횟수 만큼 사본을 만드는 매크로와는 달리 단 하나의 원본만 있으면 된다.


tip!!

상수의 정의는 대부분 헤더파일에 넣는다.

ex)

define.h 헤더 파일에 const double Width = 5.4;  정의를 해놓고

  cost double Height = 7.7;  


사용하고자 하는 파일에 가져다가 사용한다.

#include "define.h" 



#define를 상수로 교체할 때 주의점.


1. 상수포인터를 정의하는 경우


const char* const name = "Kim";

const stirng name("Kim");


참고)

1. char* name;    

2. const char* name --> 가리키는 대상을 상수화 시킨다.

3. char* const name --> 포인터 자체를 상수화 시켜 다른 객체를 가리키지 못하게 한다.

4. const char* const name --> 2,3 모두 해당


2. 클래스 멤버로 상수를 정의하는 경우


--> 상수의 유효범위를 클래스 내로 한정하고자 할 때 사용

--> 상수의 사본 개수가 한 개를 넘지 못하게 만들고 싶다면 static멤버로 만든다.


class Cost{

private:

static const int Size;    //  방법 1 : static const int Size = 10 --> 으로 해줄 수도 있으나 지원하지 않는 컴파일러가 있을 수 있다.

int arr[Size];

};

방법 2 : 

const int Cost::Size = 10;  --> static 멤버 초기화 문법 --> main호출 전에 실행된다. 다른 멤버 변수를 이렇게 정의할 수 없다.


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


2번째 방법으로 정의를 해줄 경우 int arr[Size]; 에서 오류가 발생한다.


해결책 --> 나열자 둔갑술 (enum hack)


class Player{

private:

enum { Size = 10 };

int Item[Size];

};

--> 나열자 둔감술은 동작방식이 const보다 #define에 가깝다.

--> const는 주소값을 얻어 낼 수 있지만, enum은 주소값을 얻어낼 수 없다.

--> 따라서 상수를 가지고 주소를 얻는다든지 참조자를 쓴다든지 하는 경우를 방지할 수 있다.

--> enum은 #define처럼 어떤 형태의 쓸모없는 메모리를 할당하지 않는다. --> 그럼 const는 한단 말인가?

--> 나열자 둔갑술은 상당히 상용화된 기술이며 템플릿 메타프로그램의 핵심 기법이다.


클래스 상수를 #define로 만들 경우

--> 유효범위가 없다.

--> 컴파일이 끝날 때까지 유효하다.

--> 캡슐화의 개념이 없다. (private)

--> 따라서 임의의 클래스 안에서만 쓸 수 있는 매크로를 만들 수 없다.


메크로 함수


#define CALL_WITH_MAX (a,b) f((a) > (b) ? (a) : (b))   --> 인자마다 괄호를 씌워주어야 한다.


int main(){

int a = 5;, b = 0;

CALL_WITH_MAX(++a, b);               //a가 두 번 증가   --> 매크로 함수 내에서 a가 두번 불리게 된다.

CALL_WITH_MAX(++a,b+10);          //a가 한 번 증가

}


해결책 --> 인라인함수에 대한 템플릿


template<typename T>

inline void callWithMax(const T& a, const T& b){

f(a > b ? a : b);

}


1. 이 함수는 템플릿이기 때문에 동일 계열 함수군을 만들어낸다.

2. 구현의 어려움 ( 괄호와 같은 )도 없고 인자를 여러번 평가할 (위와 같은 문제) 요소가 사라졌다.

3. 진짜 함수이기 때문에 유효범위 및 접근규칙을 따를 수 있다.


동일 계열 함수군이란?

--> 하나의 템플릿으로 int, double, char 등 여러개의 함수가 만들어 질 수 있다.

      따라서 동일 계열 함수군은 하나의 템플릿으로 만들어 질 수 있는 모든 함수들을 통칭한다.



POINT!!

1. 단순한 상수를 쓸 때는, #define보다 const 객체 혹은 enum을 우선 생각하자.

2. 함수처럼 쓰이는 매크로를 만들려면, #define 보다 인라인 함수를 우선 생각하자.

+ Recent posts