C++ 右值引用与移动构造函数

右值与右值引用

不能修改的值就是右值,右值一般为临时变量。常见的右值有字面常量值,返回右值的表达式。
所谓右值引用就是必须绑定到右值的引用。我们通过&&来获得右值引用。
右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。
因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。

void right_references()
{
    int i = 42;
    //r 为i的引用,左值引用
    int &r = i;
    //rr 不可以引用左值i,
    //因为其是右值引用
    //int &&rr = i;
    //表达式是一个右值
    //不能用左值引用
    //int &r2 = i*42;
    //可以将const引用绑定到右值上
    const int &r3 = i * 42;
    //将rr2绑定到乘法结果上
    int &&rr2 = i * 42;
}

上述代码右值引用只能捕获右值,左值引用只能捕获左值。const 引用可以捕获右值。
左值更长久,右值是短暂的。

void right_references()
{
    //右值引用捕获字面常量值
    int &&rr1 = 42;
    //右值引用不能捕获左值
    //因为rr1为左值,下面操作非法
    //int &&rr2 = rr1;
}

变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。

move操作

可以通过move操作将一个左值转化为右值引用。

void right_references()
{
    //右值引用捕获字面常量值
    int &&rr1 = 42;
    //通过move将左值转化为右值引用
    int &&rr3 = std::move(rr1);
}

move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。
我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。
在调用move之后,我们不能对移后源对象的值做任何假设。
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。

移动构造函数

在一些场景,我们不仅需要使用拷贝构造函数,还需要移动构造函数,移动构造函数减少拷贝带来的开销。
类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。
不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。
与拷贝构造函数一样,任何额外的参数都必须有默认实参。
完善之前我们实现的strvec类,实现一个移动构造函数

//移动构造函数
//声明noexcept就是不抛出异常
StrVec(StrVec &&src) noexcept : elements(src.elements), first_free(src.first_free), cap(src.cap)
{
    //将src的成员设置为空
    src.elements = src.first_free = src.cap = nullptr;
}

与拷贝构造函数不同,移动构造函数不分配任何新内存;它接管给定的StrVec中的内存。
在接管内存之后,它将给定对象中的指针都置为nullptr。
这样就完成了从给定对象的移动操作,此对象将继续存在。
最终,移后源对象会被销毁,意味着将在其上运行析构函数。

移动构造函数通常不会被标准库调用,因为移动构造函数可能抛出异常,导致移动过程中数据出现问题。
比如vector,我们调用push_back时,通常vector会利用拷贝构造函数完成数据的移动而不是利用移动构造函数。
这么做是为了保证移动的过程中出现异常后,清除新空间的数据,保留旧数据。

说白了就是怕vector调用push_back添加元素时扩容,将旧数据移动到新空间时崩溃造成旧数据丢失。
除非我们通过noexcept关键字通知标准库我们这个类的移动构造函数不会抛出异常,这样vector才会优先采用移动构造函数。
移动操作对移后源对象中留下的值没有任何要求。因此,我们的程序不应该依赖于移后源对象中的数据。

如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。
如果没定义析构函数,则编译器不会为该类定义合成的移动构造函数,因为使用移动构造函数的前提时编译器知道怎么回收对象。
当一个类既有移动构造函数又有拷贝构造函数的时候,编译器会根据条件选择最合适的。
完善之前的strvec类,实现移动赋值运算符

//移动赋值运算符
StrVec &StrVec::operator=(StrVec &&src)
{
    cout << "this is move operator = " << endl;
    if (this != &src)
    {
        //释放自己的空间操作
        this->free();
        //接管源对象资源
        this->elements = src.elements;
        this->first_free = src.first_free;
        this->cap = src.cap;
        //将源对象成员赋值为空
        src.elements = src.first_free = src.cap = nullptr;
    }

    return *this;
}

如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的。
拷贝赋值运算符和移动赋值运算符的情况类似。
建议定义拷贝构造函数,拷贝赋值运算符以及析构函数后,在定义移动构造和移动赋值,交给编译器自动调用合理的赋值和构造函数。
之前实现过消息和文件夹的两个类,现在为Message类增加移动构造函数和移动赋值函数,在此之前先实现工具函数帮助我们将Message类的文件夹信息
移动给另一个Message类对象

 //将m的Folders交接给本类对象
    //并且实现Folders和本类对象的关联
    //接触Folders和m的关联
void Message::move_Folders(Message *m)
{
    //将m的Folders交接给本对象
    this->folders = m->folders;
    //将本对象和folders关联
    for (auto f : folders)
    {
        //解除folders和m的关联
        f->remMsg(*m);
        //添加本对象和folders的关联
        f->addMsg(*this);
    }
    //清除m的folders
    m->folders.clear();
}

//利用move_Folders函数实现移动构造函数
Message::Message(Message &&m) : contents(std::move(m.contents))
{
    move_Folders(&m);
}

Message &Message::operator=(Message &&m)
{
    if (this != &m)
    {
        remove_from_Folders();
        contents = std::move(m.contents);
        move_Folders(&m);
    }
    return *this;
}

void Message::printMsg()
{
    cout << "contents is " << contents << endl;ss
}

接下来在主函数中调用如下函数。

auto msg4 = new Message("msg4");
msg4->printMsg();
auto msg3 = new Message("msg3");
msg3->printMsg();
*msg4 = std::move(*msg3);
msg4->printMsg();
delete msg4;
delete msg3;

可以看到在调用move之后msg4输出的内容变为msg3,我们的移动操作是成功的。

总结
本文通过allocator实现了一个类似于vector的类,管理string变量。演示了拷贝构造,拷贝赋值要注意的事项,同时演示了如何手动开辟内存并管理内存空间。
源码链接
https://gitee.com/secondtonone1/cpplearn

热门评论

热门文章

  1. vscode搭建windows C++开发环境

    喜欢(596) 浏览(84443)
  2. 聊天项目(28) 分布式服务通知好友申请

    喜欢(507) 浏览(6140)
  3. 使用hexo搭建个人博客

    喜欢(533) 浏览(12028)
  4. Linux环境搭建和编码

    喜欢(594) 浏览(13699)
  5. Qt环境搭建

    喜欢(517) 浏览(25017)

最新评论

  1. Qt 对话框 Spade2077:QDialog w(); //这里是不是不需要带括号
  2. visual studio配置boost库 一giao里我离giaogiao:请问是修改成这样吗:.\b2.exe toolset=MinGW
  3. slice介绍和使用 恋恋风辰:切片作为引用类型极大的提高了数据传递的效率和性能,但也要注意切片的浅拷贝隐患,算是一把双刃剑,这世间的常态就是在两极之间寻求一种稳定。
  4. 创建项目和编译 secondtonone1:谢谢支持
  5. 网络编程学习方法和图书推荐 Corleone:啥程度可以找工作
  6. 再谈单例模式 secondtonone1:是的,C++11以后返回局部static变量对象能保证线程安全了。
  7. 处理网络粘包问题 zyouth: //消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里 if (bytes_transferred < data_len) { memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred); _recv_msg_node->_cur_len += bytes_transferred; ::memset(_data, 0, MAX_LENGTH); _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self)); //头部处理完成 _b_head_parse = true; return; } 把_b_head_parse = true;放在_socket.async_read_some前面是不是更好
  8. 聊天项目(7) visualstudio配置grpc diablorrr:cmake文件得改一下 find_package(Boost REQUIRED COMPONENTS system filesystem),要加上filesystem。在target_link_libraries中也同样加上
  9. interface应用 secondtonone1:interface是万能类型,但是使用时要转换为实际类型来使用。interface丰富了go的多态特性,也降低了传统面向对象语言的耦合性。
  10. 堆排序 secondtonone1:堆排序非常实用,定时器就是这个原理制作的。
  11. C++ 线程安全的单例模式演变 183******95:单例模式的析构函数何时运行呢? 实际测试里:无论单例模式的析构函数为私有或公有,使用智能指针和辅助回收类,两种方法都无法在main()结束前调用单例的析构函数。
  12. 聊天项目(9) redis服务搭建 pro_lin:redis线程池的析构函数,除了pop出队列,还要free掉redis连接把
  13. 基于锁实现线程安全队列和栈容器 secondtonone1:我是博主,你认真学习的样子的很可爱,哈哈,我画的是链表由空变成1个的情况。其余情况和你思考的类似,只不过我用了一个无效节点表示tail的指向,最初head和tail指向的都是这个节点。
  14. boost::asio之socket的创建和连接 项空月:发现一些错别字 :每隔vector存储  是不是是每个. asio::mutable_buffers_1 o或者    是不是多打了个o
  15. 无锁并发队列 TenThousandOne:_head  和 _tail  替换为原子变量。那里pop的逻辑,val = _data[h] 可以移到循环外面吗
  16. 答疑汇总(thread,async源码分析) Yagus:如果引用计数为0,则会执行 future 的析构进而等待任务执行完成,那么看到的输出将是 这边应该不对吧,std::future析构只在这三种情况都满足的时候才回block: 1.共享状态是std::async 创造的(类型是_Task_async_state) 2.共享状态没有ready 3.这个future是共享状态的最后一个引用 这边共享状态类型是“_Package_state”,引用计数即使为0也不应该block啊
  17. 聊天项目(13) 重置密码功能 Doraemon:万一一个用户多个邮箱呢 有可能的
  18. 类和对象 陈宇航:支持!!!!
  19. 构造函数 secondtonone1:构造函数是类的基础知识,要着重掌握
  20. Qt MVC结构之QItemDelegate介绍 胡歌-此生不换:gpt, google
  21. 聊天项目(15) 客户端实现TCP管理者 lkx:已经在&QTcpSocket::readyRead 回调函数中做了处理了的。
  22. string类 WangQi888888:确实错了,应该是!isspace(sind[index]). 否则不进入循环,还是原来的字符串“some string”
  23. protobuf配置和使用 熊二:你可以把dll放到系统目录,也可以配置环境变量,还能把dll丢到lib里
  24. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  25. 利用栅栏实现同步 Dzher:作者你好!我觉得 std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); 这个例子中的assert fail并不会发生,原子变量设定了非relaxed内存序后一个线程的原子变量被写入,那么之后的读取一定会被同步的,c和d线程中只可能同时发生一个z++未执行的情况,最终z不是1就是2了,我测试了很多次都没有assert,请问我这个观点有什么错误,谢谢!
  26. 面试题汇总(一) secondtonone1:看到网络上经常提问的go的问题,做了一下汇总,结合自己的经验给出的答案,如有纰漏,望指正批评。
  27. 利用C11模拟伪闭包实现连接的安全回收 搁浅:看chatgpt说 直接传递 shared_from_this() 更安全 提问: socket_.async_read_some(boost::asio::buffer(data_, BUFFSIZE), // 接收客户端发生来的数据 std::bind(&Session::handle_read, this, std::placeholders::_1, std::placeholders::_2, shared_from_this())); socket_.async_read_some(boost::asio::buffer(data_, BUFFSIZE), std::bind(&Session::handle_read, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); 这两种方式有区别吗? 回答 : 第一种方式:this 是裸指针,可能会导致生命周期问题,虽然 shared_from_this() 提供了一定的保护,但 this 依然存在风险。 第二种方式:完全使用 shared_ptr 来管理生命周期,更加安全。 通常,第二种方式更推荐使用,因为它可以确保在异步操作完成之前,Session 对象的生命周期得到完全管理,避免使用裸指针的潜在风险。
  28. C++ 并发三剑客future, promise和async Yunfei:大佬您好,如果这个线程池中加入的异步任务的形参如果有右值引用,这个commit中的返回类型推导和bind绑定就会出现问题,请问实际工程中,是不是不会用到这种任务,如果用到了,应该怎么解决?

个人公众号

个人微信