C++ Chapter 9.12 : 대입 연산자 오버로딩, 깊은 복사, 얕은 복사

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!


chapter 9. 연산자 오버로딩 : 대입 연산자 오버로딩, 깊은 복사, 얕은 복사

  • 얕은 복사
    • 포인터 값인 주소만 복사하여 넘겨주는 것
  • 깊은 복사
    • 주소를 복사하여 넘겨주지 않고 나만의 새로운 메모리를 할당받아 확보한 뒤 그 공간에 내용물만 복사해 오는 것

🔔 얕은 복사

디폴트 복사 생성자

// MyString 클래스

MyString(const & MyString other)  // 디폴트 복사 생성자
{
    m_data = other.m_data;
    m_length = other.m_length;
}
  • 복사 생성자를 프로그래머가 정의해주지 않아도 모든 클래스는 기본적인 복사 생성자를 가지고 있다.
    • 프로그래머가 복사 생성자를 정의하지 않으면 디폴트 복사 생성자가 호출된다.
  • 디폴트 복사 생성자는 생략되어 프로그래머 눈엔 보이진 않지만 위와 같은 형태의 코드로 구성되어 있다.
    • 인수로 들어온 같은 타입의 다른 객체의 모든 멤버 값들을 복사하여 자신의 멤버 값으로 초기화 한다.
      • MyString(const & MyString other)
#include <cassert>
#include <iostream>
using namespace std;

class MyString
{
public:
	char *m_data = nullptr; 
	int m_length = 0;

	MyString(const char *source = "") 
	{
		assert(source);  // 문자열 인수로 꼭 받아야 함!

		m_length = std::strlen(source) + 1; 
		m_data = new char[m_length]; 
	
		for (int i = 0; i < m_length; ++i) 
			m_data[i] = source[i];

		m_data[m_length - 1] = '\0'; 
	}

	~MyString()  
	{
		delete [] m_data;
	}
};
  • MyClass 클래스
    • 멤버 변수
      • char * m_data
        • 문자열을 동적 할당받아 그 주소를 저장할 포인터

          이 멤버 포인터를 얕은 복사 방식으로 복사하는 과정에서 문제가 생긴다.

      • int m_length
        • 문자열 길이를 저장할 것
    • 생성자
      • MyString(const char *source = “”)
      • 이 생성자의 역할은 문자열 1개를 인수로 받아 두 멤버 m_data, m_length를 초기화 한다.
        • cf) char *타입의 포인터를 문자열 리터럴로 초기화 할 순 없지만 const char * 타입의 포인터를 문자열 리터럴로 초기화 하는 것은 가능하다! 참고 포스트
      • m_data 초기화
        • m_length 만큼의 길이를 가진 동적 배열을 할당 받아 주소를 저장한다.
        • for문 돌려서 인수로 받은 문자열의 한 글자, 한 글자를 복사해준다.
        • 마지막 원소는 \0
      • m_length 초기화
        • \0도 끝에 붙는 것을 생각해야 하므로 인수로 들어온 문자열 길이의 + 1
    • 소멸자
      • 멤버 포인터 m_data가 가리키는 동적 메모리를 해제시켜준다.


얕은 복사 사용시 문제점

int main()
{
	MyString hello("Hello");

	cout << (int*)hello.m_data << endl; 
	cout << hello.m_data << endl;

	{
		MyString copy = hello;

		cout << (int*)copy.m_data << endl;
		cout << copy.m_data << endl;
	}

	cout << hello.m_data << endl;

	return 0;
}
💎출력💎

014CFB00
Hello
014CFB00
Hello
硼硼硼硼硼硼硼硼핥퀪?  
  • MyString hello(“Hello”);
    • hello객체의 m_data 는 힙메모리 주소값으로 초기화 되고
    • hello객체의 m_length는 6의 값으로 초기화 된다.
  • cout « (int*)hello.m_data « endl;
    • hello객체의 m_data 값(주소값) 출력
    • (int*)를 붙이는 이유
      • std::cout은 문자열 포인터 (char *)타입이 들어올 경우 포인터가 아닌 문자열 내용을 출력하게끔 ostream 클래스 안에 오버로딩 되어 있기 때문에 주소값을 출력해주기 위해 int* 로 형변환
  • cout « hello.m_data « endl;
    • hello객체의 *m_data 문자열로 출력
  • {} 지역 범위
    • MyString copy = hello
      • copy 객체가 생성될 때 hello객체를 복사하므로 디폴트 복사 생성자가 호출된다.
        • 현재 Mystring 클래스 안에서 복사생성자를 정의하지 않았기 때문에 디폴트 복사 생성자가 호출 됨
          // 디폴트 복사 생성자는 다음과 같이 동작함
          MyString(const & MyString other)  
          {
              m_data = other.m_data;     // ⭐얕은 복사⭐
              m_length = other.m_length;
          }
          

          디폴트 복사 생성자에서 m_data = other.m_data; 즉, 주소가 복사 되면서 copy 객체의 m_data 포인터와 hello 객체의 m_data 포인터는 동일한 메모리를 가리키게 된다. 이러한 과정을 얕은 복사라고 한다. 두 주소값 출력 결과도 둘 다 “014CFB00”로 같은 것을 볼 수 있다.

    • cout « (int*)copy.m_data « endl;
      • copy객체의 m_data 값(주소값) 출력
    • cout « copy.m_data « endl;
      • copy객체의 m_data 문자열로 출력
    • 스코프가 끝나면서 copy 객체가 소멸된다
      • copy의 소멸자가 호출 된다.
        • copym_data 가 가리키는 동적 메모리가 해제된다.
  • cout « hello.m_data « endl;
    • 이상한 값이 출력된다!

      얕은 복사의 과정으로 인해 copy 객체의 m_data 포인터와 hello 객체의 m_data 포인터가 동일한 동적 메모리 공간을 가리키고 있던 상태에서, copy 객체가 소멸되며 호출한 소멸자에서 *m_data* 도 해제시켰기 때문! 없어져 텅 빈 공간에 접근해 출력하려고 하니 이상한 값들이 출력되는 것이다.


🔔 깊은 복사 구현하기

깊은 복사와 얕은 복사의 차이

  • 다른 객체의 멤버 값들을 내 멤버로 복사해올 때
    • 동적 메모리를 가리키는 포인터 멤버를 가지고 있는 클래스의 경우
    • 얕은 복사를 사용하여 주소를 복사하여 넘기게 되면
      • 복사 후 두 객체의 포인터 멤버가 동일한 공간을 가리키게 된다.
      • 따라서 한 객체의 포인터 멤버가 delete로 해제해도 다른 객체의 포인터 멤버는 아직 그 공간을 가리키고 있는게 되기 때문에 문제가 생긴다.
    • 주소는 복사하지 않고 새로운 공간을 할당하여 포인터 멤버가 가리키는 공간의 내용물만 복사해온다면
      • 두 객체의 포인터 멤버가 다른 공간을 가리키는 것이 되니 한 객체의 포인터 멤버가 해제되어도 다른 객체에 전혀 문제가 가지 않는다.
      • 이러한 복사 방법을 깊은 복사라고 한다.


복사 생성자와 대입 연산자 오버로딩의 차이

복사 생성자, =대입 연산자 둘 다 복사할 수 있다는 점에서 기능은 비슷하다.

  1. 복사 생성자
    • 생성자이므로 객체가 생성되는 과정에서 대입이 있는 경우 호출 된다.
     MyString str1("Hello");
    
     MyString str2(str1);  // ⭐복사 생성자 호출
     Mystring str3 = str1; // ⭐복사 생성자 호출
    
  2. 오버로딩 된 대입 연산자 =
    • 단순히 이미 존재하는 객체끼리의 대입시 호출된다.
     MyString str1("Hello");
     MyString str2;  
    
     str2 = str1;  // ⭐ 대입 연산자 오버로딩 호출
    
    • 자기 자신을 대입하는 것도 가능하므로 이 경우에 대한 처리가 필요하다.
    • 자기 자신의 기존 동적 메모리를 비워주는 과정이 필요하다.
      • 자기 자신에게 다른 객체를 복사하여 덮어 씌우는것이니까.


깊은 복사 구현하기

복사 생성자

MyString(const MyString &source) 
{
	m_length = source.m_length; 

	if (source.m_data != nullptr) 
	{
		m_data = new char[m_length];  

		for (int i = 0; i < m_length; ++i)
			m_data[i] = source.m_data[i];
	}
	else
		m_data = nullptr;	
}
  • if (source.m_data != nullptr)
    • 복사 대상이 되는 객체의 m_data가 nullptr이 아닌 경우에만 깊은 복사 진행.
      • 포인터가 가리키는 곳을 참조해야 하므로 nullptr이 아닌지 꼭 체크해 주어야 한다.
      • nullptr이라면 똑같이 나의 m_data 값도 nullptr로!
    • 깊은 복사
      1. 새로운 공간 할당
        • m_data = new char[m_length];
      2. 복사 대상이 되는 객체의 m_data가 가리키는 내용물들 (동적 배열 원소들) 복사해오기
        • for문 돌려서 일일이 복사
int main()
{
	MyString hello("Hello");

	cout << (int*)hello.m_data << endl; 
	cout << hello.m_data << endl;

	{
		MyString copy = hello;  // ⭐복사 생성자 호출⭐

		cout << (int*)copy.m_data << endl;
		cout << copy.m_data << endl;
	}

	cout << hello.m_data << endl;

	return 0;
}
💎출력💎

013DF918
Hello
013DFAD8
Hello
Hello
  • 출력 결과 hello객체의 m_data 값과 copy객체의 m_data 값이 서로 다른 것을 알 수 있다.
  • 출력 결과 copy객체가 해제 되고도 hello 객체에 영향 없이 m_data 내용물에 잘 접근할 수 있는 것을 볼 수 있다.

copy 객체가 hello 객체로부터 멤버 값들을 복사 받을때 새로운 공간을 할당해서 내용물들을 옮긴거라 두 객체의 m_data 은 아예 다른 공간을 가리키고 있기 때문이다. 즉, 깊은 복사를 했기 때문!


대입 연산자 오버로딩

📢 주의사항 : = 대입 연산자 오버로딩은 멤버 함수로만 구현이 가능하다.이유는 모르겠지만😱 전역 함수로 구현하는 것은 막혀있다.

  • 멤버 함수로 구현되기 때문에 =를 기준으로 왼쪽 피연산자 객체가 멤버 함수를 호출하는 자기 자신이 되며, 오른쪽 피 연산자 객체가 인수가 된다.
MyString& operator = (const MyString & source)
{
	if (this == &source)   // 자기 자신을 대입하는 경우
		return *this;

	delete[] m_data;   // 자신의 기존 내용물 비워주기 

    /* 아래 과정은 복사생성자 깊은 복사 구현과 같다 */

	m_length = source.m_length;

	if (source.m_data != nullptr)
	{
		m_data = new char[m_length];

		for (int i = 0; i < m_length; ++i)
			m_data[i] = source.m_data[i];
	}
    else
        m_data = nullptr;
}
  1. 자기 자신을 대입하는 경우엔 자기 자신을 리턴한다.
  2. 자신의 기존 내용물을 비워준다.
    • 자신의 m_data 가 가리키는 메모리를 해제시켜준다.
    • 새로운 공간을 할당 받을거라서!
  3. 깊은 복사
    • 이 부분은 복사생성자와 동일하다.
int main()
{
	MyString hello("Hello");

	cout << (int*)hello.m_data << endl; 
	cout << hello.m_data << endl;

	{
        Mystring copy;
		copy = hello;  // ⭐오버로딩 한 대입 연산자 호출⭐

		cout << (int*)copy.m_data << endl;
		cout << copy.m_data << endl;
	}

	cout << hello.m_data << endl;

	return 0;
}
💎출력💎

013DF918
Hello
013DFAD8
Hello
Hello
  • 출력 결과 hello객체의 m_data 값과 copy객체의 m_data 값이 서로 다른 것을 알 수 있다.
  • 출력 결과 copy객체가 해제 되고도 hello 객체에 영향 없이 m_data 내용물에 잘 접근할 수 있는 것을 볼 수 있다.

copy 객체가 hello 객체로부터 멤버 값들을 복사 받을때 새로운 공간을 할당해서 내용물들을 옮긴거라 두 객체의 m_data 은 아예 다른 공간을 가리키고 있기 때문이다. 즉, 깊은 복사를 했기 때문!



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기


Cpp 카테고리 내 다른 글 보러가기

댓글 남기기