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
키워드를 사용하여 가상 함수라는 것을 명확히 하는 것이 좋다.
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
클래스는Student
클래스의 기초 클래스이므로,hong
이라는Student
객체가 동적으로 할당된다. - 하지만 마지막 구문의
delete
키워드는~Student()
소멸자를 호출하지 않고,~Person()
소멸자를 호출할 것이다. - 그러므로
Student
객체에 동적으로 할당된 메모리는 정상적으로 할당된 메모리는 정상적으로 해제되지 않을 것이다.
- 하지만
Person
클래스의 소멸자를 가상으로 선언한다면, 위의 구문은 정상적으로~Student()
소멸자를 호출할 것이다. - 따라서 기초 클래스는 명시적으로 소멸자를 선언할 필요가 없더라도, 아무 일도 하지 않는 가상 소멸자를 선언해야 한다.
3. 추상 클래스
1) 순수 가상 함수
- C++에서 가상 함수는 파생 클래스에서 재정의할 것으로 기대하는 멤버 함수를 의미한다.
- 따라서 가상 함수는 반드시 재정의해야만 하는 함수가 아닌, 재정의가 가능한 함수를 가리킨다.
- 이와는 달리 순수 가상 함수란 파생 클래스에서 반드시 재정의해야 하는 멤버 함수를 의미한다.
- 이러한 순수 가상 함수는 일반적으로 함수의 동작을 정의하는 본체를 가지고 있지 않다.
- 따라서 파생 클래스에서 재정의하지 않으면 사용할 수 없다.
- C++에서 순수 가상 함수는 다음과 같은 문법으로 선언한다.
- 위와 같이 함수만 있고 본체가 없다는 의미로 함수 선언부 끝에
= 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] 명시적 타입 변환의 타입