복사 생성자란, 생성자의 한 종류로서 호출 시키는 객체의 선언과 동시에 초기화할 때 쓸 수 있다. 이미 생성되고 초기화 된 객체를 이용해 다른 객체를 초기화시킬 수 있는 것이다.
다음 예제를 보자.
#include <iostream>
using namespace std;
class SoSimple {
private:
int num1;
int num2;
public:
SoSimple(int n1, int n2)
:num1(n1), num2(n2)
{
}
SoSimple(SoSimple& copy)
: num1(copy.num1), num2(copy.num2)
{
cout << "Called SoSimple(SoSiple ©)" << endl;
}
void ShwSimpleData()
{
cout << num1 << endl;
cout << num2 << endl;
}
};
int main()
{
SoSimple sim1(15, 30); // n1 = 15, n2 = 30
cout << "생성 및 초기화 직전" << endl;
SoSimple sim2 = sim1; // SoSimple sim2(sim1)과 동일
cout << "생성 및 초기화 직후 " << endl;
sim2.ShwSimpleData();
return 0;
}
sim1이 매개변수 copy로 들어가서 sim1.num1 = 15, sim1.num2 = 30이 sim2.num1, sim2.num2에 대입된다.
즉 복사 생성자는 매개변수에 참조자를 이용해 객체를 넣고, main에서 동일한 클래스 객체를 삽입해 값을 초기화한다.
복사 생성자를 정의하지 않았을 경우에도 '디폴트 복사 생성자'를 이용해 자동으로 멤버 대 멤버 복사가 가능하다.
이러한 복사 방식을 '얕은 복사'라고 하는데, 멤버 변수가 힙의 메모리 공간을 참조하여 문제가 되는 것이다.
쉽게 설명하기 위해 다음 코드를 보겠다.
//Shallow
#include <iostream>
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4996)
using namespace std;
class Person
{
private:
char* name;
int age;
public:
Person(const char myname[100], int myage) {
int len = strlen(myname) + 1;
name = new char[len]; //문자열 길이 만큼 메모리 공간 동적할당
strcpy(name, myname); // 멤버 변수 초기화
age = myage;
}
void ShowPersonInfo() const
{
cout << "이름 : " << name << endl;
cout << "나이 : " << age << endl;
}
~Person() {//소멸자
delete[]name;
cout << "called destructor!" << endl;
}
};
int main()
{
Person man1("aa", 2); // char *myname = "aa"
Person man2 = man1;
man1.ShowPersonInfo();
man2.ShowPersonInfo();
return 0;
}
main 함수를 보면 디폴트 복사 생성자를 이용해 man2객체 멤버 변수에 값을 초기화 한 것을 볼 수 있다. 이 두 객체는 "aa"라는 동일한 문자열을 참조하는데, 소멸자에서 man1의 멤버 변수인 name을 delete[]하고 age를 소멸시키면, 그 다음 man2의 멤버 변수인 name이 존재하지 않아 소멸자에서 에러가 생기는 것이다.
이 에러는 '깊은 복사'를 이용해 해결될 수 있는데, 깊은 복사를 위한 생성자는 다음과 같다.
Person(const Person& copy)
:age(copy.age)
{
name = new char[strlen(copy.name) + 1];
strcpy(name, copy.name);
}
깊은 복사 생성자가 갖춰야 할 조건은 다음과 같다.
- age의 멤버 대 멤버 복사
- 메모리 공간 할당 후 문자열 복사, 메모리의 주소 값을 멤버 name에 저장
문제 05-1
#include <iostream>
#pragma warning(disable:4996)
using namespace std;
namespace COMP_POS
{
enum {
CLERK, SENIOR, ASSIST, MANAGER
};
}
class NameCard
{
private:
char* name;
char* coo;
char* tel;
int level;
public:
NameCard(const char myname[10], const char mycoo[10], const char mytel[10], int mylevel)
{
name = new char[strlen(myname) + 1];
strcpy(name, myname);
coo = new char[strlen(mycoo) + 1];
strcpy(coo, mycoo);
tel = new char[strlen(mytel) + 1];
strcpy(tel, mytel);
}
NameCard(const NameCard& copy)
: level(copy.level)
{
name = new char[strlen(copy.name) + 1];
strcpy(name, copy.name);
coo = new char[strlen(copy.coo) + 1];
strcpy(coo, copy.coo);
tel = new char[strlen(copy.tel) + 1];
strcpy(tel, copy.tel);
}
void ShowNameCardInfo()
{
cout << "이름 : " << name << endl;
cout << "회사 : " << coo << endl;
cout << "전화번호 : " << tel << endl;
switch (level)
{
case 0:
cout << "직급 : 사원" << endl;
break;
case 1:
cout << "직급 : 주임" << endl;
break;
case 2:
cout << "직급 : 대리" << endl;
break;
case 3:
cout << "직급 : 과장" << endl;
break;
}
}
};
int main()
{
NameCard manClerk("Lee", "ABCEng", "010-1111-2222", COMP_POS::CLERK);
NameCard copy1 = manClerk;
NameCard manSENIOR("Hong", "OrangeEng", "010-3333-4444", COMP_POS::SENIOR);
NameCard copy2 = manSENIOR;
copy1.ShowNameCardInfo();
cout << endl;
copy2.ShowNameCardInfo();
cout << endl;
}
코드의 문제점 : 일반 생성자와 복사 생성자 간의 겹치는 코드가 많다. 비효율적인 코드라고 생각하는데, 해결 코드를 보고 꼭 비교해보자.
>해답 코드
#include <iostream>
using namespace std;
namespace COMP_POS
{
enum {
CLERK, SENIOR, ASSIST, MANAGER
};
void ShowPositionInfo(int position)
{
switch (position)
{
case 0:
cout << "직급 : 사원" << endl;
break;
case 1:
cout << "직급 : 주임" << endl;
break;
case 2:
cout << "직급 : 대리" << endl;
break;
case 3:
cout << "직급 : 과장" << endl;
break;
}
}
}
class NameCard
{
private:
char* name;
char* company;
char* phone;
int position;
public:
NameCard(char* name, char* company, char* phone, int pos)
{
this->name = new char[strlen(name) + 1];
this->company = new char[strlen(company) + 1];
this->phone = new char[strlen(phone) + 1];
strcpy(this->name, name);
strcpy(this->company, company);
strcpy(this->phone, phone);
}
NameCard(const NameCard& ref)
:position(ref.position) {
name = new char[strlen(ref.name) + 1];
company = new char[strlen(ref.company) + 1];
phone = new char[strlen(ref.phone) + 1];
strcpy(name, ref.name);
strcpy(company, ref.company);
strcpy(phone, ref.phone);
}
void ShowNameCardInfo()
{
cout << "이름 : " << name << endl;
cout << "회사 : " << company << endl;
cout << "전화번호 : " << phone << endl;
cout << "직급 : "; COMP_POS::ShowPositionInfo(position);
cout << endl;
}
~NameCard()
{
delete[]name;
delete[]company;
delete[]phone;
}
};
int main()
{
NameCard manClerk("Lee", "ABCEng", "010-1111-2222", COMP_POS::CLERK);
NameCard copy1 = manClerk;
NameCard manSENIOR("Hong", "OrangeEng", "010-3333-4444", COMP_POS::SENIOR);
NameCard copy2 = manSENIOR;
copy1.ShowNameCardInfo();
cout << endl;
copy2.ShowNameCardInfo();
cout << endl;
}
내 답안 피드백 :
일단 우려했던 동일한 코드가 반복된다는 문제점은 해결 코드에도 마찬가지로 있었다.
그리고 소멸자를 생성하지 않았다. 생성자에 동적할당을 해줬으면 delete는 반드시 해주어야 한다.
그리고 COMP_POS 네임스페이스 안에 cout 문을 따로 만들어주면 switch 문을 클래스 안에 넣을 필요는 없다.
#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()
{
SoSimple obj(7); //num= 7
cout << "함수 호출 전 " << endl;
SimpleFuncObj(obj);
cout << "함수 호출 후 " << endl;
return 0;
}
다음 코드에서 생성자에 의해 초기화 되는 것은 ob 객체일까, obj 객체일까?
정답은 ob 객체이다. ob객체는 obj객체로 인해 초기화된다.
#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; //ob객체를 반환
}
int main()
{
SoSimple obj(7); //num= 7
SimpleFuncObj(obj).AddNum(30).ShowData();
obj.ShowData();
return 0;
}
SimpleFuncObj(obj).AddNum(30).ShowData();
이 코드에 의해서 obj 객체가 함수 호출시 생성된 ob 객체로 전달이 되고, ob 객체가 반환될 때 임시 객체에 저장이 되어 main 함수 안에서 다뤄진다. 이 임시 객체 안의 num 멤버 변수는 AddNum에 의해 30이 증가되고 해당 임시 객체가 반환된다. 그리고 ShowData 함수에 의해 37이 출력되는 것이다.
이때 임시 객체와 obj 객체는 서로 다른 객체이므로 obj.ShowData()를 하면 원래의 값인 7이 출력된다.
아래는 임시 객체를 설명하기 위한 코드이다.
#include <iostream>
using namespace std;
class Temporary
{
private:
int num;
public:
Temporary(int n) : num(n)
{
cout << "crate obj : " << num << endl;
}
~Temporary() {
cout << "destroy obj : " << num << endl;
}
void ShowTempInfo()
{
cout << "My num is " << num << endl;
}
};
int main()
{
Temporary(100); // 100으로 초기화된 Temporary 임시 객체
cout << "after make!" << endl << endl;
Temporary(200).ShowTempInfo(); // 200으로 초기화 된 임시 객체 생성
cout << "after make!" << endl << endl;
const Temporary& ref = Temporary(300); // 임시 객체를 생성(참조자로)
cout << "end of main!" << endl << endl;
return 0;
}
Temporary(200).ShowTempInfo();
여기서는 먼저 임시객체가 생성되고 (임시 객체의 참조 값).ShowTempInfo() 다음과 같은 형태가 되는 것이다.
const Temporary& ref = Temporary(300); 이 코드가 가능하나 이유도 Temporary(300)의 리턴값이 참조 값이기 때문이다. 임시 객체는 메모리에 저장되고, 참조 값만 ref에 저장된다.
이 때문에 위에서 obj를 통해 생성된 임시 객체의 값과 obj 자체의 값이 다른 것이다.
// ReturnObjDeadTime.cpp
#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 << "Param ADR:" << &ob << endl;
return ob;
}
int main()
{
SoSimple obj(7);
SimpleFuncObj(obj); //임시 객체
cout << endl;
SoSimple tempRef = SimpleFuncObj(obj); //임시객체의 참조값을 tempRef 객체에 전달
cout << "Return Obj " << &tempRef << endl; //
return 0;
}
> 컴파일 결과 분석
New Object010FFE00 //this, 즉 obj 주소 출력
New Copy obj : 010FFCE8 // 매개변수 ob 주소 출력
Param ADR:010FFCE8
New Copy obj : 010FFD1C // ob의 반환으로 새로운 임시 개체 생성
Destroy obj:010FFCE8
Destroy obj:010FFD1C
New Copy obj : 010FFCE8
Param ADR:010FFCE8
New Copy obj : 010FFDF4
Destroy obj:010FFCE8
Return Obj 010FFDF4
Destroy obj:010FFDF4
Destroy obj:010FFE00
'Langauge > C++' 카테고리의 다른 글
[열혈 C++ 프로그래밍] Chapter 07(상속의 이해) (0) | 2021.03.01 |
---|---|
[열혈 C++ 프로그래밍] Chapter06 (friend와 static그리고 const) 정리 및 문제 해결 (0) | 2021.02.18 |
[열혈 C++ 프로그래밍] Chapter04(클래스의 완성) 정리 및 문제 해결 #2 (0) | 2021.02.14 |
[열혈 C++ 프로그래밍] Chapter04(클래스의 완성) 정리 및 문제 해결 #1 (0) | 2021.02.13 |
[열혈 C++ 프로그래밍] Chapter03 - 클래스의 기본 정리 및 문제 해결 (0) | 2021.02.11 |