<개인공부>/[C++]

[C++] 연산자 재정의 기본 (overator overloading)

BlockDMask 2021. 5. 18. 00:30
반응형

안녕하세요. BlockDMask입니다.
오늘은 C++ 연산자 재정의 하는 방법에 대해서 알아보려고 합니다.

<목차>
1. 연산자 재정의란?
2. 연산자 재정의 예제 (사용자 정의, primitive 타입 순서, 연산자 오버 로딩 우선순위)

 

 

1. C++ 연산자 재정의 방법


연산자 재정의라는 것은 우리가 일반적인 타입들의 덧셈 int 들의 덧셈 1+2 = 3, 곱셈 3 * 4 = 12 이걸 코드로 나타내면

a = 1 + 2
b = 3 * 4
이런 식으로 나타낼 수 있는데 
Car라는 클래스가 있다고 했을 때 Car 객체인 c1, c2의 객체끼리의 덧셈이 일반적으로는 불가능한데,
연산자 재정의를 통해서 Car c3 = c1 + c2 가 가능하도록 할 수 있습니다.

이걸 연산자 재정의라고 하는데요. +, -, * 등의 연산자들도 함수로 만들 수 있습니다.
두 가지 방법이 있으며, 하나하나 알려드리겠습니다.

 

1-1) 일반 함수를 통한 연산자 오버 로딩 : operator+(a, b)

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
#include<iostream>
using namespace std;
 
class Car {
private:
    int x;
    int y;
public:
    Car(int _x = 0int _y = 0) : x(_x), y(_y) {}
 
    void Show() {
        cout << x << ", " << y << endl;
    }
 
    // private 멤버 변수에 접근이 가능하게 하도록 friend로 만들어줌.
    friend Car operator+(const Car& c1, const Car& c2);
};
 
// 일반 함수를 통한 연산자 재정의
Car operator+(const Car& c1, const Car& c2)
{
    Car tmp;
    tmp.x = c1.x + c2.x;
    tmp.y = c1.y + c2.y;
    return tmp;
}
 
 
int main() 
{
    Car c1(31);
    Car c2(72);
 
    Car c3 = c1 + c2; // 연산자를 재정의 했기 때문에 가능
    c3.Show();
 
 
    return 0;
}
cs

여기서는 덧셈 연산을 하기 위해서 연산자를 재정의 해보았습니다. 
이 예제에서는 아래 3가지를 주로 보시면 됩니다.
1.  operator overloading 만드는 방법
위쪽 코드를 보시면 클래스 밖에서일반 함수로 operator+ 함수를 정의했습니다.
Car operator+(Car c1, Car c2){
  Car tmp;
  tmp.x = c1.x + c2.x;
  tmp.y = c1.y + c2.y;
  return tmp;
}
이런 식으로operator를 붙이고 뒤에 연산을 재정의할 기호 (+)를 붙입니다.
덧셈이니 + 였을 것이고 곱셈을 재정의 할 것이라면 * 을 붙이면 됩니다.

매개변수로는 같은 객체 2개를 받고, 해당 객체를 연산해서 새로운 객체를 반환하면 됩니다.
반환형도 동일한 Car 클래스 임을 볼 수 있습니다.

2. 매개변수로 넘길 때에는 객체가 크다면 call by value로 넘기는 복사보다는 const & 넘긴다면 비용을 줄일 수 있습니다.

3. 일반 함수로 만들었기 때문에 클래스 외부에서 private 한 멤버 변수에 접근이 불가능합니다.
대부분의 경우에 연산자 오버 로딩에서는 위에 예제에서 처럼 friend 함수 처리를 해서 사용하곤 합니다.
friend Car operator+(const Car& c1, const Car& c2);
이런 식으로 사용하게 되면 일반 함수에서 private 멤버에 접근이 가능합니다.

만약 friend와 친하지 않으시다면 getX(), getY()처럼 public 멤버 함수를 만드셔서 그걸 이용하셔도 됩니다.

 

 

1-2) 멤버 함수로 연산자 오버 로딩 : a.operator+(b)

멤버 함수를 통한 연산자 재정의 모양은 이와 같습니다.
a.operator+(b)

모양만 저렇지 실제로 사용할 때는 Car c3 = c1 + c2 이렇게 사용하면 되는데
실제 컴파일러에서는 c3 = c1.operator+(c2) 이런 식으로 해석하게 됩니다. c1의 멤버 함수에 접근을 하기 때문이죠.
바로 예제로 확인해보겠습니다.

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 Car {
private:
    int x;
    int y;
public:
    Car(int _x = 0int _y = 0) : x(_x), y(_y) {}
 
    void Show() {
        cout << x << ", " << y << endl;
    }
 
    // 멤버 함수로 연산자 재정의
    Car operator+ (const Car& c)
    {
        Car tmp;
        tmp.x = c.x + this->x;
        tmp.y = c.y + y; //this->y 와 같습니다.
        return tmp;
    }
};
 
int main() {
    Car c1(31);
    Car c2(72);
    Car c3 = c1 + c2;
    c3.Show();
 
    return 0;
}
cs

이렇게 멤버 함수로 연산자 재정의를 할 수 있습니다.
a.operator(b)
위와 같은 모양을 잘 기억해두시면 멤버 함수로 연산자를 재정의 하는 걸 쉽게 할 수 있습니다.
Car operator+(const Car& c)
{
  //.. 연산
  return 객체;
}
조금 어색해 보이는 게 인자가 하나짜리여서 헷갈릴 수 있지만 위와 같이 만들어도,
우리는 c = a + b 이런 식으로 사용하면 됩니다.

 

두 가지 방법이 완전히 동일하다고는 할 수가 없습니다.
멤버 함수 방법으로만 오버 로딩이 가능한 연산자는 =, (), [], -> 이 있습니다.

 

2. C++ 연산자 재정의 예제


2-1) 연산자 오버로딩 멤버 함수와 일반 함수의 우선순위

이번엔 곱셈으로 연산자 재정의 예제를 들어보겠습니다.

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
#include<iostream>
using namespace std;
 
class Kimchi {
private:
    int amount;
public:
    Kimchi(int _amount = 0) : amount(_amount) {}
 
    void Show() {
        cout << "김치의 양 : " << amount << endl;
    }
 
    // 멤버 함수로 연산자 재정의
    Kimchi operator* (const Kimchi& k)
    {
        cout << "멤버함수 호출" << endl;
 
        Kimchi tmp;
        tmp.amount = k.amount * this->amount;
        return tmp;
    }
 
 
    // 일반 함수 private 접근 허용을 위한 friend.
    friend Kimchi operator* (const Kimchi& k1, const Kimchi& k2);
};
 
 
// 일반 함수로 연산자 재정의
Kimchi operator* (const Kimchi& k1, const Kimchi& k2)
{
    cout << "일반함수 호출" << endl;
 
    Kimchi tmp;
    tmp.amount = k1.amount * k2.amount;
    return tmp;
}
 
 
int main() 
{
    Kimchi k1(3);
    Kimchi k2(7);
    Kimchi k3 = k1 * k2; // 곱셈 go
    k3.Show();
    return 0;
}
 
 
cs

이렇게 두 개의 연산자 재정의가 있을 때는 어떤 것이 먼저 불리나요?
네 결과를 보듯이 멤버 함수가 먼저 불리게 됩니다.

컴파일러는 저렇게 사용자 정의 타입(클래스 같은)것의 연산이 있을 때
멤버 함수에 연산자 재정의가 있는지 a.operator*(b)를 찾아보고
멤버함수에 연산재 재정의가 없으면 그다음에 일반 함수에 연산자 재정의가 있는지 operator*(a, b)를 찾아보게 됩니다.

 

 

2-2) 사용자 정의 타입과 primitive 타입 간의 연산자 재정의

그럼 같은 클래스끼리만 연산자 재정의가 가능한 걸까요?
아닙니다. 다른 클래스, 아니면 primitive 타입(int, float 등 이미 있는 타입들)들과도 재정의가 가능합니다.

하지만 이때 멤버 함수 재정의 와 일반 함수 재정의로 가능한 게 있고, 아닌 게 있는데요.
예를 들어서 
Point라는 클래스가 있다고 하겠습니다.
Point p2 = p1 + 10; 
이런 식의 덧셈이 가능하도록 연산자를 재정의 한다고 할 때 위 코드는 아래와 같이 해석이 될 것입니다.
p1.operator+(10); 혹은
operator+(p1, 10); 이걸로 말이죠. 두 상황 모두 괜찮아 보입니다. 하지만 아래와 같은 경우는 어찌 될까요?

Point p3 = 10 + p1;
이런 경우라면 아래와 같이 해석될 것 같네요.
10.operator+(p1);  // 응??  int 타입에 멤버 함수가 있는 듯이 해석이 되네?
operaotr+(10, p1); // 음음 괜찮
이렇게 해석이 될 것입니다. 
아래와 같이 일반 함수로 연산자를 재정의 하는 것은 가능해 보이는데
10.operator+(p1)은 불가능해 보이죠? 네. 불가능합니다.

이렇게 차이가 있습니다. 코드로 한번 보시죠.

 

> 사용자 정의 타입 + primitive 타입 

편의상 아래에는 코드 하나를 두고 둘 다 가능.
테스트할 때는 하나씩 주석 처리하면서 진행했습니다.

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
#include<iostream>
using namespace std;
 
class Point {
private:
    int x;
    int y;
 
public:
    Point(int _x = 0int _y = 0) : x(_x), y(_y) {}
 
    void Show() {
        cout << "(x, y) : " << x << ", " << y << endl;
    }
 
    // Point.operator+(int)
    Point operator+ (int num)
    {
        cout << "2. Point.operator+(int)" << endl;
 
        Point tmp;
        tmp.x = this->+ num;
        tmp.y = this->+ num;
        return tmp;
    }
 
    // 일반함수를 위한 friend
    friend Point operator+(const Point& p, int num);
};
 
 
// operator+(Point, int)
Point operator+(const Point & p, int num)
{
    cout << "1. operator+(Point, int)" << endl;
 
    Point tmp;
    tmp.x = p.x + num;
    tmp.y = p.y + num;
    return tmp;
}
 
 
int main() {
    Point k1(33);
    Point k2 = k1 + 100;
    k2.Show();
    return 0;
}
 
 
 
cs

일반함수 재정의 호출
멤머함수 재정의 호출

편의상 하나씩 주석 처리하면서 테스트 진행해서 결과를 두 개 뽑았습니다.
이런 식으로 "사용자 정의 타입 + primitive 타입"처럼 사용자 정의 타입이 앞에 나와서 연산을 하는 경우에는 
일반 함수 연산자 재정의 와 멤버 함수 연산자 재정의 둘 다 가능합니다.

하지만 그 반대의 경우인 "primitive 타입 + 사용자 정의 타입" 이 순서로 연산이 된다면..?

 

> primitive 타입 + 사용자 정의 타입

이런 경우에는 일반 함수로 재정의 하는 방법만 가능합니다.

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
#include<iostream>
using namespace std;
 
class Point {
private:
    int x;
    int y;
 
public:
    Point(int _x = 0int _y = 0) : x(_x), y(_y) {}
 
    void Show() {
        cout << "(x, y) : " << x << ", " << y << endl;
    }
 
    // int.operator+(Point). 응 안돼~
    //int operator+ (Point p)
    //{
    //    cout << "2. int.operator+(Point)" << endl;
    //    // ???
    //    //return ???
    //}
 
    // 일반함수를 위한 friend
    friend Point operator+(int num, const Point& p);
};
 
 
// operator+(Point, int)
Point operator+(int num, const Point & p)
{
    cout << "1. operator+(int, Point)" << endl;
 
    Point tmp;
    tmp.x = p.x + num;
    tmp.y = p.y + num;
    return tmp;
}
 
 
int main() {
    Point k1(2030);
    Point k2 = 100 + k1; // 숫자가 먼저
    k2.Show();
    return 0;
}
cs

일반함수 연산자 재정의만 가능

이렇게 primitive가 먼저 오는 경우에는 일반 함수로 오버로딩 하는 수밖에 없습니다.
각 상황에 맞게 일반함수 재정의를 할지, 멤버 함수 재정의를 할지 선택해서 사용하시면 될 것 같습니다.

 

이상으로 오늘은 C++연산자 재정의에 대해서 알아보았습니다. 감사합니다.

반응형