}
};
voidmain()
{
Countct;
ct.init(10000);
ct.account_chk();
ct.account_chk();
ct.account_chk();
}
例4设想在一个银行中用作取款操作(为节省篇幅而有意取捎了账号查验函数)。
要强调的是为了使用户数据不被类外程序伤害,连同修改函数都放在了private区内定义。
这样使用时必须通过接口函数account_chk()才能修改,同时也只有这一种流程。
§1.3类中成员函数的一些特性。
若注意前面的几个例子后会发现定义在class中的成员函数的声明与定义是在一起的。
根据前一章的论述,本应将其声明与定义分离,这里何以又集中了呢?
根本原因在于若在类中将声明与定义合二为一,则成员函数便被编译器自动地认定为内联函数(inlinefunction)。
如果分离,则要在函数的前面插入“inline”关继字才能成为内联函数。
一般地声明与定义合二为一的函数都比较短小,结构也很简单(通常不包含循环),反之就应当分离。
由于C++语言中每个类成员函数都是有其所属的类作用域范围的,声明与定义分开后则必须在类外再对成员函数进行定义。
此时要使用一个称为类作用域算符“∷”的特殊符号来表明函数的归属。
则类成员函数在类外的通用定义格式为:
返回值类型类名∷函数名(参数表){过程语句}
如:
classA
{
…
public:
intinit(int);
…
};
intA:
:
init(inti){…}
若要定义函数为内联的话,可前缀“inline”关键字,则上一行可写成:
inlineintA:
:
init(inti){…}
实际上,类作用域算符不仅仅用于对类成员函数(包括成员数据)的定义,还可以用于struct和非类属的全局程序作用域中。
若在全局(Global)数据区中定义了与在某函数的局部(Local)数据区中相同的变量名,则程序以局部优先的原则总是先访问当前函数内的局部变量。
而在C++语言中,不同位置的程序可以通过类作用域算符来访问不同作用域的变量(当然首先应有权访问)。
当程序要访问全局数据区中的变量时,只要在变量名前缀以一个“∷”符号即可。
例5:
#include
staticinti=10;
voidmain()
{
inti;
for(i=0;i<:
:
i;i++)printf(“i=%d\n”,i);
printf(“Gloabli=%d\n”,:
:
i);
}
最后要强调的是“class{…};”全式最后的分号切不可丢失,以免把下一行的内容当作该类的对象处理。
这种疏忽往往要花费许多时间才能查出。
§2构造函数、析构函数和类对象成员数据的初始化
类对象在生成时必须进行对象成员数据的初始化,但这种过程的处理并非易事。
如程序员如何能够将对象初始数据便捷地传递给类对象?
常数、引用、静态成员数据如何初始化?
C++语言对对象成员数据的初始化过程提供了那些功能?
这就是本节要讨论的构造函数、析构函数和成员初始表将涉及的主要问题。
§2.1构造函数(constructorfunction)
若仔细观察前面所举的例子,便会发现各例中定义的成员函数名都与其class名不相同。
如果在class中出现了一个与自身类名相同的成员函数则成为一个具有特殊意义的函数——构造函数。
构造函数除了具有普通成员函数的所有特性外还有下面的几个特殊点:
①构造函数名必须和类名相同,且不得声明任何返回值类型。
例6:
classStudent
{
…
public:
Student(){…}/*不得声明任何返回值类型*/
};
②构造函数可以用形参形式带进各成员数据的初值,也可以重载出多个构造函数。
其中不带任何参数的构造函数又称为缺省构造函数。
当定义不带有任何参数的对象时则会引用缺省构造函数。
若程序员没有特别的需要,可以不必显性的编制出缺省构造函数而由C++编译器将其自动插入到类内。
无论存在多少种重载的构造函数体,构造函数都具有一个特殊功能,这就是测算本类对象所需的静态内存容量并动态占用该容量的内存资源。
因此,当一个类对象生成时,该类的某个构造函数必然被引用。
③构造函数并非没有返回值,它所返回的是一个指向其所定义对象的首地址的指针,但不能被传递。
④若直接引用声明于public区中的构造函数将导致该类的一个新对象的生成,但若没有相关的句柄与之联系,则无法与对象进行通信联系。
⑤声明于private区中的构造函数只能经由定义在public区中的函数引用。
§2.2构造函数的引用
一.缺省构造函数的引用
对缺省构造函数引用实际上是在用类定义无参数的对象时自动发生的。
例7:
#include
classDemo
{
inti;
public:
Demo(){i=0;cout<
};
voidmain()
{Demoh;}
例7中classDemo定义对象h的同时自动引用缺省构造函数Demo()。
不过为了明确起见,定义对象的格式也可写成“Demoh=Demo();”但不能写成“Demoh();”因为C++编译器会认为h()是Demo的一个对象。
二.非缺省构造函数的引用
对于非缺省构造函数的引用要在定义该类对象时通过所给的参数来指明要引用的具体构造函数。
例8:
#include
classDemo
{
inti;
public:
Demo(intk){i=k;cout<
};
voidmain()
{Demoh(0);}/*或者写成:
Demoh=4;*/
例8中h不是函数而是对象名,括号所包括的4则表示引用构造函数“Demo(intk)”。
§2.3析构函数(DeconstructorFunction)
一个类中的构造函数被引用后要负责为新对象向操作系统申请内存空间。
当引用该类对象的分程序结束时,应当能够释放该对象对内存的占用从而尽量减少内存碎片的出现。
析构函数便是实现这一功能的特殊函数,可以说析构函数是构造函数的清洁工。
析构函数由定义类对象的分程序在其运行结束之前被自动地引用。
析构函数的书写极为简单,只要在构造函数名前附加一个“~”符号即可。
与构造函数一样,析构函数也不得声明任何的返回类值型、不得带有任何参数且不得重载,也不能被置于private区中。
一句话,一个类中只能有一个析构函数。
例9:
#include
classDemo
{
chars;
public:
Demo(){s=newchar[1024];}
~Demo(){deletes;}
};
voidmain()
{Demoh;}
实际上,析构函数除了可以(用delete)释放在构造函数中指明占用的内存外,还要释放整个对象的成员等所占用的内存,这些只是不必写进去罢了。
若未明确声明成员数据的内存占用便可用空函数体表示,甚至可以不写析构函数就缺省存在一个的析构函数。
§2.4成员初始化表(MemberInitiationTable)
用构造函数初始化类对象的成员数据不外乎程序直接初始(即引用缺省构造函数)和参数初始两种。
当只做一次较为简单的赋值初始(如一个学生的成绩数据)或对const类的数据的初始时,要么就显得过于庞大(动用大量程序或占用过长的参数表),要么似乎又不可能。
因此,C++语言又在此基础上专门提供了一个称为成员初始化表的特别手段来简化这种初始过程。
由于此种手段是在编译阶段由编译器将要初始的成员数据与参数建立了对应联系,所以用此法的系统在运行阶段的开销较之其它方法都要小得多。
成员初始化表放在构造函数名与构造函数体之间,用冒号与函数名部分相分隔。
每个表的格式为:
类中成员名(初值),类中成员名(初值)…,
若一个表中含有多组初始化值可用逗号将彼此分开。
现以§1.2中的综合例l为基础,加入出生日期后列在下面以说明成员初始化表的应用方法。
例10:
#include
classST
{
charname[10];
unsignedintnum,cnum,yy,mm.dd;
public:
ST(unsignedinty,unsignedintm,unsignedintd,):
yy(y),mm(m),dd(d)
{
cout<<”\nPleaseinputthedataofstudent:
\nName:
”;
cin>>name;
cout<<”Number:
”;
cin>>num;
cout<<”ClassNumber:
”;
cin>>cnum;
}
voiddsp()
{
cout<<”Name:
”<”<”<cout<<”\nBirthdayis”<}
};
voidmain()
{
STs1(1974,3,26);
si.dsp();
}
例10中将原来的init()改为构造函数,出生日期由参数代入,但在构造函数中看不到任何赋值语句。
这里的赋值是由成员初始化表实观的。
当然出生日期也可以在构造函数中临时输入,这里是为了说明成员初始化表的作用才如此安排的。
仔细分析一下赋值与初始赋值的区别,就会明白两者有本质的不同。
在例10之前的各例中的赋初值操作是在执行阶段由程序向固定存储单元的变量写入数值的操作,此后仍容许用其它赋值函数修改其内容。
而初始赋值则是对const类的常数仅在编译时完成予留内存单元并同时填入初值,此后不允许再改动。
所以不能在构造函数内使用赋值语句(即等号)在运行阶段对常数或引用类型数据进行赋值。
在C++中语言只有const和引用类型是要在编译时就要指明其初值的。
由于定义在类中的成员都是抽象的数据结构描述,不可能分配内存单元,因此在对构造函数进行编译的阶段也就不可能完成赋初值的操作了。
为了解决这一矛盾,C++语言只有借助于类对象“成员初始化表”的描述区将要赋初值的成员名及初值予先声明,待执行时产生了对象(即分配了内存单元)后再补作上述的赋初值操作。
所以从某种意义上讲,成员初始化表是特意为这两种数据成员准备的也不为过,由此切记成员初始化表不可写在声明语句上。
引用类型与const类型的初值问题有所不同。
因为编译器不知道在使用时将与哪个对象成员相对应,自然就不允许在class中定义联系。
然而此联系最终又必须确定,否则在运行阶段真的出现了“int&i=3;”的错误是没有措施可用的。
所以也只好将其与const类型同等对待了。
例11:
#include
classDemo
{
constintl;
inti,&r;
public:
Demo(intm,intn):
l(n),r((int)i)//注:
r作为常量l的引用将改变性质,既经r可写l
{i=m;r++;cout<<”L=”<};
voidmain()
{
Demoh(8,10);
}
类中的const类型成员除了上面的特点以外还可用来临时设定成员函数的操作特性。
凡含有const保留字的成员函数统称为常数型成员函数。
其在类中声明的格式有三种:
防止本函数误写参数变量的:
返回值类型函数名(const参数,const参数,…);
防止本函数误写类对象内全部的变量的:
返回值类型函数名(参数表)const;
防止其它函数误写返回地址或引用的:
const返回值类型函数名(参数表);
凡被声明为常数类型的成员函数在对本类对象中的成员数据(无论是定义在哪个区中)的写操作将受到不同程度的限制。
比如可以把例10中的“voiddsp()”声明改为“voiddsp()const”。
此特性除了可以保护类对象中的成员数据外,在后面讨论类派生时还有更重要的意义。
§2.5用拷贝型构造函数复制类对象的指针型成员数据
在一个类中安排各种指针数据成员时必然要考虑当生成类对象时这些指针(特别是字符指针)数据成员应如何初始化。
拷贝型构造函数所涉及的技术方法则是用于解决类对象中指针数据成员初始化问题的。
在展开拷贝型构造函数所涉及的技术方法的讨论之前,有必要先就指针数据成员在初始化时可能存在的问题作出较为全面的交代。
例12:
#include
#include
#include
classDemo
{