C++ 가상 함수 | "Virtual Functions" 가이드

C++ 가상 함수 | "Virtual Functions" 가이드

이 글의 핵심

C++ 가상 함수에 대한 실전 가이드입니다.

가상 함수란?

이 글에서는 C++ 다형성의 핵심인 가상 함수가 어떤 문제를 해결하는지, 런타임에 올바른 오버라이드가 호출되는 이유를 단계적으로 설명합니다. 기본 클래스 포인터로 파생 클래스 동작을 호출하는 패턴과 override, 순수 가상 함수를 실무에 맞게 쓰는 감각을 익힐 수 있습니다.

런타임에 실제 객체 타입에 따라 호출되는 함수

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

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;
}

순수 가상 함수

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: 파일 시스템

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: 전략 패턴

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>("[email protected]"));
    cart.checkout();
}

예시 3: 로거

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

class Base {
public:
    virtual void func1() {}
    virtual void func2() {}
};

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

가상 소멸자

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: 가상 소멸자 누락

// ❌ 가상 소멸자 없음
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 누락

class Base {
public:
    virtual void func() {}
};

// ❌ 오타 (새 함수 생성)
class Derived : public Base {
public:
    void fucn() {}  // 오타!
};

// ✅ override 사용
class Derived : public Base {
public:
    void fucn() override {}  // 컴파일 에러
};

문제 3: 슬라이싱

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”

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

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

  • C++ 상속과 다형성 | “virtual 함수” 완벽 가이드
  • C++ Object Slicing | “객체 슬라이싱” 가이드
  • C++ VTable | “가상 함수 테이블” 가이드

관련 글

  • C++ 상속과 다형성 |
  • C++ 가상 소멸자 |
  • C++ Object Slicing |
  • C++ 클래스와 객체 |
  • C++ CRTP 패턴 |