定义
拷贝构造函数(copy constructor),又称复制构造函数,是在基于同一个类的其他对象进行构造的时候(赋值初始化、参数传递、返回值)被编译器调用的一种特殊构造函数。假设有一个类类型为T,拷贝构造函数的第一个形参必须是该类类型的引用,(即T&)也可以用cv限定符来修饰(const T&、volatile T&或const volatile T&),也可以有其他形参,但其他形参必须都有默认值。拷贝构造函数的声明语法如下:
1
2
3
4 1T(const T&);
2T(const T&) = default; //C++11
3T(const T&) = delete; //C++11
4
前面提到过,有三种情况会调用拷贝构造函数:赋值初始化、参数传递和作为函数返回值时。对于最后一种情况,若类存在移动构造函数,则可能不会调用拷贝构造函数。
有意思的是,对于类中的私有成员,不能在外部通过类对象直接调用,而在拷贝构造函数中却可以。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 1#include <iostream>
2
3class A
4{
5public:
6 A() : mem(0) {}
7 A(const A& a) : mem(a.mem) { std::cout << "copy constructor of class A has been called\n"; }
8 int getMem() const { return mem; }
9private:
10 int mem;
11};
12
13int main(int argc, char *argv[])
14{
15 A a1;
16 A a2 = a1;
17 std::cout << a2.getMem() << std::endl;
18 return 0;
19}
20
如上面的代码所示,类A的数据成员mem是私有的,所以在main函数中,不能直接通过类对象打印mem的值(即a.mem),但复制构造函数中却可以通过直接调用同一个类的其他对象的数据成员来完成拷贝构造(如下图所示)。
隐式拷贝构造函数
当一个类没有显式定义任何拷贝构造函数时,编译器总会自动生成一个拷贝构造函数,且是内联的,访问属性为public,这个拷贝构造函数就叫隐式拷贝构造函数(implicit copy constructor)。隐式拷贝构造函数的第一个形参的类型有两种可能,一种是const T&,另一种是T&。同时满足下列条件的隐式拷贝构造函数的第一个形参是第一种(除此之外则是第二种):
(1)类T的每个直接基类和虚基类都有拷贝构造函数,且其第一个形参有const或const volatile修饰
(2)类T的每个类类型或类类型数组的非静态数据成员都有拷贝构造函数,且其第一个形参有const或const volatile修饰
因为这个规则的存在,隐式拷贝构造函数不能被绑定到有volatile修饰的左值实参。
如果隐式声明的拷贝构造函数未被弃置,则当其被ODR式(One Definition Rule,单一定义规则)使用时,编译器就会自动生成隐式拷贝构造函数。对于共用体类型(union),隐式拷贝构造函数会像std::memmove一样直接复制其对象的值;而对于非共用体类型(class、struct),则会进行直接初始化,按照初始化顺序,对对象的各基类和非静态数据成员进行复制。
弃置的隐式拷贝构造函数
除了使用delete关键字显式定义拷贝构造函数为弃置的以外,编译器在同时满足以下条件的类中也会自动生成弃置的隐式构造函数(C++11前则不会生成):
(1)类T拥有无法复制的非静态非静态数据成员(数据成员拥有被弃置、不可访问或有歧义的拷贝构造函数)
(2)类T拥有无法复制的直接基类或虚基类(这些基类拥有被弃置、不可访问或有歧义的拷贝构造函数)
(3)类T是共用体union,且其中一个成员拥有非平凡拷贝构造函数(C++11)
(4)类T拥有右值引用类型的数据成员(C++11)
(5)类T拥有用户定义的移动构造函数或移动赋值运算符(C++11)
上述规则中,第1点和第2点是为了保证类类型的数据成员和继承体系中的基类能够被构造出来;第5点与编译器对拷贝构造函数和移动构造函数的选择相关,且此条件不会使被default关键字强制要求定义的拷贝构造函数被弃置。
平凡和非平凡的拷贝构造函数
同时满足下列条件的拷贝构造函数是平凡的:
(1)必须是隐式拷贝构造函数(即非用户定义的)
(2)类T没有虚成员函数,也没有虚基类
(3)为类T的每个直接基类选择的拷贝构造函数都是平凡的
(4)为类T的每个类类型(或类类型数组)的非静态数据成员选择的拷贝构造函数都是平凡的
不同于默认构造函数,对于非共用体类型的类,其平凡的拷贝构造函数会递归复制其子对象(即对类类型的数据成员调用他们的平凡拷贝构造函数进行递归复制,如代码所示),除此之外没有其他行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 1#include <iostream>
2
3struct A
4{
5 A()
6 {
7 std::cout << "default constructor of class A has been called\n";
8 pa = new int(3);
9 }
10 int *pa;
11};
12
13int main(int argc, char *argv[])
14{
15 A a1;
16 A a2 = a1;
17 std::cout << "a1: " << a1.pa << std::endl;
18 std::cout << "a2: " << a2.pa << std::endl;
19 return 0;
20}
21
平凡拷贝构造函数是逐位复制(bitwise copy)的,行为就像std::memmove一样,也叫浅拷贝。这种拷贝方式在类存在指针类型的数据成员或者有显式定义的析构函数时,往往会出现问题。就像上面的代码一样,a1和a2的pa指向同一个地址,说明浅拷贝只是拷贝了指针pa的值,而非拷贝指针pa指向的值,这样在析构时会重复释放同一块内存。为了让类按照我们的预期进行复制,我们需要显式定义拷贝构造函数:
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 1#include <iostream>
2
3struct A
4{
5 A()
6 {
7 std::cout << "default constructor of class A has been called\n";
8 pa = new int(3);
9 }
10 A(const A& a)
11 {
12 std::cout << "copy constructor of class A has been called\n";
13 pa = new int(*(a.pa));
14 }
15 int *pa;
16};
17
18int main(int argc, char *argv[])
19{
20 A a1;
21 A a2 = a1;
22 std::cout << "a1: " << a1.pa << std::endl;
23 std::cout << "a2: " << a2.pa << std::endl;
24 return 0;
25}
26