프로그래밍 공부
작성일
2023. 5. 11. 18:33
작성자
WDmil
728x90

C++에서 class가 생성될 때 기본적으로 만들어지는 것 또한 operator하여, 바꾸어줄 수 있다.

 

class간의 복사, 이동, 대입, 소멸 을 조정하여 class의 값변경을 더 자유롭고 편하게 지정해주는 방법이다.

#include <iostream>

using namespace std;

class Test
{
	int num1;
	int num2;

public:
	// 클래스가 만들어졌을 때 기본으로 들어가는 것
	// 기본
	// 복사
	// 이동
	// 복사 대입
	// 이동 대입
	// 소멸
	
	// 기본생성자. 로 취급되어 기본생성자는 사라진다.
	/*Test(int n1, int n2) : num1(n1), num2(n2)
	{}*/

	
	Test(int n1, int n2) : num1(n1), num2(n2)
	{}
};

class Fraction
{
	int numerator;
	int denominator;
public:
	Fraction(int num = 0, int den = 0)
		: numerator(num), denominator(den)
	{
		cout << "constructor" << endl;
	}
	
	// 복사 연산자
	// 이건 임의적으로 동작을 막는 것 이다.
	/*explicit*/ Fraction(const Fraction& fraction)
	{
		cout << "copy constructor" << endl;
	}

	// 복사 대입 연산자
	Fraction& operator = (const Fraction& rhs)
	{
		cout << "copy assignment operator" << endl;
		return *this;
	}
};

Fraction CreateFraction()// 이름 있는 객체
{
	Fraction temp(5, 2);
	return temp;
}

int main()
{
	Test t1(1, 2);
	Test t2 = t1; // 이럴경우 복사생성자가 된다.
	// 아무것도 넣지 않았을 때 디폴트 복사생성자가 들어가서 생성되게 된다.

	Fraction frac(3, 5);
	Fraction copy1(frac); // 복사 생성자. 복사 초기화
	Fraction copy2 = frac; // 복사 생성자.

	copy2 = frac; // = operator= 이다. 복사대입 생성자.

	int a = 5; // 복사
	int b = a; // 복사 대입연산자.

	int a(5); // 복사 초기화는 이와 같다.

	CreateFraction();

	return 0;
}

위 방법대로, class 내부에 복사대입, 복사, 등 연산자를 추가하여 원래 추가되는 기본연산자를 바꾸고 더 쉽게 연산을 진행시켜줄 수 있다.


C++에서 RVO와 NRVO라는 개념이 있는데, 이는 함수에서 객체를 반환할 때 복사생성자 호출을 피하여, 성능을 향상시키는 최적화 기법이다. RVO는 객체를 반환하는 경우이고, NRVO는 명시적 이름이 지정된 지역변수를 반환하는 경우에 적용된다.

 

각 기법은 C++11부터 컴파일러가 자동최적화를 수행해준다.

 

각 설명은 다음과 같다.

  • RVO(Return Value Optimization)
    • 함수가 임시객체를 반환하는 경우에 발생한다.
    • 컴파일러는 함수 내에서 생성한 이름이 없는 임시객체를 호출한 코드가 있는 변수에 직접 생성하여 복사생성을 하지 않도록 한다.
    • 그리하여, 변수를 중복생성하지 않게하여 필요없는 데이터낭비를 피하게 된다.
  • NRVO(Named Return Value Optimization)
    • 함수에서 명시적으로 이름이 지정된 지역 변수를 반환하는 경우에 발생한다.
    • 지역변수에 값을 할당한 다음 반환하는 경우, 컴파일러는 이 변수를 호출한 코드가 있는 변수에 직접 생성하게 된다.
    • RVO의 경우와 마찬가지로 복사생성자 호출을 피하는 경우라고 말하면 된다.
    • 디버그 시에는 복사하고 대입하는 식으로 연산하지만, 릴리즈 시에는 원본대입 형식으로 최적화해준다.

 

#include <iostream>

/*
	RVO = Retrun Value Optimization
	생성할 때 이름없는 객체는 자동최적화. 복사가아니라 원본대입
	NRVO = Named Return Value Optimization
	이름있는 이름있는 객체를 RVO처럼 해주는것. 
	최적화 할때 사용하는 방법.

*/

using namespace std;

class Test
{
	int value1;
	int value2;

public:
	static int count;

	Test(int value)
		:value1(value),
		value2(count++) // 생성할 때마다 카운트를 넣어준다.
	{
		cout << "constructor : " << this->value1 << ", " << this->value2 << endl;
	}

	Test(const Test& rhs)
		:value1(rhs.value1), value2(count++)
	{
		cout << "copy constructor : " << this->value1 << ", " << this->value2 << endl;
	}

	~Test()
	{
		cout << "destructor : " << this->value1 << ", " << this->value2 << endl;
	}
};

int Test::count = 1;

Test MakeRVO(int value)
{
	return Test(value);
	// 임시객체의 생성이 이루어지지 않고 그냥 반환만된다.
}

Test MakeNRVO(int value)
{
	Test test(value);
	return test;
	// 임시객체의 생성이 이루어지고 반환된다. 복사생성자가 호출된다.
}

int main()
{
	cout << "---RVO---" << endl;
	Test t1 = MakeRVO(1);

	cout << "---NRVO---" << endl;
	Test t2 = MakeNRVO(1);
	// 컴파일러 에서만 이러한 현상이 나타나고, 릴리즈에서는 나타나지 않는다.
	return 0;
}

위에서 설명한, RVO방식과 NRVO방식에도 operator를 하여, 다양한 방식으로 활용할 수 있다.

 

생성자를 operator하여, 다양한 방식대로 생성하여 데이터를 기입할 수 있는데, 이 방식을 사용하여 자동형변환을 사용할 수 있다.

#include <iostream>

using namespace std;

class Fraction
{
	int numerator;
	int denominator;

public:
	Fraction(int num, int den)
		: numerator(num), denominator(den)
	{
		cout << "constructor" << endl;
	}

	Fraction(const Fraction& other)
		:numerator(other.numerator), denominator(other.denominator)
	{
		cout << "copy constructor" << endl;
	}
	
	// 생성자를 처음에쓰는것 이 아닌 것을 막아버린다.
	explicit Fraction(int a)
		: numerator(a), denominator(1)
	{
		cout << "conversion constructor" << endl;
	}

	friend std::ostream& operator << (std::ostream& out, const Fraction frac)
	{
		cout << frac.numerator << " /" << frac.denominator << endl;
		return out;
	}
};

void PrintFraction(Fraction frac)
{
	cout << frac << endl;
}

int main()
{
	Fraction frac1(10, 20);
	Fraction frac2(frac1);

	Fraction frac3(1);

	Fraction frac4('A'); // int형으로 자동 형변환이 일어나서 int형으로 들어간다.
	Fraction frac5(3.14f); // flaot에서 int로 바뀐다.

	PrintFraction(frac1);
	PrintFraction(frac2);
	PrintFraction(frac3);
	PrintFraction(frac4);
	PrintFraction(frac5);

	//PrintFraction('A');
	//PrintFraction(3.14);
	//PrintFraction(100);

	return 0;
}

위 코드에서 Fraction frac4와 frac5는 원래라면, 변수가 들어가면 안되지만, explicit을 뺀다면, int형을 받아서, 값이 들어가게 된다.


C++에서 복사생성을 할 때 얕은복사 와 깊은복사가 있다.

 

얕은복사는, 값을 가져와서 복사하는 방법으로 복사하게 되는데 여기서 동적생성한 변수를 얕은복사를 하게되면,

값의 복사가 아닌 값의 원본을 가져오는 경우가 생길 수 있다. 그래서 가져온 값을 사용하다가 delete하게 되었을 때 허상포인터 가 생길 수 있어 피치못할 오류에 취약할 수 있다.

 

그래서 동적생성한 변수나 복사가 아닌 원본값을 가져와서 사용해야할 경우, 다음과 같은 방법을 사용해야 한다.

#include <iostream>
#include <cassert>

using namespace std;

/*
	deep copy vs shallow copy
	기본 복사 생성자 = 맴버 와 맴버의 복사를 지원하는 얕은복사이다.
	이렇게 하면 맴버에 동적할당을 하게 되면, 문제가 생긴다.
	그렇기에 내부에서 동적생성을 따로 해주고 복사해야하는 문제가 생긴다.
*/

class Mystring
{
public:
	char* data = nullptr;
	int length = 0;

	Mystring(const char* const src = " ")
	{
		assert(src);

		length = strlen(src) + 1;
		data = new char[length];

		for (int i = 0; i < length; i++)
			data[i] = src[i];
		data[length - 1] = '\0';
	}

	Mystring(const Mystring& other)
	{
		this->length = other.length;
		if (other.data != nullptr)
		{
			this->data = new char[length];
			for (int i = 0; i < this->length; i++)
				this->data[i] = other.data[i];
		}
		else
		{
			data = nullptr;
		}
	}
	~Mystring()
	{
		if (data != nullptr)
		{
			delete[] data;
			data = nullptr;
		}
	}

	Mystring& operator =(const Mystring& other)
	{
		if (this == &other)
		{
			return *this;
		}

		if (this->data != nullptr)
		{
			delete[] this->data;
			this->data = nullptr;

			cout << "copy assignment operator" << endl;
			this->length = other.length;

			if (other.data != nullptr)
			{
				this->data = new char[length];
				for (int i = 0; i < this->length; i++)
					this->data[i] = other.data[i];
			}
			else
			{
				this->data = nullptr;
			}
		}
		return *this;
	}

	Mystring(Mystring&& other)
	{
		cout << "move constructor" << endl;
		this->data = std::move(other.data);
		this->length = std::move(other.length);

		other.data = nullptr;
	}

	Mystring& operator= (Mystring&& other)
	{
		cout << "Move assignment operator" << endl;

		if (this == &other) // prevent self-assignment
			return *this;
		
		if (this->data != nullptr)
		{
			if (other.data != nullptr && this->data != other.data)
			{
				delete[] this->data;
				this->data = nullptr;

				this->data = std::move(other.data);
			}
		}
		else
			this->data = std::move(other.data);

		this->length = std::move(other.length);

		other.data = nullptr;

		return *this;
	}

};

int main()
{
	cout << "#################1#################" << endl;
	Mystring str("Hello");
	cout << (int*)str.data << endl;
	cout << str.data << endl << endl;

	cout << "#################2#################" << endl;
	// copy
	{
		Mystring copy(str);
		copy = str;
		cout << (int*)copy.data << endl;
		cout << copy.data << endl << endl;
	}

	cout << "#################3#################" << endl;
	if (str.data != nullptr)
	{
		cout << (int*)str.data << endl;
		cout << str.data << endl << endl;
	}
	else
	{
		cout << (int*)str.data << endl << endl;
	}

	cout << "#################4#################" << endl;
	{
		Mystring copy(std::move(str));
		copy = std::move(str);

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

	cout << "#################5#################" << endl;
	if (str.data != nullptr)
	{
		cout << (int*)str.data << endl;
		cout << str.data << endl << endl;
	}
	else
	{
		cout << (int*)str.data << endl << endl;
	}

	return 0;
}

위 방법대로, operator하여, 생성자 를 정의해주고, 대입연산자와 복사연산자를 따로 만들어주어 깊은복사를 실행할 수 있게 한다.

728x90