본문 바로가기

Study/C++

[C++] Operator Overloading

연산자 오버로딩

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 형변환 연산자
멤버함수 기반으로만 오버로딩이 가능한 연산자
= 대입 연산자
( ) 함수 호출 연산자
[ ] 배열 접근 연산자
-> 멤버 접근을 위한 포인터 연산자

  • 연산자 오버로딩은 다음과 같은 주의사항을 가진다
    1. 본래의 의도를 지킬것
      • +면 더하기, -면 빼기와 같이 용도에 맞게 쓰자
    2. 연산자의 우선순위와 결합성은 불변
    3. 매개변수의 디폴트 값 설정은 불가
    4. 연산자의 순수 기능은 보장

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

'Study > C++' 카테고리의 다른 글

[C++] Template  (0) 2023.10.15
[C++] Operator Overloading part.2  (1) 2023.10.15
[C++] Virtual principle and Multiple Inheritance  (1) 2023.10.10
[C++] Polymorphism  (0) 2023.10.09
[C++] Inheritance  (0) 2023.10.09