무언가를 가리키는 두 가지 방법
이미 포인터는 알고 계실 겁니다. 주소를 저장하고 그곳에 있는 객체에 도달하게 해주는 변수죠. 참조는 기존 객체를 간접적으로 다루기 위해 C++가 제공하는 또 다른 도구입니다. 둘은 충분히 겹치기 때문에 초보자는 어느 쪽을 골라야 할지 모를 때가 많습니다. 그래서 이 페이지에서는 둘을 나란히 놓고 비교합니다.
짧게 말하면, 참조는 별칭입니다. int& r = x;가 실행되면 r은 x 그 자체입니다. 같은 객체에 이름만 다릅니다. 포인터는 별도의 객체로, 마침 다른 객체의 주소를 담고 있을 뿐입니다. 이 단 하나의 차이가 나머지 모든 것을 결정합니다.
참조는 별칭이다
참조는 생성되는 순간 객체에 묶여야 하며, 그 이후로 참조를 사용할 때마다 원본을 건드립니다.
사용하는 지점에서 역참조를 위한 *도, "주소를 취하는" &도 없다는 점에 주목하세요. alias를 일반 int와 정확히 똑같이 읽고 씁니다. int& alias의 &는 타입의 일부이지 주소 연산자가 아닙니다.
어디가 다른가
아래의 동작들이 바로 두 도구가 모두 존재하는 이유입니다. 외워두어야 할 표입니다.
// reference pointer
// must be initialized? yes no (but should be)
// can be null? no yes (nullptr)
// can be reseated? no yes
// pointer arithmetic? no yes
// syntax to use it just the name *p or p->member
// taking address &ref == &original &p is the pointer's own address
이 중 두 가지가 사람들을 가장 많이 헷갈리게 합니다. 첫째, 참조는 절대 재바인딩될 수 없습니다. 참조에 대입하면 값을 참조 대상 객체 안으로 복사할 뿐, 참조를 새로운 대상으로 향하게 하지 않습니다.
반면 포인터는 언제든지 자유롭게 다른 곳을 가리킬 수 있습니다.
둘째, 참조는 절대 합법적으로 null이 될 수 없지만, 포인터는 될 수 있습니다. 그 덕분에 "값 없음"을 포인터로는 표현할 수 있지만 참조로는 표현할 수 없습니다. 이는 여러분이 끊임없이 의지하게 될 성질입니다.
함수 매개변수에서의 선택
선택이 가장 자주 드러나는 곳이 바로 여기입니다. 함수가 호출자의 객체를 읽거나 수정해야 할 때 둘 다 동작하지만, 서로 다른 의도를 알립니다.
참조 버전(addTax(cart))은 "아무것도 없음"으로 호출하는 것이 불가능하므로, 함수 안에서 null을 확인할 일이 없습니다. 객체가 거기 있다는 것이 보장되기 때문입니다. 포인터 버전(applyDiscount(&cart))은 &를 통해 호출 지점에서 인자가 변경될 수 있음을 알리고, 호출자가 "해당 없음"을 뜻하는 nullptr를 전달할 수 있게 합니다. 함수의 보장에 맞는 쪽을 고르세요.
큰 타입의 읽기 전용 매개변수에는 관용적인 선택이 const T&입니다. 복사를 피하고 수정하지 않겠다고 약속합니다. 값 전달과 참조 전달에 대한 자세한 내용은 함수 매개변수를 참고하세요.
간단한 경험 법칙
확신이 서지 않으면 기본값으로 참조를 쓰고, 참조에 없는 기능이 필요할 때만 포인터로 격상하세요.
- 객체가 항상 존재하고 그 정체성이 절대 바뀌지 않을 때는 참조를 사용하세요. 함수 매개변수와 별칭의 흔한 경우입니다.
- 다음 중 하나라도 해당하면 포인터를 사용하세요:
- "아무것도 없음"이 유효한 상태일 때(선택적 인자, 일치를 못 찾을 수 있는 검색) — 포인터는
nullptr가 될 수 있습니다. - 시간이 지나면서 다른 객체를 가리켜야 할 때 — 포인터는 재지정될 수 있습니다.
delete로 해제할 힙 메모리를 관리하거나, 포인터 산술로 배열을 순회할 때.
- "아무것도 없음"이 유효한 상태일 때(선택적 인자, 일치를 못 찾을 수 있는 검색) — 포인터는
이 중 어느 것도 해당하지 않으면 참조가 더 깔끔하고 안전한 선택입니다. 컴파일러가 "항상 유효하고, 절대 재지정되지 않음"을 여러분 대신 보장해 주기 때문입니다.
피해야 할 흔한 실수
ref = other가 재바인딩할 것이라고 기대하기. 대신 참조 대상 객체 안으로 값을 대입합니다. 참조는 평생 묶여 있습니다. 재지정이 필요하면 포인터를 사용하세요.- 지역 변수에 대한 참조(또는 포인터)를 반환하기.
int& f() { int x = 5; return x; }는 댕글링 참조를 반환합니다.f가 반환될 때x가 사라지므로 그 결과를 사용하는 것은 정의되지 않은 동작입니다. 같은 함정이 포인터에도 적용됩니다(return &x;). - "null 참조"를 위조하기.
p가nullptr일 때int& r = *p;를 쓰는 것은 안전한 "빈" 참조가 아니라, 역참조하는 순간 정의되지 않은 동작입니다. 선택성은 포인터나std::optional로 표현하세요. - 습관적으로 포인터에 손을 뻗기. 인자가 항상 존재하고 절대 재지정하지 않을 거라면, 참조는 한 부류의 null 검사와 크래시를 통째로 없애줍니다. 사용하지 않는 기능에 비용을 치르지 마세요.
다음: 동적 메모리
지금까지 여러분이 참조하거나 가리킨 모든 객체는 스택에 자동으로 생성되었습니다. 다음 페이지 동적 메모리에서는 new와 delete를 다룹니다. 런타임에 운영체제에 메모리를 요청하는 것, 왜 (참조가 아니라) 포인터가 그것을 소유하는지, 그리고 해제를 잊으면 어떻게 누수가 생기는지를 설명합니다.
자주 묻는 질문
C++에서 참조와 포인터의 차이는 무엇인가요?
참조는 기존 객체의 별칭입니다. 반드시 초기화되어야 하고, 절대 null이 될 수 없으며, 이후에 다른 객체를 가리키도록 바꿀 수도 없습니다. 포인터는 주소를 담는 별도의 변수입니다. null이 될 수 있고, 다른 곳을 가리키도록 재지정할 수 있으며, 포인터 산술을 지원합니다. 참조에는 & 문법을, 포인터에는 */->를 사용합니다.
C++에서 참조 대신 포인터를 언제 써야 하나요?
"아무것도 없음"이 유효한 상태일 때(선택적 인자, 찾지 못한 결과), 시간이 지나면서 다른 객체를 가리키도록 재지정해야 할 때, 또는 delete로 해제할 힙 메모리를 소유할 때 포인터를 사용하세요. 객체가 항상 존재하고 그 정체성이 절대 바뀌지 않을 때는 참조를 사용하세요. 이는 대부분의 함수 매개변수에 해당합니다.
C++에서 참조가 null이 될 수 있나요?
아니요. 유효한 참조는 항상 실제 객체를 가리키므로, null인지 확인할 일이 없습니다. null 포인터를 역참조해 참조를 만들면(p가 null일 때 int& r = *p;) null 참조가 아니라 정의되지 않은 동작이 됩니다. "어쩌면 아무것도 없음"을 표현해야 한다면 포인터나 std::optional을 사용하세요.