클래스 객체를 생성하면 복사 생성자를 별도로 선언하지 않아도 default 로 복사 생성자(Copy constructor)가 자동으로 생성된다.
만약 생성자 내에서 동적 할당을 한다면반드시 구현해 주어야 할 것은 소멸자, 복사 생성자이다.
'얕은 복사'는 주소값만 복사하는 것이며, '깊은 복사'는 따로 메모리 할당까지 해주는 것이다.
default copy constructor 는 얕은 복사가 발생하여 소멸자에서 에러가 발생한다.
따라서 개발자가 직접 깊은 복사가 되도록 copy constructor를 구현해 주어야 한다.
C 언어에서는 동적 메모리 할당 및 반환을 위한 malloc()/free() 등의 표준 C 함수를 이용하지만, C++에서는 new 연산자와 delete 연산자를 이용한다. new 연산자는 힙(heap)이라는 시스템 공간으로부터 메모리 할당받고, delete 연산자는 할당받은 메모리를 heap으로 반환한다.
new와 delete 연산자의 기본 형식은 다음과 같다. 데이터타입 *포인터변수 = new 데이터타입; delete 포인터변수;
new 연산자는 '데이터타입'의 크기만큼 힙으로부터 메모리를 할당받고 주소를 리턴한다. 그 결과 '포인터변수'는 할당받은 메모리의 주소를 가진다. delete 연산자는 '포인터변수'가 가리키는 메모리를 힙으로 반환한다.
char *name = new char[strlen(str) + 1]; // str 은 입력받은 문자열 동적 할당을 위해 char형 포인터 name에 char형 str길이 + 1로 새로 만든다. char를 배열로 선언시 원하는 최대 글자수의 + 1의 크기로 배열 크기를 정해야 하는데, 컴퓨터는 그냥 문자열을 char의 배열로써 인식할 뿐이다. 문자열의 마지막을 알려주는 NULL 문자('\0')가 들어갈 공간을 남겨두어야한다. 문자열을 출력하거나, 복사한 후 메모리 공간 사용이 끝나면 메모리 누수를 줄이기 위해 명시적으로 메모리 할당 해제를 해주어야 한다. 동적으로 할당된 배열을 반환할 때, delete[] 연산자 name; // name 은 포인터 변수다. delete로 메모리를 반환할 때 적절하지 못한 포인터를 사용하면, 실행 오류가 발생한다.
※ 참고로 Java/C#에는 가비지 컬렉션이 존재하여 사용되지 않는 메모리를 자동으로 해제해 준다. C/C++에는 가비지 컬렉션이 존재하지 않으므로 사용자가 명시적으로 메모리를 해제해 주어야 한다.
C++ 복사 생성자 동영상 강좌를 따라하는데 에러가 발생하고 안된다.
동영상 강좌가 Visual Studio 2017 이전 버전인가 보다. 해결방법은 아래 설명되어 있다.
Person 클래스의 ① 복사 생성자를 구현한 상태로 실행해보고 ② 다시 주석처리하고 실행해보면, 결과가 다르게 나온다는 걸 확인할 수 있다.
예제1.
#include <iostream> using namespace std;
class Point { private: int x, y; public: Point(int _x=0, int _y=0) : x(_x), y(_y){} Point(const Point& p) { // 복사 생성자 x = p.x; y = p.y; } // 복사 생성자는 명시적으로 생성해주지 않으면 default 복사생성자가 자동으로 생성된다. void ShowPosition(); };
void Point::ShowPosition() { cout << "(" << x << ", " << y << ")" << endl; }
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]; // 문자열 할당. + 1 은 NULL 문자 고려 strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사
void Person::ShowData() { // 클래스 외부에서 클래스 멤버 함수 정의 cout << "name : " << name << endl; cout << "phone : " << phone << endl; cout << "age : " << age << endl; }
int main() { Person p1("홍길동", "010-1234-5555", 30); p1.ShowData();
Person p2(p1); // 복사 생성자 호출 p2.ShowData(); // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다. }
생성자 호출이 되고, 복사 생성자가 호출된다.
소멸자가 두번 호출되는 걸 확인할 수 있고 소멸된 순서를 확인할 수 있다.
이번에는 char *name 대신에 string 으로 변경하여 #include <string> 를 추가하고 코드를 수정해서 테스트했다.
결과는 잘 출력된다.
예제3.
#include <iostream> #include <string> using namespace std;
class Point { private: int x, y; public: Point(int _x=0, int _y=0) : x(_x), y(_y){} Point(const Point& p) { // 복사 생성자 x = p.x; y = p.y; } // 복사 생성자는 명시적으로 생성해주지 않으면 default 복사생성자가 자동으로 생성된다. void ShowPosition(); };
void Point::ShowPosition() { cout << "(" << x << ", " << y << ")" << endl; }
class Person { string name; string phone; int age; public: Person(string _name, string _phone, int _age); ~Person(); Person(const Person& p); // Copy Constructor
Person::Person(string _name, string _phone, int _age) { cout << "Person 생성자 호출- 메모리 주소 : " << this << endl; name = _name; phone = _phone; age = _age; }
Person::~Person() { cout << "Person 소멸자 호출- 메모리 주소 : " << this << endl; }
Person::Person(const Person& p) { cout << "Person 복사 생성자 호출 - 메모리 주소 : " << this << endl; name = p.name; phone = p.phone; age = p.age; }