좌측값 참조 소개
한마디로 하자면, 참조( reference )는 대상으로 하는 객체에 대한 별명( alias )입니다.
C++에서는 값을 크게 두 종류로 나누는데, 좌측값( l-value )과 우측값( r-value )이 그것들입니다.
그리고, 좌측값 참조는, 이름이 말하듯이, 좌측값에 대한 참조를 말합니다.
C++11 이전에는 우측값 참조가 정의되지 않았습니다.
따라서, 우측값 참조의 반대 개념인 좌측값 참조도 존재하지 않았습니다.
그렇지만, 그때도 이미 참조( reference )가 존재했고, 이 참조는 좌측값에 대한 참조였습니다.
즉, 과거에 언급된 참조는 모두 좌측값 참조를 말하는 것입니다.
그리고, 지금도 단지 참조라고 언급하면, 좌측값 참조를 말하는 경우가 많습니다.
이 글도 구분이 필요하지 않다면, 좌측값 참조를 참조로 표시하겠습니다.
참고로, 우측값 참조에 관한 내용은 여기에서 볼 수 있습니다.
좌측값 참조의 선언
좌측값 참조의 타입은, 참조하고자 하는 객체의 타입에 & 을 붙여서 만들어집니다.
이 참조 타입을 가진 변수를 좌측값 참조 변수( l-value reference variable )라고 하며, 이 변수는 참조되는 객체의 별명 역할을 합니다.
int a = 10;
double b = 3.14;
string c = "This is a string"s;
int& ra = a; // 변수 a에 대한 참조
double& rb = b;
string& rc = c;
ra += 10; // 참조 변수에 대한 연산
cout << "a: " << a << endl;
cout << "ra: " << ra << endl;
▼출력
a: 20
ra: 20
이 참조 변수는 상수 변수와 마찬가지로, 선언 시에 참조할 대상으로 초기화되어야 합니다.
이 초기화 과정을 참조 묶기( reference binding )라고 부릅니다.
int& ra = a; // 변수 a에 대한 참조 선언
위의 문장을 "참조 변수 ra를 객체 a에 묶었다( bind )"라고 말할 수 있습니다.
그리고, 이렇게 묶인 참조 변수에 대한 연산의 결과는 모두 참조의 대상에 그대로 적용됩니다.
예를 들어, 참조 변수 ra에 10을 더하면, 이 연산은 ra의 참조 대상인 a에 적용되므로, a의 값이 20이 되는 것입니다.
이러한 참조 묶기에는 몇 가지 제한이 있습니다.
먼저, 참조 변수는 오직 수정 가능한 객체와만 묶일 수 있습니다.
참조 변수를 통해서 객체의 값이 변경될 수 있기 때문입니다.
그러므로, 상수 객체는 참조 변수와 묶일 수 없습니다. ( 만약 그렇게 되면 모순이 됩니다. )
또한, 우측값과도 묶일 수 없습니다.
const int ca = 10;
int& ra1 = ca; // error!!. 상수 객체
int& ra2 = 5; // error!!. 우측값
그리고, 참조 변수와 참조 대상 객체의 타입이 일치해야 합니다.
int a = 10;
int& ra = a; // ok
double b = 3.14;
int& rb = b; // error !
double& rc = a; // error!
이 제한 사항에는 예외가 있는데, 파생 클래스를 기반( base ) 클래스 타입의 참조 변수와 묶는 경우입니다.
이 예외는, 참조 변수를 통해서 다형성( polymorphism )을 구현할 수 있도록 하기 위함입니다.
// 기반 클래스
class Base{
public:
virtual ~Base() = default;
};
// 파생 클래스
class Derived : public Base{
};
int main(){
Derived d;
Derived& refDerived = d; // ok
Base& refBase = d; // ok
}
참조되는 객체 d의 타입은 Derived 지만, 기반 클래스인 Base 타입의 참조 변수와 묶일 수 있습니다.
그리고, 상수 변수와 같이 한 번 초기화되면, 참조 대상을 다시 변경할 수 없습니다.
int main(){
int a = 10;
int b = 20;
int& ref = a; // a를 참조
ref = b; // b를 참조??
ref += 10;
cout << "a: " << a << ", b: " << b << endl;
}
▼출력
a: 30, b: 20
위에서, ref = b 문장은 ref의 참조 대상을 변수 b로 변경하는 것처럼 보입니다.
그러나, 실제 일어나는 일은 a = b , 즉 b의 값을 a에 대입하는 것입니다.
따라서, a의 값은 20이 되고, 여전히 변수 a를 참조하고 있는 ref 에 10을 더했으므로, a의 최종값은 30이 됩니다.
이렇게 되는 이유는 참조 변수가 사실은 실제 존재하는 객체( object )가 아니기 때문입니다.
참조 변수는 단지 객체의 별명일 뿐입니다.
그래서, 컴파일러는 가능하면 참조 변수를 참조하고 있는 객체로 대체하는 최적화를 수행합니다.
단지, 이러한 참조를 참조 변수( reference varible )이라고 부르는 것은, 이것이 마치 정상적인 변수와 똑같은 방식으로 사용되기 때문입니다. ( 물론 설명 상, 번역 상의 문제이기도 합니다. )
그렇지만, 이름에 변수가 사용되는 것 때문에, 참조 변수가 객체가 아니라는 사실을 잊어서는 안 됩니다.
따라서, 참조 변수는 객체가 사용되어야 하는 장소에는 사용될 수 없습니다.
예를 들면, 참조 변수를 원소로 하는 std::array 나 std::vector 컨테이너는 생성할 수 없습니다.
int& arr[10]; // error
std::array<int&, 5> arr; // error
std::vector<string&> vec; // error
참조 변수를 다시 참조하면 어떻게 될까요?
int value = 10;
int& ref = value; // value 객체의 참조
int&& ref_ref = ref; // 참조의 참조 ?
int&&& ref_ref_ref = ref_ref; // 참조의 참조의 참조 ?
...
위와 같이 int 타입의 참조의 참조인 ref_ref 의 타입은 int&& 가 될까요?
그렇지 않습니다.
참조의 참조는, 참조 변수 ref 가 대상으로 하는 객체( 여기서는 value )를 참조하는 또 다른 참조 변수일 뿐입니다.
이것은, 한 객체에 별명이 여러 개 붙은 것과 같습니다.
//int& ref_ref = ref;
int& ref_ref = value; // 이 문장은 위의 문장과 동일
참고로, int&& 은 우측값 참조( r-value reference )를 나타냅니다.
참조의 수명과 dangling 참조 변수
참조 변수의 수명은 일반 변수의 수명과 똑같습니다.
지역 참조 변수는 이 참조 변수가 속한 함수 또는 코드 블록을 벗어나면 파괴됩니다.
그리고, 전역으로 선언된 참조 변수는 전역 변수와 마찬가지로 프로그램 종료 시까지의 수명을 갖게 됩니다.
그렇다고 해서, 참조 변수의 수명이 참조하고 있는 대상 객체의 수명과 일치한다고 말하고 있는 것은 아닙니다.
참조 변수의 수명과 참조 대상의 수명은 독립적입니다.
참조 변수가 선언된 범위를 벗어나 파괴되더라도, 참조 대상의 객체는 파괴되지 않을 수 있습니다.
// 참조 변수를 반환하는 함수
int& return_ref_func(){
int local = 10; // 지역 변수
return local;
}
int main(){
int value = 10;
{
int& ref = value;
cout << "value:" << ref << endl;
// 이 블록을 벗어나면 ref 참조 변수는 파괴됩니다.
}
int& dangling_ref = return_ref_func(); // 참조 대상이 파괴된 참조 변수
cout << "return value: " << dangling_ref << endl; // 정의되지 않은 동작
}
위의 참조 변수 ref는 코드 블록 안에서 선언되었기 때문에, 블록을 벗어나면 파괴됩니다.
그래도, 참조의 대상인 value 변수가 파괴되는 것은 아닙니다.
이것이 위에서 말한 독립성입니다.
반대의 경우도 마찬가집니다.
위의 return_ref_func 함수는 지역 변수 local을 참조하는 참조 변수를 반환합니다.
따라서, dangling_ref 참조 변수도 함수 안의 지역 변수 local을 참조합니다.
그리고, local 함수는 함수 종료 시 파괴됩니다.
그럼에도 dangling_ref 참조 변수는 파괴되지 않습니다.
그런데, 이 경우는 상황이 좋지 않게 돌아갑니다.
dangling_ref에 접근하는 것은, 이미 파괴된 객체( local )에 접근하는 것이 되기 때문입니다.
이러한 연산의 동작은 정의되지 않습니다. ( 일반적으로는 예외로 프로그램이 종료 )
이렇게 대상이 파괴된 참조 변수를 dangling 참조라고 말합니다.
( dangling을 한글로 번역하면 좀 이상합니다.
branch를 아점으로 번역하면 이상한 것과 비슷합니다.
그래서, 언급할 필요가 있으면 그냥 dangling으로 표시하겠습니다.)
그리고, 참조 변수가 dangling 상태가 되는 것은 반드시 피해야 할 문제 중 하나입니다.
위의 경우엔, 함수의 지역 변수를, 참조 변수를 사용해서 반환했기 때문에 생긴 문제입니다.
이를 바로 잡으려면, 여러 가지 방법이 있겠지만, 참조 변수가 아니라 단순히 local 변수의 값을 반환하면 될 것입니다.
그런데, 값으로 반환( return by value )을 하면, 이를 위와 같이 참조 변수에 대입할 수 없다는 것을 기억해야 합니다.
왜냐하면, 함수의 반환 값은 우측값이고, 참조 변수( 여기서는 dangling_ref )는 우측값과 묶일 수 없기 때문입니다.
이 문제에 관한 것은 이어지는 다음 글에서 얘기하겠습니다.
참조( reference )를 사용하는 이유
한마디로, 참조를 사용하는 이유는 안전하고, 가벼운 함수 인수를 전달을 위한 것입니다.
함수 호출 시, 인수를 값으로 전달( call by value )하는 경우, 인수가 복사되어 함수에 전달됩니다.
만약, 인수가 원시 타입의 객체라면 ( int, double, char... ), 복사의 비용은 그리 크기 않습니다.
그러나, 클래스 타입의 객체들이라면 얘기가 달라집니다.
void func_by_value( std::string str){ // 값으로 전달된 매개변수
std::cout << str << endl;
}
...
std::string arg{ "This is a class type object." };
func_by_value( arg );
위의 경우, 인수 arg는 함수에 복사되어 전달됩니다.
그러나, 참조 변수를 사용한, 같은 기능의 함수를 사용하면, 이러한 복사 과정은 필요 없습니다.
이것이 C++의 거의 모든 곳에 참조 변수가 사용되는 이유입니다.
void func_by_reference( std::string& str){ // 참조 매개변수
std::cout << str << endl;
}
...
string arg = "This is a class type object.";
func_by_reference( arg );
func_by_reference 함수를 호출할 때, 인수 arg 는 참조 매개변수 str 에 묶입니다.
그러므로, str 을 사용하는 것은 실제 객체인 arg 를 그대로 사용하는 것과 같습니다.
또한, 참조 변수는 선언 시, 반드시 초기화되어야 한다는 것을 기억할 것입니다.
따라서, 이 참조 변수는 초기화되지 않은 포인터( pointer ) 매개 변수가 일으킬 수 있는 문제를 피해 갈 수 있습니다.
하지만, 단점도 있습니다.
위에서 설명했듯이, 참조 변수는 수정 가능한 객체 하고만 묶일 수 있습니다.
따라서, 참조 변수를 매개변수로 갖는 함수는 상수 객체, 우측값을 전달받을 수 없습니다.
이 문제를 해결하기 위한 것이 상수 좌측값 참조 변수( l-value reference to const )입니다.
이 글과 관련있는 글들
객체이면서 참조 역할을 하는 reference_wrapper
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 사용자 정의 타입( user defined type )을 다른 타입으로 변경하기 (0) | 2024.10.01 |
---|---|
[C++] 상수 좌측값 참조( l-value reference to const )의 성질 (0) | 2024.09.29 |
[C++] 이름이나 표현식의 타입을 알려주는 decltype (2) | 2024.09.13 |
[C++] 컴파일러가 자동으로 작성하는 멤버 함수들 (2) | 2024.09.13 |
[C++] auto의 형식 추론( type deduction ) 규칙 (0) | 2024.09.13 |