전체코드
#include <iostream>
#include <cstring>
using namespace std;

class MyClass 
{
public:
	MyClass(const char _name[])
	{
		this->name = new char[strlen(_name) + 1];
		strcpy_s(this->name, strlen(_name) + 1, _name);
		cout << "기본 생성자 MyClass()" << this->name << endl;
	}

	MyClass(const MyClass& rhs)
	{
		this->name = new char[strlen(rhs.name) + 1];
		strcpy_s(this->name, strlen(rhs.name) +1, rhs.name);
		cout << rhs.name << "@@@@@@@@@@@복사 생성자 MyClass(const MyClass &rhs)@@@@@@@@@@" << endl;
	}

	//MyClass(MyClass&& rhs) : name(rhs.name)
	//{
	//	cout << rhs.name << "########## 이동 생성자(const MyClass&& rhs)##########" << endl;
	//  rhs.name = nullptr;
	//}

	operator string() { return this->name; }

	MyClass operator+(const MyClass& rhs)
	{
		cout << "operator+" << endl;
		MyClass temp(rhs.name);
		int thisNameLen = strlen(this->name);
		int rhsNameLen = strlen(rhs.name);
		strcat_s(temp.name, (thisNameLen + rhsNameLen + 1), this->name);
		return temp;
	}

	MyClass& operator=(const MyClass& rhs)
	{
		cout << "operator=" << endl;
		return *this;
	}

	void setName(const char _name[])
	{
		delete this->name;
		this->name = new char[strlen(_name) + 1];
		strcpy_s(this->name, strlen(_name) + 1, _name);
	}

	void printName()
	{
		cout << this->name << endl;
	}

private:
	char *name;
};


int main() 
{
	cout << "##########begin#########" << endl;
	MyClass a("aa"), b("bb"), c("cc");
	MyClass d = a + b + c;
	a.printName();
	cout << string(a) << endl;

	b.printName();
	cout << string(b) << endl;

	c.printName();
	cout << string(c) << endl;

	d.printName();
	cout << string(d) << endl;



	cout << "##########end#########" << endl;
	return 0;
}


// 출력값
##########begin#########
기본 생성자 MyClass()aa
기본 생성자 MyClass()bb
기본 생성자 MyClass()cc
operator+
기본 생성자 MyClass()bb
bbaa@@@@@@@@@@@복사 생성자 MyClass(const MyClass &rhs)@@@@@@@@@@
operator+
기본 생성자 MyClass()cc
ccbbaa@@@@@@@@@@@복사 생성자 MyClass(const MyClass &rhs)@@@@@@@@@@
aa
aa
bb
bb
cc
cc
ccbbaa
ccbbaa
##########end#########

MyClass라는 클래스를 만들고 인스턴스 a,b,c를 각각 정의했다.

이때는 당연히 기본생성자가 불린다.

파라미터 값으로 문자열을 받으니 _name[]으로 정의했다.

그런다음 name 포인터에 문자열을 담는 아주 간단한 기본 생성자이다.

 

문제는 MyClass d = a +b + c; 여기에 있다.

골때리게도 이 한줄이 이렇게 많은 생성자를 호출한다...

생성자를 호출한다는건 인스턴스가 생성된다는 뜻이고 여기에 따른 메모리, CPU Loss가 엄청나다.

 

여기서 주목해야할 부분은 바로 복사 생성자와 operator+ 더하기 연산자다. 

복사 생성자에서는 메모리 할당 문제 때문에 Deep Copy를 수행한다. (이전 글 참조)

2020/02/01 - [C++] - C++ 복사 생성자(참조형 파라미터)

 

C++ 복사 생성자(참조형 파라미터)

복사 생성자는 객체의 복사본을 생성할 때 호출되는 생성자이다. CMYData a CmyData b(a) 이처럼 원본 a의 복사본을 만들때 복사 생성자가 불린다. 문법은 클래스이름(const 클래스이름 &rhs) 처럼 사용한다. #inc..

leemoney93.tistory.com

operator + 를 살펴보자.

 

operator+
	MyClass operator+(const MyClass& rhs)
	{
		cout << "operator+" << endl;
		MyClass temp(rhs.name);
		int thisNameLen = strlen(this->name);
		int rhsNameLen = strlen(rhs.name);
		strcat_s(temp.name, (thisNameLen + rhsNameLen + 1), this->name);
		return temp;
	}
    
    a + b === a.operator+(b)

operator+에서는 할말이 아주 많은데 천천히 하나씩 해보면

1. operator+는 a+b를 할때 불리는 연산자이다.

2. a+b는 내부적으로 a.operator+(b)랑 같다

3. 그러므로 위의 코드 operator+의 rhs 값은 오른쪽 즉 b의 인스턴스의 참조이다.

4. operator+ 내부에서 temp 인스턴스를 생성하는데 b의 name값을 가지고 생성한다.

 ※ 기본 생성자 MyClass()bb 출력

5. a의 name값과 b의 name값을 합쳐서(strcat_s) MyClass instance temp를 return 한다.

 - (중요)이때 복사 생성자가 출력되는데 그 이유는 return temp를 할때 임시 객체를 생성하고 return 하기 때문이다.

MyClass(const MyClass& rhs)
{
  this->name = new char[strlen(rhs.name) + 1];
  strcpy_s(this->name, strlen(rhs.name) +1, rhs.name);
  cout << rhs.name << "@@@@@@@@@@@복사 생성자 MyClass(const MyClass &rhs)@@@@@@@@@@" << endl;
}
// 출력값
bbaa@@@@@@@@@@@복사 생성자 MyClass(const MyClass &rhs)@@@@@@@@@@

6. 위의 코드는 복사 생성자가 불릴땐데 rhs.name은 bbaa이다.

그러니깐 위의 temp instance를 기반으로 복사 생성이 이루어졌단뜻!

왜냐?? a + b를 하면 + c를 또 해야되는데(d = a + b + c) a + b의 결과 값을 가지는 임시 객체를 만들어야 하기 때문에 복사 생성이 필요하단 뜻이다. 근데 도대체 이동 생성이랑 무슨 관계인데?? 

위의 복사 생성은 Deep Copy를 하고있다. 아니 조만간 사라질놈이 왜 Deep 카피를 하냐 성능 떨어지게 그냥 포인터로 간단하게 끝내서 성능향상을 하면 되잖아! 이 논리로 나온게 바로 이동 생성이다.

MyClass(MyClass&& rhs) : name(rhs.name)
{
    cout << rhs.name << "########## 이동 생성자(const MyClass&& rhs)##########" << endl;
    rhs.name = nullptr;
}
    
// 출력값
bbaa########## 이동 생성자(const MyClass&& rhs)##########

전체코드의 주석을 풀자.

그럼 복사 생성자 대신 이동 생성자가 불린다.

이동 생성자의 rhs는 무엇일까? 바로 temp이다. 그러니깐 return temp를 할 때 temp instance를 기반으로 이동 생성자가 불렸다는 뜻. 당연하다 복사 생성자도 temp instance를 기반으로 했으니.

 

이동 생성자에서는 new로 메모리 할당받아서 Deep Copy이런거 안한다.

그냥 rhs.name(이때 rhs는 temp 임시 객체 r-value 참조)의 주소 값(포인터니깐)을 name에 할당 그러니깐 주소값만 name에 넣어주고 끝이다. 왜냐하면 조만간 사라질 임시 객체니깐! 그리고 그 값을 고스란히 return 하고 아름답게 전사한다. 이때 return 한 임시 객체 값은 + c 의 연산과정에서 또 쓰이게 된다!

 

이로써 Deep Copy(복사생성)에서 메모리 할당받고 어쩌고 저쩌고를 포인터 하나로 아주 성능 좋게 바꿨다.

 

 

'C++' 카테고리의 다른 글

C++ 멤버함수의 메모리 위치  (0) 2020.04.09
C++ 객체 생성  (0) 2020.04.02
C++ 대입 연산자  (0) 2020.02.02
C++ 깊은복사 얕은복사  (0) 2020.02.02
C++ 복사 생성자(참조형 파라미터)  (0) 2020.02.01
사전적 의미

깊은 복사는 실제로 두 개의 값(인스턴스가) 생성 되는 것이고, 얕은 복사는 원본은 하난데 포인터가 두개인 것이다.

 

얕은복사로 인한 에러 발생
#include <iostream>
using namespace std;

class CMYData
{
public:
	CMYData(int nParam)
	{
		m_pnData = new int;
		*m_pnData = nParam;
	};

	~CMYData()
	{
		delete m_pnData;
	};


	int GetData(void) { return *m_pnData; };

private:
	int *m_pnData = nullptr;
};


int main(void)
{
	CMYData a(10);
	CMYData b(a);
	cout << a.GetData() << endl;
	cout << b.GetData() << endl;

	return 0;
}

위의 코드를 실행하면 인스턴스 b의 소멸자를 Call 할 때 에러가 발생한다.

왜냐?? 진짜 중요한 개념인데 b의 Default 복사 생성자는 포인터를 제대로 처리하지 못하기 때문이다.

좀 더 자세히 말하면 인스턴스 b도 생성 될 때 새롭게 동적 할당 받아서 거기다가 값을 집어 넣야아하는데 Defalut 복사 생성자는 인스턴스 a의 m_pnData 값 즉 a에서 동적 할당 한 값을 그대로 b의 m_pnData에 집어 넣었기 때문이다. 그러니깐 인스턴스 a가 소멸될때 delete를 했는데 b가 소멸될 때 없는 메모리를 delete 하려 하니 에러가 발생하는것! 

 

깊은복사로 에러 탈출!
	CMYData(CMYData& rhs)
	{
		m_pnData = new int;
		*m_pnData = *rhs.m_pnData;
	}

위와 같은 복사 생성자를 넣어주자. 그럼 인스턴스 b도 생성될때 int 형 만큼의 Heap 메모리를 할당 받아서 그 메모리에 값을 쓴다!

'C++' 카테고리의 다른 글

C++ 임시객체(이동 생성자) 중요!  (0) 2020.02.07
C++ 대입 연산자  (0) 2020.02.02
C++ 복사 생성자(참조형 파라미터)  (0) 2020.02.01
C++ this 포인터  (0) 2020.02.01
C에서 C++ 객체지향의 변환 과정  (0) 2020.01.28
복사 생성자는 객체의 복사본을 생성할 때 호출되는 생성자이다.

CMYData a

CmyData b(a)

 

이처럼 원본 a의 복사본을 만들때 복사 생성자가 불린다.

문법은 클래스이름(const 클래스이름 &rhs) 처럼 사용한다.

 

#include <iostream>
using namespace std;

class CMYData
{
public:
	CMYData() { cout << "기본 생성자" << endl; };
	CMYData(const CMYData &rhs) 
	{
		this->m_nData = rhs.m_nData;
		cout << "복사 생성자" << endl;
	};

	int GetData(void) { return m_nData; };
	void SetData(int nParam) { m_nData = nParam; };

private:
	int m_nData;
};

int main(void)
{
	CMYData a;
	a.SetData(10);

	CMYData b(a);
	cout << b.GetData() << endl;
	return 0;
}

// 출력값
// 기본 생성자
// 복사 생성자
// 10

이런 식으로 CMYData b(a)와 같은 상황일 때 복사 생성자가 불린다. 여기서 rhs는 원본 즉 a 인스턴스의 참조 변수이다.

이것만 봐서는 도대체 복사 생성자가 왜 중요한지 모르겠다. 하지만 매개변수로 사용되는 복사 생성자를 보면 성능에 대해서 아주 중요한 이야기가 나온다. 결론은 가능하면 무조건 참조형으로 받자! 왜냐? 인스턴스가 하나 더 생성 되니깐

참조형으로 받겠다는 말은 주소로 넘기자 이말이다(call by reference)

 

매개변수로 사용되는 복사 생성자
#include <iostream>
using namespace std;

class CMYData
{
public:
	CMYData(int nParam)
		:m_nData(nParam)
	{
		cout << "기본 생성자" << endl; 
	};

	CMYData(const CMYData &rhs) 
		:m_nData(rhs.m_nData)
	{
		cout << "복사 생성자" << endl;
	};

	int GetData(void) { return m_nData; };
	void SetData(int nParam) { m_nData = nParam; };

private:
	int m_nData;
};

void TestFunc(CMYData param)
{
	cout << "TestFunc" << endl;
	param.SetData(20);
}

int main(void)
{
	CMYData a(10);
	TestFunc(a);
	cout << a.GetData() << endl;

	return 0;
}

//출력값
//기본 생성자
//복사 생성자
// TestFunc
// 10

다 집어치우고 main함수에서 TestFunc(a)를 호출하면 복사 생성자가 호출된다 왜냐하면 TestFunc의 매개변수는 CMYData param이기 때문이다. 그러니깐 param은 CMYData의 새로운 인스턴스다. 인스턴스를 하나 더 만들었다.

골때린다. 그리고 그 새로운 인스턴스에 20값을 집어 넣어놓고는 인스턴스가 삭제됐다(스택 프레임 종료). 그러니 a는 여전히 10일 수 밖에.. 아무것도 못하고 성능 낭비 메모리 낭비만 하고 사라지는 거지같은 함수 TestFunc다. 이것을 아주 우아하게 바꾸려면 앞에 & 참조만 붙여주면 된다.

바로 void TestFunc(CMYData &param)이렇게! 이렇게 해야 call by reference로 주소만 넘긴다! 객체를 생성하지도 않고 성능도 아주 좋다.

'C++' 카테고리의 다른 글

C++ 대입 연산자  (0) 2020.02.02
C++ 깊은복사 얕은복사  (0) 2020.02.02
C++ this 포인터  (0) 2020.02.01
C에서 C++ 객체지향의 변환 과정  (0) 2020.01.28
C++ 참조자 형식(reference)  (0) 2020.01.27

+ Recent posts