这篇文章讲述的是C++提供的一些由编译器自动生成的函数,而这些函数,对你来说,也许是不可知的。在编程的世界中,没有比不可知更让人害怕了。
C++自动提供了以下成员函数:
1.默认构造函数:如果没有定义构造函数
2.复制构造函数:如果没有定义
3.赋值操作符:如果没有定义
4.默认析构函数:如果没有定义
5.地址操作符:如果没有定义
其中,最让人心慌的是复制构造函数。下面我们一个一个来说明:
1.默认构造函数
如果没有定义构造函数,编译器将提供ClassName::ClassName(){ }这样的空的构造函数。这样的函数也可以显式的定义。需要注意的是所有参数都有默认值的构造函数也是默认构造函数。而默认构造函数只能有一个,也就是说不能出现:
ClassName::ClassName(){ }
ClassName::ClassName(int n=0){ }
这样的定义。
2.复制构造函数
复制构造函数其实经常出现在我们的程序中,比如说Pound p=12.5。上一篇文章说过的,这里的实现流程是首先,将12.5转换成一个Pound类,然后将其复制给p。注意,这里跟后面提到的赋值操作符一样,因为这里的场景是对p进行初始化。如果是Pound p; p=12.5。这样就跟赋值操作符有关了。
其实除了上面说到的这种形式,还有另外一些形式会自动生成复制构造函数,如:
Pound p1;
Pound p2(p1);
Pound p2=p1;
Pound p2=Pound(p1);
Pound *pp2=new Pound(p1)
当然,程序生成对象副本的任何时候,都会使用复制构造函数,比如说函数参数的值传递。
有人问,这种自动的复制构造函数有什么危害啊?怎么我都看不出来?
危害大了。它在于,编译器自动生成的复制构造函数,只会对类成员进行逐个复制。
又有人问,对类成员进行逐个复制,不就满足要求了吗?
非也。大家如果还记得Java里面的深复制跟浅复制,现在可能猜出一二了。
比如说,有一个类StringTest,里面的一个类成员是char *str,另外一个类成员是length,也就是说,这个类用来封装一个字符串数组,但是并不是使用数组,而是字符指针,区别就在这里了。StringTest有一个以字符指针为形参的构造函数,函数中使用str = new char[length+1]来创建字符数组。也有一个析构函数,析构函数中使用delete [] str来释放字符数组。
现在定义一个StringTest 的对象string1,用strcpy函数将string.str赋值成"test"。当然length也要设置好。
现在有一个函数Test(StringTest t){}。注意,不是传引用,而是传值哦。以string1为实参调用Test,即Test(string1) 函数啥也不做。调用这个函数之前,我们打印string1.str,屏幕显示"test"。这是正确的。但是调用完这个函数之后,你再打印string1.str。你会发现一切已经跟你想的不一样了。屏幕上输入的并不是test,而是无法识别的乱码!!
问题出在哪里呢?!
让我们一起回到案发现场:Test(StringTest t)。肯定是调用函数出问题了。将string1传入函数Test,这是值传递,因此将会复制string1到t,t是一个临时对象。这时调用的就是复制构造函数,他将逐个复制类成员变量。本来呢,这是完全没有问题,可是注意,我们的str是一个指针,复制的时候,也只是会复制一个指针到t,而字符串"test"呢?在内存中仍然是只有一个! OK,等到函数结束,它将自动调用StringTest类的析构函数来处理t,于是乎delete [] str,删除t中的字符数组,结果连原来的string1的字符数组都删掉了(因为他们本来就是同一个)
乖乖。这就是浅复制了。而原本应该进行的深复制,即对整个字符数组进行复制。这样就不会影响到原来的string1了。
如何改正呢?
当然是显式地定义一个复制构造函数了,形如:
ClassName ::ClassName(const Class _name&)
在显式复制构造函数的定义中,我们进行字符数组的生成和复制的处理,这样就保证了析构一些临时对象的时候,不会影响到原有对象!