构造函数、析构函数与赋值函数是每个类最基本的函数,在一些公司的面试中也会经常问到这方面的问题。每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类A,如果不手动编写上述函数,C++编译器将自动为类A生成四个缺省的函数:
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A& operate =(const A &a); // 缺省的赋值函数
虽然有自动生成,但是还是有必要手动写上述函数的。因为:
(1)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会,C++发明人Stroustrup的好心好意白费了。
(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
下面以类String的设计与实现为例,深入探讨这个道理。String的结构如下:
String类的普通构造函数和析构函数实现如下:
刚刚上面说,如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。以类String的两个对象a,b为例,假设a.m_data的内容为“hello”,b.m_data的内容为“world”。位拷贝拷贝的是地址,而值拷贝则拷贝的是内容。现将a赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data = a.m_data,虽然b.m_data所指向的内容会变成”hello”,但是这将造成三个错误:一是b.m_data原有的内存没被释放,造成内存泄露;二是b.m_data和a.m_data指向同一块内存,a或b任何一方变动都会影响另一方;三是在对象被析构时,m_data被释放了两次。
对于编译器,如果不主动编写拷贝函数和赋值函数,它会以“位拷贝”的方式自动生成缺省的函数。如果重写赋值函数和拷贝构造函数后,b.m_data=a.m_data,进行的是值拷贝,会将a.m_data的内容赋给b.m_data,b.m_data还是指向原来的内存区域,但是其内容改变。
有下面4个语句:
String a(“hello”);
String b(“world”);
String c = a; // 调用了拷贝构造函数,最好写成 c(a);
c = b; // 调用了赋值函数
第3语句的风格较差,宜改写成String c(a) 以区别于第4语句
下面是类String的拷贝构造函数与赋值函数
类String拷贝构造函数与普通构造函数(参见9.4节)的区别是:在函数入口处无需与NULL进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。
类String的赋值函数比构造函数复杂得多,分四步实现:
(1) 第一步,检查自赋值。
(2) 第二步,用delete释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。
(3) 第三步,分配新的内存资源,并复制字符串。
(4) 第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。注意不要将 return *this 错写成 return this.