第九章 类的构造函数、析构函数与赋值函数
类对象之间的赋值只是对数据成员赋值。
每个类只有一个析构函数和一个赋值函数,但可有多个构造函数(包含一个拷贝构造函数,其他的称为普通构造函数)。这几个函数都不被继承。对任意一个类A,若不想编写上述函数,C++编译器将自动为A产生四个缺省的函数。有缺省的,为什么还要程序员编写:
1)若使用“缺省的无参构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会。
2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,若类中含有指针变量,这两个函数将出错。
class String
{
public:
String(const char *str = NULL); //普通构造函数
String(const String &other); //拷贝构造函数
~String(void); //析构函数
String & operator =(const String &other); //赋值函数
private:
char *m_data; //用于保存字符串
}
9.1构造函数的初始化表
构造函数特殊在其初始化方式(初始化表和函数体内赋值两种方式)和执行时间。
构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数后,却在函数体{}前,说明该表里的初始化工作发生在函数体内的任何代码被执行之前。构造函数的初始化表使用规则:
1) 若类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。如:
B::B(int x, int y) : A(x) {……}//初始化表里调用A的构造函数。若基类含无参构造函数,则不必在派生类的初始化表里调用基类构造函数,但构造派生类时会调用基类的无参构造函数。所以最好还是写上。
2)类的const常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化。
3)类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。非内部数据类型的成员对象应采用第一种方式初始化,以获取更高的效率。对内部数据类型,两者几乎无差别。
B::B(const A &a) : m_a(a){…}类B的构造函数在其初始化表里调用类A的拷贝构造函数,将成员对象m_a初始化。B::B(const A &a){m_a=a;}先创建m_a对象,再调用A的赋值函数,将参数a赋给m_a。
9.2构造和析构的次序
构造从类层次的最根处开始,在每层中,先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序进行。
成员对象的初始化次序不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。
9.3示例:类String的构造函数与析构函数
//String的普通构造函数
String::String(cosnt char *str)
{
if (NULL == str)
{
m_data = new char[1];
*m_data = ‘\0’;
}
else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}
//String的析构函数
String::~String(void)
{
delete [] m_data;//因m_data是内部数据类型,也可写成delete m_data。
}
9.4不要轻视拷贝构造函数与赋值函数
1)不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。若类中有指针变量,那么这两个缺省的函数就隐含了错误。“位拷贝”又称“浅拷贝”,拷贝的是地址;而“值拷贝”又称“深拷贝”,拷贝的是内容。以类String的两个对象a,b为例,假设a.m_data的内容为“hello”,b.m_data的内容为“world”。
将a赋值给b,若缺省赋值函数,则执行的是“位拷贝”:b.m_data = a.m_data。这将造成三个错误:
Ø b.m_data原有的内存没被释放,造成内存泄露。
Ø b.m_data和a.m_data执行同一内存,a或b任一方改变都会影响第三方。
Ø 在对象被析构时,m_data被释放两次。
int *p = new int;
int *q=p; ---------浅拷贝
int *p = new int;
int *q = new int;
*p = *q; --------深拷贝
2)拷贝构造函数和赋值函数易混淆。区分:拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。试着区分以下语句:
String a(“Hello”); String b(“World”);
String c = a; //调用了拷贝构造函数,最后写成c(a);
C = b; //调用了赋值函数。
9.5示例:类String的拷贝构造函数与赋值函数
//拷贝构造函数
String::String(const String &other)
{
//允许操作other的私有成员
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}
//赋值函数
String &String::operator =(const String &other)
{
//1)检查自赋值
if (this==&other)
return *this;
//2)释放原有的内存资源
delete []m_data;
//3)分配新的内存资源,并复制内容
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
//4)返回本对象的引用
return *this;
}
1)第一步,检查自赋值。有人可能会写出间接赋值语句,如:
//内容自赋值 //地址自赋值
b = a; b = &a;
…… ……
c = b; a = *b;
……
a = c;
为何要避免出现自赋值,因第二部要delete,自杀后还能复制自己吗?为何要delete,怕原来分配的内存不够用,第三步分配足够内存。注意不要将检查自赋值的if语句if(this==&ohter)写成if(*this==other)。
2)第二步,释放原内存,若不释放,将会造成内存泄露。
3)第三步,分配新内存并分配字符串。注意strlen返回的是有效字符串长度,不包含’\0’。
4)第四步,返回本对象的引用,目的是为了实现像a=b=c这样的链式表达式。注意不要将return *this写成return this。
9.6偷懒的办法处理拷贝构造函数与赋值函数
若不想编写拷贝构造函数与赋值函数,又不允许别人使用编译器生成的缺省函数,偷懒的方法是:将构造函数与赋值函数声明为私有函数,不用编写代码。若有人试图调用这俩函数,编译器将指出错误。
9.7如何在派生类中实现类的基本函数(即构造、析构、赋值函数)
基类的构造、析构和赋值函数不被继承。若类之间存在继承关系,如何编写这些函数:
Ø 派生类的构造函数应在其初始化表里调用基类的构造函数。
Ø 基类的析构函数应为virtual,即覆盖,否则会被隐藏。最好将派生类析构函数也设为virtual,以便后续的再继承。
Ø 在编写派生类的赋值函数时,要对基类的数据成员重新赋值。