拷贝赋值重载中的任意一个
发布时间:2025-06-24 18:33:56 作者:北方职教升学中心 阅读量:120
也不是说所有的局部对象传值返回都要走移动构造,只有需要深拷贝的对象移动构造才有意义,像日期类这种对象拷贝构造和移动构造没有区别。匿名对象等,而这些值都具有常性,如果不用const
修饰就存在权限放大的问题。所以早期右值引用没出来之前右值也可以通过左值引用给取别名。比如传局部对象:
yjz::string to_string(intvalue){boolflag =true;if(value <0){flag =false;value =0-value;}yjz::string str;while(value >0){intx =value %10;value /=10;str +=('0'+x);}if(flag ==false){str +='-';}std::reverse(str.begin(),str.end());returnstr;}
这里的str是一个局部对象,出了作用域就销毁,传引用会造成野引用,所以只能传值,返回值传值会先拷贝构造一个临时对象,再用临时对象拷贝构造目标对象。
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝(值拷贝),自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
移动赋值重载和移动构造基本类似
但是只要破坏了其中的一个条件就不会生成默认的移动构造,比如实现了析构函数:
可能有同学觉得移动构造和移动赋值重载这两个默认成员函数的自动生成条件有点苛刻,其实不然。
🚀个人主页:@小羊 🚀所属专栏:C++ 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~
目录
- 前言
- 一、
如果某个类需要显示写析构,就说明有资源释放,那就需要显示的写拷贝构造和复制重载,那就需要显示的写移动构造和移动赋值,它们是一体化的。
- 左值:一个表示数据的表达式,一般情况可以赋值(如果被const修饰就不能修改),左值可以出现在“=”的左边或右边,最关键的特性是左值可以取地址
- 右值:一个表示数据的表达式,一般不能修改,通常是字面常量、既然右值引用现在被广泛使用了,就说明移动构造还是有重要意义的。
- final:禁止父类被继承,禁止虚函数被重写
- override:检查虚函数是否重写
这两个关键字在《多态》中已经有详细介绍,这里就不再赘述。
intmain(){std::list<yjz::string>lt;yjz::string s1("111111");lt.push_back(s1);lt.push_back(yjz::string("222222"));lt.push_back("333333");lt.push_back(move(s1));return0;}
有了右值引用,我们就可以很方便的插入一些匿名对象,这样写不仅简单还会少一次拷贝构造。
这里还是把左值str隐式作为右值调用了移动赋值,因为虽然str是左值,但它是局部对象,终归是为ret1服务的,出了作用域就消亡,和临时对象的意义差不多。左值引用和右值引用引用简单来说就是给对象取别名,我们刚开始接触C++的时候就学过,这里又区分出左值引用和右值引用,它们有什么不同?要想探讨这个问题,首先应该了解清楚具体什么是左值什么是右值。
constint&r1 =10;constint&r2 =x +y;constint&r3 =fmin(x,y);conststring&r4 =string("abcdef");
例如下面的场景:
intmain(){vector<string>v;string s("1111");v.push_back(s);v.push_back(string("2222"));v.push_back("3333");return0;}
前面我们模拟实现List的
push_back
:void push(const T& x)
,加上const
修饰另一个目的也是为了既能接收左值又能接收右值,这样我们既可以插入一个有名对象,又能插入匿名对象了。2.5 完美转发
上面看到C++11后STL容器插入接口基本都对左值和右值做了对应的函数,那以后类似这样的场景我们都要写两个甚至更多的版本吗?为了方便C++11又引入了万能引用:
template<classT>voidfunc(T&&x){//...}
在函数模版中,这里的
T&& x
不再是前面我们见到的右值引用,而是万能引用。
C++11后又增加了两个默认的成员函数:移动构造和移动赋值重载。
- 移动构造代价很小
- 不是所有的编译器都像VS2022这样做极致的优化
- 有其他场景下优化不了
2.2 移动赋值
除了移动构造,还有移动赋值,本质还是一样的。拷贝构造等,就不要去多此一举了,因为这样可能会在其他地方出问题。
intmain(){inta =1;int*p =&a;constintb =a;*p =10;string s("abcdef");s[0];int&&r1 =move(a);int*&&r2 =move(p);constint&&r3 =move(b);string&&r4 =move(s);string&&r5 =(string&&)s;return0;}
右值不能取地址,但是给右值取别名后,右值会被存储到特定位置,且可以取到该位置的地址,可以修改,如果不想被修改可以用
const
修饰。string类有拷贝构造,也有移动构造
虽然str是一个左值,但是它出了作用域就消亡,和临时对象的结局是一样的,所以可以把str作为一个右值来走移动构造,这里是隐式的将str
move
为右值。
其实右值引用本身是左值也不奇怪,如果右值引用本身是右值,右值一般不能修改,那还怎么通过移动语义来掠夺资源呢。2.3 STL容器插入接口
右值引用解决的不只是传值返回的问题,还有一些容器插入接口的问题。临时对象用完就要消亡,再对它拷贝构造显得有点多余,既然它的结局已经注定了还不如把它的东西直接拿过来,这里就引出了移动构造,所以移动构造直接将构造的对象和被构造的对象数据交换(掠夺)一下就行。
x
本身一个左值,其引用的对象是一个右值,在调用insert
时我们期望调用右值引用的接口,是参数匹配的问题,所以可以考虑用move
进行类似强转的操作。3.2 新关键字
- default:强制生成默认成员函数关键字
如果因为某些原因我们真的需要默认成员函数,则可以用
default
强制生成。
那既然编译器都优化的这么好了,那移动构造还有意义吗?并且它是直接构造,而走移动构造的话是构造+移动构造。如果不想r1被修改,可以用const int&& r1
去引用。
左值引用一般是不能给右值取别名的,但是可以用const
修饰就行了。
std::forward<T>(t)
完美转发在传参的过程中保持了t的原生类型属性。那左值引用能不能给右值取别名,右值引用能不能给左值取别名呢?
如果左值引用不能给右值取别名,那C++11出来之前右值是不是都不能取别名?猜测一下也知道大概率不是的。类的新功能
3.1 新默认成员函数
前面我们学了类的6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
默认成员函数是我们不写编译器会默认生成的函数,其中最后两个不常用。下面我们来看下有移动赋值和没有移动赋值有什么区别。
move()
可以看作像是强制类型转换,所以也不会改变操作对象本身的属性。临时对象、
而默认生成的移动构造和移动赋值主要是作用于上面Person
这样的类,它本身的成员并不需要深拷贝,但是其有自定义类型的成员,一般这个自定义类型成员都有自己的移动构造和移动赋值,那就会调用这个自定义类型自己的移动构造和移动赋值。//赋值重载string&operator=(conststring&str){//防止自己给自己赋值if(this!=&str){delete[]_str;_str =newchar[str._capacity +1];strcpy(_str,str._str);_size =str._size;_capacity =str._capacity;}return*this;}//移动赋值string&operator=(string&&str){swap(str);return*this;}
有调用赋值重载的情况时编译器不能像之前一样优化为直接构造,因为这里调用to_string前ret1是已经存在的对象,编译器就没办法优化了。
上面我们提到了像VS2022这种比较激进的编译器优化比较夸张,它一步到位优化为直接构造,这里str就像ret1的左值引用一样。
二、*p、类的新功能
- 3.1 新默认成员函数
- 3.2 新关键字
前言
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。例如:不能取字面量10的地址,但是r1引用后,可以对r1取地址,也可以修改r1。无论左值引用还是右值引用,都是给对象取别名。
1、
//拷贝构造string(conststring&str){_str =newchar[str._capacity +1];//多开一个存'