2. C++ 참조자(reference) 의 도입
Last updated
Last updated
기존 C 언어에서는 함수의 인자로 주소값을 매개변수로 전달해 함수 내에서 주소를 참조해 값을 변경하였다.
C 언어에서는 어떠한 변수를 가리키고 싶을 때는 반드시 포인터를 사용해야만 했다. 하지만 C++ 에서는 다른 변수나 상수를 가리키는 방법으로 또 다른 방법으로 참조자(reference) 라는 방식을 제공한다.
아래 코드에서 int&
(reference) 타입으로 another_a
를 선언하였고, 이 말인 즉슨 “another_a
는 a
의 참조자다!” 라는 의미이다.
a
의 참조자 라는 것은 a
의 또 다른 별칭이 생겨났다는 것을 컴파일러에게 전달해 주는 것이다.
때문에 아래 코드에서 another_a
에 a
를 대입했지만, 출력했을 때 5가 출력되는 것을 확인할 수 있다.
레퍼런스는 정의 시 반드시 누구의 별칭인지 명시 해야 한다. → 포인터의 경우 명시하지 않아도 된다.
초기화 하지 않은 코드는 사용이 불가하다.
한번 정의 된 레퍼런스는 다른 변수의 레퍼런스가 될 수 없다.
int& another_a = a;
코드는 another_a
보고 a
를 가리키라는 의미이다.
another_a = b;
코드의 의미는 a=b;
와 동일한 의미다.
→ 우리가 생각하는 의도대로 흘러가지 않는다.
반면의 포인터는 아래 코드와 같이 자유롭게 변경될 수 있다.
포인터를 아래와 같이 정의한다면, p
는 당당히 메모리 상에서 8바이트를 차지하게 된다.
→ 물론 32비트 시스템에서는 4바이트가 될 것이다.
하지만 레퍼런스의 경우, 내가 컴파일러라면 another_a
를 위해서 메모리 상에 공간을 할당할 필요가 있을까?
-> 아니다!
왜냐면, another_a
가 쓰이는 자리는 모두 a
로 바꿔치기 하면 되기 때문이다.
→ 이 경우 레퍼런스는 메모리 상에 존재하지 않게 된다.
물론 항상 존재하지 않는 것은 아니다! -> 예외는 존재한다.
함수의 매개변수로 int& p
를 받게 하였다.
→ p
가 정의되는 순간은 change_val(number)
가 호출될 때 이므로, 사실상 int& p = number
로 보아야한다.
→ 이는 p
에게 너는 앞으로 number
의 참조자 라고 알려주게 된다.
이후, p = 3;
코드를 통해 number = 3;
을 수행하는 것도 정확히 일치하게 동작한다.
ref 가 가리키고 있는 값이 리터럴이기 때문에, 만일 아래 코드가 정상이라면, 리터를 값을 바꾸는 말도 안되는 일이 발생한다..
때문에, C++ 문법 상 상수 리터럴을 일반적인 레퍼런스가 참조하는 것은 불가능하게 되어 있다.
물론 그 대신에, const int& ref = 4;
처럼 상수 참조자로 선언한다면 리터럴도 참조할 수 있다.
→ 따라서 int a = ref
는 a = 4;
와 동일하게 처리된다.
레퍼런스 배열은 가능할까? 불가능하다.. → C++ 규정 자체에서 불가능하다고 못박았다.
때문에, 아래 코드는 당연하게 컴파일 오류가 발생한다.
C++ 상에서 배열이 어떤 식으로 처리될까?
문법 상 배열의 이름은 첫 번째 원소의 주소값으로 변환이 될 수 있다.
때문에, arr[1]
과 같은 문장이 *(arr+1)
로 바뀌어서 처리될 수 있다.
그런데 주소값이 존재한다라는 의미는 “해당 원소가 메모리 상에서 존재한다.” 라는 의미와 같다. → 하지만 레퍼런스는 특별한 경우가 아닌 이상 메모리 상에서 공간을 차지 하지 않는다!! → 이런 모순된 상황 때문에, 레퍼런스의 배열을 정의하는 것은 언어 차원에서 금지가 되어 있다!!
그렇다고 그와 반대인 배열들의 레퍼런스가 불가능한 것은 아니다.
아래 코드에서 ref[0]
부터 ref[2]
가 각각 arr[0]
부터 arr[2]
의 레퍼런스가 됩니다. 포인터와는 다르게 배열의 레퍼런스의 경우 참조하기 위해선 반드시 배열의 크기를 명시해야 합니다.
따라서 int (&ref)[3]
이라면 반드시 크기가 3 인 int
배열의 별명이 되어야 하고 int (&ref)[5]
라면 크기가 5 인 int
배열의 별명이 되어야 합니다.
int b = function();
에서 어떤 일이 일어날까?
function
안에 정의된 a
라는 변수의 값이 b
에 복사 되었다!
→ 여기서 주목할 점은 복사 되었다는 점이다!
function
이 종료되고 나면 a
는 메모리에서 사라지게 된다.
→ 따라서 main
안에서 a
를 만날 길이 없다.
실제로 실행해보면 아래와 같은 컴파일 경고가 발생하고,
다음과 같은 런타임 에러가 발생한다.
무엇이 문제일까?
function
의 리턴 타입은 int&
이다. 따라서 참조자를 리턴하게 되는데,
문제는 리턴하는 function
안에 정의되어 있는 a
는 함수의 리턴과 함께 사라진다는 점이다.
이와 같이 레퍼런스는 있는데 원래 참조 하던 것이 사라진 레퍼런스를 댕글링 레퍼런스(Dangling reference) 라고 부른다.
아래의 코드는 위 코드와 다르게 매개변수로 받은 레퍼런스를 그대로 리턴하고 있다!
때문에, function(b)
를 실행하는 시점에서 a
는 main
의 b
를 참조하고 있게 된다. 따라서 function
이 리턴한 참조자는 아직 살아있는 변수인 b
를 계속 참조한다.
결국 아래 코드는 다음과 같은 코드이다. int c = function(b);
그렇다면 참조자를 리턴하는 경우의 장점은 무엇일까?
C 언어에서 엄청나게 큰 구조체가 있을 때 해당 구조체 변수를 그냥 리턴하면 전체 복사가 발생해서 시간이 오래 걸린다..
하지만, 해당 구조체를 가리키는 포인터를 리턴한다면 그냥 포인터 주소 한번의 복사로 매우 빠르게 끝이 난다.
마찬가지로 레퍼런스를 리턴하게 되면 레퍼런스가 참조하는 타입의 크기와 상관 없이 딱 한번의 주소값 복사로 전달이 끝나게 된다. → 매우 효율적이다.
해당 코드는 아래와 같이 컴파일 오류가 발생하는데, 내용은 상수가 아닌 레퍼런스가 function 함수의 리턴값을 참조할 수 없다는 의미가 된다.
왜냐면, 함수의 리턴값은 해당 문장이 끝난 후 바로 사라지는 값이기 때문에, 참조자를 만들게 되면 바로 다음에 댕글링 레퍼런스가 되기 때문이다.
하지만 C++ 에서 중요한 예외 규칙이 있다.
아래 코드는 function()
의 리턴값을 const 참조자
로 받았다. 이 코드는 문제 없이 컴파일 되었다.
→ 심지어 제대로 출력도 된다.
원칙상 함수의 리턴값은 해당 문장이 끝나면 소멸되는 것이 정상이지만,
하지만 예외적으로 상수 레퍼런스로 리턴값을 받게 되면 해당 리턴값의 생명이 연장된다! → 이 연장기간은 레퍼런스가 사라질 때까지이다..