본문 바로가기
Basic/C/C++

[C++ 언어] 제 11 강 : 연산자 오버로딩(1)

by boxbop 2013. 1. 7.
반응형



 2013년 첫 포스팅이네요~ 새해 복 많이들 받으시구요

최고의 해를 보내기 위해 열심히 노력합시다~


 이번 시간에는 "연산자 오버로딩" 에 관하여 살펴보도록 하겠습니다.

함수만 오버로딩 되는 것이 아니라 C++에서는 연산자도 오버로딩 됩니다~


[operator+ 라는 함수]


#include <iostream>

using std::endl;

using std::cout;


class Point {

private :

    int x, y;

public:

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

    void ShowPosition();

    void operator+ (int val);

};


void Point::ShowPosition() {

    cout<<x<<" "<<y<<endl;

}

void Point::operator+(int val) {

x+=val;

y+=val;

}


int main(void)

{

Point p(3,4);

p.ShowPosition();


p.operator+(10);

p.ShowPosition();

return 0;

}


여러운건 특별히 없습니다. 실행 결과로는

 3  4

13 14

가 출력되겠죠~? 자 이제 중요합니다 위 의 예제에서 main 함수를 변경해보겠습니다.


int main(void)

{

Point p(3,4);

p.ShowPosition();


p+10;

p.ShowPosition();

return 0;

}


위의 예제로 변경해도 결과값은 똑같습니다.

p+10 이라는 부분이 이해가가 가나요~? + 연산자 왼쪽에는 Point 객체 p가, 오른쪽엔 int 형 데이터가 있습니다.

operator 라는 키워드와 연산자 기호를 사용하여 함수이름을 정의하면, 함수의 이름을 이용한 함수 호출뿐만 아니라

연산자를 이용한 함수 호출도 허용이 가능해집니다.


 즉, p+10이라는 문장은 p가 그냥 기본 자료형 변수라면 덧셈을 할겁니다. 하지만 p가 객체라면 p.operator+(10) 이라는

문장으로 해석하게 되는 겁니다.


  p+10 ------------------> p.operator+(10) 둘은 완전히 같은 문장이라고 보셔도 됩니다.


[연산자 오버로딩의 두가지 방법]


 첫 번째로는 맴버 함수에 의한 오버로딩, 두 번째는 전역 함수에 의한 오버로딩 입니다.


 1. 맴버 함수에 대한 연산자 오버로딩


class Point {

private:

int x,y;

public:

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

void ShowPosition();

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}


 위 예제는 x, y 좌표를 나타내기 위한 Point 클래스입니다. 우리는 이러한 Point 클래스 객체를 대상으로

+ 연산을 하기 원합니다. 다음 main 함수처럼 말이죠.


int main(void)

{

Point p1(1, 2);

Point p2(2, 1);

Point p3 = p1 + p2;

p3.ShowPosition();


return 0;

}


 main 함수를 보시면 아~ p3는 p1의 좌표와 p2의 좌표를 더한 값을 나타내는구나~ 라고 생각할 수 있습니다.

분명한 것은 operator+ 라는 이름의 함수를 호출하는 과정이 필요하다는 것 입니다. 그렇다면 operator+ 라는

이름의 함수는 어디에 있어야 할까요?


  operator+ 라는 이름의 함수는 맴버 함수로 존재할 수도, 전역 함수로도 존재할 수 있습니다. 지금은 맴버 함수로

존재하는 경우를 알아보고 있으니 다음과 같이 구현해 보도록 하겠습니다.


Point Point::operator+ (const Point& p) const

{

Point temp(x+p.x, y+p.y);

return temp;

}


 위의 코드는 인자로 Point 객체를 받고 있습니다. 성능 향상을 위해서 레퍼런스로 받고 있으며, 전달 인자의 변경을

허용하지 않기 위해서 const 선언을 동시에 해주고 있습니다. 또한 함수 내에서 맴버 변수의 조작을 할 필요가 없으므로

함수도 상수화를 하여 안정성을 높였죠.


class Point {

private:

int x,y;

public:

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

void ShowPosition();

Point operator+ (const Point& p);

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}

Point Point::operator+ (const Point& p) const

{

Point temp(x+p.x, y+p.y);

return temp;

}


int main(void)

{

Point p1(1, 2);

Point p2(2, 1);

Point p3 = p1+p2; // p1+p2 ----> p1.operator+(p2)

p3.ShowPosition();


return 0;

}


 Point p3 = p1 + p2   ------------> Point p3 = " 리턴 된 temp 객체" 


이정도면 충분히 이해가 가셨나요?


 2. 전역 함수에 대한 연산자 오버로딩


이번에는 위의 예제들을 전역 함수에 의한 오버로딩 방식으로 변경해서 공부해 보겠습니다.


class Point {

private:

int x, y;

public:

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

void ShowPosition();

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}


int main(void)

{

Point p1(1, 2);

Point p2(2, 1);

Point p3 = p1 + p2;

p3.ShowPosition();


return 0;

}


 자 여기서 한가지 짚고 넘어가야 될 부분이 있습니다.

p1 + p2 가 맴버 함수로 오버로딩 된 경우는 p1.operator+(p2)와 동일한 것은 알겁니다. 그렇다면

전역 함수로 오버로딩 된 경우는 어떨까요~? operator+ (p1, p2)가 될겁니다.  왼쪽에 있는 피연산자가

첫 번째 인자로, 오른쪽에 있는 피연산자가 두 번째 인자로 전달이 됩니다.


 다음은 + 연산자를 오버로딩하고 있는 전역 함수 입니다.


Point operator+ (const Point& p1, const Point& p2)

{

Point temp(p1.x+p2.x, p1.y+p2.y);

return temp;

};


 그러나 이상한점이 있지않나요~? Point 객체의 private 맴버에 직접 접근하고 있음을 볼 수 있습니다. 그런대

여기서 정의한 operator+ 함수는 전역 함수, 즉 Point 클래스의 외부라는 것 입니다. 따라서 private 영역에 접근이

불가능 하겠죠? 때문에 friend 선언을 이용해야 합니다.


class Point {

private:

int x, y;

public:

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

void ShowPosition();

friend Point operator+(const Point& p1, const Point& p2);

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}

Point operator+ (const Point& p1, const Point& p2)

{

Point temp(p1.x + p2.x, p1.y + p2.y);

return temp;

}


int main(void)

{

Point p1(1, 2);

Point p2(2, 1);

Point p3 = p1 + p2;

p3.ShowPosition();


return 0;

}


 자 이렇게 두가지 방법을 알아보았습니다. 그렇다면 어떤 방법을 사용하는 것이 좋을까요?

사실 객체지향에는 전역이라는 개념이 존재하지 않습니다. 때문에 맴버 함수를 이용하는 방법이

좀 더 좋은 방법이라고 말할 수 있겠네요. 


[연산자 오버로딩의 주의사항]


 1. 의도를 벗어난 연산자 오버로딩은 피하자!

ex) p+3 라는 문장을 보면 p 객체의 맴버 x, y 에 3 을 더해서 새로운 객체를 생성해서 리턴하라는 것인지,

      p객체의 맴버 x, y 자체가 3씩 증가하겠는가? 

 2. 연산자 우선 순위와 결합성을 바꿀 수는 없다.

 3. 디폴트 매개 변수 설정이 불가능하다.

 4. 디폴트 연산자들의 기본 기능까지 빼앗을 수 없다.

ex)

int operaotor+(int a, int b)

{
    return a+b+3;

}

-->int 형 변수의 + 연산은 이미 그 법칙이 정해져 있습니다. 이처럼 디폴트 연산자들이 지니고 있는 기능을 변경하는

형태로는 연산자를 오버로딩 시킬 수 없습니다.


[단항 연산자의 오버로딩]


 1. 증가, 감소 연산자 오버로딩


 대표적인 단항 연산자로 증가 및 감소 연산자(++,--)가 있습니다. 단항연산자 역시 두가지 형태(맴버함수, 전역함수)로 오버로딩이 가능합니다.


++p   --->  1. p.operator++() : 맴버 함수로 오버로딩 된 경우

                2. operator++(p)  : 전역 함수로 오버로딩 된 경우


다음은 Point 클래스를 대상으로 ++, -- 연산자를 오버로딩한 예제입니다. 단, ++연산자는 맴버함수에 의한 방법이고 --연산자는 전역 함수에 의한 방법을 사용했습니다.


#include <iostream>

using std::endl;

using std::cout;


class Point {

private:

int x, y;

public:

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

void ShowPosition();

Point& operator++();

friend Point& operator--(Point& p);

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}


Point& Point::operator++() {

x++;

y++;

return *this;

}


Point& operator--(Point& p) {

p.x--;

p.y--;

return p;

}


int main(void)

{

Point p(1, 2);

++p;

p.ShowPosition();  // 2 3


--p;

p.ShowPosition();  // 1 2


++(++p);

p.ShowPosition(); // 3 4


--(--p);

p.ShowPosition();  // 1 2


return 0;

}


 이해가 가셨을꺼라 생각됩니다. 자 여기서 중요한 부분을 한가지 살펴보고 넘어가도록 하겠습니다.

세 번째 볼드 처리된 문장 보이시나요? return *this 부분입니다. *this 가 의미하는 것은 무엇일까요?

this는 객체 자신을 가리키는 포인터입니다. 여기에 * 연산을 하게 되면 포인터가 가리키는 대상을 참조하겠다는

뜻이 됩니다. 즉, 자기 자신을 리턴 하겠다는 의미를 가지고 있습니다

 그렇다면 왜 자기 자신을 리턴할까요?

맴버 변수 x, y 값을 증가시켜주는 걸로 충분하지 않을까요? 만약에 아무것도 리턴하지 않는다면 ++p 연산 후에는

아무것도 존재하지 않으므로 그 다음 연산은 불가능하게 되어 컴파일 오류를 발생시킵니다.

 리턴 타입이 Point가 아니라 Point&인 이유는 무엇일까요?

위의 예제에서 리턴 타입이 Point&가 아니라 Point라고 한다면 ++(++p) 는 값이 1만 증가할 것 입니다. ++p 연산에

의해서 리턴되는 것은 p의 참조가 아닌, p의 복사본이기 때문입니다.

==> [참조를 리턴하는 경우] : ++(++p) -> ++(p의 참조) -> p의 값 2 증가

      [복사본을 리턴하는 경우] : ++(++p) -> ++(p의 복사본) -> p의값 1 증가, p의 복사본 값 1증가


2. 선 연산과 후 연산의 구분


 예제는 위의 예제와 동일하다는 가정하에 살펴보겠습니다.


int main(void)

{

Point p1(1, 2);

(p1++).ShowPosition();


Point p2(1, 2);

(++p2).ShowPosition();

return 0;

}


 여기서 예상되는 결과 값이 있나요~? 그러나 실행 결과는 다음과 같이 나옵니다.

2 3

2 3

으로 말이죠. 그렇다면 전위 연산자와 후위 연산자가 구분이 안된다는 결론이 나옵니다.

즉, ++p 와  p++ 와는 별다를게 없이 동일하게 해석된다는 문제가 있습니다.


 그러나 c++은 다음과 같은 방법을 제안합니다.

"++ 연산의 경우, 전위 증가와 후위 증가의 형태를 구분 짓기 어려우므로, 후위 증가를 위한 함수를

오버로딩할 경우, 키워드 int를 매개변수로 선언하면 그것이 후위 증가를 의미하는 것으로 간주합니다."


 즉, ++p -> p.operator++();

      p++ -> p.operator++(int); 가 되는겁니다.


 자 이제 우리가 원하는 형태의 결과를 얻을 수 있도록 연산자를 오버로딩하는 것 입니다. 우선 main 함수와

원하는 형태의 출력 결과를 살펴보겠습니다.


int main(void)

{

Point p1(1, 2);

(p1++).ShowPosition(); //1 2 출력

p1.ShowPosition(); // 2 3 출력


Point p2(1, 2);

(++p2).ShowPosition(); // 2 3 출력

return 0;

}


#include <iostream>

using std::endl;

using std::cout;


class Point {

private :

int x, y;

public:

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

void ShowPosition();

Point& operator++();

Point operator++(int);

};


void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}


Point& Point::operator++() {

x++;

y++;

return *this;

}

Point Point::operator++(int) {

Point temp(x, y);

x++;

y++;

return temp;

}


 후위 연산자를 오버로딩하기 위한 함수를 살펴봅시다. temp 객체가 사용되었습니다. 이는 값을 증가하기 전의 상태를 보존하기 위해서  임시 객체를 생성한 것 입니다. temp를 리턴하므로 값이 증가하기 전의 복사해 놓은 객체를 내놓겠죠? temp는 맴버 함수 내에서 정의된 지역 객체기 때문에 당연히 리턴형태는 레퍼런스가 아닌 Point 형으로 지정해주었습니다.


 이번 강의는 여기까지입니다. 아직 더 남아있지만 내용이 다소 길어질 것 같아 2회로 나누어서 올리겠습니다.

수고하셨습니다^-^



반응형