참조자는 자신이 참조하는 변수를 대신할 수 있는 또 하나의 이름이다. 일종의 * 연산자를 붙이고 사용하는 포인터 변수라 생각하면 편하다.
참조자를 선언할 때는 몇 가지 주의 사항이 있다.
nullptr 로 초기화할 수 없다.int& 참조자이름 = 변수이름; // 참조자 선언
int num1 = 10;
int& num2 = num1; // 참조자 선언
int& num3; // 오류! (초기화 안됨)
int& num4 = 10; // 오류! (상수를 대상으로 선언할 수 없음)
int& num5 = NULL; // 오류! (NULL로 초기화 불가능)
여기서 사용된 & 연산자는 주소 연산자가 아닌 참조자의 선언을 뜻한다.
이미 선언된 변수의 앞에 & 연산자가 오면 주소 값의 반환을 의미하지만, 새로 선언되는 변수의 이름 앞에 & 연산자가 오면 참조자의 선언을 뜻한다.
참조자를 간략한 그림으로 나타내면 다음과 같다.

참조자는 사실상 변수로 봐도 크게 무리 없으며 그 기능과 연산의 결과가 변수와 동일하다.
#include <iostream>
int main()
{
int num1 = 10;
int& num2 = num1;
// 출력
std::cout << "변수 : " << num1 << std::endl;
std::cout << "참조자 : " << num2 << std::endl;
// num1 변경
num1 = 20;
std::cout << std::endl;
std::cout << "변수 : " << num1 << std::endl;
std::cout << "참조자 : " << num2 << std::endl;
// num2 변경
num2 = 30;
std::cout << std::endl;
std::cout << "변수 : " << num1 << std::endl;
std::cout << "참조자 : " << num2 << std::endl;
// 주소값
std::cout << std::endl;
std::cout << "&변수 : " << &num1 << std::endl;
std::cout << "&참조자 : " << &num2 << std::endl;
return 0;
}
변수 : 10
참조자 : 10
변수 : 20
참조자 : 20
변수 : 30
참조자 : 30
&변수 : 000000742BCFF834
&참조자 : 000000742BCFF834
참조자를 이용해 연산을 수행하면 원본 변수도 같이 변경되는 것을 알 수 있다.
C++ 에서 함수 외부에 선언된 변수로의 접근 방법으로는 두 가지 방법이 있다.
참조자를 기반으로 Call-by-Reference 를 진행할 수 있다.
#include <iostream>
// 두 수를 서로 교환하는 함수
void Swap(int& ref_x, int& ref_y) // 참조자를 통해 참조에 의한 호출 구현
{
int temp;
std::cout << "교환 전 함수 내부 매개변수" << "\\n";
std::cout << "(x, y) : " << ref_x << ", " << ref_y << "\\n\\n";
temp = ref_x;
ref_x = ref_y;
ref_y = temp;
std::cout << "교환 후 함수 내부 매개변수" << "\\n";
std::cout << "(x, y) : " << ref_x << ", " << ref_y << "\\n\\n";
}
int main(void)
{
int x = 10;
int y = 20;
std::cout << "함수 호출 전 원본변수" << "\\n";
std::cout << "(x, y) : " << x << ", " << y << "\\n\\n";
Swap(x, y);
std::cout << "함수 호출 후 원본변수" << "\\n";
std::cout << "(x, y) : " << x << ", " << y << "\\n\\n";
return 0;
}
함수 호출 전 원본변수
(x, y) : 10, 20
교환 전 함수 내부 매개변수
(x, y) : 10, 20
교환 후 함수 내부 매개변수
(x, y) : 20, 10
함수 호출 후 원본변수
(x, y) : 20, 10
위의 Swap 함수가 호출되는 과정을 간략히 그림으로 나타내면 다음과 같다.

참조자의 활용이 포인터의 활용보다 상대적으로 쉽기 때문에 참조자 기반의 함수 정의가 더 좋은 선택이라고 생각할 수 있지만 참조자 기반의 함수 정의에는 단점이 있다.
아래의 코드는 출력 결과를 정확히 예측할 수 없다.
int num = 10;
Func(num);
cout << num << endl;
함수가 참조자를 이용해 num 에 저장된 값을 변경할수도 있기 때문이다.
void Func(int& ref)
{
ref = 20;
}
즉, 참조자를 사용하는 경우 함수의 호출 문장만 보고는 함수의 특성을 정확히 파악할 수 없어 반드시 함수의 원형을 확인해야 한다. 또한 확인 결과 참조자가 매개 변수의 선언에 와있다면 함수 내부 코드를 분석해 참조자를 통한 값의 변경이 일어나는지를 확인해야 한다.
이러한 단점은 const 키워드를 이용하면 어느 정도 해결할 수 있다. 만약 함수 내에서 참조자를 통한 값의 변경이 일어나지 않으면 const 키워드를 붙여 함수의 원형에 명시할 수 있다. const 키워드가 붙은 참조자의 값을 변경하는 경우 컴파일 에러가 발생한다.
void Func(const int& ref)
{
ref = 20; // 컴파일 에러!
}
이 과정은 사소해 보이지만 여러 프로그래머가 협업하는 프로젝트에서는 매우 중요한 원칙과 습관으로 참조자뿐만 아니라 포인터를 통한 호출의 경우에도 함수 내부에서 값의 변경이 일어나지 않으면 반드시 매개 변수 선언에 const 키워드를 붙여 명시할 수 있다.
함수의 반환형이 참조형인 경우 반환 값을 무엇으로 저장하냐에 따라 그 결과에 차이가 있다.
참조형 반환값을 참조자에 저장하면 참조의 관계가 하나 더 추가된다.
#include <iostream>
int& Func(int& ref)
{
ref = 20;
return ref;
}
int main(void)
{
int num1 = 10;
int& num2 = Func(num1);
num1++;
num2++;
std::cout << "num1 : " << num1 << std::endl;
std::cout << "num2 : " << num2 << std::endl;
return 0;
}
num1 : 22
num2 : 22
num1 과 num2 가 동일한 메모리 공간을 가리킨다. 위의 코드를 간략히 그림으로 나타내면 다음과 같다.

참조형 반환값을 일반 변수에 저장하면 완전히 별개의 변수가 된다.
#include <iostream>
int& Func(int& ref)
{
ref = 20;
return ref;
}
int main(void)
{
int num1 = 10;
int num2 = Func(num1);
num1++;
num2--;
std::cout << "num1 : " << num1 << std::endl;
std::cout << "num2 : " << num2 << std::endl;
return 0;
}
num1 : 21
num2 : 19
num1 과 num2 가 서로 다른 메모리 공간을 가리킨다. 위의 코드를 간략히 그림으로 나타내면 다음과 같다.

참조형을 함수의 반환형으로 사용할 경우 함수의 지역변수를 참조형으로 반환하는 일은 없어야 한다.
함수의 지역변수를 참조형으로 반환할 경우 함수 호출 종료 후 지역변수가 소멸되기 때문에 참조자는 할당이 해제된 메모리 영역을 가리키게 된다.
#include <iostream>
int& Func(int num)
{
num += 10;
return num;
}
int main(void)
{
int num1 = 10;
int& num2 = Func(num1);
std::cout << "num1 : " << num1 << std::endl;
std::cout << "num2 : " << num2 << std::endl;
return 0;
}
num1 : 10
num2 : -255320256