얕은 복사 vs 깊은 복사 1, 2

CJB_ny·2022년 8월 16일
0

C++ 정리

목록 보기
56/95
post-thumbnail

얕은 복사 vs 깊은 복사

복사 시리즈

class Knight
{
public:
	int _hp = 10;
};

int main()
{
	Knight knight1;

	Knight knight2 = knight1; // 복사 생성자 호출
	// Knight knight3(knight1);

	Knight knight3; // 기본생성자 호출
	knight3 = knight1; // 복사 대입 연산자 호출


	return 0;
}

knight2를 만들때랑 knight3를 만드는 방법은 엄연히 다르다.

이코드를 실행을 하면은 굉장히 신기하게도

아무런 에러가 없이 통과를 하는데

이는 "복사 생성자", "복사 대입 연산자"는 컴파일러가 알아서 만들어 주기 때문이다.

컴파일러가 "암시적으로 만들어 준다"

차례대로 knight1~3의 주소인데 메모리에 올라간 주소는 다르고

그 객체의 멤버변수의 값은 복사를 하였기 때문에 다 똑같다.

컴파일러가 암시적으로 '복사'시리즈들을 만들어 주니까

이거밖에 사용못하나? => NO

이게 Knight라는 클래스안에 참조값이나 포인터가 들어가 있게 되면

이제 문제가 완전히 달라진다.

클래스내의 참조, 포인터


<초기화 리스트>

https://velog.io/@starkshn/%EC%B4%88%EA%B8%B0%ED%99%94-%EB%A6%AC%EC%8A%A4%ED%8A%B8

클래스 객체를 멤버 변수로 들고있을 경우

생성자함수의 초기화 리스트 부분에서 자동으로 기본생성자를 호출하여준다. (선처리 영역에서)



class Knight
{
public:
	int _hp = 10;
	Pet pet;
};

class Pet
{

};

근데 지금 pet을 포인터로 들고있지 않고 애 자체로 들고있게 되면은

단점들이 많다.

Knight가 만들어지자 마자 pet도 만들어지고

Knight가 소멸되자마자 pet도 소멸이된다.

이렇게하면 먼저 "생명 주기"의 관점에서 문제가 있고

더 심각한 부분은 Pet이라는 클래스가 엄청나게 크다고 가정을 할 경우

Knight도 덩달아서 엄청나게 비대해진다는 문제가 발생함.

근데 이거보다 더더더 문제는 뭐냐하면

이런 Pet을 상속을 받는 다른 클래스가 있다고 할 경우

이런식으로 바꿔치기 해서 사용할 수는 있겠지만

이런 거북이 펫이 있을 때 이녀석은 사용못함.

사용못한다는 말이 다시 Knight 의 멤버 변수를

TuttlePet _pet으로 바꿔 줘야한다는 말이다.

그래서 일반적으로 클래스에서 다른 클래스를 들고있을 경우

Pet* _pet; 이렇게 들고있는게 편하다.

실행

펫을 힙영역에 만들어 준다음에

knight가 펫을 소지를하고있다면 이렇게 넣어줄 수 있을 것이다.

(깔끔한거는 함수를 통해서 넣어주거나 생성자를 통해서 넣어주면 깔끔하기는 함)

그런데 처음으로 돌아와서

"복사"를 하게되면 문제가 발생을 하는데

knight2, knight3도 그대로 복사를 하기 때문에

똑같은 pet을 지니게 될 것이다.

힙에할당 받은 메모리 주소를 pet이 가지고있는데

복사 생성자, 복사 대입 연산자를 통해서 생성된 객체 애들의 _pet도 다 똑같은

힙에 할당된 _pet의 주소를 가진다.

얕은 복사 Shallow Copy

멤버 데이터를 비트열 단위로 "똑같이" 복사 (메모리 영역 값을 그대로 복사)

그래서 복사 생성자, 복사 대입 연산자를

통해서 데이터를 똑같이 싹다 복사를 하게되는 경우를

"얕은 복사"라고한다.

Stack : Knight1 [ _hp, 0x1000 ] -> Heap 0x1000 Pet [ ]
Stack : Knight2 [ _hp, 0x1000 ] -> Heap 0x1000 Pet [ ]
Stack : Knight3 [ _hp, 0x1000 ] -> Heap 0x1000 Pet [ ]

좋은 습관은 아니지만 생성자에서 동적할당하여

이렇게 펫을 만들고 소멸자에서 없앤다고 가정을 하면은

지금 딱봐도 얕은 복사일 경우 큰일 난거처럼 보인다.

지금 이게

딱 이상황이다. delete pet을 세번함.

동적할당할때 delete 여러번하면 안됨.

double free현상.

깊은 복사

멤버 데이터가 참조(주소) 값이라면, 데이터를 새로 만들어 준다. (원본 객체가 참조하는 대상까지 새로 만들어서 복사)

포인터는 주소값 바구니 -> 새로운 객체를 생성 -> 상이한 객체를 가르키는 상태가 됨.

그래서 우리가 깊은복사, 복사 대입 연산자를 깊게 사용을 하려면

직접 정의를 해주어야한다.

복사 생성자 깊은 복사로 재정의

복사 대입 연산자 깊은 복사로 재정의

인자로 들어온 녀석의 _pet의 주소를 그대로 복사를 하는것이 아니라

new Pet으로 동적할당하여 만든다음에

knight._pet의 내용물만 복사를 해준다음에

_pet에 동적할당한 주소를 주는 것임.

그러면

이런 서로 각기 다른 _pet의 주소를 가지게된다.

얕은 복사 vs 깊은 복사 2

암시적 복사 생성자 / 복사 대입 연산자 원리

어떻게 암시적으로 생겨나는지 원리는 알고있어야함.

암시적 복사 생성자 && 명시적 복사 생성자

암시적 복사 생성자

class Knight 
{
public:
	int _hp = 100;
    Pet _pet; // 포인터 아닐 경우에 멤버 클래스임. 이렇게 일반적인 형태로 물고있을 때
};

1) 부모 클래스의 복사 생성자 호출

2) 멤버 클래스 복사 생성자 호출

3) 1), 2) 둘다 아니라면 멤버가 기본 타입일 경우 메모리 복사. (얕은 복사)

명시적 복사 생성자

명시적 = 우리가 다 컨트롤 하기 때문에 기본 생성자가 호출 될 수 밖에 없다.

1) 부모 클래스의 기본 생성자 호출

2) 멤버 클래스 기본 생성자 호출

class Player
{
public:
	Player()
	{
		cout << "Player()" << endl;
	}
	Player(const Player& player) // 복사 생성자
	{
		cout << "Player() ref" << endl;
		_level = player._level;
	}
	Player& operator = (const Player& player)
	{
		cout << "Player operator = " << endl;
		_level = player._level;
		return *this;
	}
	~Player()
	{
		cout << "~Player()" << endl;
	}
public :
	int _level = 10;
};

class Knight : public Player
{
public:
	Knight()
	{
		cout << "Knight 기본 생성자 호출" << endl;
	}
	~Knight()
	{
		cout << "Knight 소멸자 호출" << endl;
	}

public:
	int		_hp = 10;
	Pet		 _pet;
};

이렇게 명시적으로 Knight의 복사 생성자와 복사 대입 연산자를 만들어 주지 않고 실행을 할 경우 어떻게 되는지 보도록 하자.

1) 부모 클래스의 복사 생성자 호출

2) 멤버 클래스 복사 생성자 호출

3) 1), 2) 둘다 아니라면 멤버가 기본 타입일 경우 메모리 복사. (얕은 복사)

이 규칙대로 실행이 되는지 보도록 하자.

암시적일 경우

1) 부모 클래스 복사생성자 호출함.

2) 멤버 클래스(Pet pet)의 복사생성자 호출함.

그리고 복사생성자를 명시적으로 만들어주면은

이렇게 해주면 차이점이 있는게 뭐냐하면은

부모님의 것들도 어느정도 복사를 해서 챙겨주어야 하는데

챙겨지지가 않는다라는 차이점이 있다.

아까와 다르게 호출이 된다.

복사생성자 어디에도 _level을 건드리는 부분이 없기 때문에

현재 knight1만 _level = 99이고 나머지는 10이다.(C++11로 초기화 해놓은 값)


중간정리 ❗❗❗

지금 얕은 복사와 깊은 복사에 대해 알아보고있다.

< 얕은 복사란? >

: 쉽게말하면 값들만 다 복사를 하는 것이다. 객체들 사이에서 복사를 하고 복사를 당하는 애들은 메모리의 개별적인 공간을 차지를한다.

< 깊은 복사란? >

: 완전히 새로운 데이터를 만드는 작업이다.
원본 객체가 참조하는 대상까지 새로 만들어서 복사하는 작업.


복사 생성자, 복대연 같은 경우 구현해 놓지 않으면 컴파일러가 자동으로 구현을 해준다.

또한 파생클래스가 컴파일러가 만들어준 복사 생성자, 복대연을 사용을 하면

부모의 "복사 생성자"를 호출하고 멤버 클래스가 있다면 멤버 클래스의 "복사 생성자"도 호출을 한다.

이럴경우 얕은 복사를 통하여 모든 값을 다 복사를 받는다.

반대로, 명시적으로 복사 생성자를 구현해 놓는 경우에는

명시적으로 구현해놓은 Knight의 복사생성자 코드만 실행을 하고

암시적으로 구현한 복사생성자는 부모클래스와 멤버 클래스의 복사생성자를 호출한 것과는 달리,

부모의 기본생성자와 멤버클래스의 기본 생성자만 호출하게된다.
(부모, 멤버 클래스의 복사생성자를 호출하고싶다면 명시적으로 적어주어야 한다.)

깊은 복사 생성자를 만들어주려면 직접 구현을 해주어야한다.

값부분의 경우에는 상관이 없는데 참조나 포인터같은 경우

이런식으로 해주어야 깊은 복사가 일어난다.


지금 사실은 명시적으로 구현해놓은 복사 생성자에

밑줄친 부분이 사실상 생략되어 있는 것이다.

명시적으로 구현한 복사생성자에서 부모의 복사 생성자도 호출하고싶다면

이렇게 명시적으로 적어 주어야한다.

그리고 또한 멤버클랫의 경우에도

이렇게 명시적으로 Pet의 복사생성자를 사용하겠다고 해주어야한다.

Pet pet(knight._pet); 이 문법이랑 똑같다.

기억해야할게 -> 명시적으로 구현을 해준 순간 복사와 관련된 것은 모든 것은 프로그래머가 챙겨야한다.

암시적 복대연 && 명시적 복대연

암시적 복사 대입 연산자

암시적 복사 생성자와 비슷하다.

1) 부모 클래스의 복사 대입 연산자를 호출한다.

2) 멤버 클래스의 복사 대입 연산자를 호출한다.

3) 멤버가 기본 타입일 경우 메모리 복사 (얕은 복사)

이렇게 부모의 복사대입 연산자 호출

멤버 클래스의 복사 대입 연산자 호출함.

명시적 복사 대입 연산자

반대로 이제 Deep Copy를 하기 위해서 명시적으로

복사대입 연산자를 구현한 순간 이제는 규칙이 달라짐.

1) 알아서 해주는거 없음.

2) 알아서 구현해서 싹다 해야됨.

이렇게 Knight클래스의 복사 대입에 이렇게 구현을 해주어야

암시적인 경우와 마찬가지로

복대연에서 복사를 해주는 것이 없어서 (명시적이기 때문에)

knight의 level이라던지 이런 데이터들이 복사가 안된다.

기본 생성자만을 호출하여 defalt값들만들 가지게된다!

와이리 혼잡하노?

객체를 "복사" 한다는 것은 두 객체의 값들을 일치 시키는 것

따라서 기본적으로 "얕은 복사 (Shallow Copy)" 방식으로 동작.

깊은 복사를 하게 될 경우 "필연적"으로 명시적으로 복사 생성자, 복사 대입 연산자를 구현해야하는데

명시적 복사 -> [모든 책임]을 프로그래머한테 위임 하겠다.

ㅇㅋ??

profile
https://cjbworld.tistory.com/ <- 이사중

0개의 댓글