"오버로딩 - 1. 함수, 생성자"에 이어 "연산자 오버로딩"을 배워보자.
연산자 오버로딩
- C++에서는 사용자 정의 연산자를 사용할 수 있다.
- 기본 연산자를 여러 의미로 사용할 수 있다. (새로운 연산자를 정의하는 것은 불가능)
연산자 오버로딩 정의
- 연산자 오버로딩은 다음과 같이 정의한다.
(반환타입) operator(연산자)(연산자가 받는 인자){
/*실행구문*/
return 반환값
}
//example
string operator+(string str1, string str2){
return str1.insert(str1.length(), str2);
}
연산자 오버로딩 정의 방법
- 연산자 오버로딩 정의 방법은 2가지가 있다.
- 1. 클래스의 멤버 함수로 정의 ▶ 왼쪽의 피연산자 객체가 함수를 호출하는 경우에 사용
- 2. 전역 함수로 정의 ▶ 매개변수 순서, 개수, 타입에 신경쓰지 않고 연산자 오버로딩을 하고 싶을 때 사용
// 클래스의 멤버 함수로 정의 예
class Ex {
private:
int x, y;
public:
Ex operator+(const Ex& ex);
}
Ex::Ex operator+(const Ex& ex){
~~~~;
return ~~;
}
// 전역 함수로 정의 (클래스 public에 구현하지 않음)
class Ex {
private:
int x, y;
}
Ex operator+(const Ex& ex1, const Ex& ex2){
~~~~;
return ~~;
}
연산자 오버로딩 제약 사항
- C++에서 연산자를 오버로딩하려면 다음의 사항을 지켜야 한다.
1. 새로운 연산자를 정의하는 것은 불가능하다. 기존 연산자에 추가적인 기능을 더하는 것만 가능하다.
2. 기본 타입을 다루는 연산자의 의미는 재정의할 수 없다. 따라서 오버로딩된 연산자의 피연산자 중 하나는 반드시 사용자 정의 타입이어야 한다. ▶ int 형 2개에 대한 덧셈 연산자(+)의 기능을 뺄셈이 되도록 오버로딩할 수 없음.
3. 오버로딩된 연산자는 기본 타입을 다룰 때 적용되는 피연산자의 수, 우선순위 및 그룹화를 준수해야 한다. ▶ *, /, % 등의 연산자는 이항 연산자이므로 단항 연산자(!, ++ 등)로 오버로딩할 수 없다.
4. 오버로딩된 연산자는 디폴트 인수를 사용할 수 없다.
오버로딩할 수 없는 연산자
- 다음의 연산자들은 오버로딩이 불가능하다.
연산자 | 설명 | 연산자 | 설명 |
:: | 범위 지정 연산자 | typeid | 타입 인식 |
. | 멤버 연산자 | const_cast | 상수 타입 변환 |
.* | 멤버 포인터 연산자 | dynamic_cast | 동적 타입 변환 |
?: | 삼항 조건 연산자 | reinterpret_cast | 재해석 타입 변환 |
sizeof | 크기 연산자 | static_cast | 정적 타입 변환 |
멤버 함수로만 오버로딩할 수 있는 연산자
- 다음의 연산자들은 전역함수가 아닌 클래스의 멤버 함수로만 오버로딩할 수 있다.
연산자 | 설명 |
= | 대입 연산자 |
() | 함수 호출 |
[] | 배열 인덱스 |
-> | 멤버 접근 연산자 |
연산자 오버로딩 예제
사칙연산 연산자 오버로딩
좌표 더하기
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
public:
Point(int _x, int _y);
void printP();
Point operator+(const Point& p) { // 바뀌지 않는 값은 const로 처리하는게 안정적
Point temp(this->x + p.x, this->y + p.y);
return temp; // 사칙연산은 반드시 값을 반환해야 함 (임시객체를 써야함)
}
};
Point::Point(int _x, int _y) {
x = _x;
y = _y;
}
void Point::printP() {
cout << "x = " << x << ", " << "y = " << y << endl;
}
int main() {
Point p1(1, 1);
Point p2(2, 5);
Point p3 = p1 + p2;
p3.printP();
Point p4 = p1 + p2 + p1;
p4.printP();
return 0;
}
x = 3, y = 6 // p3 출력
x = 4, y = 7 // p4 출력
- + 연산자를 오버로딩하여, (x, y) 좌표를 더하는 기능을 추가하였다. (코드 설명은 접은 글 참고)
- 위 예제와 같은 사칙연산(+, -, x, /)의 경우 반드시 임시 객체를 반환해야 한다. 만약 레퍼런스를 반환하여 연속적인 연산을 진행하면 잘못된 출력이 나온다.
더보기
- 연산자 오버로딩을 통해 더하기 연산이 클래스 멤버 변수에 대해서도 동작하도록 구현하였다.
- this를 통해 객체 자신의 멤버 변수를 가리키는 것임을 명시하였다.
- 레퍼런스를 반환하지 않고, 임시로 생성된 객체 자체를 반환하도록 하였다. 이렇게 구현한 이유는 바로 아래에서 설명한다.
만약 위 예제에서 레퍼런스를 반환한다면?
Point& operator+(const Point& p) {
this->x += p.x;
this->y += p.y;
return *this;
}
x = 6, y = 12 // p1 + p2 + p1 출력 (실제값: x=4, y=7)
- 레퍼런스로 반환하였더니 이상한 결과가 나왔다. 이는 다음과 같이 연산이 이루어졌기 때문이다.
- p1 + p2 + p2에서 p1 + p2를 먼저 계산 ▶ 반환값이 객체 자신이므로 p1의 좌표가 (p1.x + p2.x, p1.y + p2.y) = (3, 6)로 바뀜.
- 바뀐 좌표 p1과 바뀐 좌표 p1의 더하기 연산이 진행 ▶ (p1.x + p2.x + p1.x + p2.x, p1.y + p2.y + p1.y + p2. y) = (3 + 3, 6 + 6) = (6, 12)로 계산됨.
- 임시 객체를 반환하여 연속적인 더하기 연산을 하면, 기존 객체의 값들은 바뀌지 않고 임시 객체에 대해서 다시 연산이 진행된다. 따라서 사칙연산에 대한 연산자 오버로딩은 반드시 레퍼런스가 아닌 임시 객체를 반환하도록 한다.
문자열 + 객체 연산자 오버로딩
문자열 좌표 + 객체 좌표
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
int index;
public:
Point(int _x, int _y);
Point(const char* str);
int getNum(const char* str, int start, int end);
void printP();
Point operator+(const Point& p);
};
// 생성자 1
Point::Point(int _x, int _y) : index(0) {
x = _x;
y = _y;
}
// 생성자 2 (문자열)
Point::Point(const char* str) {
for (int i = 0; i != strlen(str); i++) {
if (str[i] == ',') {
this->index = i;
break;
}
}
this->x = getNum(str, 1, this->index);
this->y = getNum(str, this->index + 1, strlen(str) - 1);
}
// 연산자 오버로딩 1 (객체끼리 or 문자열이 뒤)
Point Point::operator+(const Point& p) {
Point temp(this->x + p.x, this->y + p.y);
return temp;
}
// 연산자 오버로딩 2 (문자열이 앞)
Point operator+(const Point& a, const Point& b) {
Point temp(a);
return temp.operator+(b);
}
// 문자열의 숫자를 구하는 함수
int Point::getNum(const char* str, int start, int end) {
int temp = 0;
for (int i = start; i < end; i++) {
temp *= 10;
temp += (str[i] - '0'); // 문자를 정수로 변환
}
return temp;
}
void Point::printP() {
cout << "x = " << x << ", " << "y = " << y << endl;
}
int main() {
Point p1(12, 13);
Point p2 = p1 + p1;
Point p3 = p1 + "(75,81)";
Point p4 = "(75,81)" + p1;
p2.printP();
p3.printP();
p4.printP();
return 0;
}
x = 24, y = 26
x = 87, y = 94
x = 87, y = 94
- 객체 p1의 좌표 (1,1)과 문자열 좌표 (7,8)의 더하기 연산에 대한 오버로딩을 진행하였고, p2에 (1 + 7, 1 + 8) = (8, 9)가 정확하게 대입되었다.
- 코드 설명은 접은 글에 있다.
더보기
생성자
- 위 예제에서 생성자는 총 2가지이다. 똑똑한 우리의 컴파일러가 상황에 따라 골라서 사용한다.
- 첫 번째 생성자는 직접 좌표를 대입하여 생성하는 일반적인 객체의 생성자이다.
- 두 번째 생성자는 문자열로부터 객체를 생성하기 위해 구현한 생성자이다.
- 문자열이 Comma(,)를 기준으로 구분되기 때문에 Comma의 index를 구하여 멤버 변수에 대입한다.
- 구한 index를 기반으로 getNum() 함수를 이용하여 문자열을 숫자로 변환한다.
- 변환한 숫자를 멤버 변수 x와 y에 각각 대입하여 객체를 생성한다.
getNum() 함수
- getNum 함수는 start와 end를 기준으로 문자열의 숫자를 추출한다.
- start와 end는 Comma(,) 기호를 기준으로 정해진다.
- temp에 10을 곱하는 이유는 자릿수를 맞춰주기 위함이다.
- (str[i] - '0')은 문자를 정수로 변환하는 기법이다.
연산자 오버로딩
- 첫 번째에 구현한 연산자 오버로딩은 객체끼리의 연산 혹은 객체 + 문자열에서 문자열이 뒤에 있는 경우에 사용된다. 이 연산자 오버로딩은 멤버 함수이다. (클래스 멤버로 정의)
- 두 번째에 구현한 연산자 오버로딩은 문자열 + 객체처럼 문자열이 앞에 있는 경우의 연산에서 사용된다. 이 연산자 오버로딩은 멤버 함수가 아니라 외부 함수이다. (전역 함수로 정의)
- 연산자 오버로딩 2의 반환을 a + b를 하면, a.operator(b)가 아니라, operator(a, b)가 호출되어 무한 재귀가 발생하므로 유의한다.
- 그리고 임시 객체를 생성하지 않고, a.operator(b)를 호출하도록 하여도 문제가 발생한다.
- 우리는 operator를 구현할 때 a와 b의 값이 절대 바뀌지 않을 것을 상정하여 const 인자로서 호출하였다. 만약 이때 a가 operator를 호출하면 상수 인자인 a의 값이 바뀌기 때문에 오류가 발생한다. (const 멤버 변수의 호출은 값을 바꾸지 않을 것이 확실한 const 멤버 함수만 가능하다.)
- 따라서 임시 객체를 생성하고 이 객체의 멤버 함수를 호출하는 것이 알맞다.
여기서 더나아가 멤버 함수를 호출하지 않고 객체 a, b의 멤버 변수를 직접 더하는 오버로딩을 구현한다고 해보자.
- 그런데, 연산자 오버로딩 2와 같이 구현하면 외부 함수이기 때문에 private 멤버 변수에는 접근할 수 없다는 문제가 발생한다.
- 하지만 놀랍게도 c++에서는 접근을 가능하게 하는 키워드 friend가 있다. (아래 게시글 참고)