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


먼저 전방 선언이란?

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

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




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

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


즉, 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!!

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






+ Recent posts