본문 바로가기

Language/C++

[C++] 참조자(레퍼런스)

728x90
반응형

C언어에서 어떤 변수를 가리키고 싶을 때 반드시 포인터를 사용해야는데 C++에서는 다른 변수나 상수를 가리키는 방법으로 또 다른 방식을 제공한다. 이를 참조자(레퍼런스)라고 부른다.

 

참조자를 정의하는 방법은, 가리키고자 하는 타입 뒤에 &를 붙이면 된다.

int& another_a = a;

위처럼 int형 변수의 참조자를 만들고 싶을 때는 int&를, double의 참조자를 만들고 싶을 때는 double&로 하면 된다. 심지어 int* 와 같은 포인터 타입의 참조자를 만들려면 int*&로 쓰면 된다. 위와 같이 선언함으로써 another_a는 a의 참조자가 되었다. 즉, another_a는 a의 또 다른 이름이라고 컴파일러에게 알려주는 것이다. 따라서 another_a에 어떤 작업을 수행하든 사실상  a에 그 작업을 하는 것과 마찬가지이다.

 

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

① 레퍼런스는 정의 시에 반드시 누구의 별명인지 명시해야 한다.

 

② 레퍼런스가 한 번 어떤 변수의 참조자가 된다면 더 이상 다른 변수를 참조할 수 없다.

another_a에 무언가를 하는 것은 사실상 a에 무언가를 하는 것과 동일하다고 했으므로 another_a = b; 문장은 다른 변수인 b를 가리키라고 하는 것이 아니라 a = b와 동치이다.

포인터는 위와 같이 누구를 가리키는지 자유롭게 바뀔 수 있다.

 

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

포인터는 메모리 상에서 포인터 공간을 차지한다. 하지만 another_a가 쓰이는 자리는 모두 a로 바꿔치기할 수 있기 때문에 이 경우 레퍼런스는 메모리 상에 존재하지 않는다. 항상 존재하지 않는 것은 아니다.

 

함수 인자로 레퍼런스 받기

정의 시에 반드시 누구의 별명인지 명시해야 한다고 하였으나 p가 정의되는 순간은 change_val(number)로 호출할 때 이므로 사실상 int& p = number 가 실행된다고 생각하면 된다. 따라서 전혀 문제가 없다. 그 후 change_val 안에서 p = 3; 이라 하는 것은 main 함수의 number에 number = 3; 을 하는 것과 정확히 같은 작업이다.

 

여러 가지 참조자 예시들

x의 참조로 y를 정의하였다. 이제 y는 x의 또다른 별명이 된다. y가 int&니까 y의 참조자 타입은 int&&가 되야 하지 않을까 하는 의문이 들 수 있다. 하지만 참조자의 참조자라는 말의 의미를 생각해보면 사실 말이 안 된다는 것을 알 수 있다. 굳이 별명의 별명을 만든 필요는 없기 때문에! 실제로 C++ 문법 상 참조자의 참조자를 만드는 것은 금지되어 있다. z 역시 x의 참조자가 되고, y와 z 모두 x의 참조자가 된다.

 

굳이 포인터로 할 수 있는 것을 참조자로 하는 이유?  불필요한 &와 *가 필요 없기 때문에 코드를 훨씬 간결하게 나타낼 수 있다.

std::cin >> user_input;
scanf("%d", &user_input);

scanf 를 이용할 때 항상 주소값을 전달해 준다. 하지만 cin에는 그냥 user_input을 전달했는데 잘 작동한다. cin이 레퍼런스로 user_input을 받기 때문이다. 구질구질하게 &을 user_input 앞에 붙일 필요가 없게 된다.

 

상수에 대한 참조자

상수 리터럴을 일반적인 레퍼런스가 참조하는 것은 불가능하다. 상수 연산자로 선언한다면 리터럴도 참조할 수 있다. 따라서 int a =  ref; 는 a = 4; 라는 문장과 동일하게 처리된다.

 

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

레퍼런스의 배열은 불가능하다. C++ 문법 상 배열의 이름은 첫 번째 원소의 주소값으로 변환이 될 수 있어야 한다. 그런데 주소값이 존재한다는 의미는 해당 원소가 메모리 상에서 존재한다는 의미와 같다. 하지만 레퍼런스는 특별한 경우가 아닌 이상 메모리 상에서 공간을 차지하지 않는다. 따라서 이러한 모순 때문에 레퍼런스의 배열을 정의하는 것은 언어 차원에서 금지되어 있다.

 

그와 대인 배열들의 레퍼런스는 가능하다.

ref가 arr를 참조하도록 하였다. 따라서 ref[0]부터 ref[2]가 각각 arr[0]부터 arr[2]의 레퍼런스가 된다. 포인터와 다르게 배열의 레퍼런스의 경우 참조하기 위해서는 반드시 배열의 크기를 명시해야 한다.

 

레퍼런스를 리턴하는 함수

function 안에 정의된 a라는 변수의 값이 b에 복사된다. 복사되었기 때문에 function이 종료되고 나면 a는 메모리에서 사라지게 된다. 따라서 더 이상 main 안에서는 a를 만날 일이 없다.

 

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

런타임 오류가 발생한다. function의 리턴 타입은 int&이다.  따라서 참조자를 리턴하게 된다. 그런데 문제는 리턴하는 function 안에 정의되어 있는 a는 함수의 리턴과 함께 사라진다는 점이다.

int b = function();  // 이 문장은 사실상
int& ref = a;  // 와 같다

// 근데 a가 사라짐
int b = ref; // !!!

이처럼 레퍼런스는 있는데 원래 참조하던 것이 사라진 레퍼런스를 댕글링 레퍼런스(Dangling reference)라고 부른다.

 

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

인자로 받은 레퍼런스를 그대로 리턴하고 있다. function(b)를 실행한 지점에서 a는 main의 b를 참조하고 있게 된다. 따라서 function이 리턴한 참조자는 아직 살아있는 변수인 b를 계속 참조한다. 결국 int c = function(b); 문장은 c에 현재의 b 값인 2를 대입하는 것과 동일한 문장이 된다.

 

이렇게 참조자를 리턴하는 경우 장점은 엄청나게 큰 구조체가 있을 때 해당 구조체 변수를 그냥 리턴하면 전체 복사가 발생해야 해서 시간이 오래 걸리지만, 해당 구조체를 가리키는 포인터를 리턴한다면 그냥 주소 한 번 복사로 매우 빠르게 끝난다. 마찬가지로 레퍼런스를 리턴하게 된다면 레퍼런스가 참조하는 타입의 크기와 상관 없이 딱 한 번의 주소값 복사로 전달이 끝나게 되어 효율적이다.

 

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

컴파일 오류가 발생한다. 레퍼런스가 function의 리턴값을 참조할 수 없다. ①과 마찬가지로 함수의 리턴값은 해당 문장이 끝난 후 바로 사라지는 값이기 때문에 참조자를 만들게 되면 바로 다음에 댕글링 레퍼런스가 되어버리기 때문이다.

하지만 C++에서 중요한 예외 규칙이 있다. const 참조자로 받았더니 문제없이 컴파일되었고, 리턴값도 제대로 출력된다. 원칙상 함수의 리턴값은 해당 문장이 끝나면 소멸되는 것이 정상이다. 따라서 기존에 int&로 받았을 때는 컴파일 자체가 안되었다. 하지만 예외적으로 상수 레퍼런스로 리턴값을 받게 되면 해당 리턴값의 생명이 연장된다. 연장되는 기간은 레퍼런스가 사라질 때까지이다.

 

  함수에서 값 리턴 ( int f() ) 함수에서 참조자 리턴 ( int& f() )
값 타입으로 받음
( int a = f() )
값 복사됨 값 복사됨. 다만 지역 변수의 레퍼런스를 리턴하지 않도록 주의
참조자 타입으로 받음
( int& a = f() )
컴파일 오류 가능. 다만 마찬가지로 지역 변수의 레퍼런스를 리턴하지 않도록 주의
상수 참조자 타입으로 받음
( const int& a = f() )
가능 가능. 다만 마찬가지로 지역 변수의 레퍼런스를 리턴하지 않도록 주의
728x90
반응형

'Language > C++' 카테고리의 다른 글

[C++] OOP - 생성자와 소멸자  (0) 2021.07.09
[C++] 공용체, 열거체  (0) 2021.07.03
[C++] 구조체  (0) 2021.07.03
[C++] 동적 할당  (0) 2021.07.01
[C++] 포인터  (0) 2021.06.28