C++이야기 스물다섯번째: 구현이 없는 가상함수는 반드시 순수가상함수로!

Posted at 2008. 11. 12. 01:19 // in S/W개발/C++ 이야기 // by 김윤수


2008/10/31 - [S/W개발/C++ 이야기] - C++이야기 스물네번째: Override할 가상함수의 Prototype은 확인 또 확인!

이번 글도 저번글에 이어 C++로 프로그래밍하다가 저지르기 쉬운 실수입니다. 이번에도 바로 코드부터 보겠습니다.

/// @file virtual.cpp
#include <iostream>
#include <string>

using namespace std;

class Base
{
public:
    virtual void hello(const string& name);
};

class Derived : public Base
{
public:
    virtual void hello(const string& name)
    {
        cout << "Hello, " << name << "!" << endl;
    }
};

int
main()
{
    Base* p = new Derived();

    p->hello("KKK");
}

이 코드를 컴파일해서 실행시키면 어떤 결과가 출력될까요? 이번에도 함정이 숨어 있으니 코드를 잘 주의해서 보시기 바랍니다. 당연히 "Hello, KKK!"라고 출력될까요? 그렇다면 제가 문제를 내지도 않았겠지요?

정답은 바로 바로
.
.
.
.
.
.
.
.
.
.
"링크시에 에러가 발생한다"였습니다. 엥? 하시면서 눈을 마구 굴리시는 분이 보이는군요. ^___^

Visual C++ 2005 Express로 컴파일했더니 다음과 같은 에러가 발생하네요.

virtual.obj : error LNK2001: unresolved external symbol "public: virtual void __thiscall Base::hello(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &)" (?hello@Base@@UAEXABV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z)
C:\Documents and Settings\김윤수\My Documents\Visual Studio 2005\Projects\virtual\Debug\virtual.exe : fatal error LNK1120: 1 unresolved externals

다시 잘 해석해 보자면 Base::hello() 멤버 함수가 정의되어 있질 않다는 거네요. Derived::hello()를 정의해 놓았으니 p->hello() 하면 Derived::hello() 하고 링크되는 거 아닌가? 왜 Base::hello()를 찾지??? 하면서 의아해 하시는 분들이 있는 것 같네요.

Base::hello()는 위 소스코드에서는 사용되진 않지만 실제로는 Base의 virtual function table(vtbl 이라고도 하죠)을 채워넣을 때, Base::hello()를 참조하게 됩니다. Base::hello()를 선언할 때 순수가상함수로 선언하지 않았기 때문이지요.

소스코드에서는 Base::hello()가 한 번도 사용되진 않지만 컴파일러가 내부적으로 생성하는 데이터 구조(vtbl)에서는 Base::hello()를 참조하고 있기 때문에 unresolved symbol에러가 발생하는 것입니다. 실제로 Base::hello()의 구현을 추가한 후, Visual C++ 디버거에서 확인하면 다음과 같이 Base 클래스와 Derived 클래스에 대해 각각 vtbl을 생성한 것을 볼 수 있습니다.


이런 버그도 프로젝트 규모가 커지다 보면 아주 간단한 실수임에도 찾기가 무척 어렵습니다. 왜냐면 사람눈에는 문제가 잘 보이지 않기 때문이죠. 컴파일러 내부적으로 vtbl을 구성할 때, 순수가상함수로 선언되어 있지 않은 것들은 모두 포함된다라는 사실을 알고 있어야 하고, 거기에 덧붙여

class Base
{
public:
    virtual void hello(const string& name) = 0;
    // 또는
    virtual void hello(const string& name) {}
};

빨갛게 표시한 부분을 빼먹었다는 걸 알아챌 수 있어야 합니다. 그런데 아주 근소한 차이이기 때문에 찾기가 무척 힘듭니다.

그러니 이런 버그 찾으려고 몇 시간씩 헤매지 마시고 코딩할 때 항상

"구현이 없는 가상함수는 순수가상함수로 선언"

하는 습관을 들이시면 좋을 것입니다. 만약 순수한 인터페이스를 선언할 의도가 아니었다면 가상함수를 반드시 구현해야 할 것입니다.