详解C++中移动语义的概念与使用

📅 2026/7/22 15:47:46
详解C++中移动语义的概念与使用
过去写 C/C 代码大家对数据做传递时都习惯先拷贝再赋值。比如把数据从 t1 复制到 t2复制完成后 t2 和 t1 的状态是一致的t1 状态没变。这里的状态指的是对象内部的非静态成员数据集合。在程序运行过程中复制过程既要分配空间又要拷贝内容对于空间和时间都是种损耗。复制操作无疑是一门很大的开销何况经常触发资源复制的时候。来看看普通的函数返回值到底有哪些开销12345678910111213std::string getString(){std::string s;// ...returns;}intmain(){std::string str getString();// ...}假设你的编译器还不支持 C 11那么在 main() 函数里调用 getString() 时需要在调用栈里分配临时对象用于复制 getString() 的返回值 s复制完成调用 s 的析构函数释放对象。然后再调用 std::string 类的复制赋值运算符函数将临时对象复制到 str同时调用临时对象的析构函数执行释放。那么有没有技巧可以实现上面示例代码同样的效果同时避免复制有的就是接下来重点介绍的移动和中国移动无关。相对于复制移动无须重新分配空间和拷贝内容只需把源对象的数据重新分配给目标对象即可。移动后目标对象状态与移动前的源对象状态一致但是移动后源对象状态被清空。实际上大部份的情况下数据仅仅需要移动即可拷贝复制显得多余。就像你从图书馆借书把自己手机的 SIM 卡拔出来再插到其它手机上去商店买东西你的钱从口袋移动到收银柜等等。那么是不是可以对所有的数据都执行移动答案是否定的。在现代 C 中只有右值可以被移动。左右值概念在 C 11 之前左右值的划分比较简单只有左值和右值两种。但是从 C 11 开始重新把值类别划分成了五种左值(lvalue, left value)将亡值(xvalue, expiring value)纯右值(prvalue, pure right value)泛左值(glvalue, generalized left value)右值(rvalue, right value)。不过后边的两种 glvalue 和 rvalue 是基于前面的三种组合而成。从集合概念来看glvalue 包含 lvalue 和 xvaluervalue 包含 xvalue 和 prvalue。左右值划分的依据是具名和可被移动。具名简单点理解就是寻址。可被移动允许对量的内部资源移动到其它位置并且保持量自身是有效的但是状态不确定。lvalue具名且不可移动xvalue具名且可移动prvalue不具名且可移动那么可以看到泛左值(glvalue)其实就是具名的量右值就是可移动的量。以往在往函数传参的时候经常有用到值引用的模式形式如下:1function(T obj)T 是类型obj 是参数。到了现代 C原来的值引用就变成了左值引用另外还出现了右值引用形式如下:1function(T obj)那么 C 11 是怎样实现移动操作的呢实现移动操作移动操作依赖于类内部特殊成员函数的执行但前提是该对象是可移动的。如果恰好对象是左值(lvalue)呢C 11 的标准库就提供了 std::move() 实现左右值转换操作。std::move() 用于将表达式从 lvalue(左值) 转换成 xvalue(将亡值)但不会对数值执行移动。当然使用强制类型转换也是可以达到同样目的。1std::move(obj);// 等价于 static_castT(obj);在 stack overflow 上看到对 std::move() 的一段描述与其说它是一个函数不如说它是编译器对表达式值评估的方式转换器。以往惯常使用 C 类定义时我们都知道有这么几个特殊的成员函数默认构造函数(default constructor)复制构造函数(copy constructor)复制赋值运算符函数(copy assignment operator)析构函数(destructor)来看看一个简单的例子123456789101112131415161718192021222324252627282930313233343536classMB// MemoryBlock{public:// 为下面代码演示简单起见// 在 public 定义成员属性size_tsize;char*buf;// 默认构造函数explicitMB(intsz 1024): size(sz), buf(newchar[sz]) {}// 析构函数~MB() {if(buf ! nullptr) {delete[] buf;}}// 复制构造函数MB(constMB obj): size(obj.size),buf(newchar[obj.size]) {memcpy(buf, obj.buf, size);}// 复制赋值运算符函数MB operator(constMB obj) {if(this! obj) {if(buf ! nullptr) {delete[] buf;}size obj.size;buf newchar[size];memcpy(buf, obj.buf, size);}return*this;}}为了支持移动操作从 C 11 开始类定义里新增了两个特殊成员函数:移动构造函数(move constructor)移动赋值运算符函数(move assignment operator)移动构造函数在构造新对象时如果传入的参数是右值引用对象就会调用移动构造函数创建对象。如果没有自定义移动构造函数那么编译器就会自动生成默认实现是遍历调用成员属性的移动构造函数并移动右值对象的成员属性数据到新对象。定义一般声明形式如下1T::T(C other);基于上面的简单例子1234567891011121314151617classMB// MemoryBlock{public:// ...// 移动构造函数MB(MB obj): size(0), buf(nullptr) {// 移动源对象数据到新对象size obj.size;buf obj.buf;// 清空源对象状态// 避免析构函数多次释放资源obj.size 0;obj.buf nullptr;}}可见移动构造函数的执行过程仅仅是简单赋值的过程不涉及拷贝资源的耗时操作自然执行效率大大提高。移动赋值运算符函数在调用赋值运算符时如果右边传入的参数是右值引用对象就会调用移动赋值运算符函数。同样如果没有自定义移动赋值运算符函数那么编译器也会自动生成默认实现是遍历调用成员属性的移动赋值运算符函数并移动成员属性的数据到左边参数对象。一般声明形式如下1T T::operator(C other);基于上面的简单例子123456789101112131415161718192021classMB// MemoryBlock{public:// ...// 移动赋值运算符函数MB MB::operator(MB obj) {if(this! obj) {if(buf ! nullptr) {delete[] buf;}// 移动源对象数据到新对象size obj.size;buf obj.buf;// 清空源对象状态// 避免析构函数多次释放资源obj.size 0;obj.buf nullptr;}return*this;}}移动赋值运算符函数的执行过程同样仅仅是简单赋值的过程执行效率明显远超复制操作。总结回顾文首的示例代码由于 C 11 加入了返回值优化 RVO(Return Value Optimization) 的特性所以代码无需变更即可获得效率提升。对于部分编译器而言比如 IBM Compiler、Visual C 2010 等已经提前具备返回值优化的支持。