증가, 감소 연산자의 오버로딩
대표적인 단항 연산자로든 다음 두가지가 있다.
- 1 증가 연산자 ++
- 1 감소 연산자 --
만약 Point 라는 클래스에 ++연산자가 오버로딩 되어있다고 가정해보자.
++pos; //pos는 Point 의 객체
멤버함수의 형태로 오버로딩 된 경우에는 다음과 같이 해석된다.
pos.operator++();
단항 연산자를 오버로딩 했기 때문에 전달할 인자는 없다.
전역함수의 형태로 오버로딩 된 경우에는 다음과 같이 해석된다.
operator++(pos);
전역함수의 경우는 피연산자가 모두 인자로 전달된다.
#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);//전역함수에 대해 friend 선언
};
Point& operator--(Point& ref) {//전역함수 형태의 오버로딩
ref.xpos -= 1;
ref.ypos -= 1;
return ref;
}
int main(void) {
Point pos(1, 2);
++pos;//pos.operator++();
pos.ShowPosition();
--pos;//operator--(pos);
pos.ShowPosition();
++(++pos);
pos.ShowPosition();
--(--pos);
pos.ShowPosition();
return 0;
}
① ++(++pos);
이 문장에서는 먼저 소괄호 부분이 다음의 형태로 해석되어서 실행된다.
②++(pos.operator++());
실행의 결과로 pos 객체의 멤버변수 값은 1씩 증가하고 pos의 참조값이 반환된다.
③++(pos의 참조 값);
이 문장은 다음과 같이 이어서 해석이 된다.
④(pos의 참조 값).operator++();
pos의 참조 값을 대상으로 하는 연산은 pos 객체를 대상으로 하는 연산이기 때문에
결과적으로 위의 문장이 실행되면서 pos 객체의 멤버변수 값은 다시 1씩 증가한다.
즉, ++(++pos); 이 문장으로 인해서 pos의 멤버변수에 저장된 값은 각각 2씩 증가한다.
operator++함수에서 객체 자신을 참조할 수 있는 참조 값을 반환하는 이유는
일반적인 ++연산자와 마찬가지로 위와 같은 형태의 연산이 가능하게 하기 위함이다.
① --(--pos);
②--(operator--(pos));
operator--연산자는 인자로 전달된 pos 객체를 참조가 ref로 받아서, 이를 그대로 참조형으로 다시 반환을 한다.
③ --(pos의 참조 값);
④operator--(pos의 참조 값);
마찬가지로 pos의 참조값을 대상으로 하는 --연산은 pos 객체를 대상으로 하는 연산이기 때문에,
결과적으로 위의 문장이 실행되면서 pos 객체에 저장된 멤버 변수의 값은 다시 1씩 감소한다.
즉. --(--pos); 이 문장이 실행되면서 pos의 멤버변수에 저장된 값은 각각 2씩 감소한다.
이렇듯, 위 예제의 operator--함수도 참조 값을 반환함으로써, 일반적인 --연산자와 마찬가지로 예제와 같은 형태로 연산이 가능하게 했다.
전위증가와 후위증가의 구분
후위증가, 후위감소 연산에 대한 연산자 오버로딩은 어떻게 해야 할까?
C++에서는 전위 및 후위 연산에 대한 해석 방식에 대해 다음의 규칙을 정해놓고 있다.
- ++pos → pos.operator++();
- pos++ → pos.operator++(int);
- --pos → pos.operator--();
- pos-- → pos.operator--(int);
즉, 키워드 int를 이용해서 후위연산과 전위연산에 대한 함수를 구분하고 있다.
여기서 int는 int형 데이터를 인자로 전달하라는 뜻과 아무 상관이 없다.
#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;
}
const Point operator++(int)//후위증가
{
const Point retobj(xpos, ypos);//const Point retobj(*this);
//반환에 사용할 복사본
xpos += 1;
ypos += 1;
return retobj;//멤버의 값이 증가하기 이전에 만들어둔 복사본을 반환
//이를 통해 후위증가의 효과를 낸다.
}
friend Point& operator--(Point& ref);//전역함수에 대해 friend 선언
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.ShowPosition();
pos.ShowPosition();
cpy = pos++;
cpy.ShowPosition();
pos.ShowPosition();
return 0;
}
반환형에서의 const선언과 const 객체
위의 예제에서
- const Point retobj(xpos, ypos);
- const Point retobj(ref);
이 두 문장에서는 후위증가 연산자와 후위 감소 연산자를 오버로딩 한 함수들인데, 반환형이 const로 선언되어있다.
retobj객체가 반환되면, 반환의 과정에서 새로운 객체가 생성되기 때문에,
retobj객체의 const 선언유무는 retobj 객체의 반환에 아무런 영향을 미치지 않는다.
그렇다면 반환형이 const로 선언된 이유는 무엇일까?
위의 두 문장은
"operator--함수의 반환으로 인해서 생성되는 임시 객체를 const객체로 생성하겠다."라는 의미를 갖고있다.
이는 pos 객체를 상수화해서 pos 객체에 저장된 값의 변경을 허용하지 않겠다는 뜻이다.
그래서 const객체인 pos를 대상으로는 const로 선언된 함수만 호출이 가능하다.
이러한 const 객체를 대상으로 참조자를 선언할 때에는 참조자도 const로 선언해야 된다.
그래야 참조자를 통한 pos 객체의 변경을 허용하지 않을 수 있기 때문이다...
마무리
const Point operator++(int)//후위증가
{
const Point retobj(xpos, ypos);//const Point retobj(*this);
//반환에 사용할 복사본
xpos += 1;
ypos += 1;
return retobj;//멤버의 값이 증가하기 이전에 만들어둔 복사본을 반환
//이를 통해 후위증가의 효과를 낸다.
}
const Point operator--(Point& ref, int) {//후위감소
const Point retobj(ref);
ref.xpos -= 1;
ref.ypos -= 1;
return retobj;
}
위의 두 함수는 객체를 반환한다. 그리고 그 과정에서 생성되는 임시객체는 반환형의 const 선언으로 인해서
값의 변경을 허용하지 않는 const(상수 객체)가 된다.
따라서 이 객체를 대상으로는 const로 선언되지 않은 멤버함수의 호출이 불가능하다.
때문에 반환형의 const 선언으로 인해서 다음의 문장 구성은 불가능하다.
int main(void){
Point pos(3,5);
(pos++)++; //컴파일 에러
(pos--)--; //컴파일 에러
}
pos++연산과 pos--연산으로 인해서 반환되는 것은 const 객체이므로 다음 두 문장의 1차 실행 결과는 다음과 같다.
- (pos++)++; → (Point형 const 임시 객체)++;
- (pos--)--; → (Point형 const 임시 객체)--;
그리고 이 두 문장은 각각 다음과 같이 해석된다.
- (Point형 const 임시객체).operator++(); //operator++(int)의 호출
- operator--(Point형 const 임시객체); //operator--(Point &ref, int)의 호출
여기서 operator++멤버 함수는 const로 선언된 함수가 아니기 때문에 const 임시객체를 대상으로는 호출이 불가능해서 컴파일 에러가 발생한다.
operator-- 전역함수는 매개변수로 참조자가 선언되었는데, 이 참조자가 const 로 선언되지 않았기 때문에 컴파일 에러가 발생한다.
후위 증가 및 후위 감소 연산에 대해서 오버로딩 한 함수의 반환형을 const로 선언한 이유는 위의 두 문장에
컴파일 에러를 일으키기 위함이다.
이는 다음의 연산의 특성을 그대로 반영한 결과이다.
int main(void){
int num = 100;
++(++num); //컴파일 OK
--(--num); //컴파일 OK
}
int main(void){
int num = 100;
(num++)++; //컴파일 OK
(num--)--; //컴파일 OK
}
'공부 > C++' 카테고리의 다른 글
[C++] 10-4. cout, cin 그리고 endl (0) | 2024.11.27 |
---|---|
[C++] 10-3. 교환법칙 문제의 해결 (0) | 2024.11.27 |
[C++] 10-1. 연산자 오버로딩의 이해와 유형 (0) | 2024.11.25 |
[C++] 9-2. 다중상속(Multiple Inheritance)에 대한 이해 (0) | 2024.11.22 |
[C++] 9-1. 멤버 함수와 가상 함수의 동작 원리 (0) | 2024.11.21 |