Chapter 0
Subtype polymorphism
서브타입 다형성이라는 것을 알아보기 전에, 다형성에 대해서 먼저 알아보자.
다형성이란, 하나의 객체가 여러가지 타입을 가질 수 있는 것을 의미한다. 객체 지향 프로그래밍의 구성에서 저번 과제때 배웠던 상속과 더불어 중요하게 작용하는 특징 중 하나이다. 다형성 중 부모 클래스의 포인터 또는 참조형을 통해 자식 클래스를 사용하는 것을 의미하는 경우가 서브타입 다형성이다.
C++에서의 다형성은 지난 과제들에서도 사용했던 가상함수, 연산자 오버로딩, 앞으로의 과제에서 사용 할 템플릿 등이 있다.
- 가상함수이 과정을 통해 같은 이름을 함수지만 내용이 다른 함수를 호출이 가능해진다.또한, 클래스를 상속할 경우, 소멸자에 virtual을 붙여주어야 깔끔하게 상속을 받을 수 있다. 소멸자도 함수이기 때문에, 소멸자를 가상함수로 설정해주지 않는다면 부모 클래스에서 소멸자를 호출하며 자식 클래스는 할당된 상태로 남아있을 수 있기 때문에 필수로 지정해주어야 한다. 그리고 이렇게 해 주어야 생성은 (부모 -> 자식 순)으로 생성자가 호출되고, 소멸은 (자식 -> 부모 순)으로 소멸자가 호출된다.
- virtual 키워드를 붙이는 것으로 가능해지는 이유는, 이 키워드를 통해 컴파일 시 어떤 함수가 호출될 지 정해지는 '정적 바인딩' 대신 런타임 시 사용하는 함수를 호출하는 '동적 바인딩'을 실행하게 한다.
- 지난 과제에서, 자식 클래스를 상속 해준 뒤, 자식 클래스에서 부모 클래스의 함수를 재정의하기 위해 가상함수를 사용했던 적이 있다. 이 과정을 오버 라이딩이라고 한다.
다형성은 다음과 같은 종류들이 있다.
- 서브타입 다형성 (런타임 다형성)
- 매개 변수 다형성 (컴파일 타임 다형성)
- 임시 다형성 (오버로딩)
- 강제 다형성 (캐스팅)
서브타입 다형성
서브타입 다형성은 주로 사람들이 C++에서 "다형성"이라는 것을 이야기 할 때 이해하고 있는 의미의 다형성이다. 부모 클래스의 포인터 또는 레퍼런스를 통해 자식 클래스를 사용하는 기능을 말하며, 런타임 다형성이라고도 한다.
다형성 함수의 호출 결정은 런타임에 가상 테이블을 통한 간접 참조를 통해 일어나게 된다. 즉, 컴파일러가 컴파일 타임 때 호출될 함수의 주소를 찾는 것이 아닌 프로그램 실행 시 가상 테이블에 있는 오른쪽 포인터를 역참조해 함수를 출력하는 것이다.
매개 변수 다형성
매개 변수 다형성은 어떤 타입에 대해 동일한 코드를 실행하기 위한 수단을 제공해준다. C++에서는 템플릿을 통해 매개변수 다형성을 구현할 수 있다.
매개 변수 다형서은 컴파일 시 일어나기 때문에, 컴파일 타임 다형성이라고도 한다.
임시 다형성
임시 다형성은 같은 이름을 가진 함수가 각 타입에 따라 다르게 행동할 수 있도록 해주며, 오버로딩이라고도 부른다.
오버로딩은 같은 이름을 가진 함수가 각 타입에 따라 다르게 행동할 수 있도록 도와준다.
강제 다형성
강제 다형성은 객체 또는 기본 타입이 다른 객체 또는 기본 타입으로 변환할 때 일어난다.
아래의 예시를 보자
float b = 6; // int는 (암시적으로) float으로 승격된다.
int a = 9.99; // float은 (암시적으로) int로 강등된다.
명시적 캐스팅은 컴파일 단계에서 암묵적으로 실행되는 형변환으로 인해 발생되는 오류를 억제하기 위해 사용한다. 하지만 컴파일러의 오류를 발생시키지 않는 장점이 있으면서 개발자의 실수에 취약해지는 단점도 존재한다.
C형식의 형변환에서는 괄호를 통해 형변환을 하였고, 타인이 그 형변환에 대해 명확하게 의미를 알 수 없었다는 단점이 있었다.
그런 단점을 보완하기 위해서 C++에서는 4가지의 형변환을 제공하고 있다.
- static_cast : 가장 흔한, 언어적 차원에서 지원하는 일반적인 형변환
- const_cast : 객체의 const를 없애주는 형변환. (const int -> int)
- dynamic_cast : 자식 클래스 사이에서의 다운 캐스팅
- reinterpret_cast : 위험을 감수하고 하는 형변환으로, 서로 관련이 없는 포인터들 사이의 형변환
abstract classes
- 순수 가상 함수이것과 다르게 순수 가상 함수는 자식 클래스에서 무조건 재정의를 해야하는 멤버 함수를 의미한다.
부모 클래스에서 virtual로 가상함수를 설정할 때, 함수의 선언만 존재하고 구현하지 않은 함수에 대해 virtual을 추가해주면 순수 가상 함수가 된다. virtual memberFunction = 0;
- 따라서 순수 가상 함수는 다음과 같은 형태로 선언한다.
- C++애서 가상 함수는 자식 클래스에서 재정의 할 것으로 기대하는 멤버 함수를 의미한다. 기대라는 말의 의미는 반드시 재정의를 해야하는 것이 아닌, 재정의가 가능한 함수를 말한다.
이러한 순수 가상 함수를 하나라도 포함한 클래스를 추상 클래스라고 한다. 추상 클래스는 객체 지향 프로그래밍에서 중요한 특징인 다형성을 가진 함수의 집합을 정의할 수 있게 해준다.
즉, 반드시 사용되어야 하는 멤버 함수를 추상 클래스에 순수 가상 함수로 선언한다면, 이 클래스를 상속받는 자식 클래스에서 이 가상 함수를 반드시 재정의해야 한다.
추상 클래스는 동작이 정의되지 않은 순수 가상 함수를 포함하고 있어서 인스턴스를 생성할 수 없다.
따라서 추상클래스는 먼저 상속을 통해 자식 클래스를 만들어주고, 자식 클래스에서 순수 가상 함수를 모두 오버라이딩 하고 난 뒤에 부모 클래스의 인스턴스를 생성할 수 있게 된다.
하지만 추상 클래스 타입의 포인터와 참조는 바로 사용할 수 있다.
C++에서 추상 클래스는 다음의 용도로는 사용할 수 없다.
- 변수 또는 멤버 변수
- 함수에 전달되는 파라미터
- 함수의 반환 타입
- 명시적 캐스팅 타입
interfaces
인터페이스는 소멸자와 순수 가상 함수만 선언된 클래스이다. C++에서는 인터페이스 형식을 제공하지는 않지만 순수 가상 함수를 통해 정의할 수 있다.
인터페이스는 멤버 변수나 구체적으로 구현한 함수들을 갖지 않고 특정 기능을 약속한 멤버 함수만 갖는다. 그리고 모든 멤버는 public으로 접근할 수 있도록 설정해야한다.
Chapter 1
Introduction
이 과제는 내가 걸어가야할 C++이라는 여행길에 시작점에 객체지향프로그래밍이 어떤건지를 설명해주는것이 목적이라고 한다.
객체지향은 많은 언어들이 사용하는데, 왜 C++을 배우게 되느냐.. 하면 나의 오랜 친구(42에서만)인 C에서 파생된 언어이기 때문이라고 한다.
C++은 복잡한 언어이기 때문에, 단순하게 유지하기 위해 내 코드를 C++ 98 standard로 컴파일 할것이라고 한다.
Chapter 2
General Rules
컴파일
- 너의 C++ 코드는 "-Wall -Wextra -Werror" 플래그와 함께 컴파일 한다.
- 너의 코드는 "-std=c++98"를 플래그로 추가하고도 컴파일이 되어야 한다.
포멧과 네임 컨벤션
- 모든 문제의 디렉토리는 다음의 방식을 따른다 : ex00, ex01, ex02 ... exn
- 너의 파일, 클래스, 함수, 멤버함수, 속성들은 가이드라인의 요구에 맞추어야 한다.
- 클래스 이름은 UpperCamelCase 형식을 사용한다. 클래스를 담은 파일의 이름은 클래스 이름을 따라야 한다.
- 얘를 들어, ClassName.hpp/ClassName.h, ClassName.cpp, ClassName.tpp 와 같은 형식이다. 그렇다면 "BrickWall"이라는 클래스를 담고있는 헤더의 이름은 BrickWall.hpp가 되어야 하는 것이다.
- 달리 명시하지 않았을 경우, 모든 출력 메세지는 개행문자로 끝나야 하며 표준 출력으로 보여주어야 한다.
- Goodbye Norminette! C++ 모듈에서는 코딩 스타일을 강요하지 않는다. 너가 좋아하는 것을 따라 할 수 있다. 하지만 동료평가시, 이해할 수 없는 코드를 사용할 경우, 동료들이 점수를 줄 수 없다는 것을 명심해라. 최선을 다해 클린하고 가독성 있는 코드를 작성하길 바란다.
허용되는 사항 / 금지되는 사항
너는 이제부터 C로 코딩하지 않는다. C++의 시간이다 ! 그러므로:
- 너는 표준 라이브러리에서의 거의 모든 것들을 사용할 수 있다. 따라서, 이미 알고 있는 것을 고수하는 대신, 익숙한 C함수의 C++ 버전을 최대한 많이 사용해보는 것이 현명할 것이다.
- 그러나, 너는 다른 외부 라이브러리를 사용할 수 없다. 이 말의 의미는 C++ 11과 Boost 라이브러리가 금지된다는 것이다. 다음 함수도 금지된다. printf(), alloc() and free(). 이걸 쓰면 너의 점수는 0점이 될 것이다.
- 달리 명시하지 않는 이상, namespace 및 friend 키워드의 사용은 금지된다. 사용하면 -42점이므로 주의하자.
- 오직 모듈 08에서만 STL의 사용이 허용된다. 이 뜻은, 컨테이너(vector, list, map and forth)와 알고리즘(<algorithm> 헤더가 포함된 어떠한 것들)이 허용되지 않는다는 것이다. 이 또한 사용하면 -42점이다.
몇 가지 디자인 요구사항
- 메모리 누수가 발생하는것은 C++도 동일하다. 만약 너가 메모리를 할당(new 키워드 사용으로)하면, 메모리 누수를 반드시 피해야한다.
- 모듈 02부터 08까지는 달리 명시된 경우를 제외하고, 클래스를 Orthodox Canonical Form으로 설계해야만 한다.
- 헤더파일에 선언되지 않은 함수들을 사용하는 경우 0점을 받는다.
- 각 헤더를 다른 헤더와 독립적으로 사용할 수 있어야 한다. 따라서 필요한 모든 종속성을 포함해야한다. 그러나 헤더가드를 사용해 이중으로 포함되는 것은 피해야만 한다. 그렇지 않으면 0점을 받을 것이다.
Read me
- 너는 필요(즉, 코드를 분할하기 위해)에 의해 파일을 추가할 수 있다. 이런 할당은 프로그램에서 확인하지 않으므로, 필수 파일을 제출하는 한 자유롭게 추가하면 된다.
- 때때로 과제의 가이드라인이 짧아보이지만, 명시적으로 작성되지 않은 요구사항들을 example에서 보여줄 수 있다.
- 시작 전에 꼭 모듈의 과제를 읽어야만 한다. 정말이다 !
Chapter 3
Ex00
문제
모든 예제에서, 너는 너가 할 수 있는 가장 완벽한 테스트를 제공해야 한다. 각 클래스의 생성자와 소멸자는 특정한 메세지를 보여주어야 한다. 같은 메세지를 모든 클레스에서 사용하지 마라.
Animal이라는 간단한 부모 클래스를 구현하자. 이건 protected 속성을 하나 가진다.
- std::string type;
Animal을 상속받는 Dog 클래스를 구현해라.
Animal을 상속받는 Cat 클래스를 구현해라.
이 두 자식 클래스는 그들의 이름에 의존하는 타입을 설정해야한다. 그렇다면, Dog 타입은 "Dog"라고 초기화될 것이고, Cat 타입은 "Cat"으로 초기화될 것이다. Animal 클래스의 타입은 빈 타입으로 남거나 너의 선택에 따라 값을 설정할 수도 있다.
모든 동물들은 멤버 함수를 사용할 수 있어야 한다.
makeSound()
이 함수는 적절한 소리를 출력할 것이다. (고양이는 짖지 않는다.)
코드가 실행되는 중에는 Dog와 Cat 클래스의 특정한 소리들이 출력되어야 한다. Animal의 소리가 아니다.
작동 방식을 이해하려면 WrongAnimal 클래스를 상속받는 WrongCat 클래스를 구현해라. 만약 위 코드에서 Animal과 Cat이 잘못된 것들로 대체된다면, WrongCat은 WrongAnimal의 소리를 내야한다.
위에 주어진 것 보다 더 많은 테스트를 구현하고 제출해라.
구현
이번 예제는 다형성에 대해서 알아보라는 문제다. Animal이라는 부모 클래스를 만들어 Dog와 Cat이라는 자식 클래스를 생성한 뒤, 모든 클래스에서 makeSound를 실행했을 때, 각각의 클래스들이 적절한 동물 울음소리를 내는지 확인하면 된다.
먼저 기본적으로 Animal 클래스에 대해 과제가 요구하는 대로 정의해주었다.
class Animal {
protected:
/*
* Animal class has one protected attribute:
* std::string type;
*/
std::string _type;
public:
/*
* A constructor
! Constructor of each class must display specific messages.
*/
Animal();
/*
* A copy constructor
*/
Animal(const Animal& animal);
/*
* A copy assignment operator
*/
Animal& operator=(const Animal& animal);
/*
* A destructor
! Destructor of each class must display specific messages.
*/
virtual ~Animal();
/*
TODO: Every animal must be able to use the member function:
! makeSound()
? It will print an appropriate sound.
*/
virtual void makeSound() const;
std::string getType() const;
};
이런식으로 클래스를 정의해주었다. 그런데 생각해보니 이번 과제에서만 총 5개의 클래스(Animal, Dog, Cat, WrongAnimal, WrongCat)를 정의해주어야 하는데 일일이 수작업으로 클래스를 구성하고 만드는 것이 마냥 귀찮게 느겨졌다. 왜냐면 OCCF를 따라서 만들어야하기 때문에 기본적으로 같은 형식으로 클래스가 만들어 질 것이기 때문이다. 그래서 코드 스니펫에 대한 자료를 찾아 틀을 만들어주었다. 슬랙이나 구글에 검색해보면 다른 분들이 구현해놓은 것들이 있을것이다. 입맛에 맞게 고치면 될 것이다.
이번 과제는 가상함수를 사용했을때와 사용하지 않았을 때의 차이점을 알아보는 문제였다. Animal 클래스의 makeSound 함수를 가상함수로 만들어주면 컴파일 단계에서 함수가 결정되는 것이 아닌 런타임 단계에서 결정이 되기 때문에 자식 클래스에서 구현한 makeSound 함수가 호출이 된다. 하지만 WrongAnimal 클래스와 WrongCat 클래스는 makeSound를 가상함수로 만들어주지 않았는데, 이런 경우 WrongCat에서 함수를 호출해도 WrondAnimal 클래스의 함수가 동작하게 된다.
이런식으로 virtual을 통한 가상함수 선언의 차이가 결과의 차이를 불러온다.
Ex01
문제
각 클래스의 생성자와 소멸자들은 특정한 메세지를 보여주어야 한다.
Brain 클래스를 구현하자. 이 클래스는 ideas라고 불리는 100개의 string 배열을 포함한다. 그런다음, Dog와 Cat 클래스가 Brain* 을 private로 가질 것이다. 생성 시, Dog와 Cat은 new Brain()을 사용하여 그들의 Brain을 생성할 것이다. 소멸 시, 그들의 Brain을 삭제할 것이다.
너의 메인 함수에서는, Animal 객체의 배열을 생성하고 채운다. 반은 Dog 객체, 나머지 반은 Cat객체가 된다. 너의 프로그램 실행이 종료될 때, 이 배열의 루프가 돌면서 모든 Animal을 삭제해준다. 너는 Animals로부터 직접적으로 dogs와 cats를 삭제해야 한다. 적절한 소멸자는 예상 순서대로 호출되어야 한다.
메모리 누수확인을 잊지 말자.
Dog 또는 Cat의 복사는 얕으면 안된다. 따라서 너의 복사본들은 깊은 복사로 테스트되어야 한다.
구현
이 문제는 객체 내부에 새로운 객체를 멤버 변수로 받아 배열을 만들어보는 문제이다. Brain 클래스를 만드는 것은 간단하니 넘어가고, Dog와 Cat의 생성자와 소멸자를 통해 Brain 클래스를 생성해주고 소멸시켜주어야 한다.
Dog::Dog() : Animal() {
std::cout << std::setw(15) << "[Dog] " << "create!!" << std::endl;
this->_type = "Dog";
this->brain = new Brain();
}
Dog::~Dog() {
delete this->brain;
std::cout << std::setw(15) << "[Dog] " << "delete!!" << std::endl;
}
또한, Dog와 Cat의 연산자 오버로딩을 깊은 복사를 통해 구현해야 한다. Dog와 Cat은 멤버 변수로 Brain 클래스를 포인터로 가지고 있기 때문에 얕은 복사를 수행하게 되면 문제가 발생하게 된다.
기본적으로 '=' 연산자는 얕은 복사를 수행한다. 그래서 OCCF에 의해 정의되지 않은 기본 복사생성자와 '=' 연산자는 컴파일러에 의해 단순 대입만을 시도하게 된다. 따라서 이럴 때 포인터를 변수로 가지고 있는 클래스의 경우, 기존 포인터가 참조하던 주소를 대입하는 포인터의 주소를 참조하게 되면서, 기존에 참조하던 주소는 매모리 누수가 발생하게 된다. 만약 얕은 복사를 수행하고 delete를 하게 되면 double free로 인해 오류가 발생할 것이다.
따라서, 포인터를 멤버 변수로 가지고 있는 클래스의 경우에는 복사 생성자와 연산자 오버로드와 같은 포인터 복사가 이루어지는 함수들을 직접 정의해서 메모리 누수를 방지해주어야 한다.
Dog::Dog(const Dog& dog) {
this->_type = dog.getType();
this->brain = new Brain(*dog.getBrain());
std::cout << std::setw(15) << "[Dog] " << "copy!!" << std::endl;
}
Dog& Dog::operator=(const Dog& dog) {
std::cout << std::setw(15) << "[Dog] " << "operator=!!" << std::endl;
if (this != &dog) {
this->_type = dog.getType();
this->brain = new Brain(*dog.getBrain());
}
return *this;
}
그리고 Brain 클래스가 가지고 있는 ideas 배열은 아무래도 다양한 생각이 들어있으면 좋겠다고 판단이 되어서 A ~ Z 까지의 알파벳을 순차적으로 넣어주었다.
Brain::Brain() {
for (int i = 0; i < 100; i++) {
this->ideas[i] = 'A' + (i % 26);
}
std::cout << std::setw(15) << "[Brain] " << "create!!" << std::endl;
}
Ex02
문제
결국 Animal 객체들을 만드는 것은 의미가 없다. 사실, 그들은 소리가 나지 않는다.
실수들을 가능한 피하기 위해서, 기본 Animal 클래스는 인스턴트화 할 수 없어야 한다. Animal 클래스를 아무도 인스턴스할 수 없도록 수정해라. 모든것은 그 전처럼 동작해야한다.
만약 너가 원한다면, Animal에 A 접두사를 추가한 이름으로 업데이트 할 수 있다.
구현
Animal 객체에서는 소리가 나지 않아야 하기 때문에, Animal 클래스에서 makeSound 함수를 순수 가상 함수로 만들어주어야 할 필요가 있다. 순수 가상 함수는 클래스 내에서 virtual 키워드를 통해 가상함수로 명시한 함수들 중에 0이 할당된 함수를 의미한다. 0을 할당함으로써 이 함수를 정의하지 않겠다는 것을 의미한다.
virtual void makeSound() const = 0;
함수를 정의하지 않는다는 점에서 알아야 할 점은 순수 가상 함수를 포함한 클래스는 자식 클래스에서의 생성을 제외하고 직접 생성할 수 없다는 점이 있다. 또, 자식 클래스에서는 무조건 순수 가상 함수를 오버라이딩으로 정의해주어야 한다. 그렇지 않으면 자식 클래스 또한 순수 가상함수를 포함한 클래스가 되어 생성할 수 없기 때문이다.
Ex03
문제
인터페이스는 C++98에는 존재하지 않는다.(C++20에도 없다) 그러나, 순수한 추상 클래스들은 일반적으로 인터페이스라고 부른다. 그렇다면, 이 마지막 예제에서는, 이번 과제를 확실히 얻었는지 확인하기 위해 인터페이스를 구현해보자.
다음 AMateria 클래스의 정의를 완료하고, 필요한 멤버 함수를 구현해라
class AMateria
{
protected:
[...]
public:
AMateria(std::string const & type);
[...]
std::string const & getType() const; //Returns the materia type
virtual AMateria* clone() const = 0;
virtual void use(ICharacter& target);
};
Materia들의 구체적인 클래스들인 Ice와 Cure를 구현하자. 소문자 이름을 사용(Ice는 'ice'로)해서 그들의 타입을 설정한다. 물론, 그들의 멤버 함수인 clone()은 동일한 유형의 새 인스턴스를 반환한다.(즉, Ice Materia를 복제하면, 새 Ice Materia를 얻게 된다)
use(ICharacter&) 멤버 함수는 아래와 같이 표시할 것이다.
- Ice: "* shoots an ice bolt at <name> *"
- Cure: "* heals <name>'s wounds *"
<name>은 매개변수로 전달된 캐릭터의 이름이다. <, >은 출력하지 않는다.
Materia를 다른 것에 할당하는 동안, 타입을 복사하는 것은 의미가 앖디.
아래의 인터페이스를 구현할 구체적인 클래스 Character를 작성해라.
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;
};
Character는 최대 4개의 Materia를 의미하는 4개의 슬롯 인벤토리를 가진다. 생성자에서 인벤토리는 비어있다. Character들은 그들이 찾은 첫 빈 슬롯에 Materia을 장착한다. 즉, 슬롯 0부터 슬롯 3까지 순서대로이다. 인벤토리가 가득 찬 상태에서 Materia를 추가하려 할 경우, 존재하지 않는 Materia를 사용/장착해제 하는 경우, 아무것도 하지 않아야한다.(여전히, 버그는 금지된다.) unequip() 멤버 함수는 Materia를 삭제하지 않아야 한다.
캐릭터가 바닥에 남겨둔 Materia는 마음대로 처리해라.
unequip() 또는 다른 것을 호출하기 전에 주소를 저장하되, 메모리 누수를 피해야 한다는 점을 잊지 말아라.
**use(int, ICharacter&) 멤버 함수는 slot[idx]에서 Materia를 사용해야 하며, 대상 매개변수를 AMateria::use에 전달해야 한다.
Character는 이름을 매개변수로 사용하는 생성자가 있어야 한다. 모든 복사본(복사 생성자 또는 복사 할당 연산자)은 깊은 복사를 해야한다. 복사하는 동안, Character의 Materias는 인벤토리에 새 재료를 추가하기 전에 삭제해야 한다. 물론, Character 소멸 시 Materias는 삭제되어야 한다.
아래의 인터페이스를 구현할 구체적인 클래스 MateriaSource를 작성해라
class IMateriaSource
{
public:
virtual ~IMateriaSource() {}
virtual void learnMateria(AMateria*) = 0;
virtual AMateria* createMateria(std::string const & type) = 0;
};
- learnMateria(AMateria*)
매개변수로 전달된 Materia를 복사하고 나중에 복제할 수 있도록 메모리에 저장해라. Character와 마찬가지로, MateriaSource는 최대 4개의 Materia를 알 수 있다. 그들은 반드시 고유하지는 않다. - createMateria(std::string const &)
새로운 Materia를 반환한다. 후자는 유형이 매개변수로 전달된 것과 동일한 MateriaSource에 의해 이전에 학습된 Materia의 복사본이다. 타입을 알 수 없는 경우 0을 반환한다.
간단히 말해서, MateriaSource는 필요할 때 Materias를 생성하기 위해 Materias의 "템플릿"을 학습할 수 있어야 한다. 그런 다음, 해당 유형을 식별하는 문자열만 사용해서 새 Materia를 생성할 수 있다.
구현
이 예제는 대충 간단히 알려주기로 자자한 42 과제에서 매우 구체적으로 설명해주는 축에 속할 정도로 많은 구현을 해야한다. 그 구현은 이번 모듈 과제를 통해 다형성과 추상 클래스를 잘 이해했냐고 물어봄과 동시에 더 나아가 인터페이스에 대해서도 알아보라는 의미로 생각했다.
이 예제에 대한 설명을 자세하게 적는것이 독자들이 보기에 지금 당장은 과제에 대한 힌트를 알아갈 수 있어서 좋을지 몰라도, 향후 C++ 과제들을 하면서 필수적으로 알고 넘어가야할 개념들에 대해 고민할 시간을 없애는 것이 아닐까 하는 생각이 들었다. 따라서 구체적인 설명은 하지 않고, 각각의 클래스에서 알고 넘어가야할 개념들을 정리해보았다.
먼저, AMateria 클래스이다. 이 클래스는 Ice 혹은 Cure 클래스를 상속해주는 클래스이다. 이 클래스를 상속받은 Ice, Cure 클래스에서 순수 가상 함수인 clone 을 정의해주어야 하고, use 또한 과제의 요구 사항에 따라 재정의해주어야 한다. 위에서 추상 클래스로 정의한 Animal과 Dog, Cat이 기억 날 것이다. AMateria 클래스와 Ice, Cure 클래스를 이와 동일하게 구현해주면 된다.
다음은 Character 클래스이다. 이 클래스는 인터페이스인 ICharacter 클래스를 상속받는다. ICharater 클래스는 순수 가상 함수들로 이루어져 있어, Character 클래스에서 재정의가 필요하다. 과제의 요구사항대로 함수들을 정의해주면 된다. 복사 생성자 혹은 할당 연산자 오버로딩을 구현할 때, 깊은 복사를 해야하고, 어떤 한 슬롯에 이미 할당되어있는 것이 있다면 먼저 할당을 해제해준 뒤 복사를 하는 것을 잊지말자.
마지막으로 MateriaSource 클래스이다. 이 클래스 또한 인터페이스인 IMateriaSource 클래스를 상속받는다. 멤버 함수들을 구현할 때, 과제 요구사항에 맞게 구현해주면 된다.
느낀 점
이번 과제에서는 저번 모듈 과제에서 배웠던 상속을 조금 더 유의미하게 사용할 수 있도록 해주는 다형성, 인터페이스, 추상클래스와 같은 개념들에 대해 배울 수 있었다.
또한, 깊은 복사를 사용해야하는 이유에 대해 다시한번 알아갈 수 있었고, 처음에는 낯설게만 느껴졌던 OCCF에 대해서도 구현의 필요성을 점차 느끼고 있다. C++ 모듈 과제는 확실히 다른 과제들과 비교했을 때 더 많이 알아갈 수 있게 만들어주는 것 같다.
- 추가
평가를 받아보았는데, 마지막 문제에서 메모리 누수가 났었다. 할당을 해주는 부분들이 많았는데, equip에 4개 이상의 AMateria가 들어오게 된다면, 그 순간부터는 메모리 누수가 났었고, 조금 더 세밀하게 코드를 작성하지 못해서 벌어진 일이었다.
'42SEOUL > Circle4' 카테고리의 다른 글
[42SEOUL] Netpractice (1) | 2023.02.20 |
---|---|
[42SEOUL] miniRT (0) | 2023.02.18 |
[42SEOUL] CPP Module 03 (0) | 2023.02.18 |
[42SEOUL] CPP Module 02 (0) | 2023.02.18 |
[42SEOUL] CPP Module 01 (0) | 2023.02.18 |