728x90

황기태 저자의 명품 C++ Programming 개정판을 읽고 학습한 내용을 정리한 포스트입니다!

https://search.shopping.naver.com/book/catalog/32436115747

 

C++ Programming : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

얕은 복사와 깊은 복사

복사에는 얕은 복사(shallow copy)와 깊은 복사(deep copy)로 구분할 수 있다. 아래 그림을 통해 예시를 살펴보자.

  • 얕은 복사의 경우 어린이만 복사하기에, 복사 후 서로 자기 것이라고 주장하는 충돌이 발생한다.
  • 반면, 깊은 복사는 원본이 소유한 모든 것까지 복사하기에 충돌은 발생하지 않는다.

 

얕은 복사
  • 객체 복사 시, 객체의 멤버를 1:1로 복사
  • 객체의 멤버 변수에 동적 메모리가 할당된 경우
    • 사본은 원본 객체가 할당 받은 메모리를 공유하는 (충돌)문제가 발생
깊은 복사
  • 객체 복사 시, 객체의 멤버를 1:1로 복사
  • 객체의 멤버 변수에 동적 메모리가 할당된 경우
    • 사본은 원본이 가진 메모리 크기만큼 별도로 동적 할당
    • 원본의 동적 메모리에 있는 내용을 사본에 복사
  • 완전한 형태의 복사
    • 사본과 원본은 메모리를 공유하는 문제 없음

 

객체의 얕은 복사와 깊은 복사
class Person {
	int id;
    char *name;
    ..........
};

  • 얕은 복사의 경우 원본 객체의 id와 name 멤버는 현재 상태 그대로 사본 객체에 복사되므로, 사본의 name은 원본의 name 메모리를 공유
  • 깊은 복사는 원본의 name 포인터가 가리키는 메모리까지 복사하여 원본과 사본의 name은 별개의 메모리를 가리키므로, 완전한 복사가 이루어진다.
객체의 얕은 복사 문제점
  • 원본과 사본이 각각 name 포인터로 문자열 배열을 공유하고 있기 때문에 사본 객체에서 name 문자열을 변경하면 원본 객체의 name 문자열이 변경되는 문제가 발생
  • 보통 인지하지 못하는 상태에서 발생하기 때문에 오류를 발견하고 수정하는데 많은 시간이 걸림

복사 생성 및 복사 생성자

복사 생성자 선언
  • 복사 생성은 객체가 생성될 때 원본 객체를 복사하여 생성되는 경우
  • C++에는 복사 생성 시에만 실행되는 특별한 복사 생성자(copy constructor)가 존재
class ClassName {
	ClassName(const ClassName& c); // 복사 생성자
};

 

  • 매개 변수는 오직 하나이고 자기 클래스에 대한 참조로 선언
  • 복사 생성자는 클래스에 오직 한 개만 선언 가능
class Circle {
	...
	Circle(const Circle& c); // 복사 생성자 선언, 자기 자신 클래스에 대한 참조 매개 변수
	...
};

Circle::Circle(const Circle& c) { // 복사 생성자 구현
	...
}

 

실행
  • 여기서 다루는 복사는 치환 연산(=)를 통한 객체 복사가 아닌, 복사 생성인 점을 까먹지 말자.
Circle src(30); // 보통 생성자 호출
Circle dest(src); // src 객체를 복사하여 dest 객체 생성. 복사 생성자 Circle(Circle& c) 호출

 

  • 컴파일러는 dest 객체가 생성될 때 보통 생성자 대신, 다음 복사 생성자 Circle(Circle& c)을 호출하도록 컴파일한다.
Circle::Circle(const Circle& c) {
	this->radius = c.radius;
}

 

  • Circle(Circle& c)이 호출될 때, src 객체가 참조 매개 변수 c로 전달된다. (여기서 c는 곧 src)
#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;
}

/*
복사 생성자 실행 radius = 30
원본의 면적 = 2826
사본의 면적 = 2826
*/

 

디폴트 복사 생성자
class Circle {
	int radius;
public:
	Circle(int r);
	double getArea();
};
  • 복사 생성자를 가지고 있지 않은 위의 코드에 복사 생성문을 실행시킨다면 오류가 발생할까? 그렇지 않다.
  • 컴파일러는 아래와 같은 디폴트 복사 생성자를 묵시적으로 삽입하고 호출하도록 한다.
Circld::Circle(const Circle& c) { // 디폴트 복사 생성자
	this-> radius = c.radius; // 원본 객체 c의 각 멤버를 사본(this)에 복사한다.
}
  • 컴파일러가 삽입하는 디폴트 복사 생성자는 얕은 복사를 실행하도록 만들어진 코드
  • 컴파일러가 삽입한 복사 생성자는 원본 객체의 모든 멤버를 일대일로 사본(this)에 복사하도록 구성
class Book {
	double price;
	int pages;
	char* title;
	char* author;
public:
	Book(double pr, int pa, char* t, char* a);
	~Book();
};


// 아래는 컴파일러가 삽입하는 디폴트 복사 생성자
Book(const Book& book) {
	this->price = book.price;
	this->pages = book.pages;
	this->title = book.title;
	this->author = book.author;
}

 

얕은 복사 생성자의 문제점
  • 포인터 타입의 멤버 변수가 없는 클래스의 경우, 얕은 복사는 전혀 문제 xxx
    • 모든 멤버 변수를 일대일로 복사해도 공유의 문제가 발생하지 않기 때문에
  • 클래스가 포인터 멤버 변수를 가지고 있는 경우, 원본 객체의 포인터 멤버 변수가 사본 객체의 포인터 멤버 변수에 복사되면, 같은 메모리를 가리키게 된다.
#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(); // 소멸자
	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() {// 소멸자
	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 객체 소멸
}

 

위 코드의 실행 과정을 하나씩 살펴보자.

디폴트 복사 생성자 자동 삽입
  • 참조 매개 변수 p로 원본 객체의 id와 name 포인터를 사본 객체(this)에 복사한다. 
Person::Person(const Person& p) {
	this->id = p.id;
	this->name = p.name;
}
main() 함수 실행
  1. father 객체 생성
  2. father를 복사한 daughter 객체 생성
    • daughter 객체를 생성하고 컴파일러가 삽입한 디폴트 복사 생성자 Person(Person& p)를 호출한다.
  3. father와 daughter 객체 출력
  4. daughter 객체의 이름 변경
  5. father와 daughter 객체 출력
    • father의 name 포인터는 daughter와 동일한 메모리를 가리키고 있기 때문에
    • 출력 결과 daughter와 father 모두 이름이 변경되었다.
  6. main() 함수 종료
    • 생성 순서 반대로 daughter 객체 먼저 소멸 후 다음 소멸자에 의해 name에 할당된 메모리를 힙에 반환

마지막 6번 로직 이후 문제가 발생한다.

  • daughter 소멸 뒤 father 객체가 소멸되고, father 객체의 소멸자가 name에 할당된 메모리를 힙에 반환한다.
  • 하지만 daughter가 소멸될 때 이미 반환한 메모리를 다시 반환하게 되므로 실행 시간 오류와 비정상 종료가 발생한다.

 

사용자 복사 생성자 작성
  • 위와 같은 문제가 발생하지 않도록 깊은 복사 생성자를 만들어 보자.
#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 객체 소멸
}
  1. father 객체 생성
  2. father를 복사한 daughter 객체 생성
    • Person daughter(father); // 복사 생성자 Person(Person&) 호출
    • 위 코드를 실행하면 아래와 같은 복사 생성자가 호출된다.
    • daughter의 name에 메모리가 따로 동적 할당되고, father의 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;
}

 

3. 이후 daughter의 이름을 변경하여도 father의 이름은 그대로 유지되며, return 0;가 실행 된 후 daughter 객체가 먼저 소멸된다. 이 때 다음 소멸자가 실행되고 daughter의 name에 할당된 메모리를 힙에 반환한다.

  • daughter가 소멸된 뒤 father 객체도 소멸되며 역시 자신의 name에 할당된 메모리를 힙에 반환한다.

 

묵시적 복사 생성
Person daughter(father); // 복사 생성자를 명시적으로 호출하는 사례

위의 코드는 명시적으로 father를 원본으로 daughter를 복사 생성하기에, 복사 생성자가 호출될 것을 인지할 가능성이 높지만, 개발자가 모르게 복사 생성자가 호출되는 다른 경우들이 있다.

 

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

Person son = father; // 복사 생성자 자동 호출
Person son(father); // 컴파일러는 이렇게 변환하여 호출

// 아래 치환문과 혼돈하지 않기
Person son;
son = father; // 복사 생성자 호출되지 않음

 

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

  • 값에 의한 호출로 객체가 전달되면, 함수의 매개 변수 객체가 생성될 때 복사 생성자가 자동으로 호출된다.
void f(Person person) { // 매개 변수 person이 생성될 때 복사 생성자 호출
	....
}
Person father(1, "Kitae");
f(father); // '값에 의한 호출'로 father 객체 전달

 

3. 함수가 객체를 리턴할 때

  • 함수가 객체를 리턴할 떄, return 문은 리턴 객체의 복사본을 생성하여 호출한 곳으로 전달.
Person g() {
	Person mother(2, "Jane");
    return mother; // mother의 복사본을 생성하여 복사본 리턴. 사본이 만들어질 때 복사 생성자 호출
}
g();
728x90

'Programming Language > C++' 카테고리의 다른 글

[C++] static 멤버  (0) 2023.12.05
[C++] 함수 중복  (0) 2023.12.04
[C++] 참조에 의한 호출, call by reference  (0) 2023.12.04
[C++] 함수와 참조  (0) 2023.12.03
[C++] string 클래스를 이용한 문자열 사용  (1) 2023.12.01