본문 바로가기
개발/C, C++

C++ - 복사 생성자를 이용한 객체의 깊은 복사 (Deep Copy using Copy Constructor)

by 피로물든딸기 2021. 8. 10.
반응형

C, C++ 전체 링크

 

링크의 코드에 setValue를 추가하고 아래의 코드를 실행해보자.

#include <stdio.h>
#include <iostream>

using namespace std;

class TEST
{
public:
	int value1;
	int value2;

	TEST()
		: value1(1), value2(2)
	{
		cout << "생성자 호출" << endl;
	}

	TEST(int val1, int val2)
	{
		value1 = val1;
		value2 = val2;
		cout << "생성자 호출 2" << endl;
	}

	void setValue(int val1, int val2)
	{
		value1 = val1;
		value2 = val2;
	}

	void printValue()
	{
		cout << value1 << ", " << value2 << endl;
	}
};

int main(void)
{
	TEST test1 = TEST(3, 4);
	TEST test2 = test1;
	TEST test3(test1);

	test1.setValue(5, 6);

	test1.printValue();
	test2.printValue();
	test3.printValue();

	return 0;
}

 

test1의 value를 3, 4로 초기화한 후, test2, 3에 복사하였다.

test1을 setValue로 변경하여도 test2, 3은 변경되지 않는 것을 알 수 있다.

 

test2 = test1과 test3(test1)의 방법으로 객체를 복사할 수 있는데, 이때 아래와 같은 복사 생성자가 정의되어있다.

class TEST
{
    TEST(const TEST& test)
		: value1(test.value1), value2(test.value2)
	{

	}
    
    ...
}

 

실제 복사 생성자가 호출 되는지 아래의 코드를 추가해보자.

class TEST
{
    TEST(const TEST& test)
		: value1(test.value1), value2(test.value2)
	{
		cout << "Copy Constructor Start" << endl;
		cout << test.value1 << ", " << test.value2 << endl;
		cout << "Copy Constructor End" << endl;
	}
    
    ...
}

 

복사 생성자는 복사가 일어날 때, 항상 호출된다.

아래의 copyTest 함수에서 TEST 객체의 값을 10, 20으로 바꿔보자.

void copyTest(TEST test)
{
	test.setValue(10, 20);
}

int main(void)
{
	TEST test1 = TEST(3, 4);
	TEST test2 = test1;
	TEST test3(test1);

	test1.setValue(5, 6);

	test1.printValue();
	test2.printValue();
	test3.printValue();

	printf("\n------ copy test -----\n\n");

	copyTest(test3);
	test3.printValue();

	return 0;
}

 

copyTest에서 test3을 copy하였기 때문에 복사 생성자가 호출되었고, copy한 값을 변경하였으므로,

원본 test3에는 변경이 없다.

 

이 함수를 원래 의도대로 실행하려면 참조형식으로 값을 넘겨주어야 한다.

그러면 의도하지 않은 복사 생성자도 호출되지않고, 값도 정상적으로 변경된다.

void copyTest(TEST& test) 
{
	test.setValue(10, 20);
}


포인터 복사

 

이제 value2pointer로 변경해보자.

value2가 int ptr이므로, new로 할당하였고, 값을 출력하는 부분, 대입 부분에 * 연산자를 이용하여 처리하였다.

#include <stdio.h>
#include <iostream>

using namespace std;

class TEST
{
public:
	int value1;
	int* value2;

	TEST()
		: value1(1), value2(nullptr)
	{
		cout << "생성자 호출" << endl;
	}

	TEST(int val1, int val2)
	{
		value1 = val1;
		value2 = new int;
		*value2 = val2;
		cout << "생성자 호출 2" << endl;
	}

	TEST(const TEST& test)
		: value1(test.value1), value2(test.value2)
	{
		cout << "Copy Constructor Start" << endl;
		cout << test.value1 << ", " << *test.value2 << endl;
		cout << "Copy Constructor End" << endl << endl;
	}

	void setValue(int val1, int val2)
	{
		value1 = val1;
		*value2 = val2;
	}

	void printValue()
	{
		cout << value1 << ", " << *value2 << endl;
	}
};

void copyTest(TEST& test)
{
	test.setValue(10, 20);
}

int main(void)
{
	TEST test1 = TEST(3, 4);
	TEST test2 = test1;
	TEST test3(test1);

	test1.setValue(5, 6);

	test1.printValue();
	test2.printValue();
	test3.printValue();

	printf("\n------ copy test -----\n\n");

	copyTest(test3);
	test3.printValue();

	return 0;
}

 

test1의 setValue를 이용하여 값을 5, 6으로 바꾸었다. 하지만 test2, 3의 값도 6으로 바뀌었다. 

value2(test.value2)에서 pointer 복사하였지만, 이것은 깊은 복사(deep copy)가 아닌 얕은 복사가 발생한 것이다.

내부적으로 아래와 같은 코드가 실행될 것이다.

// test1의 TEST(int val1, int val2)에 의해 아래의 코드가 실행
int* value2_test1 = new int;
*value2_test1 = 6;

// test2의 복사 생성자에 의해 value1(test.value1), value2(test.value2) 실행
int* value2_test2 = value2_test1;

즉, test2의 value2는 복사 생성자에 의해 test1의 value2를 가르키고 있다.

 

이 현상을 좀 더 확인하기 위해 소멸자를 이용해보자.

 

new로 value2를 동적 할당하였으므로, delete를 이용해 삭제하는 코드를 추가한다.

이 경우에는 소멸자에 추가하면 된다.

class TEST
{
	~TEST()
	{
		delete value2;
	}
    
    ...
}

 

하지만 이전 결과와 다르게 run time error가 발생한다.

 

왜 이런 현상이 발생하는지 보기 위해 소멸자에 print를 추가하자.

	~TEST()
	{
		printValue();
		delete value2;
	}

 

copy test 이후 10, 20은 test3.printValue();의 결과이다.

test3는 copyTest에서 setValue로 값이 10, 20으로 변경되었다.

하지만 value2의 얕은 복사로 test2, test1value2도 20이 된다.

 

이제 main이 종료되었으므로, test3의 소멸자가 호출된다.

test3의 소멸자가 호출되었으므로, test3value2는 delete로 해제된다.

(하지만 사실 test3value2test1value2가 된다. test2value2test1value2다.)

 

그런데 test2, test1value2도 메모리가 해제되었기 때문에 3, -572662307이라는 쓰레기 값이 나오게 되었다.

 

printValue로 출력을 한 후, delete로 value2또 해제하려고 하기 때문에

test1의 소멸자는 호출되지도 못하고 run time error가 발생하게 된다.

 

객체를 복사하였지만, 내부적으로 얕은 복사가 나타날 수 있기 때문에, 복사 생성자에서 이것을 처리해줘야 한다.

아래 처럼 코드를 고치면 각각의 객체가 원하는 value2를 가지게 된다.

	TEST(const TEST& test)
		: value1(test.value1) //, value2(test.value2) 삭제
	{
		cout << "Copy Constructor Start" << endl;
		
		value2 = new int;
		*value2 = *test.value2;

		cout << test.value1 << ", " << *test.value2 << endl;
		cout << "Copy Constructor End" << endl << endl;
	}

 

또는 아래와 같이 변수를 복사할 수 있다.

	TEST(const TEST& test)
		: value1(test.value1), value2(new int(*test.value2))

 

전체 수정 코드는 아래를 참고하자.

#include <stdio.h>
#include <iostream>

using namespace std;

class TEST
{
public:
	int value1;
	int* value2;

	TEST()
		: value1(1), value2(nullptr)
	{
		cout << "생성자 호출" << endl;
	}

	~TEST()
	{
		printValue();
		delete value2;
	}

	TEST(int val1, int val2)
	{
		value1 = val1;
		value2 = new int;
		*value2 = val2;
		cout << "생성자 호출 2" << endl;
	}

	TEST(const TEST& test)
		: value1(test.value1) // or , value2(new int(*test.value2))
	{
		cout << "Copy Constructor Start" << endl;
		
		value2 = new int;
		*value2 = *test.value2;

		cout << test.value1 << ", " << *test.value2 << endl;
		cout << "Copy Constructor End" << endl << endl;
	}

	void setValue(int val1, int val2)
	{
		value1 = val1;
		*value2 = val2;
	}

	void printValue()
	{
		cout << value1 << ", " << *value2 << endl;
	}
};

void copyTest(TEST& test)
{
	test.setValue(10, 20);
}

int main(void)
{
	int value = 10;
	int* ptr1 = &value;
	int* ptr2 = ptr1;

	TEST test1 = TEST(3, 4);
	TEST test2 = test1;
	TEST test3(test1);

	test1.setValue(5, 6);

	test1.printValue();
	test2.printValue();
	test3.printValue();

	printf("\n------ copy test -----\n\n");

	copyTest(test3);
	test3.printValue();

	return 0;
}

대입 연산자 

 

위와 같은 run time error는 객체를 대입할 때도 발생한다.

TEST test2 = test1; 에서는 복사 생성자가 호출되지만, 각각의 객체를 선언하고,

대입(=)할 경우는 대입 연산자가 호출된다.

class TEST
{
	~TEST()
	{
		//printValue();
		//delete value2;
	}
};

int main(void)
{
	TEST test1(3, 4);
	TEST test2(5, 6);

	test1.printValue();
	test2.printValue();

	test1 = test2;

	printf("\n--- set value ---\n\n");

	test1.setValue(100, 200);

	printf("\n--- print value ---\n\n");

	test1.printValue();
	test2.printValue();

	printf("\n--- main end ---\n\n");

	return 0;
}

 

이제 주석을 제거하고 결과를 실행해보자.

class TEST
{
	~TEST()
	{
		printValue();
		delete value2;
	}
};

 

위와 마찬가지로 value2의 얕은 복사가 일어나 두 번 메모리를 해제하는 현상이 발생하게 된다.

즉, 대입 연산자가 발생하는 경우에도 얕은 복사가 일어나지 않도록 코드를 추가해야 한다.

class TEST
{
	~TEST()
	{
		printValue();
		delete value2;
	}

	TEST& operator=(const TEST& test)
	{
		*value2 = *test.value2;
		return *this;
	}
    
    ...
}

 

value2의 얕은 복사가 일어나지 않고, 메모리도 정상적으로 해제되는 것을 알 수 있다.

#include <stdio.h>
#include <iostream>

using namespace std;

class TEST
{
public:
	int value1;
	int* value2;

	TEST()
		: value1(1), value2(nullptr)
	{
		cout << "생성자 호출" << endl;
	}

	~TEST()
	{
		printValue();
		delete value2;
	}

	TEST& operator=(const TEST& test)
	{
		*value2 = *test.value2;
		return *this;
	}

	TEST(int val1, int val2)
	{
		value1 = val1;
		value2 = new int;
		*value2 = val2;
		cout << "생성자 호출 2" << endl;
	}

	TEST(const TEST& test)
		: value1(test.value1)
	{
		cout << "Copy Constructor Start" << endl;
		
		value2 = new int;
		*value2 = *test.value2;

		cout << test.value1 << ", " << *test.value2 << endl;
		cout << "Copy Constructor End" << endl << endl;
	}

	void setValue(int val1, int val2)
	{
		value1 = val1;
		*value2 = val2;
	}

	void printValue()
	{
		cout << value1 << ", " << *value2 << endl;
	}
};

void copyTest(TEST& test)
{
	test.setValue(10, 20);
}

int main(void)
{
	TEST test1(3, 4);
	TEST test2(5, 6);

	test1.printValue();
	test2.printValue();

	test1 = test2;

	printf("\n--- set value ---\n\n");

	test1.setValue(100, 200);

	printf("\n--- print value ---\n\n");

	test1.printValue();
	test2.printValue();

	printf("\n--- main end ---\n\n");

	return 0;
}
반응형

댓글