연산자 오버로딩
1. 연산자 오버로딩의 이해
- 함수의 오버로딩은 오버로딩 된 수만큼 다양한 기능을 사용할 수 있다. 연산자 오버로딩도 이와 같다. 기존에 존재하던 연산자의 기본 기능 이외에 다른 기능을 추가할 수 있다.
다음의 코드를 보자.
#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;
}
void operator+(const Point &ref)
{
Point pos(xpos+ref.xpos, ypos+ref.ypos);
return pos;
}
};
- 위 클래스를 기반으로 다음과 같은 두 개의 객체를 생성해보자.
Point pos1(1,2);
Point pos2(10,20);
Point pos3 = pos1.operator+(pos2);
Point pos4 = pos1 + pos2;
- 기본적으로, pos3와 pos4의 출력은 동일할 것이다.
다만, 두 가지의 표현이 서로 다른 방식으로 컴파일러가 해석한다는 사실은 쉽게 유추가 가능할 것이다.
- 연산자 오버로딩은 일종의 약속이다. 쉽게 이야기 하자면, 앞선 pos3와 pos4는 완벽하게 같은 문장이며, 컴파일러도 동일하게 해석한다. 다만, pos4의 경우를 pos3의 경우로 컴파일러가 자체적으로 해석한단느 것이다.
- 이러한 이유를 통해, 함수의 이름을 통한 연산자의 호출을 가능하게 하기 위해, 제공되는 기능이다.
- 연산자 오버로디을 하는 두 가지 방법은 다음과 같다.
- 멤버함수에 의한 연산자 오버로딩
- 전역함수에 의한 연산자 오버로딩
pos1.operator+(pos2);
operator+(pos1, pos2);
- 위 코드는 각각 멤버함수에 의한 호출과 전역변수에 의한 호출을 나타낸다.
사실, 객체지향에서는 '전역(global)'에 대한 개념을 지양한다.
- 만일, 전역변수를 통해 연산자 오버로딩을 하고 싶다면, 멤버함수를 friend로 선언한 뒤, 위에 나온 호출대로 함수의 매개변수를 수정해서 정의해야 할 것이다.
- 서론이 길었지만, 결론적으로 이야기 하자면, C++에서 연산자 오버로딩은 멤버함수의 의한 연산자 오버로딩 방법을 굳이 쓰지 않아도, 연산자를 통한 객체의 연산이 가능하다는 것이다. 즉, pos3처럼 써야 하지만, pos4와 같은 방법이 가능한 것이다.
- 이러한 방식의 장점은 직관적이라는 것이다.
오버로딩이 불가능한 연산자 |
. |
멤버 접근 연산자 |
.* |
멤버 포인터 연산자 |
:: |
범위 지정 연산자 |
?: |
3항 연산자 |
sizeof |
바이트 단위 크기 계산 |
typeid |
RTTI 관련 연산자 |
static_cast |
형변환 연산자 |
dynamic_cast |
형변환 연산자 |
const_cast |
형변환 연산자 |
reinterpret_cast |
형변환 연산자 |
멤버함수 기반으로만 오버로딩이 가능한 연산자 |
= |
대입 연산자 |
( ) |
함수 호출 연산자 |
[ ] |
배열 접근 연산자 |
-> |
멤버 접근을 위한 포인터 연산자 |
- 연산자 오버로딩은 다음과 같은 주의사항을 가진다
- 본래의 의도를 지킬것
- +면 더하기, -면 빼기와 같이 용도에 맞게 쓰자
- 연산자의 우선순위와 결합성은 불변
- 매개변수의 디폴트 값 설정은 불가
- 연산자의 순수 기능은 보장
2. 단항 연산자의 오버로딩
- 대표적인 단항 연산자인 ++, --의 경우에도 연산자 오버로딩이 가능하다.
pos.operator++();
pos.operator--();
- 위와 같은 경우는 후위연산의 경우이다. 만일, 전위의 경우로 진행하고자 한다면 다음과 같다.
++pos -> pos.operator++();
pos++ -> pos.operator++(int);
--pos -> pos.operator--();
pos-- -> pos.operator--(int);
- 전위연산과 후위연산에서 const를 이용한 재미있는 연산이 존재한다. 다음 코드를 보면
cosnt Point operator++(int)
{
const Point retobj(xpos, ypos); // const Point retobj(*this);
xpos+=1;
ypos+=1;
return retobj;
}
cosnt Point operator--(int)
{
const Point retobj(xpos, ypos); // const Point retobj(*this);
xpos-=1;
ypos-=1;
return retobj;
}
- 위와 같이 연산자가 오버로딩되어 존재할 때, 다음과 같은 코드는 컴파일에러가 발생한다.
(pos++)++;
(pos--)--;
- 이는 pos를 참조하여 연산자 오버로딩을 하는 과정에서, operator++/--로 접근하면 먼저 const retobj객체가 생성된다. cosnt 객체는 여러차례 공부했듯 값의 변경이 불가하다.
3. 교환법칙 문제
#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;
}
};
- 위와 같은 포인터가 있을때, 오버로딩 한 곱셈 연산은 다음과 같은 형식을 갖추어야 한다.
tmp = pos * 3;
- 상식적인 수학지식을 따르면 다음과 같은 코드도 갖추어야 한다.
tmp = 3 * pos;
- 하지만, 앞선 코드의 오버로딩 형태로는 위 연산이 불가능하다. 따라서 교환법칙 성립을 위해서는 별도의 연산이 요구된다. 따라서 다음과 같은 함수를 추가하여 오버로딩 되게 하면, 교환법칙이 성립된다.
Point operator*(int times, Point &ref)
{
Point pos(ref.xpos*times, ref.ypos*times);
return pos;
}
4. cout, cin and endl
- cout과 cin 그리고 endl의 경우도 결국 연산자 오버로딩을 통해 구현이 가능하다. 예는 다음과 같다.
class ostream
{
public:
void operator<< (char * str)
{
printf("%s", str);
}
void operator<< (char str)
{
printf("%c", str);
}
void operator<< (int str)
{
printf("%d", str);
}
void operator<< (double str)
{
printf("%g", str);
}
void operator<< (ostream& (*fp)(ostream &ostm))
{
fp(*this);
}
ostream& endl(ostream &ostm)
{
ostm << '\n';
fflush(stdout);
return ostm;
}
- 위와 유사한 과정을 거치게 되는데, 이러한 방법은 cout 이후에 연속되는 << 연산을 보조할 수 없다. 따라서 다음과 같이 수정하면 더 나은 결과를 낼 수 있다.
class ostream
{
public:
void operator<< (char * str)
{
printf("%s", str);
return *this;
}
void operator<< (char str)
{
printf("%c", str);
return *this;
}
void operator<< (int str)
{
printf("%d", str);
return *this;
}
void operator<< (double str)
{
printf("%g", str);
return *this;
}
void operator<< (ostream& (*fp)(ostream &ostm))
{
fp(*this);
}
ostream& endl(ostream &ostm)
{
ostm << '\n';
fflush(stdout);
return ostm;
}
- 즉, <<와 >> 연산자를 오버로딩하면 연산 혹은 출력이 가능해진다. 이러한 연산자를 공부하기 전, 다음과 같은 사실을 숙지할 필요가 있다.
먼저 <<을 살펴보자.
- cout은 osteram 클래스 객체이다.
- ostream은 namespace std에 선언되어 있고, 이의 사용을 위해서는 <iosteram>의 헤더파일이 요구된다.
- 헤더파일이 이미 존재하고 있으므로, 일종의 데코레이터로 구현해야한다. 즉, 멤버함수의 형태로 구현할 수 없고, 전역함수에 의한 방법으로 구현할 필요가 있다.
이는 >>도 마찬가지이다.
- cin은 isteram 클래스 객체이다.
- istream은 namespace std에 선언되어 있고, 이의 사용을 위해서는 <iosteram>의 헤더파일이 요구된다.
ostream& operator<<(ostream& os, const Point& pos)
{
os << '[' << pos.xpos << ', ' << pos.ypos << ']' << endl;
return os;
}
ostream& operator>>(istream& is, const Point& pos)
{
is >> pos.xpos >> pos.ypos;
return is;
}