cpp学习-第十三章-拷贝控制

核心简记

类型的构造、拷贝、移动、赋值、销毁可被显示或隐式定义。有5种特殊的成员函数:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数,称为拷贝控制操作。
一般来说,若一个类定义了5个操作的任意一个,就应该定义所有五个操作。
移动操作当对性能需求不大时可以先不加,可以先实现拷贝操作与析构,最后优化时再加移动操作。不影响结果。

拷贝控制

自定义时机:

  1. 当需要自定义析构函数时,往往需要自定义拷贝构造函数与拷贝赋值运算符。
  2. 需要拷贝构造函数,往往需要拷贝赋值运算符。相反也是

可以对任意具有合成版本的成员函数使用=default,生成默认版本。

拷贝构造函数

第一个参数是自身类型的引用、额外参数都有默认值的构造函数。

1
2
3
4
5
class A{
public:
A();
A(const A&);//拷贝构造函数 初始化少的成员使用默认初始化
};

需要是const的,应用更广。不应该是explicit的,因为隐式使用会是=(拷贝初始化),且拷贝初始化就是使用=。必须是引用,因为不是引用要拷贝构造。

合成的拷贝构造函数:如果没有定义拷贝构造函数,就会合成,合成的操作为逐成员拷贝。非static
类类型成员-调用其拷贝构造函数
内置类型成员-直接拷贝
数组-逐元素拷贝,元素是类类型则使用元素拷贝构造函数。
等价于:

1
A::A(const A &ls):a(ls.a),b(ls.b){}

拷贝初始化与直接初始化的差异:

1
2
3
4
5
6
7
string a(10," "); //直接初始化
string b(a); //直接初始化
string c=a; //拷贝初始化
string d="123"; //拷贝初始化
//拷贝初始化的其它发生处:非引用传参、非引用返回、数组或聚合类的列表初始化
//可以理解为使用=的初始化就是拷贝初始化

直接初始化要求编译器使用普通的函数匹配。
拷贝初始化则可能使用:

  1. 拷贝构造函数
  2. 移动构造函数
  3. 隐式转化构造函数-类的列表构造也在其中

隐式转化使用隐式转化构造函数构造对象、之后再使用拷贝/移动构造函数。这个阶段编译器可以跳过,直接使用转化构造函数创建对象,即使略过,这个时候拷贝/移动构造函数必须是存在且可访问的。

拷贝赋值运算符

重载运算符本质为函数:由operator关键字后接运算符。如果重载运算符是一个成员函数,其左侧对象绑定到隐式的this参数,其余对象显示传递。
拷贝赋值运算符接受一个与其所在类同类型的参数:

1
2
3
4
class A{
public:
A& operator=(const A&);//赋值运算符 自定义时少的成员就不变了,因为左右都是对象
};

应该返回指向其左侧对象的引用,const的范围更大,且参数并不要求引用。

合成的拷贝赋值运算符:如果没有定义自己的拷贝赋值运算符,编译器就会合成。
合成行为逐成员(非static)赋予。对于数组,逐元素赋值。相当于

1
2
3
4
5
6
A& A::operator=(const A &ls)
{
a=ls.a;
b=ls.b;
return *this;
}

析构函数

名字由波浪号接类名构成,没有返回值、参数。

1
2
3
4
class A{
public:
~A();//析构函数
};

不能被重载。析构函数有函数体和析构部分。构造函数,先初始化再执行函数体。析构函数,先执行函数体再销毁成员。成员按初始化的逆序销毁(初始化顺序是按成员类内出现先后)。
构造函数可以显式用初始化列表控制初始化。但是析构函数的析构部分是隐式的,类类型成员执行自己的析构函数、内置类型什么也不做。也就是说析构自动调用成员析构。

析构自动执行时机:

  1. 变量离开作用域时自动销毁
  2. 对象销毁时成员销毁
  3. 容器销毁时元素销毁,包括数组
  4. 动态分配的对象指针执行delete时
  5. 临时对象,完整表达式结束时

合成析构函数:一个类未定义析构函数时,编译器合成。
函数体为空,等价于

1
2
3
4
class A{
public:
~A(){}
};

阻止拷贝

可以将拷贝构造函数、拷贝赋值运算符定义为删除的函数来阻止拷贝。如io类,定义为删除函数也是定义。
删除的函数声明了但是不能使用,在函数参数列表后加=delete定义为删除。

1
2
3
class A{
A(const A&)=delete;
}

delete必须出现在函数第一次声明时,可以对任意函数指定delete。
析构函数不能删除,否则只可动态分配且不能释放。

合成的拷贝控制成员可能是删除的:

  1. 如果某个成员的析构函数是删除或不可访问的,则类的合成析构函数为删除的
  2. 如果某个成员的拷贝构造函数是删除或不可访问、或成员析构函数是删除或不访问的,则类合成的拷贝构造函数为删除的。
  3. 某成员拷贝赋值运算符删除/不可访问,或类有const/引用的成员,则类的合成拷贝赋值运算符为删除的。
  4. 某成员析构函数删除/不可访问,或类有引用成员但没有类内初始化,或有const成员但没有类内初始化。且未定义默认构造函数,则默认构造函数定义为删除的。

本质上,当不可拷贝、赋值、销毁成员时,类的合成拷贝控制成员就被定义为删除的。

旧时的阻止是使用private声明且不定义。

示例

定义拷贝操作使类的行为像值或指针

像值的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A{
public:
A():a(new string()),b(0){}
A(const A &p):a(new string(*p.a)),b(p.b){}//拷贝构造
A &operator(const A&);//拷贝赋值声明
~A(){deleate a;}//析构
private:
string *a;
int b;
}
//拷贝赋值运算符,通常结合了构与析构的工作
A& A::operator=(const A &r)
{
auto newp=new string(*r.a);
delete a;
a=newp;
b=r.b;
return *this;
}

赋值运算符注意:

  1. 自身赋予自身也能正确工作
  2. 组合构与析构
  3. 少处理的成员不改变
像指针的类

类似智能指针,应该使用引用计数:构造函数初始化其为1、拷贝构造递增共享的计数器、析构递减、赋值增右减左。计数器分配在动态内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class A{
public:
A():a(new string()),b(0),use(new std::size_t(1)){}
A(const A&p):a(p.a),b(p.b),use(p.use){++*use;}//拷贝构造
A &operator=(const A&);//拷贝赋值声明
~A();//析构
private:
string *a;
int b;
std::size_t *use;
}
//析构函数
A::~A()
{
if(--*use==0)
{
delete a;
delete use;
}
}
//拷贝赋值运算符,通常结合了构与析构的工作
A& A::operator=(const A &r)
{
++*r.use;
if(--*use==0)
{
delete a;
delete use;
}
a=r.a;
b=r.b;
use=r.use;
return *this;
}

可以用swap操作简化拷贝赋值运算符。

对象移动

可以将一个对象数据移动到另一个对象。在很多情况下不需要拷贝操作,比如源即将销毁的时候,使用移动操作效率更高。
std中的容器、string、sp移动与拷贝都支持。io和up只支持移动。

右值引用

左值-使用变量的位置
右值-使用变量的内容
不论左值还是右值,在汇编下都是地址上的数据,全部都被编译为数据对象。只是编译时处理不同而已,运行时生存期也不同。

右值引用使用&&,只能绑定到一个将要销毁的对象,用来将资源移动到另一个对象中。
与任何引用相同,不过是某对象的另一个名字。右值引用与左值引用有着完全相反的绑定特性:

1
2
3
4
5
6
7
8
int i=21;
int &r=i; //正确
int &&rr=i; //错误,不可绑定左值
int &r2=i*42; //错误,不可绑定右值
const int &r3=i*42; //正确,可以将const引用绑定右值
int &&rr2=i*42; //正确
int &&rr3=123; //正确,字面值常量是右值
int &&rr4=rr3; //错误,变量是左值

返回左值引用的函数、赋值、下标、解引用、前置+-都是返回左值表达式。可以绑定左值引用。左值有持久的形态。
返回非引用类型的函数、算数、关系、位、后置+-都是右值表达式。我们可以把右值引用与const左值引用绑定到这类表达式上,右值引用可以更改它获取资源,左值引用只能读取拷贝它。右值为临时对象或字面常量。

分别为可控内存区与自动内存区,都是汇编内存区的内容。

因此右值引用:引用的对象即将销毁、该对象没有其它用户。因此使用右值引用的代码可以自由接管所引用对象的资源。

标准库move:
使用move将左值化为右值引用类型,定义在utility。其机制见后

1
int &&rr4=std::move(rr3);

使用move意味着:对rr3赋值或销毁之外,不在使用它。

移动构造函数&移动赋值运算符

定义移动构造函数与移动赋值运算符使类型支持移动操作。移动资源而不拷贝资源。如果没有移动操作,通过正常的函数匹配会使用对应的拷贝操作。
移动构造函数第一个参数是该类型的右值引用,额外参数都有默认实参。

1
2
3
4
5
A::A(A &&ls) noexcept //表示不抛出异常,必须在声明与定义中都指定,在参数列表后,初始化列表前。
:a(ls.a),b(ls.b)
{
ls.a=ls.b=nullptr;
}

移动后应该保证销毁源是无害的。除非标准库知道我们的移动构造函数不会抛出异常,否则它会认为会抛出异常并做额外处理。如vector,会在重新分配内存时使用拷贝构造而不是移动构造。

移动赋值运算符:

1
2
3
4
5
6
7
8
9
10
A &A::operator=(A &&ls) noexcept {
if(this!=&ls) //是引用就能当原对象使,只是别名
{
free(); //释放左的内容
a=ls.a;
b=ls.b;
ls.a=ls.b=nullptr;
}
return *this;
}

仍是构造与析构的结合。
移动操作后,源必须是有效、可析构的。但用户不能对其值有任何假设。

合成的移动操作:
只有当没有定义任何自定义版本的拷贝控制成员时,且每个非static成员都可以移动时,编译器才会为它合成移动操作。移动操作不会自动为删除的函数,但如果显示定义为default但又不可生成时,为delete。如果类定义了一个移动操作,则类合成的拷贝构造与拷贝赋值为删除的。也就是说默认不互容。

对于移动与拷贝构造都有的类,使用普通的函数匹配机制确定使用。传入右值先用右值版本,传入左值使用左值版本,因为精确匹配。若没有移动版本就使用拷贝版本,因为右值到左值有一次到const的转化。

右值引用与成员函数

成员函数参数类型可以提供拷贝与移动版本,一个const A &一个A&&。

指定this的类型:引用限定符
为了防止 a+a=1这样的使用
可以在参数列表后加&使其*this只能是可修改的左值。

1
A &operator=(const A&) &;//指定this指向左值。&&表示指向右值

类似const限定,&表示调用对象只能是左值,&&表示只能是右值。引用限定符在const之后

底层的const、&、&&可以区分重载版本。范围都不同:普通与const类型、左值类型、右值类型。
不过当定义俩个及以上同名同参的成员函数,必须所有函数加引用限定,或都不加

反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include<utility>
class A
{
private:
int a1;
int *a2;
public:
A(int a=1,int b=2):a1(a),a2(new int(b)){}
~A(){delete a2;}//析构
A(const A&);//拷贝构造
A &operator=(const A&);//拷贝赋值
//后俩个对性能需求不大时可以先不加
A(A&&) noexcept;//移动构造
A &operator=(A&&) noexcept;//移动赋值
};
A::A(const A &ls)
:a1(ls.a1),a2(new int(*ls.a2))
{}
A& A::operator=(const A &r)
{
auto newp=new int(*r.a2);
delete a2;
a2=newp;
a1=r.a1;
return *this;
}
//移动操作省去了分配新空间
A::A(A &&ls) noexcept
:a1(ls.a1),a2(ls.a2)
{
ls.a2=nullptr;
}
A &A::operator=(A &&r) noexcept
{
if(this!=&r)
{
delete a2;
a1=r.a1;
a2=r.a2;
r.a2=nullptr;
}
return *this;
}
int main(void)
{
int i=9;
int &&rr1=123;
int &&rr2=i*10;
A a;
A b;
A &&rra=std::move(a);
//使用拷贝 违反了规则,不应该使用a了
b=a;
//使用移动
b=rra;
}

ARM-64

指令

MADD Multiply-add
MADD , , , ; Rd = Ra + Rn * Rm

来自一个外国博客:https://quequero.org/2014/04/introduction-to-arm-architecture/

分析

main:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
.text:0000000000002DC4 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000002DC4 EXPORT main
.text:0000000000002DC4 main ; DATA XREF: LOAD:0000000000000518↑o
.text:0000000000002DC4 ; .got:main_ptr↓o
.text:0000000000002DC4
.text:0000000000002DC4 var_60 = -0x60
.text:0000000000002DC4 var_58 = -0x58
.text:0000000000002DC4 var_50 = -0x50
.text:0000000000002DC4 var_48 = -0x48
.text:0000000000002DC4 var_40 = -0x40
.text:0000000000002DC4 var_34 = -0x34
.text:0000000000002DC4 var_30 = -0x30
.text:0000000000002DC4 var_20 = -0x20
.text:0000000000002DC4 var_10 = -0x10
.text:0000000000002DC4 var_C = -0xC
.text:0000000000002DC4 var_8 = -8
.text:0000000000002DC4 var_s0 = 0
.text:0000000000002DC4
.text:0000000000002DC4 ; __unwind {
.text:0000000000002DC4 SUB SP, SP, #0x70
.text:0000000000002DC8 STP X29, X30, [SP,#0x60+var_s0]
.text:0000000000002DCC ADD X29, SP, #0x60
.text:0000000000002DD0 SUB X0, X29, #-var_20
.text:0000000000002DD4 MOV W1, #1
.text:0000000000002DD8 MOV W2, #2
.text:0000000000002DDC SUB X8, X29, #-var_10
.text:0000000000002DE0 MOV W9, #0xA
.text:0000000000002DE4 SUB X10, X29, #-var_C
.text:0000000000002DE8 MOV W11, #0x7B
.text:0000000000002DEC MOV W12, #9
.text:0000000000002DF0 MRS X13, #3, c13, c0, #2
.text:0000000000002DF4 LDR X13, [X13,#0x28]
.text:0000000000002DF8 STUR X13, [X29,#var_8]
// var_34存i
.text:0000000000002DFC STR W12, [SP,#0x60+var_34]
// var_C存123字面值
.text:0000000000002E00 STUR W11, [X29,#var_C]
//var_40存var_C地址 为rr1
.text:0000000000002E04 STR X10, [SP,#0x60+var_40]
//W9=W9*i+0 结果存于var_10
.text:0000000000002E08 LDR W11, [SP,#0x60+var_34]
.text:0000000000002E0C MADD W9, W11, W9, WZR
.text:0000000000002E10 STUR W9, [X29,#var_10]
//var_48为var_10的指针 为rr2
.text:0000000000002E14 STR X8, [SP,#0x60+var_48]
//var_20为a的this,默认构造
.text:0000000000002E18 BL sub_2EA0
//var_30为b的地址,默认构造-当带默认实参时就是调用时自动传入。
.text:0000000000002E1C ADD X0, SP, #0x60+var_30
.text:0000000000002E20 MOV W1, #1
.text:0000000000002E24 MOV W2, #2
.text:0000000000002E28 BL sub_2EA0
//A &&rra=std::move(a);
.text:0000000000002E2C SUB X0, X29, #-var_20
.text:0000000000002E30 BL sub_2EF4
//b=a 直接传的左右地址,引用就是指针。
.text:0000000000002E34 ADD X8, SP, #0x60+var_30
.text:0000000000002E38 SUB X1, X29, #-var_20
.text:0000000000002E3C STR X0, [SP,#0x60+var_50]
.text:0000000000002E40 MOV X0, X8
.text:0000000000002E44 BL sub_2D40
//b=rra; var_50是rra
.text:0000000000002E48 ADD X8, SP, #0x60+var_30
.text:0000000000002E4C LDR X1, [SP,#0x60+var_50]
.text:0000000000002E50 STR X0, [SP,#0x60+var_58]
.text:0000000000002E54 MOV X0, X8
.text:0000000000002E58 BL sub_2D40
//析构
.text:0000000000002E5C ADD X8, SP, #0x60+var_30
.text:0000000000002E60 STR X0, [SP,#0x60+var_60]
.text:0000000000002E64 MOV X0, X8
.text:0000000000002E68 BL sub_2F08
.text:0000000000002E6C SUB X0, X29, #-var_20
.text:0000000000002E70 BL sub_2F08
.text:0000000000002E74 MRS X8, #3, c13, c0, #2
.text:0000000000002E78 LDR X8, [X8,#0x28]
.text:0000000000002E7C LDUR X10, [X29,#var_8]
.text:0000000000002E80 CMP X8, X10
.text:0000000000002E84 B.NE loc_2E9C
.text:0000000000002E88 MOV W8, WZR
.text:0000000000002E8C MOV W0, W8
.text:0000000000002E90 LDP X29, X30, [SP,#0x60+var_s0]
.text:0000000000002E94 ADD SP, SP, #0x70
.text:0000000000002E98 RET
.text:0000000000002E9C ; ---------------------------------------------------------------------------
.text:0000000000002E9C
.text:0000000000002E9C loc_2E9C ; CODE XREF: main+C0↑j
.text:0000000000002E9C BL .__stack_chk_fail
.text:0000000000002E9C ; } // starts at 2DC4
.text:0000000000002E9C ; End of function main
.text:0000000000002E9C

sub_2EA0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
.text:0000000000002EA0 sub_2EA0 ; CODE XREF: main+54↑p
.text:0000000000002EA0 ; main+64↑p
.text:0000000000002EA0
.text:0000000000002EA0 var_18 = -0x18
.text:0000000000002EA0 var_10 = -0x10
.text:0000000000002EA0 var_C = -0xC
.text:0000000000002EA0 var_8 = -8
.text:0000000000002EA0 var_s0 = 0
.text:0000000000002EA0
.text:0000000000002EA0 ; __unwind {
.text:0000000000002EA0 SUB SP, SP, #0x30
.text:0000000000002EA4 STP X29, X30, [SP,#0x20+var_s0]
.text:0000000000002EA8 ADD X29, SP, #0x20
.text:0000000000002EAC MOV X8, #4
.text:0000000000002EB0 STUR X0, [X29,#var_8]
.text:0000000000002EB4 STUR W1, [X29,#var_C]
.text:0000000000002EB8 STR W2, [SP,#0x20+var_10]
.text:0000000000002EBC LDUR X0, [X29,#var_8]
.text:0000000000002EC0 LDUR W1, [X29,#var_C]
//放a1
.text:0000000000002EC4 STR W1, [X0]
.text:0000000000002EC8 STR X0, [SP,#0x20+var_18]
//分配4字节大小
.text:0000000000002ECC MOV X0, X8 ; unsigned __int64
.text:0000000000002ED0 BL _Znwm ; operator new(ulong)
.text:0000000000002ED4 MOV X8, X0
//放入刚分配的内存中数据
.text:0000000000002ED8 LDR W1, [SP,#0x20+var_10]
.text:0000000000002EDC STR W1, [X0]
.text:0000000000002EE0 LDR X0, [SP,#0x20+var_18]
//放入a2,刚new的地址
.text:0000000000002EE4 STR X8, [X0,#8]
.text:0000000000002EE8 LDP X29, X30, [SP,#0x20+var_s0]
.text:0000000000002EEC ADD SP, SP, #0x30
.text:0000000000002EF0 RET
.text:0000000000002EF0 ; } // starts at 2EA0

sub_2D40

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
.text:0000000000002D40 sub_2D40 ; CODE XREF: main+80↓p
.text:0000000000002D40 ; main+94↓p
.text:0000000000002D40
.text:0000000000002D40 var_28 = -0x28
.text:0000000000002D40 var_20 = -0x20
.text:0000000000002D40 var_18 = -0x18
.text:0000000000002D40 var_10 = -0x10
.text:0000000000002D40 var_8 = -8
.text:0000000000002D40 var_s0 = 0
.text:0000000000002D40
.text:0000000000002D40 ; __unwind {
.text:0000000000002D40 SUB SP, SP, #0x40
.text:0000000000002D44 STP X29, X30, [SP,#0x30+var_s0]
.text:0000000000002D48 ADD X29, SP, #0x30
.text:0000000000002D4C MOV X8, #4
//左右this
.text:0000000000002D50 STUR X0, [X29,#var_8]
.text:0000000000002D54 STUR X1, [X29,#var_10]
// auto newp=new int(*r.a2);
.text:0000000000002D58 LDUR X0, [X29,#var_8]
.text:0000000000002D5C STR X0, [SP,#0x30+var_20]
.text:0000000000002D60 MOV X0, X8 ; unsigned __int64
.text:0000000000002D64 BL _Znwm ; operator new(ulong)
.text:0000000000002D68 MOV X8, X0
.text:0000000000002D6C LDUR X1, [X29,#var_10]
.text:0000000000002D70 LDR X1, [X1,#8]
.text:0000000000002D74 LDR W9, [X1]
.text:0000000000002D78 STR W9, [X0]
.text:0000000000002D7C STR X8, [SP,#0x30+var_18]
//删左 delete自带nullptr检验
.text:0000000000002D80 LDR X8, [SP,#0x30+var_20]
.text:0000000000002D84 LDR X0, [X8,#8]
.text:0000000000002D88 STR X0, [SP,#0x30+var_28]
.text:0000000000002D8C CBZ X0, loc_2D9C
.text:0000000000002D90 LDR X8, [SP,#0x30+var_28]
.text:0000000000002D94 MOV X0, X8 ; void *
.text:0000000000002D98 BL _ZdlPv ; operator delete(void *)
.text:0000000000002D9C
.text:0000000000002D9C loc_2D9C ; CODE XREF: sub_2D40+4C↑j
//a2=newp;
.text:0000000000002D9C LDR X8, [SP,#0x30+var_18]
.text:0000000000002DA0 LDR X9, [SP,#0x30+var_20]
.text:0000000000002DA4 STR X8, [X9,#8]
//a1=r.a1;
.text:0000000000002DA8 LDUR X8, [X29,#var_10]
.text:0000000000002DAC LDR W10, [X8]
.text:0000000000002DB0 STR W10, [X9]
//返回指针
.text:0000000000002DB4 MOV X0, X9
.text:0000000000002DB8 LDP X29, X30, [SP,#0x30+var_s0]
.text:0000000000002DBC ADD SP, SP, #0x40
.text:0000000000002DC0 RET
.text:0000000000002DC0 ; } // starts at 2D40
.text:0000000000002DC0 ; End of function sub_2D40
.text:0000000000002DC0

new只是分配了内存。初始化还是在new外完成的。delete也是
不管左值还是右值引用汇编下都是指针。引用就是指针。
这里不知道为啥我使用了右值引用的拷贝编译出来用的函数左值的……看完move的原理再分析。

还有就是这里arm的返回地址和旧栈帧在变量的上方,以X29分割返回地址与变量

x86-64

困了,不分析86的了