CPP Module 04

개발새발·2022년 6월 10일
0

42Cursus

목록 보기
18/29
post-thumbnail

CPP Module 04

해당 과제를 진행하는데 있어 필요한 사전 지식들을 정리하였습니다.

1. 다형성 (Polymorphism)

부모 클래스 = 기반 클래스 = 기초 클래스
자식 클래스 = 파생 클래스 = 유도 클래스

1-1. 다형성

다형성(polymorphism)이란 하나의 객체가 여러 가지 타입을 가질 수 있는 것을 의미한다. 객체지향 언어에서 서로 다른 객체가 동일한 메시지에 대하여 서로 다른 방법으로 응답할 수 있는 기능이라고 정의할 수도 있다. 참조 변수의 다형성은 기초 클래스 타입의 참조 변수로 유도 클래스 타입의 객체를 참조할 수 있도록 하는 것이다. (업 캐스팅)

상속의 구조에서 다형성을 이루기 위해선 몇 가지 조건을 만족해야 하는데 이를 컴파일 타임과 런 타임, 2가지 관점으로 나누어 볼 수 있다.

컴파일 타임에서는 상속 관계에 있는 클래스들의 함수와 연산자의 overloading들이 적절히 이루어져야 하고, 런 타임에서는 virtual의 적절한 overriding을 통해 상속 관계에 있는 클래스들이 타입에 맞는 함수를 호출할 수 있어야 한다.

overloading의 경우, 여러 시그니처로 구성된 함수들 중 어느 함수를 고를 것인지 컴파일러의 규칙에 의해서 컴파일 타임에 결정된다. overridingoverloading과 달리 컴파일 타임에 어느 함수를 고를 것인지 결정 짓는 것이 아니라, 런 타임에 vTable에 유지되고 있는 virtual로 명시된 함수에 대해 자신의 타입을 확인하면서 적절한 함수를 결정 짓는다. 이 때문에 overloading은 컴파일 타임, overriding은 런 타임으로 구분된다.

Overloading vs Overriding
오버로딩(Overloading)은 매개변수의 개수 또는 타입이 다르게 하여, 같은 이름을 사용해서 메소드를 여러개 정의할 수 있는 것이다.
오버라이딩(Overriding)은 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해서 사용하는 것이다.

1-2. 업 캐스팅

기초 클래스를 상속한 유도 클래스에서 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;
}

1-3. 가상 소멸자

overriding 하려는 함수들을 virtual로 선언하는 것과 함께 소멸자도 virtual로 선언하는 습관 역시 중요하다. 이것도 업 캐스팅과 관련이 있다.

B 클래스A 클래스를 상속했다면 정상적인 생성 및 소멸 순서는 A 생성 → B 생성 → B 소멸 → A 소멸이다. 그런데 업 캐스팅하여 이용하는 객체가 소멸자를 virtual로 두지 않았을 때는 A 생성 → B 생성 → A 소멸의 결과를 얻게 된다. B로 생성된 객체가 소멸되지 않아 메모리 누수로 이어질 수 있는 것이다. 따라서 소멸자도 virtual로 선언함으로써, 런 타임에 기초 클래스의 포인터가 참조하는 값이 실제로 어떤 타입인지 확인하고 타입에 해당되는 소멸자를 올바르게 호출할 수 있게 만들어야 한다.

1-4. Polymorphic 클래스의 형 변환

형 변환 연산자 중에서 dynamic_cast 연산자는 안정적인 형 변환을 보장한다. 하지만 static_cast 연산자는 무조건 형 변환이 되기 때문에 안정성을 보장하지 않는다.

상속관계에 놓여있는 두 클래스 사이에서, 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 dynamic_cast 연산자를 사용해야 한다.

반대로, 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 static_cast 연산자를 사용해야 한다.

하지만 기초 클래스가 Polymorphic 클래스이면 dynamic_cast 연산자도 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형으로의 형 변환을 허용한다.

Polymorphic 클래스란 하나 이상의 가상함수를 지니는 클래스를 뜻한다. 그러니 상속관계에 놓여있는 두 클래스 사이에서, 기초 클래스에 가상함수가 하나 이상 존재하면 dynamic_cast 연산자를 이용해서 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 변환이 가능하다.

 

2. ex00 (Polymorphism)

정상적으로 virtual 키워드를 통해 overridingAnimal, Cat, Dog 클래스들과 virtual 키워드 없이 overridingWrongAnimal, WrongCat 클래스들을 정의한다. 그런 다음, Animal 클래스로 다형성을 이루는 객체와 WrongAnimal 클래스로 다형성을 이루는 객체의 업 캐스팅된 포인터를 확인해보면, 자신의 타입으로 함수를 호출하는 Animal 클래스의 객체와 달리 WrongAnimal 클래스의 객체는 자신의 타입으로 함수를 호출하는 것이 아니라 항상 WrongAnimal 클래스의 함수를 호출한다.

 

3. ex01 (I don't want to set the world on fire)

Brain이라는 클래스를 새로 정의하여 ex00에서 정의한 CatDog 클래스의 private 멤버 변수로 Brain 포인터를 갖게한다. 클래스 내에 멤버 변수를 포인터로 선언한 것이 있고 동적 할당을 해주었다면, 포인터에 할당된 메모리를 적절하게 해제할 수 있도록 신경을 써야한다. CatDog 클래스의 경우, 생성자 또는 대입 연산자를 통해 할당받은 Brain 객체를 각 클래스의 소멸자에서 delete 연산을 통해 할당된 메모리를 해제 해주어야 한다.

또한 포인터를 멤버 변수로 두었을 경우, 복사 생성자와 대입 연산자를 직접 정의하여 깊은 복사가 이루어지도록 해야한다. 깊은 복사를 실행하는 함수를 직접 정의하지 않을 경우, 컴파일러에 의해 자동으로 기본 복사 생성자와 기본 대입 연산자가 생성되어 얕은 복사가 이루어지게 된다.

 

4. ex02 (Abstract class)

ex02에서는 Animal 클래스 자체가 객체화 되지 못하게 하기 위하여 순수 가상 함수를 사용한다. 순수 가상 함수란 아래 코드와 같이 클래스 내에서 virtual로 선언된 함수에 0을 할당하여 해당 함수를 정의하지 않겠다는 것을 의미한다.

class A
{
public :
	virtual void func() const = 0;
}

이는 다형성을 위한 유도 클래스에서의 생성을 제외하고는 기초 클래스 직접 생성을 불가능하게 한다. 따라서 해당 함수를 유도 클래스에서 반드시 overriding 해야 한다. 그렇지 않으면 유도 클래스 역시 객체로 만들 수 없다.

즉, 순수 가상 함수를 갖고 있는 클래스는 그 자체로는 객체화 될 수 없기 때문에 유도 클래스에게 제공되는 Interface 역할을 한다. 이러한 클래스를 추상 클래스(Abstract Class)라고 한다.

 

5. ex03 (Interface & recap)

ex03은 다형성과 추상 클래스 등 지금까지 배운 모든 개념들을 활용하여 다음의 총 7개의 클래스를 정의해야 한다.

  • AMateria
  • AMateria를 인터페이스로 활용하여 상속하는 IceCure
  • ICharacter
  • ICharacter 를 인터페이스로 활용하여 상속하는 Chararcter
  • IMateriaSource
  • IMateriaSource 를 인터페이스로 활용하여 상속하는 MateriaSource

Chararcter는 사용자를 나타내고, 사용자는AMateria를 인터페이스로 하는 IceCure을 소지할 수 있다. IceCure 같은 물질을 생성하기 위해서는 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

profile
블록체인 개발 어때요

0개의 댓글