다음의 오류들과 모두 관련이 있습니다.
C2011 클래스 형식 재정의
C3861 식별자를 찾을 수 없습니다
LNK2019 함수에서 참조되는 확인할 수 없는 외부 기호
LNK2005 *.obj에 이미 정의되어있습니다.
코드 리팩터링 중 위 에러들을 겪으며 생각한 내용, 이해하고 넘어가야 할 부분 정리했다.
대부분은 전방선언과 헤더 파일 import 하는 과정에서 문제가 발생했을 것이다.
c++에서 다른 파일에 있는 클래스 참조하기
내가 지금 작성중인 a.cpp파일에서, 다른 파일에 있는 클래스를 사용하고싶으면, 먼저 컴파일러에게
해당 클래스가 다른파일에 정말 존재함을 알려줘야하는데 아래 두 가지의 방법이 있다.
- 해당 클래스가 ‘선언’된 헤더파일을 include하기
- 해당 클래스명을 내가 작성중인 a.cpp파일에 클래스를 사용하기 이전(상단)에 선언하기
중복 선언, 순환 참조가 발생할 우려를 없애 줄 전방 선언이라는 개념에 대해서 학습했다. 그리고 기존에는 다른 파일의 클래스를 참조하기 위하여 포함(include) 했었다.
근데 헤더파일들을 마구잡이로 include하다간 중복 선언이나 순환 참조 문제로 골치아픈 링크에러를 마주치게될 수 있다. 저건 진짜 골치아픔.
그래서 우선 헤더파일의 include를 최소화하는 방향으로 가고싶은데, 그렇다면 전방선언을 활용하면 좋을 것이다.
전방선언이란?
전방 선언이란 컴파일러에게 "이 클래스(또는 구조체)가 어딘가 존재하니까 나중에 Linking 될거야! 우선은 이 클래스 내부가 어떤진 몰라도 괜찮아" 라고 하는것과 같은 역할을 한다.
헤더파일을 인클루드하여 컴파일러가 직접 해당 클래스의 존재를 알 수 있게 하는 것과 달리 해당 클래스의 이름과 존재는 알지만 클래스 안에 뭐가 들어있는 지 컴파일러가 알 수 없으므로 불완전 형식 오류가 발생할 우려가 있다.
불완전 형식 오류란 완전하지 않은(해당 클래스에 어떤 멤버, 메서드가 있는지 모르는) 형식에 대하여 실제적으로 메모리를 할당하려고 할 때 발생하는 오류이다. 아래의 예제를 확인해보자
불완전 형식 오류를 발생시킬 InComplete class의 선언부
#include <string>
class InComplete {
public:
int number;
std::string name;
public:
InComplete();
~InComplete();
}
InComplete class를 사용할 User class의 정의부
//InComplete 클래스가 정의 or 선언된 다른 파일을 import 하지 않았다.
class InComplete;
User::User()
{
InComplete inComplete = new InComplete(); // <-- 메모리를 얼마나 할당해야하지?
}
User::~User()
{
delete inComplete;
}
InComplete 형 변수 inComplete에 적절한 메모리를 할당한 후 해당 주소값을 전달해줘야하는데, 여기서 문제는 적절한 메모리를 알 수 없다.
물론 개발자는 직접 InComplete class를 작성했기 때문에 메모리를 얼마나 할당해줘야 하는지 알고있기 때문에 이게 왜 안돼? 하는 등의 에러를 겪을 수 있는 것
불완전 형식의 경우 불완전 형식에 대한 포인터나 new를 통한 동적 할당 등 인스턴스를 생성하는것과 같은 행위는 불가하므로 이러한 행위를 하지 않는수준으로 해당 클래스를 사용하는 경우라면, 전방선언을 활용하면 좋다.
즉 실제로 해당 클래스의 객체를 생성하지 않지만 해당 클래스를 알고있어야 하는 경우에 활용하는 것이 전방선언이다.
예를 들면 아래와 같은 상황에서 사용되면 좋다.
User.cpp
#include "SomeOtherClass.h"
#include "MyClass.h" // MyClass 헤더 파일을 포함해야 함
void User::useMyClassMethod(MyClass& obj) {
obj.myMethod(); // MyClass의 멤버 함수를 호출
}
User.cpp 파일에서는 MyClass 참조 형식을 매개변수로 받아 해당 객체의 myMethod() 메서드를 실행시킬 것이다.
이 때는 MyClass.h 파일을 포함(include)해야 하는 것이 명확하다.
User.h 파일
class MyClass; // 이때는 include 대신 전방선언을 활용하는 것이 유리하다!
class User{
public:
void useMyClassMethod(MyClass& obj);
};
방금 User 클래스의 선언부를 작성하다보니 메서드의 매개변수로 MyClass를 작성해줘야하는데, MyClass가 존재하는지 컴파일러가 알 수 없다. 하지만 MyClass.h 파일을 포함하느니 헤더파일의 참조순환 문제가 발생할 우려가 있으며,
실제로 MyClass에 무슨 멤버 변수, 메서드가 존재하는지 알지 못해도 문제가 발생하지 않는다. 바로 이때 전방선언을 활용하는 것이다.
여기까지 읽으면 다음과 같은 flow로 이해했을 것이다.
- 다른 파일에 있는 클래스를 참조하기 위한 방법은 전방선언, include 의 두 가지가 있다.
- 헤더파일에서 cpp 파일 include를 하는 등 include를 남용하면 링크에러를 종류별로 만나볼 수 있다.
- 따라서 전방 선언이 가능한 경우 웬만하면 전방 선언을 활용하고 싶은데?
그럼 일단 다 전방선언 때려놓고 정말 필요한 경우에만(객체 생성, 포인터 사용) 완전 형식으로 사용하기 위해 헤더 파일 include 해서 사용하면 되는걸까?
컴파일러는 몰라요 결정해주세요
결론부터 얘기하자면 위 마지막 문장 처럼만 생각하면 안된다. 고약하게 꼬여있는 포함관계에서 문제가 발생할 수 있으니까
어떤 클래스를 사용함에있어, 전방선언과 헤더파일 include를 모두 해놓은 상황에서, 만약 불완전형식으로 해당 클래스를 사용해도 문제없다면, 전방선언했으니까 그걸로 처리하고, 만약 객체 생성이나 포인터 사용을 해야하는 경우라면 내가 헤더파일 include도 해놓았으니까 해당 클래스안의 내용을 파악하고 동적할당도 해줘~ 라는 방식으로 컴파일러에게 판단을 맡길수가 없다.
조금 짧게 말하면 내가 사용할 클래스가 B인데, B의 멤버를 몰라도되면 모르는대로 알아야되면 컴파일러가 “알아서 잘” inlcude 한 파일 내의 클래스 선언부를 확인해서 잘 해줘 라는 식으로는 할 수 없다.
정리
위의 내용을 이해했다면 아래 문장들을 쉽게 이해할 것이다.
- 헤더파일을 인클루드 하지않은채 불완전형식 오류가 날 경우 헤더파일을 포함(include)하고
- 헤더파일을 인클루드 했는데 이상하게 불완전형식 오류가 날 경우 무분별한 전방선언을 하지 않았는지 의심해보자 (컴파일러가 말한다 “왜 전방선언해놓고 포인터를 쓰냐!”)
2번 문제에 대해서 자세히 얘기하자면,
꼭 전방선언 문제가 해당 클래스에 국한되지는 않는다. 개발자입장에서 a.cpp 파일에서 c.h에 작성된 클래스를 전방선언했는데 아예 다른 파일인 b.cpp파일에 어떤 클래스를 동적 할당하는 부분에서 불완전형식 오류가 날 수 있음
올바른 활용 예제를 보자.
B,C의 객체를 관리할 Manager 클래스가 있다.
// Manager.h 파일
class B;
class C;
class Mananger{
public :
C* _C;
B* _B;
public:
Manager();
~Manager();
}
Manager.h, Manager.cpp 헤더파일과 소스파일을 분리해놓았기때문에, B,C의 포인터변수를 멤버로 들고있긴하지만, 이 헤더파일에서_B, _C를 동적할당하지는 않는다. 이때는 Manager.h 파일 내에서 컴파일러가 B와C의 멤버까지 알 필요가 없고 그냥 존재만 알려주면 될일이니까 전방선언을 하는게 보통이다.
//Manager.cpp 파일
#include "B.h"
#include "C.h"
Mananger::Manager()
{
_B = new B();
_C = new C();
}
Mananger::~Manager()
{
delete _B;
delete _C;
}
그러나 소스파일에서는 Manager 클래스가 생성 될 때 B,C 객체가 동적할당 되길 원하므로, 여기서는 B,C 클래스의 선언부가 포함된 헤더파일을 포함해야한다. 왜냐면 B,C의 멤버들을 알아야 적당한 크기의 메모리를 할당해 줄 수 있으니까!
이런식으로 전방선언과 헤더파일 포함을 완벽 분리해야 링크에러나 불완전형식 오류가 안나고 서로서로 참조하느라고 포함관계가꼬인채 전방선언까지 들어있다면 상단에 적은 오류 4종을 종류별로 만나볼 수 있다.