같은 것에 대한 또 다른 이름
함수 매개변수 페이지에서는 모든 인자가 함수 안으로 복사되었습니다. 바로 그 복사 때문에 함수는 호출자의 변수를 바꿀 수 없습니다. 함수는 언제나 자신의 복제본만 볼 뿐입니다. 참조는 그 벽을 허뭅니다. 참조는 별칭, 즉 기존 변수에 묶여 정확히 같은 메모리를 공유하는 두 번째 이름입니다.
참조는 선언에서 &로 만듭니다. 한 번 묶이면 참조와 원본은 구별할 수 없습니다:
두 가지 규칙이 참조를 안전하고 예측 가능하게 만듭니다. 참조는 선언되는 순간 반드시 초기화되어야 하며(int& r;은 컴파일 오류), 이후에 다른 것을 가리키도록 결코 재바인딩할 수 없습니다. 참조에 대한 대입은 항상 그것이 처음 묶인 대상에 씁니다.
참조에 의한 전달: 함수가 거꾸로 손을 뻗게 하기
진짜 이득은 함수에 있습니다. 매개변수에 &를 붙이면 함수는 호출자 인자의 복사본이 아니라 별칭을 받습니다. 이제 함수 안의 변경 사항이 밖에서도 보입니다:
&를 빼면 addBonus는 버려질 복사본을 증가시킬 뿐이고 total은 100으로 남습니다. 이 한 글자가 모든 차이입니다. 이것은 둘 이상의 결과를 반환하거나 입력을 제자리에서 편집하는 함수를 작성하는 정석입니다. 고전적인 예는 두 변수를 교환하는 것입니다:
참조가 없으면 swapValues는 지역 복사본만 교환하고 x/y는 1 2로 남습니다. (표준 라이브러리에 이미 std::swap이 있지만, 직접 작성해 보면 참조 매개변수가 정확히 무엇을 주는지 알 수 있습니다.)
const 참조: 빠르게 읽되, 건드리지 않겠다고 약속하기
참조에 의한 전달은 복사도 피합니다. 큰 객체라면 그 복사가 비쌀 수 있습니다. 그러나 단순한 T& 매개변수는 "이것을 수정할지도 모른다"는 신호를 보내는데, 읽기만 원할 때는 오해를 부릅니다. 해결책은 const T&입니다. 참조의 복사 없는 속도와 더불어, 함수가 인자를 바꾸지 않는다는 컴파일러가 강제하는 약속을 얻습니다.
비-const 참조는 수정 가능한 변수에만 묶일 수 있지만, const 참조는 리터럴과 임시 객체에도 묶일 수 있습니다. 그래서 greet("literal works too")가 컴파일됩니다. 매개변수 타입을 고르는 실용적인 경험칙은 다음과 같습니다:
void f(int x) // 저렴한 타입, 읽기 전용 -> 그냥 복사
void f(const string& s) // 무거운 타입, 읽기 전용 -> const 참조
void f(string& s) // 호출자의 객체를 수정할 의도가 있을 때
읽기만 하는 클래스 타입(string, vector, 직접 만든 구조체)에는 기본적으로 const T&를 사용하고, 정말로 되돌려 쓰고 싶을 때를 위해 비-const 참조를 남겨두세요.
참조 반환하기
함수는 참조를 반환할 수도 있어서, 이미 존재하는 무언가에 대한 별칭을 호출자에게 건넵니다. 이는 컨테이너 같은 코드에서 흔합니다. v[i] = 5가 작동하게 만드는 것이자 operator[]가 내부에서 하는 일입니다:
at이 int&를 반환하기 때문에 호출 표현식 at(data, 1) 자체가 대입할 수 있는 좌측값(lvalue)입니다. 대신 단순한 int를 반환하면 at(data, 1) = 42는 컴파일되지 않습니다. 임시 복사본에 대입하는 셈이 되기 때문입니다.
가장 큰 함정: 댕글링 참조
참조는 아무것도 소유하지 않습니다. 그저 다른 곳에 사는 메모리를 가리킬 뿐입니다. 참조가 아직 사용 중인데 그 메모리가 사라지면 댕글링 참조가 되고, 그것을 통해 읽는 것은 미정의 동작입니다. 쓰레기 값을 출력하거나, 크래시하거나, 운영 환경에서 하루를 망칠 때까지 잘 작동하는 것처럼 보일 수도 있습니다. 고전적인 실수는 지역 변수에 대한 참조를 반환하는 것입니다:
int& broken() {
int local = 42;
return local; // 버그: broken()이 반환할 때 local이 파괴된다
} // 반환된 참조는 댕글링 상태가 된다
int main() {
int& r = broken();
cout << r << "\n"; // 미정의 동작 - 죽은 메모리를 읽는다
}
변수 local은 broken이 반환하는 순간 사라지므로, 참조는 이미 회수된 스택 공간을 가리킵니다. 호출보다 오래 살아남는 것에 대한 참조만 반환하세요. 참조로 전달된 매개변수, 데이터 멤버, 또는 static 같은 것 말입니다. 값이 함수 안에서 계산된다면 대신 값으로 반환하고 컴파일러가 복사를 최적화하도록 두세요. 같은 함정이 범위 기반 루프와 임시 객체에 묶인 모든 참조에도 적용됩니다. 참조가 이름 붙인 대상의 수명을 넘겨 참조를 절대 붙들고 있지 마세요.
다음: 함수 오버로딩
참조는 모든 매개변수에 두 번째 손잡이(복사 대 별칭, 가변 대 const)를 주며, 그 손잡이는 다음 주제와 직접 맞물립니다. 다음으로, 함수 오버로딩은 같은 이름이지만 서로 다른 매개변수 목록을 가진 여러 함수를 정의할 수 있게 해주며, 컴파일러는 인자 타입을 맞추어 올바른 함수를 고릅니다. 값으로, 참조로, 아니면 const 참조로 전달되는지까지 포함해서 말입니다.
자주 묻는 질문
C++에서 참조란 무엇인가요?
참조는 기존 변수의 별칭, 즉 같은 메모리에 대한 또 다른 이름입니다. 선언에서 &로 만듭니다: int& r = x;. 그 이후로 r과 x는 서로 바꿔 쓸 수 있으며, 하나를 바꾸면 다른 하나도 바뀝니다. 참조는 선언할 때 반드시 초기화해야 하며, 나중에 다른 변수를 가리키도록 재바인딩할 수 없습니다.
C++에서 값에 의한 전달과 참조에 의한 전달의 차이는 무엇인가요?
값에 의한 전달(void f(int x))은 인자를 복사하므로 함수는 자신의 복사본 위에서 동작하고 호출자의 변수는 그대로 유지됩니다. 참조에 의한 전달(void f(int& x))은 함수에 호출자의 변수에 대한 직접 접근을 주므로 변경 사항이 호출 후에도 보이며, 복사본이 만들어지지 않아 큰 객체에서 중요합니다.
C++에서 const 참조 매개변수는 언제 사용해야 하나요?
함수가 매개변수를 읽기만 하면 되지만 타입이 복사하기에 비싼 경우(string, vector, 큰 구조체) const T&를 사용하세요. 참조의 복사 없는 속도에 더해 함수가 호출자의 값을 수정하지 않는다는 컴파일러 보장을 얻습니다. int나 double 같은 저렴한 타입에는 단순한 값에 의한 전달이 더 간단하고 똑같이 빠릅니다.