2016년 3월 9일 수요일

[c++] call by value(값에 의한 호출) & call by reference(참조에 의한 호출), 복사 손실, Reference to const

Call By Value(복사에 의한 함수 호출): 인자로 넘기는 값을 복사해서 새로운 함수에 넘겨주는 방식
값의 복사에 의한 함수 호출을 의미합니다. 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. void swap(int a, int b);  
  5.   
  6. int main()  
  7. {  
  8.     int val1 = 10;  
  9.     int val2 = 20;  
  10.     swap(val1, val2);  
  11.   
  12.     cout<<"val1 : "<<val1<<endl;  
  13.     cout<<"val2 : "<<val2<<endl;  
  14.   
  15.     return 0;  
  16. }  
  17.   
  18. void swap (int a, int b)  
  19. {  
  20.     int temp = a;  
  21.     a = b;  
  22.     b = temp;  
  23.   
  24.     cout<<"a : "<<a<<endl;  
  25.     cout<<"b : "<<b<<endl;  
  26. }  


 위의 예제는 Call by Value 예제와 결과값 입니다. Call by Value는 값을 복사해서 전달하기 때문에
원본의 값이 변경될 가능성이 없다는 특징을 가지고 있지만, 값을 넘겨 줄때마다 고비용 , 복사손실 문제가 발생하는 단점도 있습니다. 
 ※ Call by Value는 Pass by Value 라고 불리기도 합니다. 

Call by Reference(참조에 의한 호출): 주소 값을 인자로 전달하는 함수 호출 
아래 예제를 보시죠. (위와 거의 유사합니다.)
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. void swap(int *a, int *b);  
  5.   
  6. int main()  
  7. {  
  8.     int val1 = 10;  
  9.     int val2 = 20;  
  10.   
  11.     cout<< "Before the swap function" <<endl;  
  12.     cout<<"val1 : "<<val1<<endl;  
  13.     cout<<"val2 : "<<val2<<endl;  
  14.   
  15.     swap(&val1, &val2);  
  16.   
  17.     cout<<endl<<"After Swap function " <<endl;  
  18.     cout<<"val1 : "<<val1<<endl;  
  19.     cout<<"val2 : "<<val2<<endl;  
  20.   
  21.     return 0;  
  22. }  
  23.   
  24. void swap (int *a, int *b)  
  25. {  
  26.     int temp = *a;  
  27.     *a = *b;  
  28.     *b = temp;  
  29. }  
  위의 예제에서 알 수 있듯이 Call by Reference는 복사손실과 고비용 문제에서는 벗어났지만, 원본의 값의 변경이 일어 날 수 있다는 중대한 문제점을 안고 있는걸 볼 수 있습니다. 

 Call by Value 나 Call by Reference 둘다 문제점을 가지고 있는데요, 



[항목 20] Effective C++ : 값에 의한 전달보다는 상수 객체 참조자에 의한 전달 방식을 택하는 편이 대게 낫다.

Pass by Value(Call by value) 의 문제점 
   1. 고비용 문제 
     - 해결 방안 : const,. 상수 객체에 대한 참조자로 전달 하는 방법 
   2. 복사손실 (slicing problem) 문제
     - 해결 방안 : 


  기본적으로 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를 쓰자~!!!



6-3-라.C++의 참조호출

C언어는 포인터를 사용하여 참조 호출 흉내를 낸다. C++은 포인터를 이용하는 방법외에 레퍼런스라는 개념으로 참조 호출을 추가로 지원한다. 다음 예제의 plusref2함수가 C++의 레퍼런스를 사용하여 참조 호출을 하는 예이다.

  : CallRef2
#include <Turboc.h>

void plusref2(int &a);

void main()
{
     int i;

     i=5;
     plusref2(i);
     printf("결과=%d\n",i);
}

void plusref2(int &a)
{
     a=a+1;
}

int &a라는 좀 이상한 타입을 사용하는데 이것이 레퍼런스이다. 호출부에서는 &i가 아닌 i를 전달하며 함수의 본체에서는 *a를 쓰지 않고 a를 바로 쓴다. C++의 레퍼런스에 대해서는 15장에서 상세하게 다루므로 여기서는 구경만 해 보고 넘어가기로 하자. 지금 단계에서 레퍼런스를 논하는 것은 순서에 맞지 않다. C++이 참조 호출을 위해 레퍼런스라는 새로운 개념을 지원하기는 하지만 C의 포인터를 이용한 참조 호출도 알아 두어야 한다.
C언어의 포인터를 통한 참조 호출은 사실 엄밀한 의미의 참조 호출이라고 볼 수 없다. 왜냐하면 이때도 함수로 전달되는 것은 변수 그 자체가 아니라 변수의 번지값(Address Value)이라는 특수한 값이기 때문이다. C는 언제나 값만 전달한다. 다만 이 번지값으로 실인수를 조작할 수 있도록 흉내만 낼 수 있으며 흉내를 통해 참조 호출 효과가 발생할 뿐이다. 그래서 혹자는 C언어는 참조 호출을 지원하지 않는다고 주장한다. 이는 문법적으로는 사실이지만 실제로는 포인터를 통해 참조 호출과 동일한 효과를 낼 수 있으므로 다소 비약적인 표현이라고 할 수 있다.

C언어의 함수 호출방식은 모두 값 호출 방식이되 편의상 포인터를 통한 호출을 참조 호출이라고 부를 뿐이다. 그렇다면 C++의 경우는 어떠할까? C++의 레퍼런스는 C의 포인터를 사용하는 방법보다는 더 발전된 방법이지만 이것도 엄밀하게 따지면 값 호출이다. 다음에 레퍼런스를 깊게 배워 보면 알겠지만 레퍼런스는 내부적으로 포인터를 흉내낸다. 따라서 레퍼런스를 사용한 참조 호출은 포인터를 흉내내어 참조 호출을 흉내내는 아주 기만적인 참조 호출일 뿐이다.


댓글 없음:

댓글 쓰기

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

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