当前位置: 首页 > news >正文

C++的左右值引用该怎么理解?注意点有什么?

基本概念

左值定义

左值(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):临时对象,字面量或将被销毁的中间结果(例如1,std::string("hello"),函数返回的临时对象。

示例1:标量字面量是 prvalue(右值)

int main() { int x = 1 + 2; // 1 和 2 是 prvalue,表达式 1+2 也是 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::string

std::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 离开作用域
  • 但把临时传给函数参数或初始化对象的成员通常不会延长到调用者作用域,容易产生悬垂(见上例)。

为什么需要移动语义?

明天更新!!!!!

http://www.jsqmd.com/news/502558/

相关文章:

  • ViT-B-16处理小尺寸图片的实战技巧(CIFAR-100案例解析)
  • 新手也能看懂的X站cms渗透实战:从广告设置到代码执行的完整漏洞链分析
  • xManager终极指南:解锁无广告音乐体验的免费应用管理器
  • 5个理由为什么Style-Bert-VITS2正在改变语音合成游戏规则
  • 中兴B860AV3.2-M_可刷移动高清6A_2+32G_灯绿色_带root_当贝桌面线刷固件包(内存显示正常)
  • 5大核心功能赋能Windows语音识别:FunASR社区版高效部署指南
  • 保姆级教程:基于Qwen3-Embedding-4B,快速部署可视化语义搜索系统
  • 90%的人降AI失败都栽在这一步:只降了标红段落没传全文
  • 斯坦福 CS336 从零构建大模型 (2025 春) - 第十一讲:缩放定律的工业界实践与底层机制 (Scaling Laws 2)
  • 当 JavaScript 试图做加法:一场混乱的“相亲”大会
  • 超级AI医院:以AI为核心大脑,重构全生命周期医疗生态
  • Linux虚拟显示器终极指南:3分钟将平板变免费扩展显示器
  • 斯坦福 CS336 从零构建大模型 (2025 春) - 第十六讲:强化学习与自对齐 (Alignment - RL 1)
  • MMWAVE SDK中的RF控制与数据路径详解:从理论到实践
  • 国内开发者福音:SwanLab替代Wandb实现具身智能训练参数可视化(附完整配置流程)
  • Abaqus与Isight联合仿真:从参数优化到自动化流程实战
  • Cogito-V1-Preview-Llama-3B实战:构建基于智能体(Agent)的自动化任务系统
  • FUTURE POLICE与AI Agent联动实战:构建自主语音任务处理智能体
  • SDL_ttf 3.0 迁移策略深度解析:构建系统适配与API兼容性挑战
  • Eclipse项目迁移到IntelliJ IDEA避坑指南:解决Web项目导入后无法运行的问题
  • 桌面级德州扑克GTO求解器:Desktop Postflop完全指南
  • VideoAgentTrek-ScreenFilter性能优化教程:C语言底层接口调用与内存管理
  • 光耦怎么区分1234脚
  • ZYNQ时钟设计避坑指南:MMCM/PLL选型与BUFG/BUFH布线技巧
  • 编程语言扩展的外部函数接口(FFI)概述
  • GASDocumentation项目实战指南:从核心模块到配置优化
  • 从零到一:基于STM32与W25Q64的OTA BootLoader实战解析
  • YOLO-v8.3新手入门:无需配置,一键开启目标检测开发
  • Linux下NDI Aurora磁导航API配置全攻略:从串口设置到手术导航系统集成
  • Prompt Engineering实战指南:7大核心技术从原理到实践