12
12

출처
레퍼런스 씹어먹기

 

용어 정리
참조자 = 참조 = 레퍼런스

C++에서 참조는 변수나 상수를 가리키는 방법입니다.

즉, 또 다른 이름으로 똑같은 변수나 상수라고 컴파일러에게 알려주는 것입니다.

아래 예제를 보면

int a = 3; // a를 3으로 선언하고
int& another_a = a; // another_a에 a를 참조하면
another_a = 5; // another_a는 a의 별명 역할을 하게 됩니다.

std::cout << a; // a를 출력합니다.
5

another_a는 a를 참조했고
another_a에 5를 할당했습니다.

이상태에서 a를 출력해봤더니 5가 나옵니다.

즉, another_a를 변경하면 사실상 a를 변경하는 것과 같습니다.

C++에서 참조의 특징

C++에서는 참조 ( & ) 가 포인터( * ) 와는 달리
한 번 초기화되면 다른 변수를 가리킬 수 없습니다.
왜냐면 처음 참조한 변수를 끝까지 가리키기 때문에 선언 이후에
다른 변수로 변경은 절대 불가능합니다.

예제를 보면

int a = 10;
int &another_a = a; // another_a 는 이제 a 의 참조자!

std::cout << a << std::endl; // a 출력

int b = 3;
another_a = b; // b로 참조하는 것으로 변경이 불가능!
//따라서 another_a는 이제부터 a랑 똑같으니 a = b로 여겨짐

std::cout << a; // a 출력
10
3

위 상황에서

&another_a = b;

another_a에 b를 할당 하면

&a = b;

a에 b를 할당한다는 것과 똑같으니 에러가 뜹니다

참조 vs 포인터

참조는 한번 초기화 이후 가리킬 대상이 바뀔 수 없지만
포인터는 자유롭게 바뀔 수 있습니다.

포인터 예시)

int a = 10;
int *p = &a; // 포인터 p 는 a 를 가리킨다.
//이 때 포인터 p는 메모리에서 8바이트를 차지한다.

printf("%d \n", *p);

int b = 3;
p = &b; // 이제 포인터 p 는 a 를 버리고 b 를 가리킨다
//얘도 8 바이트

printf("%d \n", *p);
10
3

번외)
32비트 프로세서의 범용 레지스터의 크기는 32비트 즉 4바이트이고,
64비트 프로세서의 범용 레지스터의 크기는 64비트, 즉 8바이트이다.
그래서 위에선 int * p가 컴퓨터 프로세서의 비트따라 달라짐

또한, 포인터와는 달리 참조는 메모리 상에 공간 차지 안하도록 할 수 있습니다.
참조하는 대상으로 바꿔치기만 하면 되기 때문에 메모리 자리를 차지할 필요가 없기 때문입니다.

함수 인자로 레퍼런스 받는 예시

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

change_val 함수로 인자(number)를 &p으로 담아서 참조하도록했습니다.
그리곤 p를 3으로 변경했더니 참조한 number이 3으로 변경되어 출력이 됩니다.

상수에 대한 참조자

#include <iostream>

int main() {
  int &ref = 4;

  std::cout << ref << std::endl;
}
error C2440: 'initializing' : cannot convert from 'int' to 'int &'

4는 상수입니다. 즉, [[리터럴(literal)]] 값이므로 참조가 불가능합니다.

대신 const 키워드를 이용하면 상수 참조자로 참조가 됩니다.

const int &ref = 4;

이제부턴 ref가 4로 고정이 되고 값이 변경되지 않습니다.

참조의 배열, 배열의 참조

C++에서는 배열 참조가 특이합니다.
C++은 C언어의 문법이 허용되는게 일반적이기 때문에

int arr[2] = {1,2}

이런 코드에서 배열 arr은 주소값이 생기고, 주소값이 생긴다는 말은 메모리에 존재한다는 것입니다.

원래 문법대로면 arr[2]는 *(arr + 1)이랑 똑같습니다.

그러나 우리가 원하는 참조는 특별한 경우가 아니면 메모리 상에 존재하지 않는 것이기 때문에
특별한 모양으로 배열 참조를 하게 됩니다.

#include <iostream>

int main() {
  // 배열 선언과 배열 참조
  int arr[3] = {1, 2, 3};
  int(&ref)[3] = arr;

  //각 참조 배열 요소의 값을 변경
  ref[0] = 2;
  ref[1] = 3;
  ref[2] = 1;

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

코드를 살펴보면

  1. ref 참조 배열은 괄호()로 먼저 감싸준 뒤
  2. 참조 배열의 크기를 지정해야하므로 참조할 배열 크기인 [3]을 붙여줍니다.
  3. 배열의 시작 주소인 arr을 그대로 참조 시킵니다.
이차원 배열도 똑같이 하면 됩니다.
int arr[3][2] = {1, 2, 3, 4, 5, 6};
int(&ref)[3][2] = arr;

레퍼런스 리턴하는 함수

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

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

main()에서 b에 function() 호출로 a 값(2)을 복사해준 뒤 a는 function()의 지역변수이므로 사라집니다.

a의 사라진다는 지역변수 특성이 중요합니다.

만약 아래처럼

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

function()이 int& 타입을 리턴한다면 main()함수 안에 입장에선 int& function = a;와 똑같이 되고
이 function 값을 b같은 변수에 넣는다면 에러가 뜨게 됩니다.
왜냐하면 function는 a를 참조하는데 a가 지역변수라 사라지기 때문입니다.

이렇게 참조하던 대상이 사라진 레퍼런스를 Dangling reference라고 합니다.

함수를 참조자로 받기

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

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

보다시피 int& c 를 main()에 선언하여 function()의 리턴 값을 참조하고 있습니다.
이 경우, 아까랑 똑같이 function()의 리턴값인 a가 지역변수로 사라지기에 에러가 뜨게 됩니다.

그러면 어떻게 해야 에러가 안뜰까요?
정답은 const 키워드를 사용하시는겁니다.

const int& c = function();

이렇게 선언을 하면 상수 레퍼런스가 되어 리턴값 a의 생명이 레퍼런스의 생명까지 연장되게됩니다.

'그냥 개발글 > C++' 카테고리의 다른 글

리터럴(literal)  (0) 2023.12.12
오버로딩 (Overloading)  (0) 2023.12.12
동적 할당 new, delete  (0) 2023.12.12
클래스  (0) 2023.12.12
COMMENT