核心简记
类型的构造、拷贝、移动、赋值、销毁可被显示或隐式定义。有5种特殊的成员函数:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数,称为拷贝控制操作。
一般来说,若一个类定义了5个操作的任意一个,就应该定义所有五个操作。
移动操作当对性能需求不大时可以先不加,可以先实现拷贝操作与析构,最后优化时再加移动操作。不影响结果。
拷贝控制
自定义时机:
- 当需要自定义析构函数时,往往需要自定义拷贝构造函数与拷贝赋值运算符。
- 需要拷贝构造函数,往往需要拷贝赋值运算符。相反也是
可以对任意具有合成版本的成员函数使用=default,生成默认版本。
拷贝构造函数
第一个参数是自身类型的引用、额外参数都有默认值的构造函数。
需要是const的,应用更广。不应该是explicit的,因为隐式使用会是=(拷贝初始化),且拷贝初始化就是使用=。必须是引用,因为不是引用要拷贝构造。
合成的拷贝构造函数:如果没有定义拷贝构造函数,就会合成,合成的操作为逐成员拷贝。非static
类类型成员-调用其拷贝构造函数
内置类型成员-直接拷贝
数组-逐元素拷贝,元素是类类型则使用元素拷贝构造函数。
等价于:
拷贝初始化与直接初始化的差异:
直接初始化要求编译器使用普通的函数匹配。
拷贝初始化则可能使用:
- 拷贝构造函数
- 移动构造函数
- 隐式转化构造函数-类的列表构造也在其中
隐式转化使用隐式转化构造函数构造对象、之后再使用拷贝/移动构造函数。这个阶段编译器可以跳过,直接使用转化构造函数创建对象,即使略过,这个时候拷贝/移动构造函数必须是存在且可访问的。
拷贝赋值运算符
重载运算符本质为函数:由operator关键字后接运算符。如果重载运算符是一个成员函数,其左侧对象绑定到隐式的this参数,其余对象显示传递。
拷贝赋值运算符接受一个与其所在类同类型的参数:
应该返回指向其左侧对象的引用,const的范围更大,且参数并不要求引用。
合成的拷贝赋值运算符:如果没有定义自己的拷贝赋值运算符,编译器就会合成。
合成行为逐成员(非static)赋予。对于数组,逐元素赋值。相当于
析构函数
名字由波浪号接类名构成,没有返回值、参数。
不能被重载。析构函数有函数体和析构部分。构造函数,先初始化再执行函数体。析构函数,先执行函数体再销毁成员。成员按初始化的逆序销毁(初始化顺序是按成员类内出现先后)。
构造函数可以显式用初始化列表控制初始化。但是析构函数的析构部分是隐式的,类类型成员执行自己的析构函数、内置类型什么也不做。也就是说析构自动调用成员析构。
析构自动执行时机:
- 变量离开作用域时自动销毁
- 对象销毁时成员销毁
- 容器销毁时元素销毁,包括数组
- 动态分配的对象指针执行delete时
- 临时对象,完整表达式结束时
合成析构函数:一个类未定义析构函数时,编译器合成。
函数体为空,等价于
阻止拷贝
可以将拷贝构造函数、拷贝赋值运算符定义为删除的函数来阻止拷贝。如io类,定义为删除函数也是定义。
删除的函数声明了但是不能使用,在函数参数列表后加=delete定义为删除。
delete必须出现在函数第一次声明时,可以对任意函数指定delete。
析构函数不能删除,否则只可动态分配且不能释放。
合成的拷贝控制成员可能是删除的:
- 如果某个成员的析构函数是删除或不可访问的,则类的合成析构函数为删除的
- 如果某个成员的拷贝构造函数是删除或不可访问、或成员析构函数是删除或不访问的,则类合成的拷贝构造函数为删除的。
- 某成员拷贝赋值运算符删除/不可访问,或类有const/引用的成员,则类的合成拷贝赋值运算符为删除的。
- 某成员析构函数删除/不可访问,或类有引用成员但没有类内初始化,或有const成员但没有类内初始化。且未定义默认构造函数,则默认构造函数定义为删除的。
本质上,当不可拷贝、赋值、销毁成员时,类的合成拷贝控制成员就被定义为删除的。
旧时的阻止是使用private声明且不定义。
示例
定义拷贝操作使类的行为像值或指针
像值的类
|
|
赋值运算符注意:
- 自身赋予自身也能正确工作
- 组合构与析构
- 少处理的成员不改变
像指针的类
类似智能指针,应该使用引用计数:构造函数初始化其为1、拷贝构造递增共享的计数器、析构递减、赋值增右减左。计数器分配在动态内存中。
可以用swap操作简化拷贝赋值运算符。
对象移动
可以将一个对象数据移动到另一个对象。在很多情况下不需要拷贝操作,比如源即将销毁的时候,使用移动操作效率更高。
std中的容器、string、sp移动与拷贝都支持。io和up只支持移动。
右值引用
左值-使用变量的位置
右值-使用变量的内容
不论左值还是右值,在汇编下都是地址上的数据,全部都被编译为数据对象。只是编译时处理不同而已,运行时生存期也不同。
右值引用使用&&,只能绑定到一个将要销毁的对象,用来将资源移动到另一个对象中。
与任何引用相同,不过是某对象的另一个名字。右值引用与左值引用有着完全相反的绑定特性:
返回左值引用的函数、赋值、下标、解引用、前置+-都是返回左值表达式。可以绑定左值引用。左值有持久的形态。
返回非引用类型的函数、算数、关系、位、后置+-都是右值表达式。我们可以把右值引用与const左值引用绑定到这类表达式上,右值引用可以更改它获取资源,左值引用只能读取拷贝它。右值为临时对象或字面常量。
分别为可控内存区与自动内存区,都是汇编内存区的内容。
因此右值引用:引用的对象即将销毁、该对象没有其它用户。因此使用右值引用的代码可以自由接管所引用对象的资源。
标准库move:
使用move将左值化为右值引用类型,定义在utility。其机制见后
使用move意味着:对rr3赋值或销毁之外,不在使用它。
移动构造函数&移动赋值运算符
定义移动构造函数与移动赋值运算符使类型支持移动操作。移动资源而不拷贝资源。如果没有移动操作,通过正常的函数匹配会使用对应的拷贝操作。
移动构造函数第一个参数是该类型的右值引用,额外参数都有默认实参。
移动后应该保证销毁源是无害的。除非标准库知道我们的移动构造函数不会抛出异常,否则它会认为会抛出异常并做额外处理。如vector,会在重新分配内存时使用拷贝构造而不是移动构造。
移动赋值运算符:
仍是构造与析构的结合。
移动操作后,源必须是有效、可析构的。但用户不能对其值有任何假设。
合成的移动操作:
只有当没有定义任何自定义版本的拷贝控制成员时,且每个非static成员都可以移动时,编译器才会为它合成移动操作。移动操作不会自动为删除的函数,但如果显示定义为default但又不可生成时,为delete。如果类定义了一个移动操作,则类合成的拷贝构造与拷贝赋值为删除的。也就是说默认不互容。
对于移动与拷贝构造都有的类,使用普通的函数匹配机制确定使用。传入右值先用右值版本,传入左值使用左值版本,因为精确匹配。若没有移动版本就使用拷贝版本,因为右值到左值有一次到const的转化。
右值引用与成员函数
成员函数参数类型可以提供拷贝与移动版本,一个const A &一个A&&。
指定this的类型:引用限定符
为了防止 a+a=1这样的使用
可以在参数列表后加&使其*this只能是可修改的左值。
类似const限定,&表示调用对象只能是左值,&&表示只能是右值。引用限定符在const之后
底层的const、&、&&可以区分重载版本。范围都不同:普通与const类型、左值类型、右值类型。
不过当定义俩个及以上同名同参的成员函数,必须所有函数加引用限定,或都不加
反汇编
|
|
ARM-64
指令
MADD Multiply-add
MADD
来自一个外国博客:https://quequero.org/2014/04/introduction-to-arm-architecture/
分析
main:
sub_2EA0
sub_2D40
new只是分配了内存。初始化还是在new外完成的。delete也是
不管左值还是右值引用汇编下都是指针。引用就是指针。
这里不知道为啥我使用了右值引用的拷贝编译出来用的函数左值的……看完move的原理再分析。
还有就是这里arm的返回地址和旧栈帧在变量的上方,以X29分割返回地址与变量
x86-64
困了,不分析86的了