메뉴 닫기

C++ 가상 함수(virtual function) 완전 정복: 개념부터 예제까지 한 번에!


C++ 가상 함수(virtual function) 완전 정복: 개념부터 예제까지 한 번에!

🧠 다형성을 가능하게 하는 핵심 개념, 가상 함수 제대로 이해하고 써보세요!

C++을 공부하다 보면 꼭 만나게 되는 개념 중 하나가 가상 함수(virtual function)입니다.
클래스, 상속, 다형성을 이해하기 위해 반드시 알아야 하는 핵심 키워드이자, 실전 개발에서도 코드의 유연성과 확장성을 높이는 데 크게 기여하는 기능이죠.
하지만 처음 접하는 분들에게는 조금 낯설고 복잡하게 느껴질 수 있습니다.
그래서 이번 글에서는 가상 함수가 무엇인지, 왜 필요한지, 어떻게 동작하는지를 아주 쉽게 예제와 함께 설명드릴게요.

가상 함수는 부모 클래스에서 선언하고, 자식 클래스에서 재정의(override)할 수 있는 멤버 함수입니다.
실제 실행 시점에는 객체의 타입에 따라 적절한 함수가 호출되도록 해 주며, 이것이 바로 다형성(polymorphism)의 핵심 메커니즘입니다.
이번 포스트에서는 C++의 가상 함수에 대한 개념부터 문법, 실전 예제, 자주 하는 실수까지 모두 다뤄보겠습니다.







🔗 가상 함수의 정의와 기본 구조

C++에서 가상 함수(virtual function)는 부모 클래스에서 선언되며, 자식 클래스에서 재정의할 수 있는 함수입니다.
이 함수는 프로그램 실행 중, 즉 런타임에 실제 객체의 타입에 따라 어떤 함수가 호출될지를 결정하게 해 줍니다.
이러한 기능은 다형성을 구현하는 핵심 요소이기도 합니다.

일반적으로 함수 호출은 컴파일 시점에 결정되지만, virtual 키워드가 붙은 함수는 동적 바인딩(dynamic binding)을 통해 런타임 시점에 연결됩니다.
이 덕분에 부모 클래스 포인터로 자식 객체를 가리키더라도 자식 클래스의 오버라이딩된 함수가 호출됩니다.

CODE BLOCK
class Animal {
public:
    virtual void speak() {
        cout << "동물이 소리를 냅니다." << endl;
    }
};

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

위 예제에서 speak() 함수는 Animal 클래스에서 virtual로 선언되었고, Dog 클래스에서 이를 재정의(override)했습니다.
이제 Animal* 타입의 포인터가 Dog 객체를 가리킬 때도 Dog::speak()가 호출됩니다.

💎 핵심 포인트:
가상 함수는 다형성을 구현하기 위한 필수 구성요소입니다. 부모 클래스에서 virtual로 선언하고, 자식 클래스에서 필요 시 재정의하면 됩니다.

이 구조를 제대로 이해하면 복잡한 프로그램에서도 객체지향 설계를 훨씬 유연하고 확장 가능하게 만들 수 있습니다.


🛠️ virtual 키워드의 역할

C++에서 virtual 키워드는 가상 함수를 정의할 때 반드시 사용되는 예약어입니다.
이 키워드가 붙은 함수는 정적 바인딩이 아닌 동적 바인딩을 통해 실행 시점에 어떤 함수가 호출될지 결정됩니다.
즉, 부모 클래스 포인터나 참조가 자식 객체를 가리킬 때 자식 클래스에서 재정의한 함수가 호출될 수 있도록 해 주는 역할을 하죠.

만약 virtual 키워드가 없다면, 포인터가 가리키는 타입과 상관없이 컴파일 타임 기준으로 부모 클래스의 함수가 호출됩니다.
즉, 다형성이 작동하지 않게 되죠.

CODE BLOCK
Animal* pet = new Dog();
pet->speak();  // virtual이면 "멍멍!", 없으면 "동물이 소리를 냅니다."

이처럼 같은 함수 호출이라도 virtual 키워드의 유무에 따라 동작이 완전히 달라질 수 있습니다.
실전 코드에서도 virtual 키워드를 빠뜨리는 경우 의도치 않은 결과가 나올 수 있으니 주의해야 합니다.

⚠️ 주의: virtual 함수가 선언되지 않으면 자식 클래스에서 아무리 함수 내용을 재정의해도, 부모 포인터로 접근할 경우 부모의 함수가 실행됩니다.

따라서 가상 함수가 필요한 모든 상황에서 virtual 키워드는 잊지 말고 선언해 주세요.
이 한 줄이 객체지향 설계 전체를 바꿀 수 있습니다.







⚙️ 오버라이딩과 실행 시간 동작

가상 함수의 가장 중요한 특징은 실행 시간(runtime)에 실제 객체 타입에 따라 동작이 결정된다는 것입니다.
이를 통해 우리는 하나의 인터페이스로 다양한 객체의 동작을 실행할 수 있습니다.
이 기능은 자식 클래스에서 부모 클래스의 함수를 오버라이딩(override)할 때 발휘됩니다.

오버라이딩은 부모 클래스에 있는 virtual 함수의 시그니처와 동일하게 자식 클래스에서 재정의하는 것을 말합니다.
이때 부모 클래스 포인터로 자식 객체를 가리켜도, 자식 클래스의 함수가 호출됩니다.

CODE BLOCK
Animal* animal = new Dog();
animal->speak();  // "멍멍!" 출력됨

이처럼 동적 바인딩이 적용되면 컴파일 타임이 아닌 런타임에 함수 호출이 결정되므로,
하나의 인터페이스로 다양한 객체의 동작을 동일하게 다룰 수 있게 됩니다.
이것이 바로 C++의 강력한 다형성입니다.

💎 핵심 포인트:
오버라이딩은 virtual 함수가 선언되어 있을 때만 의미가 있습니다. override 키워드를 함께 사용하면 실수를 줄일 수 있습니다.

또한, 오버라이딩 시에는 함수의 시그니처가 정확히 일치해야 하며, override 키워드를 붙이면 컴파일러가 이를 체크해 주기 때문에 적극적으로 사용하는 것이 좋습니다.


🔌 가상 함수 테이블(VTable)의 개념

C++에서 가상 함수가 어떻게 실행 시점에 적절한 함수를 호출할 수 있는지는 내부적으로 VTable(Virtual Table)이라는 구조 덕분입니다.
VTable은 가상 함수 포인터를 저장하는 테이블로, 클래스마다 하나씩 생성되며 각 가상 함수에 대한 실제 주소를 담고 있습니다.

객체가 생성되면 해당 객체는 자신의 클래스에 대응하는 VTable을 가리키는 가상 테이블 포인터(vptr)를 내부적으로 포함하게 됩니다.
함수가 호출될 때 이 vptr을 통해 VTable에 접근하고, 적절한 함수 주소를 찾아 실행하는 구조입니다.

  • 📌VTable은 클래스 단위로 존재합니다
  • 📌객체는 vptr을 통해 VTable을 참조합니다
  • 📌가상 함수가 호출될 때 VTable을 통해 동작합니다
  • 📌자식 클래스가 함수를 오버라이딩하면 VTable 항목이 갱신됩니다

이 메커니즘 덕분에 C++은 실행 시간에 객체의 실제 타입에 따라 정확한 함수를 호출할 수 있습니다.
이는 성능적으로도 효율적이며, 객체지향 원칙 중 하나인 다형성을 안정적으로 지원합니다.

💡 참고로 VTable은 컴파일러가 내부적으로 자동 생성하는 구조이기 때문에, 직접 눈에 보이지는 않지만 디버깅 도구나 저수준 디스어셈블리로 확인할 수 있습니다.

VTable 개념을 이해하면 C++ 가상 함수의 작동 방식뿐만 아니라, 다형성이 가능한 이유까지도 명확히 파악할 수 있습니다.







💡 실무에서 자주 하는 실수와 팁

가상 함수를 사용할 때 많은 개발자들이 비슷한 실수를 반복하곤 합니다.
특히 C++ 초보자나 객체지향 개념에 익숙하지 않은 분들은 다음과 같은 부분에서 어려움을 겪습니다.
여기서는 실무에서 자주 발생하는 실수와 함께, 이를 방지하기 위한 팁을 정리해보았습니다.

  • 부모 클래스에 virtual 키워드 누락 → 다형성이 작동하지 않음
  • 자식 클래스에서 함수 시그니처 불일치 → 오버라이딩이 되지 않고 부모 함수 호출
  • virtual 소멸자 누락 → 자식 소멸자가 호출되지 않아 메모리 누수 가능
  • 모든 가상 함수는 override 키워드로 명시 → 실수 방지
  • 순수 가상 함수 사용 시 반드시 구현 → 추상 클래스 활용

위와 같은 실수는 간단해 보이지만, 실전 코드에서는 버그나 메모리 누수로 이어질 수 있습니다.
따라서 가상 함수를 사용할 때는 항상 꼼꼼한 선언과 정확한 override가 중요합니다.

💡 팁: 소멸자는 항상 virtual로 선언하는 습관을 들이세요. 그래야 자식 클래스의 자원이 안전하게 해제됩니다.

작은 차이가 큰 결과를 만드는 만큼, 위 항목들을 숙지하고 실무에 적용해보세요.
그렇다면 가상 함수에 대한 이해도와 활용도 모두 한층 높아질 것입니다.


자주 묻는 질문 (FAQ)

가상 함수는 꼭 부모 클래스에서 선언해야 하나요?
네, 가상 함수는 부모 클래스에서 선언되어야 자식 클래스에서 오버라이딩이 가능하고, 다형성도 제대로 작동합니다.
override 키워드를 생략해도 되나요?
C++11 이전에는 생략이 일반적이었지만, 지금은 override를 명시하는 것이 좋습니다. 컴파일러가 오버라이딩 실수를 잡아주기 때문입니다.
가상 함수는 생성자에서 호출되나요?
아니요. 생성자 내부에서는 가상 함수가 호출되더라도 부모 클래스의 함수가 실행됩니다. 객체가 완전히 생성되지 않았기 때문입니다.
소멸자도 virtual로 선언해야 하나요?
반드시 virtual로 선언하는 것이 좋습니다. 그렇지 않으면 자식 클래스의 소멸자가 호출되지 않아 메모리 누수가 발생할 수 있습니다.
virtual 키워드 없이도 함수가 오버라이딩되나요?
함수 자체는 재정의할 수 있지만, 부모 포인터로 접근 시 자식 함수가 호출되지는 않습니다. 다형성이 작동하려면 virtual이 필요합니다.
virtual 함수는 성능에 영향을 주나요?
아주 약간의 오버헤드가 있지만, 대부분의 경우 무시해도 될 정도이며 설계 유연성이 더 큰 이점을 줍니다.
가상 함수는 꼭 public이어야 하나요?
꼭 그렇진 않지만, 일반적으로 외부에서 호출될 필요가 있는 경우 public으로 선언합니다. 필요에 따라 protected나 private도 가능합니다.
가상 함수가 많은 클래스는 무겁지 않나요?
가상 함수가 많다고 해서 클래스가 무거워지는 것은 아닙니다. 다만 설계 시 너무 많은 책임을 한 클래스에 몰아주지 않도록 주의해야 합니다.



🧭 가상 함수 이해는 객체지향 설계의 첫걸음

C++의 가상 함수(virtual function)는 단순한 문법 이상의 의미를 가지고 있습니다.
코드를 설계하는 관점에서 보면, 가상 함수는 다형성을 실현하게 해주는 열쇠입니다.
부모 클래스 포인터로 자식 클래스의 행동을 제어할 수 있다는 것은, 복잡한 시스템에서 공통된 인터페이스로 유연하게 동작할 수 있게 해준다는 의미이기도 합니다.

이번 글에서는 가상 함수의 정의와 작동 원리는 물론, 오버라이딩, VTable 구조, 실무 팁과 자주 묻는 질문까지 전반적인 내용을 다뤄보았습니다.
초보자부터 중급자까지 C++의 객체지향 설계에 자신감을 갖게 될 수 있는 핵심 개념이니, 실제 코드를 작성하며 반복적으로 연습해보시길 추천드립니다.

가상 함수의 동작 방식을 명확히 이해하면, 추상 클래스나 인터페이스, 다형성 등 이후의 고급 개념도 훨씬 쉽게 받아들일 수 있습니다.
꼼꼼히 읽어주셔서 감사합니다 😊


🏷️ 관련 태그:C++가상함수, virtualfunction, 다형성, C++오버라이딩, VTable, 동적바인딩, 객체지향설계, C++초보자, 추상클래스, C++공부팁