[C++] Chapter 11 - 연산자 오버로딩 2

Lee Jeong Min·2021년 1월 16일
0

Cpp

목록 보기
11/16
post-thumbnail

11-1 반드시 해야하는 대입 연산자의 오버로딩

객체간 대입연산의 비밀: 디폴트 대입 연산

대입 연산자는의 오버로딩 성격은 복사 생성자와 매우 유사하다.

복사 생성자의 대표적인 특징은 다음과 같다.

  • 정의하지 않으면 디폴트 복사 생성자가 삽입된다.
  • 디폴트 복사 생성자는 멤버 대 멤버의 복사(얕은 복사)를 진행한다.
  • 생성자 내에서 동적 할당을 한다면, 그리고 깊은 복사가 필요하다면 직접 정의해야한다.

대입 연산자의 대표적인 특징

  • 정의하지 않으면 디폴트 대입 연산자가 삽입된다.
  • 디폴트 대입 연산자는 멤버 대 메멉의 복사(얕은 복사)를 진행한다.
  • 연산자 내에서 동적할당을 한다면, 그리고 깊은 복사가 필요하다면 직접 정의해야한다.

대입 연산자 예제
FirstOperationOverloading2.cpp

#include <iostream>
using namespace std;

class First
{
private:
	int num1, num2;
public:
	First(int n1 = 0, int n2 = 0) : num1(n1) ,num2(n2)
	{}
	void ShowData() { cout << num1 << ", " << num2 << endl; }
};

class Second
{
private:
	int num3, num4;
public:
	Second(int n3 = 0, int n4 = 0) : num3(n3), num4(n4)
	{

	}
	void ShowData() { cout << num3 << ", " << num4 << endl; }

	Second& operator=(const Second& ref)
	{
		cout << "Second& operaotr=()" << endl;
		num3 = ref.num3;
		num4 = ref.num4;
		return *this;
	}
};

int main(void)
{
	First fsrc(111, 222);
	First fcpy;
	Second ssrc(333, 444);
	Second scpy;
	fcpy = fsrc;
	scpy = ssrc;
	fcpy.ShowData();
	scpy.ShowData();

	First fob1, fob2;
	Second sob1, sob2;
	fob1 = fob2 = fsrc;
	sob1 = sob2 = ssrc;

	fob1.ShowData();
	fob2.ShowData();
	sob1.ShowData();
	sob2.ShowData();
	return 0;
}

Second 클래스 내에 대입 연산자 오버로딩이 진행되었고 반환형이 참조형이다.

디폴트 대입 연산자의 문제점

AssignShallowCopyError.cpp

#include <iostream>
#include <cstring>
using namespace std;

class Person
{
private:
	char* name;
	int age;
public:
	Person(const char* myname, int myage)
	{
		int len = strlen(myname) + 1;
		name = new char[len];
		strcpy(name, myname);
		age = myage;
	}
	void ShowPersonInfo() const
	{
		cout << "이름: " << name << endl;
		cout << "나이: " << age << endl;	
	}
	~Person()
	{
		delete[]name;
		cout << "called destructor!" << endl;
	}
    /*
	Person& operator=(const Person& ref) //  이것이 있어야 오류 발생안함. 없으면 얕은복사가 진행되기 때문!
	{
		delete[] name;
		int len = strlen(ref.name) + 1;
		name = new char[len];
		strcpy(name, ref.name);
		age = ref.age;
		return *this;
	}
    */
};

int main(void)
{
	Person man1("Lee dong woo", 29);
	Person man2("Yoon ji yul", 22);
	man2 = man1;
	man1.ShowPersonInfo();
	man2.ShowPersonInfo();
	return 0;
}

동적할당이 진행되고 있는데 대입 연산자를 오버로딩하지 않고 디폴트 대입 연산자를 사용하고 있기 때문에 오류가 발생한다. 따라서 객체 소멸과정에서 중복 delete를 하게 되는 결과와 원래 가리키던 메모리의 포인터를 삭제하지않고 잃어버리게 되어 메모리의 누수가 발생한다. 주석된 부분이 있어야 오류가 발생 X

상속 구조에서의 대입 연산자 호출

대입 연산자는 생성자가 아니기 때문에 유도 클래스의 대입 연산자에는 아무런 명시를 하지 않으면, 기초 클래스의 대입 연산자가 호출되지 않는다!
InheritAssignOperation.cpp

#include <iostream>
using namespace std;

class First
{
private:
	int num1, num2;
public:
	First(int n1 = 0, int n2 = 0) : num1(n1), num2(n2)
	{

	}
	void ShowData() { cout << num1 << ", " << num2 << endl; }

	First& operator=(const First& ref)
	{
		cout << "First& operator=()" << endl;
		num1 = ref.num1;
		num2 = ref.num2;
		return *this;
	}
};

class Second : public First
{
private:
	int num3, num4;
public:
	Second(int n1, int n2, int n3, int n4) : First(n1, n2) , num3(n3) ,num4(n4)
	{}
	void ShowData()
	{
		First::ShowData();
		cout << num3 << ", " << num4 << endl;
	}
	/*
	Second& operator=(const Second& ref) // 풀고 실행시, First 클래스에 오버로딩된 대입연사자 실행 X
	{
    
        //First::operator=(ref) 이것을 넣어주어야함.
		cout << "Second& operator=()" << endl;
		num3 = ref.num3;
		num4 = ref.num4;
		return *this;
	*/
};

int main(void)
{
	Second ssrc(111, 222, 333, 444);
	Second scpy(0, 0, 0, 0);
	scpy = ssrc;
	scpy.ShowData();
	return 0;
}

유도 클래스의 대입 연산자 정의에서, 명시적으로 기초 클래스의 대입 연사자 호출문을 삽입하지 않으면, 기초 클래스의 대입 연사자는 호출되지 않아서, 기초 클래스의 멤버변수는 멤버 대 멤버의 복사 대상에서 제외된다.

이니셜라이저의 성능 향상

ImproveInit.cpp

#include <iostream>
using namespace std;

class AAA
{
private:
	int num;
public:
	AAA(int n = 0) : num(n)
	{
		cout << "AAA(int n = 0)" << endl;
	}
	AAA(const AAA& ref) : num(ref.num)
	{
		cout << "AAA(const AAA& ref)" << endl;
	}
	AAA& operator=(const AAA& ref)
	{
		num = ref.num;
		cout << "operator=(const AAA& ref)" << endl;
		return *this;
	}
};

class BBB
{
private:
	AAA mem;
public:
	BBB(const AAA& ref) : mem(ref) {

	};
};

class CCC
{
private:
	AAA mem;
public:
	CCC(const AAA& ref) { mem = ref; }
};

int main(void)
{
	AAA obj1(12);
	cout << "*******************" << endl;
	BBB obj2(obj1);
	cout << "*******************" << endl;
	CCC obj3(obj1);
	return 0;
}

클래스 CCC와 BBB의 유일한 차이점은 대입연산을 통해서 초기화를 하는 지 이니셜라이저를 사용해서 초기화를 하는 지이다. 이 코드를 실행해보면 CCC의 결과에서 선언과 초기화를 각각 별도의 문장에서 진행하는 형태로 바이너리 코드가 생성되기 때문에 AAA생성자와 대입연산자의 출력결과가 같이 나온다. 그러나 BBB의 경우 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성되기 때문에 복사 생성자만 호출된 것을 확인할 수 있다.


11-2 배열의 인덱스 연산자 오버로딩

배열보다 나은 배열 클래스

C와 C++의 기본 배열은 "경계 검사를 하지 않는다"라는 단점을 가지고 있다. 이를 해결하기 위해 배열 인덱스 연산자 오버로딩을 진행한다.
ArrayClass.cpp

#include <iostream>
using namespace std;

class BoundCheckIntArray
{
private:
	int* arr;
	int arrlen;
public:
	BoundCheckIntArray(int len) : arrlen(len)
	{
		arr = new int[len];
	}
	int& operator[] (int idx)
	{
		if (idx < 0 || idx >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}
		return arr[idx];
	}
	~BoundCheckIntArray()
	{
		delete[] arr;
	}
};

int main(void)
{
	BoundCheckIntArray arr(5);
	for (int i = 0; i < 5; i++)
		arr[i] = (i + 1) * 11;
	for (int i = 0; i < 6; i++)
		cout << arr[i] << endl;
	return 0;
}

배열 인덱스 오버로딩 함수는 반환형이 참조형이고 이것을 이용해서 배열요소에 저장된 값의 참조뿐만 아니라 변경도 가능하게한다. 그리고 객체의 복사 또는 대입이 얕은 복사로 이루어지기 때문에 이를 방지하고자 private:에 다음을 추가하는 것이 좋다.
// BoundCheckIntArray(const BoundCheckIntArray& arr) {}
// BoundCheckIntArray& operator=(const BoundCheckIntArray& arr) {}

const함수를 이용한 오버로딩의 활용

StableCCOnstArraySolu.cpp

#include <iostream>
#include <cstdlib>
using namespace std;

class BoundCheckIntArray
{
private:
	int* arr;
	int arrlen;
	BoundCheckIntArray(const BoundCheckIntArray& arr) { }
	BoundCheckIntArray& operator=(const BoundCheckIntArray& arr) { }

public:
	BoundCheckIntArray(int len) : arrlen(len)
	{
		arr = new int[len];
	}
	int& operator[](int idx)
	{
		if (idx < 0 || idx >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}
		return arr[idx];
	}
	int& operator[](int idx) const
	{
		if (idx < 0 || idx >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}
		return arr[idx];
	}
	int GetArrLen() const { return arrlen; }
	~BoundCheckIntArray() { delete[] arr; }
};

void ShowAllData(const BoundCheckIntArray& ref)
{
	int len = ref.GetArrLen();
	for (int idx = 0; idx < len; idx++)
		cout << ref[idx] << endl;
}

int main(void)
{
	BoundCheckIntArray arr(5);
	for (int i = 0; i < 5; i++)
		arr[i] = (i + 1) * 11;

	ShowAllData(arr);
	return 0;
}

배열의 인덱스 연산자 오버로딩 함수에서 const를 활용해 구분하였다. 만약 저 const함수가 없다면 ShowAllData함수에서 Bound~ 객체가 const형으로 매개변수를 받아오기 때문에 cout<<ref[idx]<<endl 에서 오류가 발생한다. 왜냐하면 ref.orpeator대괄호 (idx)로 해석이 되기 때문이다.

객체의 저장을 위한 배열 클래스의 정의

객체를 저장하는 배열클래스를 2가지 형태로 정의할 수 있다.

  • 객체의 주소 값을 저장하는 배열 기반의 클래스
  • 객체를 저장하는 배열 기반의 클래스

이 두 개중에 객체의 저장은 객체간의 대입연산을 기반으로 하기 때문에 객체의 주소 값을 저장하는 배열 기반의 클래스를 많이 사용한다.
StablePointPtrArray.cpp

#include <iostream>
#include <cstdlib>
using namespace std;

class Point
{
private:
	int xpos, ypos;
public:
	Point(int x = 0, int y = 0) : xpos(x), ypos(y) {}
	friend ostream& operator<<(ostream& os, const Point& pos);
};

ostream& operator<<(ostream& os, const Point& pos)
{
	os << "[" << pos.xpos << ", " << pos.ypos << "]" << endl;
	return os;
}

typedef Point* POINT_PTR;

class BoundCheckIntArray
{
private:
	POINT_PTR* arr;
	int arrlen;
	BoundCheckIntArray(const BoundCheckIntArray& arr) { }
	BoundCheckIntArray& operator=(const BoundCheckIntArray& arr) { }

public:
	BoundCheckIntArray(int len) : arrlen(len)
	{
		arr = new POINT_PTR[len];
	}
	POINT_PTR& operator[](int idx)
	{
		if (idx < 0 || idx >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}
		return arr[idx];
	}
	POINT_PTR& operator[](int idx) const
	{
		if (idx < 0 || idx >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}
		return arr[idx];
	}
	int GetArrLen() const { return arrlen; }
	~BoundCheckIntArray() { delete[] arr; }
};


int main(void)
{
	BoundCheckIntArray arr(3);
	arr[0] = new Point(3, 4);
	arr[1] = new Point(5, 6);
	arr[2] = new Point(7, 8);

	for (int i = 0; i < arr.GetArrLen(); i++)
		cout << *(arr[i]);
	delete arr[0];
	delete arr[1];
	delete arr[2];
	return 0;
}

다음과 같이 Point객체의 주소값을 저장하게되면 객체간 복사나 대입연산 시 깊은복사냐 얕은 복사냐 하는 문제를 신경쓰지않아도 된다. 대신 new와 delete는 신경 써주어야함!

연습문제중 어려웠던 문제
2차원 배열 접근에 대한 연산자 오버로딩
Problem11-2-2.cpp

#include <iostream>
#include <cstdlib>
using namespace std;


class BoundCheckIntArray
{
private:
	int* arr;
	int arrlen;
	BoundCheckIntArray(const BoundCheckIntArray& arr) { }
	BoundCheckIntArray& operator=(const BoundCheckIntArray& arr) { }
public:
	BoundCheckIntArray(int len) : arrlen(len)
	{
		arr = new int[len];
	}
	int& operator[](int idx)
	{
		if (idx < 0 || idx >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}
		return arr[idx];
	}
	int& operator[](int idx) const
	{
		if (idx < 0 || idx >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}
		return arr[idx];
	}
	int GetArrLen() const { return arrlen; }
	~BoundCheckIntArray()
	{
		delete[] arr;
	}
};

typedef BoundCheckIntArray* BoundCheckIntPtrArray;

class BoundCheck2DIntArray
{
private:
	int arrlen;
	BoundCheckIntArray** arr;
	BoundCheck2DIntArray(const BoundCheck2DIntArray& arr) { }
	BoundCheck2DIntArray& operator=(const BoundCheck2DIntArray& arr) {}
public:
	BoundCheck2DIntArray(int col, int row) : arrlen(col)
	{
		arr = new BoundCheckIntPtrArray[col];
		for (int i = 0; i < col; i++)
			arr[i] = new BoundCheckIntArray(row);
	}
	BoundCheckIntArray& operator[] (int idx)
	{
		if (idx < 0 || idx >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}
		return *(arr[idx]);
	}
	~BoundCheck2DIntArray()
	{
		for (int i = 0; i < arrlen; i++)
			delete arr[i];
		delete arr;
	}
};

int main(void)
{
	BoundCheck2DIntArray arr2d(3, 4);

	for (int n = 0; n < 3; n++)
		for (int m = 0; m < 4; m++)
			arr2d[n][m] = n + m;
	for (int n = 0; n < 3; n++)
	{
		for (int m = 0; m < 4; m++)
			cout << arr2d[n][m] << ' ';
		cout << endl;	
	}
	return 0;
}

2차원 배열접근 연산자 오버로딩을 위해 전에 사용한 배열의 인덱스 연산자 오버로딩을 위한 클래스를 이용하여 다음과 같이 문제를 해결하는 과정이다.


11-3 그 이외의 연산자 오버로딩

new, delete연산자 및 스마트포인터와 펑터 관련한 내용인데 이 부분은 건너 뜀

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글