본문으로 바로가기

파일의 IT 블로그

  1. Home
  2. 프로그래밍/C++
  3. [C++] 참조자 (레퍼런스, Reference)

[C++] 참조자 (레퍼런스, Reference)

· 댓글개 · KRFile

기존 포인터 연산

#include <iostream>
using std::cout;
using std::endl;


int change_val(int * p){
	*p = 3; //p주소를 찾아가 해당하는 값(val)에 3을 대입한다.	
}

int main(){
	int num = 5;
	
	cout << "num : " << num << endl;
	change_val(&num);
	cout << "num : " << num << endl;
	return 0;
}

num : 5
num : 3

--------------------------------
Process exited after 0.01021 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

*아래에는 소스코드에 대한 설명이 나옵니다! 전부 아시는 내용이라면 바로 Pass 해주세요.

 

C에서 메모리 주소로 직접 접근해서 값에 참조할 수 있는건 포인터 뿐이였습니다.

주소를 & 연산자를 이용해 포인터 변수로 받고 포인팅한 후 *(간접 참조 연산자) 를 이용해 값을 바꿔내는 것이였죠.

 

C++의 경우엔 C의 문법을 그대로 계승했으므로 당연히 C에서 사용하던 포인터 문법을 똑같이 사용할 수 있습니다.

위 예제는 num 이라는 int 형 변수를 함수를 호출 할때 주소를 넘기는 방식인 Call by Reference(주소에 의한 호출) 을 통해 포인터로 접근해 값을 바꿔내고 있습니다.

 

change_val() 에는 매개변수로 int형 포인터 변수가 있는데 change_val() 을 호출할때 인수로 &num 을 넘깁니다.

매개변수 p는 이를 그대로 받아서 num을 포인팅하게 되고 

 

*p = 3 구문을 통해 p가 가리키는 주소로 이동해서 그 값에 3을 대입합니다.

p가 가리키는 주소는 num의 주소니깐 num의 값인 5에 3을 덮어씌운다고 보면 되겠네요.

그래서 change_val() 함수를 통해서도 main() 안에 선언되어 있는 지역변수인 num을 바꿔낼 수 있었습니다!

 

참조자(Reference)

C++에서는 포인터가 마음에 안들었는지 다른 변수나 상수를 가리키는 방법으로 또 다른 방식을 제공하는데, 이를 바로 참조자(레퍼런스, Reference) 라고 부릅니다! :: 오늘 알아볼 녀석이 바로 이것입니다.

 

예제에 앞서 참조자라는 것은 C++에서 처음 도입된 것으로 메모리가 할당된 변수에 또 다른 이름, 즉 별명을 붙이는 것 이라고 할 수 있습니다.

 

#include <iostream>
using std::cout;
using std::endl;


int main(){
	int a = 3;
	int& another_a = a;
	
	another_a = 5;
	cout << "a : " << a << endl;
	cout << "another a : " << another_a << endl;
	return 0;
}

a : 5
another a : 5

--------------------------------
Process exited after 0.01059 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

출력을 해보시면 a와 another_a 가 같은값이 나옵니다.

another_a에 a를 대입했기 때문에 당연히 그럴거라는 생각이 듭니다만,

 

자세히 보시면 자료형 뒤에 특이하게 &라는 기호가 있습니다.

소스코드를 한줄씩 살펴봅시다.

 

int a = 3;

우선 int형 변수 a를 만들고 3을 대입했습니다.

 

int& another_a = a;

그리고 a의 참조자로써 another_a를 정의했습니다.

참조자를 정하는 방법은, 가리키고자 하는 타입 뒤에 &를 붙이면 됩니다.

 

int 형 변수의 참조자를 만들땐 int& 를, double 의 참조자를 만들려면 double& 로 자료형을 결정해주시면 됩니다.

꼭 일반적인 정수형이나 실수형에만 사용할 수 있는게 아니라

int* 와 같은 포인터 타입의 참조자를 만들 수 있는데 int*& 로 쓰면됩니다! (이 경우 포인터 변수가 가리키는 원본 값 자체를 변경하겠다와 같은 의미입니다. 더블 포인터를 쓰지 않고 레퍼런스를 이용해 포인터 변수의 값을 직접 변경할 수 있습니다.)

 

위와 같이 선언함으로써 another_a는 a의 참조자다! 라고 공표하게 된 것입니다.

이 말은 another_a는 a의 또다른 이름 이라고 컴파일러에 알려주는 것 입니다.

 

즉 a의 별명을 another_a 라고 하는것과 마찬가지 입니다.

예전에 친구들의 별명을 부르면 별명을 부르는것이나, 그 친구의 이름을 부르는 것이나 동일한 것이였잖아요?

이것도 마찬가지 입니다.

 

따라서 another_a에 어떠한 작업을 수행하던 사실상 a에 작업을 하는것과 마찬가지 입니다.

 

#include <iostream>
using std::cout;
using std::endl;


int main(){
	int a = 3;
	int& another_a = a;
	
	//another_a = 5;
	//cout << "a : " << a << endl;
	//cout << "another a : " << another_a << endl;
	
	another_a = 7; //a = 7
	cout << "a : " << a << endl;
	return 0;
}

a : 7

--------------------------------
Process exited after 0.00782 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

맨 밑에 2줄의 코드를 추가했습니다.

another_a = 7; 로 대입하고 a를 출력해봤더니 신기하게도 a의 값이 바뀌어 있습니다.

방금도 말했듯이 another_a 는 a와 동일한 것이라 a에 7을 대입한 것과 같습니다.

 

참조자는 포인터와 상당히 유사한 개념인데, 포인터 역시 다른 변수의 주소값을 보관함으로써 간접적으로 연산을 수행할 수 있기 때문입니다.

 

아래는 레퍼런스를 사용할때 주의할 점입니다.

 

1. 레퍼런스는 반드시 처음에 누구의 별명이 될 것인지 지정해야 한다.

 

레퍼런스는 정의 시에 반드시 누구의 별명인지 명시해야 합니다. 

일반적으로 선언과 동시에 초기화 해야한다는 말입니다.

 

int& another_a;

그렇기에 위와 같은 문장은 불가합니다.

컴파일 해보면 다음과 같은 오류가 발생합니다.

[Error] 'another_a' declared as reference but not initialized (another_a 는 레퍼런스로 선언됬으나 초기화 되지 않음)

 

int* p;

반면에 포인터의 경우 이렇게 아무 값도 넣지 않아도 됬었습니다.

물론 아무것도 가리키지 않는단 의미에서 명시적으로 NULL 값으로 초기화 하는게 좋죠.

 

레퍼런스는 NULL로 초기화 하는게 불가능 합니다.

 

2.  레퍼런스가 한 번 별명이 되면 절대로 다른 이의 별명이 될 수 없다

 

레퍼런스의 한 가지 중요한 특징으로 한 번 어떤 변수의 참조자(별명)가 되면 

더 이상 다른 변수를 참조할 수 없게 됩니다.

 

예를 들어서 아래 코드를 살펴봅시다.

 

#include <iostream>

int main(){
	int a = 10;
	int& another_a = a; //another_a 는 이제 a의 참조자이다.
	
	int b = 3;
	another_a = b; //?? 
	return 0;
}

 

보시면 궁금한 부분은 another_a = b 부분인데요.

another_a를 보고 다른 변수인 b를 가리키라는 말일까요? 아닙니다

 

아까도 말했듯이 another_a 에 무언가를 하는건 a에 무언가를 하는것과 동일하기 때문에

사실 a = b; 와 같은 구문이라고 봐야 합니다.

a에 b를 대입하는 거니깐 a = 3; 이랑 같은 코드겠네요.

 

&another_a = b;

이런건 어떨까요? 참조자 기호가 & 라고 했으니깐..

사실 위 코드는 말이 안되는데 another_a 가 a랑 같은거니깐

 

&a = b; 가 됩니다. a의 주소에 b의 값을 넣는다? 말이 안되는 구문이죠.

 

#include <iostream>

int main(){
	int a = 10;
	int * p = &a; //포인터 p로 a의 주소를 가리킨다. 
	
	int b = 3;
	p = &b; //포인터 p로 b의 주소를 가리킨다. 
	return 0;
}

반면에 포인터였다면 a 던 b 던 주소만 대입해서 마음대로 포인팅 할 수 있었겠죠.

 

3. 레퍼런스는 메모리 상에 존재할 수도 존재하지 않을 수도 있다

 

안랩 창설자이신 모 정치인 분이 생각나는 대사인데요.. 레퍼런스는 메모리 상에 존재할 수도 존재하지 않을 수도 있다고 합니다. 이게 무슨 소리일까요?

 

#include <iostream>

int main(){
	int a = 10;
	int* p = &a; 
	return 0;
}

우선 다음 코드를 봅시다.

만약에 포인터 p로 저렇게 a의 주소를 가리키면 p에 a의 주소를 저장해야 하므로

p는 64비트 컴퓨터 기준으로 8바이트가 할당되어 그 공간에 저장을 할 것 입니다. (32비트 컴퓨터는 4바이트)

 

#include <iostream>

int main(){
	int a = 10;
	int &ref_a = a; //ref_a 가 자지를 차지할 필요가 있을까? 
	return 0;
}

반면 a의 참조자로 정한 ref_a를 봐봅시다.

만약에 컴파일러 입장에서 ref_a를 위해서 메모리 상에 공간을 차지할 필요가 있을까요?

ref_a 는 사실상 a랑 똑같은건데 그냥 ref_a 가 쓰이는 자리를 a로 치환시켜버리면 되겠죠.

 

따라서 이 경우 레퍼런스는 메모리 상에 존재하지 않게 됩니다.

 


메모리에 존재하지 않는 경우는 간단하게 이해 완료 O.K

그러면 메모리 상에 존재하는 경우도 알아봐야겠죠?

 

먼저 답부터 이야기 하자면 바로 함수 인자를 전달받게 되면 메모리가 할당되게 됩니다.

즉 call by reference를 하게 되면 메모리가 할당되게 됩니다.

#include <iostream>
using std::cout;
using std::endl;


int change_val(int & p){
	p = 3;
}

int main(){
	int num = 5;
	
	cout << "num : " << num << endl;
	change_val(num);
	cout << "num : " << num << endl;
	return 0;
	
}

num : 5
num : 3

--------------------------------
Process exited after 0.01023 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

아까 포인터를 이용해서 main() 함수 외부에서 즉, change_val() 함수 에서 main() 함수 내부의 지역변수인 num 을 바꿔내는 예제를 알아봤었죠.

 

그것을 그대로 레퍼런스로 바꿔보았습니다. 

 

int change_val(int & p)

가장 중요한 부분으로 함수 형태를 보시면

함수의 매개변수로 참조자를 받게 하고 있습니다.

 

그런데 int& p 와 같은건 안된다고 하지 않았나요?

라고 반문할 수 있습니다.

 

그런데 사실 p가 정의되는 순간은 change_val(num) 로 호출할 때 정의되므로

int & p = num; 가 실행된다고 생각하시면 됩니다!

 

참조자 p 에게 너는 앞으로 num의 새로운 별명이야 라고 알려주게 되었습니다.

그리고 레퍼런스를 정의할때 포인터 처럼 & 기호를 붙여서 사용해주는 것이 아니라

그냥 int& a = b 와 같이 써주면 a는 b의 별명이다 라고 해석되었죠.

 

그래서 함수를 호출해서 인자값을 넘길때 change_val(num) 처럼 호출 하면 됩니다.

포인터로 인자값을 받았다면 change_val(&num) 처럼 레퍼런스에 비해선 조금 지저분한 코드가 되었을 겁니다.

 

p를 바꿔내는건 이제 num을 바꿔내는 것과 동일한 작업이 되었으므로 

아까 포인터와 마찬가지로 참조자를 통해 Call by Reference(원본 수정)이 이루어지게 되었습니다!

 

여기서도 포인터와 비교해서 간편한 점이 나오는데 포인터를 사용할땐 *(간접 참조 연산자) 를 통해 접근해서 값을 바꿔줘야 했으나 레퍼런스의 경우 그럴필요가 없죠.

 

일단 코드 설명은 여기까지고...

int change_val(int & p){
	p = 3;
}

이런식으로 참조자로 매개변수를 전달받는다고 해도 전달받는 p의 주소값이 change_val 함수의 Stack Memory에 저장이 되어서 사용된다고 합니다.

 

아까 앞의 case와는 다르게 매개변수로 받을땐 메모리 공간이 할당된다는 것이죠!

 

4. 참조자의 참조자를 만드는건 금지되어 있다.

 

C++ 문법상 참조자의 참조자를 만드는건 금지되어 있습니다.

왜냐면 굳이 별명의 별명을 만들 필요는 없기 때문입니다.

int x;
int&y = x;
int&z = y;

다음 코드를 한번 확인해봅시다.

먼저 int&y = x; 를 통해 y를 x의 레퍼런스(별명)로 지정하고 있습니다.

 

int&z = y;

그럼 이 코드는 무엇일까요?

 

아까 어떤 타입 T의 참조자 타입은 T& 이라고 했는데, y가 int & y니깐 y의 참조자 타입은 int&& 가 되어야 된다고 생각할 수도 있겠지만 이렇게 생각하시면 안됩니다.

 

위에서도 말했듯이 별명의 별명을 만들 필요는 없죠.

사실 위 int&z = y; 라는 코드는 사실 y자체가 x의 별명으로

결국에 int&z = x;와 동일한 표현입니다.

z를 x의 별명으로 쓰겠다 이거죠.

 

그래서 위 3코드는 결론적으로 y던 z던 무슨 작업을 해도 x에 작업한것과 동일하게 됩니다.

 

cin 과 scanf에 대해

scanf에서 정수값을 받아서 int형 변수 a에 저장한다고 했을땐

scanf("%d", &a); 처럼 작성해줘야 했습니다.

 

scanf의 인자값으로 그냥 a가 아니라 a의 주소를 넘기는 이유는

scanf() 내부에서 임의의 처리를 통해 입력을 받고,  그  값을 입력이 필요한 외부의 변수(a)에 바로 저장해주려면

포인터로 주소를 받아서 * 연산자로 값을 바꿔줘야 했기 때문이죠.

 

그런데 C++에서 입력을 받는 용도인 std::cin 의 경우

std::cin >> a; 라고 하면 끝입니다. 

이건 cin이 레퍼런스로 a를 받아서 그럽니다. 

 

아까 레퍼런스로 인자값을 받을때 주소를 넘길필요가 없었던것을 기억하시면 됩니다. (&기호를 쓸필요가 없었습니다.)

 

상수에 대한 참조자

#include <iostream>
using std::cout;
using std::endl;


int main(){
	int &ref = 4;
	cout << ref << endl;
	return 0;
	
}

 

위 소스를 컴파일 하면 아래와 같은 오류가 발생합니다.

[Error] invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'

 

4라는 숫자는 프로그래밍 측면에서 리터럴 상수(즉 문자 그대로 의미를 가지는) 인데

메모리적 측면에서 살펴보면 리터럴 값은 Read-Only (읽기만 가능) 한 메모리 공간에 저장이 됩니다.

 

그런데 참조자를 사용하면 이곳에 값을 바꿔낼 여지가 있기 때문에 상수를 참조하는 것은 금지됩니다.

 

ref = 5; //4=5??

간단하게 생각해서 위 코드를 보시면 

ref이 숫자 4의 별명인데

4에 5를 대입한다?

 

말이 안되는 뜻이죠! 그래서 안됩니다.

 

#include <iostream>
using std::cout;
using std::endl;


int main(){
	const int &ref = 4;
	int a = ref;
	cout << a;
	return 0;
	
}

그런데 이런식으로 사용하는건 가능합니다.

앞에 const 키워드를 붙여서 4라는 숫자 자체를 레퍼런스로 바꿔내지 못하게 하는겁니다.

(포인터에서 const 키워드가 맨앞에 있었을때 아무 변수나 가리키는건 가능하지만 가리키는 변수를 못바꾸게 했던 그걸 생각하시면 됩니다.)

 

이러면 우선 참조자 ref를 선언할때 오류가 발생했던것을 피해갈 수 있습니다.

 

int a = ref;

4라는 숫자의 별명이 ref니깐 위 코드는

int a = 4; 와 동일하게 처리되겠군요.

 

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

각 배열 요소가 레퍼런스 변수인 레퍼런스 배열이 만들어질까요??

바로 아래와 같은 코드 말입니다.

 

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

 

 

C++ 규정을 찾아보면 표준안에서 "레퍼런스의 레퍼런스, 레퍼런스의 배열, 레퍼런스의 포인터는 존재할 수 없다"

라고 언어상에서 불가능하다고 못 박혀있습니다.

 

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

이렇게 적어서 arr[0] = a 가 되고 arr[1] = b 가 되는 그런건 왜 안되는걸까요??

 

우선 배열의 이름은 배열의 시작주소 입니다. 정확히는 배열의 이름은 배열의 첫번째 요소 주소를 가리키고 있습니다.

그런데 주소값이 존재한다라는 의미는 해당 원소가 메모리 상에 존재한다라는 의미와 같습니다.

 

하지만 아까도 봤듯이 레퍼런스는 특별한 경우가 아닌 이상 메모리 상에서 공간을 차지 하지 않습니다. 

따라서 이러한 모순 때문에 레퍼런스의 배열을 정의하는 것은 언어 차원에서 금지되어 있습니다.

 

반대로 배열의 레퍼런스는 가능합니다.

 

#include <iostream>
using std::cout;
using std::endl;


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

위 예제를 봅시다.

 

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

다음 코드를 통해서 ref가 arr을 참조하도록 했습니다.

이제 ref는 arr의 별명입니다.

 

따라서 ref[0], ref[1], ref[2] 가 각각 arr[0], arr[1], arr[2] 의 레퍼런스가 되게 되는 것입니다.

배열의 경우 배열명이 배열의 시작주소이기도 하고 애초에 내부적으로 포인터로 구현되어 있었기 때문에

포인터와 배열을 연결하려면 주소 정도만 받아내는게 끝이였습니다만,

 

레퍼런스의 경우 반드시 배열의 크기를 명시해서 참조해야 합니다.

예를 들어서 int (&ref)[3]  이라면 반드시 크기가 3인 int 배열의 별명이 되어야 하고

int (&ref)[5] 라면 크기가 5인 int 배열의 별명이 되어야 합니다.

 

위 코드를 실행해보시면 알겠지만 ref 를 바꿔냈는데 arr이 바뀌었습니다!

레퍼런스가 어떤 변수의 별명이고, 원본을 바꾸나 레퍼런스를 바꾸나 똑같은 동작이란것만 이해하면 그렇게 어렵진 않을겁니다.

 

int arr[3][2] = {1,2,3,4,5,6};
int (&ref)[3][2] = arr;

 

2차원 배열 이상의 레퍼런스를 지정하는 경우에도 동일합니다.

int (&레퍼런스명)[크기] 로 원본 배열과 자료형, 크기 모두 똑같이 맞춰주시면 됩니다.

 

레퍼런스를 리턴하는 함수

#include <iostream>


int fn(){
	int a = 2;
	return a;
}

int main(){
	int b = fn();
	return 0;
	
}

간단한 함수 호출의 형태입니다.

main() 함수에서 fn() 을 호출하고 있는데, fn() 내부에서는 return a로 a의 값을 던집니다.

a에는 2가 저장되어 있으므로 2를 던지고 fn() 의 int a 변수는 지역변수 이므로 함수의 중괄호를 탈출하면 메모리 상에서 사라지게 됩니다.

 

이제 2라는 값을 받아내고 b에 저장합니다.

그런데, 갑자기 main() 함수에서 fn() 내부의 지역변수인 a의 값을 바꿔내고 싶다고 생각해봅시다.

 

전통적인 C의 방법이라면 a의 주소값을 return &a 를 통해 던지고

main() 함수 내부에서 포인터 변수로 받아내어

a에 직접 접근할 수 있었을겁니다.

 

그렇지만 C++에서는 레퍼런스라는 좋은게 있죠.

혹시 그렇다면 한번 생각해서 레퍼런스를 반환할 수 있지 않을까요..?

한번 해봅시다.

 

#include <iostream>

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

int main(){
	int b = fn();
	std::cout << b;
	return 0;
	
}

보시면 int b로 fn() 에서 return 한 a의 참조자를 받아내고 있습니다.

그런데 위 코드를 작성하시면 다음과 같은 경고가 발생할 것입니다. (오류는 아니고 컴파일은 일단 됩니다.)

 

[Warning] reference to local variable 'a' returned [-Wreturn-local-addr]

지역변수 a의 레퍼런스를 반환하고 있다고 경고를 띄우네요. 무슨뜻일까요?

 

 

int b = fn();

위 문장은 사실 아래와 같이 동작합니다

 

int& ref = a;

int b = ref; //a는 어디에?

 

function() 이 a에 대한 참조자를 던져서 b에 값으로 저장했는데 a라는 공간은 함수 내부 지역변수기 때문에

return 으로 값을 던진뒤 메모리에서 소멸해버립니다.

 

이에 대하여 이해가 안되시면 다음 글을 참고하시길 바랍니다.

https://pgh268400.tistory.com/82

 

[C언어 강좌] #11 정적변수, 지역변수, 전역변수, 외부변수, 레지스터 변수

물론 변수는 여기서 배웠는데 왜 또배우나요? 라고 할 수 있습니다. 하지만 저번에 배웠던 변수에 대한 내용들은 기초적인 내용이고, 오늘은 그 변수가 메모리에 언제 생성되고, 언제 소멸되는

pgh268400.tistory.com

 

* 포인터에서도 이런것을 한번쯤 경험해보셨을 겁니다.

fn() 함수 내부의 a에 직접 접근하기 위해 a의 주소를 던지고 그 값을 읽어봤는데 a의 메모리 공간은 지역 변수라 이미 소멸해서 값이 안읽히는 현상이요.

 

위와 같이 레퍼런스는 있는데 원래 참조 하던 것이 사라진 레퍼런스를 댕글링 레퍼런스(Dangling reference) 로 부릅니다. Dangling 이라는 단어의 뜻은 달랑~달랑~ 이라는 뜻인데 레퍼런스가 참조해야할 변수가 사라지고 혼자서 덩그러니 남겨져 있는 상황을 생각하시면 됩니다.

 

따라서 레퍼런스를 리턴 시 함수에서 지역 변수의 레퍼런스를 리턴하지 않도록 조심해야 합니다.

 

.

.

.

.

 

 

분명 원칙상 그랬을 터인데.. 아래 코드를 한번 실행시켜보겠습니다.

 

#include <iostream>

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

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

5
100
--------------------------------
Process exited after 0.01042 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

어라.. 잘 뜨는데요?

다음 내용에 있어서 씹어먹는 C++ 강좌를 작성하신 Jaebum Lee 님의 댓글을 인용하겠습니다.

 

애초에 지역변수의 레퍼런스를 리턴하는 것은 *정의되지 않은 작업(undefined behavior)* 입니다. 즉 뭔 일이 일어나도 상관 없다는 것이죠. 컴파일러마다 다르긴 한데 gcc 나 clang 의 경우 지역변수의 레퍼런스를 리턴 시에 강제로 null 의 역참조를 리턴하기 때문에 실행시 바로 오류가 납니다. 반면에 비주얼 스튜디오 계열의 경우에는 그냥 해당 함수 내에 있는 변수의 참조자를 리턴하는 것 같네요. 물론 해당 변수는 함수가 리턴되면서 소멸되므로, 원칙적으로 접근하면 안되지만 메모리 상에서 바로 값을 지워버리는 것은 아니니까 다른 값으로 덮어 씌어지기 전 까지 잠시동안이나마 접근은 가능할 것입니다. 참고로 빌드 창에서 뜨는 메세지는 (경고 포함) 모두 오류로 간주하는 것이 좋습니다. 빌드 창에서 뭔가 메세지가 뜬다면 컴파일이 되더라도 정상적으로 작동하는 것을 보장할 수 없어요

~https://modoocode.com/141
백전능 2020-09-12T17:59:01.741Z 님의 질문

간단하게 정리해서 저렇게 지역변수의 레퍼런스를 반환하면 어떤 컴파일러는 오류를 뱉는데

vscode 계열 컴파일러의 경우에는 (저는 dev c++로 하고 있는데 동일한거 같습니다.) 그냥 변수의 참조자를 리턴한다고 합니다.

 

물론 해당 변수는 함수가 리턴되면서 소멸해서 원칙적으로 접근하면 안되는데

함수 지역변수 값 반환 후 '소멸' 된다는 것이 생각해보면 언제 소멸한다곤 배워보질 않아서 아마도 메모리 상에서 바로 바로 치워버리는 것은 아닌가 봅니다. 그래서 다른 값으로 덮어 씌우기 전에 잠시 동안이나마 접근이 가능하다고 합니다.

당연하지만 원칙, 이론적으론 불가한거라서 정상적인 동작을 보장하지 않는다고 보시면 됩니다.

 

(정확히 아시는 분들은 댓글 부탁드리겠습니다!)

 

*포인터로 접근하면 확실히 안되는걸 알 수 있는데 어쨌던 제대로 된 것은 아닌거 같습니다.

 

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

#include <iostream>

int& function(int& arg1) {
  arg1 = 10;
  return arg1;
}

int main() {
  int a = 100;
  int c = function(a);
  std::cout << c << std::endl;
  std::cout << a;

  return 0;
}

10
10
--------------------------------
Process exited after 0.01047 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

다음 예제도 살펴봅니다.

이번엔 함수에 매개변수를 하나 추가했습니다.

 

function() 함수를 만들었는데 레퍼런스로 값을 받아서 다시 그대로 레퍼런스로 리턴하고 있습니다.

function(a) 를 호출한 시점에서 우선은 a라는 지역 변수는 main() 함수의 중괄호 탈출전, 그러니깐 프로그램 종료전까진 계속 살아 있을 것이고... function() 에서는 arg1이라는 참조자로 a를 참조하겠네요.

 

function() 함수 내부에서 a의 별명인 arg1에 10을 대입했으니

실상은 a에 10을 넣은것과 동일해집니다.

 

그리고 레퍼런스로 반환을 해서 c에 저장했는데 a의 참조자를 리턴했으므로 사실 a를 리턴한것과 같겠죠.

그러니 a의 값 10은 c에 저장되게 됩니다.

 

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

#include <iostream>

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

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

위 코드를 컴파일 해보면 오류가 떠서 아예 실행자체가 되질 않습니다.

 

[Error] invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'

상수가 아닌 레퍼런스가 function() 의 리턴값을 참조할 수 없다고 하네요.

 

int & c = function();

이건 또 왜 안되는걸까요..?

 

이건 아까랑 비슷한 내용인데 리턴값은 해당 문장이 끝난 후 바로 사라져 참조자를 만들게 되면 바로 다음에 댕글링 레퍼런스가 되버리기 때문입니다.

 

그리고 아마도 return 하는건 a의 레퍼런스가 아니라 int형태로 a의 '값' 뿐일건데 5라는 숫자를 레퍼런스로 바꾸는건 허용하지 않아서 그런거 같기도 합니다.

 

물론 위와 같이 참조자로 리턴값을 받아내고 싶을때 한가지 방법,

C++에서의 중요한 예외규칙이 있는데

 

const(상수) 키워드를 이용한 방법입니다.

 

#include <iostream>

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

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

const 키워드를 붙이고 c를 출력해보면 오류도 없이 5라는 값이 잘 출력되는걸 알 수 있습니다.

원칙상 함수의 리턴값은 해당 문장이 끝나면 소멸되는 것이 정상이므로

기존에 int&로 받았을때는 컴파일 자체가 안되었는데 

 

예외적으로 상수 레퍼런스로 리턴값을 받게 되면 해당 리턴값의 생명이 연장됩니다.

그리고 그 연장되는 기간은 레퍼런스가 사라질때까지 입니다.

 

 

 

 

참고

모두의 코드 씹어먹는 C++ 강좌

https://junstar92.tistory.com/111

 

[C++] 참조자(Reference)에 대해서

우리는 변수(Variable)이 할당된 메모리 공간을 지칭하는 것, 즉 메모리 공간에 붙여진 이름이라는 것을 알고 있습니다. 그리고 C++에서 처음 도입된 참조자(Reference)는 메모리가 할당된 변수에 또

junstar92.tistory.com

 

SNS 공유하기
최근 글
파일의 IT 블로그
추천하는 글
파일의 IT 블로그
💬 댓글 개
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

이모티콘을 클릭하면 댓글창에 입력됩니다.