👨🏻‍💻 programming/◽ c, c++

(c++) 레퍼런스/reference 정리

핑크코냥 2025. 5. 26. 17:05
728x90

가끔 레퍼런스랑 포인터랑 사용을 혼돈 할 때가 있다. 이터레이터(iter) 경우도 있고 막 &(*) 여러 시도하다가 빌드 오류 안나면 그냥 넘어가는 경우가 있었다. 그럴 때 마다 한 번 제대로 정리 해야겠다. 생각하면서도 넘어가는 경우가 많았다. 

오늘은 진짜 .. 정리 한 번 해보려고 한다. 내용은 [전문가를 위한 C++(개정 4판)]이다.

 

1.레퍼런스 초기화

레퍼런스는 처음 초기화 할 때 지정한 변수만 가리킨다. 한 번 생성되고 나면 가리키는 대상을 바꿀 수 없다. 레퍼런스를 선언 할 때 어떤 변수를 '대입'하면 레퍼런스는 그 변수를 가리킨다. 하지만 이렇게 선언된 레퍼런스에 다른 변수를 대입하면 레퍼런스가 가리키는 대상이 바뀌는 것이 아니라 레퍼런스가 원래 가리키던 변수의 값이 새로 대입한 변수의 값으로 바뀌게 된다.

int a = 3, y = 4; 
int& aRef = a; 
aRef = y;
aRef = &y; // error(&y:y의 포인터, aRef는 포인터에 대한  레퍼런스가 아닌 int에 레퍼런스)
int& yRef = y;
aRef = yRef; //레퍼런스가 아닌 값의 변경

 

2. 포인터의 레퍼런스 

int* intP; 
int*& ptrRef = intP; // ptrRef: (int* or intP)의 레퍼런스
ptrRef = new int; 
*ptrRef = 5;  //레퍼런스가 가져온 주소는 그 래퍼런스가 가리키는 변수의 주소와 같다. 

int x = 3; 
int& xRef = x; 
int* xptr = &xRef; //레퍼런스의 주소는 값(x)에 대한 포인터와 같다.

 

 

- xRef : x의 레퍼런스 타입(int의 레퍼런스 타입)
- &xRef : x의 주소 값 (r-value)
- xPtr : xRef의 주소 값 
- *xPtr: xRef의 주소 값안에 있는 값

 

3. 레퍼런스 멤버 변수

레퍼런스는 어떤 변수를 가리키지 않고서는 존재할 수가 없다. 따라서 레퍼런스 데이터 멤버는 반드시 생성자의 본문이 아닌 생성자 이니셜라이저에서 초기화해야 한다.

class MyClass {
    int& ref;

public:
    MyClass(int& r) : ref(r) {} // 반드시 초기화 리스트에서
};



4. const 레퍼런스와 rvalue 레퍼런스

함수 파라미터에서 레퍼런스는 성능 최적화에도 자주 쓰인다.

void print(const std::string& msg); // 복사 방지
void moveOnly(std::string&& msg);  // rvalue 참조: move 의미


const T&: 읽기 전용 참조, 임시 객체도 받을 수 있음.

T&&: rvalue 참조, move semantics에 쓰임.



5. 리터럴과 레퍼런스

void hello(std::string& msg);
hello("hello"); // 컴파일 오류: 리터럴은 lvalue 아님

void hello(const std::string& msg); // OK
void hello(std::string&& msg);      // OK

리터럴이나 임시 객체는 const T& 또는 T&& 으로 받아야 한다.



6. 레퍼런스 대신 포인터를 써야 하는 경우

레퍼런스보다 포인터가 적합한 상황도 있다

  • 가리키는 대상을 바꿔야 할 때
  • nullptr 가능성 있는 값
  • 컨테이너에 다형성 객체 저장할 때 (업캐스팅 등)
  • 동적 메모리 사용 및 소유권 관리
  • 이 경우엔 가급적 스마트 포인터 (std::unique_ptr, std::shared_ptr) 사용
  • void reset(int*& ptr) { delete ptr; ptr = nullptr; }

메모리의 소유권이 변수를 받는 코드에 있으면 객체에 대한 메모리 해제하는 책임은 그 코드있다. 기왕이면 소유권을 이전할 필요가 있다면 항상 스마트 포인터를 사용하자. 


이터레이터 혼란 해결 팁

이터레이터의 타입은 보통 포인터처럼 동작하지만, 사실상 사용자 정의 타입이다. 아래처럼 명시적으로 타입을 지정하면 실수가 줄어든다.

std::vector<int> v = {1, 2, 3};
std::vector<int>::iterator iter = v.begin(); // 명확

// auto를 적극 활용하자
auto iter = v.begin(); // 정확한 타입 추론



9. 레퍼런스 캐스팅 주의

간혹 실수로 잘못된 캐스팅으로 위험한 코드가 생길 수 있다.

double d = 3.14;
int& ref = (int&)d; // 매우 위험!

 

이런 건 reinterpret_cast처럼 의도를 명확히 하거나, 애초에 설계를 다시 고민하는 게 좋다.
※ reinterpret_cast(타입 캐스트 연산자) : 임의의 포인터 타입끼리 변환을 허용하는 캐스트 연산자. 

 


10. std::ref와 std::reference_wrapper

표준 라이브러리는 레퍼런스를 값처럼 저장하기 위한 방법도 제공한다.
대표적으로 std::reference_wrapper 와 std::ref().

#include <functional>

void func(int& a) { a += 1; }

int x = 10;
std::function<void()> f = std::bind(func, std::ref(x));
f(); // x == 11

STL 컨테이너는 값 복사를 하기 때문에 std::ref 없이는 레퍼런스를 저장 못 함.

이럴 때 std::reference_wrapper를 써야 진짜 레퍼런스가 유지됨.



11. 실수 방지 팁

*와 & 위치는 오른쪽으로 붙이는 습관을 들이자 (int* p vs int *p 논란)

auto는 레퍼런스 타입도 정확히 추론함. 필요한 경우 auto&나 auto&&를 명시

const 위치 주의: const int* vs int* const vs int const*


const int* p1;     // 읽기 전용 값 (포인터는 변경 가능)
int* const p2;     // 포인터는 고정, 값은 변경 가능
const int* const p3; // 모두 고정

728x90