Computer >> 컴퓨터 >  >> 프로그래밍 >> C++

C++에서 비가상 함수 내부에서 가상 함수를 호출하면 어떻게 될까?

C++의 다형성(polymorphism)은 객체 지향 프로그래밍의 핵심 개념 중 하나입니다. 이 글에서는 비가상(non-virtual) 멤버 함수 내부에서 가상(virtual) 함수가 호출될 때 어떤 일이 일어나는지 알아보겠습니다. 두 가지 경우로 나누어 실행해 본 뒤, 그 원리를 분석해 보겠습니다.

  • 첫 번째 경우: 가상 함수 없이 프로그램을 실행합니다.
  • 두 번째 경우: 비가상 함수 내부에서 가상 함수를 호출하며 프로그램을 실행합니다.

첫 번째 경우: 가상 함수 없이 실행

먼저 virtual 키워드 없이 기본 클래스와 파생 클래스에 같은 이름의 함수를 정의하고 실행해 보겠습니다.

#include <iostream>
using namespace std;

class BaseClass {
public:
    void display() {
        cout << "Print function from the base class" << endl;
    }
    void call_disp() {
        cout << "Calling display() from base" << endl;
        this->display();
    }
};

class DerivedClass : public BaseClass {
public:
    void display() {
        cout << "Print function from the derived class" << endl;
    }
};

int main() {
    BaseClass *bp = new DerivedClass;
    bp->call_disp();
    return 0;
}

출력 결과

Calling display() from base
Print function from the base class

포인터 bp는 실제로 파생 클래스 객체를 가리키고 있지만, display()가 가상 함수가 아니기 때문에 컴파일 타임에 포인터의 정적 타입(기본 클래스)을 기준으로 호출될 함수가 결정됩니다. 따라서 기본 클래스의 display()가 실행됩니다.

두 번째 경우: 비가상 함수 내에서 가상 함수 호출

이번에는 기본 클래스의 display() 앞에 virtual 키워드만 추가해 보겠습니다. 주목할 점은 call_disp()는 여전히 비가상 함수라는 것입니다.

#include <iostream>
using namespace std;

class BaseClass {
public:
    virtual void display() {  // virtual 키워드 추가
        cout << "Print function from the base class" << endl;
    }
    void call_disp() {  // 여전히 비가상 함수
        cout << "Calling display() from base" << endl;
        this->display();
    }
};

class DerivedClass : public BaseClass {
public:
    void display() override {
        cout << "Print function from the derived class" << endl;
    }
};

int main() {
    BaseClass *bp = new DerivedClass;
    bp->call_disp();
    return 0;
}

출력 결과

Calling display() from base
Print function from the derived class

결과 분석

출력 결과에서 확인할 수 있듯이, 비가상 함수 내부에서 가상 함수가 호출되더라도 다형적 동작은 그대로 유지됩니다. 즉, call_disp() 자체는 가상 함수가 아니지만, 그 내부에서 호출된 display()는 가상 함수이므로 실행 시간(runtime)에 실제 객체의 타입을 기준으로 어떤 함수를 호출할지 결정됩니다.

이러한 동적 바인딩(dynamic binding)은 vptrvtable이라는 메커니즘을 통해 구현됩니다.

  • vtable(가상 함수 테이블): 클래스별로 하나씩 존재하는 함수 포인터들의 테이블입니다. 해당 클래스의 가상 함수들의 주소 정보를 담고 있습니다.
  • vptr(가상 포인터): 각 객체 인스턴스마다 유지되는 포인터로, 자신이 속한 클래스의 vtable을 가리킵니다.

객체가 생성될 때 vptr이 설정되고, 가상 함수가 호출되면 vptr을 통해 vtable을 참조하여 실제 오버라이딩된 함수의 주소를 찾아 실행합니다. 이 과정은 컴파일 타임이 아닌 런타임에 수행되기 때문에, 호출 경로가 비가상 함수 내부라 하더라도 올바른 가상 함수가 동적으로 바인딩되는 것입니다.

정리

  • 가상 함수가 아니면 포인터나 참조의 정적 타입을 기준으로 컴파일 타임에 함수가 결정됩니다(정적 바인딩).
  • 가상 함수는 런타임에 객체의 실제 타입을 기준으로 함수가 결정됩니다(동적 바인딩).
  • 이 동적 바인딩은 가상 함수가 직접 호출되었는지, 아니면 다른 멤버 함수 내부에서 호출되었는지와 무관하게 항상 적용됩니다.