资源描述
第9章 关于类和对象的进一步讨论,9.1 构造函数 9.2 析构函数 9.3 调用构造函数和析构函数的顺序 9.4 对象数组 9.5 对象指针 9.6 共用数据的保护 9.7 对象的动态建立和释放 9.8 对象的赋值和复制 9.9 静态成员 9.10 友元 9.11 类模板,在建立一个对象时,作某些初始化的工作如对数据成员赋初值。即在创建对象(分配内存时)进行数据成员的初始化,因为对象是实实在在的对象,不能无具体属性值。 注意: 类的数据成员是不能在声明类时初始化的。,9.1 构造函数作用:创建对象(分配内存时)时进行数据成员的初始化9.1.1 对象的初始化,如果一个类中所有的成员都是公用的,则可以在定义对象时对数据成员进行初始化。如 class Time public: /声明为公用成员 hour; minute; sec; ; Time t1=14,56,30; /将t1初始化为14:56:30 但是,一般数据成员是私有的,或者类中有private或protected的成员,就不能用这种方法初始化。 如何实现?,C+提供了构造函数(constructor)来处理对象的初始化。 构造函数是特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。 构造函数的名字必须与类名同名,而不能由用户任意命名,以便编译系统能识别它并把它作为构造函数处理。 它不具有任何类型,不返回任何值。 构造函数的功能是由用户定义的,用户根据初始化的要求设计函数体和函数参数。,9.1.2 构造函数的作用,例9.1 在例8.3基础上定义构造成员函数。 #include using namespace std; class Time public: Time( ) /定义构造成员函数,函数名与类名相同 hour=0; /利用构造函数对对象中的数据成员赋初值 minute=0; sec=0; void set_time( ); /函数声明 void show_time( ); /函数声明 private: int hour; /私有数据成员 int minute; int sec; ;,void Timeset_time( ) /定义成员函数,向数据成员赋值 cinhour; cinminute; cinsec; void Timeshow_time( ) /定义成员函数,输出数据成员的值 couthour:minute:secendl; int main( ) Time t1; /建立对象t1,同时调用构造函数t1.Time( ) t1.set_time( ); /对t1的数据成员赋值 t1.show_time( ); /显示t1的数据成员的值 Time t2; /建立对象t2,同时调用构造函数t2.Time( ) t2.show_time( ); /显示t2的数据成员的值 return 0; ,程序运行的情况为: 10 25 54 (从键盘输入新值赋给t1的数据成员) 10:25:54 (输出t1的时、分、秒值) 0:0:0 (输出t2的时、分、秒值) 也可以在类外定义构造函数: TimeTime( ) /要加上类名Time和域限定 符“” hour=0; minute=0; sec=0; ,有关构造函数的使用,有以下说明: (1) 在类对象进入其作用域时调用构造函数。 (2) 构造函数没有返回值,因此也不需要在定义构造函数时声明类型,这是它和一般函数的一个重要的不同之点。 (3) 构造函数不需用户调用,也不能被用户调用。 (4) 如果用户自己没有定义构造函数,则C+系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作。,不带参数构造函数,这种方式使该类的每一个对象都得到同一组初值。 带参数的构造函数,用户希望对不同的对象赋予不同的初值。 构造函数首部的一般格式: 构造函数名(类型 1 形参1,类型2 形参2,) 实参是在定义对象时给出的。 定义对象的一般格式为 : 类名 对象名(实参1,实参2,);,9.1.3 带参数的构造函数,例9.2 有两个长方柱,其长、宽、高分别为: (1)12,20,25;(2)10,14,20。求它们的体积。编一个基于对象的程序,在类中用带参数的构造函数。 #include using namespace std; class Box public: Box(int,int,int); /声明带参数的构造函数 int volume( ); /声明计算体积的函数 private: int height; int width; int length; ; BoxBox(int h,int w,int len) /在类外定义带参数的构造函数 height=h; width=w; length=len; ,int Boxvolume( ) /定义计算体积的函数 return(height*width*length); int main( ) Box box1(12,25,30); /建立对象box1,并指定box1长、宽、高的值 coutThe volume of box1 is box1.volume( )endl; Box box2(15,30,21); /建立对象box2,并指定box2长、宽、高的值 coutThe volume of box2 is box2.volume( )endl; return 0; 程序运行结果如下: The volume of box1 is 9000 The volume of box2 is 9450 注意: 带参数的构造函数中的形参,其对应的实参在定义对象时给定。,C+还提供另一种初始化数据成员的方法参数初始化表来实现对数据成员的初始化。 这种方法不在函数体内对数据成员初始化,而是在函数首部实现。例如例9.2中定义构造函数可以改用以下形式: BoxBox(int h,int w,int len):height(h),width(w),length(len) 这种写法方便、简练,尤其当需要初始化的数据成员较多时更显其优越性。甚至可以直接在类体中(而不是在类外)定义构造函数。,9.1.4 用参数初始化表对数据成员初始化,在一个类中可以定义多个构造函数,以便对类对象提供不同的初始化的方法,供用户选用。 这些构造函数具有相同的名字,而参数的个数或参数的类型不相同。这称为构造函数的重载。 在第4章第4.6节中所介绍的函数重载的知识也适用于构造函数。 通过下面的例子可以了解怎样应用构造函数的重载。,9.1.5 构造函数的重载,例9.3 在例9.2的基础上,定义两个构造函数,其中一个无参数,一个有参数。 #include using namespace std; class Box public: Box( ); /声明一个无参的构造函数 Box(int h,int w,int len):height(h),width(w),length(len) /声明一个有参的构造函数,用参数的初始化表对数据成员初始化 int volume( ); private: int height; int width; int length; ; BoxBox( ) /定义一个无参的构造函数 height=10; width=10; length=10; ,int Boxvolume( ) return(height*width*length); int main( ) Box box1; /建立对象box1,不指定实参 coutThe volume of box1 is box1.volume( )endl; Box box2(15,30,25); /建立对象box2,指定3个实参 coutThe volume of box2 is box2.volume( )endl; return 0; 在本程序中定义了两个重载的构造函数,其实还可以定义其他重载构造函数。,说明: (1) 调用构造函数时不必给出实参的构造函数,称为默认构造函数(default constructor)。显然,无参的构造函数属于默认构造函数。一个类只能有一个默认构造函数。 (2) 如果在建立对象时选用的是无参构造函数,应注意正确书写定义对象的语句。 (3) 尽管在一个类中可以包含多个构造函数,但是对于每一个对象来说,建立对象时只执行其中一个构造函数,并非每个构造函数都被执行。,构造函数中参数的值既可以通过实参传递,也可以指定为某些默认值,即如果用户不指定实参值,编译系统就使形参取默认值。 例9.4 将例9.3程序中的构造函数改用含默认值的参数,长、宽、高的默认值均为10。 在例9.3程序的基础上改写如下:,9.1.6 使用默认参数的构造函数,#include using namespace std; class Box public: Box(int h=10,int w=10,int len=10); /在声明构造函数时指定默认参数 int volume( ); private: int height; int width; int length; ; BoxBox(int h,int w,int len) /在定义函数时可以不指定默认参数 height=h; width=w; length=len; ,int Boxvolume( ) return(height*width*length); int main( ) Box box1; /没有给实参 coutThe volume of box1 is box1.volume( )endl; Box box2(15); /只给定一个实参 coutThe volume of box2 is box2.volume( )endl; Box box3(15,30); /只给定2个实参 coutThe volume of box3 is box3.volume( )endl; Box box4(15,30,20); /给定3个实参 coutThe volume of box4 is box4.volume( )endl; return 0; ,析构函数(destructor)也是一个特殊的成员函数,它的作用与构造函数相反,它的名字是类名的前面加一个“”符号。在C+中“”是位取反运算符,从这点也可以想到: 析构函数是与构造函数作用相反的函数。 当对象的生命期结束时,会自动执行析构函数。 如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。,9.2 析构函数,static局部对象,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数) 时,调用该全局对象的析构函数。 如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。 析构函数的作用:并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用。 程序设计者事先设计好析构函数,只要对象的生命期结束,程序就自动执行析构函数来完成这些工作。,析构函数不返回任何值,没有函数类型,也没有函数参数。因此它不能被重载。一个类可以有多个构造函数,但只能有一个析构函数。 析构函数的作用并不仅限于释放资源方面,它还可以输出有关的信息。 一般情况下,类的设计者应当在声明类的同时定义析构函数,以指定如何完成“清理”的工作。 如果用户没有定义析构函数,C+编译系统会自动生成一个析构函数,但它只是徒有析构函数的名称和形式,实际上什么操作都不进行。想让析构函数完成任何工作,都必须在定义的析构函数中指定。,例9.5 包含构造函数和析构函数的C+程序。 #include #include using namespace std; class Student /声明Student类 public: student(int n,string nam,char s ) /定义构造函数 num=n; name=nam; sex=s; coutConstructor called.endl; /输出有关信息 Student( ) /定义析构函数 coutDestructor called.endl; /输出有关信息 void display( ) /定义成员函数 coutnum: numendl; coutname: nameendl; coutsex: sexendlendl; ,private: int num; char name10; char sex; ; int main( ) Student stud1(10010,Wang_li,f); /建立对象stud1 stud1.display( ); /输出学生1的数据 Student stud2(10011,Zhang_fun,m); /定义对象stud2 stud2.display( ); /输出学生2的数据 return 0; ,程序运行结果如下: Constructor called. (执行stud1的构造函数) num: 10010 (执行stud1的display函数) name:Wang_li sex: f Constructor called. (执行stud2的构造函数) num: 10011 (执行stud2的display函数) name:Zhang_fun sex:m Destructor called. (执行stud2的析构函数) Destructor called. (执行stud1的析构函数),在使用构造函数和析构函数时,需要特别注意对它们的调用时间和调用顺序。 在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反: 最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。如图9.1示意。,9.3 调用构造函数和析构函数的顺序,图9.1,但是,并不是在任何情况下都是按这一原则处理的。对象可以在不同的作用域中定义,可以有不同的存储类别。这些会影响调用构造函数和析构函数的时机。 下面归纳一下什么时候调用构造函数和析构函数: (1) 在全局范围中定义的对象 它的构造函数在文件中的所有函数(包括main函数)执行之前调用。当main函数执行完毕或调用exit函数时(此时程序终止),调用析构函数。,(2) 如果定义的是局部自动对象(例如在函数中定义对象),则在建立对象时调用其构造函数。如果函数被多次调用,则在每次建立对象时都要调用构造函数。在函数调用结束、对象释放时先调用析构函数。 (3) 如果在函数中定义静态(static)局部对象,则只在程序第一次调用此函数建立对象时调用构造函数一次,只在main函数结束或调用exit函数结束程序时,才调用析构函数。,数组也可以由对象组成(对象数组的每一个元素都是同类的对象)。 例如一个班有50个学生,每个学生的属性包括姓名、性别、年龄、成绩等。如果为每一个学生建立一个对象,需要分别取50个对象名。用程序处理很不方便。这时可以定义一个“学生类”对象数组,每一个数组元素是一个“学生类”对象。例如 Student stud50; /假设已声明了Student类,定义stud数组,有50个元素,9.4 对象数组,在建立数组时,同样要调用构造函数。如果有50个元素,需要调用50次构造函数。 如果构造函数有多个参数,则不能用在定义数组时直接提供所有实参的方法,因为一个数组有多个元素,对每个元素要提供多个实参,如果再考虑到构造函数有默认参数的情况,很容易造成实参与形参的对应关系不清晰,出现歧义性。,如果构造函数有多个参数,在定义对象数组时应当怎样实现初始化呢? 在花括号中分别写出构造函数并指定实参。如果构造函数有3个参数,分别代表学号、年龄、成绩。则可以这样定义对象数组: Student Stud3= /定义对象数组 Student(1001,18,87), /调用第1个元素的构造函数,为它提供3个实参 Student(1002,19,76), /调用第2个元素的构造函数,为它提供3个实参 Student(1003,18,72) /调用第3个元素的构造函数,为它提供3个实参 ;,在建立对象数组时,分别调用构造函数,对每个元素初始化。每一个元素的实参分别用括号包起来,对应构造函数的一组形参,不会混淆。 例9.6 对象数组的使用方法。 #include using namespace std; class Box public: Box(int h=10,int w=12,int len=15): height(h),width(w),length(len) /声明有默认参数的构造函数,用参数初始化表对数据成员初始化 int volume( ); private: int height; int width;,int length; ; int Boxvolume( ) return(height*width*length); int main( ) Box a3= /定义对象数组 Box(10,12,15), /调用构造函数Box,提供第1个元素的实参 Box(15,18,20), /调用构造函数Box,提供第2个元素的实参 Box(16,20,26) /调用构造函数Box,提供第3个元素的实参 ; coutvolume of a0 is a0.volume( )endl; coutvolume of a1 is a1.volume( )endl; coutvolume of a2 is a2.volume( )endl; ,运行结果如下: volume of a0 is 1800 volume of a1 is 5400 volume of a2 is 8320,在建立对象时,编译系统会为每一个对象分配一定的存储空间,以存放其成员。对象空间的起始地址就是对象的指针。可以定义一个指针变量,用来存放对象的指针。如果有一个类: class Time public: int hour; int minute; int sec; void get_time( ); ;,9.5 对象指针 9.5.1 指向对象的指针,void Timeget_time( ) couthour pt所指向的对象中的hour成员,即t1.hour (*pt).get_time ( ) 调用pt所指向的对象中的get_time函数,即t1.get_time pt-get_time ( ) 调用pt所指向的对象中的get_time函数,即t1.get_time,对象有地址,存放对象初始地址的指针变量就是指向对象的指针变量。对象中的成员也有地址,存放对象成员地址的指针变量就是指向对象成员的指针变量。,9.5.2 指向对象成员的指针,1. 指向对象数据成员的指针 定义指向对象数据成员的指针变量的方法和定义指向普通变量的指针变量方法相同。例如 int *p1; /定义指向整型数据的指针变量 定义指向对象数据成员的指针变量的一般形式为 数据类型名 *指针变量名; 如果Time类的数据成员hour为公用的整型数据,则可以在类外通过指向对象数据成员的指针变量访问对象数据成员hour。 p1= /输出t1.hour的值,例9.7 有关对象指针的使用方法。 #include using namespace std; class Time public: Time(int,int,int); int hour; int minute; int sec; void get_time( ); /声明公有成员函数 ;,TimeTime(int h,int m,int s) hour=h; minute=m; sec=s; void Timeget_time( ) /定义公有成员函数 coutget_time( ); /调用p2所指向对象(即t1)的get_time函数 ,每个对象中的数据成员都分别占有存储空间,如果对同一个类定义了n个对象,则有n组同样大小的空间以存放n个对象中的数据成员。但是,不同对象都调用同一个函数代码段。 那么,当不同对象的成员函数引用数据成员时,怎么能保证引用的是所指定的对象的数据成员呢?,9.5.3 this 指针,在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this。 它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。例如,当调用成员函数a.volume时,编译系统就把对象a的起始地址赋给this指针,于是在成员函数引用数据成员时,就按照this的指向找到对象a的数据成员。例如volume函数要计算height*width*length的值,实际上是执行: (this-height)*(this-width)*(this-length) 由于当前this指向a,因此相当于执行: (a.height)*(a.width)*(a.length) 这就计算出长方体a的体积。,this指针是隐式使用的,它是作为参数被传递给成员函数的。本来,成员函数volume的定义如下: int Boxvolume( ) return (height*width*length); C+把它处理为 int Boxvolume(Box *this) return(this-height * this-width * this-length); 即在成员函数的形参表列中增加一个this指针。在调用该成员函数时,实际上是用以下方式调用的: a.volume( 将对象a的地址传给形参this指针。然后按this的指向去引用其他成员。,需要说明: 这些都是编译系统自动实现的,编程序者不必人为地在形参中增加this指针,也不必将对象a的地址传给this指针。 在需要时也可以显式地使用this指针。例如在Box类的volume函数中,下面两种表示方法都是合法的、相互等价的。 return(height * width * length); /隐含使用this指针 return(this-height * this-width * this-length); /显式使用this指针 可以用*this表示被调用的成员函数所在的对象,*this就是this所指向的对象,即当前的对象。例如在成员函数a.volume( )的函数体中,如果出现*this,它就是本对象a。上面的return语句也可写成 return(*this).height * (*this).width * (*this).length);,注意*this两侧的括号不能省略,不能写成*this.height。 所谓“调用对象a的成员函数f”,实际上是在调用成员函数f时使this指针指向对象a,从而访问对象a的成员。在使用“调用对象a的成员函数f”时,应当对它的含义有正确的理解。,有时人们希望在需要用到对象时才建立对象,在不需要用该对象时就撤销它,释放它所占的内存空间以供别的数据使用。这样可提高内存空间的利用率。 用new运算符动态建立对象,用delete运算符撤销对象。 如果已经定义了一个Box类,可以用下面的方法动态地建立一个对象:,9.7 对象的动态建立和释放,new Box; 编译系统开辟了一段内存空间,并在此内存空间中存放一个Box类对象,同时调用该类的构造函数,以使该对象初始化(如果已对构造函数赋予此功能的话)。但是此时用户还无法访问这个对象,因为这个对象既没有对象名,用户也不知道它的地址。这种对象称为无名对象,它确实是存在的,但它没有名字。 用new运算符动态地分配内存后,将返回一个指向新对象的指针的值,即所分配的内存空间的起始地址。用户需要定义一个指向本类的对象的指针变量来存放该地址。如 Box *pt; /定义一个指向Box类对象的指针变量pt pt=new Box; /在pt中存放了新建对象的起始地址,在程序中就可以通过pt访问这个新建的对象。如 coutheight; /输出该对象的height成员 coutvolume( ); /调用该对象的volume函数,计算并输出体积 C+还允许在执行new时,对新建立的对象进行初始化。如(调带参数的构造函数) Box *pt=new Box(12,15,18); 这种写法是把上面两个语句(定义指针变量和用new建立新对象)合并为一个语句,并指定初值。这样更精炼。新对象中的height,width和length分别获得初值12,15,18。 调用对象既可以通过对象名,也可以通过指针。用new建立的动态对象一般是不用对象名的,是通过指针访问的,它主要应用于动态的数据结构,如链表。访问链表中的结点,并不需要通过对象名,,在执行new运算时,如果内存量不足,无法开辟所需的内存空间,目前大多数C+编译系统都使new返回一个0指针值。只要检测返回值是否为0,就可判断分配内存是否成功。 在不再需要使用由new建立的对象时,可以用delete运算符予以释放。如 delete pt; /释放pt指向的内存空间,这就撤销了pt指向的对象。 在执行delete运算符时,在释放内存空间之前,自动调用析构函数,完成有关善后清理工作。,如果对一个类定义了两个或多个对象,则这些同类的对象之间可以互相赋值,即一个对象的值可以赋给另一个同类的对象。对象的值是指对象中所有数据成员的值。 对象之间的赋值也是通过赋值运算符“=”进行的。本来,赋值运算符“=”只能用来对单个的变量赋值,现在被扩展为两个同类对象之间的赋值,这是通过对赋值运算符的重载实现的。实际这个过程是通过成员复制来完成的,即将一个对象的成员值一一复制给另一对象的对应成员。,9.8 对象的赋值和复制COPY 9.8.1 对象的赋值,对象名1 = 对象名2; 注意对象名1和对象名2必须属于同一个类。 例如 Student stud1,stud2; /定义两个同类的对象 stud2=stud1; /将stud1赋给stud2 通过下面的例子可以了解怎样进行对象的赋值。 例9.9 对象的赋值。 #include using namespace std; class Box public: Box(int=10,int=10,int=10); /声明有默认参数的构造函数 int volume( ); private: int height; int width;,int length; ; BoxBox(int h,int w,int len) height=h; width=w; length=len; int Boxvolume( ) return(height*width*length); /返回体积 int main( ) Box box1(15,30,25),box2; /定义两个对象box1和box2 coutThe volume of box1 is box1.volume( )endl; box2=box1; /将box1的值赋给box2 coutThe volume of box2 is box2.volume( )endl; return 0; ,运行结果如下: The volume of box1 is 11250 The volume of box2 is 11250 说明: (1) 对象的赋值只对其中的数据成员赋值,而不对成员函数赋值。 (2) 类的数据成员中不能包括动态分配的数据,否则在赋值时可能出现严重后果。,有时需要用到多个完全相同的对象。此外,有时需要将对象在某一瞬时的状态保留下来。这就是对象的复制机制。用一个已有的对象快速地复制出多个完全相同的对象。如 Box box2(box1); 其作用是用已有的对象box1去克隆出一个新对象box2。 其一般形式为 类名 对象2(对象1); 用对象1复制出对象2。,9.8.2 对象的复制,可以看到: 它与前面介绍过的定义对象方式类似,但是括号中给出的参数不是一般的变量,而是对象。在建立对象时调用一个特殊的构造函数复制构造函数(copy constructor)。这个函数的形式是这样的: /The copy constructor definition. BoxBox(const Box 复制构造函数也是构造函数,但它只有一个参数,这个参数是本类的对象(不能是其他类的对象),而且采用对象的引用的形式(一般约定加const声明,使参数值不能改变,以免在调用此函数时因不慎而使对象值被修改)。,此复制构造函数的作用就是将实参对象的各成员值一一赋给新的对象中对应的成员。 回顾复制对象的语句 Box box2(box1); 这实际上也是建立对象的语句,建立一个新对象box2。由于在括号内给定的实参是对象,因此编译系统就调用复制构造函数(它的形参也是对象),而不会去调用其他构造函数。实参box1的地址传递给形参b(b是box1的引用),因此执行复制构造函数的函数体时,将box1对象中各数据成员的值赋给box2中各数据成员。 如果用户自己未定义复制构造函数,则编译系统会自动提供一个默认的复制构造函数,其作用只是简单地复制类中每个数据成员。,C+还提供另一种方便用户的复制形式,用赋值号代替括号,如 Box box2=box1; /用box1初始化box2 其一般形式为 类名 对象名1 = 对象名2; 可以在一个语句中进行多个对象的复制。如 Box box2=box1,box3=box2; 按box1来复制box2和box3。可以看出: 这种形式与变量初始化语句类似,请与下面定义变量的语句作比较: int a=4,b=a; 这种形式看起来很直观,用起来很方便。但是其作用都是调用复制构造函数。,请注意对象的复制和对象的赋值在概念上和语法上的区别。对象的赋值是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。而对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同(包括对象的结构和成员的值)。 可以对例9.7程序中的主函数作一些修改: int main( ) Box box1(15,30,25); /定义box1 coutThe volume of box1 is box1.volume( )endl; Box box2=box1,box3=box2; /按box1来复制box2,box3 coutThe volume of box2 is box2.volume( )endl; coutThe volume of box3 is box3.volume( )endl; 执行完第3行后,3个对象的状态完全相同。,请注意普通构造函数和复制构造函数的区别! (1) 在形式上 类名(形参表列); /普通构造函数的声明,如Box(int h,int w,int len); 类名(类名 /实参是对象名,调用复制构造函数 (3) 在什么情况下被调用 普通构造函数在程序中建立对象时被调用。 复制构造函数在用已有对象复制一个新对象时被调用,在以下3种情况下需要克隆对象:, 程序中需要新建立一个对象,并用另一个同类的对象对它初始化,如前面介绍的那样。 当函数的参数为类的对象时。在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。如 void fun(Box b) /形参是类的对象 int main( ) Box box1(12,15,18); fun(box1); /实参是类的对象,调用函数时将复制一个新对象b return 0; , 函数的返回值是类的对象。在函数调用完毕将返回值带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。如 Box f( ) /函数f的类型为Box类类型 Box box1(12,15,18); return box1; /返回值是Box类的对象 int main( ) Box box2; /定义Box类的对象box2 box2=f( ); /调用f函数,返回Box类的临时对象,并将它赋值给box2 以上几种调用复制构造函数都是由编译系统自动实现的,不必由用户自己去调用,读者只要知道在这些情况下需要调用复制构造函数就可以了。,如果有n个同类的对象,那么每一个对象都分别有自己的数据成员,不同对象的数据成员各自有值,互不相干。但是有时人们希望有某一个或几个数据成员为所有对象所共有。这样可以实现数据共享。 在第7章中曾介绍过全局变量,它能够实现数据共享。如果在一个程序文件中有多个函数,在每一个函数中都可以改变全局变量的值,全局变量的值为各函数共享。但是用全局变量的安全性得不到保证,由于在各处都可以自由地修改全局变量的值,很有可能偶一失误,全局变量的值就被修改,导致程序的失败。因此在实际工作中很少使用全局变量。 如果想在同类的多个对象之间实现数据共享,也不要用全局对象,可以用静态的数据成员。,9.9 静态成员,静态数据成员是一种特殊的数据成员。它以关键字static开头。例如 class Box public: int volume( ); private: static int height; /把height定义为静态的数据成员 int width; int length; ; 如果希望各对象中的height的值是一样的,就可以把它定义为静态数据成员,这样它就为各对象所共有,而不只属于某个对象的成员,,9.9.1 静态数据成员,所有对象都可以引用它。每个对象都可以引用这个静态数据成员。静态数据成员的值对所有对象都是一样的。如果改变它的值,则在各对象中这个数据成员的值都同时改变了。这样可以节约空间,提高效率。 说明: (1) 静态数据成员不属于某一个对象,在为对象所分配的空间中不包括静态数据成员所占的空间。静态数据成员是在所有对象之外单独开辟空间。只要在类中定义了静态数据成员,即使不定义对象,也为静态数据成员分配空间,它可以被引用。,(2) 静态数据成员,它不随对象的建立而分配空间,也不随对象的撤销而释放。静态数据成员是在程序编译时被分配空间的,到程序结束时才释放空间。 (3) 静态数据成员可以初始化,但只能在类体外进行初始化。如 int Boxheight=10; /表示对Box类中的数据成员初始化,其一般形式为 数据类型类名静态数据成员名=初值; 不必在初始化语句中加static。 注意: 不能用参数初始化表对静态数据成员初始化。如在定义Box类中这样定义构造函数是错误的: Box(int h,int w,int len):height(h) /错误,height是静态数据成员 如果未对静态数据成员赋初值,则编译系统会自动赋予初值0。 (4) 静态数据成员既可以通过对象名引用,也可以通过类名来引用。 例9.10 引用静态数据成员。,#include using namespace std; class Box public: Box(int,int); int volume( ); static int height; /把height定义为公用的静态的数据成员 int width; int length; ; BoxBox(int w,int len) /通过构造函数对width和length赋初值 width=w; length=len; int Boxvolume( ) return(height*width*length); int Boxheight=10; /对静态数据成员height初始化,int main( ) Box a(15,20),b(20,30); couta.heightendl; /通过对象名a引用静态数据成员 coutb.heightendl; /通过对象名b引用静态数据成员 coutBoxheightendl; /通过类名引用静态数据成员 couta.volume( )endl; /调用volume函数,计算体积,输出结果 上面3个输出语句的输出结果相同(都是10)。所有对象的静态数据成员实际上是同一个数据成员。 请注意: 在上面的程序中将height定义为公用的静态数据成员,所以在类外可以直接引用。 在类外可以通过对象名引用公用的静态数据成员,也可以通过类名引用静态数据成员。即使没有定义类对象,也可以通过类名引用静态数据成员。,这说明静态数据成员并不是属于对象的,而是属于类的,但类的对象可以引用它。类内可随便访问。 如果静态数据成员被定义为私有的,则不能在类外直接引用,而必须通过公用的成员函数引用。 (5) 静态数据成员的作用:各对象之间的数据有了沟通的渠道,实现数据共享,因此可以不使用全局变量。全局变量破坏了封装的原则,不符合面向对象程序的要求。 注意公用静态数据成员与全局变量的不同,静态数据成员的作用域只限于定义该类的作用域内在此作用域内,可以通过类名和域运算符“”引用静态数据成员,而不论类对象是否存在。,成员函数也可以定义为静态的,在类中声明函数的前面加static就成了静态成员函数。如 static int volume( ); 和静态数据成员一样,如果要在类外调用公用的静态成员函数,要用类名和域运算符“”。如 Boxvolume( ); 实际上也允许通过对象名调用静态成员函数,如 a.volume( ); 但这并不意味着此函数是属于对象a的,而只是用a的类型而已。,9.9.2 静态成员函数,静态成员函数的作用:不是为了对象之间的沟通,而是为了能处理静态数据成员。 前面曾指出: 当调用一个对象的成员函数(非静态成员函数)时,系统会把该对象的起始地址赋给成员函数的this指针。而静态成员函数并不属于某一对象,它与任何对象都无关,因此静态成员函数没有this指针。既然它没有指向某一对象,就无法对一个对象中的非静态成员进行默认访问。 静态成员函数与非静态成员函数的根本区别: 非静态成员函数有this指针,而静态成员函数没有this指针。由此,静态成员函数不能访问本类中的非静态成员。,静态成员函数可以直接引用本类中的静态数据成员,静态成员函数主要用来访问静态数据成员,而不访问非静态成员。假如在一个静态成员函数中有以下语句: coutheightendl; /若height已声明为static,则引用本类中的静态成员,合法 coutwidthendl; /若width是非静态数据成员,不合法 如果一定要引用本类的非静态成员,应该加对象名和成员运算符“.”。如 couta.widthendl; /引用本类对象a中的非静态成员 假设a已定义为Box类对象,且在当前作用域内有效,则此语句合法。,例9.11 静态成员函数的应用。 #include using namespace std; class Student /定义Student类 public: Student(int n,int a,float s):num(n),age(a),score(s) /定义构造函数 void total( ); static float average( ); /声明静态成员函数 private: int num; int age; float score; static float sum; /静态数据成员 static int count; /静态数据成员 ; void Studenttotal( ) /定义非静态成员函数 sum+=score; /累加总分,类内访问静态数据成员 count+; /累计已统计的人数,类内访问静态数据成员 ,float Studentaverage( ) /定义静态成员函数 return(sum/count); /只能访问静态数据成员 float Studentsum=0; /对静态数据成员初始化 int Studentcount=0; /对静态数据成员初始化 int main( ) Student stud3= /定义对象数组并初始化 Student(1001,18,70), Student(1002,19,78), Student(1005,20,98) ; int n; coutn; /输入需要求前面多少名学生的平均成绩 for(int i=0;in;i+) /调用3次total函数 studi.total( ); coutthe average score of n students is Studentaverage( )endl; /调用静态成员函数 return 0; ,运行结果为 please input the number of students:3 the average score of 3 students is 82.3333 说明: (1)在Student类中定义了两个静态数据成员sum(总分)和count(累计需要统计的学生人数),这是由于这两个数据成员的值是需要进行累加的,它们并不是只属于某一个对象元素,而是由各对象元素共享的,可以看出: 它们的值是在不断变化的,而且无论对哪个对象元素而言,都是相同的,而且始终不释放内存空间。,(2)注意: total是公有的成员函数,公有的成员函数可以引用本对象中的一般数据成员(非静态数据成员),也可以引用类中的静态数据成员。score是非静态数据成员,sum和count是静态数据成员。 (3) average是静态成员函数,可以直接引用私有的静态数据成员(不必加类名或对象名),函数返回成绩的平均值。 (4) 在main函数中,引用total函数要加对象名(今用对象数组元素名),引用静态成员函数average函数要用类名或对象名。,类的基本访问规则:在一个类中可以有公用的(public)成员和私有的(private)成员。在类外可以访问公用成员,只有本类中的函数可以访问本类的私有成员。 现在,我们来补充介绍一个例外友元(friend)。 友元可以访问与其有好友关系的类中的私有成员。友元包括友元函数和友元类。,9.10 友元,如果在本类以外的其他地方定义了一个函数(这个函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数)。 在类体中用friend对其进行声明,此函数就称为本类的友元函数。友元函数可以访问这个类中的私有成员,但是外部访问。,9.10.1 友元函数,1. 将普通函数声明为友元函数 例9.12 友元函数的简单例子。 #include using namespace std; class Time public: Time(int,int,int); friend void display(Time ,minute=m; sec=s; void display(Time /调用display函数,实参t1是Time类对象 程序输出结果如下: 10:13:56,由于声明了display是Time类的friend函数,所以display函数可以引用Time中的私有成员hour,minute,sec。 但注意在引用这些私有数据成员时,必须加上对象名,不能写成 couthour:minute:secendl; 与类的成员函数的区别!只能以外部方式访问私有成员。,2. 友元成员函数 friend函数不仅可以是一般函数(非成员函数),而且可以是另一个类中的成员函数。 例9.13 友元成员函数的简单应用。 #include using namespace std; class Date; /对Date类的提前引用声明,?因在Time类中用到 class Time /定义Time类 public: Time(int,int,int); void display(Date ,int minute; int sec; ; class Date /声明Date类 public: Date(int,int,int); friend void Timedisplay(Date ,void Timedisplay(Date ,运行时输出: 12/25/2004 (输出Date类对象d1中的私有数据) 10:13:56 (输出Time类对象t1中的私有数据) 在本例中定义了两个类Time和Date。程序第3行是对Date类的声明,因为在第7行和第16行中对display函数的声明和定义中要用到类名Date,而对Date类的定义却在其后面。 在一般情况下,两个不同的类是互不相干的。在本例中,由于在Date类中声明了Time类中的display成员函数是Date类的“朋友”,因此该函数可以引用Date类中所有的数据。请注意在本程序中调用友元函数访问有关类的私有数据方法。,不仅可以将一个函数声明为一个类的“朋友”,而且可以将一个类(例如B类)声明为另一个类(例如A类)的“朋友”。这时B类就是A类的友元类。友元类B中的所有函数都是A类的友元函数,可以访问A类中的所有成员。 不讲!,9.10.2 友元类,本章概念总结: 构造函数,析构函数; 对象指针,this指针; 复制构造函数 静态数据成员,静态函数成员 友员,友员函数,友员成员函数 掌握其作用与使用规则,
展开阅读全文