copy control

定义一个类时,可以显式或隐式地指定此类型对象拷贝、移动、赋值和销毁时的行为。我们可以通过定义五种特殊的方法来控制这些操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。

拷贝/赋值/销毁

copy-constructor

定义:一个构造函数,第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是copy-constructor。其第一个参数必须是引用类型,否则调用时,其参数也需要被拷贝,如此无限循环。
如下,定义Obj类与其拷贝构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

class Obj
{
public:
int count{};
Obj() { }
Obj(int c) : count(c) {}
Obj(const Obj & obj, int c = 0)
{
cout << "call copy-constructor" << endl;
this->count = c;
}
};
int main()
{
Obj obj1(12);
Obj obj2(obj1, 12);
cout << obj2.count << endl;
return 0;
}

其中拷贝构造函数第二个参数具有默认值0。编译运行:

1
2
3
> g++ main.cc -O3 -g -o main && ./main
call copy-constructor
12

通过以下例子,查看拷贝构造函数调用的时刻,并为Obj类添加输出提示信息

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
class Obj
{
public:
int count{};
Obj()
{
cout << "call constructor" << endl;
}
Obj(int c) : count(c) { cout << "call Obj(int) constructor" << endl; }
Obj(const Obj & obj)
{
cout << "call copy-constructor" << endl;
this->count = obj.count;
}
};

class Temp
{
public:
Obj obj;
~Temp()
{
cout << "call Temp destructor" << endl;
}
};

void foo(Obj obj)
{
// do nothing
return;
}

Obj bar()
{
return Obj(1);
}

int main()
{
// 编译器掠过拷贝初始化
cout << "编译器掠过拷贝初始化:\n";
Obj o = 1;
cout << o.count << endl;
// 1. 列表初始化
cout << "列表初始化: \n";
vector<Obj> objs{1,2,3};
for (auto & obj : objs)
cout << obj.count << ' ';
cout << endl;
// 2. 传递非引用参数
cout << "传递非引用参数: \n";
foo(o);
// 3. 返回非引用参数
cout << "返回非引用参数: \n";
Obj x = bar();

return 0;
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> g++ main.cc -g -o main && ./main
编译器掠过拷贝初始化:
call Obj(int) constructor
1
列表初始化:
call Obj(int) constructor
call Obj(int) constructor
call Obj(int) constructor
call copy-constructor
call copy-constructor
call copy-constructor
1 2 3
传递非引用参数:
call copy-constructor
返回非引用参数:
call Obj(int) constructor
rda@pa ~/T/proj (slave2)>

再看一个稍微复杂的例子,判断有几次拷贝构造函数的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Obj global;
// 1. 传参时copy-constructor
Obj bar(Obj arg)
{
Obj local = arg, *heap = new Obj(global); // 2. copy-constructor 3. global传参时copy-constructor
*heap = local; // note!!这是赋值操作
Obj pa[4] = { local, *heap }; // 4,5 copy-constructor

return *heap; // 6. 返回一个非引用参数copy-constructor
}

int main()
{
bar(global);

return 0;
}

输出结果为

1
2
3
4
5
6
7
> g++ main.cc -g -o main && ./main
call copy-constructor
call copy-constructor
call copy-constructor
call copy-constructor
call copy-constructor
call copy-constructor

总而言之,当存在使用一个已有的类对象去初始化另一个类对象时会调用copy-constructor。

another example

再看一个例子,验证合成拷贝构造函数的行为。
我们定义一个新的类Num,自定义其默认构造函数,为每个对象生成一个唯一序号,保存在数据成员id中。编译器为其生成合成拷贝构造函数和拷贝赋值运算符。

1
2
3
4
5
6
7
8
9
10
11
12
class Num
{
public:
static int next_id;
int id{};
Num()
{
id = next_id++;
}
};

int Num::next_id = 1;

如下foo和bar函数打印传入参数的id,其中foo接受非引用参数,而bar接受引用类型参数

1
2
3
4
5
6
7
8
9
10
11
void foo(const Num num)
{
cout << num.id << endl;
return;
}

void bar(const Num & num)
{
cout << num.id << endl;
return;
}

main函数如下,第一次调用foo

1
2
3
4
5
6
7
8
9
int main()
{
Num a, b = a, c = b; // 默认构造a,拷贝构造b和c
foo(a); // 通过a拷贝构造参数num
foo(b); // 通过a拷贝构造参数num
foo(c); // 通过a拷贝构造参数num

return 0;
}

输出为

1
2
3
4
> g++ main.cc -g -o main && ./main
1
1
1

因为默认的拷贝构造函数会直接复制非static成员。
man函数中调用bar。输出不变。
自定义拷贝构造函数,main函数调用bar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Num
{
public:
static int next_id;
int id{};
Num()
{
id = next_id++;
}
Num(const Num & num)
{
id = next_id++;
}
};

打印出预期的信息

1
2
3
4
> g++ main.cc -g -o main && ./main
1
2
3

调用foo则不同,因为传参时拷贝构造了参数对象,其id和传入的参数不同

1
2
3
4
> g++ main.cc -g -o main && ./main
4
5
6

拷贝赋值运算符

类可以控制其对象如何赋值,即通过重载赋值运算符

1
2
Obj obj1, obj2;
obj1 = obj2; // 拷贝赋值

重载赋值运算符,即重载operate=函数,通常返回左侧对象的引用。如下,为Obj类重载赋值运算符

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

class Obj
{
public:
int count{};
Obj()
{
cout << "call constructor" << endl;
}

Obj(int c) : count(c)
{
cout << "call Obj(int) constructor" << endl;
}

Obj & operator=(const Obj & o)
{
count = o.count;
cout << "call operator = " << endl;
return *this;
}

Obj(const Obj & obj)
{
cout << "call copy-constructor" << endl;
this->count = obj.count;
}

~Obj()
{
cout << "call deconstructor" << endl;
}
};

合成赋值运算符

如果一个类未定义其拷贝赋值运算符,编译器会生成一个合成拷贝赋值运算符,默认将右侧对象的每个非static成员赋予左侧对象,并返回一个指向左侧对象的引用。

析构函数

析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员。析构函数没有返回值,不接受参数。

何时调用

当一个对象被销毁时,自动调用:

  • 变量离开作用域,如函数的局部变量和非引用类型的参数
  • 一个对象被销毁时其成员对象被销毁
  • 对于动态分配的对象,对指向它的指针应用delete
    等等。

仍然是Obj类的例子,在其析构函数中添加输出信息,main函数中调用foo函数如下:

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

class Obj
{
public:
int count{};
Obj()
{
}
Obj(int c) : count(c)
{
}
Obj(const Obj & obj)
{
// cout << "call copy-constructor" << endl;
this->count = obj.count;
}
~Obj()
{
cout << "call deconstructor" << endl;
}
};

// 拷贝构造obj2
void fcn(const Obj * obj1, Obj obj2)
{
// 拷贝构造obj3和obj4
Obj obj3(*obj1), obj4(obj2);
// 销毁obj2、obj3、obj4
return;
}


Obj global;
int main()
{
Obj *p = new Obj();
fcn(p, global);

delete p; // 销毁p指向的对象

return 0; // 程序结束之前销毁全局对象global
}

输出结果:

1
2
3
4
5
6
7
8
9
> g++ main.cc -g -o main && ./main
call copy-constructor
call copy-constructor
call copy-constructor
call deconstructor
call deconstructor
call deconstructor
call deconstructor
call deconstructor

值行为类

定义类VPtr,

指针行为类

定义类PPtr,

swap

动态内存管理类