对象的生死时刻
对象是具有定制行为的变量,因此,与变量一样,对象同样也具有作用域和生存期。不同作用域和生存期的对象,其产生和消亡的时机也不同。其中,
n 局部域 + 自动变量/对象
在变量/对象的定义点产生(从栈区获得存储空间),在离开局部域时消亡。
n 局部域 + 静态变量/对象
在程序第一次执行到其定义点时产生(从静态数据区获得存储空间并进行初始化工作),此后持续存在,直到程序执行结束时消亡。
n 全局变量/对象
在程序开始执行时就产生(从静态存储区获得存储空间并进行初始化工作),此后持续存在,直到程序执行结束时消亡。
n 动态变量/对象
调用new运算符时产生(从堆区获得存储空间),调用delete运算符时消亡。
很自然,我们希望变量或者对象在产生时具有一个可预期的初始状态,在消亡时做一些善后清理工作。例如,通过赋值或者构造函数语法初始化变量,
但是,对象是我们自己定制的数据类型,C++语言不可能预先知道并约定好对象初始化和善后清理的行为,而是需要我们在定义类时自己进行约定。
为此,C++语言提供了两种特殊的成员函数——构造函数和析构函数。其中,构造函数专门用于约定对象在产生时的初始化行为,而析构函数则专门用于约定对象在消亡时的善后清理行为。
构造函数:约定对象产生的行为
构造函数(constructor),是专门用于对象初始化的成员函数,由系统在对象产生时自动调用,并且在该对象从产生到消亡的一生中就只会调用这一次。
对象初始化必定会调用构造函数。根据为对象产生提供的初始状态信息,可以将构造函数分为两类:默认构造函数和有参构造函数。有参构造函数又包括普通的有参构造函数、拷贝构造函数、以及转换构造函数。不同的应用场景需要不同的构造函数。
(1)默认构造函数:无参数,没有提供任何初始状态信息;例如,
CDate now; //调用默认构造函数
(2)普通有参构造函数:有参,提供了一些初始状态信息;例如,
CDate today(2008, 5, 1); //调用有参构造函数
(3)拷贝构造函数:单参数,提供了另一个同类对象;例如,
CDate birthday = today; //调用拷贝构造函数,或者
CDate birthday(today);
(4)转换构造函数:单参数,提供了另一个非同类对象;例如,
CDate tomorrow = "2005-11-5"; //调用转换构造函数,或者
CDate tomorrow("2005-11-5");
构造函数的定义语法比较特殊。首先,构造函数的名字就是类名,其次,构造函数无需返回类型说明。具体的语法形式如下,
类名( 形参列表 ) 《: 成员初始化列表》 { …… }
其中,成员初始化列表用于设定数据成员的初始值,是可选的。
默认构造函数:一种无参数的构造函数。例如,
CDate::CDate( ) { year=1900; month=1; day=1; }
CDate::CDate( ) : year(1900), month(1), day(1) { } //可选方式:初始化列表
对于CDate类而言,通过在构造函数体中给数据成员赋值,或者使用初始化列表进行初始化,两种方式效果相同,但本质不同。其中,
n 在构造函数的函数体中,对象实际上已经产生出来了,只是其初始状态不符合我们的要求,因此,我们通过赋值修改对象的状态。
n 在初始化列表中,对象正在产生且在产生的同时设置其初始状态。
在一些情况下,对象一旦产生,它的某些数据成员就不再允许修改,例如常量数据成员或者引用型数据成员,此时,就必须使用初始化列表的方式。
在初始化列表中,数据成员进行初始化的实际顺序,与数据成员在类体中声明的顺序是一致的,无论在初始化列表中数据成员处于什么样的顺序。
对象初始化必须有构造函数,如果我们没有给出任何构造函数,那么,C++语言会自动生成一个默认构造函数,称为合成的默认构造函数。一般情况下,语言提供的合成默认构造函数,仅负责创建对象,基本上不做什么初始化工作。如果我们自己给出了任意一个构造函数,无论是有参数的构造函数,还是无参数的默认构造函数,那么,C++语言就不再合成默认构造函数。
普通的有参构造函数
CDate::CDate(int y=1900, int m=1,int d=1): year(y), month(m), day(d) { }
此时,这个三参数构造函数的每个参数都设定了默认值。在调用的时候,
(1)如果年月日都提供,则无需默认值,直接调用三参数构造函数;
(2)如果只提供年月,则默认天数d=1,相当于两参数构造函数;
(3)如果只提供年,则默认月份m=1且天数d=1,相当于单参数构造函数;
(4)如果年月日都不提供,则默认y=1900,m=1,d=1,相当于默认构造函数。
此时在调用该构造函数时,需要遵循形式参数表中的参数顺序,从左向右逐个设置实参,默认形参必须按从右向左的次序省略,中间不能遗漏。例如,
CDate today(2008, 5, 1); //调用三参数构造函数
CDate today(2008, 5); //调用两参数构造函数
CDate today(2008); //调用单参数构造函数
CDate today; //调用默认构造函数
CDate today(2008, , 1); //错误,中间参数遗漏了
拷贝构造函数
在产生对象时,有时候是根据另一个已经存在的同类对象来指定对象的初始状态信息,类似于在另一个对象的基础上克隆或拷贝出一个新对象。例如,
CDate today(2008, 5, 1);
CDate birthday = today; //赋值语法调用拷贝构造函数,或者
//CDate birthday(today); //构造函数语法调用拷贝构造函数
此时,就需要一种具有特殊参数的有参构造函数——克隆构造函数或者拷贝构造函数(copy constructor)。C++语言支持使用赋值语法或者构造函数语法调用拷贝构造函数,例如上面产生birthday对象的代码,二者是等价的。
C++语言约定,拷贝构造函数的第一个参数必须是同类对象的引用,包括常量对象的引用和非常量对象的引用,除此之外,还可以有额外的参数,但是额外参数必须设定其默认值。因此,下面都是有效的CName类的拷贝构造函数:
CName(CName &clone_me){ ……} //或者,
CName(const CName&clone_me) { ……} //或者,
CName(CName &clone_me,int n=10) { ……}
很多情况下往往无需定义拷贝构造函数,此时,C++语言会自己合成一个默认的拷贝构造函数。默认拷贝构造函数,其默认行为是递归地为所有基类和对象数据成员调用拷贝构造函数,或者说按成员拷贝的方式(memberwise copy)进行初始化,等效于用源对象的每个数据成员来初始化目标对象的对应数据成员。
转换构造函数
如果在产生对象时提供了一个参数,而且该参数与对象的类型不同。例如:
CDate tomorrow = "2005-11-5"; //转换构造函数
这里,"2005-11-5"是字符串,不是CDate类对象,从表面上看就像是把一个字符串转换成一个CDate对象一样,此时就需要类型转换构造函数。
可以推断,我们需要如下一个单参数构造函数,参数类型是字符串,
CDate::CDate(stringdate) { //日期字符串格式yyyy-mm-dd
year = 0; month = 0; day = 0; int flag = 1;
const char* p = date.c_str(); //指针指向字符串
while(*p != '\0') {
if(*p=='-') { p++; flag++;}
switch(flag) {
case 1: year = year*10 + (*p - '0'); break;
case 2: month = month*10 + (*p-'0'); break;
case 3: day = day*10 + (*p-'0'); break;
}
p++;
}
}
在该构造函数中,我们从字符串"yyyy-mm-dd"中分别取得年份赋予year,取得月份赋予month,取得天数赋予day,构造出一个日期对象。
该构造函数只有一个参数且参数数据类型不是CDate类,称之为转换构造函数,可以将string类对象转换为CDate类的对象。此后遇到,
CDate tomorrow= "2005-11-5";
则系统会悄悄地调用这个转换构造函数,隐式地从string字符串"2005-11-5"构造产生出一个CDate类对象,然后用该对象初始化tomorrow对象。
总结一下,一个单参数构造函数,如果不是拷贝构造函数,那么它就是转换构造函数,可以将其它数据类型的对象转换为该类的对象。
显式转换构造函数
但是,有些时候我们只是想定义一个构造函数,根本没有预料到或者不希望这种背后偷偷摸摸的隐式转换行为,此时可以禁用隐式转换行为。
C++语言提供了关键字explicit,用于将转换构造函数声明为显式的,表示该转换构造函数必须光明正大地调用,即显式转换,禁止隐式转换。例如,
class CDate {
……
public: //公有成员函数
CDate(int=1900,int=1, int=1);
CDate(constCDate&);
explicitCDate(string date); //转换构造函数(显式)
……
};
注意,explicit 关键字只能在类体内修饰构造函数声明。此后,如果需要将string对象转换为CDate类对象时,就必须进行显式地类型转换。例如,
CDate tomorrow= "2005-11-5"; //错误,禁止隐式转换
CDate now =CDate("2005-11-5");//正确
转换成员函数
转换构造函数把一个其它类型的对象转换为类对象。那么,或许我们要问:能否把类对象转换为其它类型的对象呢?可以,需要类型转换成员函数。假设要将CDate对象转换为string字符串,则需要定义如下的转换成员函数,
CDate::operator string() {
ostringstreamoss; //使用字符流对象
oss<<year<<"-"<<month<<"-"<<day; //将年-月-日输出到字符流
returnoss.str; //返回字符流中的字符串
} //注意,使用字符流需要include <sstream>
转换成员函数的函数名非常特殊:operator+转换目标类型,而且它没有返回类型(因为函数名就指明了返回的目标数据类型)。即,
operator 转换的目标类型名();
CDate类有了上述的转换构造函数以及转换成员函数,就可以书写如下代码,
CDate tomorrow= "2005-11-5"; //隐式调用转换构造函数
string str = tomorrow; //隐式调用转换成员函数
cout<<str<<endl; //输出字符串str
析构函数:约定对象死亡的行为
死生有命,C++语言通过构造函数约定对象产生时的初始化行为,同样,也提供了析构函数(destructor),专门用于约定对象消亡时的善后清理行为。
析构函数与构造函数是成对匹配的。其中,构造函数的名字是类名,而析构函数的名字则是:~类名,同时,析构函数没有返回类型,无任何形参。例如,
CDate::~CDate( ) { …… }
析构函数用于约定对象消亡时的善后清理行为,因此,析构函数通常在对象生命期结束时被系统自动调用。例如,静态对象在程序结束时、局部对象出了局部域时、临时对象生命期结束时、通过delete删除动态对象时,都会自动调用析构函数。
析构函数只有一个,不允许重载。通常如果我们的构造函数没有做什么特别的事情,例如申请某种系统资源,那么,析构函数也不需要做什么事情,甚至可以不定义析构函数,此时,系统会自己合成一个默认的析构函数,其行为与合成的默认构造函数类似,即按成员析构,不过析构顺序与合成默认构造函数的构造顺序相反。

