파생 클래스의 메소드가 기초 클래스의 메소도와 다른 행동을 요구되는 상황이 있을 수 있다. 호출하는 객체에 따라 메소드의 내용이 달라지는 것을 다형(polymorhphic)이라 부른다. public 다형 상속을 구현하는 두 가지 중요한 방법이 있다.
- 기초 클래스 메소드를 파생 클래스에서 다시 정의한다.
- 가상 메소드(virtual method)를 사용한다.
기초 클래스의 메소드를 파생 클래스에서 다시 정의한다는 것이 곧 가상 메소드를 사용한다는 의미랑 같다고 생각한다. 가상 메소드를 사용하지 않고 재정의하는 경우 예상치못한 일이 발생할 수 있기 때문이다.
기초 클래스 Animal과 파생 클래스 Dog을 구현한다고 가정해보자. 책의 코드는 길어서 임의로 작성했다.
//Animal.h
#pragma once
#include <string>
using std::string;
class Animal
{
public:
Animal(unsigned int MaxAge);
virtual ~Animal() {}
virtual void Bark();
void Move(float x, float y);
unsigned int GetCurrentAge() { return currentAge; }
private:
unsigned int currentAge;
float xPos;
float yPos;
};
class Dog : public Animal
{
public:
Dog(unsigned int MaxAge, string BarkSound);
Dog(Animal& anim, string BarkSound));
virtual void Bark() override;
private:
string barkSound;
};
//Animal.cpp
#include "Animal.h"
#include <iostream>
Animal::Animal(unsigned int MaxAge)
: maxAge(MaxAge), currentAge(0)
{ }
void Animal::Bark()
{
cout << "Bark" << endl;
}
void Animal::Move(float x, float y)
{
xPos = x;
yPos = y;
}
Dog::Dog(unsigned int MaxAge, string BarkSound)
: Animal(MaxAge), barkSound(BarkSound)
{ }
Dog::Dog(Animal & anim, string BarkSound)
: Aniaml(anim), barkSound(BarkSound)
{ }
void Dog::Bark()
{
cout << "Mung" << endl;
}
위 코드에서 봐야 할 점이 몇 가지 있다.
- Bark 메소드 앞에 virtual 키워드가 있고, 기초 클래스와 파생 클래스 모두 갖고 있다.
- 기초 클래스의 소멸자에 virtual 키워드가 붙어 가상 소멸자로 선언되어 있다.
- 파생 클래스에서 virtaul 키워드와 override는 굳이 붙이지 않아도 된다. 하지만 가상 메소드라는 것을 알 수 있도록 붙이는 것이 정신에 이롭다.
Bark 메소드에 virtual 키워드를 붙여 가상 메소드로 선언함으로써 파생 클래스에서 재정의한 경우 파생 클래스의 Bark 메소드를 호출하고, 재정의 하지 않은 경우 기초 클래스의 Bark 메소드를 호출한다.
즉, 기초 클래스에서 선언과 구현을 할테니 맘에 들지 않으면 파생 클래스에서 재정의하고 맘에 들면 기초 클래스 것을 써 라고 받아들이면 된다.
기초 클래스가 가상 소멸자로 선언한다는 점을 알아야한다. 파생 클래스 객체가 소멸될 때 소멸자들이 올바른 순서로 호출되도록 해준다. new에 의해 대입된 객체들을 해제하기 위해 delete를 사용하는 코드는 소멸자들이 가상이 아니라면 포인터형에 해당하는 소멸자만 호출될 것이다. 예를 들어 ,파생 클래스의 소멸자에서 기초 클래스의 멤버를 사용하여 소멸를 하는 경우가 있다. 그때 기초 클래스는 이미 소멸되었으므로, 파생 클래스의 소멸에서 문제가 발생할 수 있다는 것이다.
virtual 키워드를 사용하지 않고 재정의한 경우 어떤일이 발생할까?
//virtual 키워드를 붙인 경우
Animal animal(10);
Dog dog(15);
Animal& anim_ref = animal;
Animal& dog_ref = dog;
anim_ref.Bark(); //Animal::Bark() 호출
dog_ref.Bark(); //Dog::Bark() 호출
//virtual 키워드를 붙이지 않은 경우
Animal animal(10);
Dog dog(15);
Animal& anim_ref = animal;
Animal& dog_ref = dog;
anim_ref.Bark(); //Animal::Bark() 호출
dog_ref.Bark(); //Animal::Bark() 호출
virtaul 키워드를 사용하지 않을 경우, 참조형이나 포인터형에 기초하여 메소드를 선택하고, virtual 키워드를 사용했을 경우, 참조나 포인터에 의해 지시되는 객체형에 기초하여 메소드를 선택한다.
배열의 모든 원소들의 데이터형은 같아야 한다. Animal과 Dog은 상속 관계이지만 명백히 다른 데이터형이다. 그러나 기초 클래스인 Animal를 지시하는 포인터들의 배열인 경우 파생 클래스인 Dog 객체도 지시할 수 있다.
//main.cpp
#include "Animal.h"
int main()
{
Animal* animal[3];
animal[0] = new Animal(30);
animal[1] = new Dog(10, "Yeah");
animal[2] = new Dog(20, "Mung");
for (int i = 0; i < 3; i++)
animal[i]->Bark();
for (int i = 0; i < 3; i++)
delete animal[i];
system("Pause");
return 0;
}
'서적 정리 > C++ 기초 플러스' 카테고리의 다른 글
81.클래스 설계 복습 (0) | 2022.07.19 |
---|---|
80.상속과 동적 메모리 대입 (0) | 2022.07.18 |
79.추상화 기초 클래스 (0) | 2022.07.14 |
78.접근제어: protected (0) | 2022.07.13 |
77.정적 결합과 동적 결합 (0) | 2022.07.13 |
75.상속: is-a 관계 (0) | 2022.07.12 |
74.간단한 기초 클래스부터 시작하자 (0) | 2022.07.12 |
차례 (0) | 2022.07.12 |
댓글