다형성과 가상함수
다형성
다형성(polymorphism) 이란 하나의 기호를 여러 가지 의미로 사용하는 기술이다.
C++에서 다형성은 다음과 같이 분류된다.
각기 다른 동물들에게 speak라는 명령을 내리면 각각 다른 울음 소리를 낼 것이다. 똑같은 메시지를 보내지만 객체의 타입이 다르면 서로 다른 결과를 얻는 것이 다형성이다.
상향 형변환(Up-Casting)
다형성은 객체 포인터를 통해 이루어진다. 설명을 위해 몇 가지 클래스가 정의되어 있는데 Animal 클래스에서 상속을 받아서 Dog과 Cow와 Cat 클래스를 정의한다.
객체 포인터도 타입이 맞는 객체만을 가리킬 수 있는데 다음 코드를 보고 생각해보자
Animal *pa = new Cow(); // 가능한가?
위의 문장은 가능한데, 이유는 자식 객체는 부모 객체를 포함하고 있기 때문에 자식 객체는 부모 객체이기도 하기 때문이다. 이러한 포인터 타입의 변환을 __상향 형변환(Up-Casting) __이라고 한다.
하지만, 상향 형변환을 하면 자식 클래스 중에 부모 클래스로부터 상속받은 부분만을 포인터를 통해 사용할 수 있고 나머지는 사용하지 못한다.
다음 아래코드를 보고 생각해보자.
class Animal{
public:
void speak() { cout << "Animal speak()"<<endl;}
};
class Dog : public Animal{
public:
int age;
void speak(){cout <<"멍멍"<<endl;}
};
int main(){
Animal *a = new Dog();
a->speak();
//a->age = 10; //오류!!
return 0;
}
실행 결과
Animal speak()
상향 형변환시 자식 클래스의 멤버 변수는 사용하지 못하고 심지어 상속 오버라이딩 또한 되지 않았다.
그 이유는, C++ 컴파일러가 실제로 가리키는 객체의 자료형을 기준으로 하는게 아닌, 포인터 변수의 자료형을 기준으로 판단하기 때문이다. 즉, 실제로 가리키는 객체인 Dog의 자료형이 아닌 포인터 변수 자료형인 Animal을 기준으로 판단하는 것이다.
그렇다면, 실제로 가리키는 객체에 따라 멤버 함수가 호출되도록 하려면 어떻게 해야할까? 이 문제는 곧 배울 virtual키워드를 함수 앞에 명시해주면 해결 가능하다. 이렇게 virtual 키워드가 붙은 함수를 가상함수 라고 한다.
가상 함수
함수 앞에 virtual 키워드를 붙여주어 부모 포인터를 통하여 객체의 멤버 함수를 호출 하도록 하는 방법이다.
아래 코드를 보자
class Animal{
public:
virtual void speak() { cout << "Animal speak()"<<endl;}
};
class Dog : public Animal{
public:
int age;
void speak(){cout <<"멍멍"<<endl;}
};
int main(){
Animal *a = new Dog();
a->speak();
return 0;
}
실행 결과
멍멍
virtual 키워드는 멤버 함수에만 사용할 수 있는데 멤버 변수에는 사용할 수 없다. 그리고, 부모 클래스에서 virtual로 정의하면 자식 클래스에서는 virtual 키워드를 사용하지 않아도 자동으로 가상 함수가 된다.
동적 바인딩 VS 정적 바인딩
함수 호출을 함수의 몸체와 연결하는 것을 바인딩(binding)이라고 한다. 바인딩에는 정적 바인딩과 동적 바인딩이 존재하는데 아래 표를 참고 하자.
바인딩의 종류 | 특징 | 속도 | 대상 |
---|---|---|---|
정적 바인딩 | 컴파일 시간에 호출 함수가 결정 된다. | 빠르다 | 일반 함수 |
동적 바인딩 | 실행 시간에 호출 함수가 결정 된다. | 늦다 | 가상 함수 |
C++에서 가상 함수가 아니면 모든 함수가 정적 바인딩으로 호출된다.
참조자와 가상함수
포인터 뿐만 아니라 참조자인 경우에도 다형성이 동작한다.
class Animal{
public:
virtual void speak() { cout << "Animal speak()"<<endl;}
};
class Dog : public Animal{
public:
int age;
void speak(){cout <<"멍멍"<<endl;}
};
int main(){
Animal *a = new Dog();
Dog d;
Animal &a1 = d;
a->speak();
a1.speak();
return 0;
}
실행 결과
멍멍
멍멍
가상 소멸자
다형성을 사용하는 과정에서 소멸자를 virtual로 해주지 않으면 부모 클래스 소멸자만 호출되는 문제가 발생한다.
아래의 코드를 살펴보자.
class Animal {
public:
~Animal(){cout << "Animal 소멸자"<< endl;}
};
class Dog : public Animal {
public:
~Dog(){cout << "Dog 소멸자"<< endl;}
};
int main(){
Animal* a = new Dog(); // 상향 형변환
delete a;
}
실행 결과
Animal 소멸자
이와 같은 현상을 해결하기위한 방법은 간단하다. 부모클래스 소멸자에 virtual 키워드만 붙여주면 된다.
class Animal {
public:
virtual ~Animal(){cout << "Animal 소멸자"<< endl;}
};
class Dog : public Animal {
public:
~Dog(){cout << "Dog 소멸자"<< endl;}
};
int main(){
Animal* a = new Dog(); // 상향 형변환
delete a;
}
실행 결과
Dog 소멸자
Animal 소멸자
순수 가상 함수
순수 가상 함수(pure virtual function)는 함수 헤더만 존재하고 함수의 몸체는 없는 함수이다. 다음과 같은 형식을 사용한다.
virtual 반환형 함수이름(매게변수리스트) = 0;
순수 가상 함수를 하나라도 가지고 있는 클래스를 추상 클래스(abstract class) 라고 하는데 이는 다음 글에서 자세히 다뤄보겠다.