본문 바로가기
Dairy

연산자 오버로딩 1

by OKOK 2017. 8. 1.

C++ 에서는 함수뿐만 아니라 연산자도 오버로딩이 가능합니다. 함수가 오버로딩 되면, 오버로딩 된  수만큼 다양한 기능을 제공하게 됩니다. 즉, 이름은 하나이지만 기능은 여러가지가 되는 셈입니다. 


Operator+ 라는 이름의 함수


#include <iostream>

using namespace std;


class Point

{

private:

int xpos, ypos;

public:

Point(int x=0, int y=0) : xpos(x), ypos(y)

{ }

void ShowPosition() const

{

cout << '[' << xpos << ", " << ypos << ']' << endl;

}

Point operator+(const Point &ref)

{

Point pos(xpos + ref.xpos, ypos + ref.ypos);

return pos;

}

};


int main(void)

{

Point pos1(3, 4);

Point pos2(10, 20);

Point pos3 = pos1.operator+(pos2);

pos1.ShowPosition();

pos2.ShowPosition();

pos3.ShowPosition();

return 0;

}


함수의 이름이 operator+ 입니다. 조금 독특하다고 생각해도 좋습니다. 이름의 형태가 조금 어색하지만 분명히 이는 함수의 이름입니다. pos1 객체의 멤버함수 operator+를 호출하면서 인자로 pos2 객체를 전달하고 있습니다. 따라서 이 두객체의 멤버 별 덧셈결과로 새로운 Point 객체가 만들어지고 이것이 반환되어 pos3을 초기화하게 됩니다. (이 과정에서 복사 생성자가 호출됩니다.)


#include <iostream>

using namespace std;


class Point

{

private:

int xpos, ypos;

public:

Point(int x=0, int y=0) : xpos(x), ypos(y)

{ }

void ShowPosition() const

{

cout << '[' << xpos << ", " << ypos << ']' << endl;

}

Point operator+(const Point &ref)

{

Point pos(xpos + ref.xpos, ypos + ref.ypos);

return pos;

}

};


int main(void)

{

Point pos1(3, 4);

Point pos2(10, 20);

Point pos3 = pos1 + pos2;


pos1.ShowPosition();

pos2.ShowPosition();

pos3.ShowPosition();

return 0;


pos1+pos2가 pos1.operator+(pos2)의 다른 표현이 되기 위해서 약속된 변환의 규칙이 있을 것 같은데. 중요한 것은 객체를 가지고 덧셈연산을 했다는 것입니다. 객체도 기본 자료형 변수처럼 덧셈, 뺄셈, 곱셈과 같은 연산들을 가능하게 하려고 합니다. 객체도 완변히 기본 자료형 데이터처럼 취급할 수 있습니다. operator 키워드와 연산자를 묶어서 함수의 이름을 정의하면, 함수의 이름을 이용한 함수의 호출뿐만 아니라, 연산자를 이용한 함수의 호출도 허용해 주겠다는 것입니다. 객체를 피연산자로 사용한 연산자의 이름 앞에 operator 라는 이름을 붙여서 완성되는 이름의 함수를 호출하겠다는 뜻입니다. 


연산자를 오버로딩 한 함수도 const로 선언이 가능합니다. 혼란을 최소화하기 위해서 ooperator+ 함수는 const 선언하지 않았습니다. 그러나 이 함수도 const로 선언하는 것이 좋습니다. 덧셈 연산이라는 것이 원래 연산의 대상이 되는 피연산자의 값을 변경하는 것이 아니고, 새로운 연산의 결과를 만들어내느 것이기 때문입니다. 


연산자를 오버로딩 하는 두 가지 방법

연산자를 오버로딩 하는 방법에는 다음 두 가지가 있습니다. 멤버함수에 의한 연산자 오버로딩과 전역함수에 의한 연산자 오버로딩이 있습니다. 


#pragma warning(disable:4996)


#include<iostream>

using namespace std;


class Point

{

private:

int xpos, ypos;

public:

Point(int x=0, int y=0) : xpos(x), ypos(y)

{ }

void ShowPosition() const

{

cout << '[' << xpos << ", " << ypos << ']' << endl;

}

friend Point operator+(const Point &pos1, const Point &pos2);

};


Point operator+(const Point &pos1, const Point &pos2)

{

Point pos(pos1.xpos + pos2.xpos, pos1.ypos + pos2.ypos);

return pos;

}


int main(void)

{

Point pos1(3, 4);

Point pos2(10, 20);

Point pos3 = pos1 + pos2;


pos1.ShowPosition();

pos2.ShowPosition();

pos3.ShowPosition();

return 0;


전역함수 기반의 연사자 오버로딩에 대한 일반적인 모델을 보여주고 있습니다. 특히 friend 선언이 적절히 사용된 예를 보이고 있습니다. friend 선언을 보면서 operator+ 함수 내에서는 Point 클래스의 private 영역에 접근이 가능하겠다는 생각이 듭니다. Point 클래스는 + 연산에 대해서 연산자 오버로딩이 되어 있습니다. 멤버함수를 기반으로 연산자를 오버로딩 하는 게 낫습니다. 


오버로딩이 불가능한 연산자의 종류

대입 연산자, 함수 호출 연산자, 배열 접근 연산자(인덱스 연산자), 멤버 접근을 위한 포인터 연산자. 객체를 대상으로 진행해야 의미가 통하는 연산들이기 때문에, 멤버함수 기반으로만 연산자의 오버로딩을 허용합니다. 


연산자를 오버로딩 하는데 있어서의 주의사항

연산자 오버로딩은 잘못 사용하면 프로그램을 복잡하고, 이해하기 어렵게 만든다는 단점이 있습니다. 연산자의 우선순위와 결합성은 바뀌지 않습니다. 매개변수의 디폴트 값 설정이 불가능합니다. 피연산자의 자료형에 따라서 연산자를 오버로딩 한 함수의 호출이 결정되는데, 매개변수의 디폴트 값을 설정한다는 것이 말이 되겠는가? 연산자 오버로딩 특성상 매개변수의 디폴트 값이 설정되면, 함수의 호출관계가 매우 불분명해집니다. 따라서 매개변수의 디폴트 값 설정은 허용되지 않습니다. 


연산자의 순수 기능까지 빼앗을 수 없습니다. 연사자가 오버로딩 되면, 피연산자의 종류에 따라서 연산의 방식이 달라집니다. 그래서 연산자 오버로딩이라 불리는 것입니다.


단항 연산자의 오버로딩

피연산자가 두 개인 이항 연산자와 피연산자가 한 개인 단항 연산자의 가장 큰 차이점은 피연산자의 개수입니다. 그리고 이에 따른 연산자 오버로딩의 차이점은 매개변수의 개수에서 발견됩니다. 


증가, 감소 연산자의 오버로딩

호출되는 함수의 이름은 ++연산자와 키워드 operator를 연결해서 완성되므로, operator++ 입니다. 멤버함수의 형태로 오버로딩 된 경우의 해석방법을 고민해봅니다. 멤버 함수의 형태라면, pos의 멤버함수가 호출되는 형태이니, pos.operator++(); 로 해석되어야 합니다. 전역함수의 경우는 피연산자가 모두 인자로 전달되므로, 위의 형태로 해석이 됩니다. 


#include <iostream>

using namespace std;


class Point

{

private:

int xpos, ypos;

public:

Point(int x=0, int y=0) : xpos(x), ypos(y)

{ }

void ShowPosition() const

{

cout << '[' << xpos << ", " << ypos << ']' << endl;

}

Point& operator++()

{

xpos += 1;

ypos += 1;

return *this;

}

friend Point& operator--(Point &ref);

};


Point& operator--(Point &ref)

{

ref.xpos -= 1;

ref.ypos -= 1;

return ref;

}


int main(void)

{

Point pos(1, 2);

++pos;

pos.ShowPosition();

--pos;

pos.ShowPosition();


++(++pos);

pos.ShowPosition();

--(--pos);

pos.ShowPosition();

return 0;


++ 연산자가 멤버함수의 형태로 오버로딩 되었습니다. this는 객체자신의 포인터 값을 의미하므로, *this는 객체자신을 의미합니다. 즉, 위의 함수에서는 객체자신을 반환합니다. 그런데 반환형이 참조형으로 선언되었습니다. 따라서 위 함수의 호출결과로 객체 자신을 참조할 수 있는 참조 값이 반환됩니다. 


#pragma warning(disable:4996)

#include <iostream>

using namespace std;


class Point

{

private:

int xpos, ypos;

public:

Point(int x=0, int y=0) : xpos(x), ypos(y)

{ }

void ShowPoisition() const

{

cout << '[' << xpos << ", " << ypos << ']'<< endl;

}

Point& operator++()

{

xpos += 1;

return *this;

}

const Point operator++(int)

{


const Point retobj(xpos, ypos);

xpos += 1;

ypos += 1;

return retobj;

}

friend Point& operator--(Point &ref);

friend const Point operator--(Point &ref, int);

};


Point& operator--(Point &ref)

{

ref.xpos -= 1;

ref.ypos -= 1;

return ref;

}


const Point operator--(Point &ref, int)

{

const Point retobj(ref);

ref.xpos -= 1;

ref.ypos -= 1;

return retobj;

}


int main(void)

{

Point pos(3, 5);

Point cpy;

cpy = pos--;

cpy.ShowPoisition();

pos.ShowPoisition();


cpy = pos++;

cpy.ShowPoisition();

pos.ShowPoisition();

return 0;

}


 


후의증가 연산자와 후위감소 연산자를 오버로딩 한 함수들인데, 반환형이 const로 선언되었습니다. 반환의 대상이 되는 retobj 객체가 const로 선언되어 있어서 그런 것 아니다. operator-- 함수의 반환으로 인해서 생성되는 임시객체를 const 객체로 생성하겠습니다. const 함수 내에서는 const 함수의 호출만 허용하도록 제한합니다. 


교환법칙 문제의 해결

교환법칙이란 A+B의 결과는 B+A의 결과와 같음을 뜻합니다. 즉, 연산자를 중심으로 한 피연산자의 위치는 연산의 결과에 아무런 영향을 미치지 않는다는 법칙입니다. 


자료형이 다른 두 피연산자를 대상으로 하는 연산

#include <iostream>

using namespace std;


class Point

{

private:

int xpos, ypos;

public:

Point(int x=0, int y=0) : xpos(x), ypos(y)

{ }

void SHowPosition() const

{

cout << '[' << xpos << ", " << ypos << ']' << endl;

}

Point operator*(int times)

{

Point pos(xpos*times, ypos*times);

return pos;

}

};


int main(void)

{

Point pos(1, 2);

Point cpy;


cpy = pos * 3;

cpy.SHowPosition();


cpy = pos * 3 * 2;

cpy.SHowPosition();

return 0;


곱셈 연산자를 오버로딩 하였습니다. 따라서 Point 객체와 정수간의 곱셈이 가능하게 되었습니다. 오버로딩 한 곱셈 연산자의 경우, Point 객체가 * 연산자의 왼편에 와야 합니다. 곱셈연산은 교환법칙이 성립합니다. 멤버함수의 형태로 오버로딩이 되면, 메버함수가 정의된 클래스의 객체가 오버로딩 된 연산자의 왼편에 와야 하기 때문입니다. 


#pragma warning(disable:4996)

#include <iostream>


using namespace std;

class Point

{

private:

int xpos, ypos;

public:

Point(int x=0, int y=0) : xpos(x), ypos(y)

{ }

void ShowPosition() const

{

cout << '[' << xpos << ", " << ypos << ']'<< endl;

}

Point operator*(int times)

{

Point pos(xpos*times, ypos*times);

return pos;

}

friend Point operator*(int times, Point& ref);

};


Point operator*(int times, Point& ref)

{

return ref*times;

}


int main(void)

{

Point pos(1, 2);

Point cpy;


cpy = 3 * pos;

cpy.ShowPosition();


cpy = 2 * pos * 3;

cpy.ShowPosition();

return 0;


교환법치의 성립을 목적으로 함수를 추가로 정의하였습니다. 교환법칙이 성립됨을 확인 합니다. 자연스러운 연산문의 구성이 가능합니다. 전역함수를 기반으로 연산자를 오버로딩 해야 하는 경우도 있으니, 전역함수 기반의 연산자 오버로딩에도 익숙해지기 바랍니다.


cout, cin 그리고 endl의 정체


#include <iostream>

namespace mystd

{

using namespace std;

class ostream

{

public:

void operator<< (char * str)

{

printf("%s", str);

}

void operator<< (char str)

{

printf("%c", str);

}

void operator<< (int num)

{

printf("%d", num);

}

void operator<< (double e)

{

printf("%g", e);

}

void operator<< (ostream& (*fp)(ostream &ostm))

{

fp(*this);

}

};


ostream& endl(ostream &ostm)

{

ostm << '\n';

fflush(stdout);

return ostm;

}


ostream cout;

}


int main(void)

{

using mystd::cout;

using mystd::endl;


cout << "Simple Strig";

cout << endl;

cout << 3.14;

cout << endl;

cout << 123;

endl(cout);

return 0;


cout 과 endl 을 직접 구현하기 위해서 선언한 이름공간입니다. using 선언은 이름공간 mystd 내에서 지역적으로 이뤄졌으니, 이 지역 내에서만 유요한 선언이 됩니다. 이 선언은 C언어의 표준함수인 printf 함수의 호출을 위해서 삽입되었습니다. cout은 객체의 이름이며, 이 객체 내애서는 다양한 기본 자료형 데이터르 대상으로 << 연산자를 오버로딩 하고 있습니다. 함수 포인터 부분을 참고하기 바랍니다. endl은 이렇듯 함수의 형태를 띕니다. 버퍼를 비우는 작업까지 함께 진행하고 있습니다. 이름 공간 mystd 내에 선언된 cout과 endl의 사용을 위해서 지역적으로 using 선언을 하였습니다. 따라서 main 함수 내에서의 cout과 endl은 이름공간 mystd 내에 선언 된 것을 의미하게 됩니다. 


#pragma warning(disable:4996)


#include <iostream>


namespace mystd

{

using namespace std;


class ostream

{

public:

ostream& operator<< (char * str)

{

printf("%s", str);

return *this;

}

ostream& operator<< (char str)

{

printf("%c", str);

return *this;

}

ostream& operator<< (int num)

{

printf("%d", num);

return *this;

}

ostream& operator<< (double e)

{

printf("%g", e);

return *this;

}

ostream& operator<< (ostream& (*fp)(ostream &ostm))

{

return fp(*this);

}

};


ostream& endl(ostream &ostm)

{

ostm << '\n';

fflush(stdout);

return ostm;

}

ostream cout;

}


int main(void)

{

using mystd::cout;

using mystd::endl;

cout << 3.14 << endl << 123 << endl;

return 0;


cout 객체의 참조값을 반환하는 형태로 확장하였습니다. endl 함수는 인자로 전달된 객체의 참조값을 반환하므로, 반환된 값을 재 반환하는 형태로 연산자를 오버로딩 하였습니다. 


<< , >> 연산자의 오버로딩

cout 은 ostream 클래스의 객체입니다. ostream은 이름공간 std 안에 선언되어 있으며, 이의 사용을 위해서는 헤더파일 <iostream>을 포함해야 합니다. 멤버함수에 의한 방법을 선태갛려면 cout 객체의 멤버함수를 하나 추개향 하므로, ostream 클래스를 정정해야 합니다. 그런데 이는 불가능한 방법이니, 전역함수에 의한 방법을 택하는 수밖에 없습니다.


#pragma warning(disable:4996)


#include<iostream>

using namespace std;


class Point

{

private:

int xpos, ypos;

public:

Point(int x=0, int y=0) : xpos(x), ypos(y)

{ }

void ShowPosition() const

{

cout << '[' << xpos << ", " << ypos << ']' << endl;

}

friend ostream& operator<<(ostream&, const Point&);

};


ostream& operator<<(ostream& os, const Point&pos)

{

os << '[' << pos.xpos << ", " << pos.ypos << ']' << endl;

return os;

}


int main(void)

{

Point pos1(1, 3);

cout << pos1;

Point pos2(101, 303);

cout << pos2;

return 0;


인자로 전달된 cout의 참조자를 통한 출력을 구성하였습니다.