Cpp04
Younji Kim / August 2022
다형성에 대해서 연습하는 CPP04에 대한 내용 정리.
Table of contents
다형성이란?
객체지향 프로그래밍(OOP)에서의 세가지 핵심은 ①캡슐화(encapsulation), ②상속(inheritance), ③다형성(polymorphism)이다. 여기서 다형성에 대해 정리해보려 한다.
설명에 들어가기 전, 하위 유형이란 파생 클래스에 의해 정의된 유형(자식 클래스)이고 상위 유형이란 기본 클래스에 의해 정의된 유형(부모 클래스)을 뜻한다. 부모 클래스에 새로운 기능을 추가하여 자식 클래스가 부모 클래스의 기능을 상속 받는 게 상속 관계라 할 수 있는데, 파생 클래스/자식 클래스는 부모 클래스의 특수화(specialization)가 된 것이며, 자식 클래스의 모든 인스턴스는 부모 클래스의 인스턴스이다. 그 반대는 성립되지 않는다.
다형성(그리스어로 많은 형태라는 뜻)은 상위 유형의 변수가 하위 유형 객체를 참조할 수 있음을 의미한다.
class 자식 : public 부모
{
...
}
int main()
{
자식 *B = new 자식;
부모 *A = B; // 가능. 부모 변수가 자식클래스의 객체를 참조하고 있음.
return 0;
}
- 가상 함수를 갖는 클래스를 다형성 유형(polymorphic type)이라고 한다.
소멸자에 virtual을 쓰는 이유
가상 함수는 시스템이 객체의 실제 유형에 기초하여 실행 시 어느 함수를 호출할 지 결정할 수 있도록 한다.
즉, 부모 클래스에 virtual로 정의된 A()라는 함수가 있으면, C++은 실행 시 어떤 A()함수를 호출할지를 동적으로 결정한다. 실행 시에 어떤 함수를 호출해야 하는지를 결정하는 기능을 동적 결합(dynamic binding)이라 한다.
함수에 대해 동적 결합이 가능하도록 하려면 두가지가 필요하다.
- 함수는 기본 클래스에서 virtual로 정의되어야 한다.
- 객체를 참조하는 변수는 가상 함수에서 참조에 의해 전달되거나 포인터로서 전달되어야 한다. (call by reference, call by pointer, NOT call by value)
다음은 가상 함수의 사용에 대한 주의사항이다.
- 만약 어떤 함수가 기본 클래스에서 virtual로 정의된 경우, 그 기본 클래스의 모든 파생 클래스에서 자동으로 virtual이 된다. virtual 키워드는 파생 클래스의 함수 선언에서 추가할 필요는 없다.
- 컴파일러가 매개변수 유형, 매개변수의 개수, 매개변수의 순서에 따라 일치하는 함수를 찾는 것을 정적 결합(static binding)이라고 한다. 가상 함수는 매개변수가 같아도 여러 개의 파생 클래스에서 구현될 수 있으며, 변수에 의해 참조된 객체의 실제 클래스에 의해 결정된다. 이를 동적 결합이라 하는 것이다.
- 만약 기본클래스에서 정의한 함수를 파생 클래스에서 재정의해야 한다면, 혼동과 실수가 발생하지 않도록 가상으로 정의해야 한다. 반대로 함수가 재정의되지 않을 경우에느느 가상으로 선언하지 않는 것이 효과적이다. 동적으로 가상함수를 결합하는 데에 시간과 시스템 자원이 더 많이 소요되기 때문이다.
다형성을 이용하기 위해 부모 클래스의 포인터로 자식클래스를 호출할 때, 가상 함수가 아닌 자식 클래스의 오버라이딩된 함수를 호출하면 부모 클래스의 멤버 함수가 호출된다. 소멸자도 자식 클래스에서 오버라이딩된 함수라고 볼 수 있기 때문에 부모 포인터로 객체를 삭제하면 부모 클래스의 소멸자가 호출된다. 즉, 가상 함수로 정의하지 않으면 자식 소멸자는 호출되지 않는다!!
가상 생성자
갑자기 가상 생성자는 어떻게 될까? 궁금해져서 찾아보았다.. 가상 생성자는 애초에 의미가 없다고 한다. 가상으로 정의하면 클래스들의 v-table을 참조해서 해당 객체의 함수를 찾는데, 그 과정에서 예상과 다른 문제가 일어난다. 소멸자가 delete와 같은 키워드를 이용해서 여러 번 호출할 수 있는 것과는 달리 생성자는 생성될 때 딱 1번만 호출되며 호출 과정에선 자기 자신의 객체만 생성자를 볼 수 있다는 점이다. 즉 virtual로 함수가 오버라이딩이 되어도 해당 생성자 입장에선 자신의 클래스만 볼 수 있다. 하나의 클래스의 생성자가 호출되는 시점에선 메모리상에 자식 클래스에 대한 데이터 값은 없는 것이다.
추상 클래스와 순수 가상 함수
클래스를 설계하는 케이스 대부분은 기본 클래스가 점차 구색을 갖추어 나간다. 이렇듯 클래스 설계에서 기본 클래스는 파생 클래스의 일반적인 특징을 가지고 있어야 한다. 때로는 기본 클래스가 너무 추상적이어서 어떤 특정 인스턴스도 가질 수 없을 때도 있다. 이와 같은 클래스를 추상 클래스(abstract class)라고 한다.
기하학도형-원의 관계에서 넓이를 구하는 것처럼, 넓이를 구할 수 있는 속성을 공통으로 가지고는 있지만 사실상 기하학도형에서 원의 넓이를 구하는 함수를 작성할 수는 없다. 때문에 이런 경우 넓이를 구하는 함수를 추상 함수로 설정하여 자식 클래스에서 내부를 구현할 수 있도록 한다. 추상 함수(abstract function)는 C++에서 순수 가상 함수(pure virtual function)라고 하기도 한다.
virtual double getArea() = 0;
이처럼 함수명 뒤에 = 0을 쓰면 순수 가상 함수가 된다. 순수 가상 함수를 하나라도 가지고 있는 클래스는 추상 클래스가 된다.