Shallow vs Deep Copy
중요한 Part이다.★★★★★
◆ 얕은 복사의 문제
얕은 복사의 문제에 대해 알아보자
컴파일러가 멤버 변수를 알아서 복사해주지만
문제가 없을때도 있지만 문제가 있을 수 있다.
멤버 변수의 데이터 int 포인터를 가지고 있다.
주소값을 저장하고 있다.
어디에 주소값을 저장하고 있을까?
생성자를 보면 Shallow 객체가 만들면서
Heap 저장공간에 int data 포인터값이 저장되게 된다.
인자로 받은 int d를 역참조로 받았다.
멤버 변수에는 힙 메모리의 주소 값만 가지고 있다.
소멸자 부분에서는 할당된 heap 메모리 소멸을 하도록 구현하였다.
◆ 얕은 복사의 문제
● 포인터가 가리키는 데이터가 아닌 포인터 주소값의 복사
● 아래와 같이 구현된 경우 얕은 복사이다.
◆ 얕은 복사의 문제점
● 포인터가 가리키는 데이터가 아닌 포인터 주소값의 복사
복사 생성자는 멤버 변수들을 집어 넣는다.
Shallow obj1{100} 초기화를 한 다음
{obj1}값을 사용하게 되면
이때도 복사가 된다.
s2를 만드리려고 한다면 source의 데이터를 s2의 데이터로 복사할 수 도 있다.
사용자가 만들지 않으면 컴파일러 자체에서 복사 생성자가 동작한다.
기본적으로 복사생성자는 s가 가지고 있는 멤버 변수들을
s2 타켓에 멤버 변수에 저장한다.
s가 가지고 있는 멤버 변수 주소값을 s2에 집어 넣는다
어떤 문제가 생기는지 확인해보자
Shallow obj 1 에 생성되었다고 보자
stack / Heap 메모리를 확인해보자.
◆ 깊은 복사
● 주소값을 복사하는 것이 아니라, 데이터를 복사하여 복사 생성하는 방식
● 즉, 복사 생성자가 새로운 힙 공간을 할당한 뒤 동일한 데이터 복사
☞ "얕은 복사의 문제점" 과의 차이점을 확인할 것
◆ 깊은 복사의 경우
● 데이터도 복사하므로 이중 해제의 문제 해결
Shallow obj1{100}; obj1d에 100을 넣은 다음
display shallow(obj1); obj1를 호출하였다.
함수의 값 형태로 매개변수 전달하거나 , 값 형태로 반환하거나
기존 객체로 새 객체를 생성하는 경우에 복사 생성자가 호출한다.
코드로 확인해보자.
#include <iostream>
using namespace std;
class Player
{
private:
int hp;
int xp;
int x;
int y;
int speed;
public:
Player(int hp, int xp)
: hp{ hp }, xp{ xp }
{
cout << "생성자 호출됨" << endl;
}
// 복사 생성자
Player(const Player& other)
:hp{ other.hp }, xp{ other.xp }
{
cout << "복사 생성자 호출됨" << endl;
}
void Print()
{
cout << hp << " " << xp << endl;
}
};
void PrintInformation(Player p)
{
p.Print();
}
int main()
{
Player p1{ 10,2 };
Player p2{ p1 };
}
위의 코드를 컴파일 하게 되면 해당처럼 두가지 값이 나온다.
10,20 의 값은 const Player& other 인자에서 넘어온 p1 값이다.
p1의 객체를 p2 값으로 안다.
얕은 복사를 하고 , 포인터가 없기때문에 문제가 없다.
main 함수에
Player p3;
p3 = p1;
를 작성한 경우는 생성자만 호출된다.
Player p3에서 p3는 빨간줄이 뜨는데
인자 없는 생성자를 만들지 않고, 인자 있는 생성자만 만들었기 때문에
컴파일러 에러가 난다.
public:
Player() {};을 넣어주고 컴파일을 하게 되면 p3 컴파일러 에러는 사라진다.
위의 내용의 차이를 알아야한다.
복사 생성자가 생성되는 경우 3가지를 설명하였다.
기본 복사 생성자는 사용자가 구현하지 않았을 경우에 만들어준다.
동작은 멤버 변수들끼리 값을 그대로 대입하는 경우이다. (얕은 복사)
즉, 기본 복사 생성자는 얕은 복사를 하게 된다.
포인터를 멤버 변수로 가리키고 있을 경우에는
똑같은 주소를 가리키고 있는 객체 2개의 멤버 변수들이 생기기 때문에
이중 해제 오류가 발생할 수 있다.
그렇기 때문에 포인터를 멤버변수를 가지고 있을경우에는 깊은 복사를 진행해야 하며
깊은 복사 방식은 객체를 복사할때 새로운 힙 공간을 새로 할당하여
똑같은 데이터를 복사해서 집어넣는 방식이다.
p1.hp, p1.xp 의 주소값을 확인할 수 있다.
기본적으로 Local(로컬)과 Call Stack (호출스택)을 활용하면
우리가 필요로 하는 모든 데이터 확인을 할 수 있다.
생각한 대로 동작하는지 직접 확인을 할 수 있다.
Copy Constructor
◆ 복사 생성자 잘 사용하는 법
● 포인터 타입의 멤버 변수가 존재할 때는 (깊은) 복사 생성자 직접 구현
☞ 새로운 Heap 공간을 할당하여 값을 복사해 두어야 한다는 것을 명심
● STL / smart pointer 사용
☞ STL은 내부적으로 안전한 복사 생성자가 구현되어 있음
☞ Smart Pointer는 복사를 허용하지 않거나(unique) 참조되는 동안 해제하지 않음 (shared)
● 객체가 어떤 방식으로 복사될지 정의해 주어야 한다.
☞ 사용자가 구현하지 않는 경우 컴파일러에서 자동으로 복사 생성자를 정의한다.
(Summary) Copy Constructor
◆ 복사 생성자 개요
● 매개변수 전달, value 반환, 기존 객체로 새 객체 생성시 복사 생성자 호출
◆ 기본 복사 생성자
● 컴파일러가 만들어주는 기본 복사 생성자는 멤버 변수를 그대로 대입(얕은 복사)할 뿐
● 얕은 복사는 이중 해제 오류 발생
◆ 깊은 복사
● 새로운 힙공간을 할당하여 복사 생성
https://youtu.be/_RZ1by7WEq4?si=pgETzHj2nMbhqPey
'C++ > C++ : Study' 카테고리의 다른 글
OOP 파트2 - (4) 얕은 복사와 깊은 복사, this (0) | 2024.11.18 |
---|---|
OOP 파트2 - (5) this, const, static, friend (0) | 2024.11.18 |
OOP 파트2 - (2) 복사 생성자 (0) | 2024.11.03 |
OOP 파트2 - (1) 소개 (3) | 2024.10.28 |
OOP - (6) 생성자 오버로딩 (0) | 2024.10.28 |