一. 概述
复习巩固学习过的知识C++拷贝构造器。
环境:Centos7 64位,g++ 4.8.5
二. 代码与验证
1. 构造与拷贝构造
拷贝构造器(copy constructor)的地位与构造器(constructor)的地位是一样的,都是由无到有的创建过程。拷贝构造器,是由同类对象创建新对象的过程。
通过下面的代码验证几种情况。类A中自实现了构造器,拷贝构造器,析构器。
第28行、第32行代码调用 了构造函数,第29行代码调用了拷贝构造函数,这3行代码比较好理解。
第30行,调用了拷贝构造函数,一时有点不好理解,感觉有点像是调用了赋值运算符函数。但是通过运行结果,可以看到它确实是调用了拷贝构造函数。
可以再回顾一下上面的这句话“由同类对象创建新对象”,可能会更好地帮助理解。
1 #include <iostream>
2
3 using namespace std;
4
5 class A
6 {
7 public:
8 A()
9 {
10 cout<<"constructor A()"<<endl;
11 }
12
13 A(const A &another)
14 {
15 cout<<"A(const A &another)"<<endl;
16 }
17
18 ~A()
19 {
20 cout<<"~A()"<<endl;
21 }
22 protected:
23 int m_a;
24 };
25
26 int main()
27 {
28 A a1; // constructor 构造
29 A a2(a1); // copy constructor 拷贝构造
30 A a3 = a1; // copy constructor 拷贝构造
31
32 A a4; // constructor 构造
33 a4 = a1; // assign
34
35 return 0;
36 }
运行结果如下:
再结合下面代码以进一步理解一下
1 int a = 0; // 初始化
2 a = 10; // 赋值
3 int b = a; // 初始化
2. 验证,不自实现拷贝构造器时会发生什么
注释掉类A中的自实现的拷贝构造函数第15行--第18行代码。
通过运行结果,可看到对象a1与a2调用dis()方法,两者打印结果一致,说明第40行代码的确是执行了拷贝构造。
注:此时是有系统提供的默认的拷贝构造器。
1 #include <iostream>
2
3 using namespace std;
4
5 class A
6 {
7 public:
8 A(int x = 10)
9 :m_a(x)
10 {
11 cout<<"constructor A()"<<endl;
12 }
13
14 /*
15 A(const A &another)
16 {
17 cout<<"A(const A &another)"<<endl;
18 }
19 */
20
21 ~A()
22 {
23 cout<<"~A()"<<endl;
24 }
25
26 void dis()
27 {
28 cout<<"m_a: "<<m_a<<endl;
29 }
30 protected:
31 int m_a;
32 };
33
34 int main()
35 {
36 A a1(42);
37 a1.dis();
38
39 cout<<"----------"<<endl;
40 A a2(a1);
41 a2.dis();
42
43 return 0;
44 }
3. 说明
1)系统提供了默认的拷贝构造器,拷贝的格式比较固定,一经自实现,默认的将不复存在;
2)此拷贝构造器不是空的,而是提供了一个等位拷贝机制。等位拷贝不包含成员函数;
3)系统提供的拷贝构造函数,是一种浅拷贝,shallow copy;
4)深拷贝,deep copy。如果对象中不含有堆上的空间(指针指向的堆上的空间),此时浅拷贝可以满足需求,不需要自实现。但如果对象中含有堆上的空间,此时浅拷贝不能满足需求,就需要自实现了(申请内存空间后再进行拷贝)。因为浅拷贝会带来重析构(double free)的问题。
拷贝构造格式,如下,another可以写成自己习惯的名称。
注:同类对象方法间,进行传参,可以访问其私有成员,其它则不行(同类间无私处,异类间有友元----老司机总结的结论)。
1 A(const A &another)
2 {
3 m_a = another.m_a;
4 }
4. 关于重析构double free的验证
上面第4)点,通过以下代码验证一下。类A中自实现了构造器和析构器。拷贝构造函数保持系统默认。构造函数中,给成员变量m_a申请了内存空间,并向其拷贝了字符串。类型最好转换下,不过这样也通过了编译。先不改了。
从运行结果可以看到double free的报错,报错的其它内容都看不太懂,先不管。
1 #include <iostream>
2 #include <cstring>
3
4 using namespace std;
5
6 class A
7 {
8 public:
9 A()
10 {
11 m_a = new char[100];
12 strcpy(m_a, "C++ is the intersting language.");
13 cout<<"constructor A()"<<endl;
14 }
15
16 ~A()
17 {
18 delete []m_a;
19 }
20
21 void dis()
22 {
23 cout<<"m_a: "<<m_a<<endl;
24 }
25 protected:
26 char *m_a;
27 };
28
29 int main()
30 {
31 A a1;
32 a1.dis();
33
34 cout<<"----------"<<endl;
35 A a2(a1);
36 a2.dis();
37
38 return 0;
39 }
说明
对象a1、a2中的m_a指向了同一块堆空间,对象销毁,析构的时候析构了两次,所以报错double free。下面通过代码再验证一下。
5. 再次验证重析构
类A中增加setStr()方法,25-28行代码。第47行代码,对象a1调用setStr()方法修改对象a1中的成员变量m_a。第48行代码,a2调用dis()方法打印m_a。第50行增加了一个死循环,暂不让析构函数执行。
1 class A
2 {
3 public:
4 A()
5 {
6 m_a = new char[100];
7 strcpy(m_a, "C++ is the intersting language.");
8 cout<<"constructor A()"<<endl;
9 }
10
11 /*
12 A(const A &another)
13 {
14 m_a = new char[strlen(another.m_a)+1];
15 strcpy(m_a, another.m_a);
16 cout<<"A(const A &another)"<<endl;
17 }
18 */
19
20 ~A()
21 {
22 delete []m_a;
23 }
24
25 void setStr()
26 {
27 strcpy(m_a, "PHP is a intersting language.");
28 }
29
30 void dis()
31 {
32 cout<<"m_a: "<<m_a<<endl;
33 }
34 protected:
35 char *m_a;
36 };
37
38 int main()
39 {
40 A a1;
41 a1.dis();
42
43 cout<<"----------"<<endl;
44 A a2(a1);
45 a2.dis();
46
47 a1.setStr(); // modify m_a
48 a2.dis();
49
50 while(1);
51
52 return 0;
53 }
运行结果如下:
对象 a2中的m_a居然也被修改了。佐证了"对象a1、a2中的m_a指向了同一块堆空间"。拷贝构造,只是将a1中m_a的地址拷贝给了a2中的m_a,但两者均指向同一块堆空间。
6. 深拷贝。对于含有堆上的空间,自实现拷贝构造
将上面代码块的第12到17行代码去掉注释,
1 A(const A &another)
2 {
3 m_a = new char[strlen(another.m_a)+1];
4 strcpy(m_a, another.m_a);
5 cout<<"A(const A &another)"<<endl;
6 }
执行结果如下
a2.dis(),打印是对象a2的成员变量m_a指向的重新申请的内存空间的内容。
此时,上面代码块注释掉第50行死循环的代码,运行也不会报错了。
现在,对象a1与a2中的m_a是不同的地址,并且它们指向不同的堆空间,但是它们的内容是一样的。调用析构函数时,分别free不同的m_a指向的各自的堆空间,也就不会有double free的问题了。
下面这张图可以帮助理解。这里的m_a其实就是图中的_str。
7. 拷贝构造器的适用场景
主要是用于传参和返回。
以下简单地说一下传参的问题
在上面代码块中,添加一个全局函数
1 void foo(A aa)
2 {
3 cout<<"void foo(A aa)"<<endl;
4 }
在main函数中,调用此全局函数时
1 int main()
2 {
3 A a1;
4 a1.dis();
5
6 cout<<"----------"<<endl;
7 foo(a1);
8
9 return 0;
10 }
从结果可看到,在传参时,调用了拷贝构造函数。也就是,第7行代码,在调用foo(a1)函数时,将对象a1拷贝给了函数foo形参aa。这种传参方式,调用了拷贝构造函数。
如果我们换成引用试试,即将全局函数的形参修改为A &aa,即为void foo(A &aa)。运行结果如下:
此时,没有调用拷贝构造函数。传引用,其实也就是在传递对象本身,也不会涉及到拷贝的问题了。在这种情形下,传引用比传对象开销要相对小点。
暂时就啰嗦这么多了。