• 카테고리

    질문 & 답변
  • 세부 분야

    프로그래밍 언어

  • 해결 여부

    해결됨

얕은 복사시 소유권 박탈에 관한 질문

23.03.16 05:13 작성 조회수 276

2

#include <iostream>
//#include "autoptr.h"
#include "autoptr2.h"
#include "resource.h"
#include "Timer.h"

AutoPtr<Resource> generateResource()  // AutoPtr<Resource> 타입을 리턴하는 함수
{
	// 10000000 의 length를 가진 Resource타입의 멤버를 가지는 AutoPtr 객체 생성
	AutoPtr<Resource> res(new Resource(10000000));

	return res;
}

int main()
{
	using namespace std;
	streambuf* orig_buf = cout.rdbuf();
	// cout.rdbuf(NULL); 화면에 출력되는 메세지들 끄기. 시간 어마어마하게 걸릴테니까 😎

	Timer timer;
	{
		AutoPtr<Resource> main_res; 
		main_res = generateResource(); // ⭐ generateResource() 리턴값은 R-value 
	}
	cout.rdbuf(orig_buf);
	//cout << timer.elapsed() << endl;
	timer.elapsed();//실행시간 재서 출력
}

메인.cpp

#pragma once
#include <iostream>
using namespace std;

template<typename T>
class AutoPtr
{
public:
	T* m_ptr;

public:
	AutoPtr(T* ptr = nullptr)
		:m_ptr(ptr)
	{
		cout << "AutoPtr default constructor" << endl;
	}
	~AutoPtr()
	{
		cout << "AutoPtr destructor" << endl;

		if (m_ptr != nullptr) delete m_ptr;
	}

	AutoPtr(AutoPtr&& a)  // ⭐이동생성자⭐ 
		: m_ptr(a.m_ptr) // ⭐얕은 복사⭐ 그냥 대입만 하면 땡이다!
	{
		cout << "AutoPtr move constructor" << endl;

		a.m_ptr = nullptr; // really necessary?
	}

	AutoPtr& operator = (AutoPtr&& a)  //R-value 레퍼런스 ,  ⭐*이동 대입 연산자 오버로딩⭐ 
	{
		cout << "AutoPtr move assignment" << endl;

		if (&a == this)
			return *this;

		// 공간은 비워줘야하는 것 똑같고 (delete 안하고 그냥 대입하면 메모리 누수가 발생할 수 있다)
		if (m_ptr != nullptr) delete m_ptr;

		m_ptr = a.m_ptr; // ⭐얕은 복사⭐ 그냥 대입만 하면 땡이다!
		a.m_ptr = nullptr; // 소유권 박탈

		return *this;
	}
	T& operator *() const { return *m_ptr; }
	T* operator ->() const { return m_ptr; }
	bool inNull() const { return m_ptr == nullptr; }
};

AutoPtr.h

#pragma once
#include <iostream>
using namespace std;

class Resource
{
public:
	int* m_data = nullptr;
	unsigned m_length = 0;

public:
	Resource() // 기본 생성자
	{
		cout << "Resource constructed" << endl;
	}

	Resource(unsigned length) // 일반 매개변수 1개 생성자
	{
		cout << "Resource length constructed" << endl;
		this->m_data = new int[length];
		this->m_length = length;
	}

	Resource(const Resource& res) // 💎복사 생성자💎 
	{
		cout << "Resource copy constructed" << endl;

		Resource(res.m_length);

		for (unsigned i = 0; i < m_length; ++i)  // 내용물을 전부 깊은 복사 (시간이 꽤 걸림)
			m_data[i] = res.m_data[i];
	}

	~Resource()  // 소멸자
	{
		cout << "Resource destroyed" << endl;
	}

	Resource& operator = (Resource& res)  // 💎대입 연산자 오버로딩💎
	{
		cout << "Resource copy assignment" << endl;

		if (&res == this) return *this; // 대입하려는게 자기 자신이면 아무것도 안함

		if (this->m_data != nullptr) delete[] m_data; // 1. 내 자신의 m_data 비워주기

		m_length = res.m_length; // 2. 대입으로 넘겨받은 res의 length 로 내 length 갱신

		m_data = new int[m_length]; // 3. 비워진 내 자신의 m_data에 새로운 공간 할당받기
		for (unsigned i = 0; i < m_length; ++i) // 4. m_data내용물 넣기.
			m_data[i] = res.m_data[i]; //  대입으로 넘겨받은 res의 m_data 내용물들을 **내 m_data**에 깊은 복사

		return *this;
	}
};

Resource.h

 

위 3코드는 수업에 나온 코드입니다.

 

얕은 복사시에 강사님께서 소유권 박탈의 목적으로 매개변수로 받은 참조 객체의 private변수인 포인터변수를 꼭 nullptr로 초기화를 해주는것이 깔끔하다고 하셨습니다.

예를 들면

AutoPtr(AutoPtr&& a)  // ⭐이동생성자⭐ 
		: m_ptr(a.m_ptr) // ⭐얕은 복사⭐ 그냥 대입만 하면 땡이다!
	{
		cout << "AutoPtr move constructor" << endl;

		a.m_ptr = nullptr; // really necessary?
	}

이나

AutoPtr& operator = (AutoPtr&& a)  //R-value 레퍼런스 ,  ⭐*이동 대입 연산자 오버로딩⭐ 
	{
		cout << "AutoPtr move assignment" << endl;

		if (&a == this)
			return *this;

		// 공간은 비워줘야하는 것 똑같고 (delete 안하고 그냥 대입하면 메모리 누수가 발생할 수 있다)
		if (m_ptr != nullptr) delete m_ptr;

		m_ptr = a.m_ptr; // ⭐얕은 복사⭐ 그냥 대입만 하면 땡이다!
		a.m_ptr = nullptr; // 소유권 박탈

		return *this;
	}

이 부분에서 a.m_ptr=nullptr;

이렇게요.

 

질문

1. nullptr로 초기화를 안시키면(소유권 박탈을 안하면) 문제가 생기나요?


2.제가 찾아보니까

"박탈 시키지 않으면 대입 연산자 인수로 이 인스턴스를 참조 하게 된 매개 변수 AutoPtr && a가 대입 연산자 호출이 종료됨에 따라 소멸자가 호출되어 delete될 수 있기 때문이다."

라는 글을 어디서 봤는데 이게 무슨말인지 모르겠는데 맞는 말인가요??


3.저 위의 코드에서 혹시 매개변수로 기능하는

AutoPtr && a같은 매개변수 객체도 함수 호출시에
정식적인 객체 처럼 생성자와 소멸자를 호출하나요?안하는걸로 알고있고 안하기 때문에 2번 질문이 이해가 안되서 질문하는거거든요.혹시 제가 햇갈리고 있나 싶어서 다시 질문드립니다.

답변 1

답변을 작성해보세요.

3

Soobak님의 프로필

Soobak

2023.03.16

안녕하세요, 답변 도우미 Soobak 입니다. 😀😀

질문 1. nullptr로 초기화를 안시키면(소유권 박탈을 안하면) 문제가 생기나요?
: 질문해주신 강의의 12:40 부분에서 교수님께서 설명해주시듯이, 소유권을 넘겨줬다는 것을 깔끔하게 코딩하는 것이 안전하고 좋습니다. 상황에 따라 다르겠지만, 문제가 생기는 경우가 있을 수 있기 때문입니다.
대표적으로 다음과 같은 이유들로 nullptr 로 초기화를 시키는 것이 안전하다고 생각합니다.
(1). 리소스 관리
: 이동 생성자와 이동 대입 연산자에서 소유권을 박탈하는 것은 리소스를 올바르게 관리하기 위해서입니다. 소유권이 박탈되지 않고 두 개의 AutoPtr 객체가 동일한 리소스를 가리키게 되면, 두 객체의 소멸자가 호출될 때 두 번 해제되는 문제가 발생할 수 있습니다. 이를 피하기 위해 소유권을 박탈한 객체의 포인터를 nullptr로 설정하여 해당 객체가 더 이상 리소스를 가리키지 않게 합니다.
(2). 예기치 않은 동작 방지
: 이동 생성자나 이동 대입 연산자를 사용하면, 해당 객체의 리소스는 다른 객체로 이동합니다. 소유권이 박탈된 객체가 여전히 리소스를 가리키고 있다면, 해당 객체를 사용하려는 시도는 예기치 않은 동작을 발생시킬 수 있습니다. 소유권 박탈 후 포인터를 nullptr로 설정하면, 해당 객체가 무효화되어 사용할 수 없음을 명확히 나타낼 수 있습니다.

 

질문 2. "박탈 시키지 않으면 대입 연산자 인수로 이 인스턴스를 참조 하게 된 매개 변수 AutoPtr && a 가 대입 연산자 호출이 종료됨에 따라 소멸자가 호출되어 delete될 수 있기 때문이다." 라는 글을 어디서 봤는데 이게 무슨말인지 모르겠는데 맞는 말인가요??
: 어디서 보신 글인지 출처를 알려주시면 저도 보다 정확하게 답변을 드릴 수 있을 것 같습니다. 😭😭😭
제 생각에는 아마도, 여러 개의 객체가 동일한 리소스를 가리키게되면, 각 객체들의 소멸자가 호출될 때마다 여러번 리소스를 해제하게 되는 경우에 대한 글을 보신 것 같습니다.... 😭
제 생각이 맞다는 가정하에, 두 개의 객체가 동일한 리소스를 가리키는 경우를 예를 들어 설명드려보겠습니다.

AutoPtr<Resource> ptr1(new Resource(100));
AutoPtr<Resource> ptr2(ptr1); // 얕은 복사 발생

위 코드에서는, ptr1ptr2 이 동일한 리소스를 가리키고 있습니다. 이 상태에서 ptr1ptr2가 소멸될 때, 소멸자가 각각 호출되어 동일한 리소스가 두 번 해제됩니다. 이렇게 되면 메모리 문제가 발생할 수 있습니다.

따라서, 이동 생성자와 이동 대입 연산자를 사용하여 이러한 문제를 해결합니다.

AutoPtr<Resource> generateResource()
{
    AutoPtr<Resource> res(new Resource(100));
    return res;
}

int main()
{
    AutoPtr<Resource> main_res;
    main_res = generateResource();
}

위 코드에서, generateResource() 함수는 AutoPtr<Resource> 타입을 반환합니다. 반환 과정에서 이동 생성자를 사용하여 리소스를 옮기며, 아래 코드에서 처럼 이동 생성자 내에서 소유권 박탈이 이루어집니다.

AutoPtr(AutoPtr&& a) : m_ptr(a.m_ptr)
{
    cout << "AutoPtr move constructor" << endl;
    a.m_ptr = nullptr; // 소유권 박탈
}

즉, 이동 생성자에서 a.m_ptrnullptr로 설정하여 소유권이 박탈되게 함으로써, 원래 객체는 더 이상 리소스를 가리키지 않게 되고, 새로운 객체만 원래 객체가 가리키던 리소스를 가리키게 되므로, 메모리 문제를 방지할 수 있습니다.


질문 3. 저 위의 코드에서 혹시 매개변수로 기능하는 AutoPtr && a같은 매개변수 객체도 함수 호출시에
정식적인 객체 처럼 생성자와 소멸자를 호출하나요? 안하는걸로 알고있고 안하기 때문에 2번 질문이 이해가 안되서 질문하는거거든요.혹시 제가 햇갈리고 있나 싶어서 다시 질문드립니다.

: 알고 있으신 내용이 맞습니다. 위의 코드에서 매개변수 객체 AutoPtr&& a는 함수 호출 시 추가적으로 생성자와 소멸자가 호출되지 않습니다. rvalue 참조가 참조하는 객체의 생성자와 소멸자는 해당 객체가 생성될 때와 소멸될 때 호출되기 때문입니다.
이동 생성자의 목적은 기존 객체에서 새로운 객체로 리소스를 이동하는 것이므로, 이동 생성자의 매개변수로 전달되는 rvalue 참조 객체의 생성자와 소멸자는 호출되지 않아도 문제가 없습니다.
이동 대입 연산자의 경우도 마찬가지로, rvalue 참조 매개변수 객체의 생성자와 소멸자는 추가적으로 호출되지 않습니다.
다시 한번, 질문2 에서 보신글의 출처를 알려주시면 저도 보다 정확하게 답변을 드릴 수 있을 것 같습니다. 😭😭

 

계속해서 꼼꼼하게 학습하시는 부분이 멋있으시네요!!!! 👍
학습 내용에 대하여 궁금증을 갖는 자세도 굉장히 멋있으십니다. 🙌🙌
추가적으로 궁금하신 점이 있으시면 또 편하게 질문남겨주시면 감사하겠습니다. 화이팅! 👍👍

seungmin38님의 프로필

seungmin38

질문자

2023.03.16

꼼꼼하게 설명해주셔서 이해하기 편하네요. 정성스러운 답변 감사합니다.