티스토리 뷰

레퍼런스

자신이 참조하는 변수를 대신할 수 있는 또 하나의 이름 = 별칭

 

레퍼런스는 처음 읽어보면 "포인터가 이미 주소를 가르키고 있는데?"라는 생각에 헷갈릴 수 있다. 이를 이해하기 위해 먼저 포인터를 통해 발생할 수 있는 문제를 알아봐야한다. 

 


동적 할당의 주의점

void function(){
	int* a;
    
    a = (int*)malloc(sizeof(int*));
    
    *a = 10;
    cout << *a << endl;
    
    free(a);
    
    *a = 5;
    cout << *a << endl;
}
10
5

 

위와 같은 함수가 존재할 때, 뭔가 이상한 부분이 보일 것이다.

분명 a에 malloc을 통해 동적 할당을하고 free를 통해 동적 할당을 해제하였다. 그런데 해당 코드는 아무런 이상 없이 동작한다. (다른 글에 정리해놓았다.)

 

포인터와 주소 접근

포인터는 또 하나 레퍼런스와 큰 차이점있다. 바로 연산자를 통해 여러 포인터에 접근이 가능하다는 것이다. (이 또한 다른 글에 작성되어있으니 보자.)

 

간략하게 말하면, 포인터는 연산자를 통해 시작 주소에서 다음/이전 주소로 옮겨다닐 수 있다.

이는 안정적이지 않은 주소값 처리이다. 만약, 연산을 통해 이동하고 다시 돌아오지 못한다면, 원래 사용하던 값은 쓰레기값이 되어버리기 때문이다.

 

 


레퍼런스

레퍼런스는 이와 같은 상황을 피하기 위하여, 한번 설정되면 절대 불변(다른 값으로 변경 불가능)한다는 성질을 가진다.

단, 이를 충족하기 위해서는 "레퍼런스는 반드시 선언과 동시에 초기화"라는 조건을 반드시 지켜야한다.

 

#include <iostream>

using namespace std;

int main(void) {
    int num = 10;
    cout << "Value Address : " << &num << endl;
    
    int* ptr = &num;
    cout << "Pointer Value Address : " << ptr << endl;
    cout << "Pointer Address : " << &ptr << endl;
    
    int& ref = num;
    cout << "Reference Address : " << &ref << endl;
    
    return 0;
}
Value Address : 0x7ffee636c494
Pointer Value Address : 0x7ffee636c494
Pointer Address : 0x7ffee636c498
Reference Address : 0x7ffee636c494

 

위 코드를 통해 우리는 포인터를 보면 "포인터의 주소 + 포인터가 가리키는 값의 주소"인 것을 알 수 있다. 반면, 레퍼런스가 입력 받은 변수와 완벽히 동일한 주소를 가지는 것을 알 수 있다.

 

  • 포인터는 주소 값을 저장하기 위해 별도의 메모리 공간을 소모한다.
  • 레퍼런스는 동일한 스택 메모리를 사용한다.

 

 

 

더블레퍼런스 (&&)

아까 레퍼런스의 "생성되며 초기화되고 변하지 않는다"라는 성질을 배웠다.

 

그래서 레퍼런스 변수는 데이터 영역에 할당된 변수나 원본 변수 라이프 사이클이 더 길거나 같아야한다.

이는 함수로 반환되는 레퍼런스 변수보다 원본 변수가 먼저 사라지는 경우 문제가 된다.

 

이러한 상황을 방지하는 것에 더블레퍼런스를 사용하면 함수가 반환해주는 스택 영역 값들을 저장할 수 있다. 정확히는 몇가지 방법이 있다.

 

  1. 레퍼런스 값을 const나 static으로 설정하여, 강제로 메모리에 영구 할당한다.
  2. 더블레퍼런스를 통해 새로운 값으로 인식하도록 한다.
#include <iostream>

using namespace std;

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

int main() 
{
  const int& c = function(); // const 참조자로 받으면 문제 없음
  int&& dr = function(); // 더블레퍼런스로 받으면 문제 없음
  //int& r = function(); // 참조자로 참조자가 아닌 값을 리턴 받으면 문제가 생김 (댕글링 레퍼런스)
  
  cout << "c : " << c << endl; // 3
  cout << "dr : " << dr << endl; // 3
  return 0;
}
c : 3
dr : 3

 

 

 

 

 

 


Call & Return

Call by Value

함수 외부에서 값을 복사하여 가져가는 방식이다.

외부에서 변수가 할당되고 그 변수에 값이 복사되는 개념이기에 수정이 불가하다. 

Return by Value

함수가 종료되는 시점에 반환 값을 복제하여 호출자에게 값을 반환하여 가져가는 방식이다.

 

 

Call by Address

외부에서 피변수의 주소값을 복사하여 선언된 변수를 참조하여 피변수를 수정이 가능한다.

사실상 주소 값을 복사하여 메모리에 임시 저장하고 인자로 넘기는 것이기에 Call by Value와 동일하다. (Call by Value와 다를 것 없다는 평가를 자주 받았다.)

 

Return by Address

함수가 반환되는 시점에서 주소값을 복사하여 선언한.

사실상 주소 값을 복사하여 메모리에 임시 저장하고 인자로 넘기는 것이기에 Call by Value와 동일하다. (Call by Value와 다를 것 없다는 평가를 자주 받았습니다.)

 

 

 

 

 

Call by Reference

C++에서 추가된 개념으로, Call by Address와 다르게 레퍼런스 변수는 받아온 인자로 초기화가 이루어진다. 즉, 새로운 메모리를 할당 받는 것이 아닌 원본 변수의 별칭으로 존재하게 된다. 하여 서로 원본 변수 값도 수정할 수 있게 된다. 

Return by Reference

C++에서 추가된 개념으로, Return by Reference와 다르게 레퍼런스 변수는 r-value를 반환해야 한다. 즉, 데이터 영역의 값을 전달해야하거나 && 연산자로 더블레퍼런스 변수에 할당되어야 한다. 

 

 


#include <iostream>
using namespace std;

int CallByValue(char add) {
    cout << "Call By Value : " << sizeof(add) << endl;	// add 크기 1바이트 출력
    
    int ret = 3;	// ret변수 선언
    return ret;		// ret를 복제하고 복제된 값을 반환
}

int* CallByAddress(char* add) {
    cout << "Call By Address : " << sizeof(add) << endl;	// add 포인터 크기 4바이트 출력
    
    int *ret = (int*)malloc(sizeof(int*));	// ret포인터 생성 및 동적 할당
    *ret = 5;								// 힙메모리에 5를 할당, ret포인터에 5의 주소값을 입력
    
    cout << "Value's Address : " << ret << endl;    // 숫자 5의 메모리 주소
    cout << "Pointer's Address : " << &ret << endl; // 포인터 ret의 메모리 주소
    
    return ret;		// 새로운 포인터 생성 후 ret의 주소 값과 동일하게 설정하고 반환
}

int& CallByReference(char& ref) {
    cout << "Call By Reference : " << sizeof(ref) << endl;	// buf 크기 1바이트 출력
    
    int ret = 3;	// ret변수 선언
    return ret;		// ret를 레퍼런스 값으로 반환, 오류 발생
}

int main(void) {
    char buf = 'a';
    
    int cbv = CallByValue(buf);
    int* cba = CallByAddress(&buf);
    int& cbr = CallByReference(buf);
    
    cout << "Return By Value : " << cbv << endl;			// buf가 가지고 있는 값과 동일한 값을 생성
    cout << "Return By Address (Value) : " << *cba << endl;		// cba의 값
    cout << "Return By Address (Value Address) : " << cba << endl;	// 숫자 5의 메모리 주소
    cout << "Return By Address (Pointer Value) : " << &cba << endl;	// cba의 주소값 복사 (반환된 ret와 다른 주소값을 가진다)
    cout << "Return By Reference : " << cbr << endl;			// 값을 전달하여 해당 값의 레퍼런스 변수 생성
    
    return 0;
}
main.cpp: In function ‘int& CallByReference(char&)’:
main.cpp:27:12: warning: reference to local variable ‘ret’ returned [-Wreturn-local-addr]
   27 |     return ret;         // ret를 레퍼런스 값으로 반환, 오류 발생
      |            ^~~
main.cpp:26:9: note: declared here
   26 |     int ret = 3;        // ret변수 선언
      |         ^~~
Call By Value : 1
Call By Address : 8
Value's Address : 0x55819f9a42c0
Pointer's Address : 0x7ffea878fcd0
Call By Reference : 1
Return By Value : 3
Return By Address (Value) : 5
Return By Address (Value Address) : 0x55819f9a42c0
Return By Address (Pointer Value) : 0x7ffea878fcf8

 

 


l-value & r-value

l-value

우선 l-value(left value)는 메모리 상에 존재하며 const가 아닌 새로운 값이 대입가능한 값을 의미합니다.

이는, 변수, 배열의 각 칸, 구조체 변수 등 모든 대입 연산자의 왼편에 올 수 있는 값들을 l-value라고 합니다.

 

r-value

그렇다면 r-value는 말그대로, 대입되는 대상에게 전해주는 값을 의미합니다.

이러한 값들은 메모리상 존재하지 않을 수도 있는 상수, 식, 주솟값, 함수의 리턴 값 등 모든 값이 적용됩니다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함