간단하게 말해서 우리가 사용하는 연산자들 +, -, *, / 에서부터 ++, --, [], <, > ==, =등 다양한 연산자들을 우리가 재정의해서 사용할 수 있게 해주는 방법이라고 생각하면 됩니다.
굳이 이 내용을 알아야하나? 그냥 주는대로 쓰면 되는거 아닌가? 싶지만
간단하게 예를 들면 컴파일러는 우리가 만든 객체의 연산을 이해하지 못 해요..
우리가 객체를 만들고, 그 값들을 사용하는 데 기본적인 연산자를 사용하지 못 하면 너무 불편하지 않을까...?
그래서 우리가 만든 객체들의 연산자들의 정의를 우리가 직접 정의해서 객체끼리의 연산을 편하게 만드는 겁니다.
연산자 오버로딩도 가능한 연산자가 있고 불가능한 연산자들이 있습니다.
이렇게 정해놓은 이유는 기본적인 c++의 문법이 어긋날 수 있기 때문입니다.
연산자 기호 | 이름 |
---|---|
. | 멤버 접근 연산자 |
.* | 멤버 포인터 연산자 |
:: | 범위 지정 연산자 |
?: | 조건 연산자(3항 연산자) |
연산자 기호 | 이름 |
---|---|
= | 대입 연산자 |
( ) | 함수 호출 연산자 |
[ ] | 배열 접근 연산자(인덱스 연산자) |
-> | 멤버 접근을 위한 포인터 연산자 |
연산자 오버로딩을 구현할 때 명심해두어야 할 부분들이 있습니다.
이러한 주의사항들은 기본적인 c++의 문법을 망가뜨리는 행위를 막기 위해 알아두어야 합니다.
연산자 오버로딩의 구현 방법은 전역 함수의 형태이거나, 멤버 함수의 형태가 있는데 우리가 만드는 함수들과 별 차이가 없습니다.
단지 operator연산자의 형태로 함수의 이름이 선언되고 사용됩니다.
우선 Fixed라는 클래스가 있다고 생각하고 연산자들은 다음과 같이 선언되어 사용됩니다.
//비교연산자(멤버함수)
bool operator>(const Fixed &ref) const;
bool operator<(const Fixed &ref) const;
//<<연산자(전역함수)
std::ostream &operator<<(std::ostream &out, const Fixed &ref);
그리고 연산자들은 함수의 형태로 구현되기 때문에 다음과 같이 함수의 호출처럼 변해서 해석하게 됩니다.
피연산자가 하나만 있는 연산자들이 어떤 식으로 확장되어 해석되는지 알아봅시다.
우선은 Fixed라는 객체가 있다고 생각하고 간단하게 증감연산자를 사용해 봅시다.
Fixed fix;
++fix;
이러한 객체가 있는데 이 fix라는 객체를 전위증가를 하면 어떻게 될까?
fix.operator++(); //연산자 오버로딩을 멤버함수로 구현
operator++(fix); //연산자 오버로딩을 전역함수로 구현
이런 식으로 특정 함수의 호출과 같이 확장되어 해석됩니다.
이항 연산자도 단항 연산자와 별 다를 게 없습니다. 단지 인자가 추가될 뿐.
Fixed a;
Fixed b;
a + b;
이런 식으로 두 개의 객체가 서로 덧셈을 한다면?
a.operator+(b); // 멤버함수
operator+(a, b); // 전역변수
이런 식으로 해석됩니다.
class Fixed
{
private:
int _num;
public:
Fixed &operator++(void);
Fixed &operator--(void);
const Fixed operator++(int);
const Fixed operator--(int);
};
다음과 같은 class가 있고, 그 내부에는 증감연산자가 멤버함수의 형태로 선언되어 있습니다.
전위, 후위 연산자의 구분은 매개변수를 보면 알 수 있습니다.
전위 연산자는 (void)의 형태로 되어있고, 후위 연산자는 (int)의 형태로 되어있습니다.
이 것은 매개변수를 받는 게 아니라 단순히 전위, 후위를 구분하기위해 사용됩니다.
구현은 간단하게 _num의 증감으로 구현을 합시다.
Fixed &Fixed::operator++(void)
{
_num++;
return (*this);
}
이런 식으로 간단하게 구현할 수 있는데, 리턴값을 보시면 Fixed의 참조값을 리턴하고 있습니다. 왜 이렇게 할까요?
Fixed a;
++(++a);
이런 식으로 두 번 증가를 한다고 가정을 해 보고 이 내용이 어떤 식으로 확장되는지 알아봅시다.
++(a.operator());
우선 괄호 안에 있는 것 부터 연산을 진행하고 나면 해당 값은 다음과 같이 됩니다.
++(a.operator()의 리턴값);
그리고 그 다음에 해당 리턴값의 증가 연산자를 호출해서 진행하게 됩니다.
리턴값.operator++();
이렇게 진행되는데 만약 참조값으로 리턴이 되지 않는다면 Fixed객체 a와 a.operator()의 리턴값은 별개의 값이 되어버립니다.
그렇기 때문에 우리는 참조의 형태로 Fixed를 리턴해줘야 합니다.
--연산자도 동일하게 구현합니다.
후위 연산자의 구현은 조금 다릅니다.
const Fixed Fixed::operator++(int)
{
const Fixed tmp(*this);
_num++;
return (tmp);
}
_num++는 동일한데, const Fixed객체를 만들어서 해당 객체를 리턴합니다.
이유는 단순히 증가하기 전의 값으로 이후 연산을 진행해야하기 때문입니다.
전위 증감처럼 _num++이후 *this를 리턴하면 이후 연산들은 증가된 값으로 연산이 되기 때문이죠.
그리고 왜 const로 상수화된 Fixed를 리턴할까요??
기본적으로 c++에서는 ++(++a)와 같이 전위 연산의 중첩은 허용하지만, (a++)++와 같이 후위연산의 중첩이 불가능합니다.
그렇기 때문에 const 객체를 리턴해서 더 이상의 후위 연산은 막겠다라는 의미입니다.
class Fixed
{
private:
int _num;
public:
Fixed &operator=(const Fixed &ref);
bool operator>(const Fixed &ref) const;
bool operator<(const Fixed &ref) const;
bool operator>=(const Fixed &ref) const;
bool operator<=(const Fixed &ref) const;
bool operator==(const Fixed &ref) const;
bool operator!=(const Fixed &ref) const;
Fixed operator+(const Fixed &ref) const;
Fixed operator-(const Fixed &ref) const;
Fixed operator*(const Fixed &ref) const;
Fixed operator/(const Fixed &ref) const;
};
이항 연산자들도 단항 연산자와 다른점은 없습니다.
각각 연산자에 따라 현재 객체의 _num과, ref의 _num을 비교하거나, 연산해주면 되는 겁니다.
그리고 함수를 모두 const로 선언해 준 이유는 const 객체끼리의 비교 및 연산도 가능하게 하기 위해서 입니다.
const 객체는 const 멤버함수밖에 사용할 수 없거든요.
Fixed a;
Fixed b;
Fixed c;
const Fixed c_a;
const Fixed c_b;
const Fixed c_c;
...
a < b
c = a + b;
//모두 ok
c_a < c_b;
c_c = c_a + c_b;
a = c_a + b;
//const 함수가 없다면 연산 불가!
우리가 전역으로 연산자 오버로딩을 정의할 수 밖에 없는 상황들이 있습니다.
Fixed fix;
std::cout << fix
와 같은 녀석들이 해당되죠.
<<연산자 오버로딩은 std::cout에 정의되어 있습니다.
이 cout에 우리가 만든 객체 Fixed를 출력하기 위해서 멤버함수로 정의를 하고싶다면 우리는 cout을 뜯어고쳐야 하는 수밖에 없습니다.
하지만 그건 불가능하죠... 그렇기 때문에 전역함수로 선언해서 사용하게 됩니다.
정의는 다음과 같습니다.
std::ostream &operator<<(std::ostream &out, const Fixed &ref)
{
out << ref.getNum(); //getNum을 통해서 _num에 접근
return (out);
}
연산자 오버로딩의 해석에서 말 했듯 전역 함수의 형태는 std::cout << fix
는
operator(cout, fix)
와 같은 형태로 바뀐다고 했었죠?
그래서 우리는 매개변수로 받은 out을 통해서 표준출력에 접근할 수 있게 됩니다.
그렇게 fix를 출력하고, 해당 out을 리턴해서 std::cout << fix1 << fix2 << fix3
와 같이 지속적인 출력도 가능하게 할 수 있는 거죠.
우리가 계산을 할 땐 교환법칙이라는게 있죠? 연산자 오버로딩에서도 전역함수를 통해서 그 부분을 구현할 수 있습니다.
만약 다음과 같은 코드가 있다고 생각해 봅시다.
Fixed fix;
int num;
...
cout << fix + num << endl;
이렇게 코드가 있다면 각각 a와 b의 값이 초기화 되고 fix와 num을 더한 값이 출력이 될 겁니다.
하지만 출력문이 다음과 같다면?
cout << num + fix << endl;
int타입은 Fixed타입을 더할 수 있는 함수가 없습니다.
num.operator+(fix)
가 불가능하다는 말이죠.
그렇기 때문에 이 부분을 해결하기 위해서 우리는 전역함수를 통해서 이 부분을 해결해야 합니다.
Fixed operator+(int num, const Fixed &ref);
이런 식으로 만든다면 우리는 num + fix
도 fix + num
도 동일한 값을 낼 수 있도록 만들 수 있습니다.
참고
책 : 윤성우 열혈 c++ 프로그래밍
[ c++ ] namespace
[ c++ ] 클래스, 생성자, 소멸자, 이니셜라이저, this포인터
[ c++ ] c++에서의 const와 static
[ c++ ] 참조자(reference)
[ c++ ] new와 delete
[ c++ ] 함수 오버로딩
[ c++ ] 파일 입출력 (ifstream, ofstream)
[ c++ ] 함수포인터, 멤버 함수포인터
[ c++ ]연산자 오버로딩
[ c++ ] 캡슐화란?
[ c++ ] 상속과 다형성에 대해 알아보자