右值引用

C++的引用允许你为已经存在的对象创建一个新的名字。对新引用所做的访问和修改操作,都会影响它的原型。这叫左值引用。

左值引用只能被绑定在左值上,所以不能这样写:

int& i=42;  // 错误

42是一个右值,所以无法绑定,但也有例外,比如可以使用下面的方式将一个右值绑定到一个const左值引用上:

int const& i = 42;

这算是钻了标准的一个空子。因为const引用创建一个对象,再将左值引用绑定上去。 其允许隐式转换,所以可以这样写:

void print(std::string const& s);
print("hello");  // 创建了临时std::string对象

C++11标准添加了右值引用(rvalue reference),这种引用只能绑定右值,不能绑定左值,它使用两个&&来声明:

int&& i=42;
int j=42;
int&& k=j;  // 编译失败

由于这个符号区别于左值引用的符号,所以可以与左值引用一起用于函数重载。

移动语义

开发中经常使用左值引用作为函数参数,避免拷贝以提高性能。通常还会加上const修饰符,以避免函数内部对源对象的修改。比如:

// 函数1,接受左值引用
void process_copy(const std::vector<int>& vec_) {// do_somethingstd::vector<int> vec(vec_); //  不能修改左值,所以要拷贝vectorvec.push_back(42);
}// 函数2,接受右值引用
void process_copy(std::vector<int> && vec) {vec.push_back(42); // 直接修改右值
}int main(){std::vector<int> data;process_copy(data); // 调用函数1process_copy(std::vector<int>()); // 调用函数2,临时对象作为右值,函数内部无需拷贝,降低开销return 0;
}

一般情况下,我们只编写函数1,就已经能满足需求了,它能接受左值引用,也能接受右值引用(传参时右值引用会被转化为const左值引用)。

但是函数内部无法区分调用者传递的是左值还是右值,无论如何都是拷贝后再进行修改等操作。如果传递的是右值,虽然程序可以正确运行,但进行了一次毫无必要的拷贝,如果能直接修改右值,就能节省开销。

于是可以编写第二个函数,它接受右值引用,与函数1形成重载。在传递右值引用时,就会自动调用函数2,这样无需拷贝,可以提高性能。

这称之为移动语义,将属于main函数块的临时对象的所有权,交给process_copy函数中。

为什么使用左值引用不能叫做移动呢?

虽然main函数中的对象可以通过data的左值引用将对象本身(而不是拷贝的副本)传递给process_copy函数,但process_copy并不真正拥有对象的所有权,因为调用者可能在之后还会使用该对象,所以process_copy不能修改该对象。

而使用右值引用传递对象,则代表调用者(有意或无意的)保证不会在之后继续使用该对象,所以process_copy可以任意修改该右值。

移动构造函数

减少不必要的拷贝

许多情况下,类的拷贝构造函数会被隐式调用,比如调用函数时的值传递

class Person {private:int* data;public:Person() : data(new int[1000000]) {}~Person() { delete [] data; }// 拷贝构造函数,需要拷贝动态资源Person(const Person& other) : data(new int[1000000]) {std::copy(other.data,other.data+1000000,data);}
};void func(Person p){// do_something
} int main(){Person p;func(p); // 调用func时,会调用Person的拷贝构造函数来创建实参return 0;
}

调用func时使用Person的拷贝构造函数创建一个实参。

考虑这样一种情况,使用一个临时的Person对象,作为函数参数传递给func

int main(){func(Person()); // 先创建临时的Person对象,再调用Person的拷贝构造函数来创建实参return 0;
}

这里创建的临时对象是一个右值,它作为func函数的参数,但func函数还是忠实的拷贝了它,因为拷贝构造函数的const Person&参数可以接收右值。

Person内部包含一个很大的动态分配的数组,那么拷贝它的开销会非常大,显然拷贝一个临时对象(连带着拷贝其中的动态资源)是毫无必要的,所以我们应该优化它。

如果能直接使用Person临时对象内部的动态资源(不是直接使用临时对象本身),而不进行完整的拷贝(不是完全不拷贝),就会节省非常多的开销。 并且不会影响程序的正确性(反正调用者之后也不会用它了,因为是临时对象)。

于是可以编写一个移动构造函数,与拷贝构造函数实现重载:

class Person {private:int* data;public:Person() : data(new int[1000000]){}~Person() { delete [] data; }// 拷贝构造函数,需要拷贝动态资源Person(const Person& other) : data(new int[1000000]) {std::copy(other.data,other.data+1000000,data);}// 移动构造函数,无需拷贝动态资源Person(Person&& other) : data(other.data) {other.data=nullptr; // 源对象的指针应该置空,以免源对象析构时影响本对象}
};void func(Person p){// do_something
} int main(){Person p;func(p); // 调用Person的拷贝构造函数来创建实参func(Person()); // 调用Person的移动构造函数来创建实参return 0;
}

移动构造函数接受右值引用,直接获取老数据。

移动构造函数当然产生了新的Person对象,这点与拷贝构造函数无区别。但是并没有拷贝动态分配的资源,而只是将源对象的数据移到新对象中(本例中,仅仅拷贝一个指针)。

很重要的一点,将动态数据移动到新对象中后,应该解除与源对象的关系。

在这个例子中,就是把源对象的指针置为nullptr,不然源对象析构时,会将数据释放,影响到本对象。

还可以对非临时对象调用移动构造函数。

int main(){Person p1;func(std::move(p1)); // 调用移动构造函数,应保证之后不再使用p2Person p2;func(static_cast<X&&>(p2)); // 调用移动构造函数后,应保证之后不再使用p2return 0;
}

std::move()可以提取对象的右值,而static_cast<X&&>将对应变量转换为右值。

这样显示转换为右值之后,应保证之后不再使用该对象。

管理不可拷贝的资源

有些类型的构造函数只支持移动构造函数,而不支持拷贝构造函数。

例如,智能指针std::unique_ptr<>的非空实例中,只允许这个指针指向其对象,所以拷贝函数在这里就不能用了(如果使用拷贝函数,就会有两个std::unique_ptr<>指向该对象,不满足std::unique_ptr<>定义)。

但有时我们希望可以转移对象的所有权,所以就需要实现移动构造函数。

#include <iostream>class Person {private:int* data;public:Person() : data(new int[1000000]){}~Person() { delete [] data; }// 删除拷贝构造函数Person(const Person& other) = delete;// 移动构造函数,无需拷贝动态资源Person(Person&& other) : data(other.data) {other.data=nullptr; // 源对象的指针应该置空,以免源对象析构时影响本对象}
};void func(Person p){// do_something
}int main(){Person p;func(p); // 错误,不可拷贝func(std::move(p)); // 正确,调用Person的移动构造函数来创建实参return 0;
}

这样,即使在没有拷贝构造函数的情况下,也能移动资源。

移动赋值运算符

移动赋值运算符和移动构造函数行为很接近,也很好理解。

#include <iostream>
using namespace std;class Person
{private:int age;string name;int* data;public:Person() : data(new int[1000000]){}~Person() { delete [] data; }// 拷贝构造函数Person(const Person& p) :age(p.age),name(p.name),data(new int[1000000]){std::copy(p.data, p.data+1000000, data);cout << "Copy Constructor" << endl;}// 拷贝赋值运算符Person& operator=(const Person& p){this->age = p.age;this->name = p.name;this->data = new int[1000000];std::copy(p.data, p.data+1000000, data);cout << "Copy Assign" << endl;return *this;}// 移动构造函数Person(Person &&p) :age(std::move(p.age)),name(std::move(p.name)),data(p.data){p.data=nullptr; // 源对象的指针应该置空,以免源对象析构时影响本对象cout << "Move Constructor" << endl;}// 移动赋值运算符Person& operator=(Person &&p){this->age = std::move(p.age);this->name = std::move(p.name);this->data = p.data;p.data=nullptr;cout << "Move Assign" << endl;return *this;}
};int main(){Person p1;Person p2 = p1; // 拷贝构造函数Person p3,p4;p3 = p4; // 拷贝赋值运算符Person p5;Person p6 = std::move(p5); // 移动构造函数Person p7,p8;p7 = std::move(p8); // 移动赋值运算符return 0;
}

c++类指针赋值表达式必须是可修改的左值_C++笔记 · 右值引用,移动语义,移动构造函数和移动赋值运算符相关推荐

  1. c++类指针赋值表达式必须是可修改的左值_C生万物,编程之本!(c语言基础笔记)

    c语言入门 C语言一经出现就以其功能丰富.表达能力强.灵活方便.应用面广等特点迅速在全世界普及和推广.C语言不但执行效率高而且可移植性好,可以用来开发应用软件.驱动.操作系统等.C语言也是其它众多高级 ...

  2. c++类指针赋值表达式必须是可修改的左值_C++进阶教程系列:全面理解C++中的类...

    原标题:C++进阶教程系列:全面理解C++中的类 关注Linux公社 最近刷了一些题,也面试了一些公司,把关于C++中关于类的一些概念总结了一下. 在这里也反思一下,面试前信心满满自以为什么都懂,毫无 ...

  3. c++类指针赋值表达式必须是可修改的左值_C++学习刷题8--复制构造函数和赋值运算符重载函数...

    一.前言 本部分为C++语言刷题系列中的第8节,主要讲解这几个知识点:复制构造函数和赋值运算符重载函数.欢迎大家提出意见.指出错误或提供更好的题目! 二.知识点讲解 知识点1:复制构造函数 1.当依据 ...

  4. C++对类(或者结构体)中字符数组赋值时,出现表达式必须是可修改的左值的问题

    最近自己遇到了这类问题,在csdn上找到了很多大神给的解答,非常到位 特别感谢这位: https://blog.csdn.net/JQ_AK47/article/details/53169799 问题 ...

  5. 将派生类指针赋值给基类的指针

    除了可以将派生类对象赋值给基类对象(对象变量之间的赋值),还可以将派生类指针赋值给基类指针(对象指针之间的赋值).我们先来看一个多继承的例子,继承关系为: #include <iostream& ...

  6. 字符数组赋值报“表达式必须是可修改的左值”的错误

    在C/C++程序中,main函数可以传递了两个参数(int argc, char *argv[]), 后面那个是字符数组,当我们接收直接用字符数组接收参数时会报"表达式必须是可修改的左值&q ...

  7. 表达式必须是可修改的左值怎么解决_如何解决代码腐败的味道

    一. Duplicated Code(重复代码) 如果你在一个以上的地点看到相同的程序结构,设法将他们合而为一,程序会变得更好. 同一个类的两个函数含有相同的表达式,采用Extract Method( ...

  8. c 表达式必须是可修改的左值_C++中的左值,右值,左值引用,右值引用

    童帅 2020-2-22 文中的"表达式"都是指赋值表达式 左值,右值,左值引用,右值引用 到底是什么 左值和右值 int a = 10; int b = 5; int c = a ...

  9. 基类指针和子类指针相互赋值

    首先,给出基类animal和子类fish [cpp] view plaincopy print? //================================================= ...

  10. 转赋值表达式解析的流程

    转自:http://www.cnblogs.com/nazhizq/p/6520072.html 上节说到表达式的解析问题,exprstate函数用于解析普通的赋值表达式.lua语言支持多变量赋值.本 ...

最新文章

  1. swagger2 集成无效_Springboot2 集成Swagger2,解决配置完成后不显示的坑
  2. 算法提高课-搜索-Flood fill算法-AcWing 1106. 山峰和山谷:flood fill、bfs
  3. Oracle-UNDO表空间解读
  4. python教程:循环(while和for)
  5. php软件开发--公众平台
  6. java文件选择器_java中文件选择器JFileChooser的用法
  7. python tkinter进度条_在python tkinter中Canvas实现进度条显示的方法
  8. 这五个有用的 CSS 属性完全被我忽视了
  9. c语言模拟计算机程序阶乘,辽宁省计算机二级(C语言)模拟试卷B(无答案).doc
  10. mysql 一张表的数据插入另一张表的sql语句
  11. sybase 事务 超时返回_分布式事务设计与实践-消息最终一致性
  12. ios lottie动画_在iOS中使用Lottie动画
  13. w7计算机超级管理员权限,win7系统取得管理员最高权限的操作方法
  14. 阿里云MaxComputer SQL学习之DDL
  15. 关于RGV下料的智能动态调度
  16. Linux网络容灾,关于异地容灾的感触
  17. 提示用户更改计算机密码,验证你的Microsoft账户 温馨提示:在个人电脑上更改微软账户密码...
  18. 免费的 AI 动作捕捉工具 #Rokoko Video
  19. 2020年中国电机驱动芯片行业产业链、市场规模、产量及发展趋势分析「图」
  20. ODBC数据源里没有ORACLE的驱动程序

热门文章

  1. Silverlight实用窍门系列:43.Silverlight从ListBox拖拽图标到另一ListBox
  2. 微软云计算解决方案与下一代数据中心
  3. 关于#if NET1的一点小得
  4. 成大事必备9种能力.9种手段.9种心态
  5. Linux中权威域名服务器,请在(7)~(9)处填写恰当的内容。在Linux系统中配置域名服务器,该服务..._考试资料网...
  6. mysql使用多个索引_mysql索引合并:一条sql可以使用多个索引
  7. echart markline 设置不同颜色_小学信息技术《设置文档格式》教案
  8. C#三层架构通用数据库访问类SQLHerper总结
  9. python中基础知识_Python中的一些基础知识
  10. 解决SpringBoot集成Redis出现RedisConnectionException: Unable to connect to 192.168.64.100:6379