谭浩强版《C++程序设计》第9章.ppt

上传人:xt****7 文档编号:20379914 上传时间:2021-03-14 格式:PPT 页数:89 大小:476.50KB
返回 下载 相关 举报
谭浩强版《C++程序设计》第9章.ppt_第1页
第1页 / 共89页
谭浩强版《C++程序设计》第9章.ppt_第2页
第2页 / 共89页
谭浩强版《C++程序设计》第9章.ppt_第3页
第3页 / 共89页
点击查看更多>>
资源描述
第 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 Time set_time( ) /定义成员函数,向数据成员赋值 cinhour; cinminute; cinsec; void Time show_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的时、分、秒值 ) 也可以 在类外定义构造函数: Time Time( ) /要加上类名 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; ; Box Box(int h,int w,int len) /在类外定义带参数的构造函数 height=h; width=w; length=len; int Box volume( ) /定义计算体积的函数 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中定义构造函数可以改 用以下形式: Box Box(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; ; Box Box( ) /定义一个无参的构造函数 height=10; width=10; length=10; int Box volume( ) 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; ; Box Box(int h,int w,int len) /在定义函数时可以不指定默认参数 height=h; width=w; length=len; int Box volume( ) 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 Box volume( ) 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 Time get_time( ) couthour:minute:sechour 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的地址赋给 p1, p1指向 t1.hour cout*p1endl; /输出 t1.hour的值 例 9.7 有关对象指针的使用方法。 #include using namespace std; class Time public: Time(int,int,int); int hour; int minute; int sec; void get_time( ); /声明公有成员函数 ; Time Time(int h,int m,int s) hour=h; minute=m; sec=s; void Time get_time( ) /定义公有成员函数 couthour:minute: secendl; int main( ) Time t1(10,13,56); /定义 Time类对象 t1 int *p1= /定义指向整型数据的指针变量 p1, 并使 p1指 向 t1.hour cout*p1get_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 Box volume( ) return (height*width*length); C+把它处理为 int Box volume(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; ; Box Box(int h,int w,int len) height=h; width=w; length=len; int Box volume( ) 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. Box Box(const Box width=b.width; length=b.length; 复制构造函数也是构造函数,但它只有一个参数, 这个参数是本类的对象 (不能是其他类的对象 ),而 且采用对象的引用的形式 (一般约定加 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); 类名 (类名 /复制构造函数的声明,如 Box(Box (2) 在建立对象时,实参类型不同。系统会根据实 参的类型决定调用普通构造函数或复制构造函数。 如 Box box1(12,15,16); /实参为整数,调用普通构造函数 Box box2(box1); /实参是对象名,调用复制构造函数 ( 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 Box height=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; ; Box Box(int w,int len) /通过构造函数对 width和 length赋初值 width=w; length=len; int Box volume( ) return(height*width*length); int Box height=10; /对静态数据成员 height初始化 int main( ) Box a(15,20),b(20,30); couta.heightendl; /通过对象名 a引用静态数据成员 coutb.heightendl; /通过对象名 b引用静态数据成员 coutBox heightendl; /通过类名引用静态数据成员 couta.volume( )endl; /调用 volume函数,计算体积,输出结果 上面 3个输出语句的输出结果相同 (都是 10)。所有对 象的静态数据成员实际上是同一个数据成员。 请注意: 在上面的程序中将 height定义为公用的静 态数据成员,所以在类外可以直接引用。 在类外 可以通过对象名引用公用的静态数据成员, 也可以通过类名引用静态数据成员。 即使没有定义 类对象,也可以通过类名引用静态数据成员 。 这说明 静态数据成员并不是属于对象的,而是属于 类的,但类的对象可以引用它 。 类内可随便访问。 如果静态数据成员被定义为私有的,则不能在类外 直接引用,而必须通过公用的成员函数引用 。 (5) 静态数据成员的作用 :各对象之间的数据有了 沟通的渠道,实现数据共享,因此可以不使用全局 变量。 全局变量破坏了封装的原则,不符合面向对 象程序的要求 。 注意公用静态数据成员与全局变量的不同, 静态数 据成员的作用域只限于定义该类的作用域内 在此作 用域内,可以通过类名和域运算符 “ ” 引用静态 数据成员,而不论类对象是否存在。 成员函数也可以定义为静态的,在类中声明函数的 前面加 static就成了静态成员函数。如 static int volume( ); 和静态数据成员一样, 如果要在类外调用公用的静 态成员函数,要用类名和域运算符“ ”。 如 Box volume( ); 实际上也允许通过对象名调用静态成员函数,如 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 Student total( ) /定义非静态成员函数 sum+=score; /累加总分, 类内访问静态数据成员 count+; /累计已统计的人数, 类内访问静态数据成员 float Student average( ) /定义静态成员函数 return(sum/count); /只能访问静态数据成员 float Student sum=0; /对静态数据成员初始化 int Student count=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 Student average( )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 /声明 display函数为 Time类的友元函数 private: /以下数据是私有数据成员 int hour; int minute; int sec; ; Time Time(int h,int m,int s) /构造函数,给 hour,minute,sec赋初值 hour=h; minute=m; sec=s; void display(Time int main( ) Time t1(10,13,56); display(t1); return 0; /调用 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 /display是成员函数,形参是 Date类对象的引用 private: int hour; int minute; int sec; ; class Date /声明 Date类 public: Date(int,int,int); friend void Time display(Date /声明 Time中的 display函数 为本类的友元成 员函数 private: int month; int day; int year; ; Time Time(int h,int m,int s) /类 Time的构造函数 hour=h; minute=m; sec=s; void Time display(Date /引用 Date类对象 中的私有数据 couthour:m
展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 图纸专区 > 课件教案


copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!