본문 바로가기

Algorithm 문제 풀이/C++

[C++] 참조자(Reference) 이해하기

참조자(reference)란?

자신이 참조하는 변수를 대신할 수 있는 또 하나의 이름으로, 기존에 선언된 변수에 붙이는 별칭입니다.

미리 만들어져있는 메모리 공간을 다른 변수명으로 접근가능하게 하는 것이 목적입니다.

 

int num1 = 1020;
int& num2 = num1;

이와 같이 변수 앞에 &를 추가하는 방식으로 쓰이게 됩니다.

이때 & 연산자의 경우 이미 선언된 변수 앞에 오면 주소값을 반환하라는 명령이지만, 새로 선언되는 변수 앞에 오면 참조자 선언이 됩니다. 참조자의 수에는 제한이 없으며, 참조자를 대상으로 참조자를 선언하는 것도 가능합니다.

 

참조자의 선언 가능 범위

  1. 참조자는 선언과 동시에 누군가를 참조해야 한다.
  2. 그 참조 대상은 기본적으로 변수가 되어야 한다.
  3. 참조자는 대상을 변경할 수 없다.

 

#include <iostream>

using namespace std;

int main(void) {
	int num1 = 1020;
	int& num2 = num1;

	num2 = 3047;
	cout << "Val: " << num1 << endl;
	cout << "REF: " << num2 << endl;

	cout << "Val: " << &num1 << endl;
	cout << "REF: " << &num2 << endl;
	return 0;
}

결과

전체 코드로 보면 이와 같습니다.

이 코드에서 num2는 앞에 & 연산자를 붙였으므로 num1의 참조자가 됩니다. 따라서 이후부터는 num1이 하는 모든 연산은 num2로 하는 것과 동일한 결과를 보이게 됩니다.

 

더 자세히 알아보면,

 

변수는 메모리 공간을 할당합니다. 그래서 변수 num1을 선언하게 되면, num1이라는 이름으로 메모리 공간이 할당됩니다. 반면에, 참조자는 메모리 공간을 할당하지 않습니다. 이미 만들어져 있는 변수의 메모리 공간을 가르키도록 하여 그 변수에 접근합니다. 코드로 보면, 참조자 선언으로 인해서 num1의 메모리 공간에 num2라는 이름이 추가로 붙게 되었다고 할 수 있습니다. num2가 num1의 메모리 공간에 접근한 것이므로 num1와 주소도 같게 되고 num1이 하는 모든 연산과 동일한 결과가 보이게 되는 것입니다.

 

 

그러면 왜 참조자를 사용하고 언제 사용할까요?

참조자를 이용하면 지역변수의 메모리 공간에도 접근할 수 있게 됩니다. C에서 포인터와 거의 같은 역할을 한다고 볼 수 있는데, 포인터는 주소값을 전달받아 포인터로 변수에 변수에 접근할 수 있는 것이고 C++에서는 참조자를 통해 변수에 접근할 수 있다는 것입니다.

 

대표적인 예시로는 Swap이 있습니다.

 

먼저 참조자를 사용하지 않을 때를 보겠습니다.

 

#include <iostream>

using namespace std;

void SwapByRef2(int ref1, int ref2) {
	int temp = ref1;
	ref1 = ref2;
	ref2 = temp;
}

int main(void) {
	int val1 = 10;
	int val2 = 20;

	SwapByRef2(val1, val2);
	cout << "val1: " << val1 << endl;
	cout << "val2: " << val2 << endl;
	
	return 0;
}

결과

실행결과를 보면, swap이 되지 않은 것을 볼 수 있습니다.

그 이유는 이 코드는 Call by value 방식으로, 함수를 부르는 과정에서 인수를 직접 전달하는 것이 아니라 인수로 건네 줄 변수의 복사본인 '값'을 전달하기 때문입니다. 따라서 이 방식은 원하는 기능을 구현하는 것이 불가능합니다.

복사본 대신 변수 '자체'를 접근 하려면 참조자를 이용한 Call by reference 방식으로 구현해야 합니다.

 

#include <iostream>

using namespace std;

void SwapByRef2(int& ref1, int& ref2) {
	int temp = ref1;
	ref1 = ref2;
	ref2 = temp;
}

int main(void) {
	int val1 = 10;
	int val2 = 20;

	SwapByRef2(val1, val2);
	cout << "val1: " << val1 << endl;
	cout << "val2: " << val2 << endl;
	
	return 0;
}

결과

위 코드를 참조자를 이용한 Call by reference 방식으로 작성한 것입니다.

 

이번에는 swap이 잘 이뤄졌음을 볼 수 있습니다. 매개변수 앞에 & 연산자를 붙여주는 것으로 참조자로 선언이 됩니다. 참조자로 받는 경우 해당 메모리로 접근이 가능해서 스왑이 가능해진 것입니다.

 

 

배열도 참조자의 선언이 가능할까요?

참조자의 선언은 변수의 성향을 지니는 대상이면 가능합니다. 배열의 요소 역시 변수의 성향을 지니기 때문에 참조자의 선언이 가능합니다.

 

# include <iostream>

using namespace std;

int main(void) {
	int arr[3] = { 1, 3, 5 };

	int& ref1 = arr[0];
	int& ref2 = arr[1];
	int& ref3 = arr[2];

	cout << ref1 << endl;
	cout << ref2 << endl;
	cout << ref3 << endl;
    
	return 0;
}

이와 같이 배열의 요소들을 참조자의 선언을 할 수 있음을 볼 수 있습니다.

 

하지만 주의해야할 것이 있는데요❗

 int& ref3 = arr[0];

이 코드를 추가하게 되면 어떻게 될까요?

 

이미 위 코드에서 ref3이 arr[2]를 참조했으므로 arr[0]으로 대상을 변경하려고 하면 빌드 오류가 납니다. 참조자는 대상을 변경할 수 없기 때문입니다. 

 

 

포인터 변수 대상의 참조자 선언

먼저 코드를 보겠습니다.

 

#include <iostream>

using namespace std;

int main(void) {
	int num = 12;
	int* ptr = &num;
	int** dptr = &ptr;

	int& ref = num;
	int* (&pref) = ptr;
	int** (&dpref) = dptr;

	cout << ref << endl;
	cout << *pref << endl;
	cout << **dpref << endl;
	
	return 0;
}

결과

num은 변수, ptr은 포인터 변수, dptr은 이중포인터 변수로 선언을 했는데요. 이와 같이 포인터도 주소값을 저장하는 포인터 변수이기 때문에 참조자의 선언이 가능합니다.

ref는 변수 num의 메모리주소, pref는 포인터 변수 ptr의 메모리 주소, dpref는 이중포인터 변수 dptr의 메모리주소를 참조하였습니다. 결국 다 같은 num을 가르키기 때문에 실행결과 값이 모두 12로 나오게 됩니다.

 


정리

  • 참조자란 기존에 선언된 변수를 참조하는 변수로, 그 변수의 값을 가져오는 것이 아닌, 메모리 공간에 접근하는 것이다.
  • 참조자는 새로 선언되는 변수 앞에 &를 붙여 참조자 선언을 할 수 있고, 선언과 동시에 어떤 변수를 참조해야한다.
  • 배열의 요소, 포인터 변수도 참조자 선언을 할 수 있다.