본문 바로가기

프로그래밍

[42서울] CPP Module 03 - 클래스 상속

서론

후술하겠지만 ex03에서 잘못 이해하고 구현한 부분이 있다. 그런데 테스트를 해보니 나와 평가자 둘 다 잘못 알고 있어서 한바탕 웃고 넘어갔지만, 굉장히 부끄러운 순간이었다. '내가 평가자였으면 어땟을까...' 하고 곰곰이 생각을 해보니 나는 fail 의 기준을 '평가표의 내용을 모르고 구현하지 않거나 다르게 구현한 경우'와 '잘못 알고 있으면 구현하지 못하는데 구현한 경우'를 fail 사유라고 생각하니까 아마 fail까진 아니었을 것 같단 생각도 든다. 

 

ex00

 

귀여운 클랩트랩 클래스를 구현하는 과제.

 

private :

생성자 매개변수로 초기화하는 name

HitPoint (10)

Energy points (10)

Attack damage (0)

 

public :

void attack(const std::string& target);

void takeDamage(unsigned int amount);

void beRepaired(unsigned int amount);

 

attack 호출시 target의 hp가 damage만큼 줄어듦

beRepaired 호출 시 스스로의 hp가 amount 만큼 늘어남

두 함수 모두 호출시 energy point 가 1씩 감소함

hp나 energy point가 남아있지 않으면 아무 동작도 하지 못함

모든 멤버 함수들이 호출될 때에는 상황을 이해할 수 있는 메시지가 출력됨

ex. ClapTrap <name> attacks <target>, causing <damage> points of damage!

생성자와 소멸자 호출 시 메시지가 출력되어야 하며 평가를 위해 테스트에 적당한 메인문을 만들 것.

 

 

이번 과제가 상속에 대한 문제이기 때문에 상속을 공부한 다음 상속에 대비해서 만드는 것도 좋겠지만, 상속을 위해 부모 클래스를 어떻게 구현해야 하는지를 체감하기 위해 일부러 이후 과제를 신경 쓰지 않고 각 ex 별로 요구하는 내용만 구현하였다.

 

#ifndef CLAPTRAP_HPP
# define CLAPTRAP_HPP

# include <iostream>


class ClapTrap {
 private:
	std::string name;
	unsigned int HitPoint;
	unsigned int EnergyPoint;
	unsigned int AttackDamage;
 public:
	ClapTrap(void);
	ClapTrap(std::string name);
	ClapTrap(const ClapTrap& obj);
	ClapTrap& operator=(const ClapTrap& obj);
	~ClapTrap(void);
	unsigned int getDamege(void) const;
	void attack(const std::string &target);
	void takeDamage(unsigned int amount);
	void beRepaired(unsigned int amount);
};

#endif

 

CPP02부터 08까지 Orthodox Canonical Class Form을 준수하여 헤더를 작성하도록 되어있기 때문에 이 과제에서는 사용하지 않더라도 구현해준다.

 

#include "ClapTrap.hpp"

ClapTrap::ClapTrap(void) 
{
	this->name = "default";
	this->AttackDamage = 0;
	this->HitPoint = 10;
	this->EnergyPoint = 10;
	
	std::cout << "ClapTrap default constructor called" << std::endl;
}

ClapTrap::ClapTrap(std::string name) 
{
	this->name = name;
	this->AttackDamage = 0;
	this->HitPoint = 10;
	this->EnergyPoint = 10;
	
	std::cout << "ClapTrap " << name << " constructor called" << std::endl;
}

ClapTrap::ClapTrap(const ClapTrap& obj) 
{
	this->name = obj.name;
	this->AttackDamage = obj.AttackDamage;
	this->HitPoint = obj.HitPoint;
	this->EnergyPoint = obj.EnergyPoint;
	std::cout << "ClapTrap " << name << " copy constructor called" << std::endl;

}

ClapTrap& ClapTrap::operator=(const ClapTrap& obj) 
{
	this->name = obj.name;
	this->AttackDamage = obj.AttackDamage;
	this->HitPoint = obj.HitPoint;
	this->EnergyPoint = obj.EnergyPoint;
	std::cout << "ClapTrap operator = " << name << " called" << std::endl;
	return (*this);
}

ClapTrap::~ClapTrap(void) 
{
	std::cout << "ClapTrap " << this->name << " destructor called" << std::endl;
}

unsigned int ClapTrap::getDamege(void) const
{
	return (this->AttackDamage);
}

void ClapTrap::attack(const std::string &target)
{
	if (!this->HitPoint || !this->EnergyPoint)
	{
		std::cout << "ClapTrap " << this->name << " can not move..." << std::endl;
	}
	else
	{
		std::cout	<< "ClapTrap " << this->name << " attacks " << target 
					<< ", causing " << this->AttackDamage << " points of damage!"
					<< std::endl;
		this->EnergyPoint--;
	}
}

void ClapTrap::takeDamage(unsigned int amount)
{
	if (amount > this->HitPoint)
		this->HitPoint = 0;
	else
		this->HitPoint -= amount;
	std::cout	<< "ClapTrap " << this->name << " has taken " 
				<< amount << " damage..." << std::endl;
	if (!this->HitPoint)
	{
		std::cout	<< "ClapTrap " << this->name << " is died" << std::endl; 
	}
}

void ClapTrap::beRepaired(unsigned int amount)
{
	if (!this->HitPoint || !this->EnergyPoint)
	{
		std::cout << "ClapTrap " << this->name << " can not move..." << std::endl;
	}
	else
	{
		this->HitPoint += amount;
		this->EnergyPoint--;
		std::cout	<< "ClapTrap " << this->name << " has been repaired of " 
					<< amount << " Hit points. It has now " << this->HitPoint 
					<< " Hit points" << std::endl;
	}
}

 

크게 특별한 내용 없이 서브젝트에서 요구한 대로 구현하였다. 세세하게 다른 점은 getDamage 함수를 만들거나, HP가 0일 때도 동작을 하지 않도록 구현한 부분들인데, getDamage는 나중에 보니 필요가 없었고, hp가 0일 때의 구현은 굳이 할 필요는 없었다.

 

#include "ClapTrap.hpp"

int	main(void)
{
	ClapTrap a("A");
	ClapTrap b("B");

	a.attack("B");
	b.takeDamage(5);
	b.beRepaired(3);
	b.attack("A");
	a.takeDamage(10);
	a.beRepaired(10);
	return (0);
}

 

a와 b가 서로를 때리고 수리하도록 만들고, a가 hp가 0이 되어 수리하지 못하도록 테스트를 작성하였다.

 

 

ex01

 

00에서 만든 ClapTrap을 상속받은 클래스 ScavTrap을 구현하는 과제.

 

생성자 소멸자를 상속받지만, 생성자와 소멸자, attack은 다른 메시지를 출력해야 함.

ScavTrap이 생성되면 ClapTrap을 먼저 생성하지만, 소멸할 때는 역순인 이유를 알아야 함.

ScavTrap은 ClapTrap의 속성을 사용한다.(따라서 클랩 트랩을 업데이트해야 함)

 

이름 (생성자에 매개변수로 전달)

HP(100)

EP(50)

damage(20)

그리고 추가로 void guardGate(); 를 추가한다. 해당 멤버 함수에서는 ScavTrap의 현재 gatekeeper mode 상대를 메시지로 표시해야 한다.

 

당연히 프로그램의 테스트를 변경해야 한다.

 

 

 

#ifndef CLAPTRAP_HPP
# define CLAPTRAP_HPP

# include <iostream>


class ClapTrap {
 protected:
	std::string name;
	unsigned int HitPoint;
	unsigned int EnergyPoint;
	unsigned int AttackDamage;
 public:
	ClapTrap(void);
	ClapTrap(std::string name);
	ClapTrap(const ClapTrap& obj);
	ClapTrap& operator=(const ClapTrap& obj);
	virtual ~ClapTrap(void);
	unsigned int getDamege(void) const;
	virtual void attack(const std::string &target);
	void takeDamage(unsigned int amount);
	void beRepaired(unsigned int amount);
};

#endif

 

클랩 트랩의 private 멤버를 protected로 변경하며, 소멸자와 attack에 virtual 키워드를 붙여준다.

 

해당 키워드는 attack 앞에 사용되면서 해당 함수를 가상 함수로 만들어주는데, 부모 클래스에 대한 포인터나 참조에서 자식 클래스를 가리킬 수 있는데, 이때 당연히 자식 클래스의 함수가 실행되어야겠지만 의도와는 다르게 부모 클래스의 함수가 실행된다. 따라서 해당 함수를 자식 클래스에서 변환했을 수 있다는 걸 알려주는 키워드이다.

 

소멸자 앞에 붙은 키워드는 더 중요하게 부모 클래스에 대한 포인터나 참조가 자식 클래스를 가리키고 있을 때 delete를 선언하면 부모 클래스만 소멸되고 자식 클래스는 메모리 누수가 된다. 그걸 방지하기 위해서 가상 소멸자를 만들기 위해 사용된다.

 

#ifndef SCAVTRAP_HPP
# define SCAVTRAP_HPP

# include <iostream>
# include "ClapTrap.hpp"

class ScavTrap : public ClapTrap
{
 public:
	ScavTrap(void);
	ScavTrap(std::string name);
	ScavTrap(const ScavTrap& obj);
	ScavTrap& operator=(const ScavTrap& obj);
	~ScavTrap(void);
	void guardGate(void);
	void attack(std::string const& target);
};

#endif

 

class 선언 뒤에 ClapTrap을 상속받는다고 표시해주고 생성자와 소멸자, 그리고 필요한 함수들을 넣어준다.

 

#include "ScavTrap.hpp"

ScavTrap::ScavTrap(void) 
{
	this->HitPoint = 100;
	this->EnergyPoint = 50;
	this->AttackDamage = 20;

	std::cout << "ScavTrap default constructor called" << std::endl;
}

ScavTrap::ScavTrap(std::string name)
{
	this->name = name;
	this->HitPoint = 100;
	this->EnergyPoint = 50;
	this->AttackDamage = 20;
	
	std::cout << "ScavTrap " << name << " constructor called" << std::endl;
}

ScavTrap::ScavTrap(const ScavTrap& obj) 
{
	this->name = obj.name;
	this->AttackDamage = obj.AttackDamage;
	this->HitPoint = obj.HitPoint;
	this->EnergyPoint = obj.EnergyPoint;
	std::cout << "ScavTrap " << name << " copy constructor called" << std::endl;

}

ScavTrap& ScavTrap::operator=(const ScavTrap& obj) 
{
	this->name = obj.name;
	this->AttackDamage = obj.AttackDamage;
	this->HitPoint = obj.HitPoint;
	this->EnergyPoint = obj.EnergyPoint;
	std::cout << "ScavTrap operator = " << name << " called" << std::endl;
	return (*this);

}

ScavTrap::~ScavTrap(void) 
{
	std::cout << "ScavTrap " << this->name << " destructor called" << std::endl;
}

void ScavTrap::guardGate(void)
{
	if (!this->HitPoint || !this->EnergyPoint)
	{
		std::cout << "ScavTrap " << this->name << " can not move..." << std::endl;
	}
	else
	{
		std::cout << "ScavTrap " << this->name << " has entered gate guard mode" << std::endl;
		this->EnergyPoint--;
	}
}

void ScavTrap::attack(const std::string &target)
{
	if (!this->HitPoint || !this->EnergyPoint)
	{
		std::cout << "ScavTrap " << this->name << " can not move..." << std::endl;
	}
	else
	{
		std::cout	<< "ScavTrap " << this->name << " attacks " << target 
					<< ", causing " << this->AttackDamage << " points of damage!"
					<< std::endl;
		this->EnergyPoint--;
	}
}

 

특별히 어려울 것 없이 구현하면 끝. attack을 구현하는 걸 까먹으면 안 된다. 그러면 00의 attack과 같은 메시지가 나오기 때문에 서브젝트에 위반됨.

 

메인문을 구현해서 ScavTrap을 만들어보면 서브젝트에서 말한 대로 ClapTrap 생성 - ScavTrap 생성 - ScavTrap 소멸 - ClapTrap 소멸 순서로 나오는 걸 알 수 있다.

 

이렇게 되는 이유는 상속을 받을 때 ClapTrap의 데이터를 지닌 ScavTrap이 생성되는 것이 아니라 ClapTrap이 생성되고 그 주소를 지닌 ScavTrap이 생성되기 때문에, 만약 순서가 뒤바뀐다면 CalpTrap의 변수를 ScavTrap에서 초기화 하기가 굉장히 불편할 것이고, 반대로 소멸에서는 ClapTrap이 먼저 소멸된다면, ScavTrap의 소멸자에서 부모의 함수나 변수를 사용하지 못하는 상황이 생기기 때문에 이런 순서로 동작한다.

 

 

https://velog.io/@meong9090/c-상속

 

[ c++ ] 상속과 다형성에 대해 알아보자

c++ 상속에 대해서

velog.io

https://yeolco.tistory.com/125

 

C++ 가상 함수(Virtual Function)

안녕하세요 열코입니다. 이번 시간에는 C++ 클래스의 가상 함수(Virtual Function)에 대해 알아보도록 하겠습니다. 가상 함수는 기본 클래스(상속되지 않은 클래스) 내에서 선언되어 파생 클래스에

yeolco.tistory.com

https://musket-ade.tistory.com/entry/C-가상-소멸자-Virtual-Destructor

 

[ C++ ] 가상 소멸자( Virtual Destructor )

말 그대로 virtual로 선언된 소멸자를 가상 소멸자( Virtual Destructor )라고 한다. 근데 왜 소멸자에 virtual 선언이 필요할까? 먼저 예제를 보자. ( * 혹시 VS에서 위 코드를 작성하다가 다음과 같은 에러

musket-ade.tistory.com

 

 

ex02

01과 거의 유사하게 ClapTrap을 상속받은 FragTrap를 만든다. 다른 점은 attack을 따로 구현하지 않아도 되며, guardGate 대신 highFivesGuys를 구현한다.

 

상세한 수치는 생략

 

 

 

#ifndef FRAGTRAP_HPP
# define FRAGTRAP_HPP

# include <iostream>
# include "ClapTrap.hpp"

class FragTrap : public ClapTrap
{
 public:
	FragTrap(void);
	FragTrap(const FragTrap& obj);
	FragTrap& operator=(const FragTrap& obj);
	~FragTrap(void);
	FragTrap(std::string name);
	void highFivesGuys(void);
};

#endif


#include "FragTrap.hpp"

FragTrap::FragTrap(void) 
{
	this->name = "default";
	this->AttackDamage = 30;
	this->HitPoint = 100;
	this->EnergyPoint = 100;
	
	std::cout << "FragTrap default constructor called" << std::endl;
}

FragTrap::FragTrap(const FragTrap& obj) 
{
	this->name = obj.name;
	this->AttackDamage = obj.AttackDamage;
	this->HitPoint = obj.HitPoint;
	this->EnergyPoint = obj.EnergyPoint;
	std::cout << "FragTrap " << name << " copy constructor called" << std::endl;
}

FragTrap& FragTrap::operator=(const FragTrap& obj) 
{
	this->name = obj.name;
	this->AttackDamage = obj.AttackDamage;
	this->HitPoint = obj.HitPoint;
	this->EnergyPoint = obj.EnergyPoint;
	std::cout << "FragTrap operator = " << name << " called" << std::endl;
	return (*this);
}

FragTrap::~FragTrap(void) 
{
	std::cout << "FragTrap " << this->name << " destructor called" << std::endl;

}

FragTrap::FragTrap(std::string name)
{
	this->name = name;
	this->AttackDamage = 30;
	this->HitPoint = 100;
	this->EnergyPoint = 100;
	
	std::cout << "FragTrap " << name << " constructor called" << std::endl;
}

void FragTrap::highFivesGuys(void)
{
	if (!this->HitPoint) 
	{
		std::cout << "FragTrap " << this->name << " can not move..." << std::endl;
	}
	else
	{
		std::cout << "FragTrap " << this->name << " high five!\n";
	}
}

 

01에서 구현한 것과 거의 동일하게 구현하면 된다.

 

 

ex03

위에서 괜히 두 개 만든듯한 클래스 두 개를 동시에 상속받는 DiamondTrap 클래스를 만든다.

 

Name (생성자의 매개변수)

ClapTrap::Name (생성자의 매개변수 + "_clap_name")

Hitpoints (FragTrap)

Energy points (ScavTrap)

Attack damage (FragTrap)

attack (ScavTrap)

void whoAmI() : 자기 자신의 이름과 ClapTrap::Name을 함께 출력한다.

 

 

다중 상속, 그중에서도 다이아몬드 상속을 다루는 문제이다.

 

FragTrap과 ScavTrap을 같이 상속받으면(다중 상속)  다이아몬드 상속 문제가 생긴다.

 

출처 https://ansohxxn.github.io/cpp/chapter12-8/

1번처럼 동작할 거라고 생각하지만 실제로는 2번처럼 동작하기 때문에, ClapTrap의 변수를 찾아갈 때 어떤 값을 꺼내와야 할지 모르는 모호성 문제가 생긴다.

 

이를 해결하기 위해 가상 상속을 사용할 수 있다. Frag와 Scav에서 Clap을 virtual이라고 표시해두면 동시에 상속되더라도 메모리에 Clap이 하나만 올라가게 되어 모호성 문제를 해결하게 된다.

 

다만 성능이 저하되거나 오히려 더 많은 메모리를 잡아먹는 경우도 있어서 흔히 사용되지는 않으며, 이러한 문제가 자바에서 다중 상속을 막은 이유이기도 하다

 

#ifndef DIAMONDTRAP_HPP
# define DIAMONDTRAP_HPP

# include <iostream>
# include "FragTrap.hpp"
# include "ScavTrap.hpp"

class DiamondTrap : public FragTrap, public ScavTrap
{
 private:
	std::string name;
 public:
	DiamondTrap(void);
	DiamondTrap(const DiamondTrap& obj);
	DiamondTrap& operator=(const DiamondTrap& obj);
	~DiamondTrap(void);
	DiamondTrap(std::string name);
	void whoAmI(void);
	void printStatus(void);
};

#endif

 

중간에 헷갈려서 printstatus 함수를 만들었는데, 잘 써먹긴 했다.

 

#include "DiamondTrap.hpp"

DiamondTrap::DiamondTrap(void): name(ClapTrap::name)
{
	ClapTrap::name = name + "_clap_name";
	std::cout << "DiamondTrap default constructor called" << std::endl;
}

DiamondTrap::DiamondTrap(const DiamondTrap& obj) : ClapTrap(obj), FragTrap(obj), ScavTrap(obj), name(obj.name)
{
	std::cout << "DiamondTrap " << name << " copy constructor called" << std::endl;
}

DiamondTrap& DiamondTrap::operator=(const DiamondTrap& obj) 
{
	this->name = obj.name;
	this->AttackDamage = obj.AttackDamage;
	this->HitPoint = obj.HitPoint;
	this->EnergyPoint = obj.EnergyPoint;
	std::cout << "DiamondTrap operator = " << name << " called" << std::endl;
	return (*this);
}

DiamondTrap::~DiamondTrap(void) 
{
	std::cout << "DiamondTrap " << this->name << " destructor called" << std::endl;
}

DiamondTrap::DiamondTrap(std::string name) : ClapTrap(), FragTrap(), ScavTrap()
{
	this->AttackDamage = 30;
	this->name = name;
	ClapTrap::name = name + "_clap_name";
	std::cout << "DiamondTrap " << name << " constructor called" << std::endl;
}

void DiamondTrap::whoAmI(void)
{
	std::cout	<< "This DiamondTrap name is " << this->name 
				<< " and This ClapTrap name is " << ClapTrap::name << std::endl;
}

void DiamondTrap::printStatus(void)
{
	std::cout	<< "This DiamondTrap name is " << this->name << "\n"
				<< "HitPoint is " << HitPoint << "\n"
				<< "EnergyPoint is " << EnergyPoint << "\n"
				<< "AttackDamage is " << AttackDamage << std::endl;
}

 

참고로 생성자에서 ClapTrap(), FragTrap(), ScavTrap()는 붙이든 안 붙이든 컴파일러가 알아서 동작하긴 한다. 하지만 붙일 거면 반드시 헤더에서 지정한 순서대로 불러와야 에러를 일으키지 않는다.

 

가장 헷갈렸던 부분이 서브젝트의 Hitpoints (FragTrap), Energy points (ScavTrap), Attack damage (FragTrap) 부분이었다. 해당 값을 가져오라는 건 알겠는데, 처음에 this→AttackDamage = FragTrap::AttackDamage라고 적어뒀다가 나중에 뭔가 이상한 느낌이 들어서 확인을 해보니 ScavTrap의 Damage가 들어가 있었다.

 

ScavTrap이 나중에 불러와지니 해당 기본 생성자의 값으로 덮어씌워지면서, 저 연산은 그냥 D = D와 다를 게 없는 연산이었다. 고민 끝에 해결할 방법이 생각이 안 나서 그냥 damage 만 바꿔줬는데, 나중에라도 정확한 방법을 알았으면 좋겠다.

 

평가에서 이 부분을 잘못 알고 있었단 사실을 알게 되었는데, 별다른 구현을 하지 않았음에도 Attack이 ScavTrap의 함수가 불러와지는 이유가 나는 ClapTrap의 Attack을 가상 함수로 만들어서 그렇다고 생각했고, 평가자분은 상속의 순서를 FragTrap 다음에 ScavTrap을 하기 때문이라고 생각했다. 궁금해서 둘 다 테스트를 해보니 둘다 별 상관없었다.

 

그래서 한참 찾아봤는데 별다른 설명은 못 찾고(아마 무언가 당연한 이치라서 설명하는 사람이 없는 듯) 추측하기로는 트리 형태로 부모 클래스들을 참조할 때 부모 클래스 전체를 먼저 확인한 다음에 그 부모를 찾는 게 아닐까 싶다. 확인할 방법이 생각이 안 나서 확인을 못해봤는데, 두 부모 클래스에 Attack를 동시에 만들면 모호성 에러가 뜨는 걸 보면 비슷한 이유일 것 같다

 

 

https://ansohxxn.github.io/cpp/chapter12-8/

 

C++ Chapter 12.8 : 가상 상속으로 다이아몬드 상속 문제 해결

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀 🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!

ansohxxn.github.io

 

 

https://dataonair.or.kr/db-tech-reference/d-lounge/technical-data/?mod=document&uid=235880

 

C++ 프로그래밍 : 가상 상속의 득과 실

C++ 프로그래밍 가상 상속의 득과 실 C++와 자바(Java)를 많이 비교하는데 그 중에서 제일 빈번하게 비교되는 개념이 바로 ‘다중 상속’이다. C++에서는 존재하는 다중 상속이 명목상으로는 자바에

dataonair.or.kr