CPP Module 04: Polymorphism, abstract classes, interfaces

Chaewon Kang·2021년 5월 24일
0

42 seoul

목록 보기
15/17

Concepts

복사 생성자

Type(const Type& a);

다른 Type 객체 a를 상수 레퍼런스로 받는다. 여기서 a가 const 이기 때문에 우리는 복사 생성자 내부에서 a의 데이터를 변경할 수 없고, 초직 새롭게 초기화 되는 인스턴트 변수들에게 '복사'만 할 수 있게 된다.

Type::Type(const Type& a) {
	std:;cout << "복사 생성자 호출" << std::endl;
    
    hp = a.hp;
    shiled = a.shield;
    coord_x = a.coord_x;
    coord_y = a.coord_y;
    damage = a.damage;
}

위와 같이 Type 클래스 내에 정의된 복사생성자의 내부에서 새로 생겨날 a의 인스턴수 변수들에 접근해서 객체의 변수들을 초기화할 수는 있지만,

a.shiled = 3;

이런 식으로 값 자체를 변경할 수는 없다. 왜냐하면 '상수 레퍼런스'로 인자를 받았기 때문이다. 이처럼 인자로 받는 변수의 내용을 함수 내부에서 바꾸지 않는다면, 앞에 const 를 붙여 함수 원형을 선언하는 편이 바람직하다.

주의할 점은, '생성 시에 대입하는 연산', 즉 예를 들어

Type a3 = a;

를 한다면, 복사 생성자가 호출된다. 복사 생성자는 오직, 새로운 인스턴스가 생성될 때에만 호출된다. C++ 컴파일러는 디폴트 복사 생성자를 지원한다.

그러나 디폴트 복사 생성자의 경우에는, 이를테면 이미 원래의 클래스에 동적 할당 메모리 (문자열 같은)가 담긴 멤버 변수가 있었을 경우, 복사를 하려고 했던 원본 인스턴스가 파괴되면서 소멸자를 호출하게 된다면, 복사후 생성된 메모리가 가리키던 멤버 변수의 메모리가 해제되어 버린다.

이를 피하기 위해, 메모리를 '새로 할당'해서 내용을 복사하는 것을 깊은 복사라고 한다. 이와 다르게, 단순히 대입만 해주는 것을 '얕은 복사'라고 한다. 컴파일러가 생성하는 디폴트 복사 생성자의 경우에는 얕은 복사만 할 수 있으므로, 깊은 복사가 필요한 경우 사용자가 직접 복사 생성자를 정의해야 한다.

초기화 리스트

  • 레퍼런스와 상수는 모두 생성과 동시에 초기화되어야 한다. (변경 불가)
  • 만약 클래스 내부에 레퍼런스 변수나 상수를 넣고 싶다면, 생성자에서 무조건 초기화 리스트를사용해서 이니셜라이징 해야 한다.

static 변수

  • 전역 변수 같지만, 클래스 하나에만 종속되는 변수를 static 멤버 변수로 쓸 수 있다.
  • 클래스의 모든 객체들 (인스턴스들)이 공유한다. 각 인스턴스 별로 따로 존재하는 것이 아니라, 모든 객체들이 하나의 static 멤버 변수에 접근하고, 사용할 수 있다.
  • static 멤버 변수의 경우는 객체가 소멸될 때 소멸되는 것이 아니라, 프로그램이 종료될 때 소멸된다.
  • 모든 global 및 static 변수들은 정의와 동시에 값이 자동으로 0으로 초기화 된다. 그러나 클래스 static 변수들의 경우, 아래와 같이 초기화 한다.
int Type::total_number = 0;

보통 어떤 클래스에서 자식 클래스들(인스턴스들)을 만들면서 총 개수를 세어줘야 할 때 자주 쓰는데, 생성자가 호출될때 total_number++, 소멸자가 호출될 때 total_number-- 이런 식으로 처리해 주면 쉽다.

static 함수

  • 클래스 전체에 딱 한 개 존재하는 함수.
  • static 이 아닌 멤버 함수들의 경우, 객체를 만들어야 멤버 함수들을 호출할 수 있다. 하지만, static 함수는 객체가 없어도 클래스 자체에서 호출할 수 있다.
  • 어떠한 객체(인스턴스)도 이 함수를 '소유'하고 있지 않기 때문에, 호출하는 방법도 점 접근법을 쓰지 않고, 그냥 클래스를 네임스페이스로 하여 바로 사용한다.

this

  • 객체 자기 자신을 가리키는 포인터
  • 실제로 모든 멤버 함수 내에 this 키워드가 정의되어 있으며, 클래스 안에서 정의된 함수 중 this 키워드가 '없는' 함수는 static 함수 뿐이다. (누군가의 소유가 아니므로)
  • 어떤 함수의 호출 결과로 '자기 자신'을 가리키는 포인터를 리턴해야 할 때 사용.

const 함수

기존 함수의 정의 const; 형태로 선언

  • 변수들의 값을 바꾸지 않고 그저 읽기만 하는, 상수 멤버 함수
  • 상수 함수 내에서는 객체들의 '읽기' 만이 수행됨 (READONLY)
  • 상수 함수 내에서 호출 할 수 있는 함수는 다른 상수 함수 밖에 없음 (당연하게도!)

많은 경우 클래스를 설계할 때, 멤버 변수들은 모두 private에 넣고, 이 변수들의 값에 접근하는 방법으로 사용하는 함수들을 public에 넣어 이 함수들을 사용해 값을 리턴하는 방식을 많이 사용한다.

이런 식으로 외부에서 멤버 변수에 접근하는 것을 막고, 그 값은 자유롭게 구할 수 있도록 할 때 const 함수를 많이 사용한다.

오버로딩

  • 같은 이름의 함수를 인자만 다르게 사용할 때: 함수를 오버로딩
  • 기본적으로 제공하는 연산자를 직접 사용하려고 할 때
(Return type) operator(operator) (parameter of operator)

이를테면 비교 연산자 ==를 오버로딩 하고자 한다면,

bool operator==(MyString& str);

라는 식으로 쓴다.
연산자를 오버로딩해서, 특정한 타입의 인자들이 들어왔을 때 어떤 걸 해줄건지를 함수 블럭 내부에 정의하면 됨.

friend 키워드

  • 클래스 내부에서 다른 클래스나 함수들을 friend로 정의할 수 있다.
  • friend 로 정의된 클래스나 함수들은 원래 클래스의 private로 정의된 변수나 함수들에 접근할 수 있다.
  • 하지만 우리 subject에서는 사용하지 말라고 했다. 치사하다...
  • friend 키워드는 해당 함수나 클래스에게 자기 자신의 모든 private 멤버 함수와 변수들을 공개하기 때문이지요.

입출력 연산자 오버로딩

  • 클래스의 연산자 함수를 추가하는 방법으로, 멤버 함수 말고도 전역 함수를 정의할 수 있다.
  • ostream 클래스 객체 (iostream내부의 ostream 클래스)와, 내가 정의한 객체를 인자로 받는 전역 operator<< 함수를 정의하면 된다.
// cout으로 Sorcerer 객체를 출력하기위해 <<연산자 오버로딩
std::ostream &operator<<(std::ostream &os, const Sorcerer &ref)
{
	os << "I am " << ref.getName() << ", " << ref.getTitle() << ", and I like ponies!\n";
	return (os);
}

위 함수를 해석하자면, std 네임스페이스 내에 정의된 ostream 클래스 (std::ostream) 에 대한 참조자 타입 을 리턴하는, 연산자 <<에 대한 연산자 오버로딩 함수는, std::ostream 클래스에 대한 참조자 타입의 os 변수와, 우리가 정의한 Sorcerer클래스에 대한 상수 타입의 참조자 변수 ref를 인자로 받는다.

이 함수를 실행하면, os 객체에 문자열 "I am"을 출력하고, ref 인스턴스의 getName() 함수로 이름을 가져온 후 출력하고, 다시 문자열 ", "을 출력하고, ref 인스턴스의 getTitle() 함수로 타이틀을 가져온 후 출력하고, 나머지 문자열 ", and I like ponies!\n"을 출력한다. 그리고 os를 리턴한다.

상속 (Inheritance)

  • 다른 클래스의 내용을 그대로 포함할 수 있는 기능을 함
  • 기반이 되는 클래스를 '기반 클래스', 여기서 뻗어나온 클래스를 '파생 클래스'라고 함
  • 기반은 여러개일 수 있음
  • 파생 클래스 생성자를 사용할 때는, 기반 클래스의 생성자를 호출해서 기반 클래스의 초기화를 먼저 처리한 뒤, 파생 클래스의의 초기화를 처리한다.
  • 기반 클래스의 생성자를 명시적으로 호출하지 않을 경우 디폴트 생성자가 호출된다.

protected 키워드

  • private 멤버 변수들은 그 어떤 경우에도 본인 클래스가 아닌 경우는 접근할 수 없다.
  • protected 키워드는 public과 private의 중간 위치에 있는 접근 지시자.
  • 상속받는 클래스 (파생 클래스)에서는 접근 가능하고, 그 외의 기타 경우에는 접근할 수 없다.

virtual 키워드

  • 컴파일 타임에 어떤 함수가 호출될 지 정해지는 것을 정적 바인딩(static binding)이라고 한다.
  • 컴파일 시에 어떤 함수가 실행될 지 정해지는 게 아니라, 런타임 시에 정해지는 일을 동적 바인딩(dynamic binding)이라고 한다.
  • virtual 키워드는 이를 실행시켜, 런타임 시 컴퓨터에게 어떤 대상과 바인딩하여 함수를 실행할지 지시해 준다.

가상 함수

  • virtual 키워드가 붙은 함수
  • 파생 클래스의 함수가 기반 클래스의 함수를 오버라이드 해 줄 때, 동적 바인딩을 하여 업캐스팅/다운캐스팅을 자유로이 할 수 있음
  • 가상 함수를 사용하기 위해서는 두 함수의 꼴이 정확히 같아야 함 (앞에 키워드의 유무로 판별)

polymorphism (다형성)

  • 하나의 메서드를 호출했음에도, 여러가지 상황을 고려하여 다른 작들을 하게 됨

순수 가상함수 (pure virtual function), 추상 클래스 (abstract class)

  • 함수의 정의는 이뤄지지 않음. 함수 선언만 함.
  • 어떤 클래스가 순수 가상 함수를 갖는다면, 이를 '추상 클래스(abstract class)'라고 부른다.
  • 추상 클래스는 객체로 만들어지지 못하고, 상속할 때만 기반으로 사용할 수 있다.
  • 추상 클래스를 상속받은 자식 클래스는 무조건 해당 순수 가상 함수를 오버라이딩 해야 한다.

추상 클래스의 장점

  • 순수 가상 함수를 포함하여, 파생 객체들을 무조건 오버라이드 해주게 되어있다.
  • 모든 클래스에서 해당 멤버 변수/함수들을 재정의해야 하기 때문에 빼먹지않고 기능들을 정의해 줄 수 있음.
  • 추상 클래스를 정의할 때는 사실 멤버 변수없이 순수하게 순수 가상함수로만 이루어진 클래스가 좋다. 그냥 붕어빵 틀의 '틀'로서만 기능하도록. (온전하게). 붕어빵 틀은 그것만 가지고는 아무 의미가 없는 것과 마찬가지다.
  • 이렇게 순수 가상 함수로만 이뤄진 추상 클래스를 인터페이스라고 부른다.

다중 상속

  • 순수 가상 함수로만 이뤄진 추상 클래스를 기능에 맞게 효율적으로 사용하는 방법.
  • 인터페이스로만 사용할 추상 클래스를 하나 정의해 놓고, 그 외에 implementation한 클래스들을 만들어서 다중 상속하여 사용한다.

ex00

Sorcerer class는
1. name, title을 갖습니다.
2. 파라미터로 이름, 타이틀을 받는 생성자를 포함합니다.
3. 파라미터 없이는 instanciate 될 수 없습니다. (인스턴스화)
4. 그러나 여전히 Coplien 형태로 작성되어야 합니다.

Sorcerer이 태어날 때: NAME, TITLE, is born! 이 출력되어야 하고요.
Sorcerer이 죽을 때: NAME, TITLE, is dead. Consequnces will never be the same! 이 출력되어야 합니다.
Sorcerer이 자신을 소개할 때: I am NAME, TITLE, and I like ponies! 가 출력 되어야 합니다. << 연산자 오버로딩을 통해서, Sorcerer은 자신을 소개할 수 있게 될 것입니다.

우리의 Sorcerer은 이제 피해자들을 필요로 합니다. Victim 클래스를 만듭시다. Sorcerer이랑 비슷하게, 이름을 갖고, 생성자는 이름을 인자로 받습니다.

Victim이 태어날 때: Some random victim called NAME just appeard!
Victim이 죽을 때: Victim NAME just died for no apparent reason!
Victim 또한 자신을 소개할 수 있어야 합니다: I'm NAME and I like otters!

우리의 Victim은 Sorcerer에 의해 "polymorphed" 될 수 있습니다. void getPolymorphed() const 메서드를 Victim에 추가하세요.: NAME has been turned into a cute little sheep!

그리고, void polymorph(Victim const &) const 멤버 함수도 Sorcerer에 추가하세요. 사람들을 polymorph할 수 있도록요.

이제, 우리의 Sorcerer이 Victim 말고 다른 것들도 polymorph할 수 있게 변주를 해봅시다. Peon 클래스를 만드세요. Peon 클래스가 태어날 때, "Zog Zog"하고 운답니다. 죽을 때는 "Bleuark..."라고 말하고 죽어요. Peon 클래스는 다음과 같이 polymorphed 됩니다: NAME has been turned into a pink pony!"

주어진 main 함수를 컴파일 하세요. 그리고 주어진 아웃풋이 출력되도록 해 보세요.

파생 클래스도 추가해 보시고요...

ex01

무기를 만듭시다. (코플리엔 형식을 따라서...)
무기는 이름을 갖고, 공격에 의한 데미지 포인트를 갖고, AP라는 슈팅 비용을 가집니다. 무기는 특정 소리와 빛 효과를 발생시켜요. attack() 함수를 사용할 때 말이죠. 상속받는 클래스에 따라 달라지게 될 거에요.

이후에, PlasmaRifle 과 PowerFist라는 클래스들을 만들어요.
섭젝에 나와 있는 형태로요. 가지고 놀 많은 무기들이 생겼으니 적과 싸워 봅시다. Enemy 클래스를 만들어요. 마찬가지로 섭젝에서 주어진 대로요.

몇 가지 제한이 있습니다...
1. 적은 hit points, type을 가져요.
2. 데미지를 입으면 HP가 줄어요. 만약 데미지가 0보다 작을 경우 아무 일도 일어나지 않아요. 0 HP 이하로 가면 안됩니다.

몇개의 적을 만들어 보셔요:
SuperMutant, RadScorpion... (주어진 대로요.)

우리는 이제 무기도 만들었고, 무기로 싸울 적들도 만들었고... 이제 싸울 준비만 되면 되겠죠. Character이라는 클래스를 만듭시다. 주어진 대로요.

이름, AP (Action Points), 그리고 현재 사용하는 무기를 가리키는 Aweapon 포인터를 가져요. 태어날 때 40 AP를 갖고, 각각의 상황에서 사용하는 무기에 따라 AP를 잃게 되고, recoverAP() 함수를 호출할 떄마다 10 AP씩 회복하게 될거에요.

문제에서 요구하는 대로 출력하세요: NAME attacks ENEMY_TYPE with a WEAPON_NAME 을요. attack() 함수를 호출할 때요. 이 함수는 지금 갖고 있는 무기의 attack() 메서드에 후행되어야 하겠죠. 만약 장착한 무기가 없다면 attack() 은 아무것도 하면 안돼요. 공격당한 적의 HP를 무기의 데미지 값만큼 빼고, 타겟의 HP가 0이 되면 타겟을 제거하세요.

equip() 함수 또한 무기에 대한 포인터를 저장합니다. 여기에 복사는 개입되지 않아요.

그리고 << 연산자 오버로딩을 해서, Character 클래스의 속성들을 출력하세요. 모든 필요한 getter 함수들을 넣으셔요: NAME has AP_NUMBER AP and wields a WEAPON_NAME을 출력할 것이고, 만약 무기가 장착되었다면: NAME has AP_NUMBER AP and is unarmed을 출력하게 될 겁니다.

주어진 main문을 컴파일 하고 실행시켜서, 주어진 결과가 나오는지 확인하세요.

ex02

이제, Squad 와 TacticalMarine 이라는 미래의 부대를 만듭시다.
ISquad 클래스는...
부대 안에 있는 유닛들이 몇개인지 반환하는 getCount() 함수와,
N번째 유닛에 대한 포인터를 반환하는 getUnit(N)함수,
XXX 유닛을 부대의 끝에 추가하는 push(XXX)함수를 가질 거고, 이 함수는 부대 안에 몇 개의 유닛들이 있는지 반환해요. null 유닛이나, 이미 스쿼드 안에 있는 유닛을 추가하는 건 말이 안되겠죠.

Squad는 Space Marine의 간단한 컨테이너에요. 당신의 군대를 조직하기 위해 필요한 거죠. Squad에 대한 복사 생성이나 대입에 있어서, 이 복사는 필히 깊은 복사여야 해요. 대입에 있어서는, 부대 안에 유닛이 존재한다면, 그 유닛들은 필수로 파괴되어야 해요. (대체되기 전에요.) 모든 유닛들은 new 키워드와 함께 동적 할당 되어야 한다는 것을 알겠죠?

Squad가 파괴되면, 안에 있는 유닛들도 순서대로 파괴되어야 해요.

TacticalMarine 클래스는...

  • 현재 객체의 복사본을 반환하는 clone() 함수
  • 생성될 때, "Tactical Marine ready for battle!"을 출력
  • battleCry() 함수는 "For the holy PLOT!"을 출력
  • rangedAttack() 함수는 "* attacks with a bolter *" 을 출력
  • meleeAttack() 함수는 "* attacks with a chainsword *"을 출력
  • 죽을 때, "Aaargh..." 출력

거의 비슷하게, AssaultTerminator을 만들고, 아래의 내용을 반영하세요

  • 태어날 때: "* teleports from space *"
  • battleCry() : "This code is unclean. PURIFY IT!"
  • rangedAttack : "* does nothing *"
  • meleeAttack : "* attacks with chainfists *"
  • 죽을 때 : "I'll be back..."

주어진 main을 컴파일 및 실행해 보고 주어진 출력 결과가 나오는지 확인하세요.

ex03

주어진 AMateria 클래스를 완성해 봅시다...
AMateria의 XP 시스템은 아래와 같이 동작해요:

  • 0부터 시작하고, use()가 호출될 때마다 10씩 증가합니다. 이를 핸들링하는 똑똑한 방법을 찾아보세요!
  • Ice, Cure 이라는 concrete Materias를 만드세요. (Materia로 채워져 있어요.) 타입은 이름을 소문자로 쓴 형태입니다.
  • clone () 메서드는 진짜 Materia 타입에 대한 새로운 인스턴스를 반환합니다.
  • use(ICharacter&) 메서드를 쓸 때는:
    Ice: "* shoots an ice bold at NAME"
    Cure: "* heals NAME's wounds
    "
    당연히, NAME 부분은 파라미터로 주어진 Character 클래스의 이름으로 대체해야 겠죠?

주의점은, Materia를 다른 것에 대입할 때, 복사되는 것은 말이 안된다는 거죠.

  • Character 클래스를 만드세요.
  • Character 클래스는 Materia를 최대 4개까지 담을 수 있는 인벤토리를 가지고 있고, 이 인벤토리는 비어 있는 채로 시작합니다. 0부터 3까지 순서대로 Materia를 장착할 수 있습니다.
  • 인벤토리가 꽉 차 있을 때 Materia를 하나 더 장착하려 하거나, 없는 Materia를 사용/해제하기 위해 접근하면 아무 동작도 하지 않습니다.
  • unequip()은 Materia를 삭제하는 것이 아니에요.
  • use(int, ICharacter&) 함수는 idx번 째 Materia를 사용하고, target에 AMeteria::use 함수를 전달해야 합니다.
  • 물론 Character 인스턴스 내의 인벤토리 내부에 있는 모든 AMateria에 해당해야 하죠.
  • Character 의 생성자는 name을 인자로 받아야 하고, 복사생성자/대입연산자는 깊은 복사여야 합니다. Character 내의 오래된 Materia 또한 삭제되어야 하고, Character의 소멸자 역시 마찬가지입니다.

이제 Character 인스턴스는 Materia를 장착할 수 있습니다.
Materia를 직접 만드는 건 귀찮으니까, Materia의 오리지널 소스를 만들어 보세요. MateriaSource 클래스는 다음과 같습니다.

  • learnMateria는 복사된 Materia를 인자로 받아 복사한 후 메모리에 저장합니다.
  • createMateria(std:;string const &)은 새로 생성한 Materia를 반환합니다. Materia는 매개변수와 같은 타입으로 복사되고, type이 unknown이면 0을 반환합니다.
  • 간단히 말해, Source 는 Materia의 템플릿을 학습하고 필요에 따라 다시 만들 수 있어야 합니다. 그 다음 Mateira를 real type으로 구분하지 않고 문자열로 판단합니다.
profile
문학적 상상력과 기술적 가능성

0개의 댓글