文章目录引言一、引用的本质别名而非地址1.1 别名语义1.2 引用与指针的内存视图1.3 引用必须在定义时初始化二、const 临时对象的生命线2.1 const 引用可以绑定到临时对象2.2 临时对象生命周期延长三、引用作为函数参数零拷贝传递3.1 从 C 的指针传参到 C 的引用传参3.2 传值 vs 传引用 vs 传 const 引用四、返回引用踩坑天堂4.1 可以安全返回引用的情况4.2 绝不能返回的引用五、左值引用与右值引用的第一印象5.1 什么是左值、什么是右值5.2 右值引用的基本用途移动5.3 引用折叠reference collapsing六、引用的其他细节6.1 引用成员变量6.2 指针与引用的转换6.3 不要返回函数内 lambda 捕获的引用七、引用 vs 指针决策指南总结本系列为《C深度修炼基础、STL源码与多线程实战》第9篇前置条件理解 C 语言的指针了解 C 的 const第8篇和函数第4篇引言C 程序员对指针了如指掌intx10;int*px;// p 存着 x 的地址*p20;// 通过 p 间接修改 xC 引入了引用reference——一个表面上像自动解引用的指针但实际上是一个更基础的语言概念别名。intx10;intrx;// r 是 x 的别名——不是指针不是地址就是同一个东西r20;// 等同于 x 20引用不只是更安全的指针。它引发了 C 中一整套与值类别、临时对象、完美转发相关的设计这些后话会在泛型编程章节展开。本文先打好基础引用的语义、与指针的边界、const 的妙用、以及何时用引用、何时用指针。一、引用的本质别名而非地址1.1 别名语义#includeiostreamintmain(){intx42;intrx;// r 是 x 的引用别名std::coutx x, r r\n;// 42, 42std::coutx x, r r\n;// 同一个地址r100;// 修改 r 就是修改 xstd::coutx x\n;// 100}输出x 42, r 42 x 0x7ffc1234, r 0x7ffc1234 ← 完全相同的地址 x 100取引用变量的地址得到的和被引用对象的地址是同一个地址。这一点和指针截然不同——指针变量有自己的地址其中存储的值是目标对象的地址。1.2 引用与指针的内存视图intx42;int*px;// p 是一个独立变量值为 xintrx;// r 不是独立变量它只是 x 的另一个名字// 内存视角// ┌───────┬───┐// │ x │42 │ ← 地址 0x1000// ├───────┼───┤// │ p │0x1000 │ ← 地址 0x1008p 有自己的地址// ├───────┼───┤// │ r │ (不存在独立存储r 就是 0x1000) │// └───────┴───┘引用在语言层面不占存储空间底层实现通常用指针但这不是你该依赖的细节。sizeof(r)返回的是被引用对象的大小不是指针的大小。1.3 引用必须在定义时初始化intr;// ❌ 编译错误引用必须初始化intr2x;// ✅ 定义时绑定之后不能重新绑定到别的变量inty0;r2y;// 这不是重新绑定 r2——这是把 y 的值赋给 x通过 r2与指针对比int*p;// ✅ 可以先不初始化危险但不报错px;// 后续指向 xpy;// 可以改指向 y——指针可以重新指向特性指针引用可以不初始化✅ (危险)❌ 必须初始化可以重新绑定✅p y❌ 绑定后不可改可以为空✅nullptr❌ 没有空引用有独立地址✅p ! x❌r x需要解引用✅*p 10❌ 直接使用r 10编译器可能优化掉❌✅二、const 临时对象的生命线2.1 const 引用可以绑定到临时对象这是引用最常用的模式也是 C 程序员最容易忽略的差异voidprint(conststd::strings){std::couts\n;}intmain(){print(hello);// hello 是 const char[6]不是 std::string// 但 const std::string 可以绑定到临时对象// 编译器创建一个临时 std::string(hello)引用绑定到它}没有const 你只能传std::string对象本身voidprint(std::strings){// 非 const 引用——不能绑定临时对象std::couts\n;}intmain(){// print(hello); // ❌ 不能把 const char[6] 绑定到 std::stringstd::string shello;print(s);// ✅ 可以绑定到左值}规则const T可以绑定到临时对象右值T只能绑定到左值。2.2 临时对象生命周期延长#includeiostreamclassTracer{public:Tracer(){std::coutTracer()\n;}~Tracer(){std::cout~Tracer()\n;}voidhello()const{std::couthello\n;}};intmain(){{Tracer t;// t 在作用域结束时析构std::coutbefore end of scope\n;}// t 在这里析构std::cout---\n;{constTracerrefTracer();// 临时对象生命周期延长到 ref 的作用域ref.hello();std::coutbefore end of scope\n;}// 临时 Tracer 在这里析构——因为 const 延长了它的生命}输出Tracer() before end of scope ~Tracer() --- Tracer() hello before end of scope ~Tracer()const T将临时对象的生命延长到了引用本身的作用域。这个规则确保了你不会在下一行访问已销毁的对象。三、引用作为函数参数零拷贝传递3.1 从 C 的指针传参到 C 的引用传参// C 的方式传指针voidupdate_temperature(double*temp){if(temp)*temp5.0;// 必须判空——不然解引用空指针崩掉}// 调用侧update_temperature(reading);// 需要取地址// C 的方式传引用voidupdate_temperature(doubletemp){temp5.0;// 不需要判空——引用不能为空}// 调用侧update_temperature(reading);// 不需要取地址——和传值一样的写法但零拷贝3.2 传值 vs 传引用 vs 传 const 引用#includeiostream#includestring#includevector// 传值拷贝一份voidprocess_by_value(std::vectorintv){v.push_back(42);// 修改的是副本}// 析构副本// 传引用不拷贝可修改voidprocess_by_ref(std::vectorintv){v.push_back(42);// 修改的是原对象}// 传 const 引用不拷贝不可修改voidprocess_by_cref(conststd::vectorintv){// v.push_back(42); // ❌ const不可修改std::coutv.size()\n;// ✅ 只读访问}选择标准场景传参方式小对象int, double, pointer传值大对象只读访问const T大对象需要修改T需要所有权的转移T右值引用后续章节可选参数可能为空指针T*——引用不能表示没有经验法则默认用const T传递非基本类型。需要修改时用T。需要所有权或可选时再考虑其他。四、返回引用踩坑天堂4.1 可以安全返回引用的情况情况一返回成员变量的引用classContainer{public:intat(size_t i){returndata_[i];}// 非 const 版本constintat(size_t i)const{returndata_[i];}// const 版本private:std::vectorintdata_{1,2,3};};情况二返回静态/全局对象的引用conststd::stringapp_name(){staticconststd::string nameMyApp v2.0;returnname;// 安全静态对象生命周期 整个程序}情况三返回传入的引用参数// 流操作符返回引用支持链式调用std::ostreamoperator(std::ostreamos,constPointp){returnos(p.x, p.y);}4.2 绝不能返回的引用// ❌ 灾难一返回局部变量的引用conststd::stringmake_greeting(conststd::stringname){std::string resultHello, name;// 局部变量returnresult;// 悬垂引用result 在函数返回时就销毁了}// ❌ 灾难二返回临时对象的引用constintget_value(){return42;// 临时 int 在 return 后销毁——悬垂引用}// ❌ 灾难三返回局部 unique_ptr 的引用conststd::stringbad_factory(){autopstd::make_uniquestd::string(hello);return*p;// p 在函数结束时被销毁——*p 也没了}编译器的警告可帮不少忙-Wall会警告返回局部变量的引用但不能依赖警告——逻辑上没有编译器能判断所有情况。五、左值引用与右值引用的第一印象C11 引入了右值引用rvalue reference用表示。这是移动语义和完美转发的基础——这里先给第一印象详细内容在模板章节展开。5.1 什么是左值、什么是右值简化版定义左值lvalue有名字、能取地址的表达式。如变量x、解引用*p右值rvalue临时的、没有持久身份的表达式。如字面量42、表达式结果x y、函数返回的临时对象intx10;// x 是左值intlrx;// ✅ 左值引用绑定左值// int lr2 10; // ❌ 左值引用不能绑定右值constintclr10;// ✅ const 左值引用可以绑定右值intrr10;// ✅ 右值引用绑定右值intrr2x5;// ✅ 右值引用绑定临时表达式结果// int rr3 x; // ❌ 右值引用不能直接绑定左值5.2 右值引用的基本用途移动#includeiostream#includestring#includevectorintmain(){std::vectorintv1{1,2,3,4,5};std::vectorintv2v1;// 拷贝v1 保持不变v2 是副本std::vectorintv3std::move(v1);// 移动v1 的数据被掏空并转移给 v3std::coutv1.size() v1.size()\n;// 0 —— v1 被移空了std::coutv3.size() v3.size()\n;// 5 —— 数据归 v3 了}std::move本质上是一个 cast——它把左值转成右值引用让编译器可以选择移动构造函数而非拷贝构造函数。移动操作通常很廉价对std::vector只是交换三个指针避免了深拷贝。5.3 引用折叠reference collapsing这是模板编程中才会频繁遇到的规则但了解它有助于理解一些看起来违反直觉的行为// 引用的引用在某些语境中会出现编译器自动折叠// T → T// T → T// T → T// T → T// 规则只要有一个是左值引用结果就是左值引用// 全是右值引用结果才是右值引用这个规则是std::forward完美转发能够工作的基础——后续模板章节详细展开。六、引用的其他细节6.1 引用成员变量引用可以作为类的成员classHolder{public:Holder(intref):ref_(ref){}voidset(intv){ref_v;}// 修改引用指向的外部变量private:intref_;// 引用成员};但引用成员有几个问题必须在构造的初始化列表中初始化引用不能后绑定类不能默认拷贝编译器不会自动生成拷贝赋值运算符通常用指针成员更好——除非你明确需要绑定后不可改的语义6.2 指针与引用的转换// 引用 → 指针取地址即可voidby_ref(intr){int*pr;// r 是 x 的别名r x}// 指针 → 引用先判空再解引用voidby_ptr(int*p){if(p){intr*p;// 安全已判空}}6.3 不要返回函数内 lambda 捕获的引用#includefunctional// ❌ 危险std::functionint()make_counter_bad(){intcount0;return[count](){returncount;};// count 在函数返回后销毁}// ✅ 安全按值捕获或使用 shared_ptrstd::functionint()make_counter_good(){autocountstd::make_sharedint(0);return[count](){return(*count);};}七、引用 vs 指针决策指南你是 C 程序员遇到下面场景怎么选 │ 需要不存在的值空 │ │ 是 否 │ │ 指针 需要重新绑定 │ │ 是 否 │ │ 指针 引用 │ │ 对大型对象优化传参 │ │ 是 否 │ │ const T 传值即可一句话总结引用是不会为空、不会换绑的指针。当你不想要指针的灵活度时引用是更好的约束。反过来当语义上需要表达可能没有就用指针。总结引用是 C 对 C 指针世界的最重要补丁之一——它保留了间接访问的零开销去掉了空指针和未初始化指针的危险引用的本质是别名——和原变量共享同一地址不独立占用存储语言层面const T是工程中最常用的传参方式——零拷贝 只读保证 可绑定临时对象const T延长临时对象生命周期——让你安全地接收函数返回的临时对象返回引用三思——局部变量、临时对象、局部智能指针的引用都会产生悬垂引用右值引用T是移动语义的基础——留下印象即可后续泛型编程章节会深入默认选择大型对象只读传参用const T需要修改用T可选参数用指针小对象传值第2章的4篇文章命名空间/输入输出/const/引用到此结束。这些是 C 程序员进入 C 世界必须升级的基础设施。下一篇开始进入第3章——动态内存与智能指针从new/delete一直讲到unique_ptr、shared_ptr和 RAII 的核心理念。动手练习写一个函数swap(int a, int b)用引用交换两个整数再写一个swap(int *a, int *b)用指针。对比调用侧的语法差异写一个函数返回const std::string故意返回局部变量看编译器能给出什么警告-Wall用const T改写一个之前大量传值的函数用perf统计拷贝次数的减少探索int rr 10; rr 20;能编译吗这意味着什么提示右值引用本身是左值
引用:比指针更安全的别名
文章目录引言一、引用的本质别名而非地址1.1 别名语义1.2 引用与指针的内存视图1.3 引用必须在定义时初始化二、const 临时对象的生命线2.1 const 引用可以绑定到临时对象2.2 临时对象生命周期延长三、引用作为函数参数零拷贝传递3.1 从 C 的指针传参到 C 的引用传参3.2 传值 vs 传引用 vs 传 const 引用四、返回引用踩坑天堂4.1 可以安全返回引用的情况4.2 绝不能返回的引用五、左值引用与右值引用的第一印象5.1 什么是左值、什么是右值5.2 右值引用的基本用途移动5.3 引用折叠reference collapsing六、引用的其他细节6.1 引用成员变量6.2 指针与引用的转换6.3 不要返回函数内 lambda 捕获的引用七、引用 vs 指针决策指南总结本系列为《C深度修炼基础、STL源码与多线程实战》第9篇前置条件理解 C 语言的指针了解 C 的 const第8篇和函数第4篇引言C 程序员对指针了如指掌intx10;int*px;// p 存着 x 的地址*p20;// 通过 p 间接修改 xC 引入了引用reference——一个表面上像自动解引用的指针但实际上是一个更基础的语言概念别名。intx10;intrx;// r 是 x 的别名——不是指针不是地址就是同一个东西r20;// 等同于 x 20引用不只是更安全的指针。它引发了 C 中一整套与值类别、临时对象、完美转发相关的设计这些后话会在泛型编程章节展开。本文先打好基础引用的语义、与指针的边界、const 的妙用、以及何时用引用、何时用指针。一、引用的本质别名而非地址1.1 别名语义#includeiostreamintmain(){intx42;intrx;// r 是 x 的引用别名std::coutx x, r r\n;// 42, 42std::coutx x, r r\n;// 同一个地址r100;// 修改 r 就是修改 xstd::coutx x\n;// 100}输出x 42, r 42 x 0x7ffc1234, r 0x7ffc1234 ← 完全相同的地址 x 100取引用变量的地址得到的和被引用对象的地址是同一个地址。这一点和指针截然不同——指针变量有自己的地址其中存储的值是目标对象的地址。1.2 引用与指针的内存视图intx42;int*px;// p 是一个独立变量值为 xintrx;// r 不是独立变量它只是 x 的另一个名字// 内存视角// ┌───────┬───┐// │ x │42 │ ← 地址 0x1000// ├───────┼───┤// │ p │0x1000 │ ← 地址 0x1008p 有自己的地址// ├───────┼───┤// │ r │ (不存在独立存储r 就是 0x1000) │// └───────┴───┘引用在语言层面不占存储空间底层实现通常用指针但这不是你该依赖的细节。sizeof(r)返回的是被引用对象的大小不是指针的大小。1.3 引用必须在定义时初始化intr;// ❌ 编译错误引用必须初始化intr2x;// ✅ 定义时绑定之后不能重新绑定到别的变量inty0;r2y;// 这不是重新绑定 r2——这是把 y 的值赋给 x通过 r2与指针对比int*p;// ✅ 可以先不初始化危险但不报错px;// 后续指向 xpy;// 可以改指向 y——指针可以重新指向特性指针引用可以不初始化✅ (危险)❌ 必须初始化可以重新绑定✅p y❌ 绑定后不可改可以为空✅nullptr❌ 没有空引用有独立地址✅p ! x❌r x需要解引用✅*p 10❌ 直接使用r 10编译器可能优化掉❌✅二、const 临时对象的生命线2.1 const 引用可以绑定到临时对象这是引用最常用的模式也是 C 程序员最容易忽略的差异voidprint(conststd::strings){std::couts\n;}intmain(){print(hello);// hello 是 const char[6]不是 std::string// 但 const std::string 可以绑定到临时对象// 编译器创建一个临时 std::string(hello)引用绑定到它}没有const 你只能传std::string对象本身voidprint(std::strings){// 非 const 引用——不能绑定临时对象std::couts\n;}intmain(){// print(hello); // ❌ 不能把 const char[6] 绑定到 std::stringstd::string shello;print(s);// ✅ 可以绑定到左值}规则const T可以绑定到临时对象右值T只能绑定到左值。2.2 临时对象生命周期延长#includeiostreamclassTracer{public:Tracer(){std::coutTracer()\n;}~Tracer(){std::cout~Tracer()\n;}voidhello()const{std::couthello\n;}};intmain(){{Tracer t;// t 在作用域结束时析构std::coutbefore end of scope\n;}// t 在这里析构std::cout---\n;{constTracerrefTracer();// 临时对象生命周期延长到 ref 的作用域ref.hello();std::coutbefore end of scope\n;}// 临时 Tracer 在这里析构——因为 const 延长了它的生命}输出Tracer() before end of scope ~Tracer() --- Tracer() hello before end of scope ~Tracer()const T将临时对象的生命延长到了引用本身的作用域。这个规则确保了你不会在下一行访问已销毁的对象。三、引用作为函数参数零拷贝传递3.1 从 C 的指针传参到 C 的引用传参// C 的方式传指针voidupdate_temperature(double*temp){if(temp)*temp5.0;// 必须判空——不然解引用空指针崩掉}// 调用侧update_temperature(reading);// 需要取地址// C 的方式传引用voidupdate_temperature(doubletemp){temp5.0;// 不需要判空——引用不能为空}// 调用侧update_temperature(reading);// 不需要取地址——和传值一样的写法但零拷贝3.2 传值 vs 传引用 vs 传 const 引用#includeiostream#includestring#includevector// 传值拷贝一份voidprocess_by_value(std::vectorintv){v.push_back(42);// 修改的是副本}// 析构副本// 传引用不拷贝可修改voidprocess_by_ref(std::vectorintv){v.push_back(42);// 修改的是原对象}// 传 const 引用不拷贝不可修改voidprocess_by_cref(conststd::vectorintv){// v.push_back(42); // ❌ const不可修改std::coutv.size()\n;// ✅ 只读访问}选择标准场景传参方式小对象int, double, pointer传值大对象只读访问const T大对象需要修改T需要所有权的转移T右值引用后续章节可选参数可能为空指针T*——引用不能表示没有经验法则默认用const T传递非基本类型。需要修改时用T。需要所有权或可选时再考虑其他。四、返回引用踩坑天堂4.1 可以安全返回引用的情况情况一返回成员变量的引用classContainer{public:intat(size_t i){returndata_[i];}// 非 const 版本constintat(size_t i)const{returndata_[i];}// const 版本private:std::vectorintdata_{1,2,3};};情况二返回静态/全局对象的引用conststd::stringapp_name(){staticconststd::string nameMyApp v2.0;returnname;// 安全静态对象生命周期 整个程序}情况三返回传入的引用参数// 流操作符返回引用支持链式调用std::ostreamoperator(std::ostreamos,constPointp){returnos(p.x, p.y);}4.2 绝不能返回的引用// ❌ 灾难一返回局部变量的引用conststd::stringmake_greeting(conststd::stringname){std::string resultHello, name;// 局部变量returnresult;// 悬垂引用result 在函数返回时就销毁了}// ❌ 灾难二返回临时对象的引用constintget_value(){return42;// 临时 int 在 return 后销毁——悬垂引用}// ❌ 灾难三返回局部 unique_ptr 的引用conststd::stringbad_factory(){autopstd::make_uniquestd::string(hello);return*p;// p 在函数结束时被销毁——*p 也没了}编译器的警告可帮不少忙-Wall会警告返回局部变量的引用但不能依赖警告——逻辑上没有编译器能判断所有情况。五、左值引用与右值引用的第一印象C11 引入了右值引用rvalue reference用表示。这是移动语义和完美转发的基础——这里先给第一印象详细内容在模板章节展开。5.1 什么是左值、什么是右值简化版定义左值lvalue有名字、能取地址的表达式。如变量x、解引用*p右值rvalue临时的、没有持久身份的表达式。如字面量42、表达式结果x y、函数返回的临时对象intx10;// x 是左值intlrx;// ✅ 左值引用绑定左值// int lr2 10; // ❌ 左值引用不能绑定右值constintclr10;// ✅ const 左值引用可以绑定右值intrr10;// ✅ 右值引用绑定右值intrr2x5;// ✅ 右值引用绑定临时表达式结果// int rr3 x; // ❌ 右值引用不能直接绑定左值5.2 右值引用的基本用途移动#includeiostream#includestring#includevectorintmain(){std::vectorintv1{1,2,3,4,5};std::vectorintv2v1;// 拷贝v1 保持不变v2 是副本std::vectorintv3std::move(v1);// 移动v1 的数据被掏空并转移给 v3std::coutv1.size() v1.size()\n;// 0 —— v1 被移空了std::coutv3.size() v3.size()\n;// 5 —— 数据归 v3 了}std::move本质上是一个 cast——它把左值转成右值引用让编译器可以选择移动构造函数而非拷贝构造函数。移动操作通常很廉价对std::vector只是交换三个指针避免了深拷贝。5.3 引用折叠reference collapsing这是模板编程中才会频繁遇到的规则但了解它有助于理解一些看起来违反直觉的行为// 引用的引用在某些语境中会出现编译器自动折叠// T → T// T → T// T → T// T → T// 规则只要有一个是左值引用结果就是左值引用// 全是右值引用结果才是右值引用这个规则是std::forward完美转发能够工作的基础——后续模板章节详细展开。六、引用的其他细节6.1 引用成员变量引用可以作为类的成员classHolder{public:Holder(intref):ref_(ref){}voidset(intv){ref_v;}// 修改引用指向的外部变量private:intref_;// 引用成员};但引用成员有几个问题必须在构造的初始化列表中初始化引用不能后绑定类不能默认拷贝编译器不会自动生成拷贝赋值运算符通常用指针成员更好——除非你明确需要绑定后不可改的语义6.2 指针与引用的转换// 引用 → 指针取地址即可voidby_ref(intr){int*pr;// r 是 x 的别名r x}// 指针 → 引用先判空再解引用voidby_ptr(int*p){if(p){intr*p;// 安全已判空}}6.3 不要返回函数内 lambda 捕获的引用#includefunctional// ❌ 危险std::functionint()make_counter_bad(){intcount0;return[count](){returncount;};// count 在函数返回后销毁}// ✅ 安全按值捕获或使用 shared_ptrstd::functionint()make_counter_good(){autocountstd::make_sharedint(0);return[count](){return(*count);};}七、引用 vs 指针决策指南你是 C 程序员遇到下面场景怎么选 │ 需要不存在的值空 │ │ 是 否 │ │ 指针 需要重新绑定 │ │ 是 否 │ │ 指针 引用 │ │ 对大型对象优化传参 │ │ 是 否 │ │ const T 传值即可一句话总结引用是不会为空、不会换绑的指针。当你不想要指针的灵活度时引用是更好的约束。反过来当语义上需要表达可能没有就用指针。总结引用是 C 对 C 指针世界的最重要补丁之一——它保留了间接访问的零开销去掉了空指针和未初始化指针的危险引用的本质是别名——和原变量共享同一地址不独立占用存储语言层面const T是工程中最常用的传参方式——零拷贝 只读保证 可绑定临时对象const T延长临时对象生命周期——让你安全地接收函数返回的临时对象返回引用三思——局部变量、临时对象、局部智能指针的引用都会产生悬垂引用右值引用T是移动语义的基础——留下印象即可后续泛型编程章节会深入默认选择大型对象只读传参用const T需要修改用T可选参数用指针小对象传值第2章的4篇文章命名空间/输入输出/const/引用到此结束。这些是 C 程序员进入 C 世界必须升级的基础设施。下一篇开始进入第3章——动态内存与智能指针从new/delete一直讲到unique_ptr、shared_ptr和 RAII 的核心理念。动手练习写一个函数swap(int a, int b)用引用交换两个整数再写一个swap(int *a, int *b)用指针。对比调用侧的语法差异写一个函数返回const std::string故意返回局部变量看编译器能给出什么警告-Wall用const T改写一个之前大量传值的函数用perf统计拷贝次数的减少探索int rr 10; rr 20;能编译吗这意味着什么提示右值引用本身是左值