一、构造函数的缘由
本文我们主要来讲解c++中类的构造函数,其中涉及了深拷贝和浅拷贝的问题,这也是在面试笔试中经常会碰到的问题。如果您是第一次听说构造函数,可能会觉得这个名字有点高大上,而它却和实际中的工程问题有关。在正式的讲解前,我们先来思考一个问题:(注意,本文采用的课件来源于狄泰软件学院,感谢狄泰的唐老师!)
1 #include <stdio.h> 2 3 class Test 4 { 5 private: 6 int i; 7 int j; 8 public: 9 int getI() { return i; } 10 int getJ() { return j; } 11 }; 12 13 Test gt; // 全局变量放在bss段 14 15 int main() 16 { 17 // 访问全局变量gt的的成员变量值 18 printf("gt.i = %d ", gt.getI()); 19 printf("gt.j = %d ", gt.getJ()); 20 21 Test t1; 22 // 访问局部变量t1的的成员变量值 23 printf("t1.i = %d ", t1.getI()); 24 printf("t1.j = %d ", t1.getJ()); 25 26 Test* pt = new Test; 27 // 访问堆空间pt的的成员变量值 28 printf("pt->i = %d ", pt->getI()); 29 printf("pt->j = %d ", pt->getJ()); 30 31 delete pt; // 注意释放堆空间 32 33 return 0; 34 }
通过这个简单的例子,我们发现,同样是声明一个类的对象,因为对象所在的存储空间不同(bss段、堆空间、栈),导致其对象的成员变量的初始值不相同。对于我们类的使用者来说,我们当然不希望出现这种情况。于是,我们希望在定义一个类的对象的同时,初始化其成员变量的值,统一化,不管是在bss段,堆空间、栈上。
因此,我们可以提供下面的解决方案:
这样的方式虽然可以解决我们之前遇到的问题,但是在实际的使用过程中会觉得不好用。
为此,c++的设计者想出了一个办法,即构造函数:
注意,构造函数必须满足: 1. 与类的名字相同 2.无返回值
二、有参构造函数和重载构造函数
注意这里的初始化方式,即“ ()“” 和“ = ”。
在创建一个对象数组的时候,我们可以手工调用构造函数来初始化该对象数组。
1 #include <stdio.h> 2 3 class Test 4 { 5 private: 6 int m_value; 7 public: 8 Test() 9 { 10 printf("Test() "); 11 12 m_value = 0; 13 } 14 Test(int v) 15 { 16 printf("Test(int v), v = %d ", v); 17 18 m_value = v; 19 } 20 int getValue() 21 { 22 return m_value; 23 } 24 }; 25 26 int main() 27 { 28 Test ta[3] = {Test(), Test(1), Test(2)}; // 手工调用构造函数 29 30 for(int i=0; i<3; i++) 31 { 32 printf("ta[%d].getValue() = %d ", i , ta[i].getValue()); 33 } 34 35 // int i(100); // 这样的方式是初始化i 36 Test t = Test(100); // 初始化方式 37 38 printf("t.getValue() = %d ", t.getValue()); 39 40 return 0; 41 }
三、无参构造函数和拷贝构造函数
下面来介绍两个特殊的构造函数:
注意:没有构造函数是指连拷贝构造函数都没有。
3.2 深拷贝和浅拷贝
浅拷贝只是简单的成员变量复制。我们可以看下面的例子:
1 #include <stdio.h> 2 3 class Test 4 { 5 private: 6 int i; 7 int j; 8 int* p; 9 public: 10 int getI() 11 { 12 return i; 13 } 14 int getJ() 15 { 16 return j; 17 } 18 int* getP() 19 { 20 return p; 21 } 22 Test(const Test& t) 23 { 24 i = t.i; 25 j = t.j; 26 27 /* 这里是浅拷贝,p和t.p所指向的内存空间相同 28 */ 29 // p = t.p ; 30 31 /* 注意这里是深拷贝,与浅拷贝不同的是,重新在堆空间申请内存空间, 32 * 并将该空间的值和t.p的相同,这样就可以使得对象的逻辑状态相同 33 */ 34 p = new int; 35 *p = *t.p; 36 } 37 Test(int v) 38 { 39 i = 1; 40 j = 2; 41 p = new int; 42 *p = v; 43 } 44 void free() 45 { 46 delete p; 47 } 48 }; 49 50 int main() 51 { 52 Test t1(3); 53 Test t2(t1); 54 55 printf("t1.i = %d, t1.j = %d, *t1.p = %d ", t1.getI(), t1.getJ(), *t1.getP()); 56 printf("t2.i = %d, t2.j = %d, *t2.p = %d ", t2.getI(), t2.getJ(), *t2.getP()); 57 58 t1.free(); // 释放两次,如果是浅拷贝会出问题,而深拷贝不会 59 t2.free(); 60 61 return 0; 62 }
既然我们已经了解了深拷贝和浅拷贝的区别,那么什么时候我们该使用深拷贝呢?
(即申请堆空间、打开文件、网络端口等操作)
四、初始化列表
在正式讲解初始化列表前,我们先来思考一个问题:
实际上,我们会发现这个类并没有给成员变量ci一个初始值,所以会出错。那么我们怎么样为const 成员变量初始化一个值呢?这里就引入了初始化列表的概念。如下:
可以看到初始化列表是在构造函数函数名之后加上一个冒号,之间用逗号隔开,m1、m2、m3是代表类的成员变量,括号里面的是对应的初始值。需要注意的是:
1 #include <stdio.h> 2 3 class Value 4 { 5 private: 6 int mi; 7 public: 8 Value(int i) 9 { 10 printf("i = %d ", i); 11 mi = i; 12 } 13 int getI() 14 { 15 return mi; 16 } 17 }; 18 19 class Test 20 { 21 private: 22 // 成员变量的初始化顺序和成员变量的声明顺序有关,而与初始化列表中的位置无关 23 Value m2; 24 Value m3; 25 Value m1; 26 public: 27 Test() : m1(1), m2(2), m3(3) 28 { 29 //由于初始化列表先于构造函数执行,所以这一句代码最后执行 30 printf("Test::Test() "); 31 } 32 }; 33 34 35 int main() 36 { 37 Test t; 38 39 return 0; 40 }
我们再回到之前的那个问题:
一个小问题:
(注意,本文采用的课件来源于狄泰软件学院,感谢狄泰的唐老师!)