관리 메뉴

기억을 위한 기록들

[CPP-effective] 2-7 객체의 모든 부분을 빠짐없이 복사하자. 본문

C & CPP/Effective C++

[CPP-effective] 2-7 객체의 모든 부분을 빠짐없이 복사하자.

에드윈H 2021. 3. 30. 14:01

객체지향 시스템 중 설계가 잘 된 것들을 보면, 객체를 복사하는 함수가 딱 둘만 있는 것을 알 수 있습니다.

그것은 바로 복사 생성자복사 대입 연산자입니다. 이 둘을 통틀어 복사함수라고 부른다.

 

이 둘은 컴파일러가 필요에 따라 만들어내기도 한다. 이렇게 컴파일러가 생성한 복사 함두는 비록 자동으로 만들어졌지만, 동작은 기본적인 요구에 아주 충실하다!. 빠짐없이 잘 복사한다. 

 

그러나 기본적으로 만들어진것 말고 직접 선언한다는 것은, 기본 동작에 뭔가 마음에 안드는 것이 있다는 것인데, 직접 이런 복사함수들을 구현할 때 틀린 부분이 있더라도 컴파일러는 이런 부분들을 짚어 주지 않는다는 것이다.

 

 

예를 들어 어떤 객체를 대입하는데 직접 작성한 다음에 추후에 나중에 클래스에서 멤버변수라도 새로 추가하게 된다면, 직접 작성한 복사함수로 가서 이 멤버변수에 대한 복사를 해줘야 한다는 것이다.(까먹게 된다면 그렇게 끝이다.. 컴파일러는 알려주지 않는다.)

 

그런데 이런 경우는 양반이고, 골때리는 것은 바로 상속 관계가 있는 클래스에서의 복사함수들이다.

 

class Character{
private:
    int hp;
};


class Player : public Character{

	Player(const Player& ref)
      : name(ref.name)
    {
       //...
    };
    
	Player& operator=(const Player& ref)
    {
      //..	
    };
private:
    char* name;
};

위의 코드는 문제가 없는 것 같지만, 문제가 있다. 바로 Player(const Player& ref); 복사생성자는 Character를 상속하고 있는 클래스이다. 그 말은 Character 클래스에 있는 hp정보는 새로 생길 복사생성자에게 전달되지 않고, Character는 기본생성자를 호출하면서 기본 생성자의 hp값(0일수도?)이 들어가게 될것이다.

 

그리고 또 복사생성자 외에도 마찬가지로 대입연산자 operator=에서도 처리를 해줘야한다.

class Character{
private:
    int hp;
};


class Player : public Character{

	Player(const Player& ref)
      :  Character(ref)  //추가
      ,  name(ref.name)
    {
       //...
    };
    
	Player& operator=(const Player& ref)
    {
      Character::operator=(ref); //추가
      //..	
    };
private:
    char* name;
};

이렇게 두개의 복사함수에서 "모든 부분을 빠짐 없이 복사 해줘야 한다."

두가지를 꼭 확인해야한다.

1. 해당 클래스의 데이터 멤버를 모두 복사

2. 이 클래스가 상속한 기본 클래스(부모)의 복사 함수도 꼬박꼬박 호출해 주도록하자.

 

어쩌다보니, 복사 생성자와 복사 대입 연산자의 코드 본문이 비슷하게 나온다는 느낌이 들면, 양쪽에서

겹치는 부분을 별도의 멤버 함수에 분리 해놓은 후 이 함수를 호출 하게 만드는 것이다. 대개 이런 용도의 함수는 private멤버로 두는 경우가 많고 이름이 init 어쩌구 하는 이름을 가진다. 안전할뿐만 아니라 검증된 방법으로, 복사 생성자와 복사 대입연산자에 나타나는 코드 중복을 제거하는 방법으로 사용해보기 바란다.

 

 

정리

* 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 클래스 부분을 빠트리지 말고 복사해야한다.

* 클래스의 복사 함수 두개(복사생성자,복사대입연산자)를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지마세요. 그 대신 공통된 동작의 제3의 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결하자. 

 

 

복사함수에서 중복코드를 피하기 위한 init함수 테스트

#include<iostream>
using namespace std;

class Character {
public:
	int hp;
};

class Player : public Character {
public:
	Player()
		: name(nullptr)
		, attack(0)
		, def(0)
	{};
	Player(const Player& ref)
		: Character(ref)  
		, name(ref.name)
	{
		//...
		Init(ref); //호출
	};

	Player& operator=(const Player& ref)
	{
		if(this == &ref)
		{
			return *this;
		}
		Character::operator=(ref); 
		//..	
		Init(ref); //호출

		return *this;
	};

	void SetAttack(const int& ref)
	{
		attack = ref;
	}
	void SetDef(const int& ref)
	{
		def = ref;
	}
private:
	void Init(const Player& ref)
	{
		attack = ref.attack;
		def = ref.def;
		
	};
	char* name;
	int attack;
	int def;
};

int main() {
	Player a;
	Player b;
	a.hp = 10;
	b.SetAttack(150);
	b.SetDef(50);
	b.hp = 16;
    
	a = b; //복사 대입 연산자

	Player c;
	c.hp = 50;
	c.SetAttack(20);
	c.SetDef(10);
    
	Player d(c);//복사 생성자
    
	return 0;
}