C++은 할건데, C랑 다른 것만 합니다. 7편 연산자 오버로딩 2, friend, 입출력 연산자 오버로딩, 타입 변환 연산자

0

C++

목록 보기
7/10

연산자 오버로딩 2편, friend, 입출력 연산자 오버로딩, 타입 변환 연산자

1. friend

friend 키워드는 클래스 내부에서 다른 클래스나 함수들을 friend```로 정의할 수 있다.

friend로 지정된 클래스나 함수들은 해당 클래스의 변수나 함수에 접근할 수 있는데, 심지어 private 접근 제한을 받는 변수나 함수들도 접근 가능하다.

그래서 어디에 friend 키워드를 써줘야 하나 싶을텐데, 나와 친구라고 생각하는 애가 friend 키워드를 쓴다.

즉, A가 B에게 친구라고 생각하면 A가 내부적으로 B에 대한 friend 키워드를 가진다.

친구라고 생각하는 녀석이 접근하는게 아니라, 언제나 사기당하는 사람들은 친구라고 생각하여 다 알려준다. 따라서, 사기꾼 입장에서 친구의 비밀(private)에 접근 가능하다.

A가 B를 친구라고 생각하면 A는 B를 내부적으로 friend라고 해야하고, B는 A에 대한 비밀을 알고있어 B가 A의 private까지 접근이 가능하다.

다음의 예제를 보면 더욱 와닿는다.

#include <iostream>

using namespace std;

class Point{
    private:
        int x;
        int y;
        friend void print_info(int x, int y);
        friend class PointFriend;
    public:
        Point(int _x, int _y);
};

Point::Point(int _x, int _y) : x(_x), y(_y) {}

class PointFriend{
    private:
        int id;
    public:
        PointFriend(int _id);
        void helloFriends(int x, int y);
};

PointFriend::PointFriend(int _id) : id(_id) {}
void PointFriend::helloFriends(int x, int y){
    Point point(x,y);
    cout << "hello my friend! im helloFriends ! : "<< point.x << " " << point.y << endl;
}
void print_info(int x, int y){
    Point point(x,y);
    cout << "hello my friend im print_info ! : "<<point.x << " " << point.y << endl;  
}

int main(){
    PointFriend pointFriend(1);
    pointFriend.helloFriends(10,20);
    print_info(20,30);
}
hello my friend! im helloFriends ! : 10 20
hello my friend im print_info ! : 20 30

Point 클래스가 있고 PointFriend 클래스가 있는데, Point 클래스가 생각하기를 PointFriend를 친구라고 생각한다.

이에 따라 Point 클래스는 PointFriend에게 비밀을 다 알려주어, private까지 접근이 가능하다.

따라서 다음과 같이 Pointprivate 변수인 x와 y에 대해서도 접근이 가능한 것이다.

또한, friend는 클래스 뿐만 아니라 함수에도 적용이 가능하다.

따라서, 함수에서도 마찬가지로 해당 함수 내부에서 친구의 private까지 접근이 가능한 것이다.

2. 이항 연산자 오버로딩

c++에서는 이항 간의 연산을 허용하지 않는다.

이항이라면 다음과 같은 경우이다.

int a = 1 + "123";

다른 언어인 경우에는 이항 연산을 허용하여, 정수 124이거나, 문자열 "1123"으로 나온다.

그러나, cpp은 메모리 주소를 더하는 것이므로 의미가 없는 연산이다.

하지만, 우리가 직접 이항 연산자 오버로딩을 통해 이를 구현할 수 있다.

다음의 예제를 보자

#include <iostream>
#include <string>
using namespace std;

class Point{
    private:
        int x;
        int y;
    public:
        Point(int _x, int _y);
        Point(string value); // "x,y"
        Point operator=(const Point& point);
        Point operator+(const Point& point);
        void printXY();
};

Point::Point(int _x, int _y) : x(_x), y(_y) {}
Point::Point(string value){
    int i = 0;
    for(i = 0; i < value.size(); i++){
        if(value[i] == '.') break;
    }
    string xValue = value.substr(0,i);
    string yValue = value.substr(i+1,value.size());

    x = stoi(xValue);
    y = stoi(yValue);
}
Point Point::operator=(const Point& point){
    x = point.x;
    y = point.y;
    return *this;
}
Point Point::operator+(const Point& point){
    x += point.x;
    y += point.y;
    return *this;
}

void Point::printXY(){
    cout << "point x : " << x << " y : " << y << endl;
}

int main(){
    Point point1(1,10);
    Point point2("2.4");
    Point point3 = point1 + point2;
    point3.printXY();
}
point x : 3 y : 14

생성자에 문자열을 입력받아서 정수 x, y로 받는 함수를 추가하였다.

Point(string value); // "x,y"

string에 대한 사용 방법은 아직 잘 모르니, 그러려니 하도록 하자

그럼 다음과 같은 경우는 어떻게 될까??

Point point1(1,10);
Point point3 = point1 + string("2.4");

참고로 stringchar *은 다르다. 따라서, char *을 string으로 변환하기 위해서는 string(char *)로 변환하면 된다.

point x : 3 y : 14

결과가 잘 나온 것을 알 수 있다.

그러나 앞 뒤를 바꾸면 연산이 되지 않는다.

Point point1(1,10);
Point point3 = string("2.4") + point1;

컴파일 에러가 발생하게 된다. operator+가 없다고 나오기 때문이다.

왜 이런 일이 발생할까??

이전 편에서 잠깐 이야기한 것이 있는데, operator 문법은 해당 연산자가 등장하면, 함수를 호출하는 방식이다.

operator+라면 다음과 같다.

a + b;
a.operator+(b); // 위 식이 변환되어 다음과 같이 사용된다.

// 또는 아래로 변환된다.
operator+(a,b);

총 두가지로 a.operator+(b), operator+(a,b) 이렇게 변환되어 연산된다.

Point point3 = point1 + string("2.4");

그래서 위의 식은 다음과 같다.

point1.operator+(string("2.4"));

가 되고, 이전에 확인했듯이 매개변수에 class의 생성자로 받을 수 있는 값이라면 class의 생성자로 바뀌어 생성해준다고 했다.

즉, 컴파일러는 정확히 일치하지 않는 경우, 가장 가까운 가능한 오버로딩되는 함수를 찾기 때문이다.

point1.operator+(Point("2.4"));

이렇게 바뀌는 것이다. 그래서 연산이 가능했던 것이다.

그런데, 앞뒤를 바꾸어보면

string("2.4").operator+(point1);

//또는 

operator+(string("2.4"), point1);

둘 중 하나로 연산되어야 하는데 string입장에서 operator+ 연산자 오버로딩에 Point와 연산되는게 없다.

그래서, 우리는 두 가지 선택지가 있다. stringoperator+를 오버로딩하던지, operator+(string Point) 인 함수를 하나 만들던지

string에 연산자 오버로딩을 구현하는 것은 매우 귀찮은 일이다.

따라서, 외부 함수인 operator+(string Point)를 만들어주도록 하자

한 가지 명심해야하는 것이 operator+(string Point)는 Point의 맴버 함수가 아니다.

Point operator+(const Point& point1, const Point& point2){
    Point temp = point1 + point2;
    return temp;
}

다음과 같이 만들어야하는데, 중요한 것은 Point::operator+가 아니라, 그냥 operator+라는 것이다.

즉, 외부 함수라는 것이다.

그런데, 다음과 같은 경우는 같은 기능을 하는데 컴파일 에러가 발생한다.

Point operator+(const Point& point1, const Point& point2){
    int x, y;
    x = point1.x + point1.x;
    y = point2.y + point2.y;
    Point temp(x,y);
    return temp;
}

결과는 같지만, 컴파일 에러가 발생한다. 왜냐하면 해당 함수는 Point의 맴버 함수가 아니기 때문이다.

때문에 Point의 private 맴버 변수에 접근할 수 없는 것이다.

이 때를 위해서 우리가 위에 배운 것이 하나있다. 바로 friend이다.

이를 이용하여 외부 함수인 Point operator+(const Point& point1, const Point& point2)Pointprivate 맴버 변수에 접근할 수 있다.

class Point{
    private:
        int x;
        int y;
    public:
        Point(int _x, int _y);
        Point(string value); // "x,y"
        Point operator=(const Point& point);
        Point operator+(const Point& point);
        void printXY();
        friend Point operator+(const Point& point1, const Point& point2);
};

클래스 Point의 맴버 함수로 friend Point operator+(const Point& point1, const Point& point2);를 추가하여 외부 함수인 Point operator+(const Point& point1, const Point& point2)가 private인 Point의 맴버 변수에 접근할 수 있게되었다.

이제 다음과 같이 문제의 코드가 작동하게 된다.

Point point3 = string("2.4") + point1;

위 코드가 다음처럼 변환된다.

Point point3 = operator+(Point(string("2.4"))+point1);

이렇게 이항 연산자도 쉽게 만들 수 있다는 것을 알게되었다.

3. 입출력 연산자 오버로딩

std::cout << a;

이전까지 우리가 출력문으로 사용하고 있던, cout 역시도 사실은 operator이다

따라서 다음으로 호출되고 있던 것이다.

std::cout.operator<<(a);

그래서 우리는 operator<<연산자를 오버로딩 할 수 있다.

그러나, 위에서 본듯이 cout에 우리가 만든 Point 클래스를 오버로딩해야하는데, 이는 어려운 일이므로 위에서 했듯이 외부 함수를 하나 만들어처리하도록 하자

출력은 out이기 때문에 ostream 클래스에서 처리한다.

그래서 다음과 같이 함수를 만들 수 있다.

ostream& operator<<(ostream& os, const Point& point){
    os << "point x : " << point.x << " y : " << point.y << endl;
    return os;
}

여기서 ostream&을 반환해줬는데, 이것을 반환해줘야 다음처럼 문법을 쓸 수 있다.

cout << point1 << point2 << point3 << endl;

그러나, 현재 ostream& operator<<(ostream& os, const Point& point)는 외부 함수이므로, 내부의 Point 클래스의 private 맴버 변수에 접근할 수 없다.

때문에 우리는 해당 함수를 Point의 friend로 선언해줘야 한다.

class Point{
    private:
        int x;
        int y;
    public:
        Point(int _x, int _y);
        Point(string value); // "x,y"
        Point operator=(const Point& point);
        Point operator+(const Point& point);
        void printXY();
        friend Point operator+(const Point& point1, const Point& point2);
        friend ostream& operator<<(ostream& os, const Point& point);
};
/*생략
...
*/

int main(){
    Point point1(1,10);
    Point point2("2.4");
    Point point3 = point1 + string("2.4");
    cout << point3;
}
point x : 3 y : 14

다음과 같이 friend를 선언해주면 문제없이 코드가 구동될 것이다.

이와 똑같이 cin역시도 연산자 오버로딩 할 수 있다. 참고로 cinostream이 아니라, istream이다.

cin은 input이기 때문이고, cout은 output이기 때문이다. 그래서 우리가 처음에 include 하는 라이브러리가 <iostream>인 것이다.

때문에 다음과 같이 써주면 된다.

istream& operator>>(istream& os, Point& point){
    os >> point.x >> point.y;
    return os;
}

해당 코드의 선언은 클래스에 다음과 같이 적어주면 된다.

friend istream& operator>>(istream& os, Point& point);

이제 입력을 받고 출력해보면 다음과 같다 나온다.

1 3 // 입력

point x : 1 y : 3 // 출력

4. Wrapper 클래스 - 타입 변환 연산자

Wrapper는 기존에 있는 변수나 함수, 클래스 등을 감싸서 새로운 타입을 만드는 것이다.

비슷하게 프록시 패턴같은게 있으며, Wrapper를 통해 기존에 있던 기능에 추가적인 로직들을 구현할 수 있다.

그럼, Wrapper 클래스를 구현하기 위해서는 기존의 타입에 대한 operator를 모두 오버로드해야할까??

그건 너무 비효율적이다. 따라서, 자동으로 Wrapper를 기존의 타입과 호환되도록 하는 것이 바로 타입 변환 연산자이다.

다음과 같이 사용한다.

operator 기존타입() {};

위의 코드를 Wrapper 클래스의 맴버함수로서 넣어주면 된다.

함수의 이름은 기존타입이다. 따라서 함수의 이름을 따로 정의해주면 안된다.

#include <iostream>

using namespace std;

class IntWrapper{
    private:
        int data;
    public:
        IntWrapper(int data) : data(data) {}
        IntWrapper(const IntWrapper& wrapper) : data(wrapper.data) {}

        operator int(){
            if( data < 0) return 0;
            return data;
        }
};

int main(){
    IntWrapper wrapper = 2;
    int adder = -6;
    IntWrapper ret = wrapper + 3 + adder;
    cout << ret;
}
0

아주 심플하지만 파워풀한 기능이다. 단지 타입 변환 연산자 하나 넣었다고 호환이 가능한 것이다.

operator int(){
    if( data < 0) return 0;
    return data;
}

다음의 타입 변환 연산자는 컴파일러에서 적절한 변환 연산자로 operator int를 찾아서 int로 변환해서 처리한다.

필자는 음수로 넘어가면 0을 반환하도록하여 양의 정수를 구현하였다.

0개의 댓글