
基本概念左值定义左值Ivalue有名字可取地址能被多次使用的对象比如说变量。形式上表示有持久存储的位置。左值的典型例子示例 1命名变量是左值#include iostream int main() { int x 10; // x 是左值 int r x; // 非-const 左值引用可以绑定到左值 r 20; std::cout x x \n; // 输出 x 20 // int r2 1; // 错误不能把字面量(右值)绑定到非-const 左值引用 return 0; }示例 2数组下标与解引用是左值#include iostream int main() { int a[3] {1,2,3}; a[0] 10; // a[0] 是左值 int* p a[1]; *p 20; // *p 是左值 std::cout a[0] a[1] \n; // 输出 10 20 int ra a[0]; // 引用数组元素左值合法 ra 100; std::cout a[0] \n; // 输出 100 return 0; }说明数组元素和解引用得到的对象都有存储位置是左值。示例 3成员访问非静态数据成员通常是左值#include iostream struct S { int m; }; int main() { S s{5}; s.m 6; // s.m 是左值 int rm s.m; // 可以取引用 rm 7; std::cout s.m \n; // 输出 7 return 0; }示例 4函数返回引用 - 返回的是左值#include iostream int get_static() { static int v 42; return v; // 返回引用表达式 get_static() 是左值 } int main() { get_static() 100; // 可以作为赋值目标因为是左值 std::cout get_static() \n; // 输出 100 int r get_static(); // 也可以绑定引用 r 200; std::cout get_static() \n; // 输出 200 return 0; }说明函数如果返回 T调用表达式是左值可以被赋值或绑定到 T。示例 5字符串字面量array与函数名也是左值#include iostream void foo() {} int main() { // 字符串字面量的类型是 const char[n]它在 C 中是有地址的可视为左值 const char* p abc; // 合法指向字面量的地址 std::cout p \n; // 函数名可以取地址函数名是左值 void (*fp)() foo; // 合法取得函数指针 fp(); return 0; }说明尽管字面量无名字但它们在程序中有静态存储位置C 中字符串字面量是左值/数组函数名也能取地址。右值定义右值 prvalue临时对象字面量或将被销毁的中间结果例如1stdstringhello)函数返回的临时对象。示例1标量字面量是 prvalue右值int main() { int x 1 2; // 1 和 2 是 prvalue表达式 12 也是 prvalue // int r 1; // 错误不能把 prvalue 绑定到非 const 左值引用 const int cr 1; // 合法临时绑定到 const 并延长寿命 }说明整数字面量等是 prvalue不能绑定到非 const 左值引用。示例2 临时对象构造表达式是 prvalue#include iostream struct S { S(const char* n):name(n){} std::string name; }; void f(S) { std::cout got rvalue\n; } int main() { f(S(tmp)); // S(tmp) 是 prvalue匹配 S 参数 }示例3 表达式中间的临时拼接时产生的中间结果std::string s std::string(Hello, ) std::string(world!);说明std::string(Hello, ) 和 std::string(world!) 都是 prvalue临时对象。左右值引用指的是什么引用的定义引用reference是已有对象的别名alias。引用本身不是一个独立对象语义上没有可独立地址的存储对引用的操作等价于对其所指对象的操作。C 中主要有三类引用左值引用T、常量左值引用const T和右值引用T。左右值引用左值引用T语义T 是绑定到左值的引用。它是被引用对象的别名能用来读写被引用对象若非 const。绑定规则只能绑定到左值命名对象或可以取地址的表达式。不能直接绑定到纯临时prvalue。示例int x 1; int r x; // r 绑定到 x修改 r 就修改 x r 5; // x 变为 5右值引用T语义T 是绑定到右值通常是临时对象的引用。引入它的主要目的是支持移动语义避免不必要拷贝和完美转发。绑定规则通常绑定到右值如字面量、临时对象、std::move(x) 的结果。可以把临时绑定到 T 变量上从而把临时的寿命延长到该引用的作用域结束。常见用途实现移动构造/移动赋值从右值“窃取”资源例如内存指针。示例std::string s hello; std::string r std::move(s); // r 绑定到 s 的右值视图通常用于转移所有权或者直接绑定临时std::string t std::string(tmp); // 临时寿命延长到 t 的作用域结束 t !; // 合法解释如果把一个临时prvalue直接用来初始化一个局部引用变量const T 或 T这个临时的寿命会延长到该引用变量离开作用域为止通常是该局部变量所在的 {} 块结束。但把临时传给函数参数、或把临时绑定到某个对象的成员引用并不会把临时延长到对象生存期。那类临时通常在“完整表达式”结束时销毁导致引用悬垂。补充悬垂引用的危险定义悬垂引用dangling reference是指引用或指针指向的对象已经被销毁或不再有效但代码仍然使用该引用导致未定义行为UB。为什么危险未定义行为可能表现为崩溃、随机数据、看似正常但潜伏 bug难以重现和调试。常见导致场景与示例每个例子后给出修正建议1.返回指向局部对象的引用最常见错误示例const std::string f() { std::string s tmp; return s; // 返回对局部对象的引用 - 悬垂 } auto r f(); // r 悬垂UB为什么局部变量 s 在函数返回时被销毁引用指向的内存不再有效。解决按值返回可借助移动/RVO或返回智能指针/容器保存对象。std::string f() { return std::string(tmp); } // 推荐2.初始化对象的引用成员时用临时错误示例struct S { const std::string r; }; S make() { return S{ std::string(hello) }; } // 成员 r 指向临时 - 悬垂为什么构造表达式结束后临时被销毁成员引用悬空。解决成员保存值或使用智能指针/引用包裹外部持有者。struct S { std::string r; }; // 把值拷贝/移动进来3.从临时取得内部指针如 c_str错误示例const char* p std::string(abc).c_str(); // 临时 std::string 在表达式结束时销毁 - p 悬垂解决把 string 存起来或直接使用 std::stringstd::string s std::string(abc); const char* p s.c_str(); // 安全4.把函数参数的临时绑定到返回的引用函数签名决定返回“引用”还是“值”。写成 const std::string 就是返回引用编译器不会把它自动变成按值返回。错误示例const std::string g(const std::string x) { return x; } auto r g(std::string(tmp)); // 临时在完整表达式结束后销毁r 悬垂解决按值返回或改为接受/返回合适的所有权模型。std::string g(const std::string x) { return x; } auto r g(std::string(tmp)); // 安全在表达式 auto r g(std::string(tmp)); 中auto 推导为 std::string去掉了引用所以会“从返回的引用拷贝/移动”初始化一个新的 std::string。这一步拷贝是在临时还没被销毁之前完成的因此这一具体写法通常是安全的。这边涉及到返回引用和返回值的区别返回引用const T返回的是被引用对象的别名调用者得到的是对原对象的引用没有拷贝。返回值T函数会返回一个独立的对象可以拷贝/移动/做 RVO。因为函数声明决定了返回什么const std::string 就是返回引用不会自动把被引用对象复制成返回值。当写 auto r g(std::string(tmp)); 时g 返回的是 const std::string引用类型但 auto 在没有 时会去掉引用auto 推导为 std::string编译器尝试从被引用的对象拷贝/移动初始化 r。但被引用对象临时已经在函数调用结束后被销毁拷贝就是从已销毁内存读数据——未定义行为UB。临时寿命延长与边界和悬垂关系直接用临时初始化局部引用变量const T 或 T时临时寿命延长到该引用离开作用域这是一个安全做法const std::string r std::string(tmp); // 临时延长到 r 离开作用域但把临时传给函数参数或初始化对象的成员通常不会延长到调用者作用域容易产生悬垂见上例。为什么需要移动语义明天更新