[C++] 05-2. 복사 생성자 - 얕은 복사, 깊은 복사

SunowMin·2023년 9월 22일
0

C++

목록 보기
8/8

해당 글은 함수와 참조에 대한 이해를 바탕으로 작성하였습니다.
함수와 참조에 대한 내용을 원하신다면 아래의 링크를 클릭해주세요!

[C++] 05-1. 함수와 참조




5.5 복사 생성자

1. 얕은 복사(shallow copy)와 깊은 복사(deep copy)

복사 : 원본과 동일한 별개의 사본을 만드는 것

객체의 얕은 복사와 깊은 복사

  • C++ 객체 복사에도 얕은 복사와 깊은 복사가 존재
classs Person {
	int id;
    char *name;
    ......
}
  • 원본 Person 객체의 id는 1이고 name 포인터는 "Kitae" 문자열을 가진 동적 할당 배열을 가리키고 있음
    - 얕은 복사 : 원본 객체의 멤버가 그대로 사본 객체에 복사되므로, 사본은 원본의 name 메모리를 공유
    - 깊은 복사 : name 포인터가 가리키는 메모리까지 복사하여 원본과 사본의 name은 별개의 메모리를 가리키므로, 완전한 복사가 이루어짐




2. 복사 생성 및 복사 생성자

아래 코드 : Circle 클래스의 복사 생성자와 객체 복사

#include <iostream>
using namespace std;

class Circle {
private:
	int radius; 
public:
	Circle(const Circle& c); // 복사 생성자 선언
	Circle() { radius = 1; }
	Circle(int radius) { this->radius = radius; }
	double getArea() { return 3.14*radius*radius; }
}; 

Circle::Circle(const Circle& c) { // 복사 생성자 구현
	this->radius = c.radius;
	cout << "복사 생성자 실행 radius = " << radius << endl;
}

int main() {
	Circle src(30); // src 객체의  보통 생성자 호출
	Circle dest(src); // dest 객체의 복사 생성자 호출

	cout << "원본의 면적 = " << src.getArea() << endl;
	cout << "사본의 면적 = " << dest.getArea() << endl;
}



복사 생성자 선언

  • 복사 생성 : 객체가 생성될 때 원본 객체를 복사하여 생성
  • 복사 생성자(copy construct) : 자신과 같은 클래스 타입의 다른 객체에 대한 참조를 인수로 전달받아, 그 참조를 가지고 자신을 초기화하는 방법
    - 복사 생성 시에만 실행되는 생성자
    - 매개 변수는 오직 하나이며, 자기 클래스에 대한 참조로 선언됨
    - 복사 생성자는 클래스에 오직 한 개만 선언 가능



복사 생성자 실행

  • 반지름이 30인 src 객체를 생성하며 생성자 Circle(int radius)을 호출
Circle src(30);
  • scr 객체를 복사하여 dest를 생성하는 코드
Circle dest(src);
  • 컴파일러는 dest 객체가 생성될 때 보통 생성자 대신, 다음 복사 생성자 Circle(Circle& c)을 호출하도록 컴파일 함
Circle::Circle(const Circle& c) { // 복사 생성자 구현
	this->radius = c.radius;
	cout << "복사 생성자 실행 radius = " << radius << endl;
}
  • Circle(Circle& c)이 호출될 때, src 객체가 참조 매개 변수 c로 전달 -> c는 곧 src



디폴트 복사 생성자

  • 디폴트 복사 생성자
    - 컴파일러가 삽입하는 것으로 소위 얕은 복사를 실행하도록 만들어진 코드
    - 원본 객체의 모든 멤버를 일대일 사본(this)에 복사하도록 구성
  • 아래 코드 : 디폴트 복사 생성자 사례
class Book {
	double price;
	int pages;
	char* title;
	char* author;
public:
	Book(double pr, int pa, char* tm char* a);
	~Book();
};

복사 생성자가 없는 위의 Book클래스의 경우 컴파일러가 아래 형식의 디폴트 복사 생성자를 삽입함

Book(const Book& book) {
	this->price = book.price;
	this->pages = book.pages;
	this->title = book.title;
	this->author = book.author;
}



얕은 복사 생성자의 문제점

  • 포인터 타입의 멤버 변수가 있는 경우 공유의 문제 발생
    원본 객체의 포인터 멤버 변수가 사본 객체의 포인터 멤버 변수에 복사되면, 이 둘은 같은 메모리를 가리키게 됨

✅ 원본과 사본 각 객체가 소멸될 때, 같은 메모리 공간을 공유하기 때문에 한 객체가 이미 반환한 메모리를 다시 반환하게 됨
⇒ 실행 시간 오류 발생 + 프로그램 비정상 종료



깊은 복사 생성자

#define _CRT_SECURE_NO_WARNINGS //비주얼 스튜디오에서 strcpy로 인한 오류를 막기 위한 선언문
#include <iostream>
#include <cstring>
using namespace std;

class Person { // Person 클래스 선언
	char* name;
	int id;
public:
	Person(int id, const char* name); // 생성자
	Person(const Person& person); // 복사 생성자
	~Person(); // 소멸자
	void changeName(const char *name);
	void show() { cout << id << ',' << name << endl; }
};

Person::Person(int id,const char* name) { // 생성자
	this->id = id;
	int len = strlen(name); // name의 문자 개수
	this->name = new char [len+1]; // name 문자열 공간 핟당
	strcpy(this->name, name); // name에 문자열 복사
}

Person::Person(const Person& person) { // 복사 생성자
	this->id = person.id; // id 값 복사
	int len = strlen(person.name);// name의 문자 개수
	this->name = new char [len+1]; // name을 위한 공간 핟당
	strcpy(this->name, person.name); // name의 문자열 복사
	cout << "복사 생성자 실행. 원본 객체의 이름 " << this->name << endl;
}

Person::~Person() {// 소멸자
	if(name) // 만일 name에 동적 할당된 배열이 있으면
		delete [] name; // 동적 할당 메모리 소멸
}

void Person::changeName(const char* name) { // 이름 변경
	if(strlen(name) > strlen(this->name))
		return; // 현재 name에 할당된 메모리보다 긴 이름으로 바꿀 수 없다.
	strcpy(this->name, name);
}

int main() {
	Person father(1, "Kitae");			// (1) father 객체 생성
	Person daughter(father);			// (2) daughter 객체 복사 생성. 복사 생성자 호출

	cout << "daughter 객체 생성 직후 ----" << endl;
	father.show();						// (3) father 객체 출력
	daughter.show();					// (3) daughter 객체 출력

	daughter.changeName("Grace"); // (4) 	// daughter의 이름을 "Grace"로 변경
	cout << "daughter 이름을 Grace로 변경한 후 ----" << endl;
	father.show();						// (5) father 객체 출력
	daughter.show();					// (5) daughter 객체 출력

	return 0;								// (6), (7) daughter, father 객체 소멸
}
  • daughter의 name에 메모리가 따로 동적 할당되고, father의 name 문자열이 복사되어 같은 문자열 "Kitea"로 초기화

  • daughter 객체의 name을 "Grace"로 변경해도, father의 name은 "Kitea"로 그대로 남아 있음

    ✅ 원본 객체에 동적 할당된 메모리 크기 내에서만 변경 가능

  • main() 함수의 return 0;문이 실행되면 daughter 객체가 먼저 소멸되고 daughter의 name에 할당된 메모리를 힙에 반환
    -> daughter의 소멸 뒤 father 객체가 소멸되면서 자신의 name에 할당된 메모리를 힙에 반환하게 됨
    => 얕은 복사 생성자에서 일어난 중복 반환은 발생하지 않음



묵시적 복사 생성

: 복사 생성자를 구현해 놓지 않은 상태에서 컴파일러가 복사 생성자를 자동으로 호출하는 경우


(1) 객체로 초기화하여 객체가 생성될 때

  • 객체를 생성할 때 다른 객체로 초기화하도록 선언하면,
Person son = father;   // 복사 생성자 자동 호출
  • 컴파일러는 위 문장을 아래와 같이 변환하여 복사 생성자를 호출함
Person son(father); 
  • 아래의 치환문과는 다른 경우임!
Person son;
son = father;

(2) '값에 의한 호출'로 객체가 전달될 때

  • 함수의 매개 변수 객체가 생성될 때 복사 생성자가 자동 호출
void f(Person person) { // 매개 변수 person이 생성될 때 복사 생성자 호출
	......
}
Person father(1, "Kitae");
f(father);   // 값에 의한 호출로 father 객체 전달
  • 함수가 호출되어 매개 변수 person이 생성될 때, 다음과 유사한 모양으로 복사생성자가 호출되도록 컴파일
Person person(father);
  • '값에 의한 호출'로 매개 변수 객체가 생성될 때, 생성자가 실행되지 않는 대신 복사 생성자가 실행됨, 복사 생성자가 구현되지 않은 경우에 디폴트 복사 생성자가 실행

(3) 함수가 객체를 리턴할 때
return 문은 리턴 객체의 복사본을 생성하여 호출한 곳으로 전달하는데 이때 복사생성자 호출됨

Person g(){
	Person mother(2, "Jane");
    return mother;   // mother의 복사본을 생성하여 복사본 리턴, 사본이 만들어질 때 복사 생성자 호출
};
g();





해당 포스팅은 '황기태, 『명품 C++ Programming』, 생능출판사'를 참고하여 작성하였습니다.

0개의 댓글