C++는 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때 '값에 의한 전달' 방식을 사용한다. (C에서 물려받은 특성)

값에 의한 전달 방식을 사용하면, 함수 매개변수는 실제 인자의 '복사본'을 통해 초기화되며, 함수가 반환할 때 함수를 호출한

쪽은 함수가 반환한 값의 '복사본'을 돌려받는다.

 

복사본을 만들어내는 원천이 바로 복사 생성자이므로 '값에 의한 전달'이 고비용 연산이 되기도 한다.
왜 그런지 아래 예제로 확인해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Person
{
public:
    Person();
    virtual ~Person();
 
private:
    std::string strName;
    std::string strAddress;
};
 
class Student : public Person
{
public:
    Student();
    ~Student();
 
private:
    std::string strSchoolName;
    std::string strSchoolAddress;
};
 
 
bool ValidateStudent(Student s); //유효한 학생 정보인지 체크하는 함수
 
void Func()
{
    Student plato;
 
    bool platoIsOK = ValidateStudent(plato); //plato 객체 체크.
}
cs

 

plato로부터 매개변수 s를 초기화시키기 위해 Student의 복사 생성자가 호출된다.
그리고 s는 ValidateStudent 함수가 종료되면 소멸된다.
즉, ValidateStudent가 호출되면 Student의 복사 생성자가 한 번 호출되고 Student의 소멸자가 한 번 호출된다.

 

그러나 Student에는 string 객체 2개가 있기 때문에, Student 객체가 생성될 때마다 멤버 객체들도 덩달아 생성되어야 한다.
게다가 부모클래스인 Person객체도 생성되어야 하므로 Person 객체의 string 객체 2개 또한 생성된다.

Student 객체를 값으로 전달하면, Student 복사 생성자 호출 1번, Person 복사 생성자 호출 1번, string 복사 생성자 호출

4번이 일어난다. 그리고 Student 객체의 사본이 소멸될 때도 앞에서 호출된 생성자 호출 횟수만큼 소멸자도 호출된다.
결과적으로 총 생성자 6번, 소멸자 6번이 호출되는 것이다.

 

이런 불필요한 과정을 거치지 않도록 하기 위해서 매개변수를 넘길 때 상수객체에 대한 참조자로 전달하면 된다.

 

1
2
3
4
5
6
7
8
bool ValidateStudent(const Student& s); //유효한 학생 정보인지 체크하는 함수
 
void Func()
{
    Student plato;
 
    bool platoIsOK = ValidateStudent(plato); //plato 객체 체크.
}
cs

 

이렇게 하면 새로 만들어지는 객체같은 것이 없기 때문에, 생성자와 소멸자가 호출되지 않는다.
그리고 원래 값에 의한 전달은 사본으로 작업이 되기 때문에 원본에는 아무런 영향이 없었지만 참조자를 전달하였으므로

작업 도중 원본에 영향을 미칠 수 있다. 이러한 문제를 방지하기 위해서 const를 붙여주는 것이다.

 

또한 참조자로 매개변수를 넘기면 파라미터로 넘어온 데이터가 짤리는 문제(복사 손실, slice problem)도 방지할 수 있다.
아래 예처럼 함수의 파라미터는 부모 클래스형이고 함수 호출부의 파라미터는 자식 클래스 형인 경우를 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Window
{
public:
    ...
    std::string name() const;     //해당 윈도우 이름을 반환해주는 함수.
    virtual void display() const//일반적인 윈도우를 화면에 그려주는 함수.
};
 
class windowWithScrollBars : public Window
{
public:
    ...
    virtual void display() const//스크롤바 기능이 있는 윈도우를 그려주는 함수.
};
 
void printNameAndDisplay(Window w) //함수의 파라미터는 부모클래스 형.
                                   //값에 의한 전달이므로 자식클래스부분은 싹 짤림. (slice)
{
    std::cout << w.name();
    w.display();
}
 
void Func()
{
    windowWithScrollBars wBar;
 
    printNameAndDisplay(wBar);        //함수 호출부의 파라미터는 자식클래스 형
}
cs

 

위의 예제처럼 값에 의한 전달을 하는 경우에는 매개변수 w가 생성되기는 하는데 부모클래스에 대한(Window) 데이터만

생성된다.

즉, 자식클래스 부분은 생성되지 않고 짤리게 되며 여기서w.Display()를 호출하면 Window 클래스의 Display 함수가 호출된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
void printNameAndDisplay(const Window& w) //함수의 파라미터는 부모클래스 형.
                                          //참조에 의한 전달이므로 자식클래스 부분도 전달됨.
{
    std::cout << w.name();
    w.display();
}
 
void Func()
{
    windowWithScrollBars wBar;
 
    printNameAndDisplay(wBar);        //함수 호출부의 파라미터는 자식클래스 형
}
cs

 

이제 w는 어떤 타입의 윈도우 객체가 넘겨지더라도 넘겨진 객체 자체의 성질을 그대로 갖게 된다.

 

참조자는 보통 포인터를 써서 구현된다. 즉, 참조자를 전달한다는 것은 결국 포인터를 전달하는 것과 같은 것이다.
그러므로 기본 제공 타입(int 등..)일 경우에는 그냥 값에 의한 전달을 해도 상관이 없다.

그리고 한 가지 주의할 점은 객체 크기가 작고 멤버 변수 얼마 없다고 해서 '값에 의한 전달'을 선택해서는 안된다.

이유는 컴파일러 중에는 기본제공 타입과 사용자 정의 타입을 아예 다르게 취급하는 것들이 있기 때문이다.
진짜 int나 double은 레지스터에 넣어주지만, double 하나로만 만들어진 사용자 정의 타입은 레지스터에 넣지 않는다.
그러므로 사용자 정의 타입은 참조에 의한 전달을 쓰는 편이 좋다. 포인터(참조자도 포인터로 구현됨)만큼은 레지스터에

들어가기 때문이다.

 

'값에 의한 전달'이 저비용이라고 가정해도 괜찮은 타입은 기본 제공 타입, STL 반복자, 함수 객체 타입 이렇게 세 가지 뿐이다.
이외에는 무조건 '참조에 의한 전달'을 사용하자.

 

2줄 요약

 

 - '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달'을 사용하자. 성능뿐만 아니라 복사손실 문제까지도 막아준다.

 - 기본제공 타입, STL 반복자, 함수 객체 타입에만 값에 의한 전달을 사용하자.

+ Recent posts