연산자 오버로딩이란?
연산자 오버로딩은 사용자 정의 타입에서도 +
, -
, \
와 같은 연산자를 사용할 수 있게 하는 문법이다.
예를 들어, 아래와 같이 코드를 구현했다고 가정하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
using namespace std;
class Point
{
int _x, _y;
public:
explicit Point(int x, int y)
{
_x = x;
_y = y;
}
};
int main()
{
Point p1(1, 2);
Point p2(5, 10);
p1 + p2; // 컴파일 에러 발생!!!
}
위 코드에서 p1 + p2는 +
연산자에 대한 정의가 존재하지 않음으로 컴파일 에러가 발생하게 된다.
컴파일 에러를 해결하기 위해서는 당연히 +
연산자의 정의를 작성해 주면 된다.
그것이 연산자 오버로딩이고 이를 하나씩 살펴보자.
문법 구조
연산자 오버로딩의 기본적인 문법 구조는
- operator+
- operator++
- operator+=
- operator==
- operator()
- operator->
- operator*
같이 연산자 앞에 operator 라는 이름이 붙게 된다.
그리고 연산자 오버로딩 함수 구조는 아래와 같다.
형식 operator
operator-symbol(parameter-list)
정의할 수 있는 연산자 종류는 다양한데 공식 문서에 자세히 작성되어 있으니 공식 문서를 참고하자
연산자 종류와 문법 구조는 알았으니 실제로 어떻게 사용할 수 있는지 몇 가지 연산자들을 통해 살펴보자.
연산자 오버로딩 예제
operator+()
operator+는 +
연산자를 뜻하는 함수이며 사용 방법은 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<iostream>
using namespace std;
class Point
{
int _x, _y;
public:
explicit Point() = default;
explicit Point(int x, int y)
{
_x = x;
_y = y;
}
void Print()
{
cout << "_x: " << _x << " " << "_y: " << _y << endl;
}
Point operator+(Point rhs) // + 연산자 오버로딩
{
cout << "operator+ 호출" << endl;
return Point(_x + rhs._x, _y + rhs._y);
}
};
int main()
{
Point p1(1, 2);
Point p2(5, 10);
(p1 + p2).Print();
}
// 출력 결과
operator+ 호출
_x: 6 _y: 12
p1 + p2는 실제로는 아래와 같이 호출되고 있다.
1
p1.operator+(p2)
operator+()를 정의해 줌으로써 컴파일러는 p1 + p2에 대한 처리 방법을 알고 있고 그러므로 컴파일 에러가 발생하지 않았음을 확인할 수 있다.
operator++()
++
연산자는 전위++ 연산자(Ex. ++i), 후위++ 연산자(Ex. i++)가 존재한다.
구현 자체는 operator+()와 동일하니 코드로 살펴보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using namespace std;
class Point
{
int _x, _y;
public:
explicit Point() = default;
explicit Point(int x, int y)
{
_x = x;
_y = y;
}
void Print()
{
cout << "_x: " << _x << " " << "_y: " << _y << endl;
}
Point operator++() // 전위++ (Ex. ++i)
{
return Point(++_x, _y);
}
Point operator++(int n) // 후위++ (Ex. i++)
{
return Point(n != 0 ? _x += n : _x++, _y);
}
Point operator+(Point rhs)
{
cout << "operator+ 호출" << endl;
return Point(_x + rhs._x, _y + rhs._y);
}
};
int main()
{
Point p1(1, 2);
// 둘 다 컴파일 에러 없이 정상 작동
++p1; // => p1.operator++()
p1++; // => p1.operator++(0)
}
p1++은 p1.operator++(0)으로써 파라미터 값에 0이 들어가는데 실제로 0이 들어가는지 확인하기 위해서
operator++(int n) 함수에 아래와 같이 수정하였다.
1
2
3
4
5
6
7
8
Point operator++(int n) // 후위++ (Ex. i++)
{
cout << n << endl;
return Point(n != 0 ? _x += n : _x++, _y);
}
// 출력 결과
0
코드를 실행해 보면 실제로 0이 출력되는 것을 확인할 수 있다.
전역 함수를 이용한 연산자 오버로딩
연산자 오버로딩을 정의하는 방식은 2가지가 있는데 아래와 같다.
- 멤버 함수를 이용한 연산자 오버로딩
- 전역 함수를 이용한 연산자 오버로딩
지금까지 예제를 통해서 살펴본 연산자 오버로딩은 1번 멤버 함수를 이용한 연산자 오버로딩이다.
예제를 보면 멤버 함수를 통해서도 큰 문제 없이 연산자 오버로딩이 되었는데
왜 전역 함수를 이용한 연산자 오버로딩을 해야하는지 의아할 수 있다.
이제 그 궁금증을 해소해보자
멤버 함수 연산자 오버로딩을 사용하지 못하는 경우
operator+() 예제 코드를 통해 살펴보자
Point 클래스에 Point(int x) 생성자가 하나 더 추가되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<iostream>
using namespace std;
class Point
{
int _x, _y;
public:
explicit Point() = default;
Point(int x)
{
_x = x;
}
explicit Point(int x, int y)
{
_x = x;
_y = y;
}
void Print()
{
cout << "_x: " << _x << " " << "_y: " << _y << endl;
}
Point operator+(Point rhs)
{
cout << "operator+ 호출" << endl;
return Point(_x + rhs._x, _y + rhs._y);
}
};
int main()
{
Point p1(1, 2);
Point p2(3, 4);
int num = 5;
num + p1; // 컴파일 에러 발생
p1 + num; // num의 암시적 타입 변환으로 인해 정상 작동
}
위 코드를 IDE로 돌려보면 num + p1은 컴파일 에러가 발생하는데 p1 + num은 컴파일 에러가 발생하지 않는다.
operator+()가 실제로 호출되는 방식을 보면 이해가 되는데
p1 + num일 때로 보면 p1 + num은 p1.operator+(num)이 된다.
이 때 num은 Point(int x) 생성자로 인해 암시적 타입 변환이 된 상태이다.
num + p1은 num.operator+(p1)이 되는데 num은 int 타입이고 int 타입에 대한 operator+에는 사용자 정의 클래스인 Point에 대한 정의가 존재하지 않기 때문에 컴파일 에러가 발생한다.
이러한 컴파일 에러를 극복하기 위해서 전역 함수를 이용한 연산자 오버로딩이 필요한 것이다.
전역 함수를 이용한 연산자 오버로딩 예제
전역 함수를 이용한 연산자 오버로딩 방법은 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<iostream>
using namespace std;
class Point
{
int _x, _y;
public:
explicit Point() = default;
Point(int x)
{
_x = x;
_y = 0;
}
explicit Point(int x, int y)
{
_x = x;
_y = y;
}
void Print()
{
cout << "_x: " << _x << " " << "_y: " << _y << endl;
}
int GetX()
{
return _x;
}
int GetY()
{
return _y;
}
};
// 전역 함수 operator+
Point operator+(Point lhs, Point rhs)
{
cout << "전역 함수 operator+ 호출" << endl;
return Point(lhs.GetX() + rhs.GetX(), lhs.GetY() + rhs.GetY());
}
int main()
{
Point p1(1, 2);
Point p2(3, 4);
int num = 5;
num + p1;
}
전역 함수이기 때문에 클래스 내부가 아닌 외부에 작성된 것을 볼 수 있으며
num + p1은 num.operator+(p1)이 아닌 operator+(num, p1)으로 호출하게 되면서 컴파일 에러가 발생하지 않음을 확인할 수 있다.
friend 함수를 이용하여 전역 함수 연산자 오버로딩 구현하기
전역 함수 연산자 오버로딩은 Point 클래스 내부에 있는 것이 아니기 때문에 private 변수에 대한 접근이 불가능하다.
그렇기 때문에 위 예제에서는 Getter를 이용해 처리했으나 friend 함수를 통해서도 구현이 가능하다.
friend 함수는 캡슐화를 무시하기 때문에 가급적 Getter를 쓰는 게 좋다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
using namespace std;
class Point
{
int _x, _y;
public:
// 생략
// 전역 함수 연산자 오버로딩을 friend 했다.
friend Point operator+(Point , Point );
};
Point operator+(Point lhs, Point rhs)
{
cout << "전역 함수 operator+ 호출" << endl;
// friend로 인해 private 변수에 접근이 가능해졌다.
return Point(lhs._x + rhs._x, lhs._y + rhs._y);
}
int main()
{
Point p1(1, 2);
Point p2(3, 4);
int num = 5;
num + p1;
}