본문 바로가기
코스웨어/16년 스마트컨트롤러

2016-09-28_조재찬_스터디일지_CPP-복사 생성자, 얕은 복사와 깊은 복사, 임시 객체

by 알 수 없는 사용자 2016. 9. 28.
728x90
반응형

C++ 스타일의 초기화

int num(20); // num=20;

int &ref(num); // &ref=num;


SoSimple sim2=sim1;

바로 위 문장은,

다음 문장과도 동일하게 해석된다.

SoSimple sim2(sim1);


객체 단위의 대입은 멤버 대 멤버의 복사가 일어난다. 이는 복사 생성자의 정의에 따라 달라질 수 있다.


 복사 생성자가 정의되지 않은 SoSimple 클래스
1
2
3
4
5
6
7
8
9
class SoSimple
{
private:
    int num;
public:
    SoSimple(int n) : num(n)
    {  }
    . . . .
};
cs

복사 생성자를 정의하지 않으면, 멤버 대 멤버 복사가 이뤄지는
default 복사 생성자가 아래와 같이 자동 삽입된다. (8-9행)
1
2
3
4
5
6
7
8
9
10
class SoSimple
{
private:
    int num;
public:
    SoSimple(int n) : num(n)
    {  }
    SoSimple(const SoSimple& copy) : num(copy.num)
    {  }
};
cs

복사 생성자를 활용한, 객체 단위의 대입 예제
#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num1;
	int num2;
public:
	SoSimple(int n1, int n2)
		: num1(n1), num2(n2)
	{
		// empty
	}

	SoSimple(const SoSimple &copy)	// 복사 생성자의 정의
		: num1(copy.num2), num2(copy.num1)	// num1, num2값을 swap하도록 이니셜라이저
	{
		cout << "Called SoSimple(SoSimple &copy)" << endl;
	}

	void ShowSimpleData()
	{
		cout << num1 << endl;
		cout << num2 << endl;
	}
};

int main(void)
{
	SoSimple sim1(15, 30);
	sim1.ShowSimpleData();
	cout << "생성 및 초기화 직전" << endl;	
	SoSimple sim2(sim1);	// SoSimple sim2=sim;
	cout << "생성 및 초기화 직후" << endl;
	sim2.ShowSimpleData();
	return 0;
}


Output:

1
2
3
4
5
6
7
15
30
생성 및 초기화 직전
Called SoSimple(SoSimple &copy)
생성 및 초기화 직후
30
15



키워드 explicit

: 묵시적 형 변환을 막는 키워드


SoSimple sim2=sim1;

위의 문장은, 

아래와 같이 묵시적 형 변환이 일어난다.

SoSimple sim2(sim1); 


위와 같은 묵시적 형 변환은 복사 생성자를 explicit으로 선언하면 막을 수 있다.

SoSimple sim2=sim1; 과 같은 형태의 객체 생성과 초기화를 막아줌


1
2
3
explicit SoSimple(const SoSimple &copy)
    : num1(copy.num1), num2(copy.num2)
{ }
cs


이는 복사 생성자 뿐만 아닌 생성자도 마찬가지이다.

1
2
3
4
5
6
7
8
class AAA
{
private:
    int num;
public:
    explicit AAA(int n) : num(n) { }
    . . . .
};
cs

AAA 생성자를 explicit 선언하면

다음과 같은 형태의 객체 생성은 불가능해진다.


AAA obj=3;

이는 아래와 같이 묵시적 형 변환이 이뤄지기 때문이다.

AAA obj(3);


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;
 
class SoSimple
{
private:
    int num1;
public:
    explicit SoSimple(int n1) // 생성자를 explicit 선언
        : num1(n1)
    {
        // empty
    }
    void ShowSimpleData()
    {
        cout << "num1 : " << num1 << endl;
    }
};
 
int main(void)
{
    SoSimple sim1(15);
    //SoSimple sim1=15;
    sim1.ShowSimpleData();    
    return 0;
}
cs

위의 소스에서 22행을 주석처리 후 23행의 주석 부분을 해제해  컴파일해보면, SoSimple로 변환하기 위한 적절한 생성자가 없으며,

초기화 중': 'int'에서 'SoSimple'(으)로 변환할 수 없다는 오류를 보게 된다.


c++의 대입의 의미가 다른 부분인데, 묵시적 호출을 원하지 않는다면 이렇게 explicit 키워드를 활용할 수 있다.




얕은 복사 (default 복사 생성자의 문제점)

#include <iostream>
#include <cstring>
using namespace std;

class Person
{
	char * name;
	int age;
public:
	Person(char * myname, int myage)	// 생성자 정의
	{
		int len = strlen(myname) + 1;
		name = new char[len];	// new 키워드를 통한 동적 할당
		strcpy(name, myname);
		age = myage;
	}
	void ShowPersonInfo() const
	{
		cout << "이름: " << name << endl;
		cout << "나이: " << age << endl;
	}
	~Person()
	{
		delete[]name;	// delete 키워드를 이용한 메모리 공간 해제
		cout << "called destructor!" << endl;
	}
};

int main(void)
{
	Person man1("Lee dong woo", 29);
	Person man2 = man1;		// default 복사 생성자에 의한 멤버대 멤버 복사가 이뤄짐
	man1.ShowPersonInfo();	
	man2.ShowPersonInfo();
	return 0;
}


Output :

이름: Lee dong woo

나이: 29

이름: Lee dong woo

나이: 29

called destructor!


위 예제 실행시 에러가 나며, 실행결과를 보면 문제점을 볼 수 있다.

25행에는 소멸자의 호출할 확인할 문장을 출력하도록 한 것을 볼 수 있는데, 실행결과에서는 소멸자가 딱 한번 호출된다.


이는 default 복사 생성자 호출에 의한 당연한 결과이다.


man1 객체의 name은 문자열이 저장된 메모리 공간은 가리키는 형태가 되며, 복사된 man2 객체의 name도 마찬가지로 이 메모리 공간을 가리킬 뿐이다. 

참조만 하는 멤버 name을 단순 복사했기 때문에 하나의 문자열을 두 객체가 가리키게 된 것이다.


또한 man1 객체가 소멸될 때 man1 객체의 멤버 name이 참조하는 문자열이 사라져버린다. ( delete [ ]name; )

이후 man2 객체가 소멸될 때 다음의 문장은 문제가 된다.


 delete [ ]name; 


이미 지워진 문자열을 대상으로 delete 연산을 하려 하는 것이다. 따라서 복사 생성자를 새롭게 정의해 이러한 문제를 해결 할 필요가 있다.



'깊은 복사'를 위한 복사 생성자의 정의

동일한 문자열을 포인터 참조하는 것이 아닌 객체의 문자열 자체를 복사해 참조하도록 해야한다.

따라서 이 경우 인자는 &ref 가 되게 된다.

1
2
3
4
5
6
7
Person(const Person &ref)     
        : age(ref.age)  // 이니셜라이저를 통한 초기화  
    {
        int len = strlen(ref.name) + 1;
        name = new char[len];    // new 키워드를 통한 동적 할당
        strcpy(name, ref.name);        
    }
cs


아래에서 깊은 복사를 위한 복사 생성자를 정의한 실행결과를 확인해볼 수 있다.

#include <iostream>
#include <cstring>
using namespace std;

class Person
{
	char * name;
	int age;
public:
	Person(char * myname, int myage)	// 생성자 정의
	{
		int len = strlen(myname) + 1;
		name = new char[len];	// new 키워드를 통한 동적 할당
		strcpy(name, myname);
		age = myage;
	}
	Person(const Person &copy)	// 깊은 복사를 위한 복사 생성자의 정의
		: age(copy.age)	
	{
		int len = strlen(copy.name) + 1;
		name = new char[len];	// new 키워드를 통한 동적 할당
		strcpy(name, copy.name);		
	}
	void ShowPersonInfo() const
	{
		cout << "이름: " << name << endl;
		cout << "나이: " << age << endl;
	}
	~Person()
	{
		delete[]name;	// delete 키워드를 이용한 메모리 공간 해제
		cout << "called destructor!" << endl;
	}
};

int main(void)
{
	Person man1("Jo Jae Chan", 29);
	Person man2(man1);		// 깊은 복사를 위한 복사 생성자 추가로 깊은 복사가 이뤄짐
	man1.ShowPersonInfo();
	man2.ShowPersonInfo();
	return 0;
}

Output :

이름: Jo Jae Chan

나이: 30

이름: Jo Jae Chan

나이: 30

called destructor!

called destructor!



복사 생성자의 호출 시점


1. 기존에 생성된 객체를 이용해 새로운 객체를 초기화는 경우


Person man1("Jo Jae Chan");

Person man2(man1); // 복사 생성자 호출


이 경우 man1과 man2의 멤버대 멤버 복사가 일어남은 앞의 예제에서 보았다.



2. Call-by-value 방식의 함수 호출 과정에서 객체를 인자로 전달하는 경우


#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num;
public:
	SoSimple(int n) : num(n)
	{  }
	SoSimple(const SoSimple& copy) : num(copy.num)
	{
		cout << "Called SoSimple(const SoSimple& copy)" << endl;
	}
	void ShowData()
	{
		cout << "num: " << num << endl;
	}
};

void SimpleFuncObj(SoSimple ob)
{
	ob.ShowData();
}

int main(void)
{
	SoSimple obj(7);
	cout << "함수호출 전" << endl;
	SimpleFuncObj(obj);
	cout << "함수호출 후" << endl;
	return 0;
}


Output:

1
2
3
4
함수호출 전
Called SoSimple(const SoSimple& copy)
num: 7
함수호출 후


위의 예제 30행에서 아래와 같이 obj 객체가 생성 후 SimpleFuncObj 함수가 호출 될 때, 

obj의 복사 생성자가 호출되고 obj와 ob의 멤버대 멤버 복사가 일어나게 된다.


함수 호출과 함께 복사가 이뤄지는 형태는 다음 그림과 같다.





3. 객체를 반환하되, 참조형으로 반환하지 않는 경우



아래 예제에서는 obj객체 생성후 SimpleFuncObj 함수 호출과정에서 obj 객체를 인자로 전달하고 있다.


또한 함수 호출과정에서 ob 객체가 반환된다.

객체의 반환이란건 객체의 생성과 함께, 함수가 호출된 영역으로 복사해 넘긴다는 뜻과 같다.


객체가 반환이 될 때는 임시 객체 (Temporary Object)가 생성되게 된다.


이러한 임시객체의 특성은 리터럴 상수와도 비슷하다. (int num=3+4; 에서 3과 4)

메모리 공간에 할당이 되지만 금방 사라지게 될 객체이다.


AddNum 함수의 반환형이 참조형(SelfRef &)이므로 객체 자신을 참조할 수 있는 참조자를 반환함에 유의하자. (반환된 참조값을 가지고 함수를 연이어 호출)


#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num;
public:
	SoSimple(int n) : num(n)
	{ }
	SoSimple(const SoSimple& copy) : num(copy.num)
	{
		cout << "Called SoSimple(const SoSimple& copy)" << endl;
	}
	SoSimple& AddNum(int n)
	{
		num += n;
		return *this;
	}
	void ShowData()
	{
		cout << "num: " << num << endl;
	}
};

SoSimple SimpleFuncObj(SoSimple ob)
{
	cout << "return 이전" << endl;
	return ob;
}

int main(void)
{
	SoSimple obj(7);
	SimpleFuncObj(obj).AddNum(30).ShowData();
	obj.ShowData();
	return 0;
}


Output:

1
2
3
4
5
Called SoSimple(const SoSimple& copy)
return 이전
Called SoSimple(const SoSimple& copy)
num: 37
num: 7



반환할 때 만들어진 객체의 소멸시점

#include <iostream>
using namespace std;

class Temporary
{
private:
	int num;
public:
	Temporary(int n) : num(n)
	{
		cout << "create obj: " << num << endl;
	}
	~Temporary()
	{
		cout << "destroy obj: " << num << endl;
	}
	void ShowTempInfo()
	{
		cout << "My num is " << num << endl;
	}
};

int main(void)
{
	Temporary(100);
	cout << "********** after make!" << endl << endl;

	Temporary(200).ShowTempInfo();
	cout << "********** after make!" << endl << endl;

	const Temporary &ref = Temporary(300);	// 참조값이 반환되므로 참조자로 참조 가능하다.
	cout << "********** end of main!" << endl << endl;
	return 0;
}


Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
create obj: 100
destroy obj: 100
********** after make!

create obj: 200
My num is 200
destroy obj: 200
********** after make!

create obj: 300
********** end of main!

destroy obj: 300


위의 예제에서 객체 생성은 하지만 객체의 이름이 없으며, 이는 임시 객체의 생성을 뜻한다.


하지만 참조자를 이용하면 반환된 참조값을 이용해 참조 가능하다.


실행 결과를 보면 참조자에 참조되는 임시객체는 메모리 공간이 바로 소멸되지 않음을 알 수 있다.

return 전에 소멸자의 호출됨을 출력된 문장을 통해 확인이 가능하다.



임시 객체의 생성과 소멸을 확인할 수 있는 다른 예제

: 주소 값을 통해 어떠한 객체가 생성되고 소멸되는지 확실히 알 수 있다.

#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num;
public:
	SoSimple(int n) : num(n)
	{
		cout << "New Object: " << this << endl;
	}
	SoSimple(const SoSimple& copy) : num(copy.num)
	{
		cout << "New Copy obj: " << this << endl;
	}
	~SoSimple()
	{
		cout << "Destroy obj: " << this << endl;
	}
};

SoSimple SimpleFuncObj(SoSimple ob)
{
	cout << "Parm ADR: " << &ob << endl;
	return ob;
}

int main(void)
{
	SoSimple obj(7);
	SimpleFuncObj(obj);

	cout << endl;
	SoSimple tempRef = SimpleFuncObj(obj);
	cout << "Return Obj " << &tempRef << endl;
	return 0;
}


Output:

New Object: 0133F8BC        // 31행의 obj 생성

New Copy obj: 0133F7A0    // 32행 SimpleFuncObj 함수 호출로 인한 23행의 매개변수 ob 생성

Parm ADR: 0133F7A0         // 25행의 실행

New Copy obj: 0133F7D8    // 26행 return ob로 인한 임시 객체 생성

Destroy obj: 0133F7A0        // 매개 변수 ob 소멸

Destroy obj: 0133F7D8        // 26행 return ob로 인한 임시 객체의 소멸


New Copy obj: 0133F7A0    // 35행 SimpleFuncObj 함수 호출로 인한 23행의 매개변수 ob 생성

Parm ADR: 0133F7A0        // 25행의 실행

New Copy obj: 0133F8B0    // 26행 return ob로 인한 임시 객체 생성

Destroy obj: 0133F7A0        // 매개 변수 ob 소멸

Return Obj 0133F8B0        // 36행, &tempRef는 임시 객체의 주소값과 동일하다!

Destroy obj: 0133F8B0        // tempRef가 참조하는 임시 객체의 소멸

Destroy obj: 0133F8BC        // 31행의 obj 소멸



출력 결과에서 가장 중요한 부분은 35행 문장에 의한 출력결과이다.


SoSimple tempRef = SimpleFuncObj(obj);


함수호출로 반환되는 객체를 tempRef라는 새로운 객체를 생성해서 대입연산 하는 것이 아니다!


출력결과의 주소값을 보면 알 수 있듯이 추가로 tempRef객체를 생성하는 것이 아닌, 

반환되는 임시객체에 tempRef라는 이름을 할당하고 있음을 알 수 있다.


이는 객체의 생성 수를 하나 줄여서 효율성을 높이기 위한 '반환값 최적화'의 결과이다.


만약 아래와 같이 객체를 생성후 대입한다면 이 때는 이러한 반환값 최적화가 일어나지 않는다.

SoSimple tempRef(1);

tempRef = SimpleFuncObj(obj);

728x90