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


생성자의 초기화 리스트(initializer list)


#include <iostream>

class Marine {
  int hp;                // 마린 체력
  int coord_x, coord_y;  // 마린 위치
  int damage;            // 공격력
  bool is_dead;

 public:
  Marine();              // 기본 생성자
  Marine(int x, int y);  // x, y 좌표에 마린 생성

  int attack();                       // 데미지를 리턴한다.
  void be_attacked(int damage_earn);  // 입는 데미지
  void move(int x, int y);            // 새로운 위치

  void show_status();  // 상태를 보여준다.
};

Marine::Marine() : hp(50), coord_x(0), 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() { return damage; }
void Marine::be_attacked(int damage_earn) {
  hp -= damage_earn;
  if (hp <= 0) is_dead = true;
}
void Marine::show_status() {
  std::cout << " *** Marine *** " << std::endl;
  std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) "
            << std::endl;
  std::cout << " HP : " << hp << std::endl;
}

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

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

위에 코드는 전에 학습했던 Marine 클래스 생성자를 아래와 같이 변경해보도록 하겠습니다.

Marine::Marine() : hp(50), coord_x(0), 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) {}

즉 생성자에 무엇인가를 도입한 것인데요, 바로 살펴보도록 하겠습니다.

원래 코드에서 위 코드로 변경했을 때, 생성자에 무엇이 달라졌는지 살펴보면 몸통부분이 사라진 것을 확인할 수 있습니다. 그렇다면 이 의미는 오직 위에 코드만으로 원래 있던 코드의 몸통 부분을 대체한다는 것입니다. 그렇습니다. 이것이 바로 초기화 리스트(initializer list) 입니다. 생성자 호출과 동시에 멤버 변수들을 초기화가 가능하도록 해줍니다.

멤버 초기화 리스트의 일반적인 꼴은 아래와 같습니다.

(생성자 이름) : var1(arg1), var2(arg2) {}

그렇다면 왜 초기화 리스트를 사용해야할까요?

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

위에 두 코드가 완벽히 일치하다면 가독성 이외의 초기화 리스트를 사용할 필요가 없습니다. 하지만 두개 코드에는 실제로 약간의 차이가 있습니다. 초기화 리스트를 사용한 코드의 경우 생성과 초기화를 동시에 하게됩니다. 반면에 초기화 리스트를 사용하지 않는다면 생성을 먼저하고 그 다음에 대입을 수행하게 됩니다. 따라서 초기화 리스트를 사용한다는 것이 더 효율적인 작업이라는 사실이 됩니다. 그 뿐만 아니라, 우리 경험상 반드시 생성과 동시에 초기화 되어야 하는 것들이 몇 가지 있었습니다. 대표적으로 레퍼런스와 상수입니다. 그러므로 만약에 클래스 내부에 레퍼런스 변수나 상수를 넣고 싶다면 이들을 생성자에서 무조건 초기화 리스트를 사용해서 초기화 시켜주어야만 합니다.

#include <iostream>

class Marine {
  int hp;                // 마린 체력
  int coord_x, coord_y;  // 마린 위치
  bool is_dead;

  const int default_damage;  // 기본 공격력

 public:
  Marine();              // 기본 생성자
  Marine(int x, int y);  // x, y 좌표에 마린 생성

  int attack();                       // 데미지를 리턴한다.
  void be_attacked(int damage_earn);  // 입는 데미지
  void move(int x, int y);            // 새로운 위치

  void show_status();  // 상태를 보여준다.
};
Marine::Marine()
    : hp(50), coord_x(0), coord_y(0), default_damage(5), is_dead(false) {}

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

void Marine::move(int x, int y) {
  coord_x = x;
  coord_y = y;
}
int Marine::attack() { return default_damage; }
void Marine::be_attacked(int damage_earn) {
  hp -= damage_earn;
  if (hp <= 0) is_dead = true;
}
void Marine::show_status() {
  std::cout << " *** Marine *** " << std::endl;
  std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) "
            << std::endl;
  std::cout << " HP : " << hp << std::endl;
}

int main() {
  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();
}

위에 마린 클래스는 프로그래머들의 실수로 마린의 공격력이 이상하게 변하는 것을 막기 위해서 기본 공격력이라는 상수 멤버를 도입해서 딱 고정 시켜 버리고 마음 편하게 프로그래밍 할 수 있도록 하였습니다. 따라서 이를 위해 생성자에서 초기화 리스트를 도입해서

Marine::Marine()
    : hp(50), coord_x(0), coord_y(0), default_damage(5), is_dead(false) {}

와 같이, default_damage 를 생성과 동시에 초기화 할 수 있도록 하였습니다. 따라서 우리는 상수인 default_damage 를 5로 초기화 할 수 있고, 이 값은 영원히 바뀌지 않게 됩니다.

위에 마린 클래스에서 생성자의 인자를 통하여 default_damage 를 초기화 할 수 있습니다.

Marine::Marine(int x, int y, int default_damage)
    : coord_x(x),
      coord_y(y),
      hp(50),
      default_damage(default_damage),
      is_dead(false) {}

위와 같이 코드를 입력하면 인자로 사용자로부터 default_damage 를 받을 수 있게되고 받은 값이 클래스의 값으로 초기화가 됩니다.


생성된 총 Marine 수 세기 (static 변수)


이번에는 여태까지 만들어지는 총 Marine 의 수를 알아내기 위해 코드를 짠다고 생각해봅시다. 이를 위해서 많은 방법이 있겠지만 가장 단순한 두 방식을 생각해본다면,

  1. 어떠한 배열에 Marine 을 보관해 놓고, 생성된 마린의 개수를 모두 센다.

  2. 어떤 변수를 만들어서 Marine 의 생성시에 1을 추가하고, 소멸시에 1을 뺀다.

을 생각할 수 있을 것입니다. 첫 번째 방법의 경우, 따로 크기가 자유자재로 변할 수 있는 배열을 따로 만들어야 하는 문제점이 있고, 두 번째의 같은 경우 만일 어떠한 함수 내에서 이런 변수를 정의하였다면 다른 함수에서도 그 값을 이용하기 위해 인자로 계속 전달해야 하는 귀찮음이 있습니다.

물론 전역 변수로 만들면 되지 않겠냐고 물을 수 도 있겠지만, 전역 변수의 경우 프로젝트의 크기가 커질 수 록 프로그래머의 실수로 인해 서로 겹쳐서 오류가 날 가능성이 다분하기에 반드시 필요한 경우가 아니면 사용을 하지 않습니다.

하지만 C++ 에서는 위와 같은 문제를 간단하게 해결 할 수 있는 기능을 제공하고 있습니다. 마치 전역 변수 같지만 클래스 하나에만 종속되는 변수인 것 인데요, 바로 static 멤버 변수입니다.

어떤 클래스의 static 멤버 변수의 경우, 멤버 변수들 처럼 객체가 소멸될 때 소멸되는 것이 아닌 프로그램이 종료될 때 소멸되는 것입니다.

또한, 이 static 멤버 변수의 경우 클래스의 모든 객체들이 공유 하는 변수로써 각 객체 별로 따로 존재하는 멤버 변수들과 달리 모든 객체들이 하나의 static 멤버 변수를 사용하게 됩니다. 그럼 바로 아래의 예제를 살펴보도록 합시다.

// static 멤버 변수의 사용

#include <iostream>

class Marine {
  static int total_marine_num;

  int hp;                // 마린 체력
  int coord_x, coord_y;  // 마린 위치
  bool is_dead;

  const int default_damage;  // 기본 공격력

 public:
  Marine();              // 기본 생성자
  Marine(int x, int y);  // x, y 좌표에 마린 생성
  Marine(int x, int y, int default_damage);

  int attack();                       // 데미지를 리턴한다.
  void be_attacked(int damage_earn);  // 입는 데미지
  void move(int x, int y);            // 새로운 위치

  void show_status();  // 상태를 보여준다.

  ~Marine() { total_marine_num--; }
};
int Marine::total_marine_num = 0;

Marine::Marine()
    : hp(50), coord_x(0), coord_y(0), default_damage(5), is_dead(false) {
  total_marine_num++;
}

Marine::Marine(int x, int y)
    : coord_x(x), coord_y(y), hp(50), default_damage(5), is_dead(false) {
  total_marine_num++;
}

Marine::Marine(int x, int y, int default_damage)
    : coord_x(x),
      coord_y(y),
      hp(50),
      default_damage(default_damage),
      is_dead(false) {
  total_marine_num++;
}

void Marine::move(int x, int y) {
  coord_x = x;
  coord_y = y;
}
int Marine::attack() { return default_damage; }
void Marine::be_attacked(int damage_earn) {
  hp -= damage_earn;
  if (hp <= 0) is_dead = true;
}
void Marine::show_status() {
  std::cout << " *** Marine *** " << std::endl;
  std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) "
            << std::endl;
  std::cout << " HP : " << hp << std::endl;
  std::cout << " 현재 총 마린 수 : " << total_marine_num << std::endl;
}

void create_marine() {
  Marine marine3(10, 10, 4);
  marine3.show_status();
}
int main() {
  Marine marine1(2, 3, 5);
  marine1.show_status();

  Marine marine2(3, 5, 10);
  marine2.show_status();

  create_marine();

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

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

위 코드에서는 아래와 같이 static 변수를 정의하였습니다.

static int total_marine_num;

모든 전역 및 static 변수들은 정의와 동시에 값이 자동으로 0 으로 초기화 되기 떄문에 이 경우 우리가 굳이 따로 초기화 하지 않아도 되지만 클래스 static 변수들의 경우 아래와 같은 방법으로 초기화 합니다.

int Marine::total_marine_num = 0;

간혹 클래스 내부에서 아래와 같이 초기화 할 수 도 있냐는 의문을 갖을 수 있습니다.

class Marine {
  static int total_marine_num = 0;
  ...
}

멤버 변수들을 위와 같이 초기화 시키지 못하는 것처럼 static 변수 역시 클래스 내부에서 위와 같이 초기화 하는 것은 불가능 합니다. 위와 같은 꼴이 되는 유일한 경우는 const static 변수일 때만 가능한데 실제로 아래와 같이 사용할 수 있습니다.

class Marine {
	const static int x = 0;
}

그럼 실제로 total_marine_sum 이 잘 작동하고 있는지 살펴보도록 합시다. 클래스의 편한 점이 생성자와 소멸자를 제공한다는 점인데, 덕분에 Marine 이 생성될 때, 그리고 소멸될 때 굳이 따로 처리하지 않고도, 생성자와 소멸자 안에 total_marine_num 을 조작하는 문장을 넣어주면 편하게 처리할 수 있습니다.

그럼 실제로 total_marine_sum 이 잘 작동하고 있는지 살펴보도록 합시다. 클래스의 편한 점이 생성자와 소멸자를 제공한다는 점인데, 덕분에 Marine 이 생성될 때, 그리고 소멸될 때 굳이 따로 처리하지 않고도, 생성자와 소멸자 안에 total_marine_num 을 조작하는 문장을 넣어주면 편하게 처리할 수 있습니다.

Marine::Marine()
    : hp(50), coord_x(0), coord_y(0), default_damage(5), is_dead(false) {
  total_marine_num++;
}

Marine::Marine(int x, int y)
    : coord_x(x), coord_y(y), hp(50), default_damage(5), is_dead(false) {
  total_marine_num++;
}

Marine::Marine(int x, int y, int default_damage)
    : coord_x(x),
      coord_y(y),
      hp(50),
      default_damage(default_damage),
      is_dead(false) {
  total_marine_num++;
}

~Marine() { total_marine_num--; }

로 각 생성자 또는 소멸자 호출 시에 total_marine_num 을 1 씩 증가 또는 감소 시키는 문장을 넣었습니다.

클래스 안에 static 변수만 만들 수 있는 것이 아닙니다. 놀랍게도 클래스 안에는 static 함수도 정의할 수 있는데, staitc 변수가 어떠한 객체에 종속되는 것이 아니라, 그냥 클래스 자체에 딱 1개 존재하는 것인 것 처럼 static 함수 역시 어떤 특정 객체에 종속되는 것이 아니라 클래스 전체에 딱 1개 존재하는 함수입니다.

즉, static 이 아닌 멤버 함수들의 경우 객체를 만들어야지만 각 멤버 함수들을 호출할 수 있지만 static 함수의 경우, 객체가 없어도 그냥 클래스 자체에서 호출할 수 있게 됩니다. 그럼, 아래 예제를 살펴봅시다.

// static 함수
#include <iostream>

class Marine {
  static int total_marine_num;
  const static int i = 0;

  int hp;                // 마린 체력
  int coord_x, coord_y;  // 마린 위치
  bool is_dead;

  const int default_damage;  // 기본 공격력

 public:
  Marine();              // 기본 생성자
  Marine(int x, int y);  // x, y 좌표에 마린 생성
  Marine(int x, int y, int default_damage);

  int attack();                       // 데미지를 리턴한다.
  void be_attacked(int damage_earn);  // 입는 데미지
  void move(int x, int y);            // 새로운 위치

  void show_status();  // 상태를 보여준다.
  static void show_total_marine();
  ~Marine() { total_marine_num--; }
};
int Marine::total_marine_num = 0;
void Marine::show_total_marine() {
  std::cout << "전체 마린 수 : " << total_marine_num << std::endl;
}
Marine::Marine()
    : hp(50), coord_x(0), coord_y(0), default_damage(5), is_dead(false) {
  total_marine_num++;
}

Marine::Marine(int x, int y)
    : coord_x(x), coord_y(y), hp(50), default_damage(5), is_dead(false) {
  total_marine_num++;
}

Marine::Marine(int x, int y, int default_damage)
    : coord_x(x),
      coord_y(y),
      hp(50),
      default_damage(default_damage),
      is_dead(false) {
  total_marine_num++;
}

void Marine::move(int x, int y) {
  coord_x = x;
  coord_y = y;
}
int Marine::attack() { return default_damage; }
void Marine::be_attacked(int damage_earn) {
  hp -= damage_earn;
  if (hp <= 0) is_dead = true;
}
void Marine::show_status() {
  std::cout << " *** Marine *** " << std::endl;
  std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) "
            << std::endl;
  std::cout << " HP : " << hp << std::endl;
  std::cout << " 현재 총 마린 수 : " << total_marine_num << std::endl;
}

void create_marine() {
  Marine marine3(10, 10, 4);
  Marine::show_total_marine();
}
int main() {
  Marine marine1(2, 3, 5);
  Marine::show_total_marine();

  Marine marine2(3, 5, 10);
  Marine::show_total_marine();

  create_marine();

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

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

static 함수는 앞에서 이야기 한 것과 같이, 어떤 객체에 종속되는 것이 아니라 클래스에 종속되는 것으로, 따라서 이를 호출하는 방법도 (객체).(멤버 함수) 가 아니라,

Marine::show_total_marine();

와 같이 (클래스)::(static 함수) 형식으로 호출하게 됩니다. 왜냐하면 어떠한 객체도 이 함수를 소유하고 있지 않기 때문이죠. 그렇기 때문에 static 함수 내에서 클래스의 static 변수 만을 이용할 수 밖에 없습니다. 만일 static 함수 내에서 아래처럼 클래스의 멤버 변수들을 이용한다면

void Marine::show_total_marine() {
  std::cout << default_damage << std::endl;  // default_damage 는 멤버 변수
  std::cout << "전체 마린 수 : " << total_marine_num << std::endl;
}

default_damage 가 누구의 default_damage 인지 아무도 모르는 상황이 발생합니다. 즉, 어떤 객체의 default_damage 인지 static 변수인 show_total_marine() 은 알 수 없습니다. 그 이유는 앞에서 계속 말해왔듯이 어떤 객체에도 속해있지 않기 때문이죠


this


// 자기 자신을 가리키는 포인터 this
#include <iostream>

class Marine {
  static int total_marine_num;
  const static int i = 0;

  int hp;                // 마린 체력
  int coord_x, coord_y;  // 마린 위치
  bool is_dead;

  const int default_damage;  // 기본 공격력

 public:
  Marine();              // 기본 생성자
  Marine(int x, int y);  // x, y 좌표에 마린 생성
  Marine(int x, int y, int default_damage);

  int attack();                          // 데미지를 리턴한다.
  Marine& be_attacked(int damage_earn);  // 입는 데미지
  void move(int x, int y);               // 새로운 위치

  void show_status();  // 상태를 보여준다.
  static void show_total_marine();
  ~Marine() { total_marine_num--; }
};
int Marine::total_marine_num = 0;
void Marine::show_total_marine() {
  std::cout << "전체 마린 수 : " << total_marine_num << std::endl;
}
Marine::Marine()
    : hp(50), coord_x(0), coord_y(0), default_damage(5), is_dead(false) {
  total_marine_num++;
}

Marine::Marine(int x, int y)
    : coord_x(x),
      coord_y(y),
      hp(50),

      default_damage(5),
      is_dead(false) {
  total_marine_num++;
}

Marine::Marine(int x, int y, int default_damage)
    : coord_x(x),
      coord_y(y),
      hp(50),
      default_damage(default_damage),
      is_dead(false) {
  total_marine_num++;
}

void Marine::move(int x, int y) {
  coord_x = x;
  coord_y = y;
}
int Marine::attack() { return default_damage; }
Marine& Marine::be_attacked(int damage_earn) {
  hp -= damage_earn;
  if (hp <= 0) is_dead = true;

  return *this;
}
void Marine::show_status() {
  std::cout << " *** Marine *** " << std::endl;
  std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) "
            << std::endl;
  std::cout << " HP : " << hp << std::endl;
  std::cout << " 현재 총 마린 수 : " << total_marine_num << std::endl;
}

int main() {
  Marine marine1(2, 3, 5);
  marine1.show_status();

  Marine marine2(3, 5, 10);
  marine2.show_status();

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

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

위에 코드는 어떤 것이 변경되었는지 가장 먼저 눈에 띄는 것은 바로 레퍼런스를 리턴하는 함수와 this 라는 것인데, 하나씩 살펴보도록 하겠습니다.

Marine& Marine::be_attacked(int damage_earn) {
  hp -= damage_earn;
  if (hp <= 0) is_dead = true;

  return *this;
}

일단 this 라는 것이 C++ 언어 차원에서 정의되어 있는 키워드인데, 이는 객체 자신을 가리키는 포인터의 역할을 합니다. 즉, 이 멤버 함수를 호출하는 객체 자신을 가리킨다는 것입니다. 따라서, 실제로 위 내용은

Marine& Marine::be_attacked(int damage_earn) {
  this->hp -= damage_earn;
  if (this->hp <= 0) this->is_dead = true;

  return *this;
}

과 동일한 의미가 됩니다. 실제로 모든 멤버 함수 내에서 this 키워드가 정의되어 있으며 클래스 안에서 정의된 함수 중에서 this 키워드가 없는 함수는 당연하게도 static 함수 뿐입니다.

그러면 이제 Marine& 을 리턴한다는 말이 도대체 무엇인지 생각해봅시다. 이전에 배웠던 내용으로는 레퍼런스라는 것이 어떤 변수의 다른 별명이라고 했습니다. (실제로 레퍼런스를 별명(alias)라고 부르기도 합니다.)

그런데 그 별명을 리턴한다니, 무슨말 일까요? 이제 레퍼런스를 리턴하는 함수에 대해서 알아봅시다.


레퍼런스를 리턴하는 함수


// 레퍼런스를 리턴하는 함수
#include <iostream>

class A {
  int x;

 public:
  A(int c) : x(c) {}

  int& access_x() { return x; }
  int get_x() { return x; }
  void show_x() { std::cout << x << std::endl; }
};

int main() {
  A a(5);
  a.show_x();

  int& c = a.access_x();
  c = 4;
  a.show_x();

  int d = a.access_x();
  d = 3;
  a.show_x();

  // 아래는 오류
  // int& e = a.get_x();
  // e = 2;
  // a.show_x();

  int f = a.get_x();
  f = 1;
  a.show_x();
}

access_xx 의 레퍼런스를 반환하게 되고, get_xx 의 값을 반환하게 됩니다. 실제로 이들이 어떻게 작동하는지 살펴보도록 하겠습니다.

int& c = a.access_x();
c = 4;
a.show_x();

여기서 레퍼런스 cx 의 레퍼런스, 즉 x 의 별명을 받았습니다. 따라서, cx 의 별명으로 탄생하게 되는 것입니다. 레퍼런스를 리턴하는 함수는 그 함수 부분을 원래의 변수로 치환했다고 생각해도 상관이 없습니다. 다시 말해서

int &c = x; // 여기서 x 는 a 의 x

와 동일한 말이라는 것입니다. 따라서 c 의 값을 바꾸는 것은 ax 의 값을 바꾸는 것과 동일한 의미이므로 show_x 를 실행 시에 x 의 값이 5에서 4로 바뀌었음을 알 수 있습니다. 그렇다면 아래 예도 살펴봅시다.

int d = a.access_x();
d = 3;
a.show_x();

이번에는 int& 가 아닌 그냥 int 변수에 x 의 별명을 전달했습니다. 만일 dint& 였다면, x 의 별명을 받아서 d 역시 또 다른 x 의 별명이 되었겠지만, d 가 그냥 int 변수 이므로 값의 복사가 일어나 d에는 x 의 값이 들어가게 됩니다. 그리고 당연히 dx 의 별명이 아닌 또 다른 독립적인 변수이기에 d = 3;을 해도 x 의 값은 바뀌지 않은 채, 그냥 4가 출력되게 되죠.

한 가지 재미있는 점은

a.access_x() = 3;

위 문장이 잘 작동한다는 점인데, 앞에서도 말했지만 레퍼런스를 리턴하는 함수는 그 함수 부분을 리턴하는 원래 변수로 치환해도 된다 라는 말이 명확히 들어맞는 다는 점입니다. 즉, 위 문장은 결국

a.x = 3;

과 동일한 말이 됩니다.

그럼 이제 다시 예전의 Marine 예제로 돌아가보도록 합시다.

Marine& Marine::be_attacked(int damage_earn) {
  this->hp -= damage_earn;
  if (this->hp <= 0) this->is_dead = true;

  return *this;
}

위 경우 be_attacked 함수는 Marine& 타입을 반환하게 되는데, 위 경우 *this 를 리턴하게 됩니다. 따라서 그 객체 자신을 반환하게 됩니다. 따라서

marine2.be_attacked(marine1.attack()).be_attacked(marine1.attack());

위에 명령에 경우 먼저 marine2.be_attacked(marine1.attack()) 이 먼저 실행되고 리턴되는 것이 다시 marine2 이므로 그 다음에 또 한번 marine2.be_attacked(marine1.attack()) 가 실행된다고 생각할 수 있습니다.

Marine Marine::be_attacked(int damage_earn) {
  this->hp -= damage_earn;
  if (this->hp <= 0) this->is_dead = true;

  return *this;
}

만일 위와 같이 바뀐다면,

marine2.be_attacked(marine1.attack()).be_attacked(marine1.attack());

위에 코드를 실행해보면 marine2는 실제로 두 번 공격이 아닌 1번 공격으로 감소한 HP 를 갖게됩니다. 그 이유는 리턴 타입이 Marine 으로 바뀐 이후 반환되는 *this의 값은 새로운 객체에 복사 생성자를 통해 복사됩니다. 그리고 엉뚱하게 새롭게 복사된 객체가 be_attacked 함수를 호출하게 되므로 marine2 는 한 번만 공격을 받게됩니다.


const 함수


C++ 에서는 변수들의 값을 바꾸지 않고 읽기만 하는 마치 상수 같은 멤버함수를 상수 함수로써 선언할 수 있습니다. 아래의 예제를 살펴봅시다.

// 상수 멤버 함수
#include <iostream>

class Marine {
  static int total_marine_num;
  const static int i = 0;

  int hp;                // 마린 체력
  int coord_x, coord_y;  // 마린 위치
  bool is_dead;

  const int default_damage;  // 기본 공격력

 public:
  Marine();              // 기본 생성자
  Marine(int x, int y);  // x, y 좌표에 마린 생성
  Marine(int x, int y, int default_damage);

  int attack() const;                    // 데미지를 리턴한다.
  Marine& be_attacked(int damage_earn);  // 입는 데미지
  void move(int x, int y);               // 새로운 위치

  void show_status();  // 상태를 보여준다.
  static void show_total_marine();
  ~Marine() { total_marine_num--; }
};
int Marine::total_marine_num = 0;
void Marine::show_total_marine() {
  std::cout << "전체 마린 수 : " << total_marine_num << std::endl;
}
Marine::Marine()
    : hp(50), coord_x(0), coord_y(0), default_damage(5), is_dead(false) {
  total_marine_num++;
}

Marine::Marine(int x, int y)
    : coord_x(x),
      coord_y(y),
      hp(50),

      default_damage(5),
      is_dead(false) {
  total_marine_num++;
}

Marine::Marine(int x, int y, int default_damage)
    : coord_x(x),
      coord_y(y),
      hp(50),
      default_damage(default_damage),
      is_dead(false) {
  total_marine_num++;
}

void Marine::move(int x, int y) {
  coord_x = x;
  coord_y = y;
}
int Marine::attack() const { return default_damage; }
Marine& Marine::be_attacked(int damage_earn) {
  hp -= damage_earn;
  if (hp <= 0) is_dead = true;

  return *this;
}
void Marine::show_status() {
  std::cout << " *** Marine *** " << std::endl;
  std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) "
            << std::endl;
  std::cout << " HP : " << hp << std::endl;
  std::cout << " 현재 총 마린 수 : " << total_marine_num << std::endl;
}

int main() {
  Marine marine1(2, 3, 5);
  marine1.show_status();

  Marine marine2(3, 5, 10);
  marine2.show_status();

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

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

사실 위 소스는 거의 바뀐 것이 없고 단순히 예시를 위해 아래와 같이 attack 함수를 살짝 바꾸었습니다.

int attack() const; // 데미지를 리턴한다.

일단 상수 함수는 위와 같은 형태로 선언을 하게 됩니다. 즉,

(기존의 함수의 정의) const;

그리고 함수의 정의 역시 아래와 같이 const 키워드를 꼭 넣어주어야 합니다.

int Marine::attack() const { return default_damage; }

그렇게 했으면 위 attack 함수는 상수 멤버 함수로 정의된 것입니다. 우리는 상수 함수로 이 함수를 정의함으로써, 이 함수는 다른 변수의 값을 바꾸지 않는 함수라는 다른 프로그래머에게 명시 시킬 수 있습니다. 당연하게도, 상수 함수 내에서 객체들의 읽기만이 수행되며, 상수 함수 내에서 호출 할 수 있는 함수로는 다른 상수 함수 밖에 없습니다.

사실 많은 경우 클래스를 설계할 때, 멤버 변수들은 모두 private 에 넣고, 이 변수들의 값에 접근하는 방법으로 get_x 함수 처럼 함수를 public 에 넣어 이 함수를 이용해 값을 리턴받는 형식을 많이 사용합니다. 이렇게 하면 멤버 변수들을 private 에 넣음으로써 함부로 변수에 접근하는 것을 막고 또 그 값은 자유롭게 구할 수 있게 됩니다.

⤧  Next post Oh-My-C-Lang - Section 5 - 04 ⤧  Previous post Oh-My-C-P-P 04