构造函数与析构函数的起源
把对象的初始化工作放在构造函数中,把清除工作放在析构函数中。当对象被创建时,构造函数被自动执行。当对象消亡时,析构函数被自动执行。这下就不用担心忘了对象的初始化和清除工作。
不要在构造函数内做与初始化对象无关的工作,不要在析构函数内做与销毁一个对象无关的工作。
为什么需要构造函数和析构函数
初始化就是在对象创建的同时使用初值直接填充对象的内存单元,因此不会有数据类型转换等中间过程,也就不会产生任何临时对象;而赋值则是在对象创建好后,任何时候都可以调用的而且可以多次调用的函数,由于它调用的是“=”运算符,因此可能需要进行类型转换,即会产生临时对象。
构造函数的作用是:当对象的内存分配好后把它从原始状态变为良好状态。构造函数的另一个作用是,给一些可能存在的隐含成员如vptr创造一个初始化的机会,否则虚拟机制无法实现。
最好为每个类显示地定义构造函数和析构函数,即时它们暂时空着,尤其是当类含有指针成员或引用成员地时候。
构造函数的初始化表
在构造函数体内来初始化数据成员,然后这不是真正意义上的初始化,而是赋值。
真正的初始化是所谓的“初始化表达式表”进行的。初始化列表位于构造函数之后,在函数体{}之前,该列表里的初始化工作发生在函数体内的任何代码被指行之前。
构造函数初始化表的使用规则:
如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
类的const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化
类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。
对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别。
成员真正的初始化顺序并不一定于你的初始化列表顺序一致,而是与它们在类中的声明顺序来初始化的。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。
构造和析构的次序
构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
构造函数和赋值函数的重载
构造函数分三种:默认构造函数,拷贝构造函数和其他带参数的构造函数
默认的构造函数:要么没有参数,要么所有函数都有默认值
拷贝构造函数:第一个函数为本类对象的引用、const引用、volatile引用或const volatile引用,并且没有其他参数,或者其他参数有默认值。
class A
{
public:
A(A copy){..} //错误
A(const A& other){...} //正确
}
因为这个拷贝构造函数在参数传递的过程中又要调用拷贝构造函数本身,因为它是“值”传递,而调用拷贝构造函数时又需要进行参数传递,参数传递又要调用拷贝构造函数。。于是死循环知道堆栈溢出。
类的赋值函数operator=()也时一种拷贝构造函数,如果赋值函数的参数类型就是当前类,那么就是类的拷贝赋值函数。
示例:类String 的构造函数与析构函数
|
// String 的普通构造函数
String::String(const char *str) { if(str==NULL) { 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;
}
|
何时应该定义拷贝构造函数和拷贝赋值函数,如果不主动编写拷贝构造函数和赋值函数,编译器将以“按成员拷贝”的方式自动生成缺省的函数。倘若类中含有指
针变,那么这两个缺省的函数就隐含了错误。
拷贝构造函数是在对象被创建并用另一个已经存在的对象来初始化它时调用的,而赋值函数只能把一个对象赋值给另一个已经存在了的对象,使得已经存在的对象具有和源对象相同的状态。
示例:类String 的拷贝构造函数与赋值函数
|
// 拷贝构造函数
String::String(const String &other) { // 允许操作other 的私有成员m_data
int length = strlen(other.m_data); m_data = new char[length+1]; strcpy(m_data, other.m_data); } // 赋值函数
String & String::operate =(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; }
|
类String 的赋值函数比构造函数复杂得多,分四步实现:
(1)第一步,检查自赋值。
(2)第二步,用delete 释放原有的内存资源。
(3)第三步,分配新的内存资源,并复制字符串。
(4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。
如何在派生类中实现类的基本函数
派生类的构造函数应在其初始化表里调用基类的构造函数。
基类与派生类的析构函数应该为虚(即加virtual 关键字)
在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。