深入理解C++右值引用和移动语义:全面解析

目录

  • 背景
  • 什么是右值引用
  • 为什么需要右值引用
  • 移动构造
  • move的原理
  • move的应用场景
  • 右值引用注意事项
  • 总结

背景

C++11引入了右值引用,它也是C++11最重要的新特性之一。原因在于它解决了C++的一大历史遗留问题,即消除了很多场景下的不必要的额外开销。即使你的代码中并不直接使用右值引用,也可以通过标准库,间接地从这一特性中收益。为了更好地理解该特性带来的优化,以及帮助我们实现更高效的程序,我们有必要了解一下有关右值引用的意义。

什么是右值引用

右值

在引入右值的概念前,我们不妨先看看左值。一句话加以概括:左值就是等号左边的值;同理,右值也就是等号右边的值。举个例子:int a = 2;

这里的a是等号左边,可以通过取址符&来获取地址,所以是一个左值。而5在等号右边,无法通过取址符&来获取地址,所以只一个右值。

右值引用

左值引用是对于左值的引用或者叫别名。同样地,右值引用也就是对于右值引用。语法也很简单,就是在左值引用的语法之上在多加一个&,写成类型 &&右值引用名 = 右值;的形式即可,比如:

int &&a = 5;
a = 6;
string s1 = "hello";
string &&s2 = s1 + s1;
s2 += s1;

上述简单例子,展示了右值引用的基本用法。不过通常情况下,右值引用更多的是被用于处理函数参数。比如:

struct Student {
    Student(Student &&s);
};

为什么要使用右值引用

C++11之前,很多C++程序里存在大量的临时对象,又称无名对象。主要出现在如下场景:

  • 函数的返回值
  • 用户自定义类型经过一些计算后产生的临时对象
  • 值传递的形参

先说函数的返回值,最常见的类型就是某些返回用户自定义类型的时候,如果没有将其复制,就会产生临时对象,比如:

// 返回一个Student对象
Student func1(); 
...
// 调用了func1创建了一个Student对象,但是没有使用,于是编译器创建了一个临时对象来进行存储
func1(); 

然后是某些计算操作后产生的临时对象,比如:

// 编译器先计算c1 + c2的结果,并产生一个临时对象temp来存储结果,然后计算temp + c3的结果,然后将结果复制给result
Complex result = c1 + c2 + c3; 

还有值传递的方式的形参,例如:

// 值传递
void func(Student s);
...
Student stu;
// 这里相当于是做了一次复制操作	Student s(stu);
func(stu);

而且这些临时对象随着生命周期的结束,编译器还会调用一次析构函数。随着这些操作次数的增加,或者当临时变量是个很大的类型时,这无疑会极大提高程序的开销,从而降低程序的效率。

C++11之后,随着右值引用的出现,可以有效的解决这些问题。通过move移动构造移动赋值运算符函数来获得临时对象的所有权,从而避免拷贝带来的额外开销,提高程序效率

移动构造

我们都知道,由于C++11之前,如果没有手动声明,编译器会给一个用于自定义类型(包括classstruct)自动生成的4个函数,分别是构造函数,拷贝构造函数,赋值运算符重载函数和析构函数。虽然通过传引用的方式,可以避免对象的复制。但是还是没法避免上述的临时对象的复制。而移动语义成功的解决的这个问题。

C++11之后,编译器自动生成的函数中又新增了2个,它们就是移动构造移动赋值运算符重载函数,通过它们,我们可以很好地实现对用户自定义类型的移动操作。而移动的本质就是获取临时对象的所有权,而不是通过复制的方式来获得。直接看代码:

class Foo {
public:
    Foo(Foo &&rhs) : ptr_(rhs.ptr_) {
        rhs.ptr_ = nullptr;
    }
    
    Foo &operator=(Foo &&rhs) {
        if (*this != rhs) {
            ptr_ = rhs.ptr_;
            rhs.ptr_ = nullptr;
        }
        return *this;
    }
    
private:
    int *ptr_;
};

Foo类重载了移动构造函数和移动赋值运算重载函数,使得Foo获得了移动的能力,当我们在面对产生临时的对象的时候,编译器就会根据传入的参数是左值还是右值来选择调用拷贝还是移动。如果是右值,就调用移动构造或移动赋值运算符函数。当Foo是一个很大的对象时候,就会极大的降低开销,提高程序效率。

move的应用场景

通过上述例子,我们可以看到移动并不是说完全没有开销,甚至有的时候开销并不一定比拷贝低,具体还是要看临时对象的大小和类型决定,例如:

vector<vector<int>> func() {
    vector<vector<int>> res;
    for (...) {
        vector<int> temp;
        // 没必要直接传就可以了
        temp.emplace_back(move(5));
        // 可以,替代了拷贝操作,提高了效率
        res.emplace_back(move(res));
    }
    return res;
}

STL的大部分组件都支持移动语义,比如vectorstring等即可以通过move转换右值后调用移动构造函数进行移动操作来避免深拷贝。还有一些类是只允许移动,不允许拷贝,从而更让设计更符合逻辑,比如unique_ptr

move的原理

move函数的源码并不复杂:

template<class _Ty> 
inline _CONST_FUN typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT {
    return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));
}

我们可以一眼看到,move的实现其实就做了一件事,如果是左值,就通过static_cast将传进来的参数强转为右值并返回;如果是右值,甚至不用转换,直接返回。

右值移动的注意事项

  • 和左值移动一样,都需要直接初始化
  • 右值引用无法指向左值,除非使用move将其转成右值,否则编译报错
  • 当对象是基本类型的时候,没必要调用move,因为拷贝的开销可能还不如函数调用的开销大,尤其是在循环内的时候,需要仔细考虑
  • move并不会一定真的能移动,它只是将左值强转成右值,只有当该用户自定义类型重载了移动构造和移动运算符重载函数时才会进行移动操作
  • 现代编译在处理返回值的时候,通常都会进行返回值优化,尤其是标准库的组件,使用move来接收返回值反而会增加开销
  • 移动之后的对象就被析构,所以通常是对一些临时对象,或者不再使用的对象进行移动操作。如果还要继续使用该对象,就要使用拷贝而不是移动操作
  • 右值引用变量本身是个左值,如果想要右值引用指向右值引用,需要使用move转成右值
  • const 左值引用也可以指向右值,但是无法进行修改
#C++##后端##面试题##开发##程序员#
C++编程指南:从入门到精通 文章被收录于专栏

欢迎来到C++专栏!本专栏将免费为大家全面介绍C++编程语言的基础知识、面试问题、高级特性和实际应用技巧。 无论您是学生还是有一定经验的开发者,本专栏都将为您提供有价值的内容。我们将通过丰富的例子来帮助您更好地理解和掌握C++编程语言,让您的开发之路更加顺畅和高效。 感谢您的关注和支持,期待与您一起探索C++的奥秘!

全部评论
移动构造赋值函数那里少了=操作符吧
1
送花
回复
分享
发布于 2023-03-22 20:00 广东
大佬,一个地方没明白,就是Foo那里的移动构造和移动赋值运算符重载函数那里,为啥赋值完_ptr要把原来的delete掉呢,这样的话this._ptr不是成了悬空指针了吗
1
送花
回复
分享
发布于 2023-03-23 23:51 四川
秋招专场
校招火热招聘中
官网直投

相关推荐

HTTP(超文本传输协议)是一种用于在网络上进行通信的协议。&nbsp;它是用于在Web浏览器和Web服务器之间传输超文本文档的基础协议。HTTP的核心概念和工作原理如下:https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mg58Em&amp;uuid=b48bebe08e474db8b80b853b12bafd48客户端和服务器之间的请求/响应模型:客户端发送一个HTTP请求到服务器,服务器处理该请求并返回一个HTTP响应。请求方法:HTTP定义了一组请求方法,包括GET、POST、PUT、DELETE等。这些方法用于指定请求的目的以及对资源的处理方式。URL(统一资源定位符):HTTP使用URL来标识要请求或响应的资源。URL由协议、服务器地址、端口和资源路径组成。请求头和响应头:HTTP请求和响应都包含一组头部信息。请求头包含有关请求的元数据,如请求方法、请求主机等。响应头包含关于响应的元数据,如状态码、内容类型等。状态码:HTTP响应包含一个状态码,用于指示请求的处理结果。常见的状态码包括200(请求成功)、404(未找到)和500(服务器内部错误)等。内容编码:HTTP支持使用不同的编码格式来传输数据。常见的编码方式包括gzip、deflate和br等,用于压缩数据的大小以提高传输效率。Cookies和Sessions:HTTP通过使用Cookies或Sessions来维护状态。Cookies是服务器在客户端存储的小段信息,用于跟踪用户的状态,而Sessions是由服务器维护的与用户相关的数据。缓存:HTTP允许客户端和服务器使用缓存来减少重复请求和提高性能。客户端可以使用响应头中的Cache-Control字段来控制缓存策略。安全性:HTTP可以通过HTTPS(HTTP&nbsp;Secure)来提供安全的通信,使用TLS或SSL加密数据以防止窃听和篡改。
点赞 评论 收藏
转发
-&nbsp;项目拷打(自己项目问了个遍)-&nbsp;Linux如何查看项目日志?如何查看进程id?-&nbsp;Java相对于c、c++、python等有什么区别?-&nbsp;什么是继承、方法重载与重写、抽象类?-&nbsp;static、final关键字的作用?-&nbsp;Java8大基本类型?-&nbsp;ArrayList和LinkedList区别?List中如何查找重复元素?-&nbsp;HashMap的应用场景、扩容机制?-&nbsp;有用过枚举吗?try&nbsp;catch的使用?-&nbsp;String能被继承吗?常用的String方法?-&nbsp;StringBuilder和StringBuffer的区别?-&nbsp;synchronized的作用?ReentrantLock的区别?-&nbsp;什么是乐观锁?什么是ABA问题,如何解决的?-&nbsp;什么是死锁?如何解决死锁?-&nbsp;什么是线程,什么是进程,创建线程的方式?-&nbsp;线程的状态有哪些?如何从就绪到阻塞状态?-&nbsp;排好序的数组如何找元素?(二分查找)二分查找的思路?-&nbsp;快排和冒泡的时间复杂度?-&nbsp;设计递归要考虑的因素有哪些?-&nbsp;MySQL的版本以及存储引擎?-&nbsp;b+树和b树、hash索引有什么区别?-&nbsp;设计一个索引要考虑的因素有哪些?-&nbsp;联合索引了解吗?-&nbsp;讲讲TCP的四次挥手?服务器端先断开连接吗?-&nbsp;基于TCP的协议有哪些?基于UDP的协议有哪些?-&nbsp;http和https的区别以及他们的端口号?-&nbsp;http的请求方式都有哪些?响应状态码都有哪些?请求头有哪些?-&nbsp;你了解的中间件?(RabbitMQ&nbsp;ES&nbsp;Nginx)-&nbsp;反问
点赞 评论 收藏
转发
10 45 评论
分享
牛客网
牛客企业服务