참조자
포인터는 메모리 주소를 직접 다루는 강력한 도구이지만, 그만큼 복잡하고 오용의 위험이 따릅니다.
C++에서는 포인터의 일부 기능을 더 안전하고 편리하게 사용할 수 있도록 참조자(Reference) 라는 개념을 제공합니다.
참조자는 C++에만 존재하는 독특한 기능으로, 기존 변수에 대한 별명(alias) 을 만드는 것입니다.
포인터와 유사하지만, 더 안전하고 간결한 문법을 제공하여 코드의 가독성을 높여줍니다.
참조자란 무엇인가?
참조자는 이미 존재하는 변수에 대한 다른 이름, 즉 별명입니다.
참조자를 선언할 때 &
(앰퍼샌드) 기호를 사용하며, 이는 포인터의 주소 연산자와 동일한 기호이지만 사용되는 맥락이 다릅니다.
참조자가 만들어지면, 참조자와 참조하는 변수는 동일한 메모리 공간을 공유합니다.
따라서 참조자를 통해 값을 변경하면 원본 변수의 값도 변경되고, 그 반대도 마찬가지입니다.
주요 특징
- 별명 (Alias): 참조자는 새로운 메모리 공간을 할당받지 않고, 기존 변수의 메모리 공간을 그대로 사용합니다.
- 선언 시 반드시 초기화: 참조자는 선언과 동시에 반드시 어떤 변수를 참조할 것인지 초기화해야 합니다. 한 번 초기화되면 다른 변수를 참조하도록 변경할 수 없습니다. (포인터는 언제든 다른 주소로 변경 가능)
nullptr
이 될 수 없음: 참조자는 항상 유효한 변수를 참조해야 합니다.nullptr
을 참조할 수 없습니다.- 역참조 불필요: 참조자는 마치 원본 변수처럼 직접 사용할 수 있습니다. 포인터처럼
*
연산자를 사용하여 역참조할 필요가 없습니다.
참조자 선언 및 사용 예시
참조자를 선언할 때는 데이터 타입 뒤에 &
를 붙입니다.
데이터타입& 참조자이름 = 참조할_변수;
#include <iostream>
int main() {
int num = 10; // int형 변수 num 선언 및 초기화
int& ref = num; // num에 대한 참조자 ref 선언 및 초기화
std::cout << "num의 값: " << num << std::endl; // 출력: 10
std::cout << "ref의 값: " << ref << std::endl; // 출력: 10 (num과 동일한 값)
// num의 주소와 ref의 주소는 동일합니다.
std::cout << "num의 주소: " << &num << std::endl; // (예: 0x7ffee0000010)
std::cout << "ref의 주소: " << &ref << std::endl; // (예: 0x7ffee0000010, num과 동일)
// 참조자를 통해 값 변경
ref = 20; // ref를 통해 값을 변경하면 num의 값도 변경됨
std::cout << "\nref = 20 후:" << std::endl;
std::cout << "num의 값: " << num << std::endl; // 출력: 20
std::cout << "ref의 값: " << ref << std::endl; // 출력: 20
// num을 통해 값 변경
num = 30; // num을 통해 값을 변경하면 ref의 값도 변경됨
std::cout << "\nnum = 30 후:" << std::endl;
std::cout << "num의 값: " << num << std::endl; // 출력: 30
std::cout << "ref의 값: " << ref << std::endl; // 출력: 30
return 0;
}
참조자와 포인터의 차이점
참조자와 포인터는 둘 다 다른 변수에 간접적으로 접근하는 기능을 제공하지만, 중요한 차이점들이 있습니다.
특징 | 포인터 (Pointer) | 참조자 (Reference) |
---|---|---|
선언 | 데이터타입* 변수이름; | 데이터타입& 변수이름 = 변수; |
메모리 | 주소를 저장할 별도의 메모리 공간 필요 | 별도의 메모리 공간 없이 원본 변수와 동일한 공간 사용 |
초기화 | 선언 후 나중에 초기화 가능 (nullptr 가능) | 선언 시 반드시 초기화해야 함 |
재지정 | 다른 변수의 주소를 저장하여 재지정 가능 | 한 번 초기화되면 재지정 불가능 (항상 동일한 변수 참조) |
널 상태 | nullptr 이 될 수 있음 (아무것도 가리키지 않음) | nullptr 이 될 수 없음 (항상 유효한 변수 참조) |
접근 | * (역참조) 연산자 필요 | * (역참조) 연산자 없이 직접 변수처럼 사용 |
산술 연산 | 포인터 산술 연산 가능 (ptr++ , ptr-- ) | 참조자 산술 연산 불가능 |
#include <iostream>
int main() {
int val1 = 5;
int val2 = 15;
// int& ref_uninitialized; // 컴파일 오류: 참조자는 선언 시 반드시 초기화해야 함
int& ref = val1; // OK: val1을 참조하도록 초기화
std::cout << "ref (val1): " << ref << std::endl; // 출력: 5
// ref = val2; // ERROR! (실제로는 val1의 값을 val2의 값으로 변경함. ref가 val2를 참조하도록 재지정하는 것이 아님!)
// 이 코드는 ref가 val2를 참조하도록 재지정하는 것이 아니라,
// ref가 참조하는 변수(즉, val1)에 val2의 값(15)을 대입하는 것입니다.
std::cout << "ref = val2 후, val1: " << val1 << std::endl; // 출력: 15 (val1의 값이 15로 변경됨)
std::cout << "ref = val2 후, ref: " << ref << std::endl; // 출력: 15
std::cout << "ref = val2 후, val2: " << val2 << std::endl; // 출력: 15
// 참조자는 한 번 특정 변수를 참조하도록 초기화되면 평생 그 변수만을 참조합니다.
return 0;
}
참조자를 활용하는 주요 시나리오
참조자는 주로 다음 두 가지 상황에서 매우 유용하게 사용됩니다.
-
함수 매개변수로 전달 (Pass-by-Reference): 함수에 인자를 전달할 때, 값의 복사 없이 원본 변수를 직접 참조하여 함수 내에서 변경할 수 있도록 합니다. 이는 큰 객체를 전달할 때 복사 오버헤드를 줄여 성능을 향상시키고, 함수가 원본 값을 변경해야 할 때 유용합니다. (다음 장에서 더 자세히 다룸)
참조자를 함수 매개변수로 전달 void increment(int& num) { // int& num: num은 호출자의 원래 변수를 참조 num++; // 원본 변수의 값이 증가 } int main() { int val = 10; increment(val); // val을 참조로 전달 std::cout << val << std::endl; // 출력: 11 (val이 변경됨) return 0; }
-
함수의 반환 값으로 사용: 함수가 어떤 변수에 대한 참조자를 반환하여, 함수 호출자가 반환된 참조자를 통해 해당 변수를 직접 수정할 수 있도록 합니다. 이는 특히 클래스에서 멤버 변수에 대한 접근자(accessor)나 연산자 오버로딩 시 유용하게 사용됩니다.
참조자를 함수 반환 값으로 사용 int global_val = 100; int& getGlobalVal() { // int& 반환: 전역 변수 global_val에 대한 참조 반환 return global_val; } int main() { getGlobalVal() = 200; // 반환된 참조자를 통해 global_val의 값을 변경 std::cout << global_val << std::endl; // 출력: 200 return 0; }
주의: 함수가 지역 변수에 대한 참조자를 반환하는 것은 매우 위험합니다. 지역 변수는 함수가 종료되면 메모리에서 사라지므로, 반환된 참조자는 유효하지 않은 메모리(
dangling reference
)를 가리키게 되어 런타임 오류를 유발할 수 있습니다.
const
참조자 (const
reference)
참조자를 const
로 선언하면, 해당 참조자를 통해 원본 변수의 값을 변경할 수 없게 됩니다.
이는 함수에 인자를 전달할 때 유용합니다. 복사를 피하면서도 원본 데이터가 함수 내에서 실수로 변경되는 것을 방지할 수 있습니다.
void printConstRef(const int& num) { // const int&: num을 변경할 수 없음
// num = 20; // 컴파일 오류! const 참조자는 값을 변경할 수 없음
std::cout << "상수 참조로 받은 값: " << num << std::endl;
}
int main() {
int data = 10;
printConstRef(data); // data를 변경 없이 참조로 전달
printConstRef(20); // 임시 객체도 const 참조로 받을 수 있음
return 0;
}
const
참조자는 매우 중요하며, C++에서 std::vector
, std::string
과 같은 큰 객체를 함수에 전달할 때 가장 일반적이고 효율적인 방법입니다.