23. 拷贝控制
- 拷贝控制(copy control)
拷贝构造函数(copy constructor)
拷贝赋值运算符(copy-assignment operator)
移动构造函数(move constructor)
移动赋值运算符(move-assignment operator)
析构函数(destructor)
23.1. 拷贝构造函数
拷贝构造函数的第一个参数必须是引用类型 。
在函数调用中,具有非引用类型的参数要进行拷贝初始化。类似地,当一个函数具有非引用类型的返回类型时,返回值会被用来初始化调用方的结果。
拷贝构造函数被用来初始化 非引用类类型 (被初始化的是类的非引用对象)参数,如果拷贝构造函数的参数不是引用类型,为了调用拷贝构造函数, 我们必须拷贝它的实参,然而拷贝实参又需要调用拷贝构造函数,如此无限循环。
如果我们没有为类定义拷贝控制函数,编译器会为我们定义一个。与合成默认构造函数不同(如果定义了其他构造函数,则需要我们再显式定义默认构造函数), 即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。
如果有指针类型的成员数据,编译器合成的拷贝函数就只会复制指针(而不是复制指针指向的内容),从而得到一个相同指向的复制品(浅复制)。 很可能这并不是我们想要的,这时需要考虑自己动手定义拷贝构造函数。
拷贝构造函数的调用时机有如下三种:
当用类的一个对象去初始化该类的另一个对象时。
当函数的形参是类的对象,调用函数时。
当函数的返回值是类的对象,函数执行完成返回时。
23.2. default 和 delete
- 使用
=default
将拷贝控制成员定义为
=default
,显式要求编译器生成合成的版本。类内使用
=default
修饰成员的声明,则合成的函数隐式地声明为内联函数(注:定义在类内的函数自动为内联函数)。类外使用
=default
修饰成员的定义,则合成的成员不是内联函数。只能对默认构造函数或拷贝控制成员使用
=default
。
- 使用
=delete
在函数参数列表之后加上
=delete
定义为 删除的函数 :虽然有声明,但是不能以任何形式使用它们。将拷贝构造函数和拷贝赋值运算符定义为删除的函数,从而阻止拷贝操作。
23.3. 直接初始化和拷贝初始化
直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。
1string dots(10, '.'); // 直接初始化
2string s(dots); // 直接初始化
3
4string s2 = dots; // 拷贝初始化
5string s3 = "999-9999"; // 拷贝初始化
6string s4 = string(100, '9'); // 拷贝初始化
1class ClassTest
2{
3public:
4 ClassTest()
5 {
6 c[0] = '\0';
7 cout << "ClassTest()" << endl;
8 }
9
10 ClassTest& operator=(const ClassTest &ct)
11 {
12 strcpy(c, ct.c);
13 cout << "ClassTest& operator=(const ClassTest &ct)" << endl;
14 return *this;
15 }
16
17 ClassTest(const char *pc)
18 {
19 strcpy(c, pc);
20 cout << "ClassTest (const char *pc)" << endl;
21 }
22
23 // private:
24 ClassTest(const ClassTest& ct)
25 {
26 strcpy(c, ct.c);
27 cout << "ClassTest(const ClassTest& ct)" << endl;
28 }
29private:
30 char c[256];
31};
调用:
1ClassTest ct1("ab"); // 直接初始化
2// 输出: ClassTest (const char *pc)
3
4ClassTest ct2 = "ab"; // 拷贝初始化
5// 输出: ClassTest (const char *pc)
6// 首先调用构造函数 ClassTest(const char *pc) 函数创建一个临时对象;然后调用拷贝构造函数,把这个临时对象作为参数,构造对象ct2
7// 然而结果并没有输出 ClassTest(const ClassTest& ct)。有说法是编译器优化之后,直接匹配了 ClassTest(const char *pc),不再调用拷贝构造函数
8
9ClassTest ct3 = ct1; // 拷贝初始化
10// 输出: ClassTest(const ClassTest& ct)
11// ct1 已经存在,直接调用拷贝构造函数
12
13ClassTest ct4(ct1); // 直接初始化
14// 输出: ClassTest(const ClassTest& ct)
15// ct1 已经存在,直接调用拷贝构造函数
16
17ClassTest ct5 = ClassTest(); // 拷贝初始化
18// 输出: ClassTest()
19// 首先调用默认构造函数产生一个临时对象;然后调用拷贝构造函数,把这个临时对象作为参数,构造对象ct5
20
21ct3 = ct2; // 赋值
22// 输出: ClassTest& operator=(const ClassTest &ct)
当把拷贝构造函数设置为 private
,ct3、ct4、ct5的初始化都无法完成。
23.4. explicit
This keyword is a declaration specifier that can only be applied to in-class constructor declaration.
An explicit constructor cannot take part in implicit conversions. It can only be used to explicitly construct an object.
单个参数的构造函数(或者除了第一个参数外其余参数都有缺省值的多参构造函数)承担了两个角色:
用于构建单参数的类对象;
隐含的类型转换操作符。
explicit
指定转换函数(C++11 起)或构造函数为显式,即它不能用于隐式转换和拷贝初始化。
声明为 explicit
的构造函数不能在隐式转换中使用,只能显式调用去构造一个类对象。其好处在于可以禁止编译器执行非预期(往往也不被期望)的类型转换。
但是将拷贝构造函数声明成 explicit
并不是良好的设计。
1struct A
2{
3 A(int) { } // 转换构造函数
4 A(int, int) { } // 转换构造函数 (C++11)
5 operator bool() const { return true; } // 类型转换函数
6};
7
8struct B
9{
10 explicit B(int) { }
11 explicit B(int, int) { }
12 explicit operator bool() const { return true; }
13};
14
15int main()
16{
17 A a1 = 1; // OK:复制初始化选择 A::A(int)
18 A a2(2); // OK:直接初始化选择 A::A(int)
19 A a3 {4, 5}; // OK:直接列表初始化选择 A::A(int, int)
20 A a4 = {4, 5}; // OK:复制列表初始化选择 A::A(int, int)
21 A a5 = (A)1; // OK:显式转型
22 if (a1) ; // OK:A::operator bool()
23 bool na1 = a1; // OK:复制初始化选择 A::operator bool()
24 bool na2 = static_cast<bool>(a1); // OK:static_cast 进行直接初始化
25
26// B b1 = 1; // 错误:复制初始化不考虑 B::B(int)
27 B b2(2); // OK:直接初始化选择 B::B(int)
28 B b3 {4, 5}; // OK:直接列表初始化选择 B::B(int, int)
29// B b4 = {4, 5}; // 错误:复制列表初始化不考虑 B::B(int, int)
30 B b5 = (B)1; // OK:显式转型
31 if (b2) ; // OK:B::operator bool()
32// bool nb1 = b2; // 错误:复制初始化不考虑 B::operator bool()
33 bool nb2 = static_cast<bool>(b2); // OK:static_cast 进行直接初始化
34}
23.5. initializer_list
#include <initializer_list>
initializer_list<T>
类的对象是一个访问 const T
类型对象数组的轻量代理对象。
以下场景会发生 initializer_list 对象的自动构造:
用花括号列表初始化一个对象(列表初始化),对应的构造函数接受一个 initializer_list 参数。
以花括号列表为赋值运算符的右运算数,或作为函数调用的参数,而对应的赋值运算符/函数接受 initializer_list 参数。
绑定花括号列表到
auto
,在范围 for 循环(Range For Loop)中使用。
initializer_list 可由一对指针(分别指向列表首、尾)或指针+长度实现, 复制一个 initializer_list 不会复制其底层对象。
成员函数包括: size
begin
end
。
1#include <iostream>
2#include <vector>
3#include <initializer_list>
4using namespace std;
5
6template<typename T>
7class Vec
8{
9public:
10 vector<T> v;
11 Vec(initializer_list<T>);
12 void append(initializer_list<T>);
13 pair<const T*, size_t> c_arr() const;
14};
15
16template<typename T>
17Vec<T>::Vec(initializer_list<T> lt): v(lt){}
18
19template<typename T>
20void Vec<T>::append(initializer_list<T> lt)
21{
22 v.insert(v.end(), lt.begin(), lt.end());
23}
24
25template<typename T>
26pair<const T*, size_t> Vec<T>::c_arr() const
27{
28 return {&v[0], v.size()};
29}
30
31template<class T>
32void print(T val_list)
33{
34 for(auto& val: val_list) cout << val << "#"; // range for loop
35 cout << endl;
36}
37
38int main()
39{
40 Vec<int> va = {1,2,3}; // 拷贝初始化
41 va.append({4,5});
42
43 print(va.v); // 1#2#3#4#5#
44
45 auto p = va.c_arr();
46 cout << *p.first << " " << p.second << endl; // 1 5
47
48 // print({1,2,3,4,5}); // 编译错误,{1,2,3,4,5} 不是表达式,无类型,因此 T 无法推导
49 print<vector<int>>({1,2,3,4,5}); // 1#2#3#4#5#
50 print<initializer_list<int>>({1,2,3,4,5}); // 1#2#3#4#5#
51
52 for(auto n: {1,2,3,4,5}) cout << n << ends;
53
54 return 0;
55}
23.6. push 和 emplace
在 19 章提到了 push 和 emplace 的区别,这里用一个例子说明。
\(\color{darkgreen}{Example}\)
1#include <iostream>
2#include <utility> // std::move
3
4class Foo
5{
6public:
7 Foo(std::string str) : name(str)
8 {
9 std::cout << "constructor" << std::endl;
10 }
11
12 Foo(const Foo& f) : name(f.name)
13 {
14 std::cout << "copy constructor" << std::endl;
15 }
16
17 Foo(Foo&& f) : name(std::move(f.name))
18 {
19 std::cout << "move constructor" << std::endl;
20 }
21
22private:
23 std::string name;
24};
25
26int main(int argc, char ** argv)
27{
28 std::vector<Foo> v;
29 int count = 10000000;
30 v.reserve(count);
31
32 {
33 Foo temp("test");
34 // constructor
35 v.push_back(temp);// push_back(const T&),参数是左值引用
36 // copy constructor
37 }
38
39 v.clear();
40 {
41 Foo temp("test");
42 // constructor
43 v.push_back(std::move(temp));// push_back(T &&), 参数是右值引用
44 // move constructor
45 }
46
47 v.clear();
48 {
49 v.push_back(Foo("test"));// push_back(T &&), 参数是右值引用
50 // constructor
51 // move constructor
52 }
53
54 v.clear();
55 {
56 std::string temp = "test";
57 v.push_back(temp);// 构造临时对象,push_back(T &&), 参数是右值引用
58 // constructor
59 // move constructor
60 }
61
62 v.clear();
63 {
64 std::string temp = "test";
65 v.emplace_back(temp);// 只有一次构造函数,不调用拷贝构造函数,速度最快
66 // constructor
67 }
68
69 return 0;
70}
Note
std::move
并不会真正地移动对象(真正的移动操作是移动构造函数、移动赋值运算符等完成的), std::move
只是将参数转换为右值引用。
我们可以销毁一个移动之后的源对象(moved-from),也可以赋予它新值,但是不能使用一个移后源对象的值。
如:上例中的 temp 被移动后,就不能再取它的值来使用。
23.7. 参考资料
1.《C++ Primer 第5版 中文版》 Page 440 – 442,449,470 – 475。
C++的一大误区——深入解释直接初始化与复制初始化的区别
C++11使用emplace_back代替push_back
explicit 说明符
initializer_list