news 2026/5/1 8:28:56

深入解析C++右值引用和移动语义:编写更快、更节省内存的代码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析C++右值引用和移动语义:编写更快、更节省内存的代码

一、左值和右值

C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高程序性能。

左值可以取地址,位于等号左边。 右值无法取地址,位于等号右边。

代码语言:javascript

AI代码解释

int a=10;

a可以通过&取地址,位于等号左边,所以a是左值。10位于等号右边且无法通过&取地址,所以10是右值。

代码语言:javascript

AI代码解释

struct A{ A(int a=0) { a_=a; } int a_; }; A a=A();

同样的,a可以通过 & 取地址,位于等号左边,所以a是左值。 A()是个临时值,没法通过 & 取地址,位于等号右边,所以A()是个右值。

可见,有地址的变量就是左值,没有地址的字面值、临时值就是右值。

二、左值引用

引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝。 定义:能指向左值,不能指向右值的就是左值引用。

代码语言:javascript

AI代码解释

#include <iostream> int main(int argc, char **argv) { int a = 10; int &ref_a = a; int &ref_b = 10; // 左值引用指向了右值,会编译失败 return 0; }

左值引用指向了右值时编译出现报错:

代码语言:javascript

AI代码解释

error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’

引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。

但是,const左值引用是可以指向右值的。

代码语言:javascript

AI代码解释

const int &ref_b = 10;// 编译通过

const左值引用不会修改指向值。因此可以指向右值,这也是为什么要使用 const & 作为函数参数的原因之一,如 std::vector 的 push_back 。

代码语言:javascript

AI代码解释

void push_back (const value_type& val);

如果没有 const , vec.push_back(5) 这样的代码就无法编译通过。

三、右值引用

再看下右值引用,右值引用的标志是 && ,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值。 右值引用的用途:可以修改右值。

代码语言:javascript

AI代码解释

#include <iostream> int main(int argc, char **argv) { int &&ref_a_right = 10;//编译通过 int a = 5; int &&ref_a_left = a;// 编译不过,右值引用不可以指向左值 ref_a_right = 16; // 右值引用的用途:可以修改右值 return 0; }

右值引用指向左值时编译报错:

代码语言:javascript

AI代码解释

error: cannot bind ‘int’ lvalue to ‘int&&’

四、左右值引用的本质

引用的本质就是指向目标地址来获得资源。

4.1、右值引用指向左值的办法

通过std::move()可以将右值引用指向左值。

代码语言:javascript

AI代码解释

#include <iostream> using namespace std; int main(int argc, char **argv) { int a = 5;// a是左值 int &ref_a_left = a; // 左值引用 int &&ref_a_right = std::move(a);//右值引用指向左值,编译通过 cout << "a=" << a << endl; cout << "ref_a_left=" << ref_a_left << endl; cout << "ref_a_right=" << ref_a_right << endl; return 0; }

在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。 执行结果:

代码语言:javascript

AI代码解释

a=5 ref_a_left=5 ref_a_right=5

std::move()是一个非常有迷惑性的函数: (1)不理解左右值概念的往往以为它能把一个变量里的内容移动到另一个变量; (2)事实上std::move()移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换: static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx)不会有性能提升。

同样的,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值。

代码语言:javascript

AI代码解释

#include <iostream> using namespace std; int main(int argc, char **argv) { int a = 5; int &ref_a_left = a; int &&ref_a_right = std::move(a);//编译通过 ref_a_right = 7; cout << "&a=" << &a << "\n&ref_a_left=" << &ref_a_left << "\n&ref_a_right=" << &ref_a_right << endl; cout << "a=" << a << endl; cout << "ref_a_left=" << ref_a_left << endl; cout << "ref_a_right=" << ref_a_right << endl; return 0; }

此时a等于多少呢? 运行结果如下,地址没有变,值改变了。

代码语言:javascript

AI代码解释

&a=0x7ffd7bd6ea54 &ref_a_left=0x7ffd7bd6ea54 &ref_a_right=0x7ffd7bd6ea54 a=7 ref_a_left=7 ref_a_right=7

4.2、左值引用、右值引用本身是左值还是右值?

被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。看下边代码:

代码语言:javascript

AI代码解释

// 形参是个右值引用 void change(int&& right_value) { right_value = 8; } int main() { int a = 5; // a是个左值 int &ref_a_left = a; // ref_a_left是个左值引用 int &&ref_a_right = std::move(a); // ref_a_right是个右值引用 //change(a); // 编译不过,a是左值,change参数要求右值 //change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值 //change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值 change(std::move(a)); // 编译通过 change(std::move(ref_a_right)); // 编译通过 change(std::move(ref_a_left)); // 编译通过 change(5); // 当然可以直接接右值,编译通过 cout << &a << ' '; cout << &ref_a_left << ' '; cout << &ref_a_right << endl; // 打印这三个左值的地址,都是一样的 }

std::move会返回一个右值引用 int && ,它是左值还是右值呢? 从表达式 int &&ref = std::move(a) 来看,右值引用 ref 指向的必须是右值,所以move返回的 int && 是个右值。 所以右值引用既可能是左值,又可能是右值吗? 确实如此,右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。 或者说:作为函数返回值的 && 是右值,直接声明出来的 && 是左值。 这同样也符合前面章节对左值,右值的判定方式:其实引用和普通变量是一样的, int &&ref = std::move(a) 和 int a = 5 没有什么区别,等号左边就是左值,右边就是右值。

从上述分析中得到如下结论:

  1. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。

代码语言:javascript

AI代码解释

void f(const int& n) { n += 1; // 编译失败,const左值引用不能修改指向变量 } void f2(int && n) { n += 1; // ok } int main() { f(5); f2(5); }

五、右值引用和std::move使用场景

std::move 只是类型转换工具,不会对性能有好处;右值引用在作为函数形参时更具灵活性。

5.1、右值引用优化性能,避免深拷贝

(1)浅拷贝重复释放。 对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:

代码语言:javascript

AI代码解释

#include <iostream> using namespace std; class A { public: A() :m_ptr(new int(0)) { cout << "constructor A" << endl; } ~A() { cout << "destructor A, m_ptr:" << m_ptr << endl; delete m_ptr; m_ptr = nullptr; } private: int *m_ptr; }; // 为了避免返回值优化,此函数故意这样写 A Get(bool flag) { A a; A b; cout << "ready return" << endl; if (flag) return a; return b; } int main(int argc, char **argv) { { A a = Get(false); } cout << "main finish" << endl; return 0; }

运行报错:

代码语言:javascript

AI代码解释

constructor A constructor A ready return destructor A, m_ptr:0xe4b2a0 destructor A, m_ptr:0xe4ae70 destructor A, m_ptr:0xe4b2a0 free(): double free detected in tcache 2 已放弃 (核心已转储)

(2)深拷贝构造函数。 在上面的代码中,默认构造函数是浅拷贝,main函数的 a 和Get函数的 b 会指向同一个指针 m_ptr,在析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码:

代码语言:javascript

AI代码解释

#include <iostream> using namespace std; class A { public: A() :m_ptr(new int(0)) { cout << "constructor A" << endl; } A(const A& a) :m_ptr(new int(*a.m_ptr)) { cout << "copy constructor A" << endl; } ~A() { cout << "destructor A, m_ptr:" << m_ptr << endl; delete m_ptr; m_ptr = nullptr; } private: int *m_ptr; }; // 为了避免返回值优化,此函数故意这样写 A Get(bool flag) { A a; A b; cout << "ready return" << endl; if (flag) return a; return b; } int main(int argc, char **argv) { { A a = Get(false); } cout << "main finish" << endl; return 0; }

执行结果:

代码语言:javascript

AI代码解释

constructor A constructor A ready return copy constructor A destructor A, m_ptr:0x184e2a0 destructor A, m_ptr:0x184de70 destructor A, m_ptr:0x184e2c0 main finish

(3)移动构造函数。 这样深拷贝构造函数就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?看下面的代码:

代码语言:javascript

AI代码解释

#include <iostream> using namespace std; class A { public: A() :m_ptr(new int(0)) { cout << "constructor A" << endl; } A(const A& a) :m_ptr(new int(*a.m_ptr)) { cout << "copy constructor A" << endl; } // 移动构造函数,可以浅拷贝 A(A&& a):m_ptr(a.m_ptr) { // 为防止a析构时delete data,提前置空其m_ptr a.m_ptr = nullptr; cout << "move constructor A" << endl; } ~A() { cout << "destructor A, m_ptr:" << m_ptr << endl; delete m_ptr; m_ptr = nullptr; } private: int *m_ptr; }; // 为了避免返回值优化,此函数故意这样写 A Get(bool flag) { A a; A b; cout << "ready return" << endl; if (flag) return a; return b; } int main(int argc, char **argv) { { A a = Get(false); } cout << "main finish" << endl; return 0; }

上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义),右值引用的一个重要目的是用来支持移动语义的。

执行结果:

代码语言:javascript

AI代码解释

constructor A constructor A ready return move constructor A destructor A, m_ptr:0 destructor A, m_ptr:0x1eb9e70 destructor A, m_ptr:0x1eba2a0 main finish

移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

5.2、移动(move)语义

move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要move语义起作用,核心在于需要对应类型的构造函数支持。

代码语言:javascript

AI代码解释

#include <iostream> #include <vector> #include <cstdio> #include <cstdlib> #include <string.h> using namespace std; class MyString { public: MyString() { m_data = NULL; m_len = 0; } MyString(const char *p) { m_len = strlen(p); copy_data(p); } MyString(const MyString& str) { m_len = str.m_len; copy_data(str.m_data); std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl; } MyString& operator=(const MyString& str) { if (this != &str) { m_len = str.m_len; copy_data(str.m_data); } std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl; return *this; } // 用c++11的右值引用来定义这两个函数 MyString(MyString&& str) { std::cout << "Move Constructor is called! source: " << str.m_data << std::endl; m_len = str.m_len; m_data = str.m_data; //避免了不必要的拷贝 str.m_len = 0; str.m_data = NULL; } MyString& operator=(MyString&& str) { std::cout << "Move Assignment is called! source: " << str.m_data << std::endl; if (this != &str) { m_len = str.m_len; m_data = str.m_data; //避免了不必要的拷贝 str.m_len = 0; str.m_data = NULL; } return *this; } virtual ~MyString() { if (m_data) delete m_data; } private: char * m_data; size_t m_len; void copy_data(const char *s) { m_data = new char[m_len + 1]; memcpy(m_data, s, m_len); m_data[m_len] = '\0'; } }; int main() { MyString a; a = MyString("Hello");// Move Assignment MyString b = a;// Copy Constructor MyString c = std::move(a);// Move Constructor is called! 将左值转为右值 std::vector<MyString> vec; vec.push_back(MyString("World")); // Move Constructor is called! return 0; }

执行结果:

代码语言:javascript

AI代码解释

Move Assignment is called! source: Hello Copy Constructor is called! source: Hello Move Constructor is called! source: Hello Move Constructor is called! source: World

有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。

5.3、forward 完美转发

forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。

现存在一个函数。

代码语言:javascript

AI代码解释

Template<class T> void func(T &&val);

根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。但要注意,引用以后,这个val值它本质上是一个左值! 看下面例子:

代码语言:javascript

AI代码解释

int &&a = 10; int &&b = a; //错误

a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这是不对的。

因此有了std::forward()完美转发,这种T &&val中的val是左值,但如果用std::forward (val),就会按照参数原来的类型转发。

代码语言:javascript

AI代码解释

int &&a = 10; int &&b = std::forward<int>(a);

示例:

代码语言:javascript

AI代码解释

#include <iostream> using namespace std; template <class T> void Print(T &t) { cout << "L" << t << endl; } template <class T> void Print(T &&t) { cout << "R" << t << endl; } template <class T> void func(T &&t) { Print(t); Print(std::move(t)); Print(std::forward<T>(t)); } int main() { cout << "-- func(1)" << endl; func(1); int x = 10; int y = 20; cout << "-- func(x)" << endl; func(x); // x本身是左值 cout << "-- func(std::forward<int>(y))" << endl; func(std::forward<int>(y)); cout << "-- func(std::forward<int&>(y))" << endl; func(std::forward<int&>(y)); return 0; }

执行结果:

代码语言:javascript

AI代码解释

-- func(1) L1 R1 R1 -- func(x) L10 R10 L10 -- func(std::forward<int>(y)) L20 R20 R20 -- func(std::forward<int&>(y)) L20 R20 L20

分析: func(1)由于1是右值,所以未定的引用类型T&&v被一个右值初始化后变成了一个右值引用,但是在func()函数体内部,调用PrintT(v) 时,v又变成了一个左值(因为在std::forward里它已经变成了一个具名的变量,所以它是一个左值),因此,示例测试结果第一个PrintT被调用,打印出“L1"调用PrintT(std::forward(v))时,由于std::forward会按参数原来的类型转发,因此,它还是一个右值(这里已经发生了类型推导,所以这里的T&&不是一个未定的引用类型,会调用void PrintT(T&&t)函 数打印 “R1”.调用PrintT(std::move(v))是将v变成一个右值(v本身也是右值),因此,它将输出”R1"func(x)未定的引用类型T&&v被一个左值初始化后变成了一个左值引用,因此,在调用PrintT(std::forward(v))时它会被转发到void PrintT(T&t)。

5.4、综合示例

代码语言:javascript

AI代码解释

#include "stdio.h" #include <iostream> #include <cstring> #include <vector> using namespace std; class A { public: int *m_ptr = NULL; // 增加初始化 int m_nSize = 0; A() :m_ptr(NULL), m_nSize(0) {} A(int *ptr, int nSize) { m_nSize = nSize; m_ptr = new int[nSize]; printf("A(int *ptr, int nSize) m_ptr:%p\n", m_ptr); if (m_ptr) { memcpy(m_ptr, ptr, sizeof(sizeof(int) * nSize)); } } A(const A&other)// 拷贝构造函数实现深拷贝 { m_nSize = other.m_nSize; if (other.m_ptr) { printf("A(const A &other) m_ptr:%p\n", m_ptr); if (m_ptr) delete[] m_ptr; printf("delete[] m_ptr\n"); m_ptr = new int[m_nSize]; memcpy(m_ptr, other.m_ptr, sizeof(sizeof(int) * m_nSize)); } else { if (m_ptr) delete[] m_ptr; m_ptr = NULL; } cout << "A(const int &i)" << endl; } // 右值应用构造函数 A(A &&other) { m_ptr = NULL; m_nSize = other.m_nSize; if (other.m_ptr) { m_ptr = move(other.m_ptr); other.m_ptr = NULL; } } ~A() { if (m_ptr) { delete[] m_ptr; m_ptr = NULL; } } void deleteptr() { if (m_ptr) { delete[] m_ptr; m_ptr = NULL; } } }; int main() { int arr[] = { 1, 2, 3 }; A a(arr, sizeof(arr) / sizeof(arr[0])); cout << "m_ptr in a Addr: 0x" << a.m_ptr << endl; A b(a); cout << "m_ptr in b Addr: 0x" << b.m_ptr << endl; b.deleteptr(); A c(std::forward<A>(a)); // 完美转换 cout << "m_ptr in c Addr: 0x" << c.m_ptr << endl; c.deleteptr(); vector<int> vect{1, 2, 3, 4, 5}; cout << "before move vect size: " << vect.size() << endl; vector<int> vect1 = move(vect); cout << "after move vect size: " << vect.size() << endl; cout << "new vect1 size: " << vect1.size() << endl; return 0; }

执行结果:

代码语言:javascript

AI代码解释

A(int *ptr, int nSize) m_ptr:0x219ce70 m_ptr in a Addr: 0x0x219ce70 A(const A &other) m_ptr:(nil) delete[] m_ptr A(const int &i) m_ptr in b Addr: 0x0x219d2a0 m_ptr in c Addr: 0x0x219ce70 before move vect size: 5 after move vect size: 0 new vect1 size: 5

5.5、emplace_back 减少内存拷贝和移动

对于STL,C++11后引入了emplace_back接口。 emplace_back是就地构造,不用构造后再次复制到容器中。因此效率更高。

www.dongchedi.com/article/7599372466490016280
www.dongchedi.com/article/7599372650099999257
www.dongchedi.com/article/7599372851132776984
www.dongchedi.com/article/7599372306620039742
www.dongchedi.com/article/7599372306619908670
www.dongchedi.com/article/7599370698540368409
www.dongchedi.com/article/7599370149677842969
www.dongchedi.com/article/7599370761858023961
www.dongchedi.com/article/7599370557410378302
www.dongchedi.com/article/7599369686161244697
www.dongchedi.com/article/7599370201301500441
www.dongchedi.com/article/7599370761857761817
www.dongchedi.com/article/7599371529427960344
www.dongchedi.com/article/7599369580343001625
www.dongchedi.com/article/7599335913373680190
www.dongchedi.com/article/7599334077720150590
www.dongchedi.com/article/7599334457882984984
www.dongchedi.com/article/7599335431117259326
www.dongchedi.com/article/7599334113941701182
www.dongchedi.com/article/7599333889857307198
www.dongchedi.com/article/7599332892518580761
www.dongchedi.com/article/7599334507862786584
www.dongchedi.com/article/7599332232163131928
www.dongchedi.com/article/7599332409993544254
www.dongchedi.com/article/7599331431303103000
www.dongchedi.com/article/7599331421261939224
www.dongchedi.com/article/7599331283516801561
www.dongchedi.com/article/7599330823195591193
www.dongchedi.com/article/7599236014061912601
www.dongchedi.com/article/7599236952864326168
www.dongchedi.com/article/7599233838816461337
www.dongchedi.com/article/7599234108371354174
www.dongchedi.com/article/7599232673466499646
www.dongchedi.com/article/7599231381528871449
www.dongchedi.com/article/7599230852278911550
www.dongchedi.com/article/7599230129096933950
www.dongchedi.com/article/7599224698819592766
www.dongchedi.com/article/7599377947404861977

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

零基础上手LivePortrait:人像动画工具全平台部署指南

零基础上手LivePortrait&#xff1a;人像动画工具全平台部署指南 【免费下载链接】LivePortrait Bring portraits to life! 项目地址: https://gitcode.com/GitHub_Trending/li/LivePortrait 想让静态肖像照片动起来吗&#xff1f;LivePortrait作为一款高效的人像动画解…

作者头像 李华
网站建设 2026/4/19 23:04:34

Czkawka:3步释放50GB存储空间的跨平台技术方案

Czkawka&#xff1a;3步释放50GB存储空间的跨平台技术方案 【免费下载链接】czkawka 一款跨平台的重复文件查找工具&#xff0c;可用于清理硬盘中的重复文件、相似图片、零字节文件等。它以高效、易用为特点&#xff0c;帮助用户释放存储空间。 项目地址: https://gitcode.co…

作者头像 李华
网站建设 2026/5/1 2:05:05

Boss Show Time:精准把握求职时机的智能助手

Boss Show Time&#xff1a;精准把握求职时机的智能助手 【免费下载链接】boss-show-time 展示boss直聘岗位的发布时间 项目地址: https://gitcode.com/GitHub_Trending/bo/boss-show-time 在竞争激烈的求职市场中&#xff0c;每一个机会都可能改变职业生涯。然而&#…

作者头像 李华
网站建设 2026/5/1 7:31:52

3DS模拟器Citra全攻略:在电脑上流畅体验3DS游戏的完整指南

3DS模拟器Citra全攻略&#xff1a;在电脑上流畅体验3DS游戏的完整指南 【免费下载链接】citra 项目地址: https://gitcode.com/GitHub_Trending/ci/citra 想在电脑上重温《精灵宝可梦》《塞尔达传说》等经典3DS游戏吗&#xff1f;Citra模拟器让这一愿望成为现实。作为一…

作者头像 李华
网站建设 2026/4/27 20:17:25

老款Mac升级指南:让旧设备焕发新生的系统更新方案

老款Mac升级指南&#xff1a;让旧设备焕发新生的系统更新方案 【免费下载链接】OpenCore-Legacy-Patcher 体验与之前一样的macOS 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 拥有一台老款Mac却想体验最新macOS系统&#xff1f;不必急着…

作者头像 李华