Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

s-nova 님의 블로그

[C++] 다형성, virtual, override 본문

C++

[C++] 다형성, virtual, override

s-nova 2025. 7. 27. 23:10

C++에서 다형성$^{\text{Polymorphism}}$을 구현하는 핵심 메커니즘은 바로 가상 함수$^{\text{Virtual Function}}$입니다. 같은 이름의 함수가 객체의 실제 타입에 따라 다르게 동작하는 원리, 그리고 그 내부 구조인 vtable까지 차근차근 살펴보겠습니다.

 

 

1. 다형성이란?

다형성$^{\text{Polymorphism}}$이란 같은 인터페이스를 공유하지만, 실제 동작은 객체의 타입에 따라 달라지는 성질입니다.

예를 들어, Animal 클래스를 상속받은 DogCat이 있을 때, Speak() 함수 호출은 이름이 같지만 실제 동작은 각자 다르게 실행됩니다.

 

#include <iostream>
using namespace std;

// 부모 클래스
class Animal
{
public:
    virtual void Speak()  // 가상 함수
    {
        cout << "Some generic animal sound" << endl;
    }
};

// 자식 클래스: Dog
class Dog : public Animal
{
public:
    void Speak() override  // override로 재정의
    {
        cout << "Woof! Woof!" << endl;
    }
};

// 자식 클래스: Cat
class Cat : public Animal
{
public:
    void Speak() override
    {
        cout << "Meow~" << endl;
    }
};

int main()
{
    Animal* a1 = new Dog();  // Animal 포인터지만 Dog 객체 생성
    Animal* a2 = new Cat();  // Animal 포인터지만 Cat 객체 생성

    a1->Speak();  // 출력: Woof! Woof!
    a2->Speak();  // 출력: Meow~

    delete a1;
    delete a2;
    return 0;
}

 

 

2. virtual 키워드의 역할

virtual을 붙이면 런타임 다형성$^{\text{Runtime Polymorphism}}$이 활성화됩니다. 부모 클래스의 포인터나 참조를 통해 호출하더라도, 실제 객체 타입에 맞는 함수가 실행됩니다.

 

구분 virtual 없음 virtual 있음
바인딩 시점 컴파일 타임 $($정적 바인딩$)$ 런타임 $($동적 바인딩$)$
호출되는 함수 포인터 타입 기준 실제 객체 타입 기준
다형성 불가 가능

 

class Character
{
public:
    virtual void Attack()  // virtual 선언
    {
        std::cout << "기본 공격!\n";
    }
};

class Warrior : public Character
{
public:
    void Attack() override  // 자식 클래스에서 재정의
    {
        std::cout << "검으로 공격!\n";
    }
};

💡 팁: virtual은 부모 클래스에서, override는 자식 클래스에서 사용합니다. 둘 다 명시하면 가독성이 높아집니다.

 

 

3. 소멸자에 virtual이 필요한 이유

부모 포인터로 자식 객체를 가리키고 delete할 때, 소멸자가 virtual이 아니면 부모 소멸자만 호출됩니다. 자식 객체가 관리하는 자원 $($동적 메모리, 파일 핸들 등$)$이 해제되지 않아 메모리 누수$^{\text{Memory Leak}}$가 발생합니다.

따라서 상속 관계에서 기본 클래스의 소멸자는 반드시 virtual로 선언하는 것이 안전합니다.

 

상속 관계에서 소멸자가 호출되는 순서는 항상 자식 → 부모 순입니다. Derived 객체가 해제될 때, Derived의 소멸자가 먼저 실행된 뒤 Base의 소멸자가 실행됩니다.

 

✅ 올바른 예시 $($virtual 소멸자$)$

#include <iostream>
using namespace std;

class Base
{
public:
    virtual ~Base() { cout << "Base 소멸\n"; }
};

class Derived : public Base
{
public:
    ~Derived() { cout << "Derived 소멸\n"; }
};

int main()
{
    Base* obj = new Derived();
    delete obj;  // 자식 → 부모 순으로 소멸
    return 0;
}
Derived 소멸
Base 소멸

 

❌ 잘못된 예시 $($virtual 없음$)$

class Base
{
public:
    ~Base() { cout << "Base 소멸\n"; }  // virtual 없음
};

class Derived : public Base
{
public:
    ~Derived() { cout << "Derived 소멸\n"; }
};

// Base* obj = new Derived();
// delete obj;  →  Base 소멸만 출력! Derived 소멸자 미호출
Base 소멸

Derived 소멸자가 호출되지 않아, Derived가 관리하는 자원이 해제되지 않는 메모리 누수가 발생합니다.

 

 

4. C++11의 override 키워드

C++11 이전에는 자식 클래스가 부모의 가상 함수를 재정의할 때 시그니처를 잘못 써도 컴파일 에러가 나지 않았습니다. 실수로 함수 이름이나 매개변수 타입이 달라지면, 재정의가 아닌 새로운 함수가 만들어지는 버그가 발생했습니다.

C++11부터 도입된 override 키워드를 붙이면, 부모 함수와 시그니처가 정확히 일치하지 않을 때 컴파일 에러를 발생시켜 실수를 방지합니다.

 

class Warrior : public Character
{
public:
    void Attack() override  // 시그니처가 다르면 컴파일 에러 발생
    {
        std::cout << "검으로 공격!\n";
    }
};

📌 참고: C++11 이상 프로젝트에서는 가상 함수를 재정의할 때 항상 override를 붙이는 습관을 들이는 것이 좋습니다.

 

 

5. 순수 가상 함수와 추상 클래스

= 0을 붙이면 순수 가상 함수$^{\text{Pure Virtual Function}}$가 되고, 해당 클래스는 추상 클래스$^{\text{Abstract Class}}$가 됩니다.

추상 클래스는 객체를 직접 생성할 수 없고, 반드시 자식 클래스에서 해당 함수를 구현해야만 합니다. 게임 개발에서는 Update(), Render()처럼 모든 게임 오브젝트가 반드시 구현해야 하는 공통 인터페이스 정의에 자주 활용됩니다.

 

class Character
{
public:
    virtual void Attack() = 0;  // 순수 가상 함수 → 추상 클래스
    virtual void Update() = 0;
};

// Character c;  // 컴파일 에러: 추상 클래스는 인스턴스 생성 불가

class Warrior : public Character
{
public:
    void Attack() override  // 반드시 구현해야 함
    {
        std::cout << "검으로 공격!\n";
    }

    void Update() override
    {
        // 전사 업데이트 로직
    }
};

 

 

6. vtable - 런타임 다형성의 핵심 구조

컴파일러는 가상 함수가 포함된 클래스에 대해 vtable$^{\text{Virtual Function Table}}$을 생성합니다. 각 객체는 내부적으로 vptr$^{\text{Virtual Pointer}}$를 가지고 있으며, 이 포인터가 vtable을 가리킵니다. 함수 호출 시 vptr → vtable → 실제 함수 주소 순으로 탐색하여 올바른 함수를 실행합니다.

 

구성 요소 위치 역할
vtable 클래스당 하나 $($정적 영역$)$ 가상 함수 주소 목록
vptr 객체마다 하나 $($객체 내부$)$ 자신의 vtable을 가리키는 포인터

 

💡 게임 개발 관점: vtable을 이해하면 가상 함수 호출이 일반 함수 호출보다 약간의 오버헤드 $($포인터 역참조 1회$)$가 있음을 알 수 있습니다. 성능이 극도로 중요한 핫 패스에서는 가상 함수 호출 빈도를 고려하는 설계가 필요합니다.

 

 

7. 정리

키워드/개념 위치 역할
virtual 부모 클래스 다형성 활성화, 동적 바인딩
override 자식 클래스 재정의 실수 방지 $($C++11$)$
virtual 소멸자 부모 클래스 안전한 메모리 해제 필수
순수 가상 함수 추상 클래스 인터페이스 강제, 구현 강요
vtable / vptr 컴파일러 생성 런타임 다형성의 내부 구현 구조

 

'C++' 카테고리의 다른 글

[C++] 스레드, CPU 캐시  (0) 2025.09.18
[C++] lvalue, rvalue, Perfect forwarding  (0) 2025.09.14
[C++] 스마트 포인터  (0) 2025.09.08
[C++] 얕은 복사 vs 깊은 복사  (0) 2025.08.31
[C++] 매크로, constexpr, inline, template  (0) 2025.07.19