Channi Studies

[C++] Shallow Copying & Deep Copying | 얕은 복사 & 깊은 복사 본문

C++/객체지향 프로그래밍 (OOP)

[C++] Shallow Copying & Deep Copying | 얕은 복사 & 깊은 복사

Chan Lee 2023. 12. 29. 17:31

C++ Class 의 Copy Constructor에는

default copy constructor이 사용하는 Shallow Copying과 Deep Copying이 있습니다.

 

일반적으로는 얕은 복사, shallow copying을 사용해도 괜찮지만, 

얕은 복사가 문제를 야기할 때가 있습니다.

 

이번 포스트에서는 각각 방법을 정리해보겠습니다.

 


Shallow Copying - 얕은 복사

 

Shallow copy는 컴파일러가 기본적으로 생성해주는 default copy constructor가 행하는 방법이기도 합니다.

 

Shallow copying은 복사 대상의 객체의 모든 속성을 지닌 새로운 객체를 생성합니다.

 

여기서 주의해야 할 점은 포인터가 객체에서 사용될 때 입니다.

 

복사의 과정에서 포인터는 복사되지만, 포인터가 가리키는 위치, 즉 포인터값은 복사가 안됩니다.

(여기서 복사란 동일한 값을 지닌 새로운 대상을 생성하는 과정을 말합니다.)

 

 

그렇다면 뭐가 문제일까요?

 

복사 대상의 객체 A와, 복사되어 생성된 객체 B가 동일한 값의 다른 포인터를 지니고 있기 때문에,

결국에는 같은 메모리값을 포인팅하고 있다는 것 입니다.

 

우리는 두 객체에 같은 값을 다른 위치로 복사를 하려고 했는데,

아예 동일한 위치에 있는 동일한 데이터를 다루게 되는 것이죠.

 

 


코드 예시

 

이를 코드를 통해서 살펴보겠습니다.

다음과 같은 Shallow 클래스가 존재합니다.

// Shallow 클래스
class Shallow {
private:
  int *data;	// 정수를 가리키는 포인터

public:
  void set_data_value(int val) { *data = val; }
  int get_data_value() { return *data; }

  // Constructor | 생성자
  Shallow(int d);
  // Copy Constructor | 복사 생성자
  Shallow(const Shallow &source);
  // Destructor | 소멸자
  ~Shallow();
};

 

속성으로는 정수를 가리키는 포인터 data

 

메소드로는 data를 지정하는 set_data_value와, data를 반환하는 get_data_value 가 있습니다.

그리고 일반적인 생성자, 복사 생성자와 소멸자가 존재합니다.

 

각 멤버에 대한 내용은 다음과 같습니다.

Shallow::Shallow(int d) {
  data = new int;
  *data = d;
}

Shallow::Shallow(const Shallow &source) : data(source.data) {
  cout << "Copy Constructor - shalow copy" << endl;
}

Shallow::~Shallow() {
  delete data;
  cout << "Destructor freeing data" << endl;
}

void display_shallow(Shallow s) { cout << s.get_data_value() << endl; }

 

생성자는 data attribute을 힙 영역에서 동적으로 할당한 정수로 선언하고,

해당 영역에 생성자의 입력 정수 d를 저장합니다.

 

복사 생성자는 일반적인 형태 그대로이고, 복사 생성 시 문구를 출력합니다.

 

소멸자는 각 객체의 data attribute가 생성시 동적 할당되기 때문에 delete 키워드로 

동적으로 할당된 메모리를 해제해줍니다.

 

마지막으로 display_shallow는 입력받은 Shallow 객체 s의 get_data_value 메소드를 호출하여 출력합니다.

(get_data_value 메소드는 data 정수 값을 반환해줍니다)

 

 

마지막으로 메인 함수입니다.

int main(){
  Shallow obj1 {100};
  display_shallow(obj1);
  
  Shallow obj2 {obj1};
  obj2.set_data_value(1000);
  
  return 0;
}

 


 

여기까지 살펴 보았을 때

코드에 문제가 없어 보일 수 있지만, 사실은 중요한 문제점이 있습니다.

 

obj1 객체를 선언한 뒤,

display_shallow() 함수에 obj1을 인수로 실행하였습니다.

Pass by value 방법이므로, copy constructor을 호출하였겠죠?

 

여기서 문제가 발생합니다.

shallow copying으로 생성된 임시 객체(s)는 원본 객체인 obj1의 data를 복사하여 자신의 data 속성으로 삼습니다.

 

새로운 포인터에 obj1의 data 속성값을 저장하게 되는데, 

상술했듯 두가지 포인터는 동일한 위치를 가리키고 있습니다.

 

하지만 함수 로컬 값들은 함수 실행이 종료되면 어떻게 되죠?

당연히 메모리에서 소멸합니다.

 

임시로 생성된 객체 s는 display_shallow 함수 실행이 종료되면 바로 사라지면서,

스스로의 속성인 data 포인터의 위치를 해제해버립니다.

 

그런데 그 위치는 여전히 원본 객체인 obj1의 data 포인터가 가리키고 있던 위치였죠?

그럼 obj1의 data attribute는 invalid data를 지칭하게 됩니다.

 

여기서 심지어 Shallow obj2는 obj1을 복사하여 생성하려고 하고있죠?

그럼 obj2도 역시 obj1이 가리키는 invalid data를 속성으로 지닌 객체가 되는 겁니다.

 

그 다음에는 obj2가 data를 1000으로 바꿨죠?

그럼 obj1의 data도 같은 값이므로 1000으로 변경됩니다.

 

이런식으로 제대로만 바뀐다면, 어쩌면 잘 활용할 수도 있지 않을까요?

그렇지 않습니다.

 

객체는 scope를 벗어남과 동시에 소멸자를 호출합니다.

동적으로 할당되었기 때문에 우리는 delete를 사용하는거 기억 나시나요?

 

하지만 delete할 값이 invalid data이기 때문에,

프로그램은 충돌하고 오류가 발생할 수 밖에 없습니다.

 

어떤 식으로도 써먹을 수 없고, 그저 오류일 뿐 입니다.

 

 

이를 해결하기 위해서 존재하는 것이 바로 Deep Copying, 깊은 복사 입니다.

 


Deep Copying - 깊은복사

 

shallow copying이 문제를 야기한 원인이 무엇이였죠?

포인터가 가리키는 값이 아닌, 포인터 주소값만 복사하기 때문이였죠?

 

깊은 복사는 이를 해결하기 위해 포인터가 가리키는 주소의 값 까지도 복사를 합니다.

그렇기 때문에 깊은 복사를 할 때에는 힙 영역에서 새로운 포인터를 위한 데이터를 동적 할당하게 됩니다.

 

이런 이유로 깊은 복사는 클래스의 데이터 멤버에 raw pointer를 포함하고 있을 때 사용됩니다.

 

위의 shallow copying의 예시 코드와 동일한 예시를 deep copying으로 해결해보겠습니다.

 

모든 것이 동일하지만, 복사 생성자만이 다릅니다.

#include <iostream>
#include <string>

using namespace std;

class Deep {
private:
  int *data;

public:
  void set_data_value(int val) { *data = val; }
  int get_data_value() { return *data; }

  // Constructor
  Deep(int d);
  // Copy Constructor
  Deep(const Deep &source);
  // Destructor
  ~Deep();
};

Deep::Deep(int d) {
  data = new int;
  *data = d;
}

// ❗️❗️❗️ 복사 생성자 ❗️❗️❗️
Deep::Deep(const Deep &source) {
  // ⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
  data = new int;
  *data = *source.data;
  // ⬆️⬆️⬆️⬆️⬆️⬆️⬆️⬆️⬆️⬆️⬆️
  cout << "Copy Constructor - deep copy" << endl;
}

Deep::~Deep() {
  delete data;
  cout << "Destructor freeing data" << endl;
}

void display_deep(Deep s) { cout << s.get_data_value() << endl; }

int main() {
  Deep obj1{100};
  display_deep(obj1);

  obj1.set_data_value(1000);
  Deep obj2{obj1};

  return 0;
}

 

화살표로 표시된 부분에서,

 

복사 생성자의 경우에도 동적으로 정수를 할당한 뒤, 

해당 영역에 원본 객체의 data 내부값을 복사한 것이 보이시나요?

 

새로운 영역을 할당하고, 값을 복사한 과정입니다.

 

혹은 해당 부분을 다음과 같은 코드로도 구현 가능합니다.

Deep::Deep(const Deep &source) : Deep{*source.data} {
  cout << "Copy Constructor - deep copy" << endl;
}

 


 

여기까지 다 읽으셨다면, 대충 차이를 이해하셨을 것 같습니다.

 

핵심 포인트는

'raw pointer를 attribute로 사용하는 클래스에 대해서는 shallow copying이 아닌 deep copying을 사용하는 것'

입니다.

 

깊은 복사는 당연히 더욱 복잡하고 메모리를 많이 잡아먹습니다.

필요 없는 상황에서도 깊은 복사를 사용하는 것은 매우 비효율적이기 때문에 상황에 맞춰서 적절하게 사용할 필요가 있겠습니다.