윈도우7의 설치 후 기본 설정대로 쓰다보면 XP에 비해서 이상하게 하드를 심하게 읽는다던지, 하드가 갑자기 꺼진다던지 하는 현상이 있는데요,

나름 성능 향상을 위해 넣어둔 기능이지만 실제로 효용성은 떨어져서 오히려 끄는게 좋을 기능들이 몇 가지 있습니다. 여기서 기본적인 단 3가지만 설정해서 쾌적한 윈도우7을 만드는 방법을 알려드립니다.
1. Windows Search 서비스 중지

윈도우 서치는 웹의 검색을 데스크탑으로 옮긴 기능입니다. 하드에 있는 여러 파일을 인덱싱해서 만약 파일을 찾을 필요가 있을 때 빠르게 검색할 수 있도록 해주는 기능인데, 문제는 이 인덱싱 과정이 시스템 성능을 매우 떨어트린다는 점입니다. 가뜩이나 느린 하드디스크를 읽어서 파일들을 분석하고 색인화 하는 것은 검색을 특별히 많이 하지 않는 유저라면 (특히 하드가 느린 노트북 환경에서는 더욱이) 왠만하면 꺼놓는게 좋습니다.

제어판 - 관리 도구 - 서비스에 가서 Windows Search를 찾아서 더블클릭한 후, 중지 버튼을 누르고 시작 유형을 사용 안함으로 설정합니다.
2. Superfetch, Offline Files 서비스 중지

윈도우7이 그 다음으로 하드 엑서스를 많이 하는 부분이 Superfetch와 Offline Files서비스입니다. 이 서비스들을 끄지 않은 상태에서 가만히 보면 하드가 거의 쉬지 않고 돌아갑니다.

사실 이 기능은 자주 사용하는 파일을 메모리나 레디부스트USB에 올려서 성능을 향상시키겠다는 의도인데, 사실 실제로 이렇게 캐싱되는 파일을 보면 그다지 빠른 엑서스가 필요하지 않는 동영상 파일들이 대상이더군요. 이럴바엔 오히려 캐싱이 독이 되죠.

역시 2번 항목과 마찬가지로 서비스에서 Superfetch와 Offline Files 서비스를 찾아서 중지시키고 시작 유형을 사용 안함으로 설정합니다.

3. 그래픽 효과 끄기

윈도우7의 에어로 테마는 정말 멋집니다. 하지만 멋진 만큼 성능(정확히는 응답속도라고 하는게 낫겠군요)을 깎아먹기도 하는데, 다른 항목은 건드리지 않고 '바탕 화면 구성 사용' 항목만 체크 해제하면 XP만큼 빠릿빠릿한 반응성을 보여줍니다.

제어판 - 성능 정보 및 도구 - 시각 효과 조정에서 '바탕 화면 구성 사용' 항목을 체크 해제하고 확인을 누릅니다. 반응성은 정말 빨라지는 대신, 아쉽게도 반투명한 창과 작업표시줄은 더 이상 볼 수 없게 됩니다...
이렇게 무겁고 별 필요 없는 단 3가지 옵션만 끄면 쾌적하게 윈도우7을 사용할 준비가 됩니다. 여기서 성능 튜닝이 더 필요한 분은 인터넷에 떠도는 갖가지 정보를 직접 찾아보시면 되겠습니다.

Posted by 모과이IT
,

// 주소 에디트 박스 핸들값으로 오는건 break를 걸어서 Dialog로 메시지를 보내고 아닌것은...
// SendAcceleratorToWeb함수를 이용해서 웹페이지로 보냄
// 힘들었음 이거 알아낸다고 ㅠ
// CEdit *hWndUrlAdress;

BOOL CNBrowserSetupCtrl::PreTranslateMessage(MSG* pMsg)
{
 // TODO: 여기에 특수화된 코드를 추가 및/또는 기본 클래스를 호출합니다.

 switch (pMsg->message)
 {
 case WM_KEYDOWN:
  if(GetKeyState(VK_CONTROL))
  {
   // 67 = 'c' , 86 = 'v'
   if(pMsg->wParam == 67)
   { 
    hWndUrlAdress = (CEdit*)NitsViewDlg.GetDlgItem(IDC_ADC);
    MyOutputDebugString("Ctrl Message [%d],[%d],[%d],[%d],[%d]",hWndUrlAdress->m_hWnd,pMsg->hwnd,pMsg->lParam,pMsg->message,pMsg->wParam);
    
    if(!(hWndUrlAdress->m_hWnd == pMsg->hwnd))
     return SendAcceleratorToWeb(pMsg);

    break;
   }
   if(pMsg->wParam == 86)
   {
    hWndUrlAdress = (CEdit*)NitsViewDlg.GetDlgItem(IDC_ADC);
    MyOutputDebugString("Ctrl Message [%d],[%d],[%d],[%d],[%d]",hWndUrlAdress->m_hWnd,pMsg->hwnd,pMsg->lParam,pMsg->message,pMsg->wParam);
    
    if(!(hWndUrlAdress->m_hWnd == pMsg->hwnd))
     return SendAcceleratorToWeb(pMsg);

    break;
   }
  }

 case WM_KEYUP:
  switch (pMsg->wParam)
  {
  case VK_TAB:
  case VK_RETURN:
   MyOutputDebugString("VK_TABffffffffffff");
   hWndUrlAdress = (CEdit*)NitsViewDlg.GetDlgItem(IDC_ADC);
   MyOutputDebugString("Ctrl Message [%d],[%d],[%d],[%d],[%d]",hWndUrlAdress->m_hWnd,pMsg->hwnd,pMsg->lParam,pMsg->message,pMsg->wParam);

   // urlAdress의 핸들값이 아니면 웹으로 키보스 마우스를 보내고
   // 아니면 urlAdress핸들로 키보스 메시지를 보낸다
   if(!(hWndUrlAdress->m_hWnd == pMsg->hwnd))
    return SendAcceleratorToWeb(pMsg);
   break;

  }
  break;
 }
 return COleControl::PreTranslateMessage(pMsg);
}

Posted by 모과이IT
,
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Widget{   
  5. public:   
  6.     Widget(){cout<<" Widget() "<<endl;}   
  7.     ~Widget() {cout<<" ~Widget() " <<endl;}   
  8.     void dosomething() {}   
  9. };   
  10.   
  11. void main()   
  12. {   
  13.     Widget* p = new Widget;   
  14.        
  15.     //자원을 이용하는 코드...   
  16.        
  17.     delete p;   
  18. }  
 이 책에서는 자원을 동적으로 할당된 객체에 대해서만 한정해 설명을 하고 있는데, 예를 들어 Widget* p = new Widget(); 와 같은 코드에서 p 자체를 자원이라고 부르고 있습니다. C++은 delete로 자원을 반납하는 특성을 가집니다. 동적으로 할당된 자원을 delete로 반납하지 않으면 메모리 누수가 일어나죠. 애초에 작성할 때 new로 할당을 하고 이어서 delete를 해주고 그 사이에 코드를 작성하게 되는데, 이 사이에 들어가는 코드가 return 문을 가지고 있는 경우에 delete문을 호출해 주지 않기 때문에 마지막에서 delete를 해준다고 해도 중간에 함수가 끝나서 메모리 누수가 발생합니다. 만약 이런 코드가 루프 문에 들어가 있다고 해봅시다.
  1. for(..)   
  2. {   
  3.     Widget* p = new Widget;   
  4.     break;    
  5.     continue;   
  6.   
  7.     delete p;   
  8. }  
 루프문은 break이나 continue를 이용해서 루프를 제어하게 되는데 이런 제어 코드 때문에 위 경우와 마찬가지로 delete가 실행이 되지 않고 메모리 누수가 일어 날 수 있습니다. 만약 이런 상황을 막기 위해서는 제어 코드 이전에 delete를 해주면 되지만, 이런 코드 들이 여러개 존재하고 이런 일들을 일일이 해준다고 한다면 번거롭기도 하고, 자원관리에 어려움이 있습니다. 이런 자원을 사용자가 직접 할당 받고 반납하는 과정을 다 염두에 두고 코딩을 해야 하는 것이 프로그래머에게 부담이 되고 문제가 됩니다. 그래서 이펙티브 C++에서는 자원을 사용자가 직접 해제 하지 말고, 자원 관리 객체를 이용해 그 자원관리 객체가 소멸할때 소멸자에서 자원을 소멸하도록 이용하라고 되어 있습니다. 

 auto_ptr  
 자원관리 객체는 주로 스마트 포인터를 이용하는데, 스마트 포인터 중에 첫번째로는 Auto Pointer가 있습니다. 이것은 memory 헤더 안에 선언이 되어 있어 사용할때는 헤더를 선언해주고 사용해줘야 합니다. 
  1. #include <iostream>   
  2. #include <memory>   
  3. using namespace std;   
  4.   
  5. class Widget{   
  6. public:   
  7.     Widget(){cout<<" Widget() "<<endl;}   
  8.     ~Widget() {cout<<" ~Widget() " <<endl;}   
  9.     void do() {}   
  10. };   
  11.   
  12. void main()   
  13. {   
  14.     std::auto_ptr<Widget> p(new Widget);   
  15. }  
 위와 같이 auto_ptr을 사용하면 예전처럼 동적 할당후 자원 반납을 해주지 않아도 자기 객체가 사라질때 소멸자에서 자원의 delete를 적용해줘서 결과적으로 delete를 안써줘도 되게 만들어져 있습니다. 여기 코드에서는 할당받은 자원을 자원 관리 객체 p에다가 넘기고 있는데, 자원관리 객체의 초기화 코드에 할당받은 자원을 넘기는 것을 "자원획득 초기화(RAII)" 라고 합니다.즉, 자원관리 객체 p가 있고 p의 초기화로 자원을 넘겨서 그 p가 소멸이 될때 그때 delete를 해주게 되는 방식이죠. 
 이제 return과 같은 제어코드나 다른 상황에서도 이 p는 지역변수 와 똑같이 동작을 해서 루프를 빠져나와 함수를 빠져나갈때 delete를 호출해 주기 때문에 자원누수를 막을 수 있습니다. 그러니까 자원해제를 사용자가 하는게 아니라 자원 관리 객체 라는 객체가 관리를 해주니까 누수에 대한 걱정을 하지 않아도 되는 것입니다. 

 그런데 auto_ptr에는 아래와 같은 문제가 일어날 수 있을 것이라고 생각할 수 있을것입니다.
  1. #include <iostream>   
  2. #include <memory>   
  3. using namespace std;   
  4.   
  5. class Widget{   
  6. public:   
  7.     Widget(){cout<<" Widget() "<<endl;}   
  8.     ~Widget() {cout<<" ~Widget() " <<endl;}   
  9.     void do() {}   
  10. };   
  11.   
  12. void main()   
  13. {   
  14.     std::auto_ptr<Widget> p(new Widget);   
  15.     std::auto_ptr<Widget> p2(p); //복사   
  16. }  
 위와 같이  auto_ptr을 복사를 하게 되면 똑같은 자원에 대해 두 자원관리 객체가 생기게 되는 꼴이 되는데, 이놈의 소멸자가 자원에 대한 delete를 해주게 되니까 같은 자원을 두번 해제하게 되는 일이 발생할 수도 있을 거라 예상이 될 것입니다. 그래서 auto_ptr은 그런것을 막기 위해서 소유권을 넘긴다는 방식을 취하고 있다. 
  소유권을 넘긴다? 소유권을 넘긴다는 의미는 기존의 자원은 null로 바꿔 버리고 새로 복사된 객체(이전에 있던 자원을) 유지하는 방식으로 동작을 합니다. 
  1. void main()   
  2. {   
  3.     std::auto_ptr<Widget> p(new Widget); //Null   
  4.     std::auto_ptr<Widget> p2(p);         //Widget   
  5. }  
  C++에서도 'delete NULL' 해도 정상적으로 작동을 하므로 표준에 어긋난 것은 아닙니다. 여기에서 p2가 먼저 소멸 되는데, 그때 자원이 반납되고, p는 null이기 때문에 어떤 동작도 안하게 되므로, 자원 관리 객체의 복사 문제 해결하고 있습니다. 물론 복사대입 연산자도 이런 처리가 되어 있습니다. 하지만 문제가 있습니다.
  1. #include <iostream>   
  2. #include <memory>   
  3. using namespace std;   
  4.   
  5. class Widget{   
  6. public:   
  7.     Widget(){cout<<" Widget() "<<endl;}   
  8.     ~Widget() {cout<<" ~Widget() " <<endl;}   
  9.     void dosomething(){}   
  10. };   
  11.   
  12. void main()   
  13. {   
  14.     std::auto_ptr<Widget> p(new Widget);   
  15.     std::auto_ptr<Widget> p2(p); //복사   
  16.   
  17.     p2->dosomething(); //take breakpoint   
  18.     p->dosomething();   
  19. }  

 위 소스 코드를 실행해보면, p2는 지금 자원을 가지고 있기 때문에 정상적으로 호출되는데 p는 p2의 자원을 넘겨주고 자신은 null로 바뀌었기 때문에 null에 대해서 멤버 함수를 호출하는 동작을 취하면 프로그램이 이 부분에서 뻗게 되는 것입니다. 기존 동작의 포인터와 다른 복사 동작을 가지고 있는것이 오토 포인터의 특징이라고 할 수 있습니다. 
  하지만 이런 auto_ptr은 특성 때문에 STL의 컨테이너나 같은 곳에 같이 사용하면 문제가 발생하는 경우가 생길 수 있습니다. 그래서 우리가 일반적인 포인터를 사용할때처럼, 복사는 자유로우면서, 소멸 또한 관리가 되는 또 다른 자원관리 객체의 필요성이 대두 됩니다. 그래서 나타난것이 shared_ptr입니다. 

 shared_ptr  
 : shared_ptr은 참조 카운팅 방식 스마트 포인터(reference-counting smart pointer) 중의 하나로서, 원래는 Boost Library에 있다가 C++의 새로운 표준인 tr1이라는 이름공간에 추가되었습니다.
  1. #include <iostream>   
  2. #include <memory>   
  3. using namespace std;   
  4.   
  5. class Widget{   
  6. public:   
  7.     Widget(){cout<<" Widget() "<<endl;}   
  8.     ~Widget() {cout<<" ~Widget() " <<endl;}   
  9.     void dosomething(){}   
  10. };   
  11.   
  12. void main()   
  13. {   
  14.     std::tr1::shared_ptr<Widget> p (new Widget);  //counting : 1   
  15.     std::tr1::shared_ptr<Widget> p2(p);           //counting : 2   
  16.   
  17.     p2->dosomething();   
  18.     p->dosomething();   
  19. }  
 shared_ptr은 내부적으로 카운팅을 유지 해서(참조 갯수를 유지 해서) 자원의 참조 갯수를 셉니다. 처음 생성했을 때는 한개가 되고, 그 다음에는 두개가 되는 식으로 말이죠. shared_ptr는 참조 갯수만 관리 하기 때문에 위 소스코드가 무리없이 동작 되는 것을 알 수 있습니다. 그리고 shared_ptr도 마찬가지로 소멸시 delete를 호출해주는데 이 점때문에, 복사동작이 많이 요구되고 동시에 자원이 관리 되어야 하는 상황에서는 auto_ptr 보다 shared_ptr이 더 적합합니다. 그런데 문제는 자원이 동적으로 하나만 생성된것이 아니라, 아래와 같이 동적 배열로 생성된 경우는 shared_ptr은 스스로 판단을 할 수 없다는 것이 문제점 입니다.
  1. #include <iostream>   
  2. #include <memory>   
  3. using namespace std;   
  4.   
  5. class Widget{   
  6. public:   
  7.     Widget(){cout<<" Widget() "<<endl;}   
  8.     ~Widget() {cout<<" ~Widget() " <<endl;}   
  9.     void dosomething(){}   
  10. };   
  11.   
  12. void main()   
  13. {   
  14.     std::tr1::shared_ptr<Widget> p (new Widget[4]);   
  15.     std::tr1::shared_ptr<Widget> p2(p);              
  16.   
  17.     p2->dosomething();   
  18.     p->dosomething();   
  19. }  
 auto_ptr과  마찬가지로 shared_ptr도 delete 를 호출합니다. 하지만 위와 같이 동적 배열로 생성된 경우에는 delete[]로 호출해서 소멸시켜야 하는데, 그냥 delete만 호출 하는 할당과 반납의 형태가 다르다는 문제가 생기는 것입니다. 그래서 이런 경우는 STL의 벡터(vector) 같은 컨테이너와 같이 조합을 해서 이용하는 것을 추천합니다. 
  1. #include <iostream>   
  2. #include <memory>   
  3. #include <vector>   
  4. using namespace std;   
  5.   
  6. class Widget{   
  7. public:   
  8.     Widget(){cout<<" Widget() "<<endl;}   
  9.     ~Widget() {cout<<" ~Widget() " <<endl;}   
  10.     void dosomething(){}   
  11. };   
  12.   
  13. void main()   
  14. {   
  15.     std::vector<std::tr1::shared_ptr<Widget>> p(4);   
  16.     std::tr1::shared_ptr<Widget> p2(p);              
  17.   
  18.     p2->dosomething();   
  19.     p->dosomething();   
  20. }  
 자원 관리 객체는 하나의 자원만 받고 그 자원관리 객체를 여러개를 컨테이너로 관리 하면 자원이 여러개인 경우도 쉽게 관리를 할 수 있게 되는 것입니다.

 shared_ptr 삭제자  
 : shared_ptr은 특별하게 삭제자를 지정해 줄 수 있습니다. shared_ptr의 기본 삭제 동작은 delete를 호출하는 것인데, 이런 동작을 바꿔서
delete 배열 연산자를 호출 할수 있도록 바꿔 줄 수도 있습니다. 
  1. #include <iostream>   
  2. #include <memory>   
  3. using namespace std;   
  4.   
  5. class Widget{   
  6. public:   
  7.     Widget(){cout<<" Widget() "<<endl;}   
  8.     ~Widget() {cout<<" ~Widget() " <<endl;}   
  9.     void dosomething(){}   
  10. };   
  11.   
  12. struct arrayDeleter   
  13. {   
  14.     template<typename T>   
  15.     void operator()(T* p)   
  16.     {   
  17.         delete [] p;   
  18.     }   
  19. };   
  20. void main()   
  21. {   
  22.     std::tr1::shared_ptr<Widget> p(new Widget[4], arrayDeleter());   
  23. }  
 위와 같이 삭제자를 바꿀 수가 있습니다. 여기에서는 획득시 초기화를 이용해 자원을 넘기는데, 여기에 추가적인 인수를 더 받도록 되어 있습니다. 여기에 위에서 만든 arrayDeleter를 불러줘 삭제의 동작을 바꿔 주는 것입니다. 

 이렇게 삭제자 중에서는 우리는 가끔 빈삭제자를 이용해야 할 때가 있는데, 빈삭제자(Empty Deleter)는 삭제 동작을 아무런 동작을 취하지 않는 것을 말합니다.
  1. #include <iostream>   
  2. #include <memory>   
  3. using namespace std;   
  4.   
  5. struct emptyDeleter   
  6. {   
  7.     template<typename T>   
  8.     void operator()(T* p)   
  9.     {   
  10.   
  11.     }   
  12. };   
  13.   
  14. class Widget{   
  15. public:   
  16.     Widget(){cout<<" Widget() "<<endl;}   
  17.     ~Widget() {cout<<" ~Widget() " <<endl;}   
  18.     std::tr1::shared_ptr<Widget> getThis() {return std::tr1::shared_ptr<Widget>(this);}   
  19. };   
  20.   
  21. void main()   
  22. {   
  23.     std::tr1::shared_ptr<Widget> p(new Widget[4], emptyDeleter());   
  24. }  
 지금과 같은 상황에서 빈삭제자를 쓴다면 메모리 누수가 생길텐데, 이 빈삭제자의 활용법은 자기 자신의 객체를 외부로 반환하는 함수가 있다고 할때,  std::tr1::shared_ptr<Widget> getThis() {return std::tr1::shared_ptr<Widget>(this);}
 위와 같이 작성하면, this가 자원관리 객체로 넘어가면서 반환하게 되는데 shared_ptr의 기본 동작이 delete를 호출해주기 때문에, 지금같은 경우 this를 delete하면 문제가 생길것입니다. 그래서 여기서 빈삭제자를 넣어 이런 삭제를 막는 것입니다.
  std::tr1::shared_ptr<Widget> getThis() {return std::tr1::shared_ptr<Widget>(this, emptyDeleter() );}

 끝으로 shared_ptr 같이 타입이름이 너무 기니까 typedef를 쓰게 되는데 실제로 많이 쓰는 방법은 어떤 타입에 대한 shared_ptr 포인터 타입을 만들기 위해 위에서 전방 선언만 해주고 (컴파일러에게 이름을 알려준다.) 그 이름을 shared_ptr 포인터에 넘겨 새로운 타입을 만들어서 사용하면 이런 선언에 대한 이름길이의 부담을 줄일 수 있습니다.
  1. #include <memory>   
  2. #include <iostream>   
  3. using namespace std;   
  4.   
  5. class Widget;   
  6. typedef std::tr1::shared_ptr<Widget> Widget_ptr;   
  7.   
  8. class Widget{   
  9. public:   
  10.     Widget(){cout<<" Widget() "<<endl;}   
  11.     ~Widget() {cout<<" ~Widget() " <<endl;}   
  12.     std::tr1::shared_ptr<Widget> getThis() {return std::tr1::shared_ptr<Widget>(this);}   
  13. };   
  14.   
  15. void main()   
  16. {   
  17.     Widget_ptr p(new Widget);   
  18. }  
 지금까지 자원관리에 대해 auto_ptr과 shared_ptr에 대해서 알아봤는데요, 사용자는 자원을 언제든지 놓칠 수 있기 때문에, delete를 언제나 챙길 수 없습니다. 그래서 이것을 자원관리 객체라는 또 다른 객체에 넘겨서 관리를 하자는게 이번 항목의 목적이라고 할 수 있습니다.

 * 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용합시다.
 * 일반적으로 널리 쓰이는 RAII 클래스는 tr1::shared_ptr 그리고 auto_ptr입니다. 이 둘 가운데 tr1::shared_ptr이 복사 시의 동작이 직관적이기 때문에 대개 더 좋습니다. 반면, auto_ptr은 복사되는 객체(원본 객체)를 null로 만들어 버립니다.
Posted by 모과이IT
,
우선 예를 통해서 왜 객체의 모든 부분을 빠짐없이 복사해야 하는지 그 이유를 알아 봅시다. 
  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의 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결합시다.
Posted by 모과이IT
,
자기대입(self assignment)  
 자기대입(self assignment)이란, 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말합니다. 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Widget{};   
  5.   
  6. void main()   
  7. {   
  8.     Widget w;   
  9.     w = w;     //자기대입   
  10. }  

 문제는 무엇인가?  
 같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 물어 놓고 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성을 고려하는 것이 일반적으로 바람직한 자세라고 할 수 있겠습니다. 사실, 같은 클래스 계통에서 만들어진 객체라 해도 굳이 똑같은 타입으로 선언할 필요까지는 없습니다. 파생 클래스 타입의 객체를 참조 하거나 가리키는 용도로 기본 클래스의 참조자나 포인터를 사용하면 되니까 말이죠. 그럼 대입연산자에서는 무엇을 조심해야 할까요? 아래와 같이 동적 할당된 비트맵을 가리키는 원시 포인터를 데이터 멤버로 갖는 클래스가 있다고 한번 해봅시다.
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Bitmap    
  5. {   
  6. public:   
  7.     Bitmap()   
  8.     {   
  9.         cout << "Bitmap ()" << endl;   
  10.     }   
  11. };   
  12.   
  13. class Widget       
  14. {   
  15. public:   
  16.     Widget()    
  17.     {   
  18.         cout << "Widget ()" << endl;   
  19.         pb = new Bitmap;   
  20.         pb = NULL;   
  21.   
  22.     }   
  23.   
  24.     Widget (const Bitmap& pbitmap)   
  25.     {   
  26.         cout << "Widget (const char * data)" << endl;   
  27.   
  28.         pb = new Bitmap(pbitmap);   
  29.   
  30.         memcpy(pb, &pbitmap, sizeof(pbitmap));   
  31.   
  32.     }   
  33.        
  34.     Widget& operator=(const Widget& rhs)    // 안전하지 않게 구현된 operator=   
  35.     {   
  36.         cout << "Widget& operator=(const Widget& rhs)" << endl;   
  37.   
  38.         delete pb;   
  39.         pb = new Bitmap(*rhs.pb);   
  40.   
  41.         return *this;   
  42.     }          
  43.   
  44. private:   
  45.     Bitmap * pb;    //힙에 할당된 객체를 가르키는 포인터   
  46. };   
  47.   
  48. int main()   
  49. {   
  50.     Bitmap b;   
  51.     Widget w(b);   
  52.   
  53.     Widget w2;  // 디폴트 생성자에서 꼭 초기화를 해줘야 한다.   
  54.     w2 = w;     // 자기 대입 검사를 하지 않으면, 심각한 오류가 발생한다.   
  55.        
  56.     return 0;   
  57. }  
 위의 예제 코드를 의미적으로 문제가 없을 것 같지만 자기 참조의 가능성이 있는 위험한 코드 입니다. 여기서의 자기 참조 문제는 operator= 내부에서 *this와 rhs가 같은 객체일 가능성이 있다는 것입니다. 이 둘이 만약 같은 객체라고 한다면, delete 연산자가 *this 객체의 비트맵에만 적용되는 것이 아니라 rhs의 객체까지 적용되어 버립니다. 즉, 이 함수가 끝나는 시점이 되면 해당 Widget객체는 자신의 포인터 멤버를 통해 물고 있던 객체가 삭제된 상태가 되어버릴 수 있다는 말입니다. 
 물론 이것에 대한 대책은 있습니다.
  1. Widget& operator=(const Widget& rhs)   
  2. {   
  3.     cout << "Widget& operator=(const Widget& rhs)" << endl;   
  4.     if (this == &rhs)   //  일치성 검사, 즉 객체가 같은지, 자기 대입인지 검사한다.   
  5.         return *this;   
  6.   
  7.     delete pb;   
  8.     pb = new Bitmap(*rhs.pb);      
  9.   
  10.     return *this;   
  11. }  
 위와 같이 일치성 검사(identity test)를 통해 자기대입을 검사를 통해 자기 대입이면 아무것도 하지 않도록 하는 것입니다. 하지만 이 방법은 예외에 안전하지 않습니다. 만약 'new Bitmap' 표현식 부분에서 예외가 발생하게 되면, Widget 객체는 결국 삭제된 Bitmap을 가리키는 포인터를 가지고 홀로 남게 됩니다. 이런 포인터는 delete 연산자를 안전하게 적용할 수도 없고, 안전하게 읽는 것조차 불가능하다는 문제가 있습니다. 

 전부는 아니지만, operator=을 예외에 안전하게 구현하면 대개 자기대입에도 안전한 코드가 나오게 되어 있습니다. 즉, 예외 안전성에만 집중하면 자기대입 문제에 대해서는 그렇게 걱정을 안해도 된다는 의미죠. 여기에서는 pb를 무조건 삭제 하지 말고 이 포인터가 가리키는 객체를 복사한 직후에 삭제하면 예외에 대한 문제를 해결 할 수 있습니다. 아래와 같이 말이죠. 
  1. Widget& operator=(const Widget& rhs) // 이 코드는 예외에 안전하다.   
  2. {   
  3.     cout << "Widget& operator=(const Widget& rhs)" << endl;   
  4.   
  5.     Bitmap * pOrig = pb;        // 원래의 pb를 pOrig에 저장해둔다.   
  6.     pb = new Bitmap(*rhs.pb);   // 다음, pb가 *pb의 사본을 가리키게 만든다.   
  7.     delete pOrig;               // 원래의 pb를 삭제한다.   
  8.   
  9.     return *this;   
  10. }  
 위의 코드는 new Bitmap 부분에서 예외가 발생하더라도 pb는 변경되지 않는 상태가 유지되기 때문에 예외에 안전합니다. 또한 원본 비트맵을 복사해 놓고, 복사해 놓은 사본을 포인터가 가리키게 만든 후, 원복을 삭제하는 순서로 실행되기 때문에 일치성 검사 없이도 자기대입 현상을 완벽히 처리 하고 있죠. 물론 이 방법 말고도 다른 복사 후 맞바꾸기(Copy and Swap) 이라는 방법도 있습니다.

 복사 후 맞바꾸기(Copy and Swap)  
 : 이 기법은 사실 예외 안전성과 아주 밀접한 관계가 있기 때문에 이후 항목 29에 자세히 설명이 나와있습니다. 하지만 어떤 식으로 구현 되는지 간단히 알아 보죠. 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Bitmap    
  5. {   
  6. public:   
  7.     Bitmap()   
  8.     {   
  9.         cout << "Bitmap ()" << endl;   
  10.     }   
  11. };   
  12.   
  13. class Widget       
  14. {   
  15. public:   
  16.     Widget()    
  17.     {   
  18.         cout << "Widget ()" << endl;   
  19.         pb = new Bitmap;   
  20.         pb = NULL;   
  21.     }   
  22.   
  23.     Widget (const Bitmap& pbitmap)   
  24.     {   
  25.         cout << "Widget (const char * data)" << endl;   
  26.         pb = new Bitmap(pbitmap);   
  27.         memcpy(pb, &pbitmap, sizeof(pbitmap));   
  28.     }   
  29.   
  30.     void swap(Widget& rhs)  // *this의 데이터 및 rhs의 데이터를 맞바꾼다.   
  31.     {   
  32.         Bitmap * pOrig = pb;   
  33.         pb = new Bitmap(*rhs.pb);   
  34.         rhs.pb = pOrig;        
  35.     }   
  36.   
  37.     Widget& operator=(const Widget& rhs)    
  38.     {   
  39.         cout << "Widget& operator=(const Widget& rhs)" << endl;   
  40.   
  41.         Widget temp(rhs);   // rhs의 데이터에 대해 사본을 하나 만든다.   
  42.   
  43.         swap(temp);         // *this의 데이터를 그 사본의 것과 맞바꾼다.   
  44.   
  45.         delete temp.pb;   
  46.         return *this;   
  47.     }   
  48.   
  49. private:   
  50.     Bitmap * pb;    // 힙에 할당된 객체를 가르키는 포인터   
  51. };   
  52.   
  53. int main()   
  54. {   
  55.     Bitmap b;   
  56.     Widget w(b);   
  57.   
  58.     Widget w2;  // 디폴트 생성자에서 꼭 초기화를 해줘야 한다.   
  59.     w2 = w;     // 자기 대입 검사를 하지 않으면, 심각한 오류가 발생한다.   
  60.   
  61.     return 0;   
  62. }  

 이 방법은 C++가 가진 두가지 특징을 활용해서 조금 다르게 구현할 수도 있습니다. 
 - 첫째, 클래스의 복사 대입 연산자는 인자를 값으로 취하도록 선언하는 것이 가능하다.
 - 둘째, 값에 의한 전달을 수행하면 전달된 대상의 사본이 생긴다
  1. //rhs는 넘어온 원래 객체의 사본 -- '값에 의한 전달'   
  2. Widget& operator=(Widget rhs)    
  3. {   
  4.     cout << "Widget& operator=(const Widget& rhs)" << endl;   
  5.   
  6.     swap(rhs);  // *this의 데이터를 그 사본의 데이터와 맞바꾼다.   
  7.   
  8.     delete rhs.pb;   
  9.     return *this;   
  10. }  
 위으 코드는 객체를 복사하는 코드가 함수 본문으로부터 매개변수의 생성자로 옮겨졌기 때문에, 컴파일러가 더 효율적인 코드를 생성할 수 있는 여지가 만들어지는 장점을 가지고 있습니다. 

 * operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만들자. 원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며, 복사 후 맞바꾸기 기법을 써도 된다.
 * 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게 동작하는지 확인하자.
Posted by 모과이IT
,
Effective C++의 이번 항목 내용은 관례(Convention)에 대한 이야기 입니다. C++의 대입연산은 여러 개가 사슬처럼 엮일 수 있는 성질을 갖고 있습니다. 아래와 같이 말입니다.
  1. int x,y,z;   
  2. x = y = z = 15;  
 대입 연산이 가진 또 하나의 재미있는 특성은 바로 우측 연관(Right-associative) 연산이라는 점입니다. 즉, 위의 대입 연산 사슬은 다음과 같이 분석이 됩니다.
  1. x = (y = (z = 15)));  
 위 소스코드를 보면, 15가 z에 대입되고, 그 대입 연산의 결과가 y에 대입된후에, y에 대한 대입 연산의 결과가 x에 대입이 됩니다. 이렇게 대입 연산이 사슬처럼 엮일려면 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 구현되어 있기 때문일 것입니다. 따라서, C++에서의 연산자 재정의를 할 시에 이런 관례를 지켜주는 것이 좋습니다. "좌변 객체의 참조자를 반환하게 만들자"라는 관례는 대입형 연산자 말고 모든 형태의 대입 연산자에서도 지켜져야 할 것입니다. 
  1. class Convention{   
  2. public:   
  3.        
  4.     Convention& operator= (const Convention& con)   
  5.     {   
  6.         return *this;   
  7.     }   
  8.   
  9.     // +=, -=, *= 등에도 동일하게 지켜줘야 합니다.   
  10.     Convention& operator+= (const Convention& con)    
  11.     {   
  12.         return *this;   
  13.     }   
  14.   
  15.     //대입 연산자의 매개변수 타입이 일반적이지 않는 경우도 마찬가지죠.   
  16.     Convention& operator= (int con)   
  17.     {   
  18.         return *this;   
  19.     }   
  20. };  
 관례라는 것이 굳이 따르지 않아도 되긴 하지만, 표준 라이브러리에 속한 모든 타입에서도 이런 관례를 전부 따르고 있다는 점은 무시 못할 것입니다. 그냥 이것저것 따지지 말고 묻지도 말고 따르면 되는 것입니다.

 * 대입 연산자는 *this의 참조자를 반환하도록 만들자!!
Posted by 모과이IT
,
일단은 객체의 생성 과정중에 가상함수를 호출 하면 어떤일이 발생하는지 알아 보겠는데, 그전에 객체의 생성과정중 객체 생성 순서를 알아 보도록 하겠습니다. 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Base   
  5. {   
  6. public:   
  7.     Base(){cout<<"base class 생성자 호출"<<endl;}   
  8.     ~Base(){cout<<"base class 소멸자 호출"<<endl;}   
  9. };   
  10.   
  11. class Derived :public Base   
  12. {   
  13. public:   
  14.     Derived(){cout<<"Derived class 생성자 호출"<<endl;}   
  15.     ~Derived(){cout<<"Derive class 소멸자 호출"<<endl;}   
  16. };   
  17.   
  18. int main()   
  19. {   
  20.     Derived* d = new Derived;   
  21.        
  22.        
  23.     delete d;   
  24.   
  25.     return 0;   
  26. }  
 일반적으로 파생 클래스에서 객체가 생성될때 기본 클래스에서 생성자가 먼저 호출이 됩니다.  그 다음 파생 클래스 생성자 호출 소멸자 호출, 그리고 마지막으로 기본 클래스의 소멸자가 호출되는 순서를 가집니다.  하지만 여기서 기본 클래스에서 가상함수 Call을 호출하고 있다고 가정해 봅시다. 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Base   
  5. {   
  6. public:    
  7.     Base(){cout<<"Base 생성자 호출"<<endl; Call();}   
  8.     virtual ~Base(){cout<<"Base 소멸자 호출"<<endl;}   
  9.     virtual void Call()=0;   
  10. };   
  11.   
  12. class Derived : public Base   
  13. {   
  14. private:   
  15.     const char* Message;   
  16. public:   
  17.     Derived(const char* _Message):Message(_Message) {cout<<"Derived 생성자 호출"<<endl;}   
  18.     ~Derived(){cout<<"Derived 소멸자 호출"<<endl;}   
  19.     void Call(){cout<<"Derived Call 함수 메세지 출력 : "<< Message<<endl;}   
  20.   
  21. };   
  22.   
  23. int main()   
  24. {   
  25.     Derived* d = new Derived("test");   
  26.        
  27.     delete d;   
  28.   
  29.     return 0;   
  30. }  
  일단 기본 클래스에서 함수를 호출을 했으므로 오버라이딩 하고 있는 Derived 클래스의 Call()함수를 호출하게 됩니다. 하지만 여기서 문제가 있습니다. 바로 호출되는건 Derived 클래스의 Call함수가 아닌 Base 클래스의 Call함수가 호출된다는것입니다. 그래서 아래와 같은 에러가 나옵니다. 
  앞서 Base 와 Derived 클래스의 생성과정을 얘기 했다시피, Bese 클래스는 Derived 클래스 생성자보다 앞서서 실행되기 때문에 Base 클래스 생성자가 돌아가고 있을 시점에 Derived 클래스 데이터 멤버는 아직 초기화된 상태가 아니기 때문입니다. 이런 초기화 되지 않는 영역을 건드린다는 것은 아주 위험성이 높은 상황이라고 말할 수 있습니다. 

  그럼 이런 문제에 대한 대처 방법에 대해서 알아 봅시다. 기본 클래스 부분이 생성될때는 가상 함수를 호출한다 해도 기본 클래스의 울타리를 넘어 내려갈 수 없기 때문에 가상 함수를 비가상 함수로 바꾸어  필요한 초기화 정보를 파생클래스 쪽에서 기본 클래스 생성자로 올려주도록 만들어 줌으로써 부족한 부분을 역으로 채운면 되는 것입니다. 무리 없이 출력 되는 것을 볼 수 있습니다.
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Base   
  5. {   
  6. public:    
  7.     Base(const char* _Message){cout<<"Base 생성자 호출"<<endl; Call(_Message);}   
  8.     virtual ~Base(){cout<<"Base 소멸자 호출"<<endl;}   
  9.     void Call(const char* Message){cout<<Message<<endl;}   
  10. };   
  11.   
  12. class Derived : public Base   
  13. {   
  14. public:   
  15.     Derived(const char* _Message) : Base(_Message) {cout<<"Derived 생성자 호출"<<endl;}   
  16.     ~Derived(){cout<<"Derived 소멸자 호출"<<endl;}   
  17.   
  18. };   
  19.   
  20. int main()   
  21. {   
  22.     Derived* d = new Derived("test");   
  23.   
  24.     delete d;   
  25.     return 0;   
  26. }  

Posted by 모과이IT
,
  1. #include <iostream>   
  2. #include <vector>   
  3. using namespace std;   
  4.   
  5. class DBConnection   
  6. {   
  7. public:   
  8.        
  9.     //DBConnection 객체를 반환하는 함수.   
  10.     static DBConnection create()    
  11.     {   
  12.         cout << "DBConnection::create()" << endl;   
  13.   
  14.         DBConnection temp;   
  15.         return temp;   
  16.     }   
  17.   
  18.     //연결을 닫는다. 연결이 실패하면 예외를 던진다.   
  19.     void close(){ cout << "DBConnection::close()" << endl; }       
  20. };   
  21.   
  22. class DBConn       
  23. {   
  24. public:   
  25.     DBConn(DBConnection temp)   
  26.     {   
  27.         cout << "DBConn() 생성자" << endl;   
  28.         db = temp;   
  29.     }   
  30.        
  31.     // 데이터베이스 연결이 항상 닫히도록 확실히 챙겨주는 함수   
  32.     ~DBConn()                 
  33.     {   
  34.         cout << "~DBConn() 소멸자" << endl;   
  35.         db.close(); // close()함수에서 에러가 발생하면 문제가 발생한다.    
  36.                        
  37.     }   
  38. private:   
  39.     DBConnection db;   
  40. };   
  41.   
  42.   
  43. int main()   
  44. {   
  45.     // DBConnection 객체를 생성하고 이것을 DBConn 객체로 넘겨서 관리를 맡긴다.   
  46.     DBConn dbc(DBConnection::create());    
  47.   
  48.     return 0;   
  49. }     
 위와 같이 데이터베이스를 연결하는 클래스가 있다고 가정해 봅시다. 보시다시피, 사용자가 DBConnection 객체에 대해 close를 직접 호출해야 하는 설계임을 알 수 있습니다. 이 close 호출만 성공하면 아무 문제될 것이 없는 코드 인데,  여기서 close 호출 시 예외가 발생했다고 가정해 보면 어떨까요? DBConn의 소멸자는 분명히 이 예외를 나가도록 내버려 둘 것입니다. 이게 바로 문제점입니다. 예외를 던지는 소멸자는 문제점을 가지고 있습니다. 그리고 이 문제를 피하는 방법은 두가지 정도로 나뉩니다. 
 문제해결 하나!  
 : close에서 예외가 발생하면 프로그램을 바로 끝낸다. 대개 abort를 호출해준다.
  1. DBConn::~DBConn()   
  2. {   
  3.     cout << "~DBConn() 소멸자" << endl;   
  4.   
  5.     try  
  6.     {   
  7.         db.close();   
  8.     }   
  9.     catch(...)   
  10.     {   
  11.         // close 호출이 실패했다는 로그를 작성.   
  12.         abort();   
  13.     }   
  14. }  
 객체 소멸이 진행 되다가 에러가 발생한 후에 프로그램 실행을 계속할 수 없는 상황에서는 괜찮을 선택일 것입니다. 소멸자에서 생긴 예외를 그대로 나가도록 했다가는 정의되지 않은 동작에까지 이를 수 있다면, 그런 불상사를 막아버리는 방법도 괜찮은 방법이겠죠?

 문제해결 둘!  
 : close를 호출한 곳에서 일어난 예외를 삼켜 버린다.
  1. DBConn::~DBConn()   
  2. {   
  3.     cout << "~DBConn() 소멸자" << endl;   
  4.   
  5.     try  
  6.     {   
  7.         db.close();   
  8.     }   
  9.     catch(...)   
  10.     {   
  11.         // close 호출이 실패했다믄 로그를 작성.   
  12.     }   
  13. }  
 이 방법은 중요한 정보(무엇이 잘못됐는지)가 묻혀 버리기 때문에 대부분의 경우에는 좋은 방법은 아니지만,  때에 따라서는 불완전한 프로그램 종료 혹은 미정의 동작으로 인해 입는 위험을 감수하는 것보다 그냥 예외를 먹어버리는게 나을 수도 있다는 것입니다. 단 이 방법이 제대로 빛을 보려면, 발생한 예외를 그냥 무시한 뒤라도 프로그램이 신뢰성 있게 실행을 지속할 수 있어야 합니다.

 하지만 위 둘의 방법은 각자의 문제점을 가지고 있습니다. 중요한것은 close가 최초로 예외를 던지게 된 요인에 대해 프로글매이 어떤 조치를 취할 수 있는가인데, 이런 부분에 대한 대책이 없기 때문입니다. 그럼 더 나은 방법은 뭐가 있는지 살펴 보도록 하죠. 

 새로운 문제 해결  
 : 그렇다면 DBConn 인터페이스를 잘 설계해서, 발생할 소지가 있는 문제에 대처할 기회를 사용자가 가질 수 있도록 하면 어떨까요? 예를 들어 DBConn에서 Close 함수를 직접 제공하게 하면 이 함수의 실행 중에 발생하는 예외를 사용자가 직접 처리할 수 있을 것입니다.  DBConnection이 닫혔는지의 여부를 유지했다가, 닫히지 않았으면 DBConn의 소멸자에서 닫을 수도 있을 것이고, 이렇게 하면 데이터베이스 연결이 누출 되지 않습니다. 하지만 소멸자에서 호출하는 close마저 실패한다면 이야기가 달라지겠지만 말이죠. 
  1. #include <iostream>   
  2. #include <vector>   
  3. using namespace std;   
  4.   
  5. class DBConnection   
  6. {   
  7. public:   
  8.     // DBConnection 객체를 반환하는 함수.   
  9.     static DBConnection create()   
  10.     {   
  11.         cout << "DBConnection::create()" << endl;   
  12.         DBConnection temp;   
  13.         return temp;   
  14.     }   
  15.   
  16.     // 연결을 닫는다. 연결이 실패하면 예외를 던진다.   
  17.     void close(){ cout << "DBConnection::close()" << endl; }       
  18. };   
  19.   
  20. class DBConn           
  21. {   
  22. public:   
  23.     DBConn(DBConnection temp)   
  24.     {   
  25.         cout << "DBConn() 생성자" << endl;   
  26.         db = temp;   
  27.     }   
  28.   
  29.     void close()               
  30.     {                          
  31.         cout << "close() 함수 호출" << endl;   
  32.         db.close();   
  33.         closed = true;   
  34.     }   
  35.        
  36.     ~DBConn()          
  37.     {   
  38.         cout << "~DBConn() 소멸자" << endl;   
  39.         if (!closed)   
  40.         {   
  41.             try  
  42.             {   
  43.                 cout << "try::close() 함수 호출" << endl;   
  44.                 db.close(); // 사용자가 연결을 안 닫았으면 여기서 닫는다.   
  45.             }   
  46.             catch(...)   
  47.             {   
  48.                 // close 호출이 실패했다는 로그를 작성.   
  49.             }   
  50.         }   
  51.     }      
  52.   
  53. private:   
  54.     DBConnection db;   
  55.     bool closed;   
  56. };   
  57.   
  58.   
  59. int main()   
  60. {   
  61.     DBConn dbc(DBConnection::create());    
  62.     dbc.close();   
  63.   
  64.     return 0;   
  65. }     
 여기에서 어떤 동작이 예외를 일으키면서 실패할 가능성이 있고 또 그 예외를 처리해야 할 필요가 있다면, 그 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야 한다는 것이 포인트 입니다. 왜냐하면 예외를 일으키는 소멸자는 시한폭탄과 마찬가지라서 프로그램의 불완전 종료 혹은 미정의 동작의 위험을 내포 하고 있기 때문입니다. 
 위의 예제를 보면 사용자가 호출할 수 있는 close 함수를 둬서, 사용자에게 에러를 처리할 수 있는 기회를 주고 있습니다. 이것마저 없다면 사용자는 예외에 대처할 기회를 포착하지 못하게 될테죠. 사용자가 이 기회를 무시 했다고 해도 DBConn이 close 함수를 호출해 줄것이므로 문제는 없습니다. 

 * 소멸자에서는 예외가 빠져나가면 안됩니다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 합니다. 
 * 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 합니다.
Posted by 모과이IT
,
Effective C++에 있는 내용이라고 겁먹지 말자. 우리는 이미 이 내용을 제가 C++ 포스팅 하면서 언급을 한 적이 있습니다. 왜 다형성을 가진 기본 클래스에서는 소멸자를 가상 소멸자로 써야 하는가? 우선 다형성과 그 이유에 대해서 먼저 보시죠.



  위 두 포스팅만 보더라도 왜 가상 소멸자를 쓰는지 이유는 충분히 알 수 있을 것입니다.  가상 함수를 C++에서 구현하려면 클래스에 별도의 자료구조가 하나 들어가야 합니다. 이 자료구조는 프로그램 실행 중에 주어진 객체에 대해 어떤 가상 함수를 호출해야 하는지는 결정하는 데 쓰이는 정보인데, 실제로는 포인터의 형태를 취하는 것이 대부분이고, 이를 가상 함수 테이블 포인터(Virtual table pointer) 즉, vptr이라는 이름으로 부릅니다. vptr은 가상 함수의 주소, 포인터들의 배열을 가리키고 있으며 가상 함수 테이블 포인터의 배열은 가상 함수 테이블(virtual table). vtbl이라고 부릅니다.  가상 함수를 하나라도 갖고 있는 클래스는 반드시 그와 관련된 vtbl을 갖고 있습니다. 어떤 객체에 대해 어떤 가상 함수가 호출되려고 하면, 호출되는 실제 함수는 그 객체의 vptr이 가리키는 vtbl에 따라 결정이 됩니다. vtbl에 있는 함수 포인터들 중 적절한 것이 연결된다는 의미이죠. 

  지금까지 가상 소멸자에 대해서만 이야기 했는데, 순수 가장 소멸자(pure virtual destructor)는 어떨까요? 순수 가상 함수는 해당 클래스를 추상 클래스(abstract class)로 만듭니다. 하지만 어떤 클래스가 추상 클래스였으면 좋겠는데 마땅히 넣을 만한 순수 가상 함수가 없을 때도 생기게 마련인데요. 이럴 때에는 어떻게 할까요?
 추상클래스는 본래 기본 클래스로 쓰일 목적으로 만들어진 것이고, 기본 클래스로 쓰이려는 클래스는 가상 소멸자를 가져야 합니다. 한편 순수 가상 함수가 있으면 바로 추상 클래스가 되죠. 따라서, 추상 클래스로 만들고 싶은 클래스에 순수 가상 소멸자를 선언하는 것입니다. 
  1. class AWOV{   
  2. public:   
  3.     virtual ~AWOV() =0;   
  4. };  
 위의 AWOV 클래스는 순수 가상 함수를 가지고 있고, 이 순수 가상 함수가 가상 소멸자 이므로, 앞에서 말한 소멸자 호출 문제로 고민할 필요는 없는 것이지만, 이 순수 가상 소멸자의 정의를 두지 않으면 안됩니다. 
  1. AWOV::~AWOV() {}   ///순수 가상 소멸자의 정의  
 소멸자의 동작 순서를 보자면, 상속 구조에서 가장 말단에 있는 파생 클래스의 소멸자가 먼저 호출되고, 기본 클래스 쪽으로 거쳐 올라가면서 각 기본 클래스의 소멸자가 하나씩 호출이 됩니다. 컴파일러는 ~AWOV의 호출 코드를 만들기 위해 파생 클래스의 소멸자를 사용할 것이므로, 위 함수의 본문을 준비해 두어야 합니다. (이 부분이 없다면 링커 에러가 발생합니다.)
  모든 기본 클래스가 다형성을 갖도록 설계된 것은 아닙니다. 예를 들어 표준 string 타입이나 STL 컨테이너 타입은 기본 클래스는 커녕 다형성의 흔적조차 없기 때문이죠. 한편, 기본 클래스로는 쓰일 수 있지만 다형성은 갖지 않도록 설계된 클래스도 있는데 이런 클래스에서 이들에게서 가상 소멸자를 볼 수 없는 것은 기본 클래스의 인터페이스를 통한 파생 클래스 객체의 조작이 허용되지 않기 때문이죠.  

 * 다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 합니다. 즉, 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 이 클래스의 소멸자도 가상 소멸자이어야 합니다. 
 * 기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언하지 말아야 합니다.
Posted by 모과이IT
,
이 항목은 이전 항목 5와 연장선상에 있는 항목이라고 할 수 있는데요, 이 세상에 복사가 불가능한 유일한 문서가 있다고 가정해 봅시다. 그 문서를 이름이 Unique_Doc 이라고 해 봅시다. 이 Unique_Doc를 나타내는 클래스가 있다고 해봅시다.
  1. class Unique_Doc   
  2. {   
  3.       .................   
  4. }  
 이 객체는 복사가 불가능하므로 이것의 사본(copy)을 만드는 것 자체가 이치에 맞지 않는다고 볼 수 있습니다. 그래서 Unique_Doc 객체를 복사하는 아래와 같은 코드는 컴파일 되지 않았으면 하는 생각을 가지게 됩니다. 
  1. void main()   
  2. {   
  3.     Unique_Doc Ud1;   
  4.     Unique_Doc Ud2;   
  5.   
  6.         Unique Ud3(Ud1); //경고 : 절대 사용하지 마시오!!   
  7.   
  8.     Ud1 = Ud2;//경고 : 절대 사용하지 마시오!!   
  9. }  
 하지만 아무리 주석으로 경고를 해놔도, 이것을 사용하지 않도록 100% 막을수는 없을것입니다. 제일 좋은 방법은 이런 복사 기능을 제공하는 함수를 선언하지 않는 것입니다. 하지만 항목 5에서도 살펴봤다시피 클래스의 복사 생성자와 복새 대입 연산자는 우리가 선언을 하지 않아도 외부에서 호출하려고 하면 컴파일러가 알아서 선언을 해버리기 때문에 이런 방법은 통하지 않습니다.  그럼 어떤 방법이 있을까?

 해결 방법  
 첫번째 방법은, 보통 우리가 쓰거나 컴파일러에 의해서 쓰는 복사 생성자와 복사 대입 연산자는 public 멤버에 선언하기 마련입니다. 이렇게 public 멤버에 선언한 이것들을 private으로 선언하는 방법입니다. 일단 클래스 멤버 함수가 명시적으로 선언되기 때문에, 컴파일러는 자신의 기본 버전을 만들 수 없게 되고 이 함수들이 private의 접근성을 가지므로 외부로부터의 호출을 차단할 수 있는 것입니다. 

 하지만 여기에도 허점이 있는데, private 멤버 함수는 그 클래스의 멤버 함수 및 프렌드(friend) 함수가 호출 할 수 있다는 점입니다. 이점까지 막기 위해서는 복사 생성자와 대입 연산자를 private에 선언만 해놓고 구현은 하지 않는 방법을 쓸 수 있습니다. 이런 방법은 '기법'으로까지 굳어져 C++의 iostream 라이브러리에 속한 몇몇 클래스에서도 복사 방지책으로 쓰이고 있기 까지 합니다. (아래와 같이 사용 할 수 있겠죠)
  1. class Unique_Doc{   
  2. public:   
  3.     ///////////생략/////////   
  4. private:   
  5.     Unique_Doc(const Unique_Doc&);   
  6.     Unique_Doc& operator=(const Unique_Doc&);   
  7. };  
 이렇게 선언된 클래스에 사용자가 Unique_Doc 객체의 복사를 시도하려고 하면 링크 시점에서 에러를 보일것입니다. 이것을 프로그래머가 더 보기 편하게 컴파일단으로 이 에러를 옮길 수도 있습니다. 그 방법은 복사 생성자와 복사 대입연산자를 private로 선언하되, 이것을 해당 클래스에 놓지 말고 별도의 클래스에 넣고 이 기본 클래스로 부터 Unique_Doc 클래스를 파생시키는 것입니다. (상속을 이용) 아래와 같이 구현 될 수 있겠습니다.
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. // 복사 방지 기반 클래스   
  5. class UnCopyable   
  6. {   
  7. protected:   
  8.     UnCopyable()  { }   
  9.     ~UnCopyable() { }   
  10.   
  11. private:   
  12.     UnCopyable( const UnCopyable& );   
  13.     UnCopyable& operator = ( const UnCopyable& );   
  14. };   
  15.   
  16. // 복사되면 안되는 클래스   
  17. class Unique_Doc : private UnCopyable   
  18. {   
  19. public:   
  20.     Unique_Doc(){ }    
  21.     ~Unique_Doc(){}   
  22. };   
  23.   
  24. void main()   
  25. {   
  26.     Unique_Doc Ud1;   
  27.     Unique_Doc Ud2;   
  28.   
  29.     Unique_Doc Ud3(Ud1);   
  30.     Ud1 = Ud2;   
  31. }  
빌드 해보면, 컴파일러 단에서 에러가 나오는것을 보실 수 있습니다. (이런 클래스는 boost에서도 제공하는 것이 있다고 합니다.)


이 UnCopyable 클래스의 장점을 두가지를 뽑을 수 있습니다.
 - 상속 받은 클래스에서는 복사 생성자와 대입연산자를 선언할 필요가 없다.
 -  클래스에서 직접 선언을 하면 어떤 상황에 쓰이냐에 따라서 컴파일 에러가 날 수도 있고, 링크 에러가 날 수도 있는데, 링크 에러 같은 경우 컴파일 에러보다 더 잡기 어렵죠. 하지만 상속 받아서 처리하면 항상 컴파일 에러가 납니다. 
  그 이유는 프로그래머가 상속받은 클래스의 복사 생성자와 대입연산자를 선언/구현하지 않고 해당하는 일을 하려고 할 때 컴파일러에서 자동으로 만들어주는 복사 생성자와 대입연산자를 사용하는데, 이 경우에는 부모 클래스의 복사 생성자와 대입연산자를 자동으로 불러주기 때문입니다. 

컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면, 대응되는 멤버 함수를 private로 선언한 후에 구현은 하지 않은 채로 두십시오. UnCopyable과 비슷한 기본 클래스를 쓰는것도 한 방법입니다.

Posted by 모과이IT
,