💙ref.

모두의 코드 씹어먹는 C++ <2. C++참조자(레퍼런스)의 도입>



1. 정의

C++에서 변수나 상수를 가리키는 방법으로 참조자(reference)를 사용할 수 있다.

참조자 변수의 작업은 원본 변수를 다루는 것과 동일하다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main() 
{
    int a = 3;          // int형 변수 a를 정의한 후 3 대입
    int& another_a = a; // a의 참조자 another_a 정의 (another_a가 a의 또 다른 이름이 된다!)

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

    return 0;
}

// 실행 결과
// a : 5
// another_a : 5
  • 참조자를 정의하려면 가리키고자 하는 타입 뒤에 & 기재
    • eg) int* 타입의 참조자: int*&



2. 포인터와의 차이

2.1. 반드시 선언과 동시에 참조할 변수를 명시하자.

1
2
int & another_a;  // (x) 참조할 변수 없음
int* p;           // (o) 포인터 선언을 통해 메모리 주소의 공간을 할당

2.2. 한 변수의 참조자는 다른 변수를 참조할 수 없다.

1
2
3
4
5
6
7
8
9
int a = 10;
int &another_a = a;
int* p = &a;

int b = 3;
another_a = b;    // a에 b 대입. 참조자 another_a가 b를 가리키는 것이 아니다!
p = &b;           // 포인터 p가 a 대신 b를 가리킴

&another_a = b;   // &a = b와 같은 의미이므로 오류 발생 

2.3. 참조자는 메모리 상에 존재하지 않을 수도 있다.

1
2
3
4
int a = 10;

int &another_a = a;   // 컴파일 시 별도의 공간 할당 없이 a로 대체
int* p = &a;          // 메모리에 포인터 p의 공간 할당
  • p: 메모리에서 8바이트 차지
  • another_a: 메모리 할당 대신 a로 대체
    • 단, 예외 존재


2.3.1. 예외: 함수 인자로 레퍼런스 받기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int change_val(int& p)    // 함수 인자로 참조자를 받음,
{                         // 참조자 p가 number의 새 명칭임을 정의!
  p = 3;                  // (== (number=3))
  return 0;
}

int main()
{
  int number = 5;

  std::cout << number << std::endl;
  change_val(number);   // 별도의 & 표시를 할 필요 x
  std::cout << number << std::endl;
}

// 실행 결과
// 5
// 3
  • numberchange_val안에 전달한 코드를 참조자를 이용해 수정
    • p 정의 타이밍: change_val(number) 호출되는 시점
      • int& p = number와 동일
    • 함수 호출 시 별도의 & 표기를 할 필요가 x



3. 참조자 예시

3.1. 참조자의 참조자?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main() 
{
  int x;
  int& y = x;		// x의 참조자 y 정의
  int& z = y;		// y의 참조자 z 정의 => x의 참조자가 됨 

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

// 실행 결과
// x : 1 y : 1 z : 1
// x : 2 y : 2 z : 2
// x : 3 y : 3 z : 3

cin >> input; vs scanf("%d", &intput);

cin은 변수 값을 바꾸기 위해 포인터로 주소값을 전달하지 않아도 되나?

=> cin은 참조자로 input 을 받으므로 포인터 표시 &를 따로 붙일 필요가 없다!

3.2. 상수에 대한 참조자

1
2
3
4
5
6
7
8
9
10
#include <iostream>

int main() 
{
  int &ref = 4;		// 컴파일 오류!

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

// 오류(활성) E0461: 비const 참조에 대한 초기 값은 lvalue여야 합니다.
  • 리터럴을 레퍼런스로 참조할 경우 상수 값을 바꾸는 행위가 허용

    => C++ 문법 상 상수 리터럴을 일반 레퍼런스가 참조하는 것은 불가능함

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int main()
{
  const int& ref = 4;   // 상수 레퍼런스를 선언해 리터럴 참조
  int a = ref;          // (== (a=4))

  std::cout << a << std::endl;
}

// 출력 결과
// 4

3.3. 배열과 참조자

참조자의 참조자, 참조자의 배열, 참조자의 포인트는 존재할 수 없다.

=> 위 상황이 가능하려면 레퍼런스가 메모리 상에서 존재해야 하기 때문이다!

  • 참조자의 배열을 선언하는 것은 불가능함

    eg) int& arr[2] = {a, b};(x)

    • 단, 배열의 참조자는 가능!


배열 참조 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main() 
{
  int arr[3] = {1, 2, 3};
  int(&ref)[3] = arr;		//arr를 참조하기 위해 ref의 크기 명시 후 참조

  ref[0] = 2;
  ref[1] = 3;
  ref[2] = 1;

  std::cout << arr[0] << arr[1] << arr[2] << std::endl;
  return 0;
}

// 실행 결과
// 231
  • ref의 각 원소들이 배열 arr의 원소가 됨

    • 배열을 참조하기 위한 레퍼런스 배열은 반드시 크기 명시

      cf) 포인터: 배열의 크기를 명시하지 않음(첫 번째 원소의 주소값)

3.4. 레퍼런스 리턴 함수

3.4.1. 지역 변수 레퍼런스 리턴


1. 지역변수로 값을 받을 경우

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
#include <iostream>

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

int main() 
{
  int b = function();
  // 아래 코드와 동일
  // int& ref = function();
  // int b = ref;
    
  return 0;
}

// 출력 결과
// main.cpp: In function ‘int& function()’:
// main.cpp:5:10: warning: reference to local variable ‘a’ returned [-Wreturn-local-addr]
//     5 |   return a;
//       |          ^
// main.cpp:4:7: note: declared here
//     4 |   int a = 2;
//       |       ^
// 세그멘테이션 오류 (core dumped)
  • function의 리턴 타입은 int& 이므로 참조자 리턴
    • 리턴과 함께 a가 소멸되므로 오류 발생(댕글링 레퍼런스(Dangling reference))

비주얼 스튜디오 계열의 컴파일러를 사용하면 정상적으로 출력된다.

함수가 리턴되면 지역변수가 소멸되지만, 메모리 상에서 바로 값을 지우지 않아 잠시 접근이 가능하다고 한다!

단, 컴파일러마다 다른 결과를 출력하므로 원칙적으로 맞는 방법을 사용하자.

2. 참조자로 값을 받을 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int& function1() 
{
	int a = 2;
	return a;
}

int& function2()
{
	int b = 3;
	return b;
}

int main() 
{
	int& f1 = function1();
	int& f2 = function2();

	std::cout << f1 << std::endl;
	return 0;
}

// 출력 결과
// 구름 ide: 세그멘테이션 오류
// VS2019: 3
  • 메모리에 할당된 지역변수 a가 삭제되었으므로 해당 주소 번지를 참조하는 f1 대신 다음 메모리 주소인 f2 가 출력
  • 위 코드와 마찬가지로, VS 외 다른 컴파일러에서는 런타임 에러 발생


3.4.2. 외부 변수 레퍼런스 리턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int& function(int& a)
{
  a = 5;
  return a;     // 인자로 받은 레퍼런스 그대로 리턴
}

int main() 
{
  int b = 2;
  int c = function(b);  // c에 b 갱신 값 5를 대입한 것과 동일!

  std::cout << c << std::endl;
  return 0;
}

// 출력 결과
// 5
  • function(b) 실행 시점에서 amain의 지역 변수b 참조
  • function이 리턴한 참조자 c는 소멸되지 않은 변수인b를 계속 참조


3.4.3. 참조자로 리턴값 받기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

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

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

// 출력 결과
// main.cpp: In function ‘int main()’:
// main.cpp:9:20: error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
//     9 |   int& c = function();
//       |            ~~~~~~~~^~
  • 함수가 리턴된 후 바로 사라지는 값이므로 레퍼런스가 참조 불가능(=> 댕글링 레퍼런스)
    • 단, const를 붙여 c를 상수로 선언하면 정상적으로 컴파일 및 출력
      • 레퍼런스가 소멸되기 전까지 리턴값 수명 연장

Leave a comment