Skip to content

16. OOP 다형성(OOP Polymorphism)


1. 키워드

  • OOP 다형성(OOP Polymorphism)
  • 가상 함수(Virtual Function)와 virtual 키워드
  • 동적 바인딩(Dynamic Binding) 또는 지연 바인딩(Late Binding)
  • 정적 바인딩(Static Binding) 또는 초기 바인딩(Early Binding)
  • 가상 함수 테이블(Virtual Function Table)
  • 가상 소멸자(Virtual Destructor)
  • 순수 가상 함수(Pure Virtual Function)와 추상 클래스(Abstract Class)


2. 가상 함수

  • C++에서 가상 함수는 파생 클래스에서 재정의할 것으로 기대하는 멤버 함수를 의미한다.
  • 이러한 가상 함수는 자신을 호출하는 객체의 동적 타입에 따라 실제 호출할 함수가 결정된다.


  • C++에서 가상 함수는 virtual 키워드를 사용하여 선언한다.


virtual 멤버함수의원형;


  • 기초 클래스에서 virtual 키워드를 사용해 가상 함수를 선언하면, 파생 클래스에서 재정의된 멤버 함수도 자동으로 가상 함수가 된다.
  • 참고로 파생 클래스의 멤버 함수 쪽에도 virtual 키워드를 사용하여 가상 함수라는 것을 명확히 하는 것이 좋다.


1) 동적 바인딩

  • C++ 컴파일러는 함수를 호출할 때, 어느 블록에 있는 함수를 호출해야 하고, 해당 함수가 저장된 정확한 메모리 위치까지도 알아야 한다.
  • 이처럼 함수를 호출하는 코드에서 어느 블록에 있는 함수를 실행하라는 의미로 해석하는 것을 바인딩이라고 한다.
  • 하지만 C++에서는 함수가 오버로딩될 수 있으므로 이 작업이 조금 복잡해진다.


  • 대부분 함수를 호출하는 코드는 컴파일 타임에 고정된 메모리 주소로 변환된다.
  • 이것을 정적 바인딩 또는 초기 바인딩이라고 한다.
  • C++에서는 가상 함수가 아닌 멤버 함수는 모두 이러한 정적 바인딩을 하게 된다.


  • 하지만 가상 함수의 호출은 컴파일러가 어떤 함수를 호출해야 하는지 미리 알 수 없다.
  • 왜냐하면, 가상 함수는 프로그램이 실행될 때 객체를 결정하므로, 컴파일 타임에 해당 객체를 특정할 수 없기 때문이다.
  • 따라서 가상 함수의 경우에는 런 타임에 올바른 함수가 실행될 수 있도록 해야 한다.
  • 이것을 동적 바인딩 또는 지연 바인딩이라고 한다.


  • 하지만 가상 함수도 결합하는 타입이 분명할 때에는 일반 함수와 같이 정적 바인딩을 한다.
  • 이러한 가상 함수는 기초 클래스 타입의 포인터나 참조를 통하여 호출될 때만 동적 바인딩을 하게 된다.


  • 다음의 코드는 가상 함수를 사용한 동적 바인딩을 간략하게 표현한 것이다.


#include <iostream>
using namespace std;

class A {
   public:
    virtual void Print() { cout << "A 클래스의 Print() 함수" << endl; }
};

class B : public A {
   public:
    virtual void Print() { cout << "B 클래스의 Print() 함수" << endl; }
};

int main(void) {
    A *ptr;
    A obj_a;
    B obj_b;

    ptr = &obj_a;
    ptr->Print();
    ptr = &obj_b;
    ptr->Print();

    return 0;
}

// A 클래스의 Print() 함수
// B 클래스의 Print() 함수


2) 가상 함수 테이블

  • C++에서는 가상 함수의 정의와 동작 방식만을 규정하고 있으며, 그에 따른 구현은 컴파일러마다 다르다.
  • 하지만 컴파일러가 가상 함수를 다루는 가장 일반적인 방식은 가상 함수 테이블을 이용하는 것이다.


  • C++ 컴파일러는 각각의 객체마다 가상 함수 테이블을 가리키는 포인터를 저장하기 위한 숨겨진 멤버를 하나씩 추가한다.
  • 이와 함께 가상 함수를 단 하나라도 가지는 클래스에 대해서 가상 함수 테이블을 작성한다.
  • 이렇게 작성된 가상 함수 테이블에는 해당 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장되게 된다.


  • 가상 함수를 호출하면, C++ 프로그램은 가상 함수 테이블에 접근하여 자신이 필요한 함수의 주소를 찾아 호출하게 된다.
  • 가상 함수를 사용하면 이처럼 함수의 호출 과정이 복잡해지므로, 메모리와 실행 속도 측면에서 약간의 부담을 가지게 된다.
  • 따라서 C++에서 기본 바인딩은 정적 바인딩이며, 필요한 경우에만 가상 함수로 선언하도록 하고 있다.
  • 하지만 파생 클래스가 재정의할 가능성이 있는 함수는 모두 가상 함수로 선언하는 편이 좋다.


3) 가상 소멸자

  • C++에서 기초 클래스의 소멸자는 반드시 가상으로 선언해야 한다.


  • 우선 앞서 살펴본 Person 클래스와 파생 클래스인 Student 클래스가 있다고 가정한다.


Person *hong = new Student;

...

delete hong;


  • 위의 코드에서 Person 클래스는 Student 클래스의 기초 클래스이므로, hong이라는 Student 객체가 동적으로 할당된다.
  • 하지만 마지막 구문의 delete 키워드는 ~Student() 소멸자를 호출하지 않고, ~Person() 소멸자를 호출할 것이다.
  • 그러므로 Student 객체에 동적으로 할당된 메모리는 정상적으로 할당된 메모리는 정상적으로 해제되지 않을 것이다.


  • 하지만 Person 클래스의 소멸자를 가상으로 선언한다면, 위의 구문은 정상적으로 ~Student() 소멸자를 호출할 것이다.
  • 따라서 기초 클래스는 명시적으로 소멸자를 선언할 필요가 없더라도, 아무 일도 하지 않는 가상 소멸자를 선언해야 한다.


3. 추상 클래스

1) 순수 가상 함수

  • C++에서 가상 함수는 파생 클래스에서 재정의할 것으로 기대하는 멤버 함수를 의미한다.
  • 따라서 가상 함수는 반드시 재정의해야만 하는 함수가 아닌, 재정의가 가능한 함수를 가리킨다.


  • 이와는 달리 순수 가상 함수란 파생 클래스에서 반드시 재정의해야 하는 멤버 함수를 의미한다.
  • 이러한 순수 가상 함수는 일반적으로 함수의 동작을 정의하는 본체를 가지고 있지 않다.
  • 따라서 파생 클래스에서 재정의하지 않으면 사용할 수 없다.


  • C++에서 순수 가상 함수는 다음과 같은 문법으로 선언한다.


virtual 멤버함수의원형 = 0;


  • 위와 같이 함수만 있고 본체가 없다는 의미로 함수 선언부 끝에 = 0을 추가한다.


2) 추상 클래스

  • C++에서는 하나 이상의 순수 가상 함수를 포함하는 클래스를 추상 클래스라고 한다.
  • 이러한 추상 클래스는 OOP에서 중요한 특징인 다형성을 가진 함수의 집합을 정의할 수 있게 해준다.
  • 즉, 반드시 사용되어야 하는 멤버 함수를 추상 클래스에 순수 가상 함수로 선언해 놓으면, 이 클래스로부터 파생된 모든 클래스에서는 이 가상 함수를 반드시 재정의해야 한다.


  • 추상 클래스는 동작이 정의되지 않은 순수 가상 함수를 포함하고 있으므로, 인스턴스를 생성할 수 없다.
  • 따라서 추상 클래스는 먼저 상속을 통해 파생 클래스를 만들고, 만든 파생 클래스에서 순수 가상 함수를 모두 오버라이딩하고 나서야 비로소 파생 클래스의 인스턴스를 생성할 수 있게 된다.
  • 하지만 추상 클래스 타입의 포인터와 참조는 바로 사용할 수 있다.


#include <iostream>
using namespace std;

class Animal {
   public:
    virtual ~Animal() {}     // 가상 소멸자의 선언
    virtual void Cry() = 0;  // 순수 가상 함수의 선언
};

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

class Cat : public Animal {
   public:
    virtual void Cry() { cout << "야옹야옹!" << endl; }
};

int main(void) {
    Dog my_dog = Dog();
    my_dog.Cry();
    Cat my_cat = Cat();
    my_cat.Cry();

    return 0;
}

// 멍멍!
// 야옹야옹!


  • 위의 코드에서 추상 클래스인 Animal 클래스는 순수 가상 함수인 Cry() 멤버 함수를 가지고 있다.
  • Animal 클래스를 상속 받는 파생 클래스인 Dog 클래스와 Cat 클래스는 Cry() 함수를 오버라이딩해야만 인스턴스를 생성할 수 있다.


3) 추상 클래스의 용도 제한

  • C++에서 추상 클래스는 다음과 같은 용도로는 사용할 수 없다.


1] 변수 또는 멤버 변수

2] 함수의 전달되는 인수 타입

3] 함수의 반환 타입

4] 명시적 타입 변환의 타입


References