[C++] 생성자 / 소멸자

muchan
muchan
∙∙조회 7

https://modoocode.com/173

https://modoocode.com/188

위 싸이트에서 공부한 내용 정리입니다.


함수 오버로딩

함수 오버로딩은 같은 이름의 함수를 매개변수의 개수나 타입을 다르게 하여 여러 개 정의하는 기능이다.

컴파일러는 함수 호출 시 전달된 인자의 개수와 타입을 기준으로 적절한 함수를 선택한다.

함수 오버로딩 조건

  • 함수 이름이 같아야 한다.
  • 매개변수의 개수 또는 타입이 달라야 한다.
  • 반환 타입만 다른 경우에는 오버로딩할 수 없다.
  • 매개변수 이름만 다른 경우에도 오버로딩할 수 없다.

컴파일러에서 함수를 오버로딩하는 조건

1단계

  • 자신과 타입이 정확히 일치하는 함수를 찾는다

2단계

정확히 일치하는 타입이 없는 경우 아래와 같은 형변환을 통해서 일치하는 함수를 찾아본다.

  • Char, unsigned char, short 는 int 로 변환된다.
  • Unsigned short 는 int 의 크기에 따라 int 혹은 unsigned int 로 변환된다.
  • Float 은 double 로 변환된다.
  • Enum 은 int 로 변환된다.

3단계

위와 같이 변환해도 일치하는 것이 없다면 아래의 좀더 포괄적인 형변환을 통해 일치하는 함수를 찾는다.

  • 임의의 숫자(numeric) 타입은 다른 숫자 타입으로 변환된다. (예를 들어 float -> int)
  • Enum 도 임의의 숫자 타입으로 변환된다 (예를 들어 Enum -> double)
  • 0 은 포인터 타입이나 숫자 타입으로 변환된 0 은 포인터 타입이나 숫자 타입으로 변환된다
  • 포인터는 void 포인터로 변환된다.

4단계

  • 유저 정의된 타입 변환으로 일치하는 것을 찾는다 (이 부분은 나중에 나온다고 함)

생성자

생성자는 객체 생성 시 자동으로 호출되는 함수다

scss
Date day(2011, 3, 1);         // 암시적 방법 (implicit)
Date day = Date(2012, 3, 1);  // 명시적 방법 (explicit)

마치 함수를 호출하듯이 사용하는 것이 암시적 방법, 명시적으로 생성자를 호출한다는 것을 보여주는 것이 명시적 방법 인데 많은 경우 암시적 방법으로 축약해서 쓸 수 있으므로 이를 선호한다고 함

디폴트 생성자

디폴트 생성자는 인자 없이 호출할 수 있는 생성자로, 객체 생성 시 초기화를 담당합니다.

perl
class Test {
 public:
  Test() = default;  // 디폴트 생성자를 정의해라
};

위 방법은 C++11부터 지원되는 명시적 디폴트 생성자다

소멸자

소멸자는 객체의 수명이 끝날 때 자동으로 호출되며, 객체가 사용한 자원을 정리하는 역할을 하는 특수한 멤버 함수다.

scss
class Marine {
  int hp;                // 마린 체력
  int coord_x, coord_y;  // 마린 위치
  int damage;            // 공격력
  bool is_dead;
  char* name;  // 마린 이름

 public:
  Marine();                                       // 기본 생성자
  Marine(int x, int y, const char* marine_name);  // 이름까지 지정
  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::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;
}

int main() {
  Marine* marines[100];

  marines[0] = new Marine(2, 3, "Marine 2");
  marines[1] = new Marine(1, 5, "Marine 1");

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

  std::cout << std::endl << "마린 1 이 마린 2 를 공격! " << std::endl;

  marines[0]->be_attacked(marines[1]->attack());

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

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

여기서 name은 생성자에서 동적으로 할당해주는데, 여기서 동적 할당된 name은 언제 delete해주나?
main 마지막에 delete 가 있지만 이건 marines 객체를 소멸시키고 marine안에 있는 name은 그대로 남아있어
큰 프로그램에서 name이 쌓이게 되면 메모리 누수가 발생한다고 한다.

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

이럴때를 위해 소멸자를 정의하고 name을 따로 소멸 시켜줘야한다

특징

  • 소멸자는 오버로딩이 불가능하다
  • C++에는 디폴트 소멸자가 존재하여 소멸자가 필요 없는 클래스라면 작성하지 않아도 된다

복사 생성자

r
T(const T& a);

위 식은 복사 생성자의 표준 형식이다.

ini
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;
} 

int main() {
  Photon_Cannon pc1(3, 3);
  Photon_Cannon pc2(pc1);
  Photon_Cannon pc3 = pc2;

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

해당 예제에서

arduino
Photon_Cannon::Photon_Cannon(const Photon_Cannon& pc)

이 문장이 복사 생성자이고

ini
Photon_Cannon pc2(pc1);
Photon_Cannon pc3 = pc2;

이런식으로 사용할 수 있다

디폴트 복사 생성자

위의 예제에서 복사 생성자를 지우고 실행해도 똑같이 동작하기에
복사 생성자는 아래와 같은 형식일 것이다

ini
Photon_Cannon::Photon_Cannon(const Photon_Cannon& pc) {
  hp = pc.hp;
  shield = pc.shield;
  coord_x = pc.coord_x;
  coord_y = pc.coord_y;
  damage = pc.damage;
}

디폴트 복사 생성자의 한계

csharp
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();
};

하지만 이런식으로 char *name 이라는 멤버 변수가 들어가게 된다면 런타임 오류가 발생한다

ini
Photon_Cannon::Photon_Cannon(const Photon_Cannon& pc) {
  hp = pc.hp;
  shield = pc.shield;
  coord_x = pc.coord_x;
  coord_y = pc.coord_y;
  damage = pc.damage;
  name = pc.name;
}

유추한 디폴트 복사 생성자는 위와 같은 구조일 것이다

하지만 여기서 name을 그냥 할당하는건 name의 값이 아닌 시작 주소를 넘기는 것이다

그래서 결국은 이런 구조인 것이다(같은 주소를 가리킴)

하지만 여기서 문제는 같은 값을 가리키는게 문제가 아니고
소멸자가 호출될 때 문제가 된다.

pc1과 pc2를 delete 하면 pc1에서 name을 소멸시키고 이후 pc2의 소멸자가 호출될 때
없는 값을 소멸시키려고 해서 런타임 에러가 나는 것이다.

ini
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);
}

그래서 복사 생성자에서 주소 공간을 새로 하나 만들어서 값을 넣는 것이다.

이렇게 일반 디폴트 생성자에서 해주는 복사를 얕은 복사라 하고
위처럼 직접 주소를 만들어서 값을 할당해주는 복사를 깊은 복사라고 한다.