版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第4章类与对象4.7类的友元4.8名字空间域和类域*4.9UML图形标识4.10程序实例——公司人员管理程序本章小结习题上一页返回第4章类与对象类是面向对象程序设计中实现信息封装的基础。类是一种用户定义类型,也称为类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。本章将在介绍面向对象技术的基本理论、类的基本特点的基础上,详细介绍类的定义、实现以及类的应用。下一页返回第4章类与对象■学习目标①理解类与对象的概念,掌握类的定义,能根据需求设计类。②理解对象在内存中的存储结构。③掌握对象的各种成员的使用方法。④掌握构造函数、析构函数与复制构造函数的概念、作用,理解其调用过程与顺序。⑤理解类的静态成员的作用,并掌握其使用方法。⑥理解常对象的作用与应用。⑦理解类的友元的概念、作用,并掌握友元的应用。⑧理解友元函数与友元类的概念。⑨理解名字空间的概念、作用,会使用名字空间上一页返回*4.1从面向过程到面向对象4.1.1传统的面向过程的结构化程序设计20世纪70年代,面向过程的结构化程序设计逐步成为程序设计的主流。结构化程序设计的提出与发展是伴随着软件的日益庞大和复杂进行的,但是当软件复杂到一定程度后,结构化程序设计也不能满足需要。当软件超过一定规模后,采用结构化程序设计,其开发和维护就变得越来越难以控制。根本原因在于面向过程的结构化程序设计方法与现实世界(包括主观世界和客观世界)往往不一致,结构化程序设计的思想难以实现。结构化程序设计提出的“自顶向下,逐步求精”的思想是完全正确的。通常处理一件事时,总是先分析事物,抓住其主要矛盾,然后再逐层分解,把复杂的事物分解成一个个相对简单的事物,逐一进行处理,下一页返回*4.1从面向过程到面向对象彻底解决问题。这里的具体操作方法——模块化是按功能来分的,称为功能块,或者说是从事物中抽象出来的一般操作,在C++中称为一个函数解决一个问题。一个函数实现一个功能或一个操作。对于这样的功能块的具体要求是独立性要强,即模块内部的联系要紧密,模块间的联系要弱。模块内部各功能块之间的联系称为内聚度,内聚度最高的模块是功能组合模块,该模块的各个组成部分全部为执行同一功能而存在,并且只执行一个功能。影响模块之间联系的因素有两个:一是模块之间的连接形式,二是模块之间接口的复杂性。最好的是数据连接,这是指两个模块之间的通信信息是若干个数据的连接形式。由于两者之间没有控制信号的上一页下一页返回*4.1从面向过程到面向对象交换,相互影响最小,同时模块之间传递的信息要尽可能少。全局变量会加强模块间的联系,所以要求尽量使用局部变量,基本不用全局变量。但是对于以功能抽象为基础的结构化程序设计,当程序规模和复杂性达到一定程度时,不可避免地会引入大量的全局变量,一个数万行的程序中全局变量所占的内存很轻易就可达到数十KB,也就是说,优良的模块化不能坚持到底,结果是程序的开发,特别是维护变得越来越难以控制。基于以上的原因,面向对象程序设计逐渐替代结构化程序设计成为主流的程序设计方法。4.1.2面向对象程序设计对象的概念是面向对象技术的核心所在。面向对象技术中的对象就是上一页下一页返回*4.1从面向过程到面向对象现实世界中某个具体的物理实体在计算机世界中的映射和体现。比如一部手机是现实世界中的一个实体。它由天线、发射部件、接收部件、显示屏、按键、专用集成电路芯片及外壳组成。它有着实在的功能,可以打电话,可以发短消息,可以存储、输入和编辑各种个人信息,甚至可以上网。这样一个实体可以在计算机世界中映射为一个计算机可以理解、可以操纵、具有前面所叙述的属性和操作的对象。现实世界中的实体可以抽象出类别的概念。比如草原上有很多马匹,它们之间有不同、也有相同的特性,据此可以抽象出马这样一个概念。人们只能看到某一匹具体的马,而不能见到抽象的马。对于计算机世界就有一个类的概念,因为类是一个抽象概念的对应体,所以计算机不给它分配内存,只给对象分配内存。如图4-1所示为计算机世界与上一页下一页返回*4.1从面向过程到面向对象现实世界之间的对应关系。类是一个抽象的概念,用来描述该类对象所共有的、本质的属性和行为。任何一个对象都是这个类的一个具体实现,称为实例。同类对象之间具有相同的属性和行为。自然界是由各种各样的对象组成的,这些对象之间通过信息传递产生相互作用,构成富有生机的世界。对象之间产生相互作用时所传递的信息称作消息。比如手机和人是两个对象,人按下手机上的按钮就是向手机发送消息,手机接收到消息后,按照消息及其参数执行相应的操作。在面向对象的程序设计中,对象之间的相互作用也是通过消息机制实现的。在传统的面向过程的结构化程序设计中采用的是把现实世界的问题抽象成计算机可以理解和处理的数据结构的方法,或者说使现实世界向计算机世界靠拢;而面向对象程序设计的思想是要用计算机逻辑来模拟现实世界的存在,上一页下一页返回*4.1从面向过程到面向对象是使计算机世界向现实世界靠拢。这一思路使程序员可以用更接近人的自然思维模式和更接近现实世界本来面目的方法来设计程序,使以后的程序维护更加顺利,避免了面向过程设计所遇到的困难。面向对象程序设计具有以下几个特点。1.抽象抽象,是人类认识问题的最基本手段之一。面向对象方法中的抽象,是指对具体问题(对象)进行概括,抽出一类对象的公共性质并加以描述的过程。抽象的过程,也是对问题进行分析和认识的过程。在面向对象的软件开发中,首先注意的是问题的本质及描述,其次是解决问题的具体过程。一般来讲,对一个问题的抽象应该包括两个方面:数据抽象和行为抽象(或称为功能抽象、代码抽象)。前者描述某类对象的属性或状态,也就是此类对象上一页下一页返回*4.1从面向过程到面向对象区别于彼类对象的特征;后者描述的是某类对象的共同行为或功能特征。例如,世界上有各种各样的手机,外形不同,功能也存在差异。但只要是手机,总有天线,总有显示屏,总有按键。这就是对手机所具有的数据进行抽象。另外,不管什么手机总能完成接收短信、接打电话等操作,这就是对它的行为的抽象。2.封装性封装就是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的函数代码进行有机的结合,形成“类”,其中的数据和函数都是类的成员。封装特性还体现在将一部分成员充当类与外部的接口,而将另一部分成员“隐藏”起来,从而达到对类成员访问权限的合理控制,使不同类之间的相互影响减少到最低限度,提高数据的安全性和简化上一页下一页返回*4.1从面向过程到面向对象程序的设计工作量。就像手机,大量的电子零部件、天线封装在机壳内,提供给用户的只是按键和显示屏。用户只需根据规则按键,手机就可进行工作。这样定义完好的类一旦建立,就可作为一个整体单元使用,用户不需要知道这个类是如何工作的,只需要知道如何使用即可。3.继承和派生性以手机为例,如果已经定义了手机类,那么现在需要定义某一品牌的手机时,通常不必再重复描述属于所有手机共有的特征,而是在继承手机类的基础上,描述出品牌手机的新的特征。这时可以称品牌手机继承了手机类,也可以称是由手机类派生出来的。面向对象程序设计提供了类似的机制。当定义了一个类后,又需定义一个新类,这个新上一页下一页返回*4.1从面向过程到面向对象类与原来类相比,只是增加或修改了部分属性和操作,这样,在定义新类时只需说明新类继承原来类,然后描述出新类所特有的属性和操作即可。称原来类为基类,由它派生出来的类称为子类或派生类。由基类派生出子类,子类还可继续派生它的子类,如此下去,可以形成树状派生关系,称为派生树或继承树。继承性可以简化人们对问题的认识和描述,同时在开发新程序和修改源程序时最大限度地利用已有的程序,提高了程序的可重用性和设计生产效率。4.多态性多态性指的是同样一个消息被不同对象接收时产生不同的结果。系统提供的这种机制主要用在具有继承关系的类体系中。一个类体系中的上一页下一页返回*4.1从面向过程到面向对象不同对象可以用不同方式响应同一消息,并产生不同结果,实现“同一接口,多种方法”。C++中的多态性,实际上也是对人类思维方式的一种直接模拟。例如,日常生活中经常说“打球”、“打水”、“打车”等,这个“打”字就表示了一个抽象的信息。同样一个“打”字,这里具有不同的含义,发生的实际动作也相去甚远。面向对象程序设计的着眼点是对象,程序设计的核心是从问题中抽象出合适的对象,即首先解决“做什么”的问题。至于“怎么做”,则由操作方法的设计完成并封装在对象内部。而对于操作方法的设计,核心仍然是算法的设计,这完全吸收了结构化程序设计的思想。上一页返回4.2类与对象的基本概念本节将引入类与对象的最基本的概念:类的定义、对象的创建与使用。4.2.1C++类的定义和类成员的访问控制在前面几章中讨论了基本数据类型、数组和枚举类型。其中,后两者是导出的数据类型,是由用户自己定义、自己按规则构造的,但其基本组成单位都是同一种数据类型。然而客观事物是复杂的,要描述它们必须从多方面进行,需要用不同的数据类型来描述不同的方面。如商场中的商品可以描述为:商品名称(用字符串描述)、该商品的数量(用整型数描述)、该商品的单价(用浮点数描述)、该商品的总价(用浮点数描述)。同时还必须考虑对商品的操作:输入商品名称、单价、数量、计算总价,显示商品库存情况等。这里用了属于3种不同数据类型的4个数据成员(datamember)来下一页返回4.2类与对象的基本概念描述一种商品。在C++中可以这样表述:classCGoods{public:charName[21];intAmount;floatPrice;floatTotal_value;};//最后的分号不可少定义一个类的语法格式为:class类名{[[private:]上一页下一页返回4.2类与对象的基本概念成员表1;][public:
成员表2;][protected:
成员表3;]};其中,“class类名”称为类头(classhead);“{}”中的部分称为类体(classbody),类体中定义了类成员表(classmemberlist)。成员包括数据成员和函数成员(functionmember)。现已定义的商品类还没有包括类定义的更关键部分——对数据成员的操作。这可以用6个函数来完成:上一页下一页返回4.2类与对象的基本概念voidRegisterGoods(char[],int,float); //输入数据voidCountTotal(void); //计算商品总价值voidGetName(char[]); //读取商品名intGetAmount(void); //读取商品数量floalGetPrice(void); //读取商品单价floatGetTotal_value(void); //读取商品总价值这些函数用于实现对数据的操作,可以把它们与数据成员封装在一起,于是得到了新的商品类:classCGoods{private:charName[21];上一页下一页返回4.2类与对象的基本概念intAmount;floatPrice;floatTotal_value;public:voidRegisterGoods(char[],int,float);voidCountTotal(vold);voidGetName(char[));intGetAmounl(void);floatGetPrice(void);floalGetTotal_value(void);};上一页下一页返回4.2类与对象的基本概念在类中引进了函数成员,或称为成员函数,即函数也成了类中的一员。从逻辑上完成了类的封装。类把数据(事物的属性)和函数(事物的行为——操作)封装为一个整体。与第2章结构类型的定义不同,类的定义中还规定了类成员的访问控制。为了理解类成员的访问权限,我们还是先来看上面商品这个的例子。不管是什么商品,都有商品名称、单价、数量、总价。通常商店的管理者肯定不希望所有人都能随心所欲地修改上述数据,对商品数据的访问只能通过规定的接口进行。因此需要对数据的访问加以一定的限制,这便是类成员访问控制。在类中对成员访问权限的控制是通过设置成员的访问控制属性而实现的。访问控制属性可以有以下3种:公有类型(public)、私有类型(private)和保护类型(protected)。上一页下一页返回4.2类与对象的基本概念①public类型成员定义了类的外部接口。公有成员用该关键字声明,在类外只能访问类的公有成员。商品类中的所有成员函数都是公有类型。将公有类型的成员看成是类的外部接口。通过这些接口访问内部的私有类型、保护类型的成员。②private类型定义了类的私有成员。如果私有成员紧接着类名称,则关键字private可以省略。私有成员只能被本类的成员函数访问,来自类外部的任何访问都是非法的。这样,私有成员就完全隐蔽在类中,保护了数据的安全性。商品类中的所有数据成员都是私有成员,对这些私有成员的访问都只能通过public类型成员函数进行。③protected类型成员的性质和私有成员的性质相似,其差别在于继承过程中对产生的新类影响不同。这个问题将在第8章详细介绍。图上一页下一页返回4.2类与对象的基本概念4-2类成员的访问控制用图4-2可以形象地描述对类的成员的访问控制属性:将需要隐蔽的成员设为私有类型,成为一个外部无法访问的黑盒子;将提供给外界的接口设为公有类型,对外部是透明的;而保护成员就相当于一个笼子,它给派生类提供一些特殊的访问属性。在定义一个类时应注意以下两点:①类是一种数据类型,定义时系统并不为类分配内存空间,所以不能对类的数据成员进行初始化。当然类中的任何数据成员也不能使用关键字extern、auto或register限定其存储类型。②成员函数可以直接使用类定义中的任一成员,可以处理数据成员,也可调用函数成员。私有的数据成员只有通过类的接口(公有上一页下一页返回4.2类与对象的基本概念函数)才能从外部对其进行处理。由于历史的原因,类定义也称为类声明,是一个说明语句,最后必须加分号。本书中只要是用户自定义类型的说明,一律称为定义。只有对函数格式的说明才称为声明或函数声明。4.2.2成员函数的定义在4.2.1节中,只对成员函数做了一个声明,并没有对函数进行定义。成员函数的定义通常在类定义的外部进行,其格式如下:返回值类型类名::函数名(参数表){…}其中,运算符“::”称为域解析运算符,它指出该函数是属于某个类的成员函数。类CGoods的函数可以按如下方法定义:上一页下一页返回4.2类与对象的基本概念voidCGoods::RegisterCGoods(charname[],intamount,floatprice){strcpy(Name,name);//字符串复制函数
Amount=amount;Price=price;}voidCGoods::CountTotal(void){Total_value=Price*Amount;}voidCGoods::GetName(charname[]){strcpy(name,Name);}intCGoods::GetAmount(void){return(Amount);}floatCGoods::GetPrice(void){return(Price);}上一页下一页返回4.2类与对象的基本概念floatCGoods::GetTotal_value(void){return(Total_value);}也可以在类定义中直接定义函数,但是系统在处理上是不同的。它们的差异就如同普通函数和内联函数的差异一样,体现在代码的存储上,参见4.2.3节。但在定义类时,并不分配内存,要到创建类的对象时才会按不同的方式分配内存。4.2.3对象的创建与使用正如变量是数据类型的实例一样,对象是类的实例。定义一种数据类型只是告诉编译器该数据类型的结构形式,并不会为数据类型分配内存,或者说并没有创建可用来存放数据的变量,也只是一种新的数据类型,按此类型可以在内存中建立一个同样结构的实例——对象。上一页下一页返回4.2类与对象的基本概念创建类的对象有两种常用方法,第1种类似变量的定义,进行直接定义类的实例——对象。语法格式为:类名对象名1[,对象名2,…对象名n];例如:CGoodsCar;这个定义创建了CGoods类的一个对象Car,同时为它分配了属于它自己的存储空间,用来存放数据和对这些数据实施操作的成员函数(代码)。与定义变量一样,一个对象只在定义它的域中有效。第2种是采用动态创建类的对象的方法,这将在第6章中介绍。动态是指在程序运行时建立对象。而第1种方法则是在编译时(程序运行前)建立对象。上一页下一页返回4.2类与对象的基本概念一个样板可以制造出无数相同的物品,同样,一个类可以定义出无数同样结构的对象。存储对象有两种方法。如图4-3所示是系统为每一个对象的所有成员分配了的内存空间,包括存放成员数据的数据区和存放成员函数的代码区。仔细分析后会发现:区别同一个类的各个不同对象的属性是由数据成员决定的,不同对象的数据成员的内容是不一样的。而行为(操作)是用函数来描述的,这些操作的代码对所有的对象都是一样的。如果每个对象的数据占内存1KB,代码占50KB,则100个对象共占5100KB,其中重复的代码占4950KB。如图4-4所示的方案是仅为每个对象分配一个数据区,代码区被各对象共用,同样是100个对象,只需要150KB。上一页下一页返回4.2类与对象的基本概念图4-3对应的是在类定义中定义函数,而图4-4对应的是在类定义外部定义函数。所以在绝大多数场合,总是在类定义外部定义函数,而仅在类定义中给出函数的声明。如果在类定义外部定义函数,使用关键字inline,则系统也会采用内联扩展方法实现,这时每个对象都有该函数的一份独立的代码。如RegisterGoods函数可定义为:inlinevoidCGoods::RegisterGoods(charname[],intamount,floatprice){strcpy(Name,name);Amount=amount;Price=price;}则所定义的每个对象都有RegisterGoods函数的一份独立的代码。上面所述的对象存储方式是物理的,是由计算机来完成的,它并不上一页下一页返回4.2类与对象的基本概念影响类在逻辑上的封装。程序设计是一个逻辑的概念。从程序员的角度上看,逻辑上各对象是完全独立的,不必去管物理上是怎样存储的,即类的封装在逻辑上是完善的。定义了对象后,对对象成员的访问采用成员运算符“.”进行(类似于结构)。对象名.数据成员或对象名.函数成员(实参表)当然这些成员必须是公有的成员,只有公有成员才能在类外通过对象进行访问。[例4-l]商品类对象的应用实例。上一页下一页返回4.2类与对象的基本概念#include<iostream>#include<iomanip>#include<cstring>usingnamespacestd;classCGoods{private: char Name[21]; int Amount; float Price; float Total_value;public:上一页下一页返回4.2类与对象的基本概念void RegisterGoods(char[],int,float); void CountTotal(void); void GetName(char[]); int GetAmount(void); float GetPrice(void); float GetTotal_value(void);};voidCGoods::RegisterGoods(charname[],intamount,floatprice){ strcpy(Name,name);Amount=amount;Price=price;}voidCGoods::CountTotal(void){ Total_value=Price*Amount;}上一页下一页返回4.2类与对象的基本概念voidCGoods::GetName(charname[]){ strcpy(name,Name);}intCGoods::GetAmount(void){ return(Amount);}floatCGoods::GetPrice(void){ return(Price);}floatCGoods::GetTotal_value(void){ return(Total_value);}intmain(){ CGoods car; charstr[21];上一页下一页返回4.2类与对象的基本概念 int number; floatpr; cout<<"请输入汽车型号:"; cin.getline(str,20);//输入串长必须小于20 cout<<"请依次输入汽车数量与单价:"; cin>>number>>pr; car.RegisterGoods(str,number,pr); car.CountTotal(); str[0]='\0';//字符串str清零
car.GetName(str);//string赋值car.Name cout<<setw(20)<<str<<setw(5)<<car.GetAmount()<<endl;//A上一页下一页返回4.2类与对象的基本概念 cout<<car.GetPrice()<<setw(20)<<car.GetTotal_value()<<endl;//B return0;}在上面的例子中,如果将A行和B行写成:cout<<setw(20)<<car.Name<<setw(5)<<car.Amount;cout<<setw(10)<<car.Price<<setw(20)<<car.GetTotal_value<<endl;是错误的,因为这里的对象car的4个数据成员全是私有的,在外部是不能直接访问的,必须用对象car所带的公有函数进行访问。上一页返回4.3构造函数和析构函数因为定义类只是定义一种新的类型,因此不涉及类成员的初始化。初始化只能在定义对象时进行。因为数据成员大多是私有的,要对它们进行初始化,必须用一个公有函数来完成,同时该函数应该在且仅在定义对象时自动执行一次。在C++程序设计语言中,这个函数称为构造函数。为了要理解构造函数,首先需要理解对象的建立过程。为此先来看看一个基本类型变量的初始化过程:每一个变量在程序运行时都要占据一定的内存空间,在声明一个变量时对变量进行初始化,这就意味着在为变量分配内存单元的同时,在其中写入了变量的初始值。这样的初始化在C++源程序中看似很简单,但是编译器却需要根据变量的类型自动产生一些代码来完成初始化过程。下一页返回4.3构造函数和析构函数对象的建立过程也是类似的:在程序执行过程中,当遇到对象声明语句时,程序会向操作系统申请一定的内存空间用于存放新建的对象。我们希望程序能像对待普通变量一样,在分配内存空间的同时对数据成员初始化。但是做到这一点不那么容易,因为与普通变量相比,类的对象毕竟太复杂了,编译器不知道如何产生代码来实现初始化。因此,如果需要进行对象初始化,程序员就要编写初始化程序。如果程序员没有自己编写初始化程序,却在声明对象时指定对象初始值,不仅不能实现初始化,还会引起编译时的语法错误。尽管如此,C++编译系统在对象初始化的问题上还是替我们做了很多工作。C++严格规定了初始化程序的接口形式,并有一套自动的调用机制。这里所说的初始化程序便是类的构造函数。上一页下一页返回4.3构造函数和析构函数构造函数的作用就是在对象被创建时利用特定的值构造对象,将对象初始化为一个特定的状态。构造函数也是类的一个成员函数,除了具有一般成员函数的特征之外,还具有一些特殊的性质。4.3.1构造函数的定义与使用构造函数是特殊的公有成员函数,具有如下特征:①函数名与类名相同,如CGoods类的构造函数名为CGoods。构造函数通常被声明为公有成员。②构造函数没有返回类型说明。注意是没有而不是void,因此函数名前不能写void。因为构造函数是隐式(自动)调用的,有一个固定格式,编译器才可以正确判断哪些是构造函数。③在程序运行过程中,当新的对象被建立时,该对象所属的类的上一页下一页返回4.3构造函数和析构函数构造函数自动被调用,在该对象生命期中也只调用一次。④构造函数可以重载。即类定义中可以根据需要设计多个构造函数,它们由不同的参数表区分,系统在自动调用时按一般函数重载的规则选择一个并执行。⑤构造函数可以在类中定义,也可以在类外定义。⑥如果类定义中没有给出任何构造函数,则C++编译器会自动建立一个称为默认的构造函数,其格式为:类名(){}该函数只是一种标准的形式,并不做任何工作。如[例4-1]中编译器就会为CGoods生成一个默认的构造函数。如果类中声明了构造函数(无论有无参数),编译器便不会再为之上一页下一页返回4.3构造函数和析构函数建立任何形式的构造函数。一般把程序员编写的无参的或者各参数均有默认值的构造函数都称为默认的构造函数。下面编写4.2节中商品类CGoods的构造函数。可以用3个参数实现对4个数据成员的初始化:CGoods(charname[],intamount,floatprice){strcpy(Name,name);Price=price;Amount=amount;Total_value=price*amount}在实际应用时,也可以只用两个参数:货名和单价。这时的构造函数为:上一页下一页返回4.3构造函数和析构函数CGoods(charname[],floatprice){strcpy(Name,name);Price=price;Amount=0;Total_value=0.0}这里定义了CGoods类的两个构造函数,形成了构造函数的重载。如果定义对象时的格式为:CGoodsCar1("夏利2000",30,98000.0);则调用了CGoods中的第1个构造函数。如果定义对象时的格式为:上一页下一页返回4.3构造函数和析构函数CGoodsCar2("桑塔纳2000",164000.0);则调用的是第2个构造函数,参数为两个。也可以显式地把构造函数表示出来的方式定义对象,如:CGoodsCar1=CGoods("夏利2000",30,98000.0);如果希望初始化时不带任何参数,则可做如下定义:CGoods(){Name[0]='\0';Price=0.0;Amount=0;Total_value=0.0;}但是定义对象时不能加括号。例如:CGoodsCar3,Car4();上一页下一页返回4.3构造函数和析构函数其中,Car3是类CGoods的对象,定义时调用不带参数的构造函数,但是Car4()不是表示建立一个对象,而是表示为一个不带参数的函数,它的返回值是类CGoods的对象。[例4-2]完整商品类对象的应用实例。#include<iostream>#include<iomanip>#include<cstring>usingnamespacestd;classCGoods{private: char Name[21];上一页下一页返回4.3构造函数和析构函数int Amount; float Price; float Total_value;public: CGoods(); CGoods(char[],int,float); CGoods(char[],float); void RegisterGoods(char[],int,float); void CountTotal(void); void GetName(char[]);上一页下一页返回4.3构造函数和析构函数 int GetAmount(void); float GetPrice(void); float GetTotal_value(void);};CGoods::CGoods(){ Name[0]='\0';Price=0.0; Amount=0;Total_value=0.0;}CGoods::CGoods(charname[],intamount,floatprice){ strcpy(Name,name); Price=price; Amount=amount; Total_value=price*amount;}CGoods::CGoods(charname[],floatprice){上一页下一页返回4.3构造函数和析构函数 strcpy(Name,name); Price=price; Amount=0; Total_value=0.0;}voidCGoods::RegisterGoods(charname[],intamount,floatprice){ strcpy(Name,name); Amount=amount;Price=price;}voidCGoods::CountTotal(void){ Total_value=Price*Amount;}voidCGoods::GetName(charname[]){ strcpy(name,Name);}intCGoods::GetAmount(void){ return(Amount);}上一页下一页返回4.3构造函数和析构函数floatCGoods::GetPrice(void){ return(Price);}floatCGoods::GetTotal_value(void){ return(Total_value);}intmain(){ charstring[21]={'\0'}; CGoodsCar1("夏利2000",30,98000.0); CGoodsCar2("桑塔纳2000",164000.0); CGoodsCar3; Car1.GetName(string); cout<<setw(20)<<string<<setw(5)<<Car1.GetAmount();上一页下一页返回4.3构造函数和析构函数cout<<setw(10)<<Car1.GetPrice()<<setw(20)<<Car1.GetTotal_value()<<endl; Car2.GetName(string); cout<<setw(20)<<string<<setw(5)<<Car2.GetAmount(); cout<<setw(10)<<Car2.GetPrice()<<setw(20)<<Car2.GetTotal_value()<<endl; Car3.GetName(string); cout<<setw(20)<<string<<setw(5)<<Car3.GetAmount(); cout<<setw(10)<<Car3.GetPrice()<<setw(20)<<Car3.GetTotal_value()<<endl;return0;}上一页下一页返回4.3构造函数和析构函数在该例中,3个对象在建立时分别调用了3个不同的构造函数。4.3.2析构函数的定义与使用定义一个对象时,C++会自动调用构造函数建立该对象并进行初始化,当一个对象的生命期结束时,C++也会自动调用一个特殊的成员函数进行对象生命期结束时的善后工作,这个特殊的成员函数即为析构函数。析构函数具有以下特征:①析构函数名由“~”符加类名构成。如CGoods类的析构函数名为~CGoods()。析构函数通常被声明为公有成员。②和构造函数一样,析构函数无函数返回类型。析构函数不带任何参数,因此析构函数无重载形式。③析构函数也可以为默认。默认的析构函数形式为:上一页下一页返回4.3构造函数和析构函数类名::~类名(){}显然,默认的析构函数也不做任何工作。④与构造函数相对应,析构函数在对象生命期结束时由系统自动调用。[例4-3]矩形类。要确定一个矩形(四边都是水平或垂直方向,不能倾斜),只要确定其左上角和右下角的x和y坐标即可,因此应包括4个数据成员(1eft、top、right、bottom),即左、上、右、下4个边界值。由构造函数给数据成员赋值。赋值函数完成未初始化的矩形赋值,也可以修改各数据成员,并用多文件实现。//rect.h文件lassRectangle{上一页下一页返回4.3构造函数和析构函数intleft,top; intright,bottom;public: Rectangle(intl=0,intt=0,intr=0,intb=0); ~Rectangle(){};//析构函数,在此函数体为空
voidAssign(intl,intt,intr,intb); voidSetLeft(intt){left=t;} voidSetRight(intt){right=t;} voidSetTop(intt){top=t;} voidSetBottom(intt){bottom=t;} voidShow();上一页下一页返回4.3构造函数和析构函数};//rect.cpp文件#include<iostream>#include"rect.h"usingnamespacestd;Rectangle::Rectangle(intl,intt,intr,intb){ left=l;top=t; right=r;bottom=b;}voidRectangle::Assign(intl,intt,intr,intb){ left=l;top=t;上一页下一页返回4.3构造函数和析构函数 right=r;bottom=b;}voidRectangle::Show(){ cout<<"left-toppointis("<<left<<","<<top<<")"<<'\n'; cout<<"right-bottompointis("<<right<<","<<bottom<<")"<<'\n';}//ex4-2.cpp文件#include<iostream>#include"rect.h"usingnamespacestd;上一页下一页返回4.3构造函数和析构函数intmain(){ Rectanglerect; cout<<"由缺省的构造函数生成的rect:"<<endl; rect.Show(); rect.Assign(100,200,300,400); cout<<"由赋值函数处理后的rect:"<<endl; rect.Show(); Rectanglerect1(0,0,200,200);上一页下一页返回4.3构造函数和析构函数cout<<"由构造函数生成的rect1:"<<endl; rect1.Show(); return0;}细心的读者会注意到与构造函数相比,似乎析构函数并没有作用。其实,这是由于在初学阶段还未能接触到析构函数的真正作用,在以后的章节中会介绍析构函数的作用。上一页返回4.4复制构造函数本节将讨论类的一种特殊的构造函数——复制构造函数。4.4.1复制构造函数的定义我们都使用过复印机,需要一份文件的副本时,通过复印机,复制出与原件一模一样的复制品。可以说,在日常生活中利用原件复制出和原件一样的复制品的例子是数不胜数的。面向对象程序设计就是要能够如实反映客观世界中各种问题的本来面目。同一个类的对象在内存中的结构完全相同,因此对象的复制,应该是C++系统必须具备的能力。生成一个对象的副本有两种途径,第一种途径是建立一个新对象,然后将一个已有对象的数据成员值取出来,一一赋给新的对象。这样做下一页返回4.4复制构造函数虽然可行,但不免繁琐。能否使类具有自行复制本类对象的能力呢?这正是我们希望复制构造函数的功能。类的复制构造函数声明和实现的语法格式如下:class类名{ public:
类名(类名&对象名); //复制构造函数声明
…};类名::类名(类名&对象名){ //复制构造函数实现
…}上一页下一页返回4.4复制构造函数复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本类的对象的引用。其功能是使用一个已经存在的对象(由复制构造函数的参数指定),去初始化同类的一个新对象。对于CGoods类,可以定义复制构造函数为:CGoods(Cgoods&cgd){strcpy(Name,cgd.Name);Price=cgd.price;Amount=cgd.Amount;Total_value=cgd.Total_value;}必须注意的是复制构造函数的形参必须是同类对象的引用。如果将该上一页下一页返回4.4复制构造函数形参说明为类对象,则会引起复制构造函数的无穷递归调用。因为按值传递一个对象时,会在被调函数中分配一块内存以建立与实参同类型的形参对象,再把实参对象的数据赋给形参对象。当建立形参对象时,形实对象间参数的传递又需要调用复制构造函数,这样就出现了新的一次调用复制构造函数,而新的一次调用复制构造函数又需要形实参数间的传递,这样就形成无穷递归。所以必须将复制构造函数的参数定义为一个类的对象的引用。如果没有为类定义复制构造函数,则C++编译器会自动建立一个默认的复制构造函数。该复制构造函数完成的工作是依次复制对象的所有数据成员,也称为默认的按成员语义初始化。因此如果复制构造函数仅需完成数据成员的对应复制,实际上不用显式定义。采用默认的上一页下一页返回4.4复制构造函数复制构造函数即可(如上面定义的CGoods类的复制构造函数)。但是在许多复杂情况下,就必须自行定义复制构造函数。在第6章中就会遇到。对于所有类对象,赋值运算符“=”称为默认的按成员复制赋值操作符,同类对象之间可以用“=”直接复制。通常情况下,这种按成员语义支持的“=”直接复制已经满足要求。但在一些复杂情况下,它就不能满足对象复制的要求,甚至产生安全性问题。这时就要求类的设计者提供特殊的复制构造函数和赋值操作符的定义。4.4.2复制构造函数的调用复制构造函数在以下3种情况中被调用。(1)当用类的一个对象去初始化另外一个对象时。例如:上一页下一页返回4.4复制构造函数CGoodsCar1("夏利2000",30,98000.00); //调用3个参数的构造函数CGoodsCar2=Car1; //A调用复制构造函数,Carl为实参CGoodsCar3(Car1); //B调用复制构造函数,Carl为实参在执行A、B行语句时,分别由Car2和Car3调用复制构造函数,复制构造函数的参数都是Carl对象本身。这样Car2、Car3和Car1对象的结果完全一样。(2)如果函数的形参是类的对象,调用该函数进行形实结合时。例如:上一页下一页返回4.4复制构造函数voiddisp(CGoodsC1){…}voidmain(){CGoodsCar1("夏利2000",30,98000.00);disp(Car1);在调用disp函数将实参对象Car1传递给形参对象C1时,调用复制构造函数(3)如果函数的返回值是类的对象,函数执行结束返回调用者时。例如:CGoodsf(){CGoodstempCar("夏利2000",30,98000.00);returntempCar;上一页下一页返回4.4复制构造函数}voidmain(){CGoodsCarCar=f();}为什么函数返回对象时,会调用复制构造函数呢?表面上函数f将tempCar返回给了主函数,但是和普通变量一样,tempCar是函数f的局部对象,离开f函数,tempCar就消失了,不可能在返回主函数后继续存在。所以在处理这种情况时编译系统会在主函数中创建一个临时的无名对象,该临时对象的生存期只在函数调用所处的表达式中,也就是表达式Car=f();中。执行语句returntempCar;时,实际上是先上一页下一页返回4.4复制构造函数调用复制构造函数将tempCar复制到临时对象中。函数f运行结束时对象tempCar消失,临时对象通过表达式Car=f();赋给Car。计算完这个表达式后,临时对象的使命也就完成了,该临时对象便自动消失。4.4.3成员对象与构造函数在类中,数据成员的类型可以是基本数据类型,还可以是自定义的类型,当然可以是类类型,即其他类的对象作为一个类的成员。这样的成员称为对象成员(或称成员对象),使用成员对象的技术称为组合(又称聚合),含有对象成员的类成为组合类(又称聚合类)。C++中将一个类的对象作为另一各类的成员,也是真实反映客观世界中各类问题的复杂性。在面向对象程序设计中,可以对复杂对象进行分解、上一页下一页返回4.4复制构造函数抽象,将一个复杂对象分解为简单子对象的组合,由比较容易理解和实现的子对象装配而成。复杂类和子类的呈现包含关系。在建立组合类对象时,不仅要对组合类对象进行初始化,还要对对象成员初始化。因此组合类的构造函数具有固定的语法格式。类名::构造函数名(参数总表):对象成员1(参数名表1),…,对象成员n(参数名表n){组合类构造函数体}其中,构造函数参数总表的部分称为成员初始化列表,用逗号隔开的是组合类中要初始化的各对象成员和相应的参数表。这些表中的参数名来自参数总表,相当于函数的实参,因此不需要在进行类型说明。成员初始化列表实际上是构造函数体的一部分,因此在构造函数原型声明时,这部分内容不能出现了。上一页下一页返回4.4复制构造函数对于类中需要初始化的数据成员,也可以套用以上格式,写在成员初始化列表中进行初始化。类名::构造函数名(参数表):数据成员l(初值1),…,数据成员n(初值n){…}组合类的构造函数执行顺序是:①依次调用各对象成员的构造函数。各对象成员的构造函数调用次序是对象在组合类中定义的顺序。②执行构造函数成员初始化列表中数据成员的初始化。③执行构造函数的函数体。因为析构函数没有参数,所以包含成员对象的组合类的析构函数形式上并无特殊之处,但是撤销该类对象时,会首先调用组合类本身的析构函数,再调用成员对象的析构函数,调用次序与构造函数的次序相反。上一页下一页返回4.4复制构造函数[例4-4]含有成员对象的组合类的构造函数。定义Point类表示平面中的一个点,定义线段组合类Line,包含两个Point的成员对象p1、p2。Line类具有计算两个点间线段长度的功能。#include<iostream>#include<cmath>usingnamespacestd;classPoint{public: Point(intxx=0,intyy=0){X=xx;Y=yy;} Point(Point&p); intGetX(){returnX;}上一页下一页返回4.4复制构造函数 intGetY(){returnY;}private: intX,Y;};Point::Point(Point&p){ X=p.X; Y=p.Y;}//组合类classLine{public: Line(Pointxp1,Pointxp2); Line(Line&);上一页下一页返回4.4复制构造函数doubleGetLen(){returnlen;}private: Pointp1,p2; //Point类的对象p1,p2 doublelen; };//组合类的构造函数Line::Line(Pointxp1,Pointxp2):p1(xp1),p2(xp2){ doubledeltx=double(p1.GetX()-p2.GetX()); doubledelty=double(p1.GetY()-p2.GetY()); len=sqrt(deltx*deltx+delty*delty);}上一页下一页返回4.4复制构造函数//组合类的复制构造函数Line::Line(Line&Seg):p1(Seg.p1),p2(Seg.p2),len(Seg.len){}//主函数voidmain(){ Pointmyp1(1,1),myp2(4,5); //建立Point类的对象
Lineline1(myp1,myp2); //建立Line类的对象
Lineline2(line1); //利用复制构造函数建立一个新对象
cout<<"Thelengthofthelineis:"; cout<<line1.GetLen()<<endl; cout<<"Thelengthoftheline2is:";上一页下一页返回4.4复制构造函数 cout<<line2.GetLen()<<endl;}程序运行结果Thelengthoftheline1is:5Thelengthoftheline2is:5与变量类似,对象也有全局、局部、静态的区别,也存在作用域、生命期、可见性问题。对象的作用域、生命期、可见性问题总结如下:①对于全局定义的对象,当程序进入主函数main()之前,对象就已经定义了,这时要调用构造函数;整个程序结束时全局对象生命期结束调用析构函数。②对于局部定义的对象,每当程序控制流到达该对象定义处时,上一页下一页返回4.4复制构造函数调用构造函数;当程序控制离开该局部域时,局部对象生命期结束调用析构函数。③对于定义的静态对象,在程序控制首次到达该对象定义处时,调用构造函数;当整个程序结束时静态对象生命期结束调用析构函数。最后作为构造函数和析构函数的总结,对于不同作用域的对象,构造函数和析构函数的调用时机见[例4-5]。[例4-5]演示对象创建和撤销的对应关系。#include<iostream>usingnamespacestd;classcomplex{private:上一页下一页返回4.4复制构造函数doublereal,image;public:complex(){//默认的构造函数
real=0.0; image=0.0; cout<<"Initializing00"<<endl; } complex(doubler,doublei=0.0){//带参数的构造函数
real=r; image=i; cout<<"Initializing"<<r<<'\t'<<i<<endl;上一页下一页返回4.4复制构造函数} complex(complex&com);//复制的构造函数声明
~complex(){//析构函数
cout<<"Destructor"<<endl; } voidassign(complexcom){ real=com.real;//先建立临时对象com image=com.image; }voidprint(){ cout<<real<<'+'<<image<<'i'<<endl;上一页下一页返回4.4复制构造函数 }};inlinecomplex::complex(complex&com){ //复制的构造函数说明
cout<<"Copy"<<com.real<<'\t'<<com.image<<endl; real=com.real; image=com.image;}complexfun(complex);complexglobal; //全局对象首先建立intmain(){上一页下一页返回4.4复制构造函数 cout<<"Enteringmain"<<endl; complexcom1,com2(5.6,7.5); complexcom3=com1; com3.print(); global.print(); com1=fun(com2); com1.print(); cout<<"Exitingmain"<<endl; return0;}complexfun(complexcom){上一页下一页返回4.4复制构造函数 cout<<"Enteringfunction"<<endl; global.assign(com); cout<<"Exitingfunction"<<endl;returnglobal;}请读者自行写出程序运行结果。4.4.4类的前项引用声明我们知道,C++的类应当先定义,然后再使用。但是在处理相对复杂的问题时,很可能遇到两个类相互引用的情况,这种情况也称为循环引用。例如:classA{上一页下一页返回4.4复制构造函数public;voidf(Bb); //这里将引起编译错误,因为B为未知类型};classB{public:voidg(Aa); //以A类对象a为形参的成员函数};这里类A的公有成员函数f的形式参数是类B的对象,同时类B的公有成员函数g也以类A的对象为形参。由于在使用一个类之前,必须首先定义该类,因此无论将哪一个类的定义放在前面,都会引起编译错误。解决这种问题的办法,就是使用前向引用声明。前向引用上一页下一页返回4.4复制构造函数声明,是在引用未定义的类之前,将该类的名字告诉编译器,使编译器知道那是一个类名。这样,当程序中使用这个类名时,编译器就不会认为是错误,而类的完整定义可以在程序的其他地方。在上述程序加上前向引用声明,问题就解决了。classB; //前向引用声明classA{ //A类的定义public:voidf(Bb); //以B类对象b为形参的成员函数};classB{ //B类的定义public:上一页下一页返回4.4复制构造函数voidg(Aa); //以A类对象a为形参的成员函数};使用前向引用声明虽然可以解决一些问题,但它并不是万能的。需要注意的是,尽管使用了前向引用声明,但是在提供一个完整的类定义之前,不能定义该类的对象,也不能在内联成员函数中使用该类的对象。请看下面的程序段:classB; //前向引用声明classA{Bx; //错误:类B的定义尚不完善};classB{上一页下一页返回4.4复制构造函数Ay;};对于这段程序,编译器将指出错误。原因是对类名B的前向引用声明只能说明B是一个类名,而不能给出该类的完整定义,因此在类A中就不能定义类B的对象作为数据成员。因此使两个类以彼此的对象为数据成员是不合法的。再看下面这一段程序:classB; //前向引用声明classA{public:voidmethod(){上一页下一页返回4.4复制构造函数x->SomeThing(); //错误:B类的对象在定义之前被使用
}private:B*x; //正确,经过前向引用声明,可以声明B类的对象指针};classB{public:voidSomeThing();private:A*y;};上一页下一页返回4.4复制构造函数编译器在编译时会指出错误,因为在类A的内联函数中使用了由x指向B类的成员,而此时B类尚未被完整地定义。解决这个问题的方法是更改这两个类的定义次序,或者将函数method()改为非内联形式,并且在类B的完整定义之后,再给出函数的定义。应该记住:当使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节。上一页返回4.5静态成员静态类成员是由关键字static修饰说明的类成员。虽然同样使用static修饰说明,但与函数中的静态变量有明显差异。类的静态成员是解决同一个类的不同对象之间数据和函数共享问题的。一个类不管建立了多少对象,静态成员只有一个,存于全局数据区。4.5.1静态数据成员在类定义中,用关键字static修饰的数据成员为静态数据成员。该类产生的所有对象共享由系统为静态成员分配的一块存储空间,而这块存储空间是在编译时分配的,在定义对象时不再为静态成员分配空间。静态数据实际上是该类所有对象所共有的,它更像面向过程程序设计的全局变量,可提供同一类的所有对象之间信息交换的捷径,而不像函数中的静态变量保留上次进入该函数留下的信息。正因为静态数据下一页返回4.5静态成员成员不属于类的某一特定对象,而是属于整个类的,所以静态成员具有“类属性”,可以用以下格式来使用:类名::静态数据成员名[例4-6]用静态数据成员对同一类的对象数量进行计数。#include<iostream>usingnamespacestd;classPoint{ //Point类定义public: //外部接口
Point(intxx=0,intyy=0){X=xx;Y=yy;countP++;} Point(Point&p); ~Point(){countP--;}上一页下一页返回4.5静态成员intGetX(){returnX;} intGetY(){returnY;} voidGetC(){cout<<"Objectid="<<countP<<endl;}//输出静态数据成员private: intX,Y; staticintcountP; //静态数据成员引用性说明,用于记录Point对象个数};Point::Point(Point&p){ X=p.X; Y=p.Y;上一页下一页返回4.5静态成员countP++;//在构造函数中对countP累加,所有对象共同维护同一个countP}intPoint::countP=0; //A静态数据成员定义性说明和初始化,使用类名限定voidmain(){PointA(4,5); //定义对象A,其构造函数使countP增1 cout<<"PointA,"<<A.GetX()<<","<<A.GetY(); A.GetC(); //输出对象个数
PointB(A); //定义对象B,其构造函数使countP增1 cout<<"PointB,"<<B.GetX()<<","<<B.GetY();上一页下一页返回4.5静态成员 B.GetC(); //输出对象个数}程序运行结果:PointA,4,5Objectid=1PointB,4,5Objectid=2本例中的A行是对静态数据成员count做定义性说明,必须在文件作用域中做一次且只能做一次说明,只有这时,C++编译器才为静态数据成员分配存储空间。C++静态数据成员默认的初值为0,所以A行中“=0”是可以省略的。不管静态变量是私有还是公有,类外定义性说明均有效。也只有在这个特殊场合才能对静态私有成员进行访问,在任何其他地方就是非法了。上一页下一页返回4.5静态成员静态数据成员虽然具有全局变量的一些特性,但受到访问权限的约束,建议静态成员说明为私有的,从而保证面向对象程序设计的封装性。如果说明为公有的,那么它会带来与全局变量同样的副作用。由于静态数据成员对所有类对象只有一个,所以在物理上,一个类对象向另一个类对象按成员进行复制仅复制非静态数据成员。*4.5.2静态函数成员将函数成员说明为静态的,该函数同样具有“类属性”,与该类的不同对象无关。因此静态函数访问类的非静态成员具有特殊之处。静态函数成员可以直接访问类的静态数据和静态函数。而访问非静态数据成员必须通过参数传递的方式得到对象名,然后通过对象名来访问非静态成员。静态函数成员的调用在对象之外采用下面的方式:上一页下一页返回4.5静态成员类名::函数名(对象名,其他参数表);或对象名.函数名(对象名,其他参数表);[例4-7]改写[例4-6]使用静态函数成员访问非静态数据成员。#include<iostream>usingnamespacestd;classPoint{public: Point(intxx=0,intyy=0){X=xx;Y=yy;countP++;} Point(Point&p); ~Point(){countP--;}上一页下一页返回4.5静态成员staticintGetX(Point&p){returnp.X;}//静态函数成员
staticintGetY(Point&p){returnp.Y;}//静态函数成员
staticvoidGetC(){cout<<"Objectid="<<countP<<endl;}private: intX,Y; staticintcountP;};Point::Point(Point&p){ X=p.X; Y=p.Y; countP++;}上一页下一页返回4.5静态成员intPoint::countP=0;voidmain(){ Point::GetC(); //A通过类访问静态成员PointA(4,5); cout<<"PointA,"<<Point::GetX(A)<<","<<A.GetY(A); A.GetC(); PointB(A); cout<<"PointB,"<<B.GetX(B)<<","<<Point::GetY(B); B.GetC();}上一页下一页返回4.5静态成员因为静态函数成员具有类属性,为同一个类的所有对象所共有。因此在A行可以通过类名访问静态函数成员。从[例4-7]中可以看出,通过静态函数成员访问非静态成员是比较麻烦的(学习了第6章就会知道,这是静态成员函数不存在this指针的缘故)。一般情况下它主要用来访问同一个类中的静态数据成员,维护对象共享的数据。如果静态成员函数在类外定义,则不能在定义时再加static,因为static不属于数据类型的组成部分。上一页返回*4.6常对象与常成员由于常量是不可改变的,因此根据需要可以将类的对象、成员定义为“常对象”或“常成员”,以保护对象、成员不被改变。常对象或常成员概念的建立,明确规定了程序中各种对象的变与不变的界线,从而进一步增强了C++程序的安全性和可控性。C++中常引用、常对象、常数据成员、常成员函数的访问和调用各有其特别之处。下面将分别予以讨论。
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 公安考试面试题及答案
- 26年复方中药靶点筛选指南
- 《生活科学思维课堂|发现身边的模型思维知识》
- 窝沟封闭术的微创技术应用
- 肺栓塞的护理制定
- 脑卒中康复护理中的物理因子治疗
- 2025北京小汤山医院面向应届毕业生(含社会人员)招聘15人备考试题含答案
- 《趣味学散文赏析|让课堂告别枯燥 爱上学习》
- 老年科护理信息化应用
- 查房中医科脑卒中康复调治难点专项|手把手教学规避临床失分点
- 2026年留疆战士政策理解练习题及解析
- 高中数学必修一2.2基本不等式常见题型(含答案)
- 2026年贵州省六盘水市初二地生会考试卷题库及答案
- 20kV及以下配电网工程预算定额(2022版)全5册excel版
- 煤矿总工程师岗位职责及技术管理体系
- 城镇污水处理厂资产管理方案
- 二级营销员题库及答案
- 妇产科专科护理实践指南(2025年版)
- 3D数字游戏角色设计与制作(Maya)课件:材质制作
- 中级注册安全工程师《安全生产法律法规》历年真题(2021-2025)
- 2025年四川警务辅助人员招聘考试综合能力素质测试综合试题及答案二
评论
0/150
提交评论