레퍼런스
C++에서 새로 등장한 기술로 특정 변수의 이름 대신 새로운 이름을 붙일 수 있다. 참조자라고 한다.
- &(앰퍼샌드) 기호를 이용한다.
- 레퍼런스를 이용하여 변수를 선언할 수 있으며 이를 참조형 변수라고 한다.
- 하나의 객체가 다른 객체를 연결하는 변수이며, 대입된 변수의 값과 주소를 그대로 가진다.
- 쉽게 말해서 다른 변수에 별명을 붙이는 변수라고 생각하자.
레퍼런스 변수 선언
- 레퍼런스 변수는 다음과 같이 선언한다.
datatype &name = 변수명;
int &p = a;
Ex) 간단한 레퍼런스 변수 예제
#include <iostream>
using namespace std;
int main() {
int a = 10;
int& p = a;
cout << "a = " << p << endl;
p = 20;
cout << "a = " << p << endl;
return 0;
}
a = 10
a = 20
- p는 a의 참조자이다.
- int& p = a; 라는 코드에 의해 p는 a의 별명이라는 것을 컴파일러가 알게 된다.
레퍼런스 변수와 포인터의 차이점
- 레퍼런스 변수와 포인터는 비슷한 기능을 수행하지만, 엄연히 다른 것이다.
1. 선언과 초기화
#include <iostream>
using namespace std;
int main() {
int *ptr;
int& ref;
return 0;
}
- 포인터는 선언만 하여도 상관없지만, 참조형 변수는 선언과 초기화를 동시에 진행하여야 한다.
- 즉, 누군가의 별명인가를 정확히 명시해야 한다.
2. 값 변경 가능 여부
#include <iostream>
using namespace std;
int main() {
int a = 10;
int b = 20;
cout << "a의 주소: " << &a << endl;
cout << "b의 주소: " << &b << endl;
cout << endl;
int *ptr = &a;
int& ref = a;
cout << "ptr이 가리키는 주소: " << ptr << endl;
cout << "ref이 가리키는 주소: " << &ref << endl;
cout << endl;
ptr = &b;
ref = b;
cout << "ptr이 가리키는 주소: " << ptr << endl;
cout << "ref이 가리키는 주소: " << &ref << endl;
cout << endl;
cout << "ptr이 가리키는 값: " << *ptr << endl;
cout << "ref이 가리키는 값: " << ref << endl;
cout << endl;
cout << "a의 값: " << a << endl;
cout << "b의 값: " << b << endl;
return 0;
}
a의 주소: 00BEFA8C
b의 주소: 00BEFA80
ptr이 가리키는 주소: 00BEFA8C
ref이 가리키는 주소: 00BEFA8C
ptr이 가리키는 주소: 00BEFA80
ref이 가리키는 주소: 00BEFA8C
ptr이 가리키는 값: 20
ref이 가리키는 값: 20
a의 값: 20
b의 값: 20
- 포인터 ptr과 참조자 ref가 가리키는 변수를 a에서 b로 변경하였다.
- 포인터 ptr은 성공적으로 b를 가리키도록 변경되었지만, 참조자 ref는 그대로 a의 값과 주소를 가리키고 있다.
- 초기화와 동시에 참조자 ref는 변수 a의 별명으로 고정되었고, ref = b; 라는 코드는 a=b;와 동치이다. 따라서 ref가 b를 가리키는 코드가 아닌, a의 값이 10에서 20으로 변하는 코드이다.
- 즉, 포인터는 값을 변경할 수 있지만 참조자는 한 번 초기화를 진행하면 값을 변경할 수 없다.
3. 레퍼런스는 메모리 공간이 할당되지 않을 수 있다.
- 포인터는 선언과 동시에 메모리 상에서 공간이 할당되는 변수이다.
- 반면, 레퍼런스 변수는 별명으로만 쓰이는 경우에는 별도의 메모리 공간을 할당하지 않는다. 기존 변수의 메모리 공간을 그대로 사용한다.
- 그러나 레퍼런스가 항상 메모리 공간을 할당받지 않는 것은 아니다. 필요에 따라 메모리 공간을 할당한다.
- 자세한 내용은 아래 사이트를 참고한다.
레퍼런스 변수를 인자로 넘기기
- 레퍼런스 변수 또한 함수의 인자로 넘길 수 있다.
- call-by-reference로 동작한다. 포인터로 넘기는 것보다 더욱 깔끔한 구현이 가능하다.
Ex) 간단한 레퍼런스 인자 넘기기 예제
#include <iostream>
using namespace std;
int plus_ten(int&);
int main() {
int a = 10;
cout << "a의 값(함수 호출 전): " << a << endl;
plus_ten(a);
cout << "a의 값(함수 호출 후): " << a << endl;
return 0;
}
int plus_ten(int& n) { // 레퍼런스를 인자로 넘기기
n += 10;
return 0;
}
a의 값(함수 호출 전): 10
a의 값(함수 호출 후): 20
- 참조자를 이용하여 간단하게 call-by-reference를 구현할 수 있다.
상수 참조하기
- 기본적으로 레퍼런스로 상수 리터럴을 참조하는 것은 불가능하다.
- 대신 참조자 또한 상수 처리한다면 상수 리터럴도 참조할 수 있다.
Ex) const 참조자
#include <iostream>
using namespace std;
int main() {
const int& ref = 10;
cout << "ref의 값: " << ref << endl;
int a = ref; // a = 10; 과 동치
cout << "a의 값: "<< a << endl;
return 0;
}
ref의 값: 10
a의 값: 10
- 상수 참조자를 선언하면 상수 리터럴을 참조할 수 있다.
레퍼런스 배열
- 결론부터 말하자면 레퍼런스 배열은 illegal(불법)이다.
- 레퍼런스 배열은 구현할 수 없다는 것이 C++ 규정이다. ▶ 레퍼런스의 레퍼런스, 레퍼런스의 포인터 또한 불가능
- 배열의 이름은 주소값으로 변환할 수 있어야 한다. ▶ 배열 arr이 있을 때 arr은 *(arr + 0)와 동일하다. 배열의 이름이 주소값으로 변환될 수 있으며, 각 배열의 원소들이 메모리 공간을 차지하고 있음을 의미한다.
- 그러나 레퍼런스는 앞서 말했듯이 대부분의 경우 메모리 공간을 할당하지 않는다. 따라서 레퍼런스 배열을 정의한다는 것 자체가 모순이다.
배열 레퍼런스
- 배열 레퍼런스는 가능하다.
- 구현할 시 배열의 크기를 명시해야 한다.
Ex) 배열 레퍼런스 구현
#include <iostream>
using namespace std;
int main() {
int arr[2][2] = { {1, 2}, {3, 4} };
int(&ref)[2][2] = arr;
ref[0][0] = 10;
ref[0][1] = 20;
ref[1][0] = 30;
ref[1][1] = 40;
cout << "arr[0][0]의 값: " << arr[0][0] << endl;
cout << "arr[0][1]의 값: " << arr[0][1] << endl;
cout << "arr[1][0]의 값: " << arr[1][0] << endl;
cout << "arr[1][1]의 값: " << arr[1][1] << endl;
return 0;
}
arr[0][0]의 값: 10
arr[0][1]의 값: 20
arr[1][0]의 값: 30
arr[1][1]의 값: 40
- 배열 레퍼런스는 꼭 배열의 크기를 명시해야 함에 유의하자.
레퍼런스 리턴 함수
- 레퍼런스는 반환할 수 있다.
- 레퍼런스 리턴은 크기가 매우 큰 구조체를 처리할 때 유용하게 사용된다. 해당 구조체 전체를 복사할 필요없이 주소값만 복사하면 되기 때문이다. 포인터의 장점과 동일하다.
- 다만, 지역변수의 레퍼런스를 리턴하는 경우는 없어야 한다.
지역변수의 레퍼런스 리턴
#include <iostream>
using namespace std;
int &ref();
int main() {
int b = 0;
cout << "b의 값(함수 호출 전): " << b << endl;
b = ref();
cout << "b의 값(함수 호출 후): "<< b << endl;
return 0;
}
int &ref() {
int a = 10;
return a;
}
b의 값(함수 호출 전): 0
b의 값(함수 호출 후): 10
- 좀 전에 지역변수의 레퍼런스를 반환하지 말라 했는데 값은 제대로 나왔다.
- 컴파일 오류는 발생하지 않았지만 다음과 같은 경고가 나타난다.
댕글링 레퍼런스(Dangling Reference)
- 코드를 다시 간단하게 해석하자면 다음과 같다.
int &ref = a; // a의 값은 10
int b = ref;
- 참조 변수 ref는 a의 별명이다. 그리고 b에는 ref를 대입한다.
- 위의 경우에서는 b가 a 값을 제대로 받기는 했다.
- 하지만 ref의 참조 대상인 a가 메모리 상에서 사라졌다. 이때 변수 a가 할당받았던 메모리는 garbage memory이다. (다시 반환되어야 할 메모리)
- 이와 같은 상황에서 레퍼런스는 참조할 대상 없이 존재하는 상황이 되어 버리며, 이를 댕글링 레퍼런스라고 한다. (달랑달랑 거리는 상태를 의미)
- 다음과 같은 상황이 있다고 해보자.
#include <iostream>
using namespace std;
int &ref();
int main() {
int b = 0;
cout << "b의 값(함수 호출 전): " << b << endl;
// 짧은 순간에 메모리가 재활용 된다는 가정
int *garbage;
garbage = &ref();
*garbage = 40;
// b는 재활용 과정에 의해 이상한 값을 받게 됨
b = *garbage;
cout << "b의 값(함수 호출 후): "<< b << endl;
return 0;
}
int &ref() {
int a = 10;
return a;
}
b의 값(함수 호출 전): 0
b의 값(함수 호출 후): 40
- 위처럼 짧은 순간에 재할당되어야 할 가비지 메모리를 재활용하여 값이 변경되는 상황이 발생할 수 있다.
- 따라서 지역변수의 레퍼런스를 반환하는 일은 매우 큰 오류를 야기할 수 있다.
(이를 참고하였음. 틀릴 수 있으므로 잘못된 내용은 댓글 부탁드립니다.)
외부 변수의 레퍼런스 리턴
#include <iostream>
using namespace std;
int &ref(int&);
int main() {
int b = 0;
int c = 3;
cout << "b의 값(함수 호출 전): " << b << endl;
b = ref(c);
cout << "b의 값(함수 호출 후): " << b << endl;
return 0;
}
int &ref(int &a) {
a = 10;
return a;
}
b의 값(함수 호출 전): 0
b의 값(함수 호출 후): 10
- 이전과 동일하게 인자로 받은 레퍼런스를 그대로 반환했음에도 컴파일 에러와 warning 구문이 발생하지 않는다.
- 이번에는 a가 외부 변수인 c를 참조하였기 때문에, 함수가 종료되더라도 ref가 반환한 참조자는 살아있는 변수 c를 계속해서 참조하게 된다.
- 위 코드는 단순히 함수 호출과 함께 바뀐 c의 값 10을 b에 대입하는 것과 동일하다.
참조자가 아닌 값을 리턴하는 함수를 참조자로 받기
- 이 경우 또한, 댕글링 레퍼런스가 되어버리며 아예 컴파일도 되지 않는다.
Ex) 참조자가 아닌 값을 리턴하는 함수를 참조자로 받기
#include <iostream>
using namespace std;
int ref();
int main() {
int& b = ref();
cout << "b = " << b << endl;
return 0;
}
int ref() {
int a = 10;
return a;
}
- 참조자가 상수가 아니라면 lvalue 초기값을 가져야 한다.
- lvalue란 예를 들어 a = 10이라는 대입식에서 대입연산자 '=' 기호의 왼쪽에 올 수 있는 값이다. 해당 식이 실행완료되어도 사라지지 않는 값이며 이름을 갖는 변수를 말한다. 반면 rvalue는 대입연산자 오른쪽에 해당하는 값으로, 대입식이 실행된 후 사라지는 임시 변수를 말한다.
- 즉 해당 오류는 사라지지 않는 값(지역을 벗어나면 메모리가 사라지는 지역변수가 아닌)을 초기값으로 가져야 함을 의미한다. ▶ b가 댕글링 레퍼런스가 되어버리기 때문
- 이 오류는 다음과 같이 해결할 수 있다.
Ex) const 참조자 이용
#include <iostream>
using namespace std;
int ref();
int main() {
const int& b = ref();
cout << "b = " << b << endl;
return 0;
}
int ref() {
int a = 10;
return a;
}
b = 10
- const 키워드를 이용하여 상수 레퍼런스로 정의하였다.
- 이 경우에는 상수 처리를 하였기 때문에 리턴값이 그대로 메모리 상에 유지되며, 해당 상수 레퍼런스가 사라지지 않는 이상 계속 값이 유지된다.
- 그냥 이것저것 따지지 말고 지역변수의 레퍼런스를 리턴하지 말자.