대입과 초기화를 구분하자  
  1. class B {...};   
  2. class A {   
  3. public:   
  4.     A(const std::string& name, const std::string& address, const std::list<B>& phone);   
  5. private:   
  6.     std::string theName;   
  7.     std::string theAddress;   
  8.     std::list<B> thePhone;   
  9.     int num;   
  10. };   
  11.   
  12. A::A(const std::string& name, const std::string& address, const std::list<B>& phone)   
  13. {   
  14.     thName = name;   
  15.     theAddress = address;   
  16.     thePhone = phone;   
  17.     num = 0;   
  18. }  
  C++규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화 되어야 한다고 명시되어 있다. 위의 예제는 초기화가 아니라 대입이다. 초기화는 진작 지나갔다. A의 생성자에 진입하기도 전에 기본 생성자가 호출된 것이다. 하지만 num은 미리 초기화 되지 않았다. 기본제공 타입의 데이터 멤버이기 때문이다. 생성자 안에서 대입되기 전에 초기화 되리란 보장이 없다. 따라서 이런경우 아래와 같이 초기화 리스트를 사용한다.
  1. A::A(const std::string& name, const std::string& address, const std::list<B>& phone)   
  2.     : theName(name), theAddress(address), thePhone(phone), num(0)    
  3. {   
  4.   
  5. }  
사용자가 원하는 값을 주고 시작하는 점은 같지만, 더 효율적일 가능성이 크다. 초기화 리스트에 들어가는 인자는 바로 데이터 멤버에 대한 생성자의 인자로 쓰이기 때문이다. 기존 방법은 기본 생성자를 호출하고 복사대입 연산자를 호출하는 방식이지만 초기화 리스트를 사용하면 복사생성자를 한 번 호출하므로 더 효율적이다. 기본타입은 상관 없지만 초기화 리스트에 넣어 두는 쪽이 좋다.

 기본제공 타입의 멤버를 초기화 리스트로 넣는 일이 의무일 경우가 있다. 이것은 바로 상수이거나, 참조자로 되어 있는 데이터 멤버의 경우엔 반드시 초기화되어야 하기 때문이다. 상수와 참조자는 대입 자체가 불가능하기 때문이다.

※ 객체를 구성하는 데이터의 초기화 순서
1. 기본 클래스는 파생 클래스보다 먼저 초기화 된다. 
2. 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화 된다. 

※ 비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.
 - 정적객체(static object) : 자신이 생성된 시점부터 프로그램이 끝날때 까지 살아 있는 객체.
 - 정적객체 범주에 들어가는 것
  1. 전역 객체
  2. 네임스페이스 유효범위에서 정의된 객체
  3. 클래스 안에서 static으로 선언된 객체
  4. 함수 안에서 static으로 선언된 객체
  5. 파일 유효범위에서 static으로 정의된 객체

  이들중 4번은 지역 정적 객체라고 하고, 나머지는 비지역 정적 객체라고 한다. 다섯종류 모두 프로그램이 끝날때 자동으로 소멸된다. 즉, main() 실행이 끝날때 정적 객체의 소멸자가 호출된다. 

※ 서로 다른 번역 단위에 정의된 비지역 정적 객체들 사이의 상대적인 초기화 순서는 정해져 있지 않다.  설계에 약간의 변화만 주면 이 문제를 사전에 봉쇄할수 있다. 비지역 정적 객체를 하나씩 맡는 함수를 준비... 이안에 각 각체를 넣는다. 함수속에서도 정적 객체로 선언.
  함수에서는 이들에 대한 참조자를 반환하게 만든다. 그리고 함수 호출로 대신한다. 지역 정적 객체는 함수 호출 중에 그 객체의 정의에 최초로 닿았을 때 초기화 되도록 만들어져 있다. 

 마무리...  
 - 기본제공 타입 객체는 직접 초기화 하자.
 - 객체의 모든 부분에 대한 초기화에는 멤버 초기화 리스트를 사용하자.
 - 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계하자.  -> 비지역 정적 객체를 지역 정적 객체로 바꾸면 된다. 
Posted by 모과이IT
,
 우선 이번 항목에서는 쟁점이라고 할 수 있는 것은 Pass by Value(Call by value) 의 두가지 문제점을 제기 하는데요. 첫번째는 바로 고비용 문제 입니다. 
  기본적으로 C++는 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때 값에 의한 전달 방식을 사용하는데요. 특별히 다른 방식을 지정하지 않는 한, 함수 매개변수는 실제 인자의 '사본'을 통해 초기화되며, 어떤 함수를 호출 한 쪽은 그 함수가 반환한 값의 사본을 돌려받습니다. 이들 사본을 만들어 내는 곳이 바로 복사 생성자인데, 이런 점 때문에 고비용의 연산이 되기도 합니다.

  한번 예제를 보면서 알아 보도록 하겠습니다. 
  1. #include <iostream>   
  2. #include <string>   
  3. using namespace std;   
  4.   
  5. class person{   
  6. public:   
  7.     person() : m_strName("name"), m_strAddress("address")   
  8.     {   
  9.         cout<<"person default con call"<<endl;   
  10.     }   
  11.     person(const person& rhs)   
  12.     {   
  13.         this-> m_strName = rhs.m_strName;   
  14.         this-> m_strAddress = rhs.m_strAddress;   
  15.   
  16.         cout<<"person copy con call"<<endl;   
  17.     }   
  18.     virtual ~person()   
  19.     {   
  20.         cout<<"~person call "<<endl;   
  21.     }   
  22. protected:   
  23.     string  m_strName;   
  24.     string  m_strAddress;   
  25. };   
  26.   
  27. class student :public person   
  28. {   
  29. public:   
  30.     student() : m_strSchoolName("school")      
  31.     {      
  32.         cout<<"student default con call"<<endl;    
  33.     }   
  34.     student (const student& rhs)   
  35.     {   
  36.         this ->m_strName = rhs.m_strName;   
  37.         this->m_strAddress = rhs.m_strAddress;   
  38.         this->m_strSchoolName = rhs.m_strSchoolName;   
  39.         cout<<"student copy con call"<<endl;   
  40.     }   
  41.   
  42.     ~student()   
  43.     {   
  44.         cout<<"~student call"<<endl;   
  45.     }   
  46. private:   
  47.     string m_strSchoolName;   
  48. };   
  49.   
  50. bool validatestudent (student stu); //student를 값으로 전달받는 함수   
  51.   
  52. int main()   
  53. {   
  54.     student stu;   
  55.   
  56.     cout<<"-------- pass by value ----------"<<endl;   
  57.     validatestudent(stu);   
  58.     cout<<"---------------------------------" <<endl;   
  59.   
  60.     return 0;   
  61. }   
  62.   
  63. bool validatestudent(student stu)   
  64. {   
  65.     return true;   
  66. }  
실행결과 :


 위의 예제를 실행해 보면 Pass by Value방식에는 문제가 없습니다. 실행을 해보면 생성자와 소멸자가 제대로 실행되는게 보입니다. 하지만, student 객체 생성시 student 객체에는 string 객체 두개가 멤버로 들어 있기 때문에, cout으로 명시적으로 보여준 생성자 , 소멸자 생성, 소멸의 과정 뿐만 아니라, student 객체가 생성될때마다 이들 string 객체 두개도 덩달아 생성되고 소멸이 되고, 이때마다 생성자, 소멸자를 호출 해야 하므로, 고비용의 문제가 발생하게 됩니다. 

그래서 이런 고비용 문제를 피하기 위한 방법이 있습니다. 바로 "상수 객체에 대한 참조자"로 전달하는 방법입니다. (Reference to const바로 Call by Reference 방법에 상수화를 더한 것이죠. 

방법은 bool validatestudent부분에서 괄호 부분을 (const student &stu); 이렇게 바꾸면 됩니다.이 방식의 좋은점은 새로 만들어지는 객체 같은 것이 없기 때문에, 생성자와 소멸자 호출도 당연히 없습니다.(Call by Reference의 장점과 같죠) 이로서 고비용의 부담문제를 해결 할수 있습니다. 

또한 여기서 주목해야 할 점은 const 상수 선언인데, 그 전에  call by value와 call by referecne가 가진 특징을 다시 한번 상기 시켜 봅시다. (제가 이미 포스팅한  링크 참조)


 우리가 사용하려는 참조자에 의한 전달 방법으로 고비용 문제는 해결했지만, 원본값이 변경될 우려가 있으므로, const를 씀으로서 이런 변화로부터 안전하게 보호를 해주는 것입니다. 

두번째 문제는 바로 복사손실 (slicing problem) 문제 입니다. Call by Reference를 씀으로 인해서 바로 복사손실을 방지할 수 있는 장점이 있는데요. 우선 예제 코드를 보겠습니다. 예제 코드는 윈도우 클래스로 만들어지는 객체는 이름을 갖고 있고, 이 이름은 name 멤버 함수로 얻어내고,  display 함수로 이를 화면표시를 해주는 예제 인데요. 
  1. #include <iostream>   
  2. #include <string>   
  3. using namespace std;   
  4.   
  5. class window   
  6. {   
  7. public:   
  8.     window()   
  9.     {   
  10.         cout<<"window class construcgtor call "<<endl;   
  11.         window_name = "window is shit";   
  12.     }   
  13.     string name() const  
  14.     {   
  15.         cout<<"window::name() call "<<endl;   
  16.         return window_name;   
  17.     }   
  18.     virtual void display() const  
  19.     {   
  20.         cout<<"-------------------------"<<endl;   
  21.         cout<<"window::display() call "<<endl;   
  22.         cout<<"-------------------------"<<endl;   
  23.   
  24.     }   
  25.     ~window()   
  26.     {   
  27.         cout<<"~window() call" <<endl;   
  28.     }   
  29. private:   
  30.     string window_name;   
  31. };   
  32.   
  33. class wnidowwithscrollbar :public window   
  34. {   
  35. public:   
  36.     wnidowwithscrollbar()   
  37.     {   
  38.         cout<<"wnidowwithscrollbar() call"<<endl;   
  39.     }   
  40.     void display() const  
  41.     {   
  42.         cout<< "------------------------"<<endl;   
  43.         cout<<"wnidowwithscrollbar :: display()"<<endl;   
  44.         cout<<"------------------------"<<endl;   
  45.   
  46.     }   
  47.     ~wnidowwithscrollbar ()   
  48.     {   
  49.         cout<<"~wnidowwithscrollbar() call "<<endl;   
  50.     }   
  51. };   
  52.   
  53. void print_name_and_diplay(window w) //윈도우의 이름을 출력하고 그 윈도우를 화면에 표시 하는 예제 함수입니다.    
  54. {   
  55.     cout<<"------ print name and display ---------"<<endl;   
  56.     cout<<w.name()<<endl;   
  57.     w.display();   
  58. }   
  59.   
  60. int main()   
  61. {   
  62.     wnidowwithscrollbar wsb;   
  63.     print_name_and_diplay(wsb);   
  64.   
  65.     return 0;   
  66. }  

 위 함수들을 파생클래스 객체를 넘긴다면 , 매개변수 w가 생성되기는 하지만, 어떤 타입의 객체가 넘어가든 window 클래스 객체의 면모만을 갖게 되므로, 가상함수를 사용했음에도 불구하고 windowwithscroolbars::display는 출력되지 않는 것이 바로 문제가 됩니다. 여기도 마찬가지로 문제를 해결하기 위해서는 상수 객체에 대한 참조자로 전달 하도록 만들면 됩니다. 
  1. void print_name_and_diplay(const Window &w)  
이렇게 만들면 어떠한 객체 타입이 넘어와도 그 고유 성질을 그대로 갖게 되므로 복사손실은 발생하지 않습니다. 

항목 20의 제목에서 나타난거와 같이 상수객체 참조자에 의한 전달방식이 택하는 편이 고비용, 복사손실 두개의 문제해결능력으로 봐서는 대게는 나은 결과를 보여주지만, 기본제공 타입, STL반복자 그리고 함수 객체 타입, 이 세가지는 이전부터 값으로 전달되도록 설계해 왔기 때문에 이 두가지 경우에는 pass by Value 방법을 사용하는 것이 더 적절합니다. 

 ※ 상황에 따라 적절하게 Call by Value , Call by Reference를 쓰자~!!!
Posted by 모과이IT
,