개인적으로 공부한 C++ 내용을 공유합니다.


스타크래프트 만들기


객체의 대한 실습으로 스타크래프트 만들기를 진행해보도록 합시다.

첫 번째로 마린이라는 유닛의 객체를 만들어 보겠습니다.

#include <iostream>

class Marine {
	int		hp;
	int		coord_x, coord_y;
	int		damage;
	bool	is_dead;

public:
	Marine(void);
	Marine(int x, int y);

	int		attack();
	void	be_attacked(int damage_earn);
	void	move(int x, int y);

	void	show_status();
};

Marine::Marine(void) {
	hp = 50;
	coord_x = coord_y = 0;
	damage = 5;
	is_dead = false;
}

Marine::Marine(int x, int y) {
	coord_x = x;
	coord_y = y;
	hp = 50;
	damage = 5;
	is_dead = false;
}

void	Marine::move(int x, int y) {
	coord_x = x;
	coord_y = y;
}

int		Marine::attack(void) { return damage; }
void	Marine::be_attacked(int damage_earn) {
	hp -= damage_earn;
	if (hp <= 0) is_dead = true;
}

void	Marine::show_status(void) {
	std::cout << " *** Marine ***" << std::endl;
	std::cout << " Location : ( " << coord_x << ", " << coord_y << " ) "
		<< std::endl;
	std::cout << " HP : " << hp << std::endl;
}

int		main(void) {
	Marine marine1(2, 3);
	Marine marine2(3, 5);

	marine1.show_status();
	marine2.show_status();

	std::cout << std::endl << "마린 1 이 마린 2 를 공격! " << std::endl;
	marine2.be_attacked(marine1.attack());

	marine1.show_status();
	marine2.show_status();
}

위에 코드에서 객체를 new를 이용해서 생성해봅시다.


class Marine{
	...
}


int		main(void) {
	Marine* marines[2]

	marines[0] = new Marine1(2, 3);
	marines[1] = new Marine2(3, 5);

	marines[0]->show_status();
	marines[1]->show_status();

	std::cout << std::endl << "마린 1 이 마린 2 를 공격! " << std::endl;
	marines[1]->be_attacked(marines[0].attack());

	marines[0]->show_status();
	marines[1]->show_status();
}

위와 같이 newmalloc 과 다르게 객체를 동적으로 생성하면서와 동시에 자동으로 생성자를 호출할 수 있습니다.


소멸자 (Destructor)


위에 마린 객체의 코드에서 마린의 이름을 넣어주는 옵션을 추가해봅시다.


// Add name attribute to class

Marine::Marine(int x, int y, const char* marine_name) {
  name = new char[strlen(marine_name) + 1];
  strcpy(name, marine_name);
  coord_x = x;
  coord_y = y;
  hp = 50;
  damage = 5;
  is_dead = false;
}

위에 코드와 같이 name에 받아온 문자열을 동적으로 생성하여 문자열을 복사할 수 있습니다. 이렇게 되면 Marine 객체 하나는 동적으로 할당된 메모리를 속성으로 하게 됐는데, 그렇다면 우리는 언제 이 동적할당된 메모리를 해제해야할까를 생각해야합니다. C++에서는 생성했던 객체가 소멸 될때 자동으로 호출되는 함수가 존재하는데 이것이 바로 소멸자 (Destructor) 입니다.


class Marine {
	...

public:
	...
	~Marine(); // 소멸자
}

...
...

Marine::~Marine() {
	std::cout << name << " 의 소멸자 호출 ! " << std::endl;
	if (name != NULL) {
		delete[] name;
	}
}

...

int main() {
	...
	...

	delete marines[0];
	delete marines[1];
}

위에 코드에서 ~Marine() 는 소멸자를 의미하고 이것은 객체가 제거될 때 소멸자가 호출됩니다. 소멸자 안에 있는 코드처럼 보통 소멸자에서 동적할당된 모든 메모리들을 해제해주고 객체가 소멸될 수 있도록 구성을 합니다. 소멸자 또한 디폴트 소멸자가 존재하는데 디폴트 소멸자 내부에서는 아무런 작업도 수행하지 않습니다. 만일 소멸자가 필요없는 클래스의 경우에는 굳이 소멸자를 따로 써줄 필요는 없습니다.


복사 생성자


하나의 객체를 갖고 다른 똑같은 객체를 만드는 방법으로 복사 생성자를 사용해볼 수 있습니다. 이를 스타크래프트에서 포토캐논 겹치기와 비교를 하여 아래와 같은 코드를 작성해볼 수 있습니다.

#include <string.h>
#include <iostream>

class Photon_Cannon {
  int hp, shield;
  int coord_x, coord_y;
  int damage;

 public:
  Photon_Cannon(int x, int y);
  Photon_Cannon(const Photon_Cannon& pc);

  void show_status();
};
Photon_Cannon::Photon_Cannon(const Photon_Cannon& pc) {
  std::cout << "복사 생성자 호출 !" << std::endl;
  hp = pc.hp;
  shield = pc.shield;
  coord_x = pc.coord_x;
  coord_y = pc.coord_y;
  damage = pc.damage;
}
Photon_Cannon::Photon_Cannon(int x, int y) {
  std::cout << "생성자 호출 !" << std::endl;
  hp = shield = 100;
  coord_x = x;
  coord_y = y;
  damage = 20;
}
void Photon_Cannon::show_status() {
  std::cout << "Photon Cannon " << std::endl;
  std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) "
            << std::endl;
  std::cout << " HP : " << hp << std::endl;
}
int main() {
  Photon_Cannon pc1(3, 3);
  Photon_Cannon pc2(pc1);
  Photon_Cannon pc3 = pc2;

  pc1.show_status();
  pc2.show_status();
}

위에 코드에서 핵심 부분인 복사 생성자가 호출이 되는 부분을 보겠습니다.

Photon_Cannon pc1(3, 3);
Photon_Cannon pc2(pc1);

일단 pc1int x, int y 를 인자로 가지는 생성자가 오버로딩 되었고, pc2 의 경우 인자로 pc1 을 넘겼으므로 복사 생성자가 호출 되었음을 알 수 있습니다.

Photon_Cannon pc3 = pc2;

그렇다면 위 코드는 어떻게 동작을 할까요? 위 코드 역시 복사 생성자가 호출됩니다. C++ 컴파일러는 위 문장을 아래와 동일하게 해석합니다.

Photon_Cannon pc3(pc2);

따라서 복사 생성자가 호출되게 되는 것입니다. 물론, 위는 아주아주 특별한 경우 입니다. 만일 그냥 pc3 = pc2; 와 같은 코드를 작성했다면 이는 평범한 대입 연산이겠지만, 생성 시에 대입하는 연산, 즉 위에 같이 Photon_Cannon pc3 = pc2; 를 한다면 복사 생성자가 호출되게 되는 것 입니다. 이런식으로 코드를 작성하면 사용자가 상당히 직관적이고 깔금한 프로그래밍을 할 수 있습니다.

참고로 한 가지 더 말하자면 아래 두 코드는 염연히 다른 문장입니다.

// Code 1
Photon_Cannon pc3 = pc2;

// Code 2
Photon_Cannon pc3;
pc3 = pc2;

그 이유는 Code 1 은 말 그대로 복사 생성자가 1번 호출 되는 것이고 Code 2 는 생성자가 1번 호출 되고 pc3 = pc2; 라는 명령이 실행되는 것이기 때문입니다. 따라서 명심해야할 것은 복사 생성자는 오직 생성 시에 호출된다는 것입니다.

그런데 사실 디폴트 생성자와 디폴트 소멸자 처럼, C++ 컴파일러는 이미 디폴트 복사 생성자(Default copy constructor) 를 지원해 주고 있습니다. 위 코드에서 복사 생성자를 한 번 지워보시고 실행을 해보면, 이전과 정확히 동일한 결과가 나타남을 알 수 있습니다. 디폴트 복사 생성자의 경우 기존의 디폴트 생성자와 소멸자가 하는 일이 아무 것도 없었던 것과 달리 실제로 복사 를 해줍니다. 결과적으로 디폴트 복사 생성자는 대응 되는 원소들을 말 그대로 1 대 1 복사를 해주게 됩니다. 따라서 간단한 클래스의 경우 귀찮게 굳이 복사생성자를 써주지 않고도 디폴도 복사 생성자만 이용하여 복사 생성을 쉽게 처리 할 수 있습니다.


디폴트 복사 생성자의 한계


위에 소멸자를 배울때와 같이 Photon_Cannon 클래스에 이름을 추가해보겠습니다.

#include <string.h>
#include <iostream>

class Photon_Cannon {
  int hp, shield;
  int coord_x, coord_y;
  int damage;

  char *name;

 public:
  Photon_Cannon(int x, int y);
  Photon_Cannon(int x, int y, const char *cannon_name);
  ~Photon_Cannon();

  void show_status();
};

Photon_Cannon::Photon_Cannon(int x, int y) {
  hp = shield = 100;
  coord_x = x;
  coord_y = y;
  damage = 20;

  name = NULL;
}
Photon_Cannon::Photon_Cannon(int x, int y, const char *cannon_name) {
  hp = shield = 100;
  coord_x = x;
  coord_y = y;
  damage = 20;

  name = new char[strlen(cannon_name) + 1];
  strcpy(name, cannon_name);
}
Photon_Cannon::~Photon_Cannon() {
  if (name) delete[] name;
}
void Photon_Cannon::show_status() {
  std::cout << "Photon Cannon :: " << name << std::endl;
  std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) "
            << std::endl;
  std::cout << " HP : " << hp << std::endl;
}
int main() {
  Photon_Cannon pc1(3, 3, "Cannon");
  Photon_Cannon pc2 = pc1;

  pc1.show_status();
  pc2.show_status();
}

위에 코드를 실행하게되면 런타임 오류가 발생합니다. 그 이유는 무엇을까요? 우리는 원래 Photon_Cannon 클래스에 이름만 추가를 하고 복사 생성자를 이용하여 두 객체를 생성하였습니다. 그렇다면 복사 생성자가 잘못된 것일까요?

클래스의 생성과 소멸 동작을 따라가보도록 하겠습니다. Photon_Cannon pc2 = pc1 이 실행이 되면, pc2pc1 의 값들이 디폴트 복사 생성자로 인하여 1 대 1로 원소들 간의 복사가 이루어집니다. 하지만 여기서 name 속성의 경우 동적할당된 메모리가 저장이 됩니다. 그렇기 때문에 pc2pc1의 이름 문자열의 주소가 같아지게 됩니다. 이러한 과정에서 오류가 나는 것은 아닙니다. 그러면 더 동작 따라가보겠습니다. 이렇게 pc1pc2 가 잘 생성이 되고 show_status 함수의 호출이 끝나면 메인 함수가 끝나게되어 pc1pc2 객체는 소멸하게 됩니다. 하지만 둘 중 하나가 소멸될 때 문제가 발생합니다. 소멸자의 호출로 인하여 중복으로 참조되고 있던 name 이 메모리 해제가 됩니다. 이로인해 이후로 다른 하나가 소멸되게 했을 때, 이미 해제된 주소인 name 을 한번 더 해제하게 됩니다. 하지만 한번 해제된 메모리는 다시 해제될 수 없으므로 오류가 나게 됩니다. 이렇게 런타임 오류가 발생하게 되는 것입니다.

그렇다면 어떻게 이 문제를 해결하면 좋을까요? 답은 바로 name 을 복사할 때, 새롭게 동적할당을 해준 문자열을 넣어주면 되는 것입니다. 아래 코드처럼 복사 생성자를 수정을 해볼 수 있습니다.

Photon_Cannon::Photon_Cannon(const Photon_Cannon &pc) {
  std::cout << "복사 생성자 호출! " << std::endl;
  hp = pc.hp;
  shield = pc.shield;
  coord_x = pc.coord_x;
  coord_y = pc.coord_y;
  damage = pc.damage;

  // 추가된 부분
  name = new char[strlen(pc.name) + 1];
  strcpy(name, pc.name);
}
⤧  Next post Oh-My-C-P-P 05 ⤧  Previous post Oh-My-C-P-P 03