객체지향프로그래밍(8) - 참조자와 복사 생성자

참조자를 통한 복사 생성자 이해

Featured image

🔚 짧게 하는 복습

✅ 1. 메모리 구조에 대해 알고, 영역별 특징을 안다.

✅ 2. 영역별 변수의 수명을 이해한다.

✅ 3. 소멸자에 대해 안다.

혹시 기억이 안 난다면, 다시 돌아가자


혹시 C언어나 자료구조를 배우면서 가장 어려웠던 것이 무엇인가?

사람마다 다르겠지만, 본인은 포인터가 정말 어려웠다.

주소를 가리킨다는 설명은 포인터가 가지는 기능을 설명하기엔 너무 간단했다.

사실 포인터는 연결리스트나 트리 구현처럼 주소 자체의 의미로도 많이 사용되지만, 일반적인 상황에서 함수에서 특정 변수의 값을 바꾸고 싶을 때 넘기는 용도로 많이 사용되었다.

왜냐하면, 함수에서 매개변수를 받아들일 때는 그 값을 복사하기 때문이었다.

이러한 방식을 Call by Value(값에 의한 호출)라고 한다.

C에서와는 다르게 C++ 다르게 그 변수 자체를 가져오는 방법이 있다.

이를 Call by Reference(참조에 의한 호출)이라고 하고, 오늘은 여기서 참조자를 배울 것이다.


참조자란?

참조자어떤 변수에 별칭을 제공하는 기능이다.

(변수의 자료형) &별칭 = (변수);

의 형식으로 선언되며, &는 주소 연산자와 전혀 상관없다.

포인터와 비슷한가 싶지만, 그냥 아예 다른 개념이다.

참조자는 말 그대로, 그 변수에 다른 이름만 주는 것이기 때문에 자체 메모리를 가지지 않는다.

또한, 포인터와 달리 NULL 등의 값을 가질 수 없다. 무조건 표적이 될 변수가 있어야 한다.

같은 이유로 함수의 반환형으로 잘 사용하지 않는다.. (함수가 종료될 때, 함수 내에서 참조자가 가리키는 로컬 변수가 사라지면 유효하지 않은 참조자가 반환되는 오류가 발생하기 때문이다.)

이 참조자를 수정하면, 원래 변수를 수정하는 것과 같다. 또한, 원래 변수를 수정하면 참조자의 값도 수정된다.

둘은 같은 변수이기 때문이다.

아래의 코드를 직접 실행해 보자.


참조에 의한 호출(Call by Reference)

우리가 두 변수의 값을 교환할 때, C에서는 어떻게 했었는지 기억하는가?

포인터를 이용해서 교환했었다. 그 이유는 새로 만든 스택 프레임에서 매개변수를 받아들일 때 그 값만 복사하기 때문이다.

즉 변수의 값만 복사해서는 바뀌지 않고, 변수 자체 (주소를 통해서)를 이용해야 값을 바꿀 수 있었다.

이러한 접근을 값에 의한 호출(Call by Value)라고 한다.

그런데 변수 자체의 접근? 참조자가 떠오른다면 훌륭하다.

C++에서는 함수의 매개변수로 참조자를 이용할 수 있다.

참조자를 통해 변수 자체 접근을 하고, 값 교환을 시도해보겠다.

함수가 호출되면 아래와 같은 코드가 실행된다.

int &x = data1;
int &y = data2;

x는 data1 그 자체가 되고, y는 data2가 된다.

그렇다면 아래의 코드는 x와 y의 값을 바꾸는 값이니, data1과 data2가 값이 바뀌게 되는 것이다.

이러한 접근은 참조에 의한 호출이라고 한다.

더 이상 주소를 통해 접근하는 것이 아니라, 그 값 자체에 접근할 수 있는 것이다.

이제 이를 통해 복사 생성자라는 것을 생성해보겠다.


복사 생성자

클래스를 만들 때, 보통 디폴트 생성자, 오버로딩 생성자, 복사 생성자 3가지를 만든다.

복사 생성자어떠한 객체의 값을 새로운 객체를 만들 때 그대로 복사해오는 생성자이다.

기본적으로 클래스에서는 암묵적으로 디폴트 복사 생성자를 지원한다.

대입 연산자 =을 이용하면 복사하며 생성이 된다.

하지만 여기서 복사 생성자를 수정하고 싶다면, 참조자를 이용해서 명시적으로 만들어주면 된다.

주의할 점은 const를 꼭 써줘야 한다. 참조자를 이용한다는 것은 원본도 수정될 수 있다는 위험이 있기 때문이다.


근데 포인터 써도 되는 거 아니에요?

맞다. 사실 위에서 다룬 모든 내용은 참조자가 아니라 포인터를 사용해도 된다.

하지만 참조자를 사용하는 데에는 가장 큰 이점이 있다.

포인터는 메모리적 손해가 있다. 참조자는 메모리를 아예 차지하지 않는다.

또한, 값 자체를 복사해오지 않기 때문에 속도가 빠르다.

보통은 공간에서 이득을 보면 시간에서 손해를 보고, 시간에서 이득을 보면 공간에서 손해를 보는 트레이드-오프(Trade-off) 관계다.

하지만, 특이하게 참조자공간적으로나 시간적으로나 참조자가 이득이다.

심지어 조금만 잘못 참조해도 오류가 나는 포인터와 달리, 값 자체를 다루기에 훨씬 안정적이다.

그렇기에 C++에서 단순한 기본자료형을 제외하고, 배열, 구조체, 객체 등을 다룰 때는 Call by reference는 고민의 여지가 없다.

손해를 줄이기 위해 C에서는 포인터, C++에서는 대부분 참조자를 사용한다.


✅ 1. 참조자의 정의를 알고, 포인터와의 차이를 안다.

✅ 2. 참조자의 특징과 장점을 안다.

✅ 3. 복사 생성자가 무엇인지 알고, 디폴트 복사 생성자를 오버로딩해서 복사 생성자를 만들 수 있다.

⚠️ 가끔 포인터를 이용해서 값을 바꾸는 방법을 Call by Value라고 하는데, 이는 대표적인 오개념이다.

⚠️ 참조자를 함수의 반환형은 되도록 사용하지 말자.

⚠️ 복사 생성자에서는 const를 잊어서는 안 된다.

💣 과제, 없음