第六节、封装目标:1.类的定义2.对象声明及使用3.this指针4.对象的初始化和清除5.类型转换和拷贝构造函数6.对象赋值7.对象成员8.静态成员9.常量成员10.指向类成员的指针一、类的定义封装是类的一个特性,封装是把描述类的特征的数据和描述类的行为的函数包围起来,形成一个独立的抽象数据类型-----类。函数作为与外界交互的界面,对数据的访问一般通过一组经过定义的函数界面。客观世界中的对象本身就具有封装性。比如:老虎的特征(体重、身长、体温)和虎的行为(跑、跳等行为)是一体的。他们形成了一个具体的虎。既然类是数据函数的封装,那么我们来看一下什么是类。类是一种抽象的数据类型,与结构体类似,在使用类之前,要先定义它。Class类名{Public://共有数据和函数Protected://保护的数据和函数Private://私有数据和函数};例如:ClassCCircle{Public:IntGetRsdius(){Returnradius;}CharGetColor(){Returncolor;}Private:Intradius;Charcolor;};关键字private,public及protected用于声明类成员的可被访问性。例如:在一个合伙的公司里,对于公司里的设备,就是所有股东公有的。每个股东都可以使用公司里的设备。对于股东甲的房子来讲,就是保护型的,股东甲的儿子可以使用,并且拥有继承权。而对于股东甲的身份证来讲,就是私有的,只有股东甲拿着使用。别人是不能使用的。注意:1.不能在类定义中对数据成员使用表达式初始化。2.在类中的任何数据成员都不能使用extern、auto和register关键词进行修饰。3.类的成员函数的定义可以在类定义体内,也可以在类定义体外。定义在类体内的函数默认是内联函数。定义在类的体外的函数,若要成为内联函数,必须加inline关键词。二、对象声明及使用既然类是一种新的数据类型,它实现了数据和函数的封装,那它是如何封装的呢?他跟传统的数据类型有什么差别呢?要研究类,我们需要从具体的对象谈起。说到这里有几个注意事项:类是一种抽象的数据类型,既然是数据类型,就跟我们早期讲过的int、char这些数据类型概念上一致。类似于结构体,如果在程序中只定义了类,而未声明该类的实例----对象,则编译器不给该类分配任何空间。例如:intp;CCirclecircle;过去我们称呼p叫做变量,现在我们称呼circle为对象。注:编译器只为变量分配空间,而不会为类型分配空间。在这里我们所说的对象,其实就是我们前面所讲的变量。在前面我们讲过一个进程的用户区可以被划分为:静态/全局区、代码区、堆区、栈区。对象也可以出现在这几个区域内。对于一个对象,c++是怎样进行封装的呢?我们通过试验的方式带着大家进一步的探索c++对象的奥秘。首先,我们要知道,类里面的成员类型。有静态类型和非静态类型。我们首先来看对象的成员变量。参见example1.通过上面的测试我们看到。1.对象aa是一个栈区的变量。2.aa的大小是4个字节。非静态成员a的地址与对象aa的地址一样。并且大小也是4个字节。3.静态成员b不在对象aa内,静态成员b出现在全局/静态存储区。接下来,我们看一下对象的成员函数,也分为静态成员和非静态成员。根据显示结果,我们看到对象aa的成员函数,无论是静态成员函数还是非静态成员函数,它们都不包含在对象内。它们出现在代码区。通过上面的探索,我们看到,只有对象的非静态成员变量是被封装在对象体内的,也就是说对于类的每个对象,这些非静态成员变量是每个对象独有的。讲到这里,我们不仅会想,对于不在对象体内的静态成员变量和成员函数,它们是共享的?还是对于每个对象,都有对应的一份呢?我们看下面的例子通过上面的测试,我们清楚地看到,静态成员变量b,以及对象的静态成员函数和非静态成员函数,无论对于对象aa还是对象bb,它们占用的内存空间是一样的。在这里我们可以总结一下:对象的静态成员变量、静态成员函数以及非静态成员函数,他们是对该类的所有对象共享的。三、this指针由前面我们知道,在c++中,相同类的多个对象是共享代码的,那么相同的代码怎么会知道究竟应该操作哪一个对象的数据呢?如:intCObject::GetA(){returna;}在调用aa.GetA和调用bb.GetA,GetA函数怎么知道返回a是哪个对象的呢?我们说其中的奥妙就在于this指针。C++是通过this指针控制多个对象共享的代码访问正确的对象数据。C++编译器所认识的GetA函数的定义形式实际是:IntCObject::GetA(CObject*constthis){returnthis-a;}也就是说,当类成员函数被调用时,编译器传给被调用的成员函数的不仅是其在类中定义时所声明的参数,还有一个常量指针---this。this是C++的关键字。当aa调用GetA时,this指向aa的地址,因此返回aa对应的a值。当bb调用GetA时,this指向bb的地址,因此返回的是bb对应的a值。若没有this指针,实际上同类的多个对象是不可能共享代码的。This指针是隐含的,编译器保证this指针总是指向正确的对象。对于静态成员函数和非静态成员函数,是不是都隐含this指针呢?接下来我们做个测试。我们假设静态成员函数GetB也隐含this指针,那么,GetB就可以像GetA函数那样。于是我们修改GetB函数为:intCObject::GetB(){returna;}结果我们编译的时候,编译器提示如下错误:我们通过这个实例,可以验证,GetB不含this指针,GetB只能调用静态成员变量。汇总前面所讲的,我们可以总结为:类的每个对象的静态成员变量和成员函数是共享的。静态成员函数可以直接访问静态成员变量,不能访问非静态成员变量。This指针的作用就是利用共享的成员函数来访问每个对象封装的成员变量。四、对象的初始化与清除在声明一个变量的时候给变量赋初值叫做变量的初始化。在c++中,不允许在类定义时对数据成员进行初始化。因为类是一种数据类型,概念上等同于int、char等等的数据类型。而类的对象,就是我们所说的变量。前面我们讲过变量的初始化,那么对于一个特殊的变量---对象,我们如何初始化呢?前面我们讲过,1)在对象的数据成员中,有公有的,有私有的,有保护型的。而外界是不能直接访问对象的私有数据成员的,因此无法通过使用外界代码来初始化对象数据成员。2)此外,对象是封装的,外界也不清楚对象的具体实现机制,无法编写初始化代码。因此,对象的初始化,应该由对象的成员函数来进行。而且该成员函数应当是公有的,以便于对象之外的代码调用,来实现对象的初始化。在c++中,初始化对象数据成员是通过定义一个特殊的成员函数,构造函数来实现的。每当定义一个对象时,构造函数自动被调用。构造函数除了初始化数据成员外,还可以执行一些其它的初始化任务。在前面的例子中,CObject::CObject(){a=10;b=30;printf(a=%#x,b=%#x\n,&a,&b);}这个函数,就是构造函数,构造函数名与类名相同,并且没有返回值。如果允许构造函数有返回值,编译器无法确定把返回值自动赋给哪一个变量。由于构造函数是被自动调用的,因而不能被显式调用。构造函数可以有多个。例如:#include“stdio.h”classCObject{public:CObject();CObject(intx);staticintGetB();intGetA();private:inta;staticintb;};intCObject::b;CObject::CObject(){a=10;b=30;printf(a=%#x,b=%#x\n,&a,&b);}CObject::CObject(intx){a=x;b=30;}intCObject::GetB(){returnb;}intCObject::GetA(){returna;}intmain(intargc,char*argv[]){printf(对象aa的成员变量如下:\n);CObjectaa;printf(aa=%#x,Size=%d\n,&aa,sizeof(aa));printf(对象bb的成员变量如下:\n);CObjectbb;printf(bb=%#x,Size=%d\n,&bb,sizeof(bb));printf(aa.GetA=%#x,aa.GetB=%#x\n,aa.GetA,aa.GetB);printf(bb.GetA=%#x,bb.GetB=%#x\n,bb.GetA,bb.GetB);CObjectcc(100);printf(cc=%#x,cc.GetA()=%d,cc.GetB()=%d\n,&cc,cc.GetA(),cc.GetB());return0;}运行结果如下:这个例子中CObject();CObject(intx);都是构造函数。对象aa和对象bb,调用的是第一个构造函数初始化的,而对象cc是调用第二个构造函数初始化的。与构造函数对应,对象的清除工作由析构函数完成。析构函数名是在类名前加“~”,析构函数也必须是公有的,其原因与构造函数一样。析构函数不带参数,不能指定返回值,也不能重载,因此,析构函数是唯一的。如果在类中未定义析构函数,则编译器为该类自动生成一个析构函数。与构造函数不同的是,析构函数可以被显式的调用。一般来说,每当创建一个对象就调用对象的构造函数,每当撤销一个对象就调用该对象的析构函数。具体有以下四种情况:1.对于全局定义的对象,每当程序开始运行,在主函数main(在windows应用程序中是winmain)接受程序控制权之前,就调用全局对象的构造函数。整个程序结束时调用析构函数。应当知道,程序的第一条代码并不是main中的第一条语句。而是由连接程序加载程序前部的启动代码。2.对于局部定义的对象,每当程序流程到达该对象的定义处,调用构造函数,每当流程离开定义该对象的程序块时,调用析构函数。3.对于关键字static定义的局部对象,当程序流程第一次到达该对象定义处调用构造函数,在这个程序结束时调用析构函数。4.对于用new运算符创建的对象,每当创建该对象时调用构造函数,当用delete删除对象时,调用析构函数,否则析构函数不被调用。对于上述4点,大家自己写一个程序验证。五、类型转换和拷贝构造一个对象可以作为操作数用在表达式中,对表达式求值时,可能需要对操作数进行类型转换,这种类型转换是否合法取决于对象定义的构造函数。例如:example2.运行结果如下:分析:CTesttesta(1);建立一个对象调用构造函数。Testa=2;由变量赋值我们了解到,同种类型可以赋值。执行这句时,testa希望接收一个CTest类的对象,所以这是要调用构造函数CTest(intn),将2转换成CTest类型的对象。在转换过程中要建立一个临时对象,用2初始化该临时对象,然后再把临时对象赋值给testa。赋值完成后,再删除临时对象。因此执行这句输出。testa=CTest(3);这句并不是以3为参数调用CTest类的构造函数,然后把构造函数的返回值赋值给testa。原因是:1)构造函数不能显式调用,它总是在声明类的对象时自动调用。2)构造函数不能有返回值。该表达式是一个强制类型转换语句,即把3强制转换为CTest类型的变量---即对象。该对象是一个临时对象,然后把该对象赋值给testa。因此执行这句的输出是最后一句的输出,使因为testa对象是一个局部变量,在main函数执行完时,自动析构输出的。可以看出,构造函数除了可以初始化对象外,还可以用于类型转换。在c++中,变量的初始化和赋值是两个不同的概念。在声明变量时使用的等号不是运算符,而表示对声明的变量初始化。初始化中的“=”号不产生代码,由编译器在程序数据段中产生初值。而在赋值表达式中,“=”号产生代码,被编译器在程序代码段翻译成若干条mov指令。例如:Inta=5