728x90

C++에서의 생성자는 다양한 종류가 있다.

class LeafC
{
public:
	string _name;
};

int main()
{
	LeafC leafC;			// 1. 기본 생성자 호출
	LeafC leafC2 = leafC;	// 2. 복사 생성자 호출
	LeafC leafC3;			// 3. 기본 생성자 호출
	leafC3 = leafC;			// 4. 복사 대입 연산자.
}
 

2번의 복사 생성자와 4번의 복사 대입 연산자는 같은 역할을 하는 것처럼 보이지만 사실 다르다. 중간에 3번이 껴있기 때문이고 엄연히 생성자가 아닌 대입 연산자를 호출해준 것이기 때문에 결과는 같아도 과정은 다르다고 할 수 있다. 그리고 LeafC라는 클래스에는 어떠한 생성자도 정의해주지 않았는데 잘 실행되는 것을 볼 수 있다. 그 이유는 아무것도 정의해주지 않으면 컴파일러가 알아서 암시적으로 생성해주기 때문인데 지금은 아무런 문제가 되지 않지만 여기서 참조값 즉, 포인터와 같은 것이 내부에 포함되면 상황이 달라지게 된다.

class LeafCPtr
{

};

class LeafC
{
public:
	string _name;
	LeafCPtr* _ptr;
};

int main()
{
	LeafCPtr* ptr = new LeafCPtr(); // 포인터 생성

	LeafC leafC;			// 1. 기본 생성자 호출
	leafC._ptr = ptr;
	LeafC leafC2 = leafC;	// 2. 복사 생성자 호출
	LeafC leafC3;			// 3. 기본 생성자 호출
	leafC3 = leafC;			// 4. 복사 대입 연산자.
}
 

새로운 클래스를 하나 더 만들고 이번에는 포인터를 전달한 후에 디버깅 해보면 아래 사진과 같이 주소를 들고 있다는 것을 확인할 수 있다. 그 말은 leafC의 _ptr을 수정하면 leafC2, leafC3에도 영향이 당연히 간다는 소리가 된다.

이로 인해 확인할 수 있는 사실은 기본 복사 생성자와 복사 대입 연산자는 메모리에 저장되어 있는 값을 그대로 복사해준다는 것이고, 포인터는 주소를 저장하는 객체 이니 객체 안에 담겨있는 정보 즉, 주소값을 그대로 복사하여 전달했기 때문에 다음과 같은 상황이 생겼다는 것을 확인할 수 있다.

이런 복사를 얕은 복사(Shallow Copy)라고 한다. 그럼 깊은 복사(Deep Copy)란 무엇 일까? 간단하다. 얕은 복사의 문제점 이였던 포인터를 내부 변수로 들고있을 경우 주소값이 복사되는 문제를 해결하면 깊은 복사가 된다. 즉 같은 주소를 들고 있는게 아니라 새로운 메모리를 할당 받아 내용물만 복사해주면 된다는 것이다.

class LeafCPtr
{

};

class LeafC
{
public:
	LeafC() { }
	LeafC(const LeafC& val)
	{
		_name = val._name;
		_ptr = new LeafCPtr(*val._ptr);
	}

	LeafC& operator=(const LeafC& val)
	{
		_name = val._name;
		_ptr = new LeafCPtr(*val._ptr);
		return *this;
	}

public:
	string _name;
	LeafCPtr* _ptr;
};

int main()
{
	LeafCPtr* ptr = new LeafCPtr(); // 포인터 생성

	LeafC leafC;			// 1. 기본 생성자 호출
	leafC._ptr = ptr;
	LeafC leafC2 = leafC;	// 2. 복사 생성자 호출
	LeafC leafC3;			// 3. 기본 생성자 호출
	leafC3 = leafC;			// 4. 복사 대입 연산자.

	LeafC leafC4;
	LeafC leafC5;
	leafC4 = leafC5 = leafC;
}
 

명시적으로 복사 생성자를 만들어 주었고 포인터는 새로 메모리를 할당받아 내용물만 복사해 주었다. 기본 생성자 외에 다른 생성자를 명시해주기 위해서는 기본생성자가 존재해야하니 기본 생성자도 만들어 주었다. 포인터의 내용물을 복사하는 과정을 보면 new LeafCPtr(*val._ptr)로 되어있는데 이 과정에서 LeafCPtr의 기본 복사 생성자가 호출되게 되고 LeafCPtr의 모든 내용을 그대로 복사해주게 될 것이다. 만약 LeafCPtr에도 또 다른 포인터를 내부 변수로 두고 있다면 같은 문제가 또 생기게 되니 이 점을 주의해야한다.

추가적으로 복사 대입 연산자 같은 경우에도 명시를 해주어 깊은 복사가 실행되게끔 해주어야하는데, 여기서 반환 타입이 해당 클래스의 참조값을 반환하는 것을 볼 수 있다. 아래와 같이 void로 두고 해도 되지 않나 싶었는데 맨 아래와 같이 a = b = c 와 같은 문법을 지원하기 위한 방법이라고 할 수 있다.

void operator=(const LeafC& val)
{
	_name = val._name;
	_ptr = new LeafCPtr(*val._ptr);
}
 

그리고 이제는 깊은 복사가 일어났기 때문에 주소값이 모두 다르게 되어 있는 것을 확인할 수 있다.

암시적인 복사 생성자와 복사 대입 연산자는 기본적으로 얕은 복사를 수행 하려고 한다.

1) 부모 클래스의 복사 생성자(대입 연산자) 호출

2) 멤버 클래스의 복사 생성자(대입 연산자) 호출

위 두 단계를 거친 후 얕은 복사가 일어나게 된다.

복사 생성자나 복사 대입 연산자를 명시할 경우는 달라지게 되는데 일단 복사 생성자의 경우

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

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

위 두 단계를 거친 후 아무런 일도 일어나지 않는다. 부모와 멤버 클래스도 복사생성자를 호출하고 싶다면 아래와 같이 부모 클래스의 복사 생성자를 실행하게끔 명시해주어야한다.

class LeafC : public LeafCParent
{
public:
	LeafC() { }
	LeafC(const LeafC& val) : LeafCParent(val)
	{
		_name = val._name;
		_ptr = new LeafCPtr(*val._ptr);
	}

	LeafC& operator=(const LeafC& val)
	{
		_name = val._name;
		_ptr = new LeafCPtr(*val._ptr);
		return *this;
	}

public:
	string _name;
	LeafCPtr* _ptr;
};
 

복사 대입연산자의 경우는 정말 아무런 일도 해주지 않는다. 기본 생성자 또한 호출해주지 않기 때문에 모든 것을 손수 작성하는 수고를 해야한다.

 

728x90

'C++ > C++ 기본' 카테고리의 다른 글

생성자 (Constructor)  (1) 2023.05.16
템플릿 기초  (0) 2023.05.12
함수 포인터  (0) 2023.05.12
캐스팅  (0) 2023.05.12

+ Recent posts