[C++] 가상 함수(Virtual function)

3 분 소요

가상 함수의 필요성

C++은 상속을 지원하므로 아래 코드처럼 기본 클래스의 멤버 함수를 파생 클래스에서 재정의(override) 하여 사용할 수 있다.

#include <bits/stdc++.h>

class Base {
public:
    Base() = default;
    ~Base() = default;

    void print() {
        std::cout << "Base class\n";
    }
};

class Derived : public Base {
public:
    Derived() = default;
    ~Derived() = default;

    void print() {
        std::cout << "Derived class\n";
    }
};

int main() {
    Base b;
    Derived d;

    b.print();
    d.print();

    return 0;
}
Base class
Derived class

이러한 상속 관계에서 ‘DerivedBase를 상속한다.’라고 하며 is a 관계이므로 Derived is Base라고 할 수 있다. 따라서 아래와 같은 코드가 정상적으로 실행될 수 있고 이렇게 파생 클래스에서 기본 클래스로 캐스팅하는 것을 ‘업 캐스팅’(Upcasting)이라고 부른다.

int main() {
    Derived d;
    Base *bp = &d; // Upcasting
    bp->print();
    return 0;
}
Base class

실제 가리키고 있는 객체 타입은 Derived이지만 Base 타입의 포인터이기 때문에 Base class가 출력된다.

업 캐스팅과 반대로 기본 클래스에서 파생 클래스로 캐스팅 하는 것을 ‘다운 캐스팅’(Downcasting)이라고 부른다. 일반적으로 파생 클래스는 기본 클래스 보다 확장되어 있기 때문에 컴파일러에서 에러를 발생시킨다. 안전한 작동이 보장되는 경우에는 명시적 형변환을 하여 다운캐스팅을 할 수는 있다.

int main() {
    Base b;
    Derived *dp = static_cast<Derived*>(&b); // Downcasting
    dp->print();
    return 0;
}
Derived class

실제 가리키고 있는 객체 타입은 Base이지만 Derived 타입의 포인터이기 때문에 Derived class가 출력된다.


그래서 업케스팅을 활용하여 실제 가리키는 객체의 멤버 함수를 호출하고 싶지만 실행해보면 다음과 같은 결과가 나온다.

#include <bits/stdc++.h>

class Animal {
protected:
    std::string type;

public:
    void printType() {
        std::cout << "Animal class - No name" << '\n';
    }
};

class Dog : public Animal {
public:
    Dog() {
        type = "Dog";
    }
    
    void printType() {
        std::cout << type << '\n';
    }
};

class Cat : public Animal {
public:
    Cat() {
        type = "Cat";
    }

    void printType() {
        std::cout << type << '\n';
    }
};


int main() {
    std::vector<Animal*> animals;
    animals.push_back(new Dog());
    animals.push_back(new Dog());
    animals.push_back(new Cat());
    animals.push_back(new Cat());

    for (Animal* animal : animals) {
        animal->printType();
    }
    return 0;
}
Animal class - No name
Animal class - No name
Animal class - No name
Animal class - No name

“Dog”이나 “Cat”이 출력되는 것을 의도했겠지만 C++은 정적 바인딩을 기본으로 하고 있기 때문에 실제 포인터가 가리키고 있는 객체의 멤버 함수를 호출하는 것이 아니라 포인터 변수 타입에 따라서 호출하게 된다.

따라서 의도한 결과가 출력되도록 하기 위해서는 동적 바인딩을 위해 virtual 키워드를 사용하여 가상 함수로 생성해야 한다.

가상 함수 (Virtual function)

기본 클래스(base class)에서 virtual 키워드를 사용하여 가상 함수를 선언하면 파생 클래스에서 재정의 된 멤버 함수도 가상 함수가 된다.

가상 함수는 파생 클래스(derived class)에서 재정의 할 것으로 기대하는 멤버 함수인데, 이러한 가상 함수는 객체가 실제 가리키고 있는 동적 타입에 따라 호출될 함수가 결정된다.

순수 가상 함수 (Pure virtual function)

함수 몸체가 없는 가상함수이며 함수 원형(prototype)을 선언하고 0 값을 대입하면 순수 가상 함수가 된다.

순수 가상 함수는 가상 함수(virtual function)의 정의를 포함하고 있지만 동작이 정의되지 않았기 때문에 순수 가상 함수를 포함한 클래스를 상속받는 파생 클래스가 모든 순수 가상 함수를 반드시 재정의 해야한다.

그리고 순수 가상 함수는 정의된 동작이 없기 때문에 순수 가상 함수를 포함한 클래스의 객체는 생성할 수 없다. 순수 가상 함수를 최소 한 개 이상 포함하는 클래스를 추상 클래스(abstract class)라고 부른다.


일반적인 가상 함수는 작동될 로직을 정의할 수 있는데 이때는 파생 클래스에서 가상 함수를 재정의 하지 않아도 되며 가상 함수 몸체에 정의한 로직이 실행된다.

하지만 순수 가상 함수는 기본적으로 정의된 동작이 없기 때문에 파생 클래스에서 반드시 재정의 해야 한다.

다형성 (Polymorphism)

다형성은 기본 클래스 타입의 포인터나 lvalue reference가 파생 클래스의 객체를 가리키고 있을 때, 파생 클래스에서의 가상 함수 재정의에 의해 호출되는 함수가 달라지는 것을 의미한다. 다형성을 통해 하나의 멤버 함수를 호출했음에도 다른 형태의 작업을 수행할 수 있다.

C++ 컴파일러는 가상 함수가 존재하는 클래스에 대해 가상 함수 테이블(virtual function table) 을 생성하는데 가상 함수를 호출하였을 때는 가상 함수 테이블을 거쳐서 실제로 어떤 함수를 호출해야 하는지 결정한다. 따라서 아래와 같은 호출이 가능하며 가상 함수를 사용했기 때문에 실제 가리키고 있는 클래스의 멤버 함수가 호출된다.

#include <bits/stdc++.h>

class Animal { // Abstract class
protected:
    std::string type;

public:
    virtual void printType() = 0;
};

class Dog : public Animal {
public:
    Dog() {
        type = "Dog";
    }
    
    void printType() override {
        std::cout << type << '\n';
    }
};

class Cat : public Animal {
public:
    Cat() {
        type = "Cat";
    }

    void printType() override {
        std::cout << type << '\n';
    }
};


int main() {
    std::vector<Animal*> animals;
    animals.push_back(new Dog());
    animals.push_back(new Dog());
    animals.push_back(new Cat());
    animals.push_back(new Cat());

    for (Animal* animal : animals) {
        animal->printType();
    }
    return 0;
}
Dog
Dog
Cat
Cat