다형성 기법
다형성
- 다형성이란 여러 개의 서로 다른 객체가 동일한 기능을 서로 다른 방법으로 처리할 수 있는 기능을 의미
- 게임 프로그램, 칼, 대포, 총 등의 무기들은 공통적으로 공격이라는 동일한 기능을 수행할 수 있음
- 아래와 같은 구성에서는 무기 객체에서 attack() 함수를 실질적으로 구현할 필요가 없음
- 이럴 때 무기 객체를 추상 클래스로 구현하면 효과적으로 설계를 할 수 있음
무기 객체 : 칼, 대포, 총
자식 클래스에서 오버라이딩의 문제점
- 자식 클래스에서 멤버 함수를 재정의하여 사용하는 것은 일반적으로 정상적으로 동작함
- 포인터 변수로 객체에 접근할 때는 예상치 못한 결과가 발생
- C++ 컴파일러는 포인터 변수가 가리키고 있는 변수의 타입을 기준으로 함수를 호출하지 않고 포인터의 타입을 기준으로 함수를 호출
- 따라서 A라는 객체를 가리키는 포인터 변수는 A객체의 멤버 함수만을 호출할 수 있음
* 일반적인 함수의 정적 바인딩
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <stdio.h>
using namespace std;
class A {
public:
void show() { cout << "A 클래스" << endl; }
};
class B : public A {
void show() { cout << "B 클래스" << endl; }
};
int main(void) {
A* p;
A a;
B b;
p = &a; p->show();
p = &b; p->show(); // 여전히 A 클래스의 show() 함수를 호출
system("pause");
return 0;
}
동적 바인딩
- C++은 특정한 함수를 호출할 때 해당 함수의 루틴이 기록된 메모리 주소를 알아야 함
- 특정한 함수를 호출하는 소스코드에서 실제로 함수가 정의된 메모리 공간을 찾기 위해서는 바인딩 과정이 필요
- 일반적으로 함수의 호출은 컴파일 시기에 고정된 메모리 주소를 이용함
- 이러한 방식을 정적 바인딩(Static Binding)이라고 하는데 C++의 일반적인 멤버 함수는 모두 이러한 정적 바인딩을 사용
- 가상 함수는 프로그램이 실행될 때 객체를 결정한다는 점에서 컴파일 시간에 객체를 특정할 수 없음
- 가상 함수는 실행 시간 때 올바른 함수가 실행될 수 있도록 동적 바인딩(Dynamic Binding)을 사용
가상 함수(Virtual Function)
- 가상 함수란 자식 클래스에서 재정의할 수 있는 멤버 함수
- vritual 키워드를 이용해 가상 함수를 선언할 수 있으며 자식 클래스에서 가상 함수를 재정의 하면 재정의된 멤버 함수 또한 가상 함수로 분류
- C++ 컴파일러는 가상 함수 테이블(Virtual Function Table)을 이용해 가상 함수를 다루게 됨
- 컴파일러는 각각의 객체마다 가상 함수 테이블을 가리키는 포인터를 저장하기 위한 멤버를 하나씩 저장함
- 가상 함수 테이블에는 특정한 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장됨
- 가상 함수를 호출하면 C++ 프로그램은 가상 함수 테이블에 접근하여 자신이 필요한 함수의 주소를 찾아 호출하게 됨
- 이러한 과정은 말 그대로 동적 바인딩을 통해 이루어지므로 컴퓨팅 리소스를 소모하게 됨
- C++은 기본적으로 정적 바인딩을 채택하고 있음
- 자식 클래스가 재정의할 가능성이 있는 멤버 함수들은 가상 함수로 선언하는 것이 좋음
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <stdio.h>
using namespace std;
class A {
public:
virtual void show() { cout << "A 클래스" << endl; } // 가상 함수
};
class B : public A {
virtual void show() { cout << "B 클래스" << endl; } // 가상 함수
};
int main(void) {
A* p;
A a;
B b;
p = &a; p->show();
p = &b; p->show();
system("pause");
return 0;
}
가상 클래스의 소멸자
- 상속 관계가 있으면서 그와 동시에 메모리 해제를 해야 하는 경우에는 반드시 부모 클래스의 소멸자를 가상 함수로 선언해야 함
- 부모 포인터로 객체를 삭제하면 부모 클래스의 소멸자가 호출되기 때문
- 만약 다형성을 이용할 때 소멸자를 가상 함수로 선언하지 않으면 자식 클래스의 소멸자는 호출되지 않고 부모 클래스의 소멸자만 호출되기 때문에 자식 클래스의 객체는 여전히 정상적으로 해제되지 않음
순수 가상 함수(Pure Virtual Function)
- 가상 함수는 기본적으로 반드시 재정의할 필요는 없음
- 순수 가상 함수는 자식 클래스에서 반드시 재정의를 해주어야 하는 함수
- 일반적으로 순수 가상 함수는 부모 클래스에서 함수 동작의 본체를 정의하지 않음
- 자식 클래스에서 반드시 이를 정의해야 사용할 수 있음
- 순수 가상 함수는 =0 키워드를 붙여 선언할 수 있음
* 순수 가상 함수는 함수 프로토타입 정의만 있고, 함수 바디 즉 코딩은 없어야 함
함수 바디는 상속 받아서 만들어야 함
=0; 는 순수 가상 함수를 정의하므로, 뒤에 { .. } 로 함수 바디 코딩을 할 수 없음
#include <iostream>
#include <string>
#include <stdio.h>
using namespace std;
class A {
public:
virtual void show()=0
};
class B : public A {
public:
virtual void show() { cout << "B 클래스" << endl; } // show() 함수를 재정의 하지 않으면 B 클래스의 객체를 사용할 수 없음
};
int main(void) {
A* p;
B b;
p = &b; p->show();
system("pause");
return 0;
}
추상 클래스(Abstract Class)
- 추상 클래스란 하나 이상의 순수 가상 함수를 포함하는 클래스를 의미
- 추상 클래스를 활용하면 다형성을 효과적으로 프로그램 상에서 구현할 수 있음
- 따라서 자식 클래스는 추상 클래스를 상속받은 이후에 반드시 순수 가상 함수를 모두 오버라이딩 해야 비로소 해당 객체를 사용할 수 있음