Programming/C, C++

[C++] 가상 함수에서 조금 알아두면 좋은 간단한 읽을거리(vtable, pure virtual function 포함)

변화의 물결1 2025. 4. 5. 00:05

 

 

안녕하세요.

 

 C++ 가상 함수에 대해서 조금 알아보려고 합니다. 뭐 virtual 만 붙이면 되는 거 아니야?라고 할 수 있지만, 간단한 원리, vtable 구조, 장단점 등을 조금 알아보겠습니다.

 

 C++에서 가상 함수(virtual function)는 객체 지향 프로그래밍의 다형성(polymorphism)을 구현하는 핵심 개념입니다. 이를 통해 기본 클래스 포인터 또는 참조를 사용하더라도, 런타임에 실제 객체의 타입에 따라 적절한 함수가 호출되도록 만들 수 있습니다.

 


 

 

1. 가상 함수란? — 런타임 다형성의 시작

 

 가상 함수는 기본 클래스에서 virtual 키워드로 선언되며, 파생 클래스에서 재정의(override)할 수 있습니다.

 가상 함수 호출은 정적 바인딩(static binding)이 아닌, 동적 바인딩(dynamic binding)을 통해 수행됩니다.

 

 아래 예제를 통해 간단한 가상함수 호출을 볼 수 있습니다. 마지막 객체 타입을 선언한 형태의 함수가 호출되는 것을 알 수 있습니다.

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void makeSound() {
        cout << "Animal sound" << endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        cout << "Woof!" << endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        cout << "Meow!" << endl;
    }
};

int main() {
    Animal* dog = new Dog();
    Animal* cat = new Cat();

    dog->makeSound(); // Woof!
    cat->makeSound(); // Meow!

    delete dog;
    delete cat;
}

 

override 키워드는 C++11에서 도입된 것으로, 오버라이딩이 정확히 이루어졌는지 컴파일 타임에 검증할 수 있습니다.

 

 

 

2. vtable과 ptr: 가상 함수의 내부 메커니즘

 

 C++에서 가상 함수는 가상 함수 테이블(vtable)이라는 구조를 통해 구현됩니다.

 위의 소스의 구조를 요약하자면, 아래와 같을 것입니다.

 

 vtable : 클래스당 하나 존재하는 테이블로, 해당 클래스의 모든 가상 함수 주소를 보관

 ptr     : 각 객체가 내부적으로 가지는 포인터로, 자신이 속한 클래스의 vtable을 가리킴

 

 

  파생 클래스가 기본 클래스의 가상 함수를 오버라이드할 경우, vtable에는 재정의된 함수 주소로 갱신됩니다.

 

 

3. 가상 함수의 장점과 단점

 

1) 장점

 

다형성 제공: 객체 타입에 따라 런타임에 맞는 함수 호출 가능

유지보수성 향상: 인터페이스 기반의 확장이 쉬움

코드 재사용성 증가: 공통 기반 클래스를 통한 다양한 기능 구현

 

 

2) 단점

 

성능 오버헤드: 간접 호출을 위한 포인터 역참조(vtable lookup)로 약간의 비용 발생

인라인 최적화 제한: 함수 주소가 런타임에 결정되므로 인라인화가 어려움

메모리 오버헤드: vptr을 위한 추가 공간 필요

 

 

4. 성능 고려사항 및 최적화 팁

 

1) 성능 저하의 원인

 

가상 함수 호출은 함수 포인터 간접 호출 방식이기 때문에, 일반 함수보다 약간 느릴 수 있습니다.

CPU의 분기 예측 실패 가능성이 증가하고, 컴파일러 인라인 최적화가 제한됩니다.

 

2) 최적화 팁

 

 불필요한 가상 함수 사용을 피하고, 상속보다 조합(composition)을 우선 고려

 성능 민감한 코드에서는 final, override, noexcept 등을 통해 최적화 유도

 컴파일러의 LTO(Link Time Optimization) 기능을 적극 활용

 

 

5. 추가 내용(순수 가상 함수, pure virtual function)

 

1) 추상 클래스란?

 

 추상 클래스(abstract class)는 하나 이상의 순수 가상 함수(pure virtual function)를 포함하는 클래스입니다.

 C++에서는 virtual 키워드와 함께 함수 선언을 = 0;으로 끝내면 해당 함수가 순수 가상 함수가 됩니다.

 

추상 클래스의 특징:

 직접 인스턴스화할 수 없습니다.

 반드시 파생 클래스에서 순수 가상 함수를 오버라이딩(재정의) 해야 합니다.

 공통된 인터페이스를 제공하는 역할을 합니다.

 

2) 순수 가상 함수란?

 

 순수 가상 함수는 함수의 선언만 제공하고, 구현을 제공하지 않는 함수입니다.

 이를 통해 C++에서도 인터페이스(interface) 스타일의 설계를 구현할 수 있습니다.

 

class Shape {
public:
    virtual void draw() = 0;  // 순수 가상 함수 → Shape는 추상 클래스가 됨
};

 

 위의 코드에서 draw()는 순수 가상 함수이므로 Shape는 추상 클래스가 됩니다.

 따라서, Shape의 객체를 직접 생성할 수 없습니다.

 

 

3) 순수 가상 함수의 역할

 

(1) 강제적인 오버라이딩 요구

 순수 가상 함수를 선언하면 파생 클래스에서 반드시 이를 구현해야 하므로, 실수로 구현을 빼먹는 것을 방지할 수 있습니다.

 

(2) 인터페이스 역할 제공

 추상 클래스는 여러 파생 클래스에서 공통적으로 가져야 할 기본 동작을 정의하는 역할을 합니다.

 예를 들어, Shape 클래스에서 draw()를 순수 가상 함수로 선언하면, 모든 도형 클래스(Circle, Rectangle 등)는 draw()를 반드시 구현해야 합니다.

 

(3) 다형성(polymorphism) 활용

 기본 클래스 포인터를 사용하여 다양한 파생 클래스 객체를 동적으로 관리할 수 있습니다.

 컴파일 타임이 아닌 런타임에 적절한 함수가 호출되도록 보장됩니다.

 

 

4) 순수 가상 함수 예제 코드:

 

 Shape draw()라는 순수 가상 함수를 가지므로 추상 클래스가 됩니다.

 Shape 객체는 직접 생성할 없으며, 반드시 이를 상속받는 Circle Rectangle에서 draw() 구현해야 합니다.

 Shape* 포인터를 사용하여 다형성을 활용할 있습니다.

 

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() = 0; // 순수 가상 함수 → 추상 클래스가 됨
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Draw Circle like this ㅇ " << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "Draw Rectangle like this ㅁ " << endl;
    }
};

int main() {
    // Shape s;  // 오류! 추상 클래스는 인스턴스화할 수 없음
    Shape* circle = new Circle();
    Shape* rectangle = new Rectangle();

    circle->draw(); // 원을 그립니다.
    rectangle->draw(); // 사각형을 그립니다.

    delete circle;
    delete rectangle;
}

   

 

감사합니다.

 

 

반응형