다형성 이란 하나의 객체가 여라가지 타입을 가질수 있는것을 의미.
객체지향 언어에서 서로다른 객체가 동일한 메세지에 대해 서로 다른 방법으로 응답할수 있는 기능.
참조 변수의 다형성은 기반 클래스 타입의 참조 변수로 파생 클래스 타입의 객체를 참조할 수있도록 하는것( 업 캐스팅 )
상속의 구조에서 다형성을 이루기 위해선 조건이 있다(컴파일, 런타임 가지 관점)
overloading 의 경우, 여러 시그니처로 구성된 함수들중 어느 함수를 고를 것인지 컴파일러의 규칙에 의해서 컴파일 타임에 결정된다.
overriding 은 런타임에 vTable에 유지되고 있는 virtual 로 명시된 함수에 대해 자신의 타입을 확인 하면서 적절한 함수를 결정 짓는다.
즉, overloagin 은 컴파일// overriding은 런타임
- 오버로딩(Overloading)은 매개변수의 개수 또는 타입이 다르게 하여, 같은 이름을 사용해서 메소드를 여러개 정의할 수 있는 것이다.
- 오버라이딩(Overriding)은 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해서 사용하는 것이다.
기초 클래스를 상속한 파생 클래스에서 overriding 하려는 함수들을 virtual로 선언할 필요가 있다.
virtual로 선언 하지 않는다고 해서 기초 클래스에 존재하는 함수를 파생 클래스에서 overriding하지 못하는것은 아니지만
그럼에도 그렇게 하는 이유는 업 캐스팅 때문이다.
상속이 이루어졌을때, 기초 클래스들과 파생 클래스들은 기초 클래스의 포인터로 묶어서 관리하는것이 가능하다.
이를 업캐스팅이라고 한다.
이때 파생클래스를 업캐스팅하여 기초 클래스 포인터로 파생 클래스의 overriding한 함수를 호출하려면 파생 클래스의 함수가 아니라 기초 클래스의 함수를 호출해여한다.
이는 호출시 기초 클래스의 포인터를 이용하여 기초 클래스로 타입을 인식하면서 발생하는데
이를 극복하여 파생 클래스 타입을 정상적으로 인식 시키기 위해선 런타임에 자신이 파생 클래스라는것을 알게 해주어야 한다
그게 virtual키워드 이다.?
클래스 상속의 구조가 일반적으로 기초 클래스가 위에 있고 파생클래스가 아래있는것처럼
아래에서 위로의 클래스가 포인터로 캐스팅 되므로 업 캐스팅이라고한다
반대로 위에서 아래 즉, 자식 에서 부모 클래스의 포인터로 캐스팅 하는것은 다운 캐스팅 이라한다
이는 dynamic_cast 와 관련이 있다.
멤버 함수들은 클래스 내에 귀속되는 엔트리를 갖는데
virtual 키워드로 선언된 함수들은 클래스 내에 엔트리를 갖지 않는다.
virtual 키워드로 선언된 함수들은 vTable(virtual table)에 자신의 타입과 묶여서 별도의 엔트리를 유지한다.
따라서 런 타임 중에 해당 함수를 호출하려 했을때 클래스 내에 엔트리가 없는 것이 인식 되면 ,
vTable 에서 기초 클래스의 포인터가 실제로 어떤 타입인지 확인한 뒤에 타입에 맞는 함수를 호출한다.
overriding 하려는 함수들을 virtual 로 선언하자 ~
소멸자 역시 virtual로 선언하자~
업 캐스팅과 관련
B클래스가 A클래스 상속 했다면
정상적인 소멸 순서는 A 생성-> B생성 -> B 소멸-> A 소멸
그러나 소멸자가 virtual이 아니면
위의 순서에서 B소멸이 빠진다.
런 타임에 기초 클래스의 포인터가 참조하는 값이 실제로 어떤 타입인지 확인하고 타입에 해당되는 소멸자를 올바르게 호출할 수 있게 만들어야 한다.
형 변환 연산자중 dynamic_cast 연산자는 안정적인 형 변환을 보장한다.
But static_cast
연산자는 무조건 형 변환이 되기 때문에 안정성을 보장 하지 못한다.
상속관예에 놓여있는 두 클래스 사이에서, 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 dynamic_cast 연산자를 사용해야한다.
반대로 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 static_cast 연산자를 사용 해야한다.
하지만 기초 클래스가 Ploymorphic 클래스 이면 dynamic_cast 연산자도 기초 클래스의 포인터 밑 참조형 데이터를 유도 클래스의 포인터 및 참조형으로의 형변환을 허용함.
Polymorphic 클래스란?
하나 이상의 가상함수를 지니는 클래스를 의미
그러나 상속 관계에 놓여있는 두 클래스 사이에서,
기초 클래스에 가상함수가 하나 이상 존재하면
dynamic_cast 연산자를 이용해 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 변환이 가능 .
Polymorphism
Turn-in directory : ex00/
Files to turn in : Makefile, main.cpp, *.cpp, *.{h, hpp}
Forbidden functions : None
정상적으로 virtual 키워드를 통해 overriding한 Animal, Cat, Dog 클래스들과 virtual 키워드 없이 overriding한 WrongAnimal, WrongCat 클래스들을 정의한다.
그런 다음, Animal 클래스로 다형성을 이루는 객체와 WrongAnimal 클래스로 다형성을 이루는 객체의 업 캐스팅된 포인터를 확인해보면,
자신의 타입으로 함수를 호출하는 Animal 클래스의 객체와 달리 WrongAnimal 클래스의 객체는 자신의 타입으로 함수를 호출하는 것이 아니라 항상 WrongAnimal 클래스의 함수를 호출한다.
const std::string &getType(void) const;
#include <iostream>
class Test
{
private:
int n;
public:
Test() :n(10) {}
int &getN(void) { return this->n; }
};
int main()
{
Test test;
test.getN()++;
std::cout << test.getN() << std::endl;
}
>> 11
int &getN(void) { return this->n; }
여기에서
const int &getN(void) {return this->n;} const;
하게 되면
위와 같은 메인 문에서 직접 멤버변수 값을 바꾸는 것을 막을수 있다
함수 앞에 있는 const.는 반환값이 참조자 일때만 유호하다.
함수 뒤에 있는 const 는 함수 내부에서 변수가 변하지 않게하는것.
Brain이라는 클래스를 새로 정의하여 ex00에서 정의한 Cat과 Dog 클래스의 private 멤버 변수로 Brain 포인터를 갖게한다.
클래스 내에 멤버 변수를 포인터로 선언한 것이 있고 동적 할당을 해주었다면, 포인터에 할당된 메모리를 적절하게 해제할 수 있도록 신경을 써야한다.
Cat과 Dog 클래스의 경우, 생성자 또는 대입 연산자를 통해 할당받은 Brain 객체를 각 클래스의 소멸자에서 delete 연산을 통해 할당된 메모리를 해제 해주어야 한다.
또한 포인터를 멤버 변수로 두었을 경우, 복사 생성자와 대입 연산자를 직접 정의하여 깊은 복사가 이루어지도록 해야한다.
깊은 복사를 실행하는 함수를 직접 정의하지 않을 경우, 컴파일러에 의해 자동으로 기본 복사 생성자와 기본 대입 연산자가 생성되어 얕은 복사가 이루어지게 된다.
직접 하나하나 넣어 줘야 한다.
까먹을까봐 쓰는 동적 할당 되어있을시 자기자신 대입연산 하게 되었을때
- delete 하고 다시 넣는데 자기자신이 인자로 들어 오면
자기자신을 삭제 하므로 다음 대입 연산을 하게 되면 터진다.?
그래서 동적할당 했을시에는 자기자신이 들어오면 그냥 return 하는 등의 예외 처리를 해줘야 한다.
A:
추상클래스, 순수 가상함수
class Animal
{
public:
Animal() {}
virtual ~Animal() {}
virtual void speak() = 0 // 순수 가상함수
}
//virtual로 선언된 함수에 0을 할당하여 해당 함수를 정의하지 않겠다는 것을 의미한다.
즉, 순수 가상 함수를 갖고 있는 클래스는 그 자체로는 객체화 될 수 없기 때문에 유도 클래스에게 제공되는 Interface 역할을 한다. 이러한 클래스를 추상 클래스(Abstract Class)라고 한다.
다형성과 추상클래스 등 지금까직 배운 모든 개념을 활용해서 총 7개 클래스 정의 해야한다.
Chararcter 는 사용자를 나타내고, 사용자는 AMateria를 인터페이스로 하는 Ice 와 Cure 을 소지할수 있다.
Ice와 Cure 같은 물질을 생성하기 위해서는 IMateriaSource 를 인터페이스로 하는 MateriaSource를 이용해야한다.
Chararcter는 최초에 최대 4개의 비어있는 Materia인벤토리를 갖고있고
인덱스 0번에서 인덱스 3번까지 순서대로 장착한다.
만약 인벤토리가 가득찬 상태에서 장착하려고 하거나,
없는 Materia를 사용하거나 장착을 해제 하려고 하면 아무동작도 하지 않아야 한다.
컴파일 시간을 단축 시키며, 헤더포함 의존성을 줄여준다.
/* Core.hpp */
class UtilClass;
class Core
{
private:
UtilClass *util;
}
/* Core.cpp */
#include "UtilClass.h"
Core::Core() { util = new UtilClass; }
사용 하려는 클래스를 .hpp에서 먼저 선언하고 .cpp에서 사용하려는 객체의 .hpp 를 include 하는 방식
전방 선언 하려면 .hpp 에는 객체의 포인터만 사용해야 한다.
이런 제한은 문법적으로 만들어 진게 아니라 프로그램이 실행되는 구조에 의한 것이다.
모든 포인터는 4byte의 메모리를 필요로하므로 우선 메모리만 확보해 두면 runtime에 생성되는 개체의 주소값을 저장할 수 있게 됩니다.
포인터가 아닌 객체의 인스턴스가 사용된다면 컴파일러는 클래스의 구조를 알아야 되므로 include 된 .hpp 파일이 필요하게 된다.
전방선언을 사용해서 얻게 되는 이점은 컴파일 시간 단축이다.
위 구조에서 전방 선언이 아니라 .hpp 를 include했다면 UtilityClass 가 수정될 때마다 컴파일로는 .hpp를 다시 분석하게 된다.
만약 UtilityClass 가 다른 h를 포함하고 Core클래스가 또 다른 곳에서 사용하게 된다면 의존성이 증가되어 컴파일 시간이 점점 늘어나게 된다.
또 전방선언은 A객체가 B를 사용하고 B객체가 A를 사용하는 상호참조
에서도 유용하게 사용 됩니다.
그리고 API개발 등에서 불필요한 .hpp를 포함하지 않을 수 있다는 장점이 있다.
if
overloading
: 함수명을 같지만 파라미터의 개수와 타입을 다른게하여 함수를 만드는 것.
overriding
: 상속간에 함수를 재정의하는 것.
순수 가상함수
: 기반 클래스에서는 정의하지 않으며, 상속 받는 파생 클래스에서 무조건 Overriding(재정의)하도록 하는 함수.
추상 클래스
: 순수가상함수를 하나 이상 포함하는 클래스로 객체를 생성할 수 없다.
인터페이스
: 순수가상함수와 가상 소멸자로 이루어진 클래스이며 구현부가 없다.
추상클래스는 IS - A "~이다".
인터페이스는 HAS - A "~을 할 수 있는".
만약 모든 클래스가 인터페이스를 사용해서 기본 틀을 구성한다면... 공통으로 필요한 기능들도 모든 클래스에서 오버라이딩 하여 재정의 해야하는 번거로움이 있습니다.
이렇게 공통된 기능이 필요하다면 추상클래스를 이용해서 일반 메서드를 작성하여 자식 클래스에서 사용할 수 있도록 하면 된다.
어!? 그러면 그냥 추상클래스만 사용하면 되는 거 아닌가요?
하지만 추상클래스로 다중 상속을 받는 경우 설정이 복잡하기 때문에 인터페이스로 구현하는 것이 편하다.
따라서 만약 각각 다른 추상클래스를 상속하는데 공통된 기능이 필요하다면?
해당 기능을 인터페이스로 작성해서 구현하는게 편하겠죠?
#ifndef AMATERIA_HPP
# define AMATERIA_HPP
# include <iostream>
# include <string>
# include "ICharacter.hpp"
class ICharacter;
class AMateria
{
protected:
std::string type;
public:
AMateria(std::string const &type);
virtual ~AMateria(void) {}
std::string const &getType() const;
virtual AMateria *clone() const = 0;
virtual void use(ICharacter &target) { (void) target;}
};
#endif
#include "ICharacter.hpp"
class Character : //public ICharacter
{
private:
std::string name;
static const int inventory_size = 4;
AMateria *inventory[Character::inventory_size];
int n_of_equip;
Character(void);
public:
Character(std::string name);
Character(const Character &rhs);
~Character();
Character &operator=(const Character &rhs);
std::string const &getName() const;
void equip(int idx);
void use(int idx, ICharacter &target);
};
private 변수로 name과 인벤토리 배열을 가짐.
주의해야할점
equip은 inventory 중 먼저 나오는 empty배열에 Materia 를 저장한다.
unequip은 inventory의 idx번째 Materia 를 NULL로 초기화 해줬다.
이때 delete는 하지 않기 때문에 외부에서 사용시 포인터 주소를 먼저 복사해 뒀다가 delete를 따로 해줘야한다.
use는 AMateria 의 use함수를 사용하고 사용한 Materia를 unequip하는 식으로 구현.
#include <iostream>
#include "IMateriaSource.hpp"
class MateriaSource : public IMateriaSource
{
private:
static const int materias_size = 4;
int n_of_learned;
AMateria *materias[MateriaSource::materias_size];
public:
MateriaSource(void);
MateriaSource(const MateriaSource &rhs)
~MateriaSource(void);
MateriaSource &operator=(const MateriaSource &rhs)
void learnMateria(AMateria *m);
AMateria *createMateria(std::string const &type);
};
In file included from srcs/AMateria.cpp:13:
In file included from incs/AMateria.hpp:18:
incs/ICharacter.hpp:25:21: error: unknown type name 'AMateria'
virtual void equip(AMateria *m) = 0;
AMateria.hpp 와 ICharacter가 서로의 헤더를 include해서 생기는 문제.
위에서 소개한 전방 뭐시기 if방법2 를 쓰면
#ifndef ICHARACTER_CLASS_H
# define ICHARACTER_CLASS_H
# include "AMateria.hpp"
class AMateria;
class ICharacter
{
public:
virtual ~ICharacter() {}
virtual std::string const &getName() const = 0;
virtual void equip(AMateria *m) = 0;
virtual void unequip(int idx) = 0;
virtual void use(int idx, ICharacter &target) = 0;
};
#endif