해당 과제를 진행하는데 있어 필요한 사전 지식들을 정리하였습니다.
부모 클래스
=기반 클래스
=기초 클래스
자식 클래스
=파생 클래스
=유도 클래스
다형성(polymorphism)이란 하나의 객체가 여러 가지 타입을 가질 수 있는 것을 의미한다. 객체지향 언어에서 서로 다른 객체가 동일한 메시지에 대하여 서로 다른 방법으로 응답할 수 있는 기능이라고 정의할 수도 있다. 참조 변수의 다형성은 기초 클래스 타입의 참조 변수로 유도 클래스 타입의 객체를 참조할 수 있도록 하는 것이다. (업 캐스팅)
상속의 구조에서 다형성을 이루기 위해선 몇 가지 조건을 만족해야 하는데 이를 컴파일 타임과 런 타임, 2가지 관점으로 나누어 볼 수 있다.
컴파일 타임에서는 상속 관계에 있는 클래스들의 함수와 연산자의 overloading
들이 적절히 이루어져야 하고, 런 타임에서는 virtual
의 적절한 overriding
을 통해 상속 관계에 있는 클래스들이 타입에 맞는 함수를 호출할 수 있어야 한다.
overloading
의 경우, 여러 시그니처로 구성된 함수들 중 어느 함수를 고를 것인지 컴파일러의 규칙에 의해서 컴파일 타임에 결정된다. overriding
은 overloading
과 달리 컴파일 타임에 어느 함수를 고를 것인지 결정 짓는 것이 아니라, 런 타임에 vTable
에 유지되고 있는 virtual
로 명시된 함수에 대해 자신의 타입을 확인하면서 적절한 함수를 결정 짓는다. 이 때문에 overloading
은 컴파일 타임, overriding
은 런 타임으로 구분된다.
Overloading vs Overriding
오버로딩(Overloading)은 매개변수의 개수 또는 타입이 다르게 하여, 같은 이름을 사용해서 메소드를 여러개 정의할 수 있는 것이다.
오버라이딩(Overriding)은 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해서 사용하는 것이다.
기초 클래스를 상속한 유도 클래스에서 overriding
하려는 함수들을 virtual
로 선언할 필요가 있다. virtual
로 선언하지 않았다고 해서 기초 클래스에 존재하는 함수를 유도 클래스에서 overriding
하지 못하는 것은 아니지만 그럼에도 그렇게 해야하는 이유는 업 캐스팅 때문이다.
상속이 이루어졌을 때, 기초 클래스들과 유도 클래스들은 기초 클래스의 포인터로 묶어서 관리하는 것이 가능하다. 이를 업 캐스팅이라고 한다. 이 때 유도 클래스를 업 캐스팅하여 기초 클래스 포인터로 유도 클래스의 overriding
한 함수를 호출하려면 유도 클래스의 함수가 아니라 기초 클래스의 함수를 호출해야 한다. 이는 호출 시 기초 클래스의 포인터를 이용하여 기초 클래스로 타입을 인식하면서 발생하는 것인데, 이를 극복하여 유도 클래스 타입을 정상적으로 인식시키기 위해선 런 타임에 자신이 유도 클래스라는 것을 알게 할 수 있어야 한다. 이를 가능하게 하는 것이 virtual
키워드이다.
클래스 상속의 구조가 일반적으로 기초 클래스가 위에 있고 유도 클래스가 아래있는 것처럼 유도 클래스가 그 위에 있는 기초 클래스의 포인터로 캐스팅 되므로 업 캐스팅이라고 한다. 반대로 기초 클래스를 유도 클래스의 포인터로 캐스팅하는 것은 다운 캐스팅이라고 하는데 이는 dynamic_cast
와 관련이 있다.
멤버 함수들은 클래스 내에 귀속되는 엔트리를 갖는데, virtual
키워드로 선언된 함수들은 클래스 내에 엔트리를 갖지 않는다. virtual
키워드로 선언된 함수들은 vTable(Virtual Table)
에 자신의 타입과 묶여서 별도의 엔트리를 유지한다. 따라서 런 타임 도중에 해당 함수를 호출하려 했을 때 클래스 내에 엔트리가 없는 것이 인식되면, vTable
에서 기초 클래스의 포인터가 실제로 어떤 타입인지 확인한 뒤에 타입에 맞는 함수를 호출할 수 있게 된다.
기초 클래스의 포인터로 일반화하여 각 상속 객체들을 운용하는 방법은 굉장히 유용한데, 이를 이용할 때 예기치 못한 결과를 얻고 싶지 않다면 overriding
하려는 함수들을 기초 클래스에서 반드시 virtual
로 선언해야 한다.
예제
class A
{
public :
virtual void func(void)
{ ... }
};
class B : public A
{
public :
void func(void)
{ ... }
};
int main(void)
{
B b;
A *a = &b; // 업 캐스팅
b.func(); // B의 func가 실행됨
a->func(); // B의 func가 실행됨 (A의 func에 virtual 선언이 있기 때문)
return 0;
}
overriding
하려는 함수들을 virtual
로 선언하는 것과 함께 소멸자도 virtual
로 선언하는 습관 역시 중요하다. 이것도 업 캐스팅과 관련이 있다.
B 클래스
가 A 클래스
를 상속했다면 정상적인 생성 및 소멸 순서는 A 생성 → B 생성 → B 소멸 → A 소멸
이다. 그런데 업 캐스팅하여 이용하는 객체가 소멸자를 virtual
로 두지 않았을 때는 A 생성 → B 생성 → A 소멸
의 결과를 얻게 된다. B로 생성된 객체가 소멸되지 않아 메모리 누수로 이어질 수 있는 것이다. 따라서 소멸자도 virtual
로 선언함으로써, 런 타임에 기초 클래스의 포인터가 참조하는 값이 실제로 어떤 타입인지 확인하고 타입에 해당되는 소멸자를 올바르게 호출할 수 있게 만들어야 한다.
형 변환 연산자 중에서 dynamic_cast
연산자는 안정적인 형 변환을 보장한다. 하지만 static_cast
연산자는 무조건 형 변환이 되기 때문에 안정성을 보장하지 않는다.
상속관계에 놓여있는 두 클래스 사이에서, 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 dynamic_cast
연산자를 사용해야 한다.
반대로, 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 static_cast
연산자를 사용해야 한다.
하지만 기초 클래스가 Polymorphic
클래스이면 dynamic_cast
연산자도 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형으로의 형 변환을 허용한다.
Polymorphic
클래스란 하나 이상의 가상함수를 지니는 클래스를 뜻한다. 그러니 상속관계에 놓여있는 두 클래스 사이에서, 기초 클래스에 가상함수가 하나 이상 존재하면 dynamic_cast
연산자를 이용해서 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 변환이 가능하다.
정상적으로 virtual
키워드를 통해 overriding
한 Animal
, Cat
, Dog
클래스들과 virtual
키워드 없이 overriding
한 WrongAnimal
, WrongCat
클래스들을 정의한다. 그런 다음, Animal
클래스로 다형성을 이루는 객체와 WrongAnimal
클래스로 다형성을 이루는 객체의 업 캐스팅된 포인터를 확인해보면, 자신의 타입으로 함수를 호출하는 Animal
클래스의 객체와 달리 WrongAnimal
클래스의 객체는 자신의 타입으로 함수를 호출하는 것이 아니라 항상 WrongAnimal
클래스의 함수를 호출한다.
Brain
이라는 클래스를 새로 정의하여 ex00
에서 정의한 Cat
과 Dog
클래스의 private 멤버 변수로 Brain
포인터를 갖게한다. 클래스 내에 멤버 변수를 포인터로 선언한 것이 있고 동적 할당을 해주었다면, 포인터에 할당된 메모리를 적절하게 해제할 수 있도록 신경을 써야한다. Cat
과 Dog
클래스의 경우, 생성자 또는 대입 연산자를 통해 할당받은 Brain
객체를 각 클래스의 소멸자에서 delete
연산을 통해 할당된 메모리를 해제 해주어야 한다.
또한 포인터를 멤버 변수로 두었을 경우, 복사 생성자와 대입 연산자를 직접 정의하여 깊은 복사가 이루어지도록 해야한다. 깊은 복사를 실행하는 함수를 직접 정의하지 않을 경우, 컴파일러에 의해 자동으로 기본 복사 생성자와 기본 대입 연산자가 생성되어 얕은 복사가 이루어지게 된다.
ex02
에서는 Animal
클래스 자체가 객체화 되지 못하게 하기 위하여 순수 가상 함수를 사용한다. 순수 가상 함수란 아래 코드와 같이 클래스 내에서 virtual
로 선언된 함수에 0을 할당하여 해당 함수를 정의하지 않겠다는 것을 의미한다.
class A
{
public :
virtual void func() const = 0;
}
이는 다형성을 위한 유도 클래스에서의 생성을 제외하고는 기초 클래스 직접 생성을 불가능하게 한다. 따라서 해당 함수를 유도 클래스에서 반드시 overriding
해야 한다. 그렇지 않으면 유도 클래스 역시 객체로 만들 수 없다.
즉, 순수 가상 함수를 갖고 있는 클래스는 그 자체로는 객체화 될 수 없기 때문에 유도 클래스에게 제공되는 Interface
역할을 한다. 이러한 클래스를 추상 클래스(Abstract Class)라고 한다.
ex03
은 다형성과 추상 클래스 등 지금까지 배운 모든 개념들을 활용하여 다음의 총 7개의 클래스를 정의해야 한다.
AMateria
AMateria
를 인터페이스로 활용하여 상속하는 Ice
와 Cure
ICharacter
ICharacter
를 인터페이스로 활용하여 상속하는 Chararcter
IMateriaSource
IMateriaSource
를 인터페이스로 활용하여 상속하는 MateriaSource
Chararcter
는 사용자를 나타내고, 사용자는AMateria
를 인터페이스로 하는 Ice
와 Cure
을 소지할 수 있다. Ice
와 Cure
같은 물질을 생성하기 위해서는 IMateriaSource
를 인터페이스로 하는 MateriaSource
를 이용해야 한다.
Chararcter
는 최초에 최대 4개의 비어있는 Materia 인벤토리를 갖고있고 인덱스 0번에서 인덱스 3번까지 순서대로 장착한다. 만약 인벤토리가 가득 찬 상태에서 장착하려고 하거나, 없는 Materia를 사용하거나 장착을 해제 하려고 하면 아무 동작도 하지 않아야 한다.
참고
http://www.tcpschool.com/java/java_polymorphism_concept
https://bigpel66.oopy.io/library/42/inner-circle/15