[C++] 참조자(Reference)

2022. 2. 8. 17:40Languages/C++

*<씹어먹는 C++>을 보며 공부하여 정리한 글입니다.

 

 

1. 참조자(Reference)란?

 

  • C 언어에서는 함수에 인자 전달 시, 원본을 바꾸는 방법으로는 Call by address밖에 없었다.
  • 하지만, C++에서는 Call by address 외에도 다른 변수나 상수를 가리킬 수 있는 참조자(Reference) 방식또한 지원한다.
  • 포인터와 달리 &연산자 사용이 줄기 때문에 코드를 훨씬 간결하게 나타낼 수 있다.
  • scanf()와 달리 cin&를 사용하여 주소값을 줄 필요가 없는 것도 cin 내부적으로 참조를 받기 때문이다.
int x = 5;
int& referenceX = x;     // 참조자 선언
referenceX = 10;

std::cout << "x = " << x << std::endl;
std::cout << "referenceX = " << referenceX << std:endl;


/* 결과
x = 10
referenceX = 10

*/

 

  • 참조자 선언 방법은 가리키고자 하는 타입 뒤에 &를 붙이면 된다. ( Ex. int& )
    • 참조자 선언을 통해 컴파일러에게 또 다른 이름이라고 알려주는 것
  • 그렇기 때문에 위 코드에서 referenceX의 값을 바꿔도 사실상 x에서 작업을 하는 것과 같다.

 

 

참조자는 반드시 초기값을 주어야 한다.

 

  • 포인터와 달리, 참조자는 정의 시에 반드시 누구의 별명인지 명시 (그러지 않을 경우 오류 발생)
int x = 5;
// int& referenceX;  오류

int& referenceX = x;   // 초기값 지정 필수

 

  • 허나, 함수의 매개변수로 받을 때는 명시하지 않아도 된다.
void Swap(int& x, int& y) {   // 매개변수로 참조를 받을 수 있음
   int temp = x;
   x = y;
   y = temp;
}

int main() {
   int x = 10;
   int y = 20;
   
   Swap(x, y);
}
  • Swap(x, y) 함수를 호출하는 시점에서 매개변수 int& xint& y가 정의되므로 사실상 int& x = x, int& y = y가 실행됨
  • 따라서, 문제가 될 소지가 없음

 

 

한 번 별명 설정이 되면, 다른 별명으로 변경이 불가능

 

  • 참조자 별명이 설정되면, 다른 별명으로 설정하려고 해도 단순 대입 연산으로 취급된다.
  • 번외로, 포인터는 다른 대상을 가리키도록 자유롭게 변경이 가능하다.
int x = 5;
int& referenceX = x;   // 별명 설정 연산

int y = 10;
referenceX = y;        // referenceX = 10; 과 같은 연산

// &referenceX = y;    불가능

 

 

 

참조(Reference)는 메모리 상에 존재하지 않을 수도 있다.

 

  • 포인터 변수 역시, 메모리 주소를 담아놓는 변수이기 때문에 메모리 상에 존재한다. (64bit 아키텍처의 경우, 8 byte)
  • 허나, 참조의 경우에는 참조가 쓰이는 코드 자리는 참조한 원본 대상으로 교체하면 된다.
  • 그렇기에 참조는 메모리 상에 존재할 필요가 없으나, 반드시 그런 것은 아니다.
int x = 10;
int& referenceX = x;

...

referenceX = 20;  // x = 20;으로 컴파일러가 바꾸면 된다.

 

 

 


2. 참조자와 관련된 금지 사항

 

참조의 참조자를 생성하는 경우는 불가능

 

int x = 5;
int& y = x;
int& z = y;   // 참조자의 참조자를 만드는 게 아닌 x의 참조자를 생성하는 연산

std::cout << x << std::endl;     // 5
std::cout << y << std::endl;     // 5
std::cout << z << std::endl;     // 5

 

단순 리터럴에 대한 참조자 생성은 불가능

 

  • 상수 그 자체인 리터럴에 대한 참조자 생성이 가능하면, 리터럴 값 자체를 바꿀 수 있게 되는 모순이 생기기 때문
  • 상수 참조자로 선언한다면 리터럴로 참조가 가능
// int& x = 10;         'initializing' 오류 발생

const int& x = 10;   // 상수 참조자로는 리터럴 상수 참조 가능

 

참조 배열을 생성하는 것은 불가능

 

/*
참조 배열은 사용할 수 없다.
int x, y;
int& arr[2] = { x, y };
*/

 

  • 참조자의 경우, 특별한 경우가 아닌 이상 메모리 상에서 공간을 차지하지 않는다.
  • 배열의 특성상, 배열의 이름은 첫 번째 원소의 주소값으로 변환이 될 수 있어야 하는데 여기에 모순이 생긴다.
  • 배열에 대한 참조는 가능하다. 단, 포인터와 달리 반드시 배열의 크기를 명시해야 함
// 배열 참조는 사용할 수 있다.
int arr[3] = {10, 20, 30};
int (&refArr)[3] = arr;    // 배열 크기 명시

for (int i = 0; i < 3; i++) {
	std::cout << refArr[i] << std::endl;
}

// 이차원 배열 역시 동일
int arr[2][2] = {1, 2, 3, 4};
int (&refArr)[2][2] = arr;

 

포인터에 대한 참조자는 생성 불가능

 

int x = 5;
int* ptr = &x;

// int& refPtr = ptr;   오류

 

참조를 리턴하는 함수에서 지역 변수의 참조를 리턴하지 않도록 조심

 

  • 함수 내부에서 지역변수에 대한 참조를 리턴하고 나면, 지역변수의 생존 시간 특성 때문에 자동으로 메모리 반납이 되고, 결과적으로 별명만 남게 된다.
  • 그렇기에 오류가 발생할 수 있다.
  • 이렇게 참조만 존재하고 참조하던 게 사라진 참조자를 Dangling reference라고 부른다.
int& f() {
   int x = 5;
   return x;      // 지역변수 참조 리턴
}

int main() {
   int x = f();   // 참조만 남고 참조하던 변수는 사라짐
   ...
}

 

  • 외부 변수에 대한 참조를 리턴하는 방식을 사용하는 것을 괜찮다.
  • 참조자를 리턴할 시 장점은 함수에 인자 전달이나 리턴 시, 값 복사가 이루어지는 시간 소요가 없다.
int& f(int& x) {
   x = 5;
   return x;      
}

int main() {
   int x = 10;
   int y = f(x);   // x에 대한 참조를 리턴받음
   ...
}

 

  • 참조자가 아닌 값을 리턴하는 함수를 참조자로 받을 경우, const 참조자를 사용하여 받으면 가능하다.
  • C++에서 예외 규칙으로, 상수 참조자로 리턴값을 받게 되면 해당 리턴값의 생명이 연장된다.
  • 연장되는 기간은 참조자가 사라질 때까지
int f() {
   int x = 5;
   return x;      
}

int main() {
   // int& y = f();        Dangling reference
   const int& y = f();     // 가능
   ...
}

 

 

728x90
반응형