객체지향 프로그래밍을 접하다가 보면 깊은복사, 얕은복사 내용이 나오게 됩니다.

글로만 읽어서 보기에 이해력이 다소 떨어져서 코드와 함께 보도록 하겠습니다

 

이해를 돕기 위해 한글로 변수나 클래스명을 작성하였으나, 실제로는 영어로 해주세요..

 

#include <iostream>

//▼먼저 한영샘이라는 클래스를 만들었습니다
class 한영샘
{
	int age = 72;
};

//▼플밍반이라는 클래스도 만들었습니다
class 플밍반
{
private:
	int students;

public:
	한영샘* teacher; //플밍반은 한영샘을 동적할당으로 보유할 예정입니다

	플밍반() //생성자입니다
	{
		students = 0;
		teacher = new 한영샘;
		std::cout << "한영샘 생성" << std::endl;
	}

	~플밍반() //소멸자입니다
	{
		delete teacher;
		std::cout << "한영샘 제거" << std::endl;
	}
};

int main()
{
	플밍반 플밍25기;
	플밍반 플밍26기 = 플밍25기;

	std::cout << "플밍25기 주소값: " << &플밍25기 << std::endl;
	std::cout << "플밍26기 주소값: " << &플밍26기 << std::endl;

	std::cout << "-----------------" << std::endl;
	std::cout << "플밍25기 한영샘 주소값: " << &플밍25기.teacher << std::endl;
	std::cout << "플밍26기 한영샘 주소값: " << &플밍26기.teacher << std::endl;
	std::cout << "-----------------" << std::endl;

	std::cout << "플밍25기 한영샘 가리키는 주소값: " << &(*플밍25기.teacher) << std::endl;
	std::cout << "플밍26기 한영샘 가리키는 주소값: " << &(*플밍26기.teacher) << std::endl;

	std::cout << "디버깅용" << std::endl;

}

 

천천히 스텝을 하나씩 읽어보겠습니다. 이해가 가지 않는 번호는 댓글을 달아주세요

 

▼설계단계

1. 한영샘이라는 클래스를 작성하었습니다.

2. 플밍반이라는 클래스를 작성하습니다. 동적 할당된 한영샘을 보유할 예정입니다

 

▼main에서 진행단계

3. 플밍25기라는 기수를 main에서 정적할당으로 만들었습니다.

4. 플밍25기가 생성됨과 동시에, 플밍반 생성자가 호출 되었습니다

5. 플밍25기 생성자로 인하여 한영샘(변수명 teacher)이 동적할당으로 메모리를 가지게 되었습니다

 

 

이제 핵심이 나옵니다

6. 플밍반 플밍26을 만듦과 동시에 플밍 25기를 대입하였습니다.

7. 아이러니합니다. 분명 대입연산자에 대한 연산자오버로딩을 한 적이 없는데 코드가 문제없다고 합니다

8. 확인을 위해 콘솔에 주소값을 띄워보았습니다

 

9. ▼결과 사진은 아래와 같습니다

 

10. ▼그림으로 그려보겠습니다

11. 콘솔창을 보니, 몇몇 재미있는 것 들이 보입니다.

11-1. 플밍25기에서 보였던 생성자 관련 내용(한영샘 생성)이 플밍 26기에선 보이지 않습니다

11-2. 플밍25기 객체와 플밍26기 객체는 각기 다른 메모리 공간을 가지고 있습니다

11-3. 플밍25기 객체와 플밍26기 객체의 포인터도 결국 변수이기에 다른 메모리 공간에 존재합니다

11-4. 포인터야 각자 가지지만 가리키는 주소값을 복사해왔기에 같은 '한영샘' 객체를 가리키고 있습니다

 

▼여기까진 문제 없지만 해당 객체 둘을 소멸시킬때가 걱정입니다. 한번 알아보겠습니다

12. 프로그램을 종료한다면 플밍25, 플밍26 객체가 모두 소멸될 것입니다

13. 플밍반 소멸자 코드에 동적할당한 메모리 해제 코드가 있습니다. 일단 종료시켜보겠습니다

 

14. 바로 터집니다. 어떤 일이 일어나고 있을지, 한번 순서대로 가보겠습니다

15. 프로그램이 종료되며 플밍26 객체가 사라지며 소멸자를 호출합니다.

16. 동적할당되어있던 한영샘이 플밍26기 소멸자에 의해 사라졌습니다.

17. 이제 플밍25기 객체를 삭제할 시간입니다. 플밍25기 소멸자도 호출이 됩니다

18. 플밍25기 소멸자도 마찬가지로 가리키고 있던 위치의 한영샘을 지우려 하고 있는 상황입니다.

19. 이미 지워진 상태의 한영샘을 delete하려 하니 이제 문제가 생깁니다

 

※이미 해제된 영역을 가리키는 위험한 포인터를 보고 댕글링 포인터라고 부릅니다


모든 일의 원흉은 7번 입니다. 내가 작성한 코드도 아닌데, 생성자와 대입연산자, 뭔가 기본으로 존재하는 느낌입니다

 

생각을 해보니, 생성자와 소멸자도 코딩하지 않으면 알아서 기본으로 주어지는 것이 있는데,

생성하는 방법 중, 생성과 동시에 대입을 하면 호출되는 무언가가 있지 않을까요?

 

 ClassName (const ClassName &old_obj); 

네 맞습니다. 위 방식으로 만들 수 있는 생성자가 보이지 않게 존재합니다

생성자긴 하지만, 인자값으로 같은 객체의 참조값을 받습니다

 

플밍반 플밍26기(플밍25기);
플밍반 플밍26기 = 플밍25기;

즉 위와 같은 생성 방식이 이미 존재한다는 뜻 입니다.

기본 생성자가 있지만 저희가 재정의 하여서 쓰듯,

이 생성자도 재정의하여 우리가 원하는 방식으로 만들면 되지 않을까요?

▼예를들어 이런식으로요

    플밍반(const 플밍반& 복사대상)
    {
        students = 0;
        teacher = new 한영샘;
        std::cout << "한영샘 클론 생성" << std::endl;
    }

 

▼합쳐보겠습니다

#include <iostream>

//▼먼저 한영샘이라는 클래스를 만들었습니다
class 한영샘
{
	int age = 72;
};

//▼플밍반이라는 클래스도 만들었습니다
class 플밍반
{
private:
	int students;

public:
	한영샘* teacher; //플밍반은 한영샘을 동적할당으로 보유할 예정입니다

	플밍반() //생성자입니다
	{
		students = 0;
		teacher = new 한영샘;
		std::cout << "한영샘 생성" << std::endl;
	}

	플밍반(const 플밍반& 복사대상) //복사생성자 재정의
	{
		students = 0;
		teacher = new 한영샘;
		std::cout << "한영샘 클론 생성" << std::endl;
	}
    
	~플밍반() //소멸자입니다
	{
		delete teacher;
		std::cout << "한영샘 제거" << std::endl;
	}



};

int main()
{
	플밍반 플밍25기;
	플밍반 플밍26기 = 플밍25기;

	std::cout << "플밍25기 주소값: " << &플밍25기 << std::endl;
	std::cout << "플밍26기 주소값: " << &플밍26기 << std::endl;

	std::cout << "-----------------" << std::endl;
	std::cout << "플밍25기 한영샘 주소값: " << &플밍25기.teacher << std::endl;
	std::cout << "플밍26기 한영샘 주소값: " << &플밍26기.teacher << std::endl;
	std::cout << "-----------------" << std::endl;

	std::cout << "플밍25기 한영샘 가리키는 주소값: " << &(*플밍25기.teacher) << std::endl;
	std::cout << "플밍26기 한영샘 가리키는 주소값: " << &(*플밍26기.teacher) << std::endl;

	std::cout << "디버깅용" << std::endl;

}

 

▼원하던 방식은 객체당 고유의 한영샘을 가지는 것 이었습니다 결과물을 보겠습니다

일단 결과물을 보면 재미있는게, 대입을 통해 태어난 플밍26기는 기존 생성자가 아닌

재정의된 복사생성자 속 내용을 가지고 나타났습니다.

 

 

▼그림으로 표현하면 다음과 같습니다

▼이제 복사까지 진행된 두 객체를 삭제해보는 최종 단계를 수행하여 보겠습니다

 

이제는 문제 없이 소멸이 진행됩니다.


 

복사생성자를 재정의 하여 원하는 구동방식 달성하는것도 정답이지만

완전 복사 자체를 막아버리는 방법도 있습니다

 

1. 일단 아예 복사생성자 자체를 지워버리는 것이 가능합니다.

    플밍반(플밍반& 복사대상) = delete;

위 코드가 있을경우, 결과는 아래와 같습니다

누군가 실수로 대입으로 생성하려는 시도 자체를 막을 수 있습니다. 방어코딩이 가능해집니다

 

2. 프라이빗 영역에 복사생성자를 숨겨버리는 방법도 있습니다.

 

임의로 속에 코드가 들어있긴 한데, 중괄호 내용 속에 굳이 내용이 없어도 어차피 막을거라 무방합니다

결과는 다음과 같습니다.

 

 


얕은 복사는 맴버대 맴버 복사만 이루어지고 포인터와 같은 참조방식일경우, 참조위치만 복사하는 방식입니다

깊은 복사는 맴버복사는 물론, 참조방식이 있다면 별도의 메모리 공간까지 할당하여 객체 전체를 복사합니다.

복사생성자는 기본적으로 주어지는 맴버대 맴버 복사 방식 생성자로, 얕은복사를 진행시킵니다

'정보들 > 토막지식 관련' 카테고리의 다른 글

C++코드로 보는 객체지향 4대 특징  (0) 2022.11.08
WinAPI 아이콘 바꾸는 방법  (0) 2020.06.23
상대 카메라 설명 영상  (0) 2020.06.23
운영체제 - Round Robin  (0) 2020.06.23
바이트 패딩  (0) 2020.06.23

+ Recent posts