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. 두 개 이상의 객체에 대해 동작하는 함수가 있다면,

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

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


함수 포인터와 void 포인터


함수포인터와 void 포인터

이것들이 도대체 무엇인지에 대해

정말 간만보는 포스팅을 진행하겠다.




함수는 CPU에 의해서 실행된다.

CPU에 의해 실행되려면 메모리에 올라와 있어야한다.

(메인 메모리)




함수의 몸체가 메모리 상에 올라가고

그 놈이 어딨는지를 알아야 한다.

고로 함수의 이름이 함수의 몸체가 

메모리의 어디에 있는지 가리키는 포인터가 된다.


void fct(int n, int n2){

..

..

}




함수 포인터의 구성 = 몸체의 주소 + 자료형


함수의 포인터 타입을 결정짓는 요소

1. 리턴 타입

2. 매개 변수 타입




함수 포인터의 선언

void (*pfct)(int n, int n2) = fct;

리턴 타입과 매개 변수의 타입이 같다!

fct는 포인터이기 때문에 선언된 함수포인터에

자신이 가리키는 함수의 주소값을 넘겨줌.


int n = 10;

int * p = &n;

int *fp = p;

위와 같은 형태이다.


사용법 또한 같다

pfct(1,2);

fct(2,4);


pfct = fct ==> 주소값도 같다




void형 포인터

포인터는 주소값을 저장할 수 있는 변수

하지만 자료형에 대한 정보가 제외된 형태.


void* p;

 int, char 구분 없이 저장 가능하다.


int n = 10;

char c = 'c';

void* vp;

vp = &n;

vp = &c;


가능한 이유?

자료형에 대한 정보가 제외되어 있기 때문.


하지만 기능이 제한된다.

포인터 연산, 메모리 참조와 관련된 일에 활용 할 수 없다.


왜?

포인터 연산을 했을 때 메모리를 얼마나 참조할 것인가에 대한 정보 --> 자료형

void 포인터는 어떠한 데이터 특성을 지니는지의 정보가 없다

메모리를 어떻게 참조할지 모른다.(타입에 대한 정보가 없기 때문)


int main(){

int n = 10;

void * vp = &n;

*vp = 20;

 --> 1바이트를 참조할지 4바이트를 참조할지 모른다.

vp++;

--> 이 또한 마찬가지

}




함수 포인터와 void 포인터의

정말 기본적인 내용에 대해 알아보았다.

활용법에 대해서도 충분히 알아야 할 것 같으니

다음 포스팅에서 좀 더 자세히 알아보도록 하겠다.

GoodBye~









STL에 필요한 주요 연산자 오버로딩


이번 시간은 STL에 필요한

연산자 오버로딩에 대해 알아보겠다.

연산자 오버로딩에 대한 기초적인 지식만 있다면

충분히 따라올 수 있는 내용이다.


1. 함수 호출 연산자 오버로딩



2. 배열 인덱스 연산자 오버로딩


3. 멤버 접근 연산자 오버로딩

스마트 포인터란?

일반 포인터를 사용하면 new 연산 후

delete 연산을 꼭 해주어야 한다.(안해주면 메모리 누수)

스마트 포인터는 소멸자에서 자신이

동적으로 생선한 객체를 자동으로

소멸시켜주기 때문에 메모리 누수의 위험이 없다.


4. 타입 변환 연산자 오버로딩





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

[C++ STL] STL에 필요한 템플릿 예제  (0) 2016.12.21
[C++ STL] 간단한 콜백 메커니즘  (0) 2016.12.20
[C++] 전방 선언  (0) 2016.12.19
[C++] 팩토리 함수  (0) 2016.12.19
[C++] API란?  (0) 2016.12.15

전방 선언에 대해 알아보자.


먼저 전방 선언이란?

불필요한 헤더 파일이 복잡하게 포함되는 것을 방지하며, 컴파일 속도를 향상시켜준다.

전방 선언은 헤더 포함 의존성을 줄이기 위해 사용된다.




특정 클래스를 정의할 때, 멤버 변수나 리턴 값, 파라미터 등으로

또 다른 클래스를 사용한다면 해당 클래스의 정의도 포함시켜야 한다.


즉, AAA 라는 클래스를 정의할 때

BBB 를 사용한다는 것이다.


일반적으로는 


#include "BBB.h"

class AAA{

...

...

}


별 문제없는 방법으로 보일 수 있으나,

임포트한 헤더 파일에는 현재 클래스에서 필요로 하는

정보 외에도 많은 정보를 포함하고 있다.

또한 BBB.h에 다른 h들도 포함되어 있다면..?

필요이상의 시간을 소비하는 결과를 낳게 된다.




이러한 문제의 해결을 위해 우리는 전방선언과 친하게 지내야 한다.

문제가 되었던 상호 참조 문제라던가

컴파일 부하문제를 깔끔하게 해결해준다.


예제를 보자.


AAA 클래스.



BBB 클래스.


BBB 클래스 --> AAA를 include하지 않고 전방선언을 해주고 있다.

주의 할 점은 BBB클래스는 AAA라는 클래스의 존재 유무만 알고있다.

때문에 AAA의 구체적인 크기에 대해선 알지 못한다.


따라서 포인터로만 선언될 수 있다.

이를 동적 생성하거나 함수를 호출하면 에러!!

AAA a; 로 선언 되어도 에러!!


다시 한번 말하지만 전방 선언은 단순히 선언이기 때문에

생성, 호출은 실제 데이터 구조를 모르는 상태에서 진행될 수 없다.




전방 선언된 AAA를 BBB.cpp에서 구현


전방 선언을 하는 것은 선언된 클래스의 이름을 간단히 컴파일러에게

심볼 테이블에 추가하라고만 하는 것이 아니라, 실제로 이 클래스가

필요할 때 그 정의를 함께 제공하겠노라고 약속하는 것이다.


따라서 전방선언한 클래스에 접근하는 cpp 파일에는

반드시 #include "AAA.h" 를 선언하고 사용해야 한다.




예제를 보았지만 예제가 매우 허접하여 감이 안잡힌다.

전방선언이 요긴하게 쓰이는 부분에 대해 더 짚어보자.


1. 포인터/ 참조 형식으로 이름만 참조할 경우


class AAA;

class BBB{

public:

BBB doSomething1 (const AAA* a);

BBB doSomething2 (const AAA& a);

};


2. 매개변수나 리턴 타입을 위한 이름만 참조할 경우


class AAA;

class BBB{

public:

void SetData(AAA a);

AAA GetData() const;

};


이 경우 컴파일러가 AAA의 크기를 알아야 한다고 생각할 수 있지만,

함수를 구현하는 코드와 호출하는 코드에서만 클래스의 크기를 요구한다.




간단하게나마 전방선언에 대해 알아보았다.

그 필요성에 대해서는 알겠으나,

아직 많이 낯선것도 사실. 분발하자.















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

[C++ STL] 간단한 콜백 메커니즘  (0) 2016.12.20
[C++ STL] 연산자 오버로딩  (0) 2016.12.19
[C++] 팩토리 함수  (0) 2016.12.19
[C++] API란?  (0) 2016.12.15
[C++] 예외처리 // 열혈강의  (0) 2016.12.14

대입 연산자는 *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. 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 

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

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






팩토리 함수


팩토리 함수, 팩토리 메소트, 팩토리 메소드 패턴이라 불리는데

객체를 생성하기 위한 인터페이스를 제공하는데, 어떤 클래스의

인스턴스를 만들지는 서브클래스에서 결정한다.

즉, 클래스의 인스턴스를 만드는 일은 서브클래스가 한다는 말이다.




팩토리 메서트 패턴은 추상클래스에서 객체를 만드는 인터페이스만 제공한다.

추상클래스는 어떤 객체가 생성될지 전혀 모른다.

어떤 객체가 생성될 지는 이 추상클래스를 상속받은

서브클래스에서 실제로 팩토리 함수를 구현함으로써 이뤄진다.


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


MonMgr이라는 클래스는 객체를 생성하기 위한 인터페이스만 제공한다.

그리고 객체의 포인터를 리턴하는 팩토리 함수를 멤버로 가지고 있으며

MonMgr의 파생클래스에서 이 함수를 구현함으로써 어떤 객체가 생성될지 결정된다.




예제가 많이 허접하다. 그래도 팩토리 함수가 무엇인지는 감이 오는 것 같다.

그렇다면 이 팩토리 함수를 어떤 경우에 써야 효율적인 것일까.



1. 구체적으로 어떤 객체를 생성해야할지 알 수 없는 경우

2. 하위 클래스가 객체를 생성하기를 원할 때

3. 하위 클래스들에게 개별 객체의 생성 책임을 분산시켜

객체의 종류별로 객체 생성과 관련된 부분을 국지화 시킬 때



음.. 어떤 것인지는 알았으나 어떤 경우에 써먹어야 하는지

아직은 모르겠다.. 코딩 경험이 쌓이다보면 자연스레 알게되겠지

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

[C++ STL] 연산자 오버로딩  (0) 2016.12.19
[C++] 전방 선언  (0) 2016.12.19
[C++] API란?  (0) 2016.12.15
[C++] 예외처리 // 열혈강의  (0) 2016.12.14
[C++] 템플릿 // 열혈강의  (0) 2016.12.12


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


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


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

+ Recent posts