Post List

2015년 1월 4일 일요일

class간 교차 참조시 오류가 발생하는 경우

error C2146: 구문 오류 : ';'이(가) ... 식별자 앞에 없습니다.
error C4430: 형식 지정자가 없습니다. int로 가정합니다. 참고: C++에서는 기본 int를 지원하지 않습니다.
error C4430: 형식 지정자가 없습니다. int로 가정합니다. 참고: C++에서는 기본 int를 지원하지 않습니다.

...

위와 같은 에러가 나는 이유는 클래스를 찾을 수 없는 경우에 발생합니다. 분명히 클래스가 있는데도 불구하고 이런 에러메시지가 발생하는 경우는.. 보통 교차포함을 한 경우에 이런 에러가 자주 발생합니다.
이게 무슨 얘기냐.. 다음을 보시죠..

b.h
#pragma once
#include "a.h"
class B
{
public:
 A a;
};

a.h
#pragma once
#include "b.h"
class A
{
public:
 B b;
};

양쪽의 클래스들은 서로를 필요로 하기 때문에 서로 #include을 해주었습니다. 언뜻 보기에는 문제가 없어보이죠. 실제로 자바에서는 에러가 나질 않습니다. 하지만 c++에서는 에러를 유발합니다. 무수히 많은 에러를 유발하죠.... 아주 많이....

하나하나 따라가보면 그 이유를 알 수 있습니다.
일단 b.cpp파일을 컴파일한다고 합시다. 그럼 b.cpp에는 최상단에 b.h가 include되어있을 겁니다. 그래서 컴파일러는 b.h를 포함시키죠. b.h를 가보니 상단에 a.h가 include되어있습니다. 그럼 그 헤더파일을 포함시키려 하겠죠. 그래서 a.h에 가보니.. 상단에 b.h가 include되어있습니다. 좀전에 b.cpp에서 b.h를 include했는데도 불구 한번더 include하려하지요.(아직 class B가 전부 읽혀진건 아닙니다.) 하지만 #pragma once구문 보이시죠? 이 구문때문에 헤더파일은 단 한번만 불려와지게 됩니다. 결국 a.h에 있는 #include "b.h"구문은 있으나 마나한 셈이 되죠. 그래서 class A의 B b부분에서 클래스 B는 이세상에 없는 클래스로 인식하고 형식이 없다는 에러메시지를 발생시키는 겁니다. 만약 class A에 포함된 에러 헤더파일 중 B를 필요로 하는 헤더파일이 있다면.. 에러메시지는 더욱더 많이 발생할 겁니다. 마찬가지로 a.cpp를 컴파일 하는경우 똑같은 에러가 나겠네요.

또한 서로 포함하지 않았는데도 불구, 이 에러메시지가 발생하는 경우가 있습니다.
그 경우는 서로는 포함하지 않았지만, 각 파일에 포함된 헤더파일들에 포함되어 간접적으로 서로를 포함한 경우가 해당됩니다.
b.h
#pragma once
#include "a.h"
class B
{
public:
 A a;
};

a.h
#pragma once
#include "c.h"
class A
{
public:
 B b;
};

c.h
#pragma once
#include "b.h"
class C
{
};


정말 간단하게 생각하면 #pragma once가 문제라고 생각하실 수 있습니다.
하지만 이걸 없애버리면 더 큰 문제가 발생합니다. class B가 두번 로드되는 경우죠. 심벌은 단 한개만 유효합니다. class B가 여러개 있을수는 없겠죠. 그렇다면, 단 한번만 불려와지고.. a.h에서도 인식할 수 있는 방법이 있느냐..? 물론 존재합니다. 여러가지 기법들이 존재하죠.



해결책

1. 가장 간단한 방법
헤더파일에서는 컴파일러에 단지 "암시"만 주고 포함시키지 않는 방법입니다.
b.h
#pragma once
class A; // class A가 존재한다고만 알려줍니다.
class B
{
public:
 A* a; // 포인터로 바꾸었습니다.
};

a.h
#pragma once
class B; // class B가 존재한다고만 알려줍니다.
class A
{
public:
 B* b;
};

※ 주의) 각각의 소스파일엔 포함시켜줘야합니다. 이렇게 하면 서로 include를 하지 않기 때문에 위와 같은 에러가 발생하지 않습니다. class A; 라고 하기만 하고 끝낸 부분이 눈에 띄는데, 이 부분은 실제로 클래스를 정의하진 않고 단지 앞으로 정의될 것이다, 나중에 링크과정때 찾아보라고 하는 일종의 암시입니다. 당장은 컴파일을 위해 때우는(?) 수단이라고 보시면 되겠죠. 그리고 A a에서 A* a로 포인터로 바뀐 부분을 알 수가 있는데, 일반 변수의 경우 크기를 알아야 하기 때문이죠. 따라서 일반 변수로 했다면 B를 컴파일 하는 도중 A의 크기를 구하려고 할 것이고, 에러를 유발하곤 합니다. 반면에 포인터 변수는 어떤 타입에도 상관없이 4바이트를 고수합니다.

한가지 주의하실 점은, 저렇게 하고 b.h에서 A의 멤버변수(함수)에 접근하는 인라인 함수같은 코드는 넣으시면 안됩니다. 컴파일 되는 당시로썬 A의 상세코드는 알길이 없기 때문이죠. 단 다음과 같은 경우는 가능합니다. 그 이유는 당장엔 A의 내용을 알필요가 없기 때문입니다. 소스파일에 넣으시기 바랍니다.
b.h
#pragma once
class A; // class A가 존재한다고만 알려줍니다.
class B
{
public:
 A* a; // 포인터로 바꾸었습니다.
 B() : a(NULL) {}
 ~B(){ if( a != NULL)delete a; } // 이 구문은 절대 안됩니다. 현재로썬 a의 크기를 알수 없기 때문입니다.
 A* GetAPtr() { return a; }
};




2. 좀더 나은 방법b.h
#pragma once
#include "ABase.h"
#include "BBase.h"
class B : public BBase
{
public:
 B();
 virtual ~B();
 ABase* a;
};

a.h
#pragma once
#include "ABase.h"
#include "BBase.h"
class A : public ABase
{
public:
 A();
 virtual ~A();
 BBase* b;
};

b.cpp
#include "b.h"
#include "a.h"

B::B()
{
  a = new A();
}
B::~B()
{
  if( a!=NULL ) delete a;
}

BBase.h
class BBase
{
public:
  virtual ~BBase(){}
};




클래스를 상속으로 계층관계로 만들어버린 후, 부모클래스를 포함시키는 방법입니다. 정의는 부모클래스지만 당연히 할당은 자식클래스로 하게 되겠죠?^^; 이 방법은 실제로 자주 사용되고 있습니다. 유명한 패턴중 상태패턴에서도 이런 구조로 사용되죠. 그리고 컨테이너들도 저런 방식과 유사하게 설계되었을거라 생각합니다. 하지만, 무리하게 이 방식으로 할 필욘 없다고 생각합니다. 몇몇의 경우는 제외하고는 이 방식보단 처음방식이 더 나을수도 있겠습니다..^^

맺음말
실제로 이런 디자인은 말라고 합니다만.. 현재 라이브러리에서는 이런 디자인이 비일비재합니다. 가장 간단한 예로 MFC에서 부모와 차일드 윈도우 관계죠. 또는 게임 오브젝트 관리자와 게임 오브젝트들과 서로 포함시켜서 프로그래밍을 단순화 시키기도 합니다. 때로는 이런 디자인이 간편하고 가독성에도 일조한단 얘기죠.

출처 : http://ekessy.tistory.com/20