c++类指针赋值表达式必须是可修改的左值_C++笔记 · 右值引用,移动语义,移动构造函数和移动赋值运算符
![](/assets/blank.gif)
右值引用
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++笔记 · 右值引用,移动语义,移动构造函数和移动赋值运算符相关推荐
- c++类指针赋值表达式必须是可修改的左值_C生万物,编程之本!(c语言基础笔记)
c语言入门 C语言一经出现就以其功能丰富.表达能力强.灵活方便.应用面广等特点迅速在全世界普及和推广.C语言不但执行效率高而且可移植性好,可以用来开发应用软件.驱动.操作系统等.C语言也是其它众多高级 ...
- c++类指针赋值表达式必须是可修改的左值_C++进阶教程系列:全面理解C++中的类...
原标题:C++进阶教程系列:全面理解C++中的类 关注Linux公社 最近刷了一些题,也面试了一些公司,把关于C++中关于类的一些概念总结了一下. 在这里也反思一下,面试前信心满满自以为什么都懂,毫无 ...
- c++类指针赋值表达式必须是可修改的左值_C++学习刷题8--复制构造函数和赋值运算符重载函数...
一.前言 本部分为C++语言刷题系列中的第8节,主要讲解这几个知识点:复制构造函数和赋值运算符重载函数.欢迎大家提出意见.指出错误或提供更好的题目! 二.知识点讲解 知识点1:复制构造函数 1.当依据 ...
- C++对类(或者结构体)中字符数组赋值时,出现表达式必须是可修改的左值的问题
最近自己遇到了这类问题,在csdn上找到了很多大神给的解答,非常到位 特别感谢这位: https://blog.csdn.net/JQ_AK47/article/details/53169799 问题 ...
- 将派生类指针赋值给基类的指针
除了可以将派生类对象赋值给基类对象(对象变量之间的赋值),还可以将派生类指针赋值给基类指针(对象指针之间的赋值).我们先来看一个多继承的例子,继承关系为: #include <iostream& ...
- 字符数组赋值报“表达式必须是可修改的左值”的错误
在C/C++程序中,main函数可以传递了两个参数(int argc, char *argv[]), 后面那个是字符数组,当我们接收直接用字符数组接收参数时会报"表达式必须是可修改的左值&q ...
- 表达式必须是可修改的左值怎么解决_如何解决代码腐败的味道
一. Duplicated Code(重复代码) 如果你在一个以上的地点看到相同的程序结构,设法将他们合而为一,程序会变得更好. 同一个类的两个函数含有相同的表达式,采用Extract Method( ...
- c 表达式必须是可修改的左值_C++中的左值,右值,左值引用,右值引用
童帅 2020-2-22 文中的"表达式"都是指赋值表达式 左值,右值,左值引用,右值引用 到底是什么 左值和右值 int a = 10; int b = 5; int c = a ...
- 基类指针和子类指针相互赋值
首先,给出基类animal和子类fish [cpp] view plaincopy print? //================================================= ...
- 转赋值表达式解析的流程
转自:http://www.cnblogs.com/nazhizq/p/6520072.html 上节说到表达式的解析问题,exprstate函数用于解析普通的赋值表达式.lua语言支持多变量赋值.本 ...
最新文章
- undefined reference to `__gxx_personality_v0' collect2: ld returned 1 exit status
- 油气储运工程中计算机的应用,中国石油大学(北京) 油气储运工程专业介绍
- java 下雪_如何用JAVA实现下雪场景
- NYOJ 370 波动序列
- windows下开发准备
- php坐标轴取整,PHP取整函数:ceil,floor,round,intval的区别详细解析
- docker swarm的应用----docker集群的构建
- C++网络编程快速入门(二):Linux下使用select演示简单服务端程序
- 第七届山东省Acm程序设计竞赛赛后总结
- Paypal 在线支付接口应用从零开始,第2节,[支付API原理及流程]
- ReportViewer教程(15)-矩阵报表-4
- Android学习笔记(27):日历视图Calendar
- java的vector_Java中 Vector的使用详解
- Python 2.x 即将终止支持,是时候和 Python 2 讲再见了
- 关于生成静态页--终极解决方案
- 企业网站电子邮件营销的优势与转化率分析
- HD2500显卡驱动linux,Intel HD Graphics 4000/2500集成显卡驱动
- 俄亥俄州立大学计算机科学转学成功,国内普二本学生成功转学美国俄亥俄州立大学...
- 基于词频的文件相似度
- Portainer添加远程Docker(Docker API)
热门文章
- 讲解对于Java中如何计算日期之间的天数知识
- Softether软件原理分析
- js 验证文本框为数字的正则表达式
- 【Docker】 命令速查
- 高校教材:计算机网络技术与应用,《网络技术与应用》计算机网络-教材-高等学校.pdf...
- java 文本框怎么属性绑定_如何将对象属性绑定到angular2中的文本框
- Unity UGUI优化:解决EventSystem耗时过长的问题 第一部分
- 此笔记只作为自身笔记,结构比较混乱,不建议参考,如有需要请访问其他文献,servlet的基础知识和使用
- 外包以小时计算金额的费用_全了!各大税种的计算公式,建议收藏!
- LeetCode(2) 两数相加递归解法,速度最快,内存消耗最小