본문 바로가기
Programming/C++

연산자 오버로딩 2

by OKOK 2017. 8. 1.

대입 연산자의 오버로딩은 클래스 정의에 있어서 생성자, 복사 생성자와 하께 빠질 수 없는 요소입니다. 


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

정의하지 않으면 디폴트 복사 생성자가 삽입됩니다. 디폴트 복사 생성자는 멤버 대 멤버의 복사를 진행합니다. 생성자 내에서 동적 할당을 한다면, 깊은 복사가 필요하다면 직접 정의해야 합니다. 정의하지 않으면 디폴트 대입 연산자가 삽입됩니다. 디폴트 대입 연산자는 메버 대 멤버의 복사(얕은 복사)를 진행합니다. 연산자 내에서 동적 할당을 한다면, 깊은 복사가 필요하다면 직접 정의해야 합니다. pos2도, 그리고 pos1도 이미 생성 및 초기화가 진행된 객체라는 사실입니다. 


#pragma warning(disable:4996)


#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 << "Seoncd& operator=()" << 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;


디폴트 대입 연산자가 삽입되어 멤버 대 멤버의 복사가 진행됩니다. 대입 연산자는 오른쪽에서 왼쪽으로 진행됩니다. 다라서 이 문장이 실행된다는 사실을 통해서 디폴트 대입 연산자의 반환형을 유추할 수 있습니다. 단순한 대입연산이 아닌, 대입 연산자를 오버로딩 한 함수의 호출이기 때문입니다.


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


#pragma warning(disable:4996)

#include <iostream>

#include <cstring>


using namespace std;


class Person

{

private:

char * name;

int age;

public:

Person(char * myname, int myage)

{

int len = strlen(myname) + 1;

name = new char[len];

strcpy(name, myname);

age = myage;

}


void ShowPersonInfo() const

{

cout << "name: " << name << endl;

cout << "age: " << age << endl;

}

~Person()

{

delete[]name;

cout << "called destructor" << endl;

}

};


int main(void)

{

Person man1("Lee dong woo", 29);

Person man2("Yoon ji yul", 22);

man2 = man1;

man1.ShowPersonInfo();

man2.ShowPersonInfo();

return 0;


하나의 문자열을 두 개의 객체가 동시에 참조하는 상황이 벌어지게 되며, 이로 인해 다음의 두 가지 문제가 발생하게 됩니다. 문자열 Yoon ji yul 을 가리키던 문자열의 주소 값을 잃게 됩니다. 얕은 복사로 인해서, 객체 소멸과정에서 지워진 문자열을 중복 소멸하는 문제가 발생합니다. 깊은 복사가 진행하도록 정의합니다. 메모리 누수가 발생하지 않도록, 깊은 복사에 앞서 메모리 해체의 과정을 거칩니다. 



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

우도 클래스의 대입 연사자에는 아무런 명시를 하지 않으면, 기초 클래스의 대입 연산자가 호출되지 않는다. 

#pragma warning(disable:4996)

#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)

{

cout<<"Secoond& 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;


기초 클래스에는 멤버 대 멤버의 복사가 이뤄지도록 대입 연산자가 오버로딩 하였습니다. 유도 클래스에도 멤버 대 멤버의 복사가 이뤄지도록 대입 연사자를 오버로딩 하였습니다. 유도 클래스의 대입 연산자 정의에서, 명시적으로 기초 클래스의 대입 연산자 호출문을 삽입하지 않으면, 기초 클래스의 대입 연산자는 호출되지 않아서, 기초 클래스의 멤버 변수는 멤버 대 멤버의 복사 대상에서 제외됩니다. 


이니셜라이저가 성능 향상에 도움을 줍니다.


#pragma warning(disable:4996)


#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=(cont 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;


BBB 클래스는 이니셜라이저를 이용해서 멤버를 초기화하고 있습니다. CCC 클래스는 대입연산을 이용해서 멤버를 초기화하고 있습니다. 이러한 초기화 방식의 차이가 BBB 클래스와 CCC 클래스의 유일한 차이점입니다. BBB 객체의 생성과정에서는 복사 생성자만 호출되었는데, CCC 객체의 생성과정에서는 생성자와 대입 연사자까지 호출되었습니다. 이니셜라이저를 이용하면 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성됩니다. 


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

객체 arrObject의 멤버함수 호출로 이어집니다. 연산자가 []이므로 멤버함수의 이름은 operator[] 입니다. 함수호출 시 전달되는 인자의 값은 정수 2입니다. 


#pragma warning(disable:4996)


#include<iostream>

#include<cstring>

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 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;


반환형에 주목합니다. return 문에서는 인자로 전달된 인덱스에 해당하는 배열요소를 반환하는데, 반환형이 참조형입니다. 떄문에 배열요소의 참조값이 반환되고, 이 값을 이용해서 배열요소에 저장 된 값의 참조뿐만 아니라 변경도 가능합니다. 접근하는 문장만 놓고 보면, 배열에 직접 접근하는 느낌을 줍니다. 이렇듯 실제로 배열처럼 느끼고 사용할 수 있습니다. 벗어난 범위의 배열접근 결과의 확인을 위해서 반복의 범위를 0~5로 지정하였습니다. 클래스 객체의 복사 또는 대입은 얕은 복사로 이어지기 때문에, 단순히 코드만 놓고 보면, 깊은 복사가 진행되도록 복사 생성자와 대입 연산자를 별도로 정의해야 한다고 생각할 수 있습니다. 배열은 저장소의 일종이고, 저장소에 저장된 데이터는 유일성이 보장되어야 하기 때문에, 대부분의 경우 저장소의 복사는 불필요하거나 잘못된 일로 간주됩니다. 따라서 깊은 복사가 진행되도록 클래스를 정의할 것이 아니라, 위의 코드에서 보이듯이 빈 상태로 정의된 복사 생성자와 대입 연산자를 private 멤버로 둠으로써 복사와 대입을 원척적으로 막는 것이 좋은 선택이 되기도 합니다. 


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

const 선언유모도 함수 오버로딩의 조건에 해당합니다. 


#pragma warning(disable:4996)

#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 멤버 함수가 추가되었습니다. 위의 함수와 오버로딩의 관계를 가지며, 참조값이 아닌, 배열요소의 값을 단순히 반환하는 형태로 정의되었습니다. 


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

Point 객체의 주소 값을 저장하는 배열 기반의 클래스와 Point 객체를 저장하는 배열 기반의 클래스 입니다. 


#pragma warning(disable:4996)


#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;

}


class BoundCheckPointArray

{

private:

Point * arr;

int arrlen;


BoundCheckPointArray(const BoundCheckPointArray& arr) { }

BoundCheckPointArray& operator = (const BoundCheckPointArray& arr){ }


public:

BoundCheckPointArray(int len) : arrlen(len)

{

arr = new Point[len];

}

Point&  operator[] (int idx)

{

if (idx < 0 || idx >= arrlen)

{

cout << "Array index out of bound exception" << endl;

exit(1);

}

return arr[idx];

}

Point 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; }

~BoundCheckPointArray() { delete[]arr; }

}; 


int main(void)

{

BoundCheckPointArray arr(3);

arr[0] = Point(3, 4);

arr[1] = Point(5, 6);

arr[2] = Point(7, 8);


for (int i = 0; i < arr.GetArrLen(); i++)

cout << arr[i];


return 0;


Point 객체로 이뤄진 배열을 생성하고 있습니다. 인자를 받지 않는 void 생성자의 호출을 통해서 배열요소를 이루는 객체가 생성됨로 정의된 생성자에 설정된 디폴트 값에 의해 객체의 모든 멤버가 0으로 초기화됩니다. 임시객체를 생성해서 배열요소를 초기화하고 있습니다. 물론 초기화의 과정에서 디폴트 대입 연산자가 호출되어 멤버 대 멤버의 복사가 진행됩니다 저장의 대상이 객체라면, 여기서 보이는 것과 같이 대입 연산자를 통해서 객체에 저장된 값을 복사해야 합니다. 


#pragma warning(disable:4996)


#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 BoundCheckPointPtrArray

{

private:

POINT_PTR * arr;

int arrlen;


BoundCheckPointPtrArray(const BoundCheckPointPtrArray& arr) { }

BoundCheckPointPtrArray& operator = (const BoundCheckPointPtrArray& arr){ }


public:

BoundCheckPointPtrArray(int len) : arrlen(len)

{

arr = new POINT_PTR[len];

}

POINT_PTR& operator[] (int idx)

{

if (idx < 0 || idx >= arrlen)

{

cout << "Array index ouf 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; }

~BoundCheckPointPtrArray() { delete[]arr; }

};


int main(void)

{

BoundCheckPointPtrArray 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 포인터 형을 의미하는 Point_ptr 을 정의하였습니다. 저장의 대상, 또는 연산의 주 대상이 포인터의 경우, 이렇듯 별도의 자료형을 정의하는 것이 좋습니다.



그 이외의 연산자 오버로딩


new 와 delete 도 연산자이기 때문에 오버로딩이 가능합니다. 스마트 포인터와 펑터에 대해서도 간단히 설명하도록 하겠습니다.