LISTORY
[C++] 복사 생성자 본문
열혈 C++ 책 YouTube 강의를 참조하여 정리한다.
YouTube 주소
복사 생성자의 의미
이번 강의에서 다룰 내용은 다음과 같다.
무엇을 복사생성자라 하는가
복사 생성자가 제공해주는 기능(필요성)
복사 생성자를 필요에 맞게 정의하는 방법
이러한 내용을 다루기 전에 먼저 알고 가야할 것이 있다.
두 가지 형태의 초기화
C++에서 초기화 하는데엔 두가지 형태가 있다.
// C 스타일의 초기화 문장
int main(void)
{
int val1 = 20;
AAA a1 = 10;
...
이를 문장으로 해석해 보면, AAA 클래스의 객체 a1을 생성하는데 이를 10으로 초기화 하겠다 정도로 해석할 수 있다ㅣ.
이는 묵시적으로 이렇게 변환이 이루어진다.
AAA a1(10);
이 둘의 객체 생성은 C++에서는 같다
다만 그냥 같은건 아니고, 이렇게 묵시적으로 변환이 되기 때문에 같은 것이라 생각해야 한다.
복사생성자의 경우 호출 시기를 아는게 중요한데, 그때 이와 같은 문장을 구사할 것이다.
// C++ 스타일의 초기화 문장
int main(void)
{
int val1(20);
AAA a1(10);
...
C++ 스타일에서는 변수를 선언과 동시에 초기화한다.
int라는 객체를 생성하며 이름은 val1으로 주고 객체 생성 과정에서 20이라는 정수를 받을 수 있는 생성자를 호출하겠다.
기본 자료형의 경우, val1이라는 변수를 20으로 초기화 하겠다 라는 뜻이므로 위에와 뜻이 같다.
복사 생성자의 형태
class AAA
{
public:
AAA (){
cout<<"AAA() 호출"<<endl;
}
AAA(int i){
cout<<"AAA(int i) 호출"<<endl;
}
AAA(const AAA& a){
cout<<"AAA(const AAA& a) 호출"<<endl;
}
}
int main(void)
{
AAA obj1;
AAA obj2(10);
AAA obj3(obj2);
return 0;
}
class AAA의 함수들은 전부 다 생성자이다(오버로딩).
main을 보면 AAA의 객체들을 생성하는데, 전달하는 인자가 각각 다르다.
주목해야 할 것은 마지막이다. 인자를 obj2를 보냈다.
obj2는 AAA의 객체이다. 흥미로운 점은 AAA의 객체를 생성하면서, 인자로 AAA의 객체를 전달한다는 점이다.
그럼 이러한 형태의 인자를 전달받을 수 있는 함수가 있는지 보니 AAA(const AAA& a)가 있다.
이는 인자값으로 AAA의 객체를 참조로 받고, 받아진 객체의 데이터 조작을 안하겠다는 뜻이다.
이러한 형태의 생성자를 가리켜 복사생성자라고 한다. 복사생성자도 생상자의 범주 내에 포함된다.
* 왜 이름이 복사생성자?
- 복사를 위한 용도로 사용될 수 있는 생성자이기 때문
디폴트 복사 생성자
...
class Point
{
int x,y;
public:
Point(int _x, int _y) {
x = _x;
y = _y;
}
Point(const Point& p) {
x = p.x;
y = p.y;
}
void ShowData(){
cout<<x<<' '<<y<<endl;
}
};
int main(void)
{
Point p1(10,20);
Point p1(p1);
p1.ShowData();
p2.ShowData();
return 0;
}
해당 예제를 보면, p2 객체를 생성 하되, p1이 지니고 있는 멤버 변수 값으로 p2객체의 멤버를 초기화하길 원한다.
다시 말하자면 p1의 값을 p2에 복사하길 원한다.
이를 위해 p1이 인자로 전달되고 있다. 생성자를 보면 p라는 이름을 가지고 p1에 접근이 가능하다.
이 말은 p1의 멤버 값을 참조해낼 수 있다는 뜻이다.
일반적인 생성자를 보면 복사하고자 하는 대상에 직접 접근하지 못한다.
복사하고픈 값만 따로 전달을 받아 자신의 멤버 변수를 초기화하는 것이다.
반면 Point(const Point& p)의 경우에는 가능하다. 그래서 이를 복사 생성자라고 한다.
...
class Point
{
int x,y;
public:
Point(int _x, int _y) {
x = _x;
y = _y;
}
void ShowData(){
cout<<x<<' '<<y<<endl;
}
};
int main(void)
{
Point p1(10,20);
Point p1(p1);
p1.ShowData();
p2.ShowData();
return 0;
}
이 경우는 위에서 복사생성자만 생략한 경우이다. 적절한 생성자가 없으므로 컴파일에러가 날까?
그렇지 않다. 위에와 똑같은 결과가 출력된다.
여기서 유추를 하자면 이러한 생성자가 이미 존재한다는 뜻이 된다.
우리가 제공해주지 않아도 컴파일러에 의해 자동으로 제공된다.
복사생성자를 직접 정의해주지 않을 경우 디폴트 복사 생성자가 생긴다.
주의깊게 봐야할 것은 생성자, 소멸자의 경우, 자동으로 생성되면 별다른 기능을 제공하지 않는다. 그냥 형식적인 제공에 불과하다.
하지만 복사생성자는 조금 다르다.
만일 복사생성자가 생성자와 마찬가지로 하는일이 아무것도 없으면 초기화가 안됐기 때문에 쓰레기 값이 나와야한다.
하지만 복사생성자 정의 안해도 실행결과 동일하다. 이 말은 곧, 디폴트 복사 생성자는 알아서 멤버 대 멤버 값을 복사해준다.
디폴트 복사 생성자는 멤버대 멤버 복사를 해준다.
다시 정리해보겠다.
⊙디폴트 복사 생성자
- 사용자 정의 복사 생성자가 없을 때 자동 삽입
- 멤버 변수 대 멤버 변수의 복사를 수행
⊙ 디폴트 복사 생성자 복사 형태
- 얕은 복사(Shallow copy)
디폴트 복사 생성자
먼저 예제를 보겠다.
...
class Person
{
char *name;
char *phone;
int age;
public:
Person(char* _name, char* _phone, int _age);
~Person();
void ShowData();
};
Person::Person(char* _name, char* _phone, int _age)
{
name = new char[strlen(_name)+1];
strcpy(name, _name);
phone = = new char[strlen(_phone)+1];
strcpy(phone, _phone);
age= _age;
}
Person::~Person()
{
delete [] name;
delete [] phone;
}
void Person::ShowData()
{
cout<<"name "<<name<<endl;
cout<<"phone "<<name<<endl;
cout<<"age "<<name<<endl;
}
int main()
{
Person p1("KIM", "010-1111-1111", 22);
Person p2 = p1;
}
이를 그림으로 봐보자
name과 phone 변수는 new라는 키워드에 의해 할당되었으므로 heap에 존재한다.
코드를 보면 변수는 생성자 내에서 동적 할당하고 객체가 소멸되는 순간에 소멸하기 위해 소멸자에서 동적할당이 해제된다.
즉, 생성 시 동적 할당이 자동으로 이루어지고 소멸 시 소멸도 자동으로 이루어 진다.
main 함수를 보자.
p1을 생성했고 p2 생성을 원하고 있다. 이 말은 p1이 지닌 값을 복사하여 p2를 생성해달라는 뜻이다.
근데 우리는 이 문장이 묵시적으로 다음과 같은 코드가 된다는 것도 알고있다.
Person p2(p1);
그럼 Person 객체를 생성하는데 있어서 Person 객체를 인자로 받고싶다는 뜻이고 이는 복사 생성자를 필요로 한다는 뜻이다.
코드를 보면 복사 생성자는 존재하지 않지만, 우리가 생성 안해도 컴파일러가 디폴트 복사생성자 자동으로 만들어준다.
그래도 한번 디폴트 복사 생성자를 구현해보자
여기서 기억해야 할 것은 name이 멤버지 name 포인터가 가리키는 문자열이 멤버가 아니다.
즉 여기서 멤버는 두개의 포인터와 하나의 변수이다.
그리고 디폴트 복사생성자는 멤버 대 멤버로 복사를 한다.
class Person
{
...
Person(const Person& p) {
name = p.name;
phone = p.phone;
age = p.age;
}
...
}
컴파일 시, 아무 문제없다. 하지만 실행하는 순간에 에러가 뜬다 (런타임 에러).
이유가 무엇일까? 다시 그림을 보자
우리가 원한 복사는 아마 오른쪽 그림과 같은 복사(깊은 복사)일 것이다.
하지만 실제로 복사되는 모습은 왼쪽(얕은 복사)과 같다.
디폴트 복사 생성자는 멤버대 멤버로 복사를 진행한다.
p2는 p1이 지니고 있는 name의 값, 즉 문자열을 동일하게 가리키는 것이다. 이건 문제가 되지 않는다.
하지만 소멸자가 호출될 때 문제가 발생한다.
p1과 p2는 main함수가 끝나면 각각 소멸자 호출하며 사라진다.
먼저 p2 객체가 사라짐 (객체가 사라지는 순서는 객체가 생성되는 순서의 반대 .. 스택이므로)
p2객체가 사라질 때 소멸자의 호출에 의해 다 소멸된다.
그리고 나서 p1 객체게 소멸될때, 이미 걔네들을 앞에서 소멸되고 없는 것이다.
즉, 하나의 메모리 공간을 두번 소멸하려고 했기 때문에 문제가 생긴다.
이와같은 복사 형태를 얕은 복사라고 하고, 디폴트 복사 생성자는 얕은 복사를 한다.
생성자 내에서 메모리 동적 할당을 한다면, 그에 다른 소멸자를 따로 제공해야 한다.
또한, 깊은 복사를 하도록 복사 생성자도 제공해줘야한다.
생성자 내에서 동적 할당 할 시, 소멸자와 복사 생성자 제공해줘야 한다.
그럼 다시 코드를 작성해보겠다.
...
class Person
{
char *name;
char *phone;
int age;
public:
Person(char* _name, char* _phone, int _age);
Person(const Person& p) {
name =new char[strlen(p.name)+1];
strcpy(name, (p.name);
phone = = new char[strlen(p.phone)+1];
strcpy(phone, p.phone);
age= _age;
}
~Person();
void ShowData();
};
...
잘 작동하는 것을 확인할 수 있다.
복사 생성자 호출 형태 3가지
본격적으로 내용에 들어가기 전에 질문이 있다. 우리가 프로그램을 작성하고 있고, A 클래스를 정의했다고 가정해보자.
그리고 A의 클래스의 생성자에서는 동적 할당을 하고 있다. 즉, 깊은 복사 생성자를 만들어야 한다.
이번 시간에는 복사 생성자가 호출되는 세가지 경우 다룬다. 세가지 경우는 다음과 같다.
CASE 1. 기존에 생성된 객체로 새로운 객체 초기화
CASE 2. 함수 호출 시 객체를 값에 의해 전달
CASE 3. 함수 내에서 객체를 값에 의해 리턴
근데, 프로그램 내에서 하단의 케이스들은 등장하지 않는다. 이 말은 복사 생성자가 호출될 일이 없다는 뜻이다.
A는 여전히 동적 할당 하고 있을 경우, 복사 생성자 정의 해야할까?
일단 프로그램 관점에서는 필요하지 않다. 하지만 A 클래스 입장에서는 정의하는게 맞다.
이럴때는 반드시 정의해야한다. 나중에 필요해질 경우가 있을 수 있고, 확장성을 생각해야 하기 때문이다.
이렇게 복사 생성자를 넣을지 말지 결정해야 할 때엔 클래스를 기준으로 생각해야 한다.
이제 복사 생성자가 호출되는 세가지 경우에 대하여 정리해보겠다.
Case 1.
class AAA
{
int val;
public:
AAA(int i){
val = i;
}
AAA(const AAA& a){
cout<<"AAA(const AAA& a) 호출"<<endl;
val = a.val;
}
void ShowData(){
cout<<val<<endl;
}
}
int main ()
{
AAA obj1(10);
AAA obj2 = obj1;
return 0;
}
main 함수를 보겠다. 처음 obj1을 생성하면서, 메모리 공간 할당 하고 val은 10이 된다.
그 다음, obj2를 생성하는데, 여기서는 앞에서 말했듯이 묵시적으로 다음과 같은 코드라 생각해야 한다.
AAA obj2(obj1);
이미 생성된 객체, 기존의 객체(ob1)으로 새로운 객체(obj2)를 생성하므로 복사생성자가 호출된다.
Case 2.
void function(AAA a) {
a,ShowData();
}
int main()
{
AAA obj3(30);
function(obj3);
return 0;
}
int fct(int a){
...
}
int main()
{
int b = 10;
fct(b);
}
두 예제를 비교해가며 설명하겠다.
일단 밑의 예제부터 보자. fct()에 인자가 전달되는 부분을 보면, b라는 인자를 전달하며 a가 초기화된다.
1. b가 지니고 있는 값을 받기 위해 a라는 이름으로 메모리 공간 할당
2. b가 지니고 있는 값 전달되며 a 초기화
이렇게 메모리 공간 할당과 초기화, 두 단계를 거친다.
다시 위의 예제를 살펴보자.
obj3라는 객체를 생성하고 전달하는 과정을 거친다. 다만 인자를 참조가 아닌 값으로 전달한다.
전달되는 상황을 정리하면 이렇다.
1. a라는 이름으로 메모리 공간 할당
2. 실제로 전달되는 값으로 초기화가 이루어져야함
하지만 여기서 obj3 객체가 지니고 있는 값이 다이렉트로 전달되지 않는다.
객체의 경우, obj3 객체가 a라는 이름의 객체 생성자의 매개변수로 전달된다.
정리해보자
1. obj3가 지니고 있는 값이 전달되며 a를 초기화
2. 객체의 경우, obj3가 a 객체의 복사 생성자를 호출(복사하고자 원하는 인자의 대상으로 obj가 들어감)
결과적으로보면 매개변수 a도 객체라 할 수 있다. 메모리 할당도 이루어지고 생성자의 호출도 이루어지기 때문이다.
Case 3.
AAA function() {
AAA a(10);
return a;
}
int main()
{
function();
function().ShowData();
return 0;
}
function() 함수를 보면 a라는 이름으로 AAA 객체를 생성한다.
그 후, a 객체를 값에 의해 리턴하는 방식으로 main에게 전달한다.
이는 값에 의한 전달이므로 a 객체의 복사본이 main으로 리턴된다.
이 a 객체의 복사본도 Case 2와 마찬가지로 메모리 공간 할당, 초기화 과정을 거치므로 객체라고 할 수 있다.
AAA 클래스 참조하여 메모리 공간 할당, 복사본 객체의 멤버변수 val은 현재 쓰레기 값을 가지고 있다.
하지만 복사본 객체의 복사 생성자를 호출하여 a 객체를 인자로 전달한다.
복사 생성자 내부에서는 a객체가 지니고있는 멤버변수 val의 값을 복사본에 전달한다.
전체적인 과정은 Case 2와 비슷하다 볼 수 있다.
'IT > C++' 카테고리의 다른 글
[C++] 템플릿(Template) (1) | 2018.11.26 |
---|---|
[C++] 레퍼런스 함수 정리 (0) | 2018.11.22 |