C++

[C++] 가상 함수(virtual)

seungwoo-dev 2026. 5. 20. 13:40

안녕하세요! 오늘은 C++ 객체 지향 프로그래밍의 핵심이자 다형성을 구현하는 치트키, virtual 키워드에 대해 알아보겠습니다. 면접 단골 질문이기도 하고, 실무에서도 정말 자주 쓰이지만 처음 접하면 헷갈리기 쉬운 개념인데요. 가장 직관적인 예제와 함께 쉽게 풀어보겠습니다.

 

1. virtual 키워드는 왜 필요할까?

한 줄로 요약하면, virtual은 "상황에 따라 유연하게 대처하기 위한 장치"입니다.

부모 클래스의 포인터나 참조로 자식 객체를 가리킬 때, virtual이 없다면 프로그램은 실제 객체가 무엇인지 상관하지 않고 '포인터의 타입'만 보고 함수를 실행해 버립니다.

백문이 불여일견, 코드로 어떤 문제가 발생하는지 바로 확인해 보시죠.

 

  • ❌ virtual이 없을 때 발생하는 문제

C++

#include <iostream>

class Animal {
public:
    void Speak() {
        std::cout << "동물이 소리를 냅니다.\n";
    }
};

class Dog : public Animal {
public:
    // 부모의 함수를 재정의(Override)
    void Speak() {
        std::cout << "멍멍!\n";
    }
};

int main() {
    // 부모 타입의 포인터로 자식 객체를 가리킴
    Animal* myAnimal = new Dog();
    
    // 과연 어떤 소리가 날까요?
    myAnimal->Speak(); 

    delete myAnimal;
    return 0;
}

 

실행 결과:

Plaintext
동물이 소리를 냅니다.

컴퓨터의 시선: "음, myAnimal 변수의 타입이 Animal*이네? 그럼 Animal 클래스에 있는 Speak()를 실행해야지!"

실제 알맹이는 Dog 객체인데도 불구하고, 겉모습(타입)만 보고 부모의 함수를 호출하는 불상사가 발생합니다. 개가 사람 말을 하는 상황인 거죠.

 

 

2. virtual을 사용한 해결 (가상 함수)

이 문제를 해결하기 위해 부모 클래스의 함수 앞에 virtual 키워드를 붙여줍니다. 이를 가상 함수(Virtual Function)라고 합니다.

 

⭕ virtual을 적용한 코드

C++
#include <iostream>

class Animal {
public:
    // virtual 키워드 추가!
    virtual void Speak() {
        std::cout << "동물이 소리를 냅니다.\n";
    }
    
    // 실무에서는 소멸자에도 반드시 virtual을 붙여야 합니다 (아래에서 후술)
    virtual ~Animal() {} 
};

class Dog : public Animal {
public:
    // 부모의 가상 함수를 재정의 (C++11부터는 override를 붙여주는 것이 안전합니다)
    void Speak() override {
        std::cout << "멍멍!\n";
    }
};

int main() {
    Animal* myAnimal = new Dog();
    
    // 이제 똑똑하게 자식의 함수를 찾아갑니다!
    myAnimal->Speak(); 

    delete myAnimal;
    return 0;
}

실행 결과:

Plaintext
 
멍멍!

virtual을 붙이면 프로그램은 컴파일 시점이 아니라, 실행 시점(Runtime)에 실제 객체가 무엇인지 확인하고 그에 맞는 함수를 호출합니다. 이를 동적 바인딩(Dynamic Binding)이라고 부릅니다.

 

3. 원리가 무엇인가요? (vtable 맛보기)

컴퓨터가 실행 중에 실제 객체의 함수를 어떻게 찾는지 궁금하실 텐데요. 내부적으로는 가상 함수 테이블(vtable)이라는 일종의 '지도'를 이용합니다.

  1. virtual 함수가 포함된 클래스는 컴파일될 때 자신만의 vtable(가상 함수 주소록)을 만듭니다.
  2. 객체가 생성될 때, 그 객체 내부에 vtable을 가리키는 포인터(vptr)가 몰래 생성됩니다.
  3. myAnimal->Speak()를 호출하면, 컴퓨터는 이 vptr 유도등을 따라가서 "아, 실제로는 Dog 클래스의 Speak() 주소가 적혀있네!" 하고 정확하게 찾아갑니다.

 

4. 🔥 실무 필수: 가상 소멸자 (Virtual Destructor)

virtual을 공부할 때 가장 중요한 실무 팁입니다. 상속 관계를 설계할 때 부모 클래스의 소멸자에는 무조건 virtual을 붙여야 합니다.

만약 붙이지 않으면 어떤 대참사가 일어나는지 보겠습니다.

C++
 
class Parent {
public:
    ~Parent() { std::cout << "부모 소멸자 호출\n"; }
};

class Child : public Parent {
private:
    int* data;
public:
    Child() { data = new int[100]; } // 동적 할당
    ~Child() { 
        delete[] data; 
        std::cout << "자식 소멸자(메모리 해제) 호출\n"; 
    }
};

이 상태에서 Parent* p = new Child(); delete p;를 하면 어떻게 될까요? virtual이 없기 때문에 부모의 소멸자만 호출되고 자식의 소멸자는 패스됩니다. 즉, Child에서 동적 할당한 data 변수가 해제되지 않아 메모리 누수(Memory Leak)가 발생합니다.

황금 규칙: 상속을 고려한 부모 클래스를 만든다면, 소멸자 앞에 꼭 virtual을 붙여주세요!


5. 요약 및 정리

  • virtual 왜 쓰나요? 부모 포인터로 자식 객체를 가리킬 때, 실제 알맹이(자식)의 재정의된 함수가 실행되도록 만들기 위해서입니다.
  • 언제 결정되나요? 프로그램이 실행되는 중(Runtime)에 동적으로 결정됩니다.
  • 이것만은 꼭! 자식 클래스에서 재정의할 때는 override 키워드를 붙여 실수를 방지하고, 부모 클래스의 소멸자에는 반드시 virtual을 붙입시다.

궁금한 점이나 의견이 있으시다면 언제든 댓글로 남겨주세요. 도움되셨다면 공감 부탁드립니다! 😊