눈에 잘 드러나지 않는 자동 생성되는 코드를 이해하는 부분이 항상 까다롭다.
그 중 하나가 복사 생성자다.
대부분 자동으로 생성되는 복사 생성자를 사용하게 되고 큰 문제는 없다.
(좀더 인텔리전트한 개발자가 되기 위해서)
몇몇 부분에서 성능 향상에 도움이 되는 복사 생성자 정의하는 포인트가 있다. 알아두면 정말 좋은 내용이다.
copy constructor ( 복사 생성자 )
- 자신과 동일한 타입 한 개를 인자로 가지는 생성자
- 사용자가 복사 생성자를 만들지 않으면 컴파일러가 자동으로 제공한다.
- 디폴트 복사 생성자다 (default copy constructor)
- 모든 멤버를 복사 한다.
#include <iostream>
class Point
{
public:
int x;
int y;
Point() : x(0), y(0)
{
std::cout << "ctor 1" << std::endl;
}
Point(int a, int b) : x(a), y(b)
{
std::cout << "ctor 2" << std::endl;
}
};
int main()
{
Point p1;
Point p2(1,2); // ok
Point p3(1); // Point ( int ) error 1개 인자 생성자가 없다.
Point p4(p2); // Point ( Point ) - ok
Point p5{p2};
Point p6{p2};
Point p7 = {p2};
Point p8 = p2;
}
Point p3(1); 의 경우 아래와 같이 인자 한개짜리 생성자가 없다고 에러를 보여준다.
main.cpp:23:15: error: no matching function for call to ‘Point::Point(int)’
Point p4(p2); 가 에러가 나지 않는 이유는 컴파일러가 자동으로 복사 생성자를 만들기 때문이다.
아래 코드가 컴파일러가 자동으로 생성하고 있어 에러가 발생하지 않는다.
Point (const Point& p)
: x(p.x), y(p.y)
{
}
사용자가 만드는 복사 생성자
컴파일러가 자동으로 생성하는 복사 생성자를 사용자가 직접 만들 수 있다.
#include <iostream>
class Point
{
public:
int x;
int y;
Point(){}
Point(int a, int b) : x(a), y(b)
{
std::cout << "ctor" << std::endl;
}
//사용자가 만드는 복사 생성자
Point( const Point& p) : x(p.x), y(p.y)
{
std::cout << "copy ctor" << std::endl;
}
};
int main()
{
Point p1;
Point p2(1,2); // ok
//Point p3(1); // Point ( int ) error 1개 인자 생성자가 없다.
Point p4(p2); // Point ( Point ) - ok
Point p5{p2};
Point p6{p2};
Point p7 = {p2};
Point p8 = p2;
}
복사 생성자가 호출되는 3가지 경우
1) 자신과 동일한 타입의 객체로 초기화 될 때
Point p2{p1}; // 복사 생성자
Point p4 = {p1}; // 복사 초기화
Point p5 = p1; // 복사 초기화 : explicit가 아닌 경우만
아래 복사 생성자 Point (const Point& p) : x(p.x), y(p.y) 의 경우 잘 수행 된다.
#include <iostream>
class Point
{
public:
int x;
int y;
Point(int a, int b) : x(a), y(b)
{
std::cout << "ctor" << std::endl;
}
Point( const Point& p) : x(p.x), y(p.y)
{
std::cout << "copy ctor" << std::endl;
}
};
int main()
{
Point p1(1,2); // 생성자
Point p2{p1}; // 복사 생성자
Point p3{p1}; // 직접 초기화
Point p4 = {p1}; // 복사 초기화
Point p5 = p1; // 복사 초기화
}
위의 코드와 모두 동일하나, 복사 생성자에 explicit 를 넣어준 차이만 있다.
#include <iostream>
class Point
{
public:
int x;
int y;
Point(int a, int b) : x(a), y(b)
{
std::cout << "ctor" << std::endl;
}
explicit Point( const Point& p) : x(p.x), y(p.y) // explicit 복사 생성자
{
std::cout << "copy ctor" << std::endl;
}
};
int main()
{
Point p1(1,2); // 생성자
Point p2{p1}; // 복사 생성자
Point p3{p1}; // 직접 초기화
Point p4 = {p1}; // 복사 초기화
Point p5 = p1; // 복사 초기화
}
main.cpp:24:19: error: no matching function for call to ‘Point::Point(Point&)’
main.cpp:25:16: error: no matching function for call to ‘Point::Point(Point&)’
2) 자신과 동일한 타입의 객체로 초기화 될 때
함수 인자를 call by value로 받을 때 복사 생성자가 호출된다.
함수 인자를 const reference로 사용하면 복사본을 만들지 않으므로 복사 생성자가 호출되지 않는다.
#include <iostream>
class Point
{
public:
int x;
int y;
Point(int a = 0, int b = 0) : x(a), y(b)
{
std::cout << "ctor" << std::endl;
}
Point( const Point& p) : x(p.y), y(p.x)
{
std::cout << "copy ctor" << std::endl;
}
};
// call by value함수 : 메모리 증가, 복사 생성자 불리고, 파괴될 때 소멸자 불린다. 함수 호출 2번됨.
//void foo( Point pt) // Point pt = p1
// 복사 생성자, 소멸자 안 불리므로 성능 향상에 좋다.!!!
void foo( const Point& pt) // const Point& pt = p1
{
}
int main()
{
Point p1(1,2);
foo(p1);
}
함수 인자를 const reference로 사용하면 복사 생성자가 호출되지 않는다.
3) 자신과 동일한 타입의 객체로 초기화 될 때
- 리턴용 임시 객체가 생성될 때 복사 생성자가 호출된다.
- 참조로 반환 (Point&) 하면 리턴 용 임시객체가 생성되지 않는다.
#include <iostream>
class Point
{
public:
int x;
int y;
Point(int a = 0, int b = 0) : x(a), y(b)
{
std::cout << "ctor" << std::endl;
}
Point( const Point& p) : x(p.y), y(p.x)
{
std::cout << "copy ctor" << std::endl;
}
};
Point p; // 생성자
//Point foo() // 값 타입 반환
//{
// return p; // 리턴용 임시 객체를 만들어서 반환한다.
//}
Point& foo() // 임시 객체를 만들지 않고 참조로 반환! (지역 변수는 참조 반환하면 안된다!)
{
return p;
}
int main()
{
foo();
}
복사 생성자가 호출되지 않았음을 알 수 있다.
default copy constructor
편리하지만 문제가 되는 경우가 있다.
기본 적인 디폴트 복사 생성자 호출 케이스다.
#include <iostream>
class Point
{
public:
int x;
int y;
Point(int a, int b) : x(a), y(b) { }
};
int main()
{
Point p1(1,2); // ok
Point p2(p1); // 디폴트 복사 생성자 호출 , 모든 멤버가 복사
std::cout << p2.x << std::endl; // 1
std::cout << p2.y << std::endl; // 2
}
디폴트 복사 생성자의 문제점 (defulat copy constructor의 문제)
별 문제 없어보이는 아래 코드는 p1에 메모리 자원이 할당되고 p2가 생성되면서 문제가 발생한다.
왜 문제가 발생하는지 아래 순서를 하나씩 따라가면서 확인해 본다.
#include <iostream>
#include <cstring>
class Person
{
char* name;
int age;
public:
Person(const char* n, int a) : age(a)
{
name = new char[strlen(n) + 1]; // 자원 할당
strcpy(name, n); // name에 n을 복사 해 넣어줍니다.
}
~Person() { delete[] name;} // 자원 해제
};
int main()
{
Person p1("kim", 20);
Person p2 = p1; // 실행시 오류 발생 (컴파일러에 따라 다른 결과)
}
1) Person p1 이 만들어 지면 *name과 age 공간이 생성된다.
2) 생성자에 의해 해당 2 줄을 실행하면 아래와 같은 구조를 갖는다.
name = new char[strlen(n) +1];
strcpy(name, n);
3) 해당 코드가 실행되면 복사 생성자가 호출된다. 컴파일러가 만들어주는 디폴트 복사 생성자가 수행된다.
Person p2 = p1;
* 디폴트 복사 생성자는 모든 멤버를 복사해 준다.
4) 모든 코드 수행이 끝나면, 소멸자가 호출된다.
p2의 소멸자가 호출되면서 name이 delete[] 되면서 메모리 공간 "kim" 공간도 역시 삭제된다.
5) 그 다음 p1의 소멸자가 불릴 때,
이미 char* name 의 공간이 p2 소멸자에 의해 이미 지워졌기 때문에 잘못된 메모리 공간을 지울 수 있다.
>> 이는 메모리를 통채로 복사하지 못하고 주소만 복사하고 있는데 이를 "얕은 복사"라고 부른다.
얕은 복사 (Shallow Copy)
클래스 안에 포인터 멤버가 있을 때 디폴트 복사 생성자가 메모리 자체를 복사하지 않고 주소만 복사하는 현상.
c++ 개발자라면 반드시 알아둬야 하는 내용이다.
해결책
개발자가 직접 복사 생성자를 만들어야 한다.
어떻게 복사해야 하는가?
깊은 복사 (deep copy)
클래스 안에 포인터 멤버가 있을 때 메모리 주소를 복사하지 말고 메모리 자체의 복사본을 만드는 코딩 방법.
#include <iostream>
#include <cstring>
class Person
{
char* name;
int age;
public:
Person(const char* n, int a) : age(a)
{
name = new char[strlen(n) + 1];
strcpy(name, n);
}
~Person() { delete[] name;}
// ******** 사용자 정의 : 복사 생성자 *********
Person(const Person& p) : age(p.age) // 초기화 리스트를 통해 초기화!!!
{
// age = p.age; // -> 초기화 리스트로 대체
// name = p.name; // -> 아래 코드로 대체
// ******* 포인터는 복사 하지말고. 새롭게 메모리 할당 ********
name = new char[strlen(p.name) + 1];
strcpy(name, p.name);
}
};
int main()
{
Person p1("kim", 20);
Person p2 = p1;
}
복사 생성자 인자로 받아온 const Person& 타입의 p에서 p.name으로 메모리 생성한다.
name = new char[strlen(p.name) + 1];
strcpy(name, p.name);
깊은 복사(deep copy)의 단점
객체를 여러 번 복사하면 동일한 자원이 메모리에 여러 번 놓이게 된다.
자원의 크기가 큰 경우 메모리 낭비가 발생 한다.
깊은 복사의 단점을 해결하는 또 다른 방법
자원을 같이 쓰자.
2명이 쓰고 있다고 자원을 관라한다.
p2를 파괴할 때 자원 delete하지 않고 자신의 것만 해제한다.
참조 계수 (reference counting)
- 여러 객체가 하나의 자원을 공유한다.
- 몇 몇의 객체가 자원을 사용하는지 개수를 관리한다.
- c++뿐 아니라 c에서도 자주 사용하는 방법
참조 계수 (reference counting) 복사
참조 계수를 활용해 객체를 복사/관리하는 방법
#include <iostream>
#include <cstring>
class Person
{
char* name;
int age;
int* ref; // 참조 계수 관리 위한 포인터
public:
Person(const char* n, int a) : age(a)
{
name = new char[strlen(n) + 1];
strcpy(name, n);
ref = new int(1); // 1로 초기화
//*ref = 1;
}
~Person() {
// ******* 참조 계수 기반인 경우의 소멸자. ******
if ( --(*ref) == 0 )
{
delete[] name;
delete ref;
}
}
Person(const Person& p) : name(p.name), age(p.age), ref(p.ref) {
++(*ref);
}
};
int main()
{
Person p1("kim", 20);
Person p2 = p1;
}
여기서 또 다른 문제?
- p1 객체가 자신의 이름을 변경하면 어떻게 ?
- p2 이름 변경이 되면 안되기 때문에 공유 했던 자원은 분리 되어야 한다.
복사 금지
사람은 언제나 고유한 한 사람만이 존재한다. 그러니 복사 안되도록 하자!
객체 복사 못하도록 만든다. 복사 생성자를 delete한다.
디폴트 복사 생성자를 삭제 했으니 복사 생성자 호출시 컴파일 에러 발생한다. (심플!)
Person(const Person&) = delete;
String을 사용
문자열이 필요하면 STL 의 string 클래스를 사용하자
동적 메모리 할당할 필요가 없다.
stirng 이 내부적으로 자원 관리 해준다.
int 변수처럼 사용하면 된다.
#include <iostream>
#include <cstring>
#include <string>
class Person
{
std::string name;
int age;
public:
Person(std::string n, int a) : name(n), age(a)
{
}
};
int main()
{
Person p1("kim", 20);
Person p2 = p1;
}
또 다른 방법? move 를 활용하는 방법! (다른 포스팅에서 다루기로...)
'C++' 카테고리의 다른 글
[C++] 상수 멤버 함수 const (1) | 2021.08.23 |
---|---|
[C++] static member (정적 멤버), static 변수, static 함수 (0) | 2021.08.22 |
[C++] Explicit 생성자 (0) | 2021.08.20 |
[C++] Class : 멤버 초기화 리스트 (member initializer lists) (0) | 2021.08.19 |
[C++] 생성자, 소멸자, 위임 생성자, default 생성자 (0) | 2021.08.19 |
댓글