cpp의 참조자(레퍼런스)

·2022년 5월 13일
0

cpp_study

목록 보기
3/25

참조자

지금까지는 두 가지 변수 타입(일반 변수, 포인터)을 공부했다.

  • 일반 변수: 직접 값을 보유
  • 포인터: 다른 값의 주소(또는 null)을 보유

지금부터 보게 될 참조형은 다른 객체 또는 값의 별칭으로 사용되는 C++ 타입으로,
C++은 non-const 값 참조형, const 값 참조형, r-value 참조형을 지원한다.

참조자 예제

#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;
}

C언어와는 달리 C++에서는 다른 변수나 상수를 가리키는 방법으로 참조자(레퍼런스 - reference)를 사용한다.

위 예제에서 선언하면서 another_a는 a의 참조자, 즉 a의 또다른 이름으로 컴파일러에게 알려주는 꼴이 된 것이다.
따라서 another_a는 a의 또다른 이름이라고 컴파일러에게 알려주는 것이다.

레퍼런스와 포인터의 차이점

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

int& another_a;
// 실행 안되는 코드

반면에 포인터의 경우

int* p;
// 실행되는 코드

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

int a = 10;
int &another_a = a; // another_a 는 이제 a 의 참조자!
int b = 3;
another_a = b; // **a = another_a 의 공간에 b의 값을 대입함**

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

  • 포인터의 경우 int *p = 8이라는 포인터 p를 정의한다면 p는 메모리 상에서 8바이트를 차지함.
  • 그러나 레퍼런스의 경우, 메모리 상에 존재하지 않을 수 있음. 컴파일러가 another_a가 쓰이는 자리를 모두 a로 바꿔치기하면 되기 때문.

함수 인자로 레퍼런스 받기

#include <iostream>

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

int main() {
  int number = 5;
  std::cout << number << std::endl;
  change_val(number);
  std::cout << number << std::endl;
}

여러 가지 참조자 예시들

  • 참조자의 참조자를 만드는 것은 금지되어 있음
  • 레퍼런스가 메모리상에 존재할 수 있는 경우에 대해 더 알아봐야 함
// 참조자 이해하기
#include <iostream>

int main() {
	int x;
	int& y = x;
	int& z = y;
    
	x = 1;
	std::cout << "x : " << x << " y : " << y << " z : " << z << std::endl;
    
	y = 2;
	std::cout << "x : " << x << " y : " << y << " z : " << z << std::endl;
    
	z = 3;
	std::cout << "x : " << x << " y : " << y << " z : " << z << std::endl;
}
// y, z는 x에 대한 레퍼런스

레퍼런스가 메모리상에 존재할 수 있는 경우

모호한 질문이다.
레퍼런스 자체는 메모리상에 존재하지 않는다.
그러나 레퍼런스가 별칭하는 값은 메모리상에 존재한다.

아래 코드에서 a는 func1의 실행이 끝난 이후 별칭으로서도 사라진다.

#include <iostream>

int func1(int &a){ 
    a++;
    return a; 
}
  
int &func2(int &a){ 
    a++;
    return a;
}
  
int main(){
    int x = 10;
    int y = 20;
    int& j = x;
    
    std::cout << fn1(x) << std::endl;
    std::cout << fn2(y) << std::endl;
}

상수에 대한 참조자

리터럴에 대한 참조자는 불가
단, 상수 참조자로 선언한다면 리터럴도 참조 가능
null 참조 역시 상수 참조자(const 사용)로 사용하는 경우 가능

#include <iostream>
int main() {
  int &ref = 4;	// 불가
  const int &ref2 = 4; // 가능
  
  std::cout << ref << std::endl;
}

상수 참조자를 쓰는 경우

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

  • 배열을 초기화할 때 레퍼런스로 하는 건 불가
    -> 레퍼런스는 메모리 상에서 공간 차지 X기 때문
  • 배열을 가지고 있는 변수의 레퍼런스를 정의하는 것은 가능
#include <iostream>

int main() {
  int a,b;
  int &arr[2] = {a,b};	// 불가한 코드: 레퍼런스로 배열을 정의하는 것은 불가

  int arr[3] = {1, 2, 3};
  int(&ref)[3] = arr;	// 가능한 코드: 배열의 레퍼런스로 ref를 정의하는 것은 가능
  // 따라서 ref[0]부터 ref[2]가 각각 arr[0]부터 arr[2]의 레퍼런스가 됨
  
  ref[0] = 2;
  ref[1] = 3;
  ref[2] = 1;
  
  std::cout << arr[0] << arr[1] << arr[2] << std::endl;
  return 0;
}

레퍼런스를 리턴하는 함수

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

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

위와 같이 코드를 실행하면 function 안에 정의된 a라는 변수의 값이 b에 복사됨
function이 종료되면 a는 메모리에서 사라짐(main 안에서는 a를 만날 길이 없음)

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

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

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

불가하다.
dangling reference가 되어버리기 때문이다.
즉, 리턴하는 function 안에 정의되어 있는 a는 함수의 리턴과 함께 사라진다.

레퍼런스를 리턴하는 함수에서 지역 변수의 레퍼런스를 리턴하지 않도록 조심해야 함.

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

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

function()은 인자로 받은 레퍼런스인 a를 그대로 리턴함
function(b)를 실행한 시점에서 a는 main의 b를 참조하고 있음(a는 b의 레퍼런스)

참조자를 리턴하는 경우의 장점

: 레퍼런스가 참조하는 타임의 크기와 상관 없이 딱 한번의 주소값 복사로 전달이 끝나게 됨.
매우 효율적임!

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

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

int main() {
  int& c = function();
  	// 컴파일 오류 발생
  return 0;
}

컴파일 오류 발생
-> 상수가 아닌 레퍼런스가 function 함수의 리턴값을 참조할 수 없음

이유
-> 함수의 리턴값은 해당 문장이 끝난 후 바로 사라지는 리터럴과 다름이 없음.
따라서 참조자를 만들게 되면 바로 다음에 댕글링 레퍼런스가 되어버림.

그런데 다음과 같이 const 참조자로 받으면 문제 없이 컴파일되는 것을 확인할 수 있음.

#include <iostream>

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

왜 상수 참조자로 받으면 문제 없이 컴파일되는가?

상수 레퍼런스로 리턴값을 받게 되면 해당 리턴값의 생명이 연장됨.
BUT 연장 기간은 레퍼런스가 사라질 때까지임.

~82p

profile
이것저것 개발하는 것 좋아하지만 서버 개발이 제일 좋더라구요..

0개의 댓글