2016년 3월 20일 일요일

[Effective C++] 항목 12 : 객체의 모든 부분을 빠짐없이 복사하자.

우선 예를 통해서 왜 객체의 모든 부분을 빠짐없이 복사해야 하는지 그 이유를 알아 봅시다. 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Customer{  
  5.     char* name;  
  6. public:  
  7.     Customer():name(0)  {}  
  8.     Customer(char * _name)  
  9.     {  
  10.         name = new char[strlen(_name) +1];  
  11.         strcpy(name, _name);  
  12.     }  
  13.     Customer(const Customer& c)  
  14.     {  
  15.         name = new char[strlen(c.name) +1];  
  16.         strcpy(name, c.name);  
  17.     }  
  18.     ~Customer() {   delete[] name;  }  
  19.     Customer& operator=(const Customer& c)  
  20.     {  
  21.         //자기대입을 위해 swap함수 처리를 해야 하지만, 여기선 if를 쓰겠다.  
  22.         if(this == &c)  
  23.             return *this;  
  24.         delete[] name;  
  25.   
  26.         name = new char[strlen(c.name) +1];  
  27.         strcpy(name, c.name);  
  28.   
  29.         return *this;  
  30.     }  
  31.     const char* GetName(){ return name;}  
  32. };  
  33.   
  34. void main()  
  35. {  
  36.     Customer c1("test");  
  37.     Customer c2 = c1;  
  38.     cout << c2.GetName() <<endl;  
  39.   
  40.     Customer c3;  
  41.     c3 = c2;  
  42.     cout<< c3.GetName() <<endl;  
  43. }  
 일단 이 예제는 문제없이 출력 되는 것을 알 수 있습니다. 그럼 여기에서 일어날 수 있는 문제는 무엇일까요? 만약 Customer 클래스에 멤버 변수로 나이를 추가 한다고 해봅시다. int형 데이터 변수 age를 추가 시켜줄 경우에 복사 함수에서는 완전한 복사가 이루어지지 않고 부분 복사가 이루어집니다. 왜냐하면 지금은 name만 copy가 되고 있기 때문에, 새로 추가된 멤버 변수에 대해서는 따로 추가를 시켜 줘야 하기 때문이죠. 이러한 부분은 컴파일러가 해결해 주지 않습니다. 이러한 경우에 복사 함수에 일일이 추가로 구현을 해줘야 하는데 지금같은 경우에는 멤버 변수가 별로 없지만, 프로젝트가 커질 경우 이런 작업은 프로그래머에게 아주 번거로운 작업이 되겠죠. 
 하지만 지금같은 경우 더 심각한 문제가 발생할 수 있습니다. 바로 이 Customer 클래스를 상속받을 경우에 심각한 문제가 발생합니다. 아래와 같이 Customer를 상속받는 PriorityCustomer 클래스가 있다고 가정해 봅시다. (똑같이 복사 생성자, 대입 연산자 등을 지정해 줍니다.)
  1. class PriorityCustomer : public Customer{  
  2.     int priority;  
  3. public:  
  4.     PriorityCustomer () :priority(0){}  
  5.     PriorityCustomer (char *_name, int pri=0) : Customer(_name) , priority(pri) //기본 클래스의 이름을 넘겨줌  
  6.     {  
  7.   
  8.     }  
  9.     PriorityCustomer(const PriorityCustomer & p) //복사 생성자  
  10.         : priority(p.priority)  
  11.     {  
  12.   
  13.     }  
  14.     ~PriorityCustomer (){}  
  15.     PriorityCustomer& operator=(const PriorityCustomer& p)  
  16.     {  
  17.         priority = p.priority;  
  18.         return *this;  
  19.     }  
  20.     const int GetPriority(){ return priority;}  
  21.     void Print()  
  22.     {  
  23.         cout << GetName() << " " << GetPriority() <<endl; //이름과 우선순위 출력  
  24.     }  
  25. };  
  26.   
  27. void main()  
  28. {  
  29.     Customer c1("test");  
  30.     Customer c2 = c1;  
  31.     cout << c2.GetName() <<endl;  
  32.   
  33.     Customer c3;  
  34.     c3 = c2;  
  35.     cout<< c3.GetName() <<endl;  
  36.   
  37.     PriorityCustomer p1("pTest");  
  38.     p1.Print(); //현재까지 문제 없음  
  39.   
  40.     PriorityCustomer p2 = p1;  
  41.     p2.Print(); //한번만 출력된다.  
  42. }  

 실행해보면, 문제가 발생합니다. 현재 복사 생성자에서 현재 자신의 클래스(PriorityCustomer Class)만 초기화를 시켜 주고 있는데, 이런 경우에 기본 클래스는 기본 생성자를 호출 하게 됩니다. 그래서 name의 값이 제대로 대입이 안되고 있기 때문에 이런 문제가 발생하는 것입니다. 이 상황을 해결하기 위해서는 기본 클래스의 생성자도 호출을 해줘야 문제가 해결이 됩니다. 
  1. PriorityCustomer(const PriorityCustomer & p) //복사 생성자  
  2. : Customer(p), priority(p.priority)  
  3. {  
  4.   
  5. }  
 그럼 이번에는 복사 대입 연산자는 어떤지 알아 봅시다.
  1. void main()  
  2. {  
  3.     Customer c1("test");  
  4.     Customer c2 = c1;  
  5.     cout << c2.GetName() <<endl;  
  6.   
  7.     Customer c3;  
  8.     c3 = c2;  
  9.     cout<< c3.GetName() <<endl;  
  10.   
  11.     PriorityCustomer p1("pTest");  
  12.     p1.Print(); //현재까지 문제 없음  
  13.   
  14.     PriorityCustomer p2 = p1;  
  15.     p2.Print(); //한번만 출력된다.  
  16.   
  17.     PriorityCustomer p3;  
  18.     p3 = p2;  
  19.     p3.Print(); //에러  
  20. }  
 역시 같은 에러가 나오고 있는걸 볼 수 있습니다. 이 경우도 마찬가지로 복사 대입 연산자는 지금 현재 있는 클래스의 멤버 변수만 대입을 해주고 있지, 기본 클래스의 멤버 변수의 대입은 안해주고 있기 때문입니다. 그래서 여기도 마찬가지로 기본 클래스의 복사 대입 연산자 호출을 해줍니다.
  1. PriorityCustomer& operator=(const PriorityCustomer& p)  
  2. {  
  3.     Customer::operator= (p); //기본 클래스의 복사 대입 연산자 호출  
  4.     priority = p.priority;  
  5.     return *this;  
  6. }  
 객체의 복사 함수 작성시 확인해야할 사항이 두가지 있습니다. 첫번째, 해당 클래스의 멤버 변수를 모두 복사했는지? 두번째, 상속한 기본 클래스의 복사 함수도 호출해줘야 합니다. 그 외에도 Effective C++에서는 한가지 더 설명하고 있는 내용이 있는데, 이런 복사 함수를 살펴보면 중복된 코드들이 있습니다. 이 중복된 코드를 피하기 위해서 (예를 들어) 복사 대입 연산자에서 복사 생성자를 호출한다거나 이 반대로 하거나 하면 문제가 생깁니다. 그럼 실제로 Customer의 복사 대입 연산자 부분에 복사 생성자를 호출해 봅시다.
  1. Customer& operator=(const Customer& c)  
  2.     {  
  3.         //자기대입을 위해 swap함수 처리를 해야 하지만, 여기선 if를 쓰겠다.  
  4.         if(this == &c)  
  5.             return *this;  
  6.         delete[] name;  
  7.   
  8.         //이렇게 호출 한다면?  
  9.         Customer::Customer(c);    
  10.           
  11.         return *this;  
  12.     }  
  복사 대입 연산자는 이미 객체가 완성된 상황에서 대입을 하기 때문에, 이 상황에서는 이미 생성자가 호출된 상태이기에 또 생성자를 호출하게 되는 형태를 가지게 됩니다. 이렇게 되면 멤버 변수의 값이 아까 처럼 쓰레기 값이 들어 가는 경우가 발생하므로 이렇게 사용해서는 안되고 이 반대의 경우도 마찬가지이다. 복사 생성자도 생성자 이기 때문입니다.

  그럼 이 중복된 코드를 어떻게 피할 수 있을까요? 제 3의 함수를 생성해서 호출해서 쓰는게 바로 답입니다. 
  1. class Customer{  
  2.     char* name;  
  3.     void InitName(const Customer& c)  
  4.     {  
  5.         name = new char[strlen(c.name) +1];  
  6.         strcpy(name, c.name);  
  7.     }  
  8. public:  
  9.     Customer():name(0)  {}  
  10.     Customer(char * _name)  
  11.     {  
  12.         name = new char[strlen(_name) +1];    
  13.         strcpy(name, _name);   
  14.     }  
  15.     Customer(const Customer& c)  
  16.     {  
  17.          InitName(c);   
  18.     }  
  19.     ~Customer() {   delete[] name;  }  
  20.     Customer& operator=(const Customer& c)  
  21.     {  
  22.         //자기대입을 위해 swap함수 처리를 해야 하지만, 여기선 if를 쓰겠다.  
  23.         if(this == &c)  
  24.             return *this;  
  25.         delete[] name;  
  26.         InitName(c);   
  27.         return *this;  
  28.     }  
  29.       
  30.     const char* GetName(){ return name;}  
  31. };  
전체 소스

 * 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠드리지 말고 복사해야 합니다. 
 * 클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지 마세요. 그 대신, 공통된 동작을 제 3의 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결합시다.

댓글 없음:

댓글 쓰기

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

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