2016년 3월 15일 화요일

[c++] 연산자 오버로딩 (Operator Overloading)

연산자 오버로딩(Operator Overloading)은 C++ 문법과 우리와의 약속이라고 할 수 있는데, 우리가 함수를 정의하는데 이어서 operator라는 키워드와 연산자를 붙여서 함수의 이름을 만들 경우에 (operator + 이렇게 ) 우리가 일반적인 함수를 호출 방법 말고도, 연산자만을 이용해서도 함수가 호출 되도록 해주겠다는 이야기이다.

1. 멤버 함수에 의한 오버로딩 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Point {  
  5. private:  
  6.     int x, y;  
  7. public:  
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}  
  9.     void ShowPosition();  
  10.     Point operator+(const Point& p);  
  11. };  
  12. void Point::ShowPosition(){   
  13.     cout<<x<<" "<<y<<endl;   
  14. }  
  15. Point Point::operator+(const Point& p){  
  16.     Point temp(x+p.x, y+p.y);  
  17.     return temp;  
  18. }  
  19.   
  20. int main(void)  
  21. {  
  22.     Point p1(1, 2);  
  23.     Point p2(2, 1);  
  24.     Point p3=p1+p2;  
  25.     p3.ShowPosition();  
  26.   
  27.     return 0;  
  28. }  
 그럼 위의 소스 코드 24번째 줄의 p1+p2:는 어떤의미를 갖는지 알아 보자. 여기서 p1 , p2는 객체 이므로 기본적으로 '+' 덧셈 연산이 불가능하다. 그래서 C++은 어떤 약속을 하였느냐 하면, '+' 라는 사칙연산을 할려고 하는게 아니라,  앞에 operator 라는 키워드를 붙여서 p1과 p2를 이용해서 operator +라는 함수를 호출해주게 되는 것이다. 
 그래서 이제 C++은 'operator +' 라는 함수를 호출 해야 하는데, C++에서는 함수를 만드는 방법 멤버 함수, 전역함수 두가지가 있다. 그러므로 'operator +'는 이 둘로 정의 될 수 있는 것이다. 우리가 멤버로 만들건, 전역으로 만들건 C++은 'operator +'에 대해 알아서 처리해줄 수 있다. 그럼 멤호 함수에 의한 오버로딩은 어떻게 일어 나는지 알아 보자. 
 24번째 라인의 p1 + p2;  이것은 "이항 연산자의 왼쪽에 오는 객체의 operator + 함수를 호출 하면서, 이항연산자의 오른쪽의 피연산자를 인자로 전달한다" 라는 의미가 된다. 즉 
  1. p1.operator +(p2);   
이렇게 바뀐다는 말이다. 
  덧셈 연산이 끝나면 p1+p2 자리에 temp 라는 이름의 객체의 복사본이 리턴되서 들어 오게 된다. 그리고 리턴된 값은 p3를 초기화 해준다. 여기서 temp 객체는 포인터 클래스의 객체 이므로, 이 경우의 복사 생성자가 호출 되는 것도 알 수 있다. temp 함수의 연산에 의해서 아래와 같이 결과가 나오는 것도 알 수 있다. 



2. 전역 함수에 의한 오버로딩  
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Point {  
  5. private:  
  6.     int x, y;  
  7. public:  
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}  
  9.     void ShowPosition();  
  10.     friend Point operator+(const Point& p1, const Point& p2);  
  11. };  
  12. void Point::ShowPosition(){   
  13.     cout<<x<<" "<<y<<endl;   
  14. }  
  15.   
  16. Point operator+(const Point& p1, const Point& p2) //전역함수다  
  17. {  
  18.     Point temp(p1.x+p2.x, p1.y+p2.y);  
  19.     return temp;  
  20. }  
  21.   
  22. int main(void)  
  23. {  
  24.     Point p1(1, 2);  
  25.     Point p2(2, 1);  
  26.     Point p3=p1+p2;  
  27.     p3.ShowPosition();  
  28.   
  29.     return 0;  
  30. }  
 위의 소스코드는 전역함수를 friend 선언해주고있다. friend 선언을 해줌으로서,  operator+ 함수를 포인트 클래스 객체의 private 멤버에 직접 접근이 가능하다. (friend는 연산자 오버로딩에 주로 사용된다.)
 그럼 전역함수에서는  p1 + p2; 가  C++의 약속에 의해 어떤 식으로 해석이 되는 것일까? 바로 아래와 같이 해석된다. 
  1. operator+ (p1, p2);  
 전역함수는 함수 이름만 있어도 호출이 가능하므로, 위와 같은식으로 해석되는 것을 알 수 있다. 여기까지 멤버함수, 전역 함수에 의한 오버로딩을 알아 봤다. 


3. 증가 감소 연산자 오버딩 
 이전에는 사칙연산인 이항연산자 오버로딩에 대해서 알아 봤는데 이번에는 단항 연산자인 증가(++), 감소(--) 연산자 오버로딩에 대해서 알아 보자. 만약 P객체가 증가가 된다면 멤버함수와 전역함수에서는 C++의 약속에 의해 어떻게 표현 될까? 이전의 이항 연산자 오버로딩때와 마찬가지로 아래와 같이 됨을 알 수 있을 것이다. 
  1. p.operator++ ( ) //멤버  
  2. operator ++(p)   //전역  
그럼 단항 연산자 오버로딩의 예제 소스코드를 한번 살펴보자. 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Point {  
  5. private:  
  6.     int x, y;  
  7. public:  
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}  
  9.     void ShowPosition();  
  10.     Point& operator++(); //멤버 함수  
  11.     friend Point& operator--(Point& p);   
  12. };  
  13. void Point::ShowPosition(){   
  14.     cout<<x<<" "<<y<<endl;   
  15. }  
  16.   
  17. Point& Point::operator++() //멤버함수 정의   
  18. {  
  19.     x++;  
  20.     y++;  
  21.     return *this;  
  22. }  
  23.   
  24. Point& operator--(Point& p) //전역함수  
  25. {  
  26.     p.x--;  
  27.     p.y--;  
  28.     return p;  
  29. }  
  30.   
  31. int main(void)  
  32. {  
  33.     Point p(1, 2);  
  34.     ++p;   
  35.     p.ShowPosition();    
  36.   
  37.     --p;   
  38.     p.ShowPosition();    
  39.   
  40.     ++(++p);  
  41.     p.ShowPosition();    
  42.   
  43.     --(--p);  
  44.     p.ShowPosition();    
  45.   
  46.     return 0;  
  47. }  
 여기에서 눈여겨 봐야 될 것은, 24번째줄에 operator++ 멤버 함수는 리턴 타입이 포인터의 참조를 리턴 하고 있는것을 알 수 있다. 물론 전역도 마찬가지로 참조를 리턴해주고 있다. 그럼 왜 참조형을 리턴해주는 것일까? 그것은 바로 40번째 라인과 같은 ++(++p) 형태의 연산을 허용해 주기 위해서이다.  ++(++p)는 어떤 의미를 갖는가? 
  1. void main()  
  2. {  
  3.     int n;  
  4.     ++(++n);  
  5. }  
 여기 n이라는 변수가 있다. ++(++n) 연산을 하면, ( )괄호 연산에서 n은 2에서 3의 값이 되고 이 3이 된 n을 ++( n)  이와 같이 증가 시켜 줘서 결국 4란 값이 되는것이다. 여기서 알 수 있는 것은 괄호 연산 앞의 증가 연산은 어디에 영향을 미치느냐 하면은, 바로 변수 n에 영향을 미치기 때문에 이런 증가 연산이 성립이 되는 것이다. 결국 변수 or 객체를 기준으로 증가 연산을 하는 것이다. 

 따라서 ++(++p) 연산에서도 괄호 안의 연산인 p.operator++() 함수 호출이 끝나고 나서 이 자리에 p가 그대로 리턴이 돼야 이런 증가 연산이 제대로 이루어 질 수 있다. 
  1. Point& Point::operator++() //멤버함수 정의   
  2. {  
  3.     x++;  
  4.     y++;  
  5.     return *this;  
  6. }  
 멤버 함수 정의부를 보면 return 타입이 *this인 것을 알 수 있다. 여기서의 this는 Point 객체의 포인터를 의미하는데, *를 붙였으니까 Point객체 자신을 의미한다. 그럼 나 자신을 무엇으로 리턴 하느냐? 바로 Point& 즉, 참조로 리턴을 하는 것이다. 
  괄호 안은 p.operatr++()이 되고 이 연산이 끝나고 p의 참조를 리턴하는데 p의 참조는 p와 같다. ++(p참조) -> p참조 . operator++() 이 것이 된다. p참조는 p자신 이므로, p.operator++() 이 되는 것과 같다. 그래서 결과가 ' 3 4 '로  연산이 제대로 되는 것이다. 

 만약 참조로 리턴 안하고 Point로 리턴 한다면 어떻게 될까? 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Point {  
  5. private:  
  6.     int x, y;  
  7. public:  
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}  
  9.     void ShowPosition();  
  10.     Point operator++(); //멤버 함수  
  11.     friend Point operator--(Point& p);   
  12. };  
  13. void Point::ShowPosition(){   
  14.     cout<<x<<" "<<y<<endl;   
  15. }  
  16.   
  17. Point Point::operator++() //멤버함수 정의  
  18. {  
  19.     x++;  
  20.     y++;  
  21.     return *this;  
  22. }  
  23.   
  24. Point operator--(Point& p) //전역함수  
  25. {  
  26.     p.x--;  
  27.     p.y--;  
  28.     return p;  
  29. }  
  30.   
  31. int main(void)  
  32. {  
  33.     Point p(1, 2);  
  34.     ++p;   
  35.     p.ShowPosition();    
  36.   
  37.     --p;   
  38.     p.ShowPosition();    
  39.   
  40.     ++(++p);  
  41.     p.ShowPosition();    
  42.   
  43.     --(--p);  
  44.     p.ShowPosition();    
  45.   
  46.     return 0;  
  47. }  
 그냥 Point를 리턴 한다면, 나 자신을 복사 해서 리턴 하는것이다. 그래서 ++(++p) 에서 괄호 안의 연산후에 이 자리에 오는 것은 p가 아니라 p객체의 복사본이 리턴되는것이다. 그리고 p객체의 복사본을 가지고 ++연산을 하게 되는것이다. 결과적으로 리턴하는 순간에 새로운 객체를 만들어서 리턴하는 꼴이 되므로 p객체는 단지 한번만 증가 하게 되는 것이다. (이것이 문제가 된다)


4. 선연산과 후연산의 구분 
 단항 연산자 ++, --를 변수나 객체의 앞에 붙이느냐 뒤에 붙이느냐에 따라 증가를 먼저 하고 연산을 할것인가, 연산을 먼저 한 후, 값을 증가 시킬 것인지 나뉘게 된다. 
 - ++ p : 변수의 값을 먼저 증가 시킨후 연산
 - p++ : 연산후 값 증가
 우리가 단항 연산자 오버로딩을 설계할 때, 이처럼 문법상 기본 자료형이 하는 일을 따라 간다면 우리는 선연산 후연산이 다르게 동작 되어야 하는것을 알 수 있는 것이다. 그래서 우리는 호출 되는 함수의 구분을 위해 C++은 또 하나의 약속을 해야 하는 것이다.  다음과 같이 말이다. 
 - ++p 은 p.operator ++( );
 - p++ 은 p.operator ++(Data Type);

 여기서 선언된 데이터 타입은(int, double 등등의...)  ++ 연산을 구분 지어 주기 위해서만 의미를 가진다. 예제 소스를 보자.

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Point {  
  5. private:  
  6.     int x, y;  
  7. public:  
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}   
  9.     void ShowPosition();   
  10.     Point& operator++(); //++p  
  11.     Point operator++(int); // p++  
  12. };  
  13. void Point::ShowPosition(){   
  14.     cout<<x<<" "<<y<<endl;   
  15. }  
  16.   
  17. Point& Point::operator++(){  
  18.     x++;  
  19.     y++;  
  20.     return *this;  
  21. }  
  22. Point Point::operator++(int){  
  23.     Point temp(x, y);  // Point temp(*this);  
  24.     x++;  
  25.     y++;  
  26.     return temp;  
  27. }  
  28.   
  29.   
  30. int main(void)  
  31. {  
  32.     Point p1(1, 2);   
  33.     (p1++).ShowPosition();   
  34.     p1.ShowPosition();    
  35.   
  36.     Point p2(1, 2);  
  37.     (++p2).ShowPosition();   
  38.     return 0;  
  39. }  
 결가 값을 보면 (p1++).ShowPosition(); 이 부분에 1 2 값을 출력하는 것을 알 수 있다. p1객체 뒤에 증가 연산자가 왔으므로, 이것은 후 증가 의미를 가지기 때문이다. 그후 p1을 출력해보면, 후 증가의 값을 눈으로 확인해볼 수 있는 것이다. 

 그럼 증가 하기 이전의 값을 얻기 위한 함수는 어떻게 만들까? 그 함수는 바로 이 부분이다. 
  1. Point Point::operator++(int){  
  2.     Point temp(x, y);  // Point temp(*this);  
  3.     x++;  
  4.     y++;  
  5.     return temp;  
  6. }  




증가하기 이전에 값을 만든 다음에 (temp) 그 다음에 값을 증가 시키고, 

리턴 할때는 증가 하기 이전에 객체를 이전하면 된다. 이 함수의 경우에는 참조로 리턴이 안됐는데, 이 함수는 


참조로 리턴 할 수 없다. 
 왜냐? 참조로 리턴(return) 할 수 없는것이 무엇인가? 바로 지역 변수, 지역객체 이다. 여기서 temp는 


지역적으로 선언된것이므로 참조로 리턴 될 수 없다. 만약 리턴 된다고 해도 리턴 되고 나서 이 리턴값은 바로 사라지기 때문이다. 이게 바로 연산자 오버로딩의 한계점이라고 할 수 있다.


5. 교환 법칙 
 3 + 2 라는 연산이 있다. 여기에서 피연산자 위치를 다르게 바꿔도 ( 2 + 3 ) 동일한 결과를 가져오게 하는 법칙이 바로 교환법칙이다. 우리는 연산자 오버로딩을 사용함에 있어서 이렇게 교환법칙도 성립할 수 있게 만들어야 한다. 아래의 연산자 오버로딩을 보자.
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Point {  
  5. private:  
  6.     int x, y;  
  7. public:  
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}  
  9.     void ShowPosition();  
  10.     Point operator+(int val); //operator+라는 이름의 함수  
  11. };  
  12. void Point::ShowPosition() {   
  13.     cout<<x<<" "<<y<<endl;   
  14. }  
  15. Point Point::operator+(int val) {  
  16.     Point temp(x+val, y+val);  
  17.     return temp;  
  18. }  
  19.   
  20. int main(void)  
  21. {  
  22.     Point p1(1, 2);  
  23.     Point p2=p1+3;   
  24.     p2.ShowPosition();  
  25.   
  26.     return 0;  
  27. }  
 23번째 라인의 Point p2=p1+3; 이 문장은 C++에서는 p1.operator+( 3 ) 이와 같이 인식되는것을 우리는 앞서 알아봤었다. 교환법칙이 성립되려고 하면 다음과 같이 써도 에러가 나지 않아야 한다. 
  1. Point p3 = 3 + p1;   
 하지만 실제로 저 부분을 소스코드에 집어 넣고 컴파일 해보면 에러가 생기는 것을 알 수 있다. 
(Visual Studio 2010에서는 컴파일 하지 않아도 에러를 실시간으로 잡아내는 기능이 추가 되었다)

 그럼 왜 이런 교환법칙이 성립이 안되는 것일까? 자세히 한번 알아보자. 
  1. Point p3 = 3 + p1;  
 여기에서 숫자 3은 int형 데이터이고,  p1은 객체이다 타입이 다르기 때문에 덧셈이 불가능 하다. 그래서 오버로딩 되어 있는 operator +함수를 호출 하게 된다. 이 operator + 함수를 호출 하기 전에 이것이 멤버 함수인지 전역함수 인지 일단 알아 볼텐데, 우리는 멤버 함수로서 정의을 했으므로, 멤버 함수에서의 객체는 무조건 왼쪽이 기준으로 operator+ 함수를 호출하므로 3.operator+ (p1) C++에서는 이러한 문장으로 인식이 될 것이다. (멤버 함수로의 오버로딩시 무조건 연산자의 왼쪽이 피연산자의 기준이 된다.)
 여기서 바로 문제가 생긴다. 숫자 3은 int형 데이터로 operator+라는 함수가 멤버로서 존재할리 없다. 이 문장은 논리적 으로 문제가 있는 것이다. 그러면 우리는 교환법칙이 성립 되도록 추가적인 코드를 만들어야 한다.

 : 우리는 멤버 함수 오버로딩으로는 문제가 있다는 것을 알았다. 그러면 전역 함수 오버로딩시에는 어떻게 될까? Point p3 = 3 + p1; 이 문장은 전역함수에서 인식될때 왼쪽에 있는 피연산자가 첫번째 인자로, 오른쪽에 피연산자가 두번째 인자로 가기 때문에 operator+ (3, p);  로 C++에서 인식되고 이것은 논리적으로 문제가 전혀 없다. 결론이 나왔다. 우리는 교환법칙을 성립 해주기 위해서 전역함수 오버로딩을 제공해주기만 하면 되는것이다. 바로 아래와 같이 말이다. 결과도 이상없이 출력되는 것을 알 수 있다.
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Point {  
  5. private:  
  6.     int x, y;  
  7. public:  
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}  
  9.     void ShowPosition();  
  10.     Point operator+(int val); //operator+라는 이름의 함수  
  11.     friend Point operator+(int val, const Point& p);   
  12. };  
  13. void Point::ShowPosition() {   
  14.     cout<<x<<" "<<y<<endl;   
  15. }  
  16. Point Point::operator+(int val) {  
  17.     Point temp(x+val, y+val);  
  18.     return temp;  
  19. }  
  20.   
  21. Point operator+(int val, Point& p)  
  22. {  
  23.     return p+val;  
  24. }  
  25.   
  26. int main(void)  
  27. {  
  28.     Point p1(1, 2);  
  29.     Point p2=p1+3;  
  30.     p2.ShowPosition();  
  31.   
  32.     Point p3=3+p2;  
  33.     p3.ShowPosition();  
  34.   
  35.     return 0;  
  36. }  


6. 쉬프트 연산자 ( <<, >> ) 오버로딩 

쉬프트 연산자란, 우리들이 흔히 쓰는 cout, cin, endl 키워드를 쓸때 앞이나 뒤에 붙이는 연산자이다. 일반적으로 표준 라이브러리에 std라는 namespace에는 cout이나 cin에 관련된 클래스들이 있다. 그것이 바로 istream과 ostream이다. 이런 namespace를 일일이 입력하기 싫어서 using namespace 라는 간편한 문법을 쓰기도 한다. C++에서는 이런 쉬프트 연산자도 연산자 오버로딩이 가능하다.
  1. int main(void)  
  2. {  
  3.     Point p(1, 3);  
  4.     cout<<p;  
  5.   
  6.     return 0;  
  7. }  
 메인함수를 보자. 어떤 Point 클래스가 있고, 이 Point 클래스를 p라는 객체를 통해 생성하고 있고, 이 객체를 cout을 이용해 출력하려는 문장이다. 하지만 기본적으로 cout은 기본자료형만 출력을 하지 객체인 사용자 타입의 자료형은 출력을 할 수가 없다. 4번째 줄을 멤버 함수로의 오버로딩 해석에 의해 풀어 써 보자. 
 cout.operator<< (p) 즉, p객체를 인자값을 받을 수 있도록 cout 객체에는 operator<< 연산자가 오버로딩 되어 있어야 하는데, 표준 std의 ostream 클래스에서도 쉬프트 연산에 대한 오버로딩이 정의 되어 있지 않다. 그래서 우리가 표준 라이브러리를 건드리면서까지 객체를 출력하는 일은 불가능 할 것이다. 
 이로서 멤버 함수로의 오버로딩으로는 객체를 출력할 수 없다는 것을 알 수 있었다. 그럼 전역 함수의 오버로딩을 보자.전역함수로의 오버로딩에서는 cout << p 문장은 operator (cout, p); 로 풀어 써지고, 이것은 논리적으로 문제가 없다. 그럼 전역함수로 오버로딩으로 다음과 같이 함수를 정의 할 수 있겠다. 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Point {  
  5. private:  
  6.     int x, y;  
  7. public:  
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}  
  9.     friend ostream& operator<<(ostream& os, const Point& p);  
  10. };  
  11.   
  12. ostream& operator<<(ostream& os, const Point& p)  
  13. {  
  14.     os<<"["<<p.x<<", "<<p.y<<"]"<<endl;   
  15.     return os;  
  16. }  
  17.   
  18. int main(void)  
  19. {  
  20.     Point p(1, 3);  
  21.     cout<<p;  
  22.   
  23.     return 0;  
  24. }  


7. 대입 연산자 오버로딩 
 위와 같은 문장이 있다. 여기서 b는 A클래스의 b객체이다. 이 문장은 다음과 같이 묵시적으로 변환이 일어난다. A a(b); 결국 a라는 이름으로 객체 생성할때 복사 생성자를 호출 하는 형태가 된다. 그럼 다음과 같은 문장은 어떤 일이 일어날까?
  1. A a;  
  2. B b;  
  3. a = b;  
 여기서 a = b; 이 문장은 a(b) 이런 형태로 묵시적 변환이 일어 나지 않는다. 위와 같은 대입연산을 썼지만, 객체 생성문장이 아니기 때문이다. 그럼 여기서 a = b;는 다음과 같이 해석 될 것이다. a.operator=(b); 복사생성에서 호출되는 대입연산과 오버로딩 되어 있는 operator 함수 호출에서의 대입연산을 혼동하지 말자

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Point {  
  5. private:  
  6.     int x, y;  
  7. public:  
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}  
  9.     friend ostream& operator<<(ostream& os, const Point& p);  
  10.     Point& operator = (const Point &p)  
  11.     {  
  12.         x = p.x;  
  13.         y = p.y;  
  14.         return *this;  
  15.     }  
  16. };  
  17.   
  18. ostream& operator<<(ostream& os, const Point& p)  
  19. {  
  20.     os<<"["<<p.x<<", "<<p.y<<"]";   
  21.     return os;  
  22. }  
  23.   
  24. void main()  
  25. {  
  26.     Point p1(1, 3);  
  27.     Point p2(10, 30);  
  28.     cout<<p1<<endl;  
  29.     cout<<p2<<endl;  
  30.   
  31.     p1=p2;   
  32.     cout<<p1<<endl;  
  33. }  

  위에서 말한것과 같이, 31번째 라인에서는 a.operator=(b); 이런 형태로 대입연산 오버로딩이 일어날 것입니다. 하지만 여기 소스코드에서는 어디에서도 대입 연산을 정의 하는 함수를 찾아 볼 수 없다. 그것은 바로 대엽 연산자 오버로딩에 대해서만 "디폴트 대입 연산자"가 존재하기 때문이다. 그래서 우리가 명시적으로 써주지 않아도 이 디폴트 대입 연산자를 호출해 문제가 발생하지 않는 것이다. 
 만약 우리가 정의 한다고 하면 아래와 같이 정의 할 수 있겠다. 
  1. Point& operator = (const Point & p);  
  2. {  
  3.     x = p.x; y = p.y;  
  4.     return *this;  
  5. }  
 왜 참조형으로 리턴을 하냐면, 대입 연산도 p1 = p2 = p3; 이런 형태의 대입연산도 쓰기 때문에, 이것을 감안해서 참조형으로 리턴을 해줘야 한다. 그럼 이렇게 디폴트 대입 연산자가 있는데 굳이 우리가 정의를 해서 쓸 일이 있을까? 반드시 정의를 해서 써야 하는 경우가 있는데, 그 경우가 바로 깊은 복사(Deep Copy)가 요구 되어 지는 경우이다. 이와 같이 디폴트 대입 연산자의 문제점을 알아 볼 수 있다. 

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Person {  
  5. private:  
  6.     char* name;  
  7. public:  
  8.     Person(char* _name);  
  9.     Person(const Person& p);  
  10.     ~Person();  
  11.     friend ostream& operator<<(ostream& os, const Person& p);  
  12. };  
  13.   
  14. Person::Person(char* _name){  
  15.     name= new char[strlen(_name)+1];  
  16.     strcpy(name, _name);  
  17. }  
  18. Person::Person(const Person& p){  
  19.     name= new char[strlen(p.name)+1];  
  20.     strcpy(name, p.name);  
  21. }  
  22. Person::~Person(){  
  23.     delete[] name;  
  24. }  
  25.   
  26. ostream& operator<<(ostream& os, const Person& p)  
  27. {  
  28.     os<<p.name;  
  29.     return os;  
  30. }  
  31.   
  32. int main()  
  33. {  
  34.     Person p1("One");  
  35.     Person p2("Two");     
  36.   
  37.     cout<<p1<<endl;  
  38.     cout<<p2<<endl;  
  39.   
  40.     p1=p2;  //error  
  41.   
  42.     cout<<p1<<endl;  
  43.   
  44.     return 0;  
  45. }  
 위의 소스 코드를 실행해 보면 컴파일에는 문제가 없지만, 런타임에서 문제가 생긴다. 그럼 이런 문제는 왜 일어 나는 것일까?
 여기서는 Person 클래스의 p1과 p2 객체를 생성하는데 p1과 p2는 객채 내에 name이라는 포인터가 있어서 문자열의 길이 만큼 동적 할당을하고 이 저장한 공간을  p1 객체가 포인터를 유지하는 형태를 띠게 되는데, 동적할당은 Heap 영역에 p1과 p2객체는 스택에 생성이 된다.
 그 후, 40번째 라인의 p1 = p2; 은 p1.operator=(p2) 이것으로 해석 되므로, 대입 연산자 오버로딩이 일어 날 것이다. 우선 여기서는 대입 연산자가 따로 정의 되어 있지 않으므로, 디폴트 대입 연산자를 사용해서 p2가 인자로 전달되어 멤버간의 복사가 일어 날 것이다. (p2가 지니는 값을 p1에 복사하는 것이다.) 하지만 p2의 멤버는 객체의 포인터이다. 그래서 객체의 포인터 값을 p1에다 복사 할텐데, 처음 가리키고던 "One"이 끊어지고 p1은 "Two"를 가리키게 된다. 이게 바로 문제다. 
 이 문제는 복사 생성자에서도 생겼던 문제 인데, 하나의 문자열을 두개의 객체가 참조 하다 보니까 소멸자 호출에 있어서 문제가 생기고,(이것은 복사생성자에서도 생겼던 문제) 포인터를 잃어 버렸기 때문에 프로그램 종료될때까지 메모리 공간에서 메모리를 잡아 먹게 되어 메모리 유출이 발생하는 문제가 발생된다. 그래서 이 경우에는 대입 연산자를 꼭 재정의 해줘야 한다. 
 일단 포인터가 끊어지고 생기는 메모리 유출에 대한 문제를 해결하고, 메모리 공간을 다시 할당해 복사를 해주자. 아래는 새로 정의된 대입 연산자 함수와 결과 출력이다. 이제는 문제 없이 출력되는 것을 알 수 있다.

대입 연산자를 사용할 할 것이라면 반드시 재정의 !!!! 왜냐하면 얕은 복사로 인하여 메모리 유출이 생길수 있따. 
  1. Person& Person::operator=(const Person& p)  
  2.     {  
  3.         delete []name;  
  4.         name= new char[strlen(p.name)+1];   
  5.         strcpy(name, p.name);   
  6.         return *this;  
  7.     }  






댓글 1개:

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...