news 2026/8/10 18:57:06

右值和移动

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
右值和移动

值类别(value categories)

lvalue

通常可以放在等号左边的表达式, 左值

例子

  • 变量,函数或数据成员的名字
  • 返回左值引用的表达式,如++x, x = 1, cout << ’ '. x = 1 和 ++x返回的都是对x的int&. x++则返回的是int
  • 字符串字面量如 “hello world”

rvalue

通常只能放在等号右边的表达式, 右值

glvalue

generalized lvalue, 广义左值

xvalue

expiring lvalue, 将亡值

prvalue

pure rvalue, 纯右值

值类型(value type)

是与引用类型(reference type)相对而言,表明一个变量是代表实际数值,还是引用另一个数值

在C++里,所有的原生类型,枚举,结构,联合,类都代表值类型。只有引用(&)和指针( * ) 才是引用类型

在Java里,数字等原生类型是值类型,类则属于引用类型

在Python里,一切类型都是引用类型

引用类型(reference value)

非常左值引用

可以绑定非 const lvalue

常左值引用

可以绑定 lvalue, xvalue, prvalue

非常右值引用

可以绑定非 const xvalue, prvalue

常右值引用

可以绑定 xvalue, prvalue

生命周期

C++的规则是:一个临时对象(prvalue)会在包含这个临时对象的完整表达式估值完成后,按生成顺序的逆序被销毁,除非有生命周期延长发生

C++对临时对象有特殊的生命周期延长规则

  • 如果一个prvalue 被绑定到一个引用上,它的生命周期则会延长到跟这个引用变量一样长
    result&& r = process_shape( circle(), triangle());
  • 这条生命周期延长规则只对prvalue有效,而对xvalue无效

移动的意义

右值引用的目的是实现移动,而实现移动的意义是减少运行的开销

移动案例

string result = string("Hello, ") + name + ".";

C++11 以前的执行流程

  1. 调用构造函数 string(const char*),生成临时对象 1;"Hello, " 复制 1 次
  2. 调用 operator+(const string&, const string&),生成临时对象 2;"Hello, " 复制 2 次,name 复制 1 次
  3. 调用 operator+(const string&, const char*),生成对象 3;“Hello, " 复制 3 次,name 复制 2 次,”." 复制 1 次
  4. 假设返回值优化能够生效(最佳情况),对象 3 可以直接在 result 里构造完成
  5. 临时对象 2 析构,释放指向 string("Hello, ") + name 的内存
  6. 临时对象 1 析构,释放指向 string("Hello, ") 的内存

C++11 以后的执行流程

  1. 调用构造函数 string(const char*),生成临时对象 1;"Hello, " 复制 1 次
  2. 调用 operator+(string&&, const string&),直接在临时对象 1 上面执行追加操作,并把结果移动到临时对象 2;name 复制 1 次
  3. 调用 operator+(string&&, const char*),直接在临时对象 2 上面执行追加操作,并把结果移动到 result;“.” 复制 1 次
  4. 临时对象 2 析构,内容已经为空,不需要释放任何内存
  5. 临时对象 1 析构,内容已经为空,不需要释放任何内存

如何实现移动

除了拷贝构造函数外,增加移动构造函数

增加swap成员函数,实现与另一个对象的快速交换

在你的对象的名空间下,应当有一个全局的 swap 函数,调用成员函数 swap 来实现交换。支持这种用法会方便别人(包括你自己在将来)在其他对象里包含你的对象,并快速实现它们的 swap 函数

实现通用的 operator=

上面各个函数如果不抛异常的话,应当标为 noexcept。这对移动构造函数尤为重要

  • noexcept告诉编译器,函数不会抛异常,有利于编译器对程序做更多的优化
  • 以下情形鼓励用noexcept: 移动构造函数(move constructor)、移动分配函数(move assignment)、析构函数(destructor)

不要返回本地变量的引用

在C++编程中,如果在函数内部返回一个指向本地对象的引用,这是一个常见的错误

int& getLocalVariable() { int num = 42; return num; // 错误:返回了一个指向本地变量的引用 } int main() { int& ref = getLocalVariable(); // 错误:引用指向已被销毁的本地变量 cout << ref << endl; // 可能输出任意值,因为引用指向已被销毁的内存 }

引用坍缩(又称“引用折叠”)

当一个泛型函数模板中的类型参数与实际参数类型相同时,编译器会进行类型折叠,即将模板中的类型参数替换为实际参数类型,从而简化代码。

template <typename T> void process_shape(T&& shape) { puts("Processing shape"); } void foo() { process_shape(circle()); // 调用 process_shape(circle()) process_shape(triangle()); // 调用 process_shape(triangle()) }

当我们调用 process_shape(circle()) 和 process_shape(triangle()) 时,编译器会根据实际参数类型进行类型折叠,即 T 的推导结果分别是 circle 和 triangle

在有模板的代码中,当类型推导结果相同时,编译器会将模板中的类型参数替换为实际参数类型,从而简化代码。这个现象在实际编程中经常会遇到

完美转发

定义一个函数模板,该函数模板可以接收任意类型参数,然后将参数转发发给其他目标函数,且保证目标函数接受的参数其类型与传递给模板函数的类型相同

对于一个函数,如果形参是右值引用,但在函数体内,这个“右值引用”实际上是一个左值变量,然后函数内再有一个函数传入这个参数,那么就会调用对应的左值引用版本,而完美转发的意义就相当于做一次类型转换,让这个参数保持一开始传入时的左值右值类别

移动案例

#include <string> #include <ctime> #include <chrono> #include <iostream> void func1(std::string s) { } void func2(const std::string &s) { } void test2() { std::string a = "hello"; auto start = std::chrono::system_clock::now(); for (size_t i = 0; i < 20000000; i++) { func1(std::string("hello")); } auto end = std::chrono::system_clock::now(); std::chrono::duration<double> elapsed_seconds = end - start; std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n"; start = std::chrono::system_clock::now(); for (size_t i = 0; i < 20000000; i++) { func2(std::string("hello")); } end = std::chrono::system_clock::now(); elapsed_seconds = end - start; std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n"; } // 改进版本 void test3() { std::string a = "hello"; auto start = std::chrono::system_clock::now(); for (size_t i = 0; i < 20000000; i++) { func1(a); } auto end = std::chrono::system_clock::now(); std::chrono::duration<double> elapsed_seconds = end - start; std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n"; start = std::chrono::system_clock::now(); for (size_t i = 0; i < 20000000; i++) { func2(a); } end = std::chrono::system_clock::now(); elapsed_seconds = end - start; std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n"; } int main() { std::cout << "---- test2 测试时间 ----" << std::endl; test2(); std::cout << "---- test3 测试时间 ----" << std::endl; test3(); }

结果:

---- test2 测试时间 ---- elapsed time: 1.8719s elapsed time: 1.90104s ---- test3 测试时间 ---- elapsed time: 0.424229s elapsed time: 0.0873127s

问题: 在test2 case 中为什么func1比func2快?

func1(string(“func1”))时,传入的参数是一个右值.同时在函数调用中,临时对象的所有权会被转移给函数的参数s,因此会发生一次移动构造操作

func2(string(“func2”))时,传入的参数也是一个右值,虽然函数参数s是通过常量引用传递的,但临时对象string(“func2”)可以绑定到常量引用,因此不会发生拷贝操作。

为什么test3整体时间比test2快?

例如func1(a)和func2(a),传入的参数是一个左值

因为a是一个已命名的变量,它具有持久性,并且可以被多次使用. 原因在于使用已命名的变量作为参数时,可以直接进行拷贝构造,而不需要进行额外的临时对象的创建和销毁

参考资料

  • C++右值引用
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/10 18:55:33

Hiccup vs Hickory格式:哪种更适合你的HTML数据处理需求?

Hiccup vs Hickory格式&#xff1a;哪种更适合你的HTML数据处理需求&#xff1f; 【免费下载链接】hickory HTML as data 项目地址: https://gitcode.com/gh_mirrors/hic/hickory Hickory是一个将HTML解析为Clojure数据结构的强大工具&#xff0c;支持将HTML转换为Hiccu…

作者头像 李华
网站建设 2026/8/10 18:51:48

VRM与VRChat化身互转实战:开源工具打通虚拟形象数据壁垒

1. 项目概述&#xff1a;为什么我们需要VRM到VRChat的转换&#xff1f;如果你和我一样&#xff0c;是个喜欢折腾虚拟形象的创作者&#xff0c;那你肯定遇到过这个让人头疼的问题&#xff1a;花了好几天甚至几周时间&#xff0c;在Blender或者VRoid Studio里精心雕琢出一个满意的…

作者头像 李华