참조자의 도입

#CPP #reference #참조자 C에선 어떤 변수를 가리키고 싶을 땐, 반드시 포인터를 사용 해야 했으나, 이를 쉽게 해결하는 방법을 C++ 에서 제공하는 것

#include <iostream>

int main() {
  int a = 3;
  int& another_a = a;

  another_a = 5;
  std::cout << "a : " << a << std::endl;
  std::cout << "another_a : " << another_a << std::endl;

  return 0;
}

정수값 a 는 3으로 초기화 되어 있습니다. 그러나 여기서 참조자가 새롭게 정의 되어 있습니다.

  • 참조자 기본형 : {원하는 자료타입}& 변수명

예시에서는 그렇기에 위에서 a의 참조자로 another_a 가 지정되었으며, 이로써 값이 바뀌게 됩니다.

  • 주의 : 레퍼런스는 반드시 처음에 누구의 별명이 될 것인지를 지정해야 합니다. 따라서 아래처럼 설정은 불가능합니다.
int& anoter_a;

레퍼런스가 한 번 별명이 되면 절대로 다른이의 별명이 될 수 없습니다. 또한 레퍼런스는 메모리 상에 존재하지 않을 수도 있습니다.

int a = 10;
int& another_a = a; // another_a의 자리는 사실 필요 없습니다. 
int x;
int& y = x;
int& z = y;

이러한 경우 뭔가 문제가 있어 보인다. 어떤 타입T기준으로 참조자 타입은 T&이며 따라서 이중으로 참조를 하게 되면, T&& 로 하는게 맞을 것으로 보이며, 그 경우 z는 int&& 가 될 것처럼 생각된다. 하지만, 참조자의 참조자 라는 것은 말이 안되는 말이고, 실제로는 참조자 y, z 모두 C++ 문법상 x의 참조자가 됩니다.

C에서는 입력을 받을 때, 포인터를 활용해 받거나 했음에도 왜 C++ 은 cin >> 으로 쉽게 받게 되어 있을까요? 바로 위에서 이야기 한 참조자 개념을 활용했기 때문입니다.

상수에 대한 참조자

#include <iostream>

int main(void){
	int &ref = 4;

	std::cout << ref << std::endl;
}

위의 소스를 컴파일 해보면 오류가 발생합니다.

이는 상수값 = 리터럴이기 때문입니다. 리터럴은 텍스트 영역 메모리 안에 저장되고, 해당 영역에 저장되는 리터럴의 경우 C++ 문법 상 상수로 레퍼런스 참조가 불가능합니다.

단, 상수 참조자로 선언 한다면 리터럴도 참조가 가능합니다.

#include <iostream>

int main(void){
	const int &ref = 4;

	std::cout << ref << std::endl;
}

또한 상수 참조자의 경우 레퍼런스화 되기 때문에 int a = ref = int a = 4와 동일하게 처리된다고 생각하면 됩니다.

레퍼런스의 배열과 배열의 레퍼런스

int a, b;
int& arr[2] = {a, b};

해당 예시는 그럴듯 하지만, 불법이라고 컴파일 에러가 발생합니다. 이는 불법으로 되어 있는데, 생각해보면 충분히 될 수도 있어 보입니다.

하지만 레퍼런스 arr[2] 가 의미하는 바는 arr[0] = *(arr + 1) 로 주소값이 존재해야 한다는 것 또한 의미합니다.

그러나 여기서 보이듯, ‘주소값’이 존재한다는 말은 메모리 상에 어떤 의미든 해당 원소가 존재해야 한다는 거지만, 레퍼런스는 특별한 경우가 아닌 이상 메모리 상에 공간을 차지하지 않습니다. 따라서 이런 모순에 의해 레퍼런스들의 배열 정의는 언어 차원에서 금지 되어 있습니다.

하지만 그렇다고 불가능한 것은 아닙니다. 다음처럼 설정시 배열의 레퍼런스가 가능합니다.

#include <iostream>

int main(){
	int	arr[3] = {0, 1, 2};
	int (&ref)[3] = arr;

	for (int idx = 0; idx < 3; idx++)
		std::cout << ref[idx] << std::endl;
	return (0);
}

다음 에시는 refarr를 참조하도록 하였습니다. 단 이때, 포인터와는 다르게 배열의 레퍼런스의 경우 참조하기 위해 반드시 배열 크기를 명시 해야 합니다.

레퍼런스를 리턴하는 함수

지역 변수의 레퍼런스를 리턴

int& function() {
  int a = 2;
  return a;
}

int main() {
  int b = function();
  b = 3;
  return 0;
}
  • 이 경우 에러가 발생하지만, 실행은 됩니다. 여기서의 문제점은 int& 반환 타입이라는 점은 곧 레퍼런스(참조자)를 반환한다는 점 때문입니다.
  • 지역변수 a를 리턴하게 되는데, 이때 함수에 대해서 참조자가 자동 변환되어 나오게 되는 것인데, 문제는 로컬 함수가 종료 되면서, 그 값은 사라지고, 아무것도 가리키고 있지 않는 레퍼런스만이 남게 되는 것이 문제인 것이었습니다.
  • 이를 댕글링 레퍼런(Dangling reference) 라고 부릅니다. 즉, 이처럼 레퍼런스를 리턴하는 함수에서는 지역 변수의 레퍼런스를 리턴하지 않도록 해야 합니다.

외부 변수의 레퍼런스를 리턴

int& function(int& a){
	a = 5;
	return a;
}

int main(){
	int b = 2;
	int c = function(b);
	return (0);
}

이 경우 레퍼런스를 받아 그대로 리턴하는 구조입니다. main 이 살아있는 이상, 댕글링 레퍼런스가 되지 않으며, 이는 c = b 와 같은 의미로 동작하게 됩니다.

하지만 이렇게 참조자를 리턴하는 경우를 굳이 써야 하는 이유, 장점은 무엇일까요?

이는 C언어의 구조체를 사용하는 것과 동일한 효과를 냅니다.

레퍼런스를 리턴하게 된다면, 레퍼런스가 참조하는 타입의 크기와 상관없이 딱 한 번의 주소값 복사로 전달이 끝나게 되므로 매우 효율적일 수 있습니다.

참조가 아닌 값을 리턴하는 함수를 참조자로 받기

int function(){
	int a = 5;
	return a;
}

int main(){
	int& c = function();
	return (0);
}

이 경우 컴파일 오류가 발생합니다. 그리고 이는 레퍼런스가 function 함수의 리턴값을 참조할 수없다는 의미입니다.

이러한 경우는 위에서 언급한 댕글릴 레퍼런스가 되어 버리기 때문입니다. 그렇다면 여기서 예외는 분명 존재할 것입니다.

#include <iostream>

int function() {
  int a = 5;
  return a;
}

int main() {
  const int& c = function();
  std::cout << "c : " << c << std::endl;
  return 0;
}

위 코드는 성공적으로 컴파일이 되며, 실행시 성공적으로 c : 5라는 출력을 확인할 수 있습니다. 심지어 표준 출력으로 값도 접근이 가능합니다.

이러한 현상이 나오는 이유는 상수 레퍼런스에서는 리턴값을 받게 되면 해당 리턴값의 생명이 연장되며, 레퍼런스가 사라질 때까지 그 값을 보존합니다.

함수에서 값 리턴(int f()) 함수에서 참조자 리턴
(int& f())
값 타입 받음
(int a = f())
값 복사됨 값 복사됨, 다만 지역 변수의 레퍼런스를 리턴하지 않도록 주의.
참조자 타입 받음
(int& a = f())
컴파일 오류 값 복사됨, 다만 지역 변수의 레퍼런스를 리턴하지 않도록 주의.
상수참조자 타입 받음
(const int& a = f())
가능 값 복사됨, 다만 지역 변수의 레퍼런스를 리턴하지 않도록 주의.