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

Posted at 2008.11.11 08: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) {}
};

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

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

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

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

신고
  1. esstory

    2008.11.11 15:59 신고 [수정/삭제] [답글]

    실수를 예방하기 위해 Visual Studio 에서만 통하지만
    __interface 라는 것도 있습니다.
    __interface IMyInterface
    {
    HRESULT CommitX();
    HRESULT get_X(BSTR* pbstrName);
    };
    식으로 사용하면 두 함수 모두 순수 가상 함수
    virtual HRESULT CommitX() = 0;
    virtual HRESULT get_X(BSTR* pbstrName) = 0

    로 해석 되거든요.
    실수를 예방하기 위한 거 같습니다.
    인터페이스를 설계할 일이 많아지니 이런 편의 클래스를 더 자주 사용하게 되더군요^^;
    좋은 하루 보내세요~

  2. Hybrid

    2008.11.12 06:30 신고 [수정/삭제] [답글]

    사실 virtual = 0; 과 virtual {} 의 차이는 크죠.
    패턴의 관점(이라고 표현하는게 맞나..) 의미로 보면, virtual {} 로짜는건 그야 말로 다중 상속을 염두해서 짜는 것이고, = 0; 로 짜는 것은 '현재 클래스를 추상(abstract) 클래스로 선언하겠다' 라고 말하는 셈이 되니까요..

    virtual 을 쓰다보면 다양한 디자인 패턴이 들어가고.. 그러다보면 코드가 무척 꼬이게 됩니다. 좀 더 정확하게 알고 사용하는 것이 좋겠죠.

  3. 감사합니다.

    2010.02.08 23:40 신고 [수정/삭제] [답글]

    초본데요.
    프로그램 연습하다가 딱 막히는 부분이 있었습니다.
    하루 종일 찾아봐도 문제가 없는거 같은데 계속 에러만 나더라구요.
    콘솔에선 되던게 MFC에선 안되니 미치는줄 알았습니다.
    님의 글보고 한방에 해결했네요.
    정말정말 감사합니다.^^*

댓글을 남겨주세요.

티스토리 툴바