가끔 레퍼런스랑 포인터랑 사용을 혼돈 할 때가 있다. 이터레이터(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; // 모두 고정
'👨🏻💻 programming > ◽ c, c++' 카테고리의 다른 글
(c++) 함수 뒤 const (0) | 2024.06.27 |
---|---|
(c++20) STL::Container #2 - contains, starts_with, ends_with (2) | 2024.06.17 |
(c++20) STL::Container #1 - std::to_array, erase, erase_if (0) | 2024.06.17 |
(c++20) Conditional Explicit Constructor (0) | 2024.06.17 |
(c++20) [Three-way Comparsion(3방향 비교 연산자)/우주선] 연산자 (0) | 2024.06.14 |
안 하는 것 보다 낫겠지
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!