공부/C || C++

복사생성자란(copy constructor)?

sudo 2021. 7. 5. 01:28

1학년때 C++를 배울때 copy constructor를 처음 접했을땐 어려워서 열심히 공부해서 겨우겨우 이해해서 시험친 기억이 난다. 그때 기억으로는 다 이해했고 안까먹을거라 생각하고 넘어갔지만 막상 몇년이 지나니 까먹은 것 같아서 이 기회에 다시 정리한다.

 

1. 왜 써야 하는가?

우선 copy constructor라는걸 왜 써야하는지에 대한 의문은 쓰지 않았을 때 생기는 문제에 대해 생각해보면 쉽게 해결된다. 예를 들어 다음와 같은 코드가 있다고 하자

class person {

	int m_tAge;
	char* m_pName;

public:
	person() { }
	person(int _age, char* _name)
	{
		m_tAge = _age;
		m_pName = new char[strlen(_name) + 1];
		strcpy_s(m_pName, strlen(m_pName) + 1,
			_name );
	}
	~person() { delete m_pName; }
	
};


int main()
{
	person A(20, "Paul");
	person B(A); // default 복사 생성자 호출

	return 0;
}

이 프로그램은 double free로 인한 crash가 난다. 이유는 A가 B가 생성과 동시에 복사 생성자가 호출됐는데 위의 코드에선 복사 생성자가 따로 정의되어 있지 않기 때문에 디폴드 복사 생성자가 호출 될 것이다. 디폴드 복사 생성자는 얕은 복사(shallow copy)를 한다.

 

2. 얕은 복사(shallow copy) vs 깊은 복사(deep copy)?

얕은 복사란 흔히 포인터 값만 복사하는 것을 의미하고 깊은 복사는 포인터가 가리키는 값 자체를 복사하는 것을 의미한다. 위의 예시에선 m_pName = _name 이런식으로 쓰이면 얕은 복사이며 strcpy_s(m_pName, strlen(m_pName)+1, _name) 이런식으로 쓰이면 깊은 복사를 하는 것이다.

 

3. 복사 생성자는 언제 호출 되는가?

위의 예시에서는 person B(A) 코드를 작성하니 복사 생성자가 호출되었는데, 복사 생성자들은 언제 호출되는 걸까

1. 위의 예시처럼 기존에 생성된 객체로 새로 만들어진 객체를 초기화 할때

2. 함수가 반환값으로 객체를 리턴할 때

3. 함수 인자로 객체를 전달할 때

 

4. 인자 형태는 왜 const에 &를 달아준 형태가 되어야 하는가?

우선 const를 달아줘야 하는 이유는 명확하다. 말 그대로 '복사'생성자니까 값을 복사해주는 과정에서 값이 바뀌면 안된다. 그렇다면 왜 참조 연산자(&)가 붙어야 하는걸까.

 

위에서 함수 인자가 객체일 때 복사 생성자가 호출된다고 했다. 그런데 복사 생성자의 파라미터 형식이 객체이다. 이 말은 복사 생성자가 한번 불리면 복사 생성자로 들어온 인자 때문에 복사 생성자가 또 불리고 계속해서 불리는 무한 루프에 빠질 것이다. 그런데 복사 생성자의 인자에 참조자를 붙여주면 생성자나 복사 생성자가 호출되지 않는다. 그래서 &를 꼭 붙여줘야 한다.

 

copy constructor뿐만 아니라 모든 객체를 인자로 받는 함수에서는 &를 붙이는걸 권장하는데 아래와 같은 경우 때문이다.

class person {

	int m_tAge;
	char* m_pName;

public:
	person() { cout << "calling constructor" << endl; }
	person(int _age, char* _name)
	{
		m_tAge = _age;
		m_pName = new char[strlen(_name) + 1];
		strcpy_s(m_pName, strlen(_name) + 1,
			_name);
	}
	person(const person& p)
	{
		cout << " calling copy constructor" << endl;
		m_tAge = p.m_tAge;
		m_pName = new char[strlen(p.m_pName) + 1];
		strcpy_s(m_pName, strlen(p.m_pName) + 1,
			p.m_pName);
	}

	~person()
	{
		cout << "calling destructor" << endl;
		delete m_pName;
	}

	char* getName() { return m_pName; }
};

void showName(person s) { cout << "My name is " << s.getName() << endl; }

int main()
{
	person A(20, "Paul");
	showName(A);

	return 0;
}

showName함수가 추가되었는데 이 함수는 객체를 레퍼런스로 받지 않고 그냥 값으로(Call by value)로 받고 있다. 당연히 이렇게 되면 showName로 점프하기 전에 복사 생성자를 호출하고 A를 복사한 또 다른 객체가 showName함수에게 전달될 것이다. 그리고 showName 함수가 끝나자 마자 복사된 객체에 대해서 소멸자가 호출된다. 이렇게 되면 불필요하게 복사 생성자와 소멸자가 한번씩 호출되어서 낭비다.

showName 인자에 참조자를 쓴 경우
showName 함수에 참조자를 쓰지 않은 경우

 

따라서 객체를 인자로 받는 함수의 경우 참조자를 붙여주는게 좋다