Reference
C의 포인터와 용도가 비슷하지만, 포인터는 메모리 공간의 주소를 담는 자료형인 것에 반해, 레퍼런스는 인스턴스 그 자체를 가르키는 의미로써 사용됩니다.
l-value reference
l-value reference 는 C++에서 전통적으로 의미하는 참조 방식입니다.
#include <iostream>
int main(int argc, char** argv)
{
int value = 20;
int& ref= value;
printf("%d\n", ref);
printf("%x %x", &ref,&value);
return 0;
}
l-value reference
는 변수를 선언할때 자료형에 &를 하나 붙입니다.
- 반드시 초기화가 함께 이루어져야 합니다. reference 는 단독으로 선언될 수 없습니다.
ref 는 완전히 value 라는 변수와 동일하게 취급됩니다.
- 단순히 값 뿐만 아니라 주소까지 동일합니다.
- l-value reference 가 데이터가 있는 메모리 공간 자체를 가르키는 것이기 때문입니다.
Call by Value
#include <iostream>
int plus(int op1, int op2)
{
printf("plus address : %x %x\n", &op1, &op2);
return op1 + op2;
}
int main(int argc, char** argv)
{
int op1 = 20;
int op2 = 15;
printf("main address : %x %x\n", &op1, &op2);
printf("answer : %d\n", plus(op1,op2));
return 0;
}
일반적인 함수 호출입니다. op1과 op2를 plus 라는 함수에 넘겨 값을 계산했습니다.
main 함수와 plus 함수 내의 op가 주소 값이 다른 것을 확인할 수 있습니다.
이를 Call by Value (값에 의한 호출)
라고 부르며, op1과 op2는 plus 내의 지역 변수로써 복사되어 사용됩니다.
- 함수 호출시 인수 전달은 기본적으로 값 복사를 통해 이루어집니다.
- plus 함수 내에서 op1과 op2에 다른 값을 대입해도 main 함수의 op1과 op2는 영향을 받지 않습니다.
Call by Reference
#include <iostream>
int plus(int& op1, int& op2)
{
printf("plus address : %x %x\n", &op1, &op2);
return op1 + op2;
}
int main(int argc, char** argv)
{
int op1 = 20;
int op2 = 15;
printf("main address : %x %x\n", &op1, &op2);
printf("answer : %d\n", plus(op1,op2));
return 0;
}
plus 함수의 인수를 레퍼런스로 변경했습니다.
main 함수와 plus 함수 내의 op가 주소 값이 같은 것을 확인할 수 있습니다.
이를 Call by Reference (참조에 의한 호출)
라고 부릅니다.
- plus 함수 내에서 op1과 op2에 값을 변경하면, main 함수의 op1과 op2 역시 영향을 같이 받게 됩니다.
- Call by Value 에 비해 함수 호출 비용을 줄일 수 있습니다.
- 인수를 복사하는 비용이 들어가지 않습니다.
C의 포인터를 이용해서도 Call by Reference 를 구현할 수는 있습니다.
포인터는 주소를 담는 자료형이기 때문에 밖에서 포인터를 넘기면, 메모리 조작으로 함수 내에서 외부의 변수에 영향을 줄 수 있습니다. 그러나 C++ l-value Reference 의 장점은 '메모리 공간이 있다' 라는 사실을 보장해 준다는 것입니다.
포인터는 자료형이기 때문에 엉뚱한 값을 대입해도 문법상 에러가 발생하지 않습니다. 그러나 레퍼런스는 반드시 공간이 있는 l-value 만을 받을 수 있기 때문에 상대적으로 안전합니다.
- 흔한 경우는 아니지만, 레퍼런스를 취한 이후에 본래 변수가 소멸하는 경우에는 문제가 발생할 수 있습니다.
r-value reference (C++11)
r-value reference 는 C++11 부터 사용할 수 있는 참조 방식입니다.
l-value reference 와는 다르게 '임시 값'을 담는데 사용합니다.
r-value reference 는 자료형에 &&를 붙이며, 다시
named r-value reference
와 unnamed r-value reference
로 나뉩니다.
#include <iostream>
int plus(int&& op1, int&& op2)
{
// 이 식은 사용할 수 없습니다.
//int&& op3 = op1;
printf("plus address : %x %x\n", &op1, &op2);
return op1 + op2;
}
int main(int argc, char** argv)
{
int op1 = 20;
printf("answer : %d\n", plus(20+op1, 10));
return 0;
}
본래 r-value 는 '임시 값' 이기 때문에 휘발성이며 명시적인 주소가 없습니다.
- 20 + op1의 결과 값이나 10이란 값은 사용된 후 사라집니다.
named r-value reference 는 이 값을 잡아두어 l-value 처럼 사용할 수 있게 해줍니다.
생성된 named r-value reference 는 이름이 붙고 코드 내에서 활용될 수 있기 때문에,
l-value 로 취급 되고, 다시 r-value reference 를 바로 취할 수 없습니다.
- 따라서 int&& op3 = op1;는 사용할 수 없습니다.
하지만 std::move 함수를 이용해서 l-value 를 다시 r-value 처럼 취급되도록 할 수 있습니다.
int plus(int&& , int&& )
{
printf("unnamed r-value");
return 0;
}
위의 코드는 unnamed r-value reference 입니다. 변수 명이 지정되어 있지 않습니다.
- 해당 값은 계속해서 r-value 로 유지되며, 제어할 수 없습니다.
l-value reference 의 특징이 확정된 메모리 공간이 있다는 것이였다면
named r-value reference 의 특징은 임시적인 메모리를 잡는 것입니다.
C++ 언어의 특성상 임시 값은 수 없이 만들어졌다가 사라집니다.
20 + 10 - op1 + 10 - op2처럼 조금 더 복잡한 식이 있다고 하면, 연산자 우선순위에 따라 차례로 계산합니다.
temp1 = 10 - op2;
temp2 = -op1 + temp1;
temp3 = 10 +temp2;
temp4 = 20 + temp3;
이 과정에서 임시 값이 4번이 생성 되었습니다.
이 경우는, int 를 사용했기 때문에 CPU 내의 레지스터에서 처리 될 수도 있지만,
많은 멤버변수를 가지고 있는 클래스가 위와 같은 연산을 반복할 경우, 여러 번의 복사가 발생하여 성능을 낮추는 원인이 됩니다.
temp = 10 - op2;
temp = -op1 + temp;
temp = 10 + temp;
temp = 20 + temp;
r-value reference 를 사용하면 위와 같은 임시 메모리 공간을 재활용 할 수 있는 알고리즘을 구성할 수 있습니다.