[C++] 입출력 연산자 오버로딩
개인의 학습을 목적으로 정리한 글입니다. 이점 참고하고 읽어주세요 ;)
글을 시작하기 앞서 머리에 각인하고 가야 하는 개념은,
C++에서 출력을 할 때 자주 사용하는 'cout <<'에서 바로 이 '<<' 역시 '+', '-'과 같은 연산자다!라는 개념입니다.
덧셈을 하려면 + 연산자가 있어야 하고
뺄셈을 하려면 - 연산자가 있어야 하듯이,
출력을 하려면 역시 << 연산자가 있어야 합니다.
그렇기 때문에 출력 연산자를 사용자 정의 자료형인 클래스에서 따로 정의하는 것이고요
저는 처음에 이 개념이 잡히지 않아서 헤매었던 시간이 많은데, 혹시나 하는 마음에 먼저 적고 정리를 시작합니다.
class Point
{
private:
double m_x, m_y, m_z;
public:
Point(double x =0.0, double y=0.0, double z=0.0)
: m_x(x), m_y(y), m_z(z)
{}
double getX() { return m_x; }
double getY() { return m_y; }
double getZ() { return m_z; }
void print()
{
cout << m_x << " " << m_y << " " << m_z << endl;
}
};
우선 예제로 사용하는 Point 클래스는 위의 코드 블록과 같습니다.
현재는 출력 연산자가 정의되어있지 않기 때문에, Point 자료형의 m_x, m_y, m_z 멤버들을 출력하기 위해서는 print() 함수를 따로 정의하고 호출해야 합니다. 즉, 지금 이 클래스 정의만으로는 main 함수에서
int main()
{
Point p1(0.0, 0.1, 0.2);
Point p2(3.4, 1.5, 0.2);
p1.print();
p2.print();
return 0;
}
위의 코드 블록처럼 실행을 해야 p1과 p2의 멤버들을 출력할 수 있고,
cout << p1 << " " << p2 << endl;
일반 자료형 변수를 생각하고 위의 코드 블록처럼 만들어 실행을 하면 에러가 발생하게 됩니다.
이는 클래스에 입출력 연산자(<<, >>)를 정의하면 해결됩니다.
friend std::ostream& operator << (std::ostream& out, const Point& point)
{
out << m_x << " " << m_y << " " << m_z;
return out;
}
위의 코드 블록이 클래스 내부에 friend 함수로 출력 연산자를 정의하는 코드입니다. 얼핏 보기에 꽤 복잡해 보이는 코드를 쪼개서 설명해볼게요.
우선 friend 함수 선언을 위한 friend 다음에 나오는 'std::ostream&'은 출력 연산자의 return 자료형입니다. 우리가 사용했던 cout이 출력 연산자 중 하나예요. int add(int x1, x2) 함수에서 return type 위치의 int라고 생각하면 됩니다. TMI로 ostream은 'output stream'의 약자입니다. 출력 스트림의 약자예요.
다음으로 operator << 는 위에서 말한 add함수의 이름인 add라고 보면 될 것 같아요.
물론 일반 함수에서는 함수의 이름만 바꾼다고 함수의 기능이 변하지 않지만, 출력 연산자는 operator <<가 이름이자 곧 정체성이기 때문에 바꾸시면 안 됩니다!
다음으로는 매개변수 부분입니다. 출력 연산자는 두 개의 매개변수를 받아요. 첫 번째 매개변수는 위에서 말한 std::ostream& 자료형의 out이라는 변수입니다. 여기서 매개변수의 이름은 꼭 out이 아니어도 상관없습니다. 대신 정한 매개변수명이 출력 연산자의 기능을 정의하는 블록 안에서도 같은 이름으로 사용되어야 합니다(당연한 말이지만).
저는 처음에 저 out이라는 매개변수명이 cout과 비슷해서 많이 헷갈렸어요.
이제 출력 연산자의 기능을 정의하는 블록으로 들어가 볼게요.
예제에서 정의한 출력 연산자는 첫 번째 매개변수인 ostream& 자료형인 out을 출력하고 차례로 priavate 멤버인 m_x, m_y, m_z를 출력하도록 정의했어요. 처음에 out을 출력하도록 설계한 건 다음 줄인 return에서 설명할 수 있는데
return으로 ostream& 자료형인 out을 받게 되니까, 하나의 출력문에서 연속으로 out을 이어받아 출력할 수 있게 됩니다.
friend std::istream& operator >> (std::istream& in, Point& point)
{
in >> point.m_x >> point.m_y >> point.m_z;
return in;
}
이번에는 입력 연산자입니다. 큰 틀은 출력 연산자 정의와 많이 비슷해요.
다만 다른 점은 이번엔 출력 타입이 ostream&(출력 연산자)가 아닌 istream&(입력 연산자)입니다. istream은 'in stream'의 약자예요. 그리고 operator 부분도 이번엔 입력이니 '<<'가 아니라 '>>'를 입력하면 됩니다. cin >> 에서 >>을 사용하는 것처럼요.
그리고 매개변수 역시 첫 번째 매개변수는 입력 스트림, 두 번째 매개변수는 Point자료형을 레퍼런스로 가져옵니다.
출력 연산자의 경우 출력하려는 값이 변하는 걸 막기 위해 const 선언을 해주는 반면, 입력 연산자는 그 목적이 빈 메모리를 채우거나 메모리에 있는 값을 바꾸는 거니까 const 선언을 하면 안 되겠죠?
입력 연산자의 바디를 채우는 부분의 원리는 출력 연산자를 정의할 때와 동일합니다!
int main()
{
Point p1, p2;
cin >> p1 >> p2;
cout << p1 << " " << p2;
return 0;
}
이렇게 모두 입력 연산자와 출력 연산자를 설정하고 main함수를 실행시키면
사진과 같이 차례대로 p1과 p2의 멤버들에 대입할 값을 입력하고 엔터를 치면 차례대로 p1과 p2의 멤버들이 출력되는 걸 확인할 수 있습니다!