jam 블로그

[C++] 010. 연산자 오버로딩 본문

개발 및 관련 자료/C

[C++] 010. 연산자 오버로딩

kid1412 2013. 5. 12. 20:16
728x90

I. 연산자를 오버로딩한다는 것은 어떤 의미인가?

  •  operator+라는 이름의 함수
  1. #include <iostream>
  2. using namespace std;
  3. class Point
    {
    private:
     int x,y;
    public:
     Point(int _x=0,int _y=0):x(_x),y(_y){}
     void ShowPosition();
     void operator+(int val);
    };
  4. void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
  5. void Point::operator+(int val)
    {
     x += val;
     y += val;
    }
  6. int main()
    {
     Point p(3,4);
     p.ShowPosition();
  7.  p.operator +(10);
     p.ShowPosition();
     return 0;
    }
  •  operator + 에 10을 전달하면 각각의 수에 10을 더해진다 여기서 조금 변형을 해본다.
  1. int main()
    {
     Point p(3,4);
     p.ShowPosition();
  2.  p+10;
     p.ShowPosition();
     return 0;
    }
  • 위의 소스를 보면 p+10이라고 했는데도 되었다.

    • 이유는 위에서 operator + 라고 지정을 해주었기 때문에 +를 쓰면 알아서 operator+로 해석을 한다.
    • 이것이 연산자 오버로딩이다.

 

II. 연산자를 오버로딩하는 두 가지 방법

  • 멤버 함수에 의한 연산자 오버로딩
  1. #include <iostream>
    using namespace std;
    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);
    };
    Point Point::operator +(const Point& p) cost
    {
     Point temp(x+p.x,y+p.y);
     return temp;
    }
    void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
  2. int main()
    {
     Point p1(1,2);
     Point p2(2,1);
     Point p3 = p1+p2;
     p3.ShowPosition();
     return 0;
    }
  •  Point Point::operator+(const Point& p) 이 구문을 보면 operator+ 함수를 Point 객체로 받고 있다. 단 성능 향상을 위해서 레퍼런스로 받고 있으며 전달 인자의 변경을 허용하지 않기 위해서 const를 썼다. 함수 내에서 멤버 변수의 조작을 할 필요가 없으므로 함수도 const를 했다. 안정성을 위한 것이다.

 

  • 전역 함수에 의한 오버로딩
  1. #include <iostream>
    using namespace std;
    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);
    };
    Point operator +(const Point& p1,const Point& p2)
    {
     Point temp(p1.x+p2.x,p1.y+p2.y);
     return temp;
    }
    void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
  2. int main()
    {
     Point p1(1,2);
     Point p2(2,1);
     Point p3 = p1+p2;
     p3.ShowPosition();
     return 0;
    }
  • 이번에는 +연산자를 오버로딩하면서 전역함수로 나타낸 것이다. friend는 private에 있는 변수에 접근하기 위에서 쓴 것이다.

    • 객체지향에는 전역이라는 개념이 존재하지 않기 때문에 가급적 멤버함수를 이용하는 것이 좋다. (간결해 진다고 한다.그러나 전역함수를 써야만 하는 경우도 있다.)

 

  • 오버로딩이 가능한 연산자의 종류

    • 오버로딩이 불가능한 연산

      • . .* :: ?: sizeof

 

  • 연산자 오버로딩에 있어서의 주의사항

    • 첫번째 : 본 의도를 벗어난 연산자 오버로딩은 좋지 않다.
    • 두번째 : 연산자 우선 순위와 결합성을 바꿀 수는 없다.

    • 세번째 : 디폴트 매개 변수 설정이 불가능하다.
    • 네번째 : 디폴트 연산자들의 기본 기능까지 빼앗을 수는 없다.

 

III. 단항 연산자의 오버로딩

  •  증가, 감소 연산자 오버로딩
  1.  #include <iostream>
    using namespace std;
  2. 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);
    };
  3. void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
    Point& Point::operator ++()
    {
     x++;
     y++;
     return *this;
    }
  4. Point& operator--(Point& p)
    {
     p.x--;
     p.y--;
     return p;
    }
  5. int main()
    {
     Point p(1,2);
     ++p;
     p.ShowPosition();
     --p;
     p.ShowPosition();
     ++(++p);
     p.ShowPosition();
     --(--p);
     p.ShowPosition();
  6.  return 0;
    }
  • *this가 의미하는 바는?

    • 자기 자신을 리턴하기 위해서
  • x와 y를 증가 시킨후에  자기자신을 리턴을 한 이유는?

    • ++p 연산 후에 그 다음 연산을 가능케 하기 위해서
  • 리턴 타입이 Point& 이유는?

    • 만약 리턴 타입이 Point 였다면 1만 증가 하게 된다. 이유는 ++p 연산에 의해서 리턴 되는 것은 p의 참조가 아닌  p의 복사본이기 때문이다.

       

  • 선 연산과 후 연산의 구분
  1. #include <iostream>
    using namespace std;
  2. 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);
    };
  3. void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
    Point& Point::operator ++()
    {
     x++;
     y++;
     return *this;
    }
  4. Point& operator--(Point& p)
    {
     p.x--;
     p.y--;
     return p;
    }
  5. int main()
    {
     Point p1(1,2);
     (p1++).ShowPosition();
     
     Point p2(1,2);
     (++p2).ShowPosition();
     return 0;
  6. }
  •  위의 소스의 결과값을 찍어보면 원래 나와야할 1,2가 아닌 2,3이 나와 버린다. 왜 그럴까?

    • 원래는 연산자의 위치에 따라 의미가 달라지는데 ++p와 p++가 동일하게 해석이 되어 버린다.
    • ++ 연산의 경우, 전위 증가와 후위 증가의 형태를 구분 짓기 어려워서, 후위 증가를 위한 함수를 오버로딩할 경우, 키워드 int를 매개변수로 선언을 하면 된다.
  1.  Point Point::operator ++(int)
    {
     Point temp(x,y);  // Point temp(*this)
     x++;                  // ++(*this)
     y++;                  //
     return temp;
    }
  • 위와 같이 정의를 하면 된다.

    •  Point temp(*this 이런 형태로 구현하는 것도 좋다. 이는 복사 생성자를 호출하는 형태로 객체를 복사하는 것이다.
    • 객체는 지역적으로 선언되어 있는 지역객체이다. 전역 객체가 아닌기 때문에 레퍼런스 형태로 리턴하면 안된다.

 

IV. 교환 법칙 해결하기

  •  교환 법칙의 적용

    • 앞에서 제시한  소스들에서 p+1은 되어도 1+p는 안되었다.
  1. #include <iostream>
    using namespace std;
  2. class Point
    {
    private:
     int x,y;
    public:
     Point(int _x =0, int _y=0):x(_x),y(_y){}
     void ShowPosition();
     Point operator+(int val);
     friend Point operator+(int val,Point& p);
    };
  3. void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
  4. Point Point::operator +(int val)
    {
     Point temp(x+val,y+val);
     return temp;
    }
  5. Point operator+(int val,Point& p)
    {
     return p+val;
    }
  6. int main()
    {
     Point p1(1,2);
     Point p2 = p1+3;
     p2.ShowPosition();
  7.  Point p3 = 3+p2;
     p3.ShowPosition();
  8.  return 0;
    }
  •   이런식으로 소스를 추가하면 p+1 이나 1+p와 같은 교환법칙이 성립하게 된다.

 

  • 임시 객체의 생성
  1.  #include <iostream>
    using namespace std;
  2. class AAA
    {
     char name[20];
    public:
     AAA(char* _name)
     {
      strcpy(name,_name);
      cout<<name<<" 객체 생성 "<<endl;
  3.  }
     ~AAA()
     {
      cout<<name<<"객체 소멸"<<endl;
     }
    };
  4. int main()
    {
     AAA aaa("aaaOBJ");
     cout<<" ---------------- 임시 --------------------"<<endl;
     AAA("temp");
     cout<<" ---------------- 임시 --------------------"<<endl;
     return 0;
    }
  •  실행을 해보면 임시 객체가 생성되었다가, 그 다음 줄로 넘어가면서 바로 소멸된다는 것을 보여주고 있다.

    • 이러한 임시 객체를 어디에 쓸까?
  1. Point Point::operator+(int val)         //Point Point::operator+(int val)
  2. {                                                //{
  3. Point temp(x+val,y+val);               // return Point(x+val,y+val);
  4. return temp;                                // }
  5. }
  • 원래 소스에서 주석쪽 소스로 바꾸면 (기능은 같다.) 주석쪽 소스가 임시 객체를 생성하자마자 바로리턴해 주고 있다.

 

V. cout, cin 그리고 endl의 비밀

  • cout, cin 그리고 endl의 구현 이해
  1. #include <stdio.h>
  2. namespace mystd
    {
     char* endl= "\n";
     class ostream
     {
     public:
      void operator<<(char*str)
      {
       printf("%s",str);
      }
      void operator<<(int i)
      {
       printf("%d",i);
      }
      void operator<<(double i)
      {
       printf("%e",i);
      }
     };
     ostream cout;
    }
  3. using namespace mystd;
  4. int main()
    {
     cout<<"Hello World\n";
     cout<<3.14;
     cout<<endl;
     cout<<1;
     cout<<endl;
     return 0;
    }
  •  위에서 만든 네임스페이스를 사용해서 출력이 잘된다.

    • 하지만 cout<<"hello"<<100<<3.12<<endl; 이렇게 쳐서 컴파일 하면 오류가 난다.
    • (((cout<<"hello")<<100) <<3.12)<<endl;이런 식으로 하면 잘 된다. 귀찮기 때문에 소스를 좀 변경해 보자.
  1.  ostream& operator<<(char*str)
      {
       printf("%s",str);
       return *this;
      }
      ostream& operator<<(int i)
      {
       printf("%d",i);
       return *this;
      }
      ostream& operator<<(double i)
      {
       printf("%e",i);
       return *this;
      }
  • 위에 처럼 연산자를 오버로딩하고 있는 operator<< 함수는 cout 객체를 반환하면 된다.

 

VI. 배열의 인덱스 연산자 오버로딩의 예

  • 기본 자료형 데이터를 저장할 수 있는 배열 클래스
  1.  #include <iostream>
  2. using namespace std;
  3. const int SIZE = 3;
  4. class Arr
    {
    private:
     int arr[SIZE];
     int idx;
    public:
     Arr():idx(0){}
     int GetElem(int i);
     void SetElen(int i,int elem);
     void AddElem(int elem);
     void ShowAllData();
  5. };
  6. int Arr::GetElem(int i)
    {
     return arr[i];
    }
  7. void Arr::SetElen(int i, int elem)
    {
     if (idx <=i)
     {
      cout<<"존재하지 않는 요소!"<<endl;
      return;
     }
     arr[i] = elem;
    }
    void Arr::AddElem(int elem)
    {
     if (idx>=SIZE)
     {
      cout<<" 용량 초과"<<endl;
      return ;
     }
     arr[idx++] = elem;
    }
    void Arr::ShowAllData()
    {
     for (int i=0;i<idx;i++)
     {
      cout<<"arrr ["<<i<<"] = "<<arr[i]<<endl;
     }
    }
  8. int main()
    {
     Arr arr;
     arr.AddElem(1);
     arr.AddElem(2);
     arr.AddElem(3);
     arr.ShowAllData();
  9.  arr.SetElen(0,10);
     arr.SetElen(1,20);
     arr.SetElen(2,30);
  10.  cout<<arr.GetElem(0)<<endl;
     cout<<arr.GetElem(1)<<endl;
     cout<<arr.GetElem(2)<<endl;
     return 0;
    }
  • 위에서 정의한 클래스가 보다 배열다워지려면 인덱스 연산자 [] 를 통한 참조가 가능해야 한다.
  1. int main()
    {
     Arr arr;
     arr.AddElem(1);
     arr.AddElem(2);
     arr.AddElem(3);
     arr.ShowAllData();
     arr[0]=10;
     arr[1]=20;
     arr[2]=30;
     for (int i;i<SIZE;i++)
     {
      cout<<arr[i]<<endl;
     }
     return 0;
    }
  • 위의 소스를 찍어 내려면
  1. int& Arr::operator[](int i)
    {
     return arr[i];
    }
  • 위의 함수를 추가해 주면 된다.

 

  • 객체를 저장할 수 있는 배열 클래스
  1.  #include <iostream>
    using namespace std;
  2. /******************* POINT Class*************************/
    class Point
    {
    private:
     int x,y;
    public:
     Point (int _x =0 , int _y=0):x(_x),y(_y){}
     friend ostream& operator<<(ostream& os, const Point& p);
    };
  3. ostream& operator<<(ostream& os, const Point& p)
    {
     os<<"["<<p.x<<" ," <<p.y<<"]";
     return os;
    }
  4. /****************** PointArr Class*********************/
  5. const int Size = 3;
    class PointArr
    {
    private:
     Point arr[Size];
     int idx;
    public:
     PointArr():idx(0){}
     void AddElem(const Point& elem);
     void ShowAllData();
     Point& operator[](int i);
    };
  6. void PointArr::AddElem(const Point &elem)
    {
     if (idx >= Size)
     {
      cout<<" 용량 초과"<<endl;
      return;
     }
     arr[idx++] = elem;
    }
    void PointArr::ShowAllData()
    {
     for (int i=0;i<idx;i++)
     {
      cout<<"arr["<<i<<"]="<<arr[i]<<endl;
     }
    }
    Point& PointArr::operator [](int i)
    {
     return arr[i];
    }
  7. int main()
    {
     PointArr arr;
     arr.AddElem(Point(1,1));
     arr.AddElem(Point(2,2));
     arr.AddElem(Point(3,3));
     arr.ShowAllData();
  8.  arr[0] = Point(10,10);
     arr[1] = Point(20,20);
     arr[2] = Point(30,30);
  9.  cout<<arr[0]<<endl;
     cout<<arr[1]<<endl;
     cout<<arr[2]<<endl;
    }
  • 메인에서 arr.AddElem(Point(1,1));줄에서 생성하는 Point 객체는 임시 객체의 형태는 띠고 있다.

 

VII. 반드시 해야만 하는 대입 연산자의 오버로딩

  •  오버로딩된 대입 연산자의 존재 : 디폴트 대입 연산자
  1. #include <iostream>
    using namespace std;
  2. class Point
    {
    private:
     int x,y;
    public:
     Point(int _x=0,int _y=0):x(_x),y(_y){}
     friend ostream& operator<<(ostream& os,const Point& p);
  3. };
  4. ostream& operator<<(ostream& os,const Point& p)
    {
     os<<"["<<p.x<<","<<p.y<<"]"<<endl;
     return os;
    }
  5. int main()
    {
     Point p1(1,3);
     Point p2(10,30);
     cout<<p1<<endl;
     cout<<p2<<endl;
     p1=p2;
     cout<<p1<<endl;
     return 0;
    }
  •  메인에서 보면 p1 = p2 이렇게 되어있는데 =연산자를 오버로딩하고 있는 함수는 보이지 않는다.

    • 디폴트 대입 연산자가 제공되기 때문이다. 멤버 변수 대 멤버 변수의 복사가 이뤄지고 있음알 수 있기 때문에 밑의 소스와 유사할 것이다.
    • 리턴 타입이 Point&형이고 자기자신을 리턴하는 이유는 p1=p2=p3 같은 연산을 허용하기 위해서이다

 

  • 디폴트 대입 연산자의 문제점
  1. #include <iostream>
    using namespace std;
  2. class Person
    {
    private:
     char* name;
    public:
     Person(char* _name);
     Person(const Person& p);
     ~Person();
     friend ostream& operator<<(ostream& os, const Person& p);
  3. };
    Person::Person(char* _name)
    {
     name = new char[strlen(_name)+1];
     strcpy(name,_name);
    }
    Person::Person(const Person &p)
    {
     name = new char[strlen(p.name)+1];
     strcpy(name,p.name);
    }
    Person::~Person()
    {
     delete[] name;
    }
  4. ostream& operator<<(ostream& os, const Person& p)
    {
     os<<p.name;
     return os;
    }
    int main()
    {
     Person p1("lee");
     Person p2("hone");
  5.  cout<<p1<<endl;
     cout<<p2<<endl;
     p1=p2;
     cout<<p1<<endl;
     return 0;
    }
  • 위의 소스를 컴파일후 실행 하면 실행중에 오류가 난다. 왜냐 하면 디폴트 대입 연산자 때문이다.

    • 메모리 공간을 동적 할당 했기 때문에 깊은 복사를 하도록 해야하는데 디폴트 대입 생성자는 얕은 복사를 하기 때문이다.
    • 또 하나의 문제는 메모리 유출에 관련이 있다.
  1.  Person& Person::operator=(const Person& p)
    {
     delete []name;
     name = new char[strlen(p.name)+1];
     strcpy(name,p.name);
     return *this;
    }
  • 위의 소스를 추가를 해주면 된다.

    • 생성자 내에서 메모리 공간을 동적 할당하게 되면, 할당된 메모리를 해제하는용도의 소멸자를 정의해야하며, 복사 생성자와 대입 연산자도 깊은 복사를 하도록 정의해야한다.
Comments