클래스 전방 선언(Forward declarations)와 상호 참조 헤더 파일

Linux/C++ 2013. 12. 4. 10:57

class A 가 a.h에 있고

class B 가 b.h 에 있는데

B가 A를 사용하고 A가 B를 사용할 경우 :

거의 설계가 잘 못되었다고 볼 수 있다.

이렇게 프로그램을 짜면 이해하기가 힘들고 추적이 힘들기 때문에 경험많은

프로그래머들은 이렇게 짜지를 않는다.

 

흔히 전방선언이라고 하는 것은

 

class A;

class B

{

...

    A *pA;

};

 

와 같이 포인터만 이용할 때 가능합니다. class A의 포인터라면 그냥 4바이트(32bit OS)만

할당해 놓을 수 있지만 이것을

 

class A;

class B

{

...

   A a;

};

 

와 같이 하면 몇 byte를 할당해야 할지 알 수가 없으므로 에러를 냅니다.

이럴 경우에는 할 수 없이

 

#include "a.h"

class B

{

...

    A a;

};

 

이렇게 해야하는데

A a;

를 확인하려고 a.h 를 살펴보니

 

#include "b.h"

class A

{

...

   B b;

};

 

이렇게 (헤더 꼬임 현상)이 발생해서 무한 루프에 빠지게 됩니다.

(당연히 최상단에 올린 해더는 아래 해더를 모르므로 컴파일러는 알아서 에러를 내줍니다.)

이를 해결 하는 방법은 포함순서를 잘 맞추든지 아니면...

설계를 약간 바꾸는 것(전방 선언후 객체의 포인터 사용)이 좋지 않을까 합니다.

 

//////////////

 

class A / class B 가 있다. 그리고 A 는  B 를 사용할 것이다.

그럼 우리는 무엇을 해주어야 하는가?

 물론 A 쪽에 #include "B.h" 를 해주어야 할 것이다. 그래야 A 에서 B를 사용할 수 있으니까....

 

그런데.......

#include 의 수가 많아질수록 컴파일 속도가 저하된다는 것도 아시는지.....

이유는 생각해보면 알수 있다. 위에서 처럼 A 에 B 를 include 한다고 할때 만약 B.h 가 수정되면 include 부분도 이 영향을 받게 되며 수정이 많아질수록 컴파일 속도도 느려지게 된다.

 

이것을 해결하는 방법은? 전방선언을 사용하는 것이다.

(전방선언의 좋은점은 참조하려는 해더파일에 변경이 생겨도 참조 하는 해더파일에서는

재컴파일이 이루어 지지 않는다는 점이다.)

 

전방선언이란 가령 Player 라는 클래스가 선언된 헤더파일이 있을 경우

#include "Player.h" 대신

class Player;

이렇게 선언하는 것이다.

 

단, 주의해야 할 점이 있다. 전방선언자를 사용할 경우에는 그 클래스 관련 객체는 포인터형으로 선언해야 한다는 것이다. 만약 포인터형이 아닌 객체를 생성할 경우 전방선언자의 특징상 그 객체의 크기를 정확히 파악하여 할당을 못해주기 때문이다. (해당 클래스가 있다는 정보만 알고 사이즈를 모른다)

 

컴파일 적인 면에서 우리는 전방선언이 인클루드보다 속도, 의존관계 면에서 더 좋다고 하였다.

여기에 하나 더 추가하자면 다음과 같은 경우 우리는 전방선언을 유용하게 써먹을 수 있다.

 

class A, B 가 있다.

B는 A를 사용하고 있다 ( A 헤더를 인클루드하여 사용.... 상속은 아니다 )

그런데 우리는 부득이하게 A에서 B의 정보를 알아야 하는 상황에 놓이게 된다면....?

 

A는 B의 데이터를 알기위해선 B 의 구조를 알아야 하고 즉 B 헤더를 인클루드 해야 된다.

하지만 B에서 이미 A 헤더를 인클루드 하고 있기에 A 에서 B 헤더를 인클루드하면 상호 참조 에러가 되어버리고 만다. 즉 구조를 바꾸지 않는한 A 는 B 헤더를 인클루드 할수 없다는 것이다.

 

만약에 인클루드 대신에 앞에글에서 처럼 전방선언자를 사용한다면...?

 

#include "A.h"                                class B;

 

class B                                         class A

{                                                  {

   ///....                                           ///....

}                                                   }

 

이렇게 하면 우선 에러는 나지 않는다. ( 상호참조의 초석을 놓았다 )

다만 전방선언자를 사용하는 class A 에서는 B 를 포인터형으로 선언 또는 받는 처리만 가능할뿐

이를 동적 생성하거나 함수를 호출하면 에러가 나게 된다. 왜냐하면 전방선언자는 단순히 선언이기 때문에 생성, 호출은 실제 데이터 구조를 모르는 상태이므로 에러가 나게 되는 것이다.

그럼 동적 생성이나 호출을 하고 싶으면 어떻게 해야 하는가?

A.cpp 에서 구현을 하면 된다. 그리고 cpp 에 B.h 를 인클루드 하면 비로소 상호 참조가 가능하게 된다.


[출처] [펌] 전방선언자 (게임 프로그래밍 카페) |작성자 나르메


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

가상함수  (0) 2013.12.04

가상함수

Linux/C++ 2013. 12. 4. 10:49

가상함수라는것이 어떻게 쓰일 수 있는지를 생각해보자. 객체지향 구조에서 클래스의 상속 기능을 이용할 때 이 가상 함수가 많이 사용될 수 있다. 


일반적으로 가상함수는 함수를 선언할 때 제일 앞쪽에 virtual 키워드를 붙여서 선언할 수 있다. 그리고 이함수는 부모객체의 포인터로 자식개체의 함수를 불러올 수 있게 해준다. 보통의 멤버함수는 자식 클래스에서 오버라이딩을 하더라도 부모 객체의 포인터에서 호출하면 부모클래스의 함수가 호출된다. 하지만 가상함수를 쓰게 되면 자식클래스의 함수가 호출이 된다는 것이다. 사실 이렇게만 말하면 무슨 말인지는 잘 모르겠다. 예를 보면 확실히 이해를 할 수 있을 것이다. 

#include <iostream>
using namespace std;

class B
{
public :
  virtual foo(int a)
  {
    cout << "B inside int " << a << endl;
  }
  virtual foo(double a)
  {
    cout << "B inside double " << a << endl;
  }
};

class D : public B
{
public:
  foo(int a// B 에서 오버라이딩한 함수 
  {
    cout << "D inside int " << a << endl;
  }
};

int main(int argc, char **argv)
{
  D d;
  B b, *pb = &d;
  
  b.foo(9);  // B inside int 9
  b.foo(9.5);  // B inside double 9.5
  
  d.foo(9);  // D inside int 9
  d.foo(9.5);  // D inside int 9
  
  pb->foo(9);  // D inside int 9
  pb->foo(9.5)// B inside double 9.5
  
  return 0;
}  


자, 이 예시의 주석만 보더라도 위에서 이야기 했던것이 다 이해가 갈 것이다. 클래스 B가 부모 클래스이고 C는 B에서 상속을 받은 자식 클래스이다. 그리고 B는 객체를 두개 만들었는데 일반 객체 b와 포인터 객체 pb를 선언했고 pb는 D의 객체인 d의 주소값을 받아온다. 이럴 경우에 d, b는 함수를 실행시키면 각각 D,B자체에 선언된 함수가 실행이 된다.(d의 경우 int로 받는 foo(int a) 밖에 없지만 9.5가 들어갈 경우 알아서 int로 형변환이 된다. 그리고 pb는 int값이 들어갈 경우 D(자식)함수가, double값이 들어갈 경우 B의 함수가 실행이 된다. 객체하나로 필요한 D, B의 함수를 다 실행 시킬 수 있다. 만약 virtual로 선언 되지 않았다면 어떻게 될까? 위에 말한대로 B의 함수만 실행 될 것이다.(확실하지는 않다. 안해봤기 때문에..)


추가) 순수 가상함수라는것이 있다. 이것은 함수의 원형만 있고 구현은 없는 가상함수를 이야기 하는 것이다. 
virtual int myclassfunc(int a, int b) = 0
이와 같은 형식으로 함수 원형에 0을 대입시켜서 만들 수 있다. 
이와 같은 순수가상함수를 가진 클래스를 추상클래스라고 하고 추상클래스는 순수가상함수 때문에 객체를 만들 수 없다. 단순히 껍데기 역활은 하는듯 하다.