- 이미 지워진 문자열을 대상으로 p1의 delete 연산을 진행해야 한다는 문제점이 발생한다.
★ 따라서 깊은 복사를 위한 복사생성자를 만들어 각각의 문자열을 참조해 문제가 발생하지 않게 해줘야 한다.
※깊은복사
- 멤버 뿐만 아니라 포인터로 참조하는 대상까지 복사하는 방법
- default 생성자가 불충분할 때 사용자 정의 복사생성자를 선언한다.
위의 코드에서 아래 코드를 추가해주면 된다.
- 멤버변수 age의 멤버 대 멤버 복사
- 메모리공간 할당 후 문자열 복사, 할당된 메모리 주소값을 멤버 name에 저장
즉, 두 객체를 각각 동적으로 할당된 배열을 갖게 하는데 이를 깊은 복사(deep copy)라 한다.
§ 또 다른 예제
DynamicArray (const DynamicArray& other) : mSize(other.mSize), mArray(nullptr){
// 자신만의 동적데이터공간 할당
mArray = new int[mSize];
// 다른 객체로부터의 데이터 복사
for (int i = 0; i < mSize; i++) {
mArray[i] = other.mArray[i];
}
}
※ 복사생성자 호출 시점:객체를 새로 생성하되 생성과 동시에 동일한 자료형의 객체로 초기화 할 때.
1. 기존에 생성된 객체를 이용해 새로운 객체를 초기화 할 때. [Person p2 = p1;]
2. Call-by-Value 방식의 함수호출과정에서 객체를 인자로 전달할 때
3. 객체를 반환하지만 참조형으로 반환하지 않는 경우.
- 함수의 값을 반환하면 별도의 메모리공간이 할당되고 이 공간에 반환값이 저장된다. (반환값으로 초기화)
2. CBV 함수호출에서 객체를 인자로 전달하는 경우 예시
출처) 윤성우의 열혈C++프로그래밍
3. 객체를 반환하지만 참조형으로 반환하지 않는경우
출처) 윤성우의 열혈C++ 프로그래밍
cf. 생성자, 소멸자, 복사생성자 호출시점 명확히 알고가자!
Q. 어 그러면 생성자가 모두 출력된 후 소멸자가 출력되는 거면 되게 쉽게 생각해도 되겠네요?
A: 그렇게 생각하면 안된다. 객체 소멸은 여러 가지의 경우가 있는데 그 중 하나로는 객체는 함수가 종료되면 사라진 다는 점이다. 정확히는 함수 내부의 nested block이 끝나면 사라지게 되는 것이다.
예를 들어 아래와 같이 p1객체에 block을 씌웠다 가정하자.
{
Person p1(5);
cout << "부모 객체 p1 생성" << endl;
}
Person p2(p1); //오류 발생
cout << "부모 객체 p2 생성 및 객체 p1 복사" << endl;
만약 실행되었다면 다음과 같이 출력이될 것이다.
부모의 생성자
부모 객체 p1 생성
부모의 소멸자
부모의 생성자
부모 객체 p2 생성 및 객체 p1 복사
부모의 생성자
하지만 부모의 소멸자가 호출되어 객체 p1이 없어지니 객체 p2는 객체 p1을 복사하지 못하게 되는 것이다.
이해를 위해 또 다른 예시를 들어 보겠다.
{
Person p1(5);
}
cout << "부모 객체 p1 생성" << endl;
이렇게 된다면 출력은 어떻게 될까?
이는 아래와 같다.
부모의 생성자
부모의 소멸자
부모 객체 p1 생성
즉, 블럭이 종료되면 소멸자가 호출된다는 것을 알 수 있다.
cf. 만약 private멤버로 char* name과 같은 배열관련 변수가 들어갔다면 delete []name로 memory leak을 막아야 한다.
※ 임시객체를 이용한 obj.func().func(); 연산
※ 반환시 만들어진 (임시)객체의 소멸
1. 임시객체는 다음행으로 넘어가면 바로 소멸된다.
2. 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.
또 다른 예시)
※ r-value 참조
r-value 참조는 상수나 임의 저장소를 참조하는 개념으로 데이터 타입에 Double Ampersand(&&)를 붙여 표기한다.
이런 더블 엡퍼센드를 r-value 참조자라 부른다.
사실 이런 r-value 참조는 l-value랑 달리 OOP의 원칙을 준수하는 것보다
프로그램의 성능향상 및 프로그램 개발 생산성에 초점을 맞춰 생겨난 개념이다.
/*---------------일반 변수--------------- */
void f(){
int val = 10; // val 변수 생성 후 10 입력
}
// 함수 반환 시 변수를 소멸
/*---------------r value 참조 변수--------------- */
void f(){
int&& val = 10; // 저장소를 만들어 10을 입력, 저장소에 대해 r value 참조
}
// 함수 반환 시 임시저장소와 함께 r value 참조를 소멸
위의 경우, 표현은 다르지만 작업결과는 동일한데 이말은 즉, r value 참조자는 위와 같은 목적으로 만들어진 개념이 아니다
- 이동생성자가 호출되면얕은복사(shallow copy) => 원본의 소유권을 대상으로 이전하는(move)방식으로 객체를 생성.
- 그 이후 원본 객체를 NULL로 초기화 하여 접근할 수 없게 한다.
Q. 복사생성자가 있는데 굳이 이동생성자를 써야하는 이유는 뭘까?
A. 아래 코드를 통해 설명하겠다.
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class Animal {
private:
char *name;
int age;
public:
Animal(int age_, const char* name_) {
age = age_;
name = new char[strlen(name_) + 1];
strcpy(name, name_);
}
Animal(const Animal & a) { //복사생성자에 의한 깊은복사
age = a.age;
name = new char[strlen(a.name) + 1];
strcpy(name, a.name);
cout << "복사생성자" << endl;
}
Animal(Animal && a) noexcept { //이동생성자에 의한 얕은복사
age = a.age;
name = a.name;
cout << "이동생성자" << endl;
a.name = nullptr;
}
~Animal(){
cout <<"Destructor!!" << endl;
if(name)
delete [] name;
}
void changeName(const char *newName) {
strcpy(name, newName);
}
void printAnimal() {
cout << "Name: " << name << " Age: " << age << endl;
}
};
int main() {
Animal A(10, "Jenny");
A.printAnimal();
vector<Animal> vec; // Animal 벡터 타입
// Animal 벡터 객체 삽입
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
A.printAnimal();
for (int i = 0; i < 5; i++) {
vec[i].printAnimal();
}
return 0;
}
복사생성자만 있는 경우, 새 vector 생성마다 기존 vector에 저장된 객체들이
새로운 vector로 복사될 때, 복사생성자가 호출되고 성능저하가 일어남
이는 복사생성자의 new로 인한 메모리 할당의 반복으로 일어나는 현상임.
이런 문제를 해결하기 위해 shallow copy를 수행하는 이동생성자를 정의해주는데,
새로운 vector로 이동시, 이동생성자가 호출되며 불필요한 메모리할당을 줄여준다.
[이동생성자 사용 시 주의할 점]
1. 이동생성자에는 noexcept 키워드가 지정되어야 한다. (이동생성자 수행중, 예외가 없다는 것을 컴파일러가 인지해야함)
2. shallow copy가 일어나는 변수(포인터, 주소관련 변수)에 nullptr를 넣어줘야 함