스물네번째 열혈강의
이제 우리는 연산자 오버로딩 을 배우고 있다. 상당히 중요한 부분이고, 앞서 배운 것들을 연관지어 고민해보며 잘 배워가자.
이번엔 교환법칙 문제에 대한 해결을 해볼것이다.
먼저 한가지 예제를 볼 것인데, 연산자 오버로딩을 통해서 서로 다른 자료형의 두 데이터 간의 연산을 할 것이다.
#include <iostream>
using namespace std;
class Point{
private:
int xpos, ypos;
public:
Point(int x=0, int y=0) :xpos(x), ypos(y){}
void ShowPosition() const{
cout << "[" << xpos << ", " << ypos << "]" << endl;
}
Point operator*(int times){
Point pos(xpos*times, ypos*times);
return pos;
}
};
int main(){
Point pos(1, 2);
Point cpy;
cpy = pos * 3;
cpy.ShowPosition();
cpy = pos*3*2;
cpy.ShowPosition();
return 0;
}
위 예제에서 객체간 멤버 대 멤버 복사가 진행되는데, 이는 나중에 설명할 것이고, 동일한 자료형 두 객체를 대상으로 대입연산이 가능 하고, 그 결과로 멤버 대 멤버 복사가 이루어 진다 는 것만 알고 있도록 하자.
여기서 곱샘의 위치를 바꿀 수 없다는 문제점을 알 수 있어야 한다. Point 클래스의 멤버함수로 연산자 오버로딩을 했기 때문에, 항상 Point 객체가 왼쪽
에 있어야 한다.
그러나 곱셈은 교환 법칙 이 성립하므로, 우측에서도 똑같이 가능해야 한다.
먼저, 전역함수 형태로 곱셈 연산자를 오버로딩 해야 한다. (멤버함수로는 좌측에서만 연산이 가능하므로)
cpy = 3 * pos;
이라는 식이, cpy = operator*(3, pos);
라고 해석이 되도록 연산자를 오버로딩 해야 한다.
Point operator*(int times, Point &ref){
Point pos(ref.xpos * times, ref.ypos * times);
return pos;
}
그런데 3 * pos
를 pos * 3
이 되도록 바꾸는 형태로 오버로딩을 해도 된다.
Point operator*(int times, Point &ref){
return ref*times;
}
그럼 이걸 반영해서 오버로딩을 해보자.
#include <iostream>
using namespace std;
class Point{
private:
int xpos, ypos;
public:
Point(int x=0, int y=0) :xpos(x), ypos(y){}
void ShowPosition() const{
cout << "[" << xpos << ", " << ypos << "]" << endl;
}
Point operator*(int times){
Point pos(xpos*times, ypos*times);
return pos;
}
friend Point operator*(int times, Point& ref);
};
Point operator*(int times, Point& ref){
return ref * times;
}
int main(){
Point pos(1, 2);
Point cpy;
cpy = pos*3;
cpy.ShowPosition();
cpy = 3 * pos;
cpy.ShowPosition();
cpy = pos*3*2;
cpy.ShowPosition();
cpy = 2*pos*3;
cpy.ShowPosition();
return 0;
}
이전 포스터에 전역함수로 연산자 오버로딩을 써야 할 때가 있다고 한 적이 있는데, 이 처럼 전역함수를 기반으로 하는 것도 익숙해 지는 것이 좋다.
cout, cin 그리고 endl
이제 연산자 오버로딩에 대해 많은 걸 알게 되었다. 따라서 C++ 콘솔 입출력에 대한 개념을 이해할 수 있게 되었는데, 예제만 제시를 해도 아마 이해하기 쉬울 것이다.
cout 과 endl 부터 이해해 보자.
#include <iostream>
namespace mystd{
using namespace std;
class ostream{
public:
void operator<< (char* str){
printf("%s", str);
}
void operator<<(char str){
printf("%c", str);
}
void operator<<(int num){
printf("%d", num);
}
void operator<<(double e){
printf("%g", e);
}
void operator<< (ostream& (* fp)(ostream &ostm)){ //endl 함수를 인자로 전달 - 함수포인터
fp(* this);
}
};
ostream& endl(ostream &ostm){
ostm<<"\n";
fflush(stdout);
return ostm;
}
ostream cout;
}
int main(){
using mystd::cout;
using mystd::endl;
cout << "Simple String";
cout << endl;
cout << 3.14;
cout << endl;
cout << 123;
endl(cout);
return 0;
}
이 구문이 어떻게 해석되는지 보면,
cout.operator<<("Simple String")
cout.operator<<(3.14)
cout.operator<<(123)
그리고 endl
에 있어서는, cout.operator<<(endl)
와 같이 해석된다.
즉, 이름공간 mystd 내에 선언된 함수 endl을 인자로 전달하면서 void operator<< (ostream& (* fp)(ostream &ostm))
를 호출한다.
그리고 endl(cout);
여기서는 인자로 cout을 전달하는데, 이렇게 가능하는지 한번 해보면 실제로 호출이 가능하다는 걸 알 수 있다.
그리고 이 외 cin
과 >>
연산자 역시 의미하는 바가 같다.
그런데 한가지 잘못된 부분이 있는데 그건 다음과 같다.
cout << 123 << endl << 3.14 << endl;
<<
연산자는 왼쪽에서 오른쪽으로 진행이 된다. 여기서는 다음과 같이 진행이 된다.
( ( ( ( cout << (123) ) << endl ) << 3.14 ) << endl );
즉, 모든 <<
연산의 결과로는 cout이 반환되야 하는데 그렇게 되지 못한다.
따라서 다음과 같이 바꿔보자.
#include <iostream>
namespace mystd{
using namespace std;
class ostream{
public:
ostream& operator<< (char* str){
printf("%s", str);
return * this;
}
ostream& operator<<(char str){
printf("%c", str);
return * this;
}
ostream& operator<<(int num){
printf("%d", num);
return * this;
}
ostream& operator<<(double e){
printf("%g", e);
return * this;
}
ostream& operator<< (ostream& (*fp)(ostream &ostm)){ //endl 함수를 인자로 전달 - 함수포인터
return fp(* this);
}
};
ostream& endl(ostream &ostm){
ostm<<"\n";
fflush(stdout);
return ostm;
}
ostream cout;
}
int main(){
using mystd::cout;
using mystd::endl;
cout << 3.14 << endl << 123 << endl;
return 0;
}
그럼 어느정도 표준 입출력이 이해가 될 것이다.
’«’, ‘»’ 연산자의 오버로딩
바로 위쪽에 <<
연산자에 대해 오버로딩 해 보았다.
그럼 앞선 Point 연산자에 대해 오버로딩을 해보자.
int main(){
Point pos(3, 4);
cout << pos << endl;
}
그런데 여기선 다음같은 사실을 알고 있어야 한다.
- cout은 ostream 클래스의 객체이다.
- ostream은 namespace std에 있고, 이를 사용하려면
iostream
을 포함해야 한다.
그럼 다음 구문을 관찰 해보자.
cout << pos
이것이 가능하기 위해선 <<
연산자가 오버로딩 되어 있어야 한다. 만약 멤버함수의 형태로 한다면,
cout.operator<<(pos)
전역함수의 형태로 오버로딩한다면,
operator<<(cout, pos)
그럼 어떤게 더 나은 방법일까? 멤버함수에 대한 방법을 선택하려면 cout 객체의 멤버함수를 하나 추가해야 하므로, ostream 클래스를 정정해야 한다. 그런데 불가능 하므로, 전역함수 방법을 택하는 수 밖에 없다.
#include <iostream>
using namespace std;
class Point{
private:
int xpos, ypos;
public:
Point(int x=0, int y=0) : xpos(x), ypos(y){}
void ShowPosition() const{
cout << '[' << xpos << ", " << ypos << ']' << endl;
}
friend ostream& operator<<(ostream&, const Point&);
};
ostream& operator<<(ostream& os, const Point& pos){
os << '[' << pos.xpos << ", " << pos.ypos << ']' << endl;
return os;
}
int main(){
Point pos1(1, 3);
cout << pos1;
Point pos2(101, 303);
cout << pos2;
return 0;
}
이제 다음은 >>
연산자를 해봐야 하는데, 이건 다음과 같이 주의사항만 알고 직접 해보도록 하자.
cin은 istream 클래스의 객체이다.
istream은 namespace std 안에 선언되어 있으며, 이의 사용을 위해서는 헤더파일 iostream을 포함해야 한다.
다음 포스팅엔 대입 연산자에 대해 알아보자.
계속 연산자 오버로딩을 알아볼 것인데, 조금 지루해도 꼭 알아야 하니 잘 이해하도록 하자.