[2026] C++ 가상 함수 완벽 가이드 | 다형성과 vtable 동작 원리 (override/final)

[2026] C++ 가상 함수 완벽 가이드 | 다형성과 vtable 동작 원리 (override/final)

이 글의 핵심

C++ 가상 함수의 C++, Virtual, Functions, 가상 함수란?를 실전 예제와 함께 상세히 설명합니다.

가상 함수란?

이 글에서는 C++ 다형성의 핵심인 가상 함수가 어떤 문제를 해결하는지, 런타임에 올바른 오버라이드가 호출되는 이유를 단계적으로 설명합니다. 기본 클래스 포인터로 파생 클래스 동작을 호출하는 패턴과 override, 순수 가상 함수를 실무에 맞게 쓰는 감각을 익힐 수 있습니다. 런타임에 실제 객체 타입에 따라 호출되는 함수 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Animal {
public:
    virtual void speak() {
        cout << "Animal sound" << endl;
    }
};
class Dog : public Animal {
public:
    void speak() override {
        cout << "Woof!" << endl;
    }
};
class Cat : public Animal {
public:
    void speak() override {
        cout << "Meow!" << endl;
    }
};
int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();
    
    animal1->speak();  // Woof!
    animal2->speak();  // Meow!
    
    delete animal1;
    delete animal2;
}

virtual vs non-virtual

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Base {
public:
    void nonVirtual() {
        cout << "Base::nonVirtual" << endl;
    }
    
    virtual void virtualFunc() {
        cout << "Base::virtualFunc" << endl;
    }
};
class Derived : public Base {
public:
    void nonVirtual() {
        cout << "Derived::nonVirtual" << endl;
    }
    
    void virtualFunc() override {
        cout << "Derived::virtualFunc" << endl;
    }
};
int main() {
    Base* ptr = new Derived();
    
    ptr->nonVirtual();   // Base::nonVirtual (정적 바인딩)
    ptr->virtualFunc();  // Derived::virtualFunc (동적 바인딩)
    
    delete ptr;
}

순수 가상 함수

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Shape {
public:
    // 순수 가상 함수 (= 0)
    virtual double area() const = 0;
    virtual void draw() const = 0;
    
    virtual ~Shape() = default;
};
class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(double r) : radius(r) {}
    
    double area() const override {
        return 3.14159 * radius * radius;
    }
    
    void draw() const override {
        cout << "Drawing Circle" << endl;
    }
};
class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double area() const override {
        return width * height;
    }
    
    void draw() const override {
        cout << "Drawing Rectangle" << endl;
    }
};
int main() {
    // Shape shape;  // 에러: 추상 클래스
    
    vector<unique_ptr<Shape>> shapes;
    shapes.push_back(make_unique<Circle>(5.0));
    shapes.push_back(make_unique<Rectangle>(4.0, 6.0));
    
    for (const auto& shape : shapes) {
        shape->draw();
        cout << "Area: " << shape->area() << endl;
    }
}

실전 예시

예시 1: 파일 시스템

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class FileSystemNode {
protected:
    string name;
    
public:
    FileSystemNode(const string& n) : name(n) {}
    virtual ~FileSystemNode() = default;
    
    virtual void print(int indent = 0) const = 0;
    virtual size_t getSize() const = 0;
};
class File : public FileSystemNode {
private:
    size_t size;
    
public:
    File(const string& n, size_t s) : FileSystemNode(n), size(s) {}
    
    void print(int indent = 0) const override {
        cout << string(indent, ' ') << "- " << name 
             << " (" << size << " bytes)" << endl;
    }
    
    size_t getSize() const override {
        return size;
    }
};
class Directory : public FileSystemNode {
private:
    vector<unique_ptr<FileSystemNode>> children;
    
public:
    Directory(const string& n) : FileSystemNode(n) {}
    
    void add(unique_ptr<FileSystemNode> node) {
        children.push_back(move(node));
    }
    
    void print(int indent = 0) const override {
        cout << string(indent, ' ') << "+ " << name << "/" << endl;
        for (const auto& child : children) {
            child->print(indent + 2);
        }
    }
    
    size_t getSize() const override {
        size_t total = 0;
        for (const auto& child : children) {
            total += child->getSize();
        }
        return total;
    }
};
int main() {
    auto root = make_unique<Directory>("root");
    
    auto docs = make_unique<Directory>("docs");
    docs->add(make_unique<File>("readme.txt", 1024));
    docs->add(make_unique<File>("guide.pdf", 5120));
    
    root->add(move(docs));
    root->add(make_unique<File>("main.cpp", 2048));
    
    root->print();
    cout << "Total size: " << root->getSize() << " bytes" << endl;
}

예시 2: 전략 패턴

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class PaymentStrategy {
public:
    virtual ~PaymentStrategy() = default;
    virtual void pay(double amount) = 0;
};
class CreditCardPayment : public PaymentStrategy {
private:
    string cardNumber;
    
public:
    CreditCardPayment(const string& card) : cardNumber(card) {}
    
    void pay(double amount) override {
        cout << "카드 " << cardNumber << "로 " 
             << amount << "원 결제" << endl;
    }
};
class PayPalPayment : public PaymentStrategy {
private:
    string email;
    
public:
    PayPalPayment(const string& e) : email(e) {}
    
    void pay(double amount) override {
        cout << "PayPal " << email << "로 " 
             << amount << "원 결제" << endl;
    }
};
class ShoppingCart {
private:
    unique_ptr<PaymentStrategy> paymentStrategy;
    double total = 0;
    
public:
    void setPaymentStrategy(unique_ptr<PaymentStrategy> strategy) {
        paymentStrategy = move(strategy);
    }
    
    void addItem(double price) {
        total += price;
    }
    
    void checkout() {
        if (paymentStrategy) {
            paymentStrategy->pay(total);
            total = 0;
        }
    }
};
int main() {
    ShoppingCart cart;
    cart.addItem(10000);
    cart.addItem(20000);
    
    cart.setPaymentStrategy(make_unique<CreditCardPayment>("1234-5678"));
    cart.checkout();
    
    cart.addItem(15000);
    cart.setPaymentStrategy(make_unique<PayPalPayment>("user@example.com"));
    cart.checkout();
}

예시 3: 로거

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Logger {
public:
    virtual ~Logger() = default;
    virtual void log(const string& message) = 0;
};
class ConsoleLogger : public Logger {
public:
    void log(const string& message) override {
        cout << "[Console] " << message << endl;
    }
};
class FileLogger : public Logger {
private:
    string filename;
    
public:
    FileLogger(const string& file) : filename(file) {}
    
    void log(const string& message) override {
        ofstream ofs(filename, ios::app);
        ofs << "[File] " << message << endl;
    }
};
class Application {
private:
    unique_ptr<Logger> logger;
    
public:
    void setLogger(unique_ptr<Logger> l) {
        logger = move(l);
    }
    
    void run() {
        if (logger) {
            logger->log("Application started");
            // ...
            logger->log("Application finished");
        }
    }
};
int main() {
    Application app;
    
    app.setLogger(make_unique<ConsoleLogger>());
    app.run();
    
    app.setLogger(make_unique<FileLogger>("app.log"));
    app.run();
}

vtable과 vptr

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

class Base {
public:
    virtual void func1() {}
    virtual void func2() {}
};
// 내부적으로:
// Base 객체 = [vptr] + [멤버 변수들]
// vptr -> vtable (func1, func2 주소)

가상 소멸자

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Base {
public:
    virtual ~Base() {
        cout << "~Base()" << endl;
    }
};
class Derived : public Base {
private:
    int* data;
    
public:
    Derived() : data(new int[100]) {}
    
    ~Derived() {
        cout << "~Derived()" << endl;
        delete[] data;
    }
};
int main() {
    Base* ptr = new Derived();
    delete ptr;  // ~Derived() -> ~Base() 순서로 호출
}

자주 발생하는 문제

문제 1: 가상 소멸자 누락

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 가상 소멸자 없음
class Base {
public:
    ~Base() {
        cout << "~Base()" << endl;
    }
};
class Derived : public Base {
private:
    int* data;
    
public:
    Derived() : data(new int[100]) {}
    
    ~Derived() {
        cout << "~Derived()" << endl;
        delete[] data;  // 호출 안됨!
    }
};
Base* ptr = new Derived();
delete ptr;  // 메모리 누수
// ✅ 가상 소멸자
class Base {
public:
    virtual ~Base() {
        cout << "~Base()" << endl;
    }
};

문제 2: override 누락

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Base {
public:
    virtual void func() {}
};
// ❌ 오타 (새 함수 생성)
class Derived : public Base {
public:
    void fucn() {}  // 오타!
};
// ✅ override 사용
class Derived : public Base {
public:
    void fucn() override {}  // 컴파일 에러
};

문제 3: 슬라이싱

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Base {
public:
    virtual void func() {
        cout << "Base" << endl;
    }
};
class Derived : public Base {
public:
    void func() override {
        cout << "Derived" << endl;
    }
};
// ❌ 슬라이싱
Derived d;
Base b = d;  // 복사 (Derived 부분 잘림)
b.func();    // Base (가상 함수 동작 안함)
// ✅ 포인터/참조 사용
Base* ptr = &d;
ptr->func();  // Derived

FAQ

Q1: 가상 함수는 언제 사용하나요?

A:

  • 다형성이 필요할 때
  • 런타임에 타입 결정
  • 인터페이스 정의

Q2: 성능 오버헤드는?

A: vtable 조회로 약간의 오버헤드. 대부분 무시 가능.

Q3: 순수 가상 함수는?

A: = 0으로 선언. 추상 클래스 생성. 파생 클래스에서 구현 필수.

Q4: override 키워드는?

A: C++11 이상. 오버라이드 의도 명시. 오타 방지.

Q5: 가상 소멸자는 필수?

A: 다형성 사용 시 필수. 메모리 누수 방지.

Q6: 가상 함수 학습 리소스는?

A:

  • “Effective C++”
  • cppreference.com
  • “C++ Primer”

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3