【文档说明】第3章面向对象程序设计语言基础课件.ppt,共(388)页,788.061 KB,由小橙橙上传
转载请保留链接:https://www.ichengzhen.cn/view-44843.html
以下为本文档部分文字说明:
第3章面向对象程序设计语言基础3.1面向对象程序设计语言掌握面向对象程序设计,首先应该学习面向对象程序设计语言。当今有很多种编程语言能体现基本的面向对象思想,如C++、Java、Smalltalk、CLOS等,但普遍被推崇的、流行的有两种,即C++和Java。下面将介绍C++和Java
的一些基础知识。3.1.1程序程序是有目的的动作序列,它是一种软件。对于程序员来说,程序是以某种语言为工具编制出来的动作序列,它表达了人解决现实世界问题的思想。计算机程序是用计算机程序设计语言所要求的规范书写出来的一系列动
作,它表达了程序员要求计算机执行的操作。对于计算机来说,一组机器指令就是程序。我们所说的机器代码或者机器指令,都是指程序,它是按计算机硬件设计规范的要求编制出来的动作序列。在计算机中,程序通常以文件的
形式保存。3.1.2语言及其分类语言按其等级可分为高级语言和低级语言。机器语言和汇编语言都是低级语言。程序语言越低级,程序的描写越复杂,越难懂,程序描述的问题越接近机器。最早,程序员使用最原始的计算机指令,即机器语言程序。只
有机器语言才能被机器识别和运行,这些指令以一串二进制数表示。不久,发明了汇编语言,它可以将机器指令映射为一些能被人读懂的助记符。以后,各种高级语言迅速发展起来。发展过程中,经历了严酷的优胜劣汰过程,最后剩下一些比较优秀的高级语言。C++作为一种多范型语言,就
是一种优秀的高级语言。例如,我们用C++和汇编语言分别来实现表达式a=2b+3c?1,用C++实现为a=2*b+3*c?1;用汇编来实现为moveax,DWORDPTRb_$[ebp]movecx,DWORDPTRc_$[ebp]mule
ax,2mulecx,3addeax,ecxdeceaxmovDWORDPTRb_$[ebp],eax语言越低级,就表明越靠近机器;越高级,就表明越靠近人的表达与理解、越自然。程序语言的发展,是从低级到高级,直到可用人的自然语言来描述。程序语言的发展也是从具
体到抽象的发展过程。从上面的例子中可以看出,作为高级语言的C++在实现表达式时要比汇编语言简洁得多。3.1.3C++是面向对象程序设计语言正如第1、2章所述,程序设计方法正在从结构化程序设计走向面向对
象程序设计。C语言能够很好地支持结构化范型程序设计,而C++能够很好地支持面向对象范型程序设计。在第4、5章中将介绍用C++来实现面向对象范型的程序设计。3.2C++语言基础3.2.1C++的发展史C++仅有二十几年的历史,它是C语言的扩展和完善。C语言是贝尔实验室的DennisRitchie在
B语言的基础上开发出来的。1972年在一台DECPDP-11计算机上实现了最初的C语言。到20世纪70年代末,C已经演化为现在所说的“传统的C语言”。Kernighan和Ritchie在1978年出版的《TheCProgrammi
ngLanguage》一书中全面地介绍了传统的C语言,1989年美国国家标准协会制定了C语言的标准(ANSIC)。Kernighan和Ritchie编著的第二版《TheCProgrammingLangu
age》(1988年版)介绍了ANSIC的全部内容。至此,C语言以其如下独有的特点风靡了全世界:(1)语言简洁、紧凑,使用方便、灵活。C语言只有32个关键字,程序书写形式自由。(2)丰富的运算符和数据类型。(
3)C语言可以直接访问内存地址,能进行位操作,能够胜任开发操作系统的工作。(4)生成的目标代码质量高,程序运行效率高。(5)可移植性好。C语言盛行的同时,也暴露出了其局限性:(1)类型检查机制相对较弱,这使
得程序中的一些错误不能在编译时发现;(2)C本身几乎没有支持代码重用的语言结构,因此一个程序员精心设计的程序很难为其他程序所用;(3)当程序的规模达到一定的程度时,程序员很难控制程序的复杂性。为满足管理程序复杂性的需要和克服C语言的以上缺点,就必须对C语言进行完善或以一种新的语言来代替C语言,
C++就是在这种环境下应运而生的。1980年,贝尔实验室的BjarneStroustrnp开始对C进行改进和扩充。最初的成果称为“带类的C”,1983年正式取名为C++。在对C++进行了三次修订后,于1994年制定了ANSIC++标准的草案,以后又经过不断完善成为目前的C++。C
++仍在不断发展中。C++包含了整个C,C是建立C++的基础;添加了对面向对象程序设计(OOP)的完全支持。下面介绍在用C++来实现面向对象程序设计中,必须掌握的C++的关于支持面向对象方面等的语法基础。对C缺乏了解的读者,建议在本
书之外,尽快浏览一下C语言的有关内容。3.2.2C++基础语法1.关键字关键字是预先定义好的标识符,这些标识符在C++编写程序的时候,有特殊的含义。在C++中共有61个关键字。在程序设计的过程中应注意,程序中用到的其他名字不能与这些关键字有相同的拼
法(包括大小写都不能相同)。表3-1给出了C++中定义的关键字。表3-1C++中定义的关键字staticautobreakunsignedprotectedconstuniondefaultdoubleelseexplictcontinuefloatforg
otointlongregistervolatileshortsignedsizeofstructswitchtypedefoperatorcasevoidreturnboolcatchclassconst_castdele
teenumfalseinlinemutablenamespacedynamic_castnewexternprivatepublicthistemplatereinterpret_castthrowturetrytypenam
eusingvirtualstatic_castwchar_tdoifwhilefriendchartypeid2.基本数据类型在程序中要用到数据,数据常以变量或常数的形式来描述。每个变量或常数都有数据类型。变量
是存储信息的存储单元,它对应于某个内存空间,用变量名代表其存储空间,程序能在变量中存储和取出值。数据类型用来告诉编译器应为变量分配多少内存空间、怎样组织已分配的内存空间,以及变量中要存储值的类型。数据类型分为基本数据类型和非基本数据类型。非基本数据类型包括数组、指针和结构等类型。基本数据类型有c
har、int、float、double、wchar_t和bool等。图3-1描述了C++的数据类型的分类。图3-1中的type表示非空的数据类型,是程序员可以自己定义的数据类型。在图3-1中只是给出
了常用数据类型和部分数据类型修饰符,它们用来改变基本类型的意义,用于适应各种情况的需要。修饰符有long(长型符)、short(短型符)、signed(有符号)和unsigned(无符号)等。数据类型修饰符确定了该数据所占内存空间的
大小和可表示的数值范围。表3-2是16位计算机中各数据类型所能表示的数值范围。图3-1C++的数据类型整型int字符型单字符型char宽字符型w_char实型单精度型float双精度型double逻辑型bool基本数据类型非基本数据类型数组
type[]指针type*空类型void结构struct联合union枚举enum类class数据类型表3-2常用基本数据类型描述类型说明长度(字节)表示范围备注char字符型1-128~127-27~(27-1)unsignedchar无符号字符型10~2550~(28-1)signedch
ar有符号字符型1-128~127-27~(27-1)int整型2-32768~32767-215~(215-1)unsignedint无符号整型20~655350~(216-1)signedint有符号整型2-327
68~32767-215~(215-1)shortint短整型2-32768~32767-215~(215-1)longint长整型4-2147183648~2147483647-231~(231-1)float浮点型(单精度)4
-3.4×1038~3.4×10387位有效位double双精度8-1.7×1038~1.7×103815位有效位longdouble长双精度16-3.4×104932~3.4×10493219位有效位3.变
量定义1)变量名的命名C++中变量的命名应该遵守以下规则:(1)不能是C++关键字;(2)第一个字符必须是字母或下划线;(3)不要太长,一般不超过31个字符;(4)不能以数字开头;(5)中间不能有空格;(6)变量名中不能包含“.;,"'+?'之类的特殊符号。(7)
变量名不要与C++中库函数名、类名、和对象名相同。2)变量定义方式在给变量命名时,通常用带有描述性的字符串,例如用area命名一个面积变量,用time命名时间变量。这些变量在应用时,程序员和读者对其意义一目了然。例如:
intnumber;doublesum;floatlength;3)变量赋值与初始化用赋值运算符“=”给变量赋值。变量初始化是指在定义变量时直接给变量赋值,也可以先定义变量,然后用赋值语句给其赋值。并不是所有的变量都需要初始化,未初始化的变
量在编译器编译时并不会出错。例如:unsignedshortwidth;doublearea,radius=23;width=4;其中,变量width先定义后赋值;而变量radius在定义时初始化;变量area未初始化。这些变量在编译时均不会出现错误。4)typedef用typedef可以为一个已
有的类型名提供一个同义词,该同义词可以代替该类型在程序中使用。用法是以typedef开始,随后是要表示的类型,最后是同义词和分号。typedef实际上没有定义一个新的数据类型,在建立一个typedef类型时没有分配内存。例如:typedefdoubleprofit;//定义double
的同义类型typedefintINT,integer;//定义int的两个同义类型INTa;//同inta;integera;//同inta;profitd;//同doubled;3.2.3常数1.实数常数实数常数的表示方法有两种:(1)十进制小数。它由数字和小数点组成,如
0.123,0.234,0.0等都是十进制数。(2)指数形式。如:425e4或425E4表示425×104。要注意E或e的前面必须是数字,且E或e后面的指数必须为整数。实数型常数常分为单精度(float)、
双精度(double)和长双精度(longdouble)3类。在16位计算机中,float型数据在内存中占4个字节,double型数据在内存中占8个字节,longdouble数据类型在内存中占16个字节。floa
t型提供7位有效数字,double型提供15位有效数字,longdouble提供19位有效数字。在C++中,一个实数如果没有说明,则其类型为默认类型double型。要表示float型数,则必须在实数后面加上F或f;要表示longdouble型数,则必须在
实数后面加上L或l。例如:3.2f//float型实数3.2L//longdouble型实数3.2//没有说明,故为默认类型double型2.字符常数字符常数是用单引号括起来的一个字符,它分为一般字符常数和特殊字符常数。一般字符常数
有a、x等字符,特殊字符常数是以“\”开头的字符。表3-3列出了常用的特殊字符。表3-3常用的特殊字符字符形式值功能\a0x07响铃\n0x0A换行\t0x09制表符(横向跳格)\v0x0B竖向跳格\b0x08退格\r0x0D回车\\0x5C反斜杠字符“\”\"0x22双引号\'0x27单引号\d
dd1~3位八进制\xhh1~2位十六进制表中列出的字符又称为转义字符,即将反斜杠后面的字符转变成另外的意义。有些是控制字符,如“\n”;有些字符是在该字符前加转义字符来表示的,如“\”、“'”、“"”。
在内存中,字符数据以ASCII码存储,即以整数表示,所以字符数据和整型数据在0~255(一个字节范围内)之间可以相互赋值。只要注意其表示的范围合理即可。例如:inta='b';//正确,将整型变量赋一个字符值(b的ASCII值为98)c
harc=97;//正确,将一个字符变量赋一个整型值(97代表的ASCII字符是a)在下列C++语句中,将自动根据变量定义时的数据类型,来确定输出的究竟是字符类型值还是整型类型值。例如:cout<<a<<en
dl;//输出字符b的ASCII值98cout<<c<<endl;//输出ASCII值97的字符b上面两行语句自动根据a或c被定义的数据类型确定其输出结果及类型。3.字符串常数字符串常数是由一对双引号括起来的字符序列,字符串常数和字符常数是不同的,字符串常数总是以“\0”结束。如果一个字
符串常数为“welcome”,那么它在内存中占用连续8个内存字节。在编程时应注意变量类型的匹配,不能将字符串常数赋给字符变量。4.枚举常数枚举常数是通过关键字enum来定义的。定义的格式为以enum开始,后跟类型名和花括号。花括
号内是要定义的枚举常数(又称为助记常数,该助记常数往往代表一个整型常数值),各常数之间用逗号分隔,最后以分号结束。例如:enumCOLOR{RED=150,BULE=200,GREEN,WHITE=400};其中,有一个枚举常数BLUE,代表2
00;GREEN没有被显式写出,在编译时其值自动被确定为201。如果没有显式地为枚举常量确定值,C++会为其自动确定,即花括号后的第一个枚举常数会被确定为0,其后的每个枚举常数的值为前一个枚举常数值加1。在下面程序中,企图用赋值语句给枚举变量赋值为不是定
义枚举类型时规定的值,是不正确的。COLORpaint=BULE;//正确piant=200;//错误,因为200不是规定的枚举类型常数5.常量定义C++中,常量是代表固定值的变量,即表示变量的值已被固定。程序中如果想让变量内容初始化后一直保持不变,则可以将其定义成一个常量。本书对常
量与常数进行了区分(这与其他书有所不同),常数代表了书写时的字面含义。常量定义的格式是:在关键字const后接变量名,然后是常数值,最后以分号结束该语句。例如:constfloatpi=3.1415926;在定义了常量pi之后,就可以在程序中用pi来代替
3.1415926。常量不能放在赋值语句的左边,即常量不能被赋值。常量定义中初始化的值可以是一个由常数和操作符构成的表达式。在程序运行之前就已经知道了常量值,因此编译时就能求值。注意:该表达式中不能含有函数,例如:constintsi
ze=20*sizeof(int);//oksizeof是运算符constintnumber=max(23,34);//错误6.引用引用是变量的别名,用&符号来声明。对别名的存取就是对变量的存取。别名没有存储空间,对引用声明的同时需对其进行初
始化。例3.1引用实例。intx=10;int&y=x;//声明y是x的引用cout<<y<<endli;//y=10x=20;cout<<y<<endl;//y=20;y=30;cout<<x<<endl;//x
=30使用引用比C语言中用指针来实现两数交换容易理解且语句简洁。例如:voidswap(int&xx,int&yy){inttemp;temp=xx;xx=yy;temp=xx;}voidmain(){inta=10,b=20;cout<<a<<"--"<<b<
<endl;swap(a,b);//直接用a、b作为实参,而C语言用指针实现交换时,要用其地址cout<<a<<"--"<<b<<endl;}3.2.4输入/输出1.输入/输出(I/O)的书写格式I/O流是输入或输出的一系列字节。当程序需要在屏幕上显示输出时,使用操作符“<<”向cout输出流中插
入字符;当程序需要执行从键盘输入时,使用操作符“>>”从cin输入流中输入字符。例3.2I/O书写格式实例。#include<iostream.h>voidmain(){charch;cout<<"inputthecharacteryouwanttoprint:"<<endl;ci
n>>ch;cout<<ch<<endl;}程序运行结果为:inputthecharacteryouwanttoprint:aacout代表I/O的输出对象(默认为屏幕),在例3.2中它输出一行提示语句;cin代表I/O的输入对象,在例3.2中从键盘输入了一个字符a,最后我们有一行输出语句输出所输
入的字符。头文件iostream.h包含涉及cout和cin的一些必要的定义内容。在输入和输出时还可以使用控制符来控制输入/输出的格式,以满足输入/输出的需要,表3-4中列出了常用输入/输出控制符。表3-4常用输入/输出控制符控制符描述
dec置基数为10hex置基数为16oct置基数为8setfill设置填充字setiosflags(ios::fixed)固定的浮点显示etw设置小数精度setiosflags(ios::left)左对齐setiosflags(ios::right)右对齐setiosflags(
ios::scientific)指数表示setiosflags(ios::skipws)忽略前导空白setiosflags(ios::uppercase)十六进制数大写输出setiosflags(ios::lowercase)十六进制小写输出3.2.5表达式和语句程序是一些按次序执行的语
句,而大部分的语句是由表达式构成的。表达式和语句是C++的重要内容。表达式由操作符、操作数构成,其目的是用来说明一个计算过程。在C++中操作符的优先级和结合性等规则与C语言是完全一致的,使用方法与C语言中表达式的使用方法也是相同的。
C++中所有的操作运算都是通过表达式来实现的。由表达式组成的语句称为表达式语句,由一个表达式和一个分号组成。大多数表达式语句是赋值语句和函数调用语句。语句用来规定程序执行的控制流。复合语句又被称为块,它是一对花括号内的语句序列。C++中的语句和C语言中的语句是大致相同的,
其使用方法也是一样的,在本书中不再赘述。下面对过程化语句进行介绍。1.过程化语句语句按功能可分为两大类:第一大类用于描述计算机执行的操作运算,即操作语句;第二大类用于控制操作运算的执行顺序的控制语句,即流程控制语句。流程控制语句也被称为过程化语句。图3-2wh
ile语句的流程结构循环变量初始化继续条件循环体改变循环变量算法NY1)while语句while语句由4个部分组成:循环变量初始化、继续条件、循环体、改变循环变量的算法。while用于判断一个条件表达式,当条件成立的时候进入循环体,不满足这
个条件则不进入循环体。while语句的流程结构如图3-2所示。例3.3while语句应用实例。voidmain(){intj=1;intsum;while(j<=50){sum=sum+j;j++;}cout<<"sum="<<sum<<endl;}2)do-w
hile语句do-while语句的格式如下:do循环体while(条件表达式);当流程到达do后,立即执行循环体语句,然后对条件表达式进行测试,如满足条件,则重复循环,否则退出,该语句结构至少使循环体执行一次。d
o-while语句的流程结构如图3-3所示。图3-3do-while语句的流程结构循环变量初始化继续条件循环体改变循环变量算法YN例3.4do-while语句应用实例。#include<iostream.h>voidmain(){intj,sum=0;j=1;do{sum=sum+j;j+
+;}while(j<=50)cout<<"sum="<<sum<<endl;}例3.3和例3.4都是求sum=1+2+…+50的和的程序,它们使用了不同的循环控制语句,但结果是相同的。3)for语句for语句也可以用来控制循环,for语
句的格式如下:for(表达式1;表达式2;表达式3)循环体for语句的执行过程如下:(1)求解表达式1;(2)求解表达式2,若为0则结束循环转到(5);(3)若表达式2为真,则执行循环体,然后求解表达式3;(4)转回(2);(
5)执行for语句下面的语句。图3-4for语句的流程结构表达式1表达式2循环体表达式3YNfor语句的流程结构如图3-4所示。在使用for语句时,并不是每个表达式都必须赋值,并且每个表达式都可以省略,但在省略表达式时,
应该注意一些问题:在省略表达式1时,控制变量应该在使用之前的程序中被初始化;省略表达式2,即不进行判断而使循环不停的进行下去,这样在循环体中需要有跳出循环的控制语句,否则就会出现死循环;在省略表达式3时,应该注意必须另外设法使循环控制变量变化,以保证循环能正常结束。不仅每个表达式
可以单独省略,而且它们可以同时省略。在省略表达式时,应注意表达式后面的分号不能省略。例3.5for语句应用实例。i=0;for(;i<=10;)//分号不能省略sum+=i++;//求值的同时改变循环变量/
/省略所有的表达式i=0;for(;;){sum+=i++;//求值的同时改变循环控制变量if(i>10)//控制结束条件break;//当i大于10时循环结束,跳出循环体}在本例中省略了表达式1和表达式3。4)switch语句当要实现多分支选择语句时,就要用到switch语句。sw
itch的格式如下:switch(表达式){case常量表达式1:执行语句1case常量表达式2:执行语句2case常量表达式n:执行语句ndefault:执行语句n+1}switch后面括号中的表达式只能是整型、字符型或枚举类型。case后面的常量表达式必须与表达式匹配,如果不匹
配,则在编译时会出现错误。当表达式的值与某一个case后面常量表达式的值相等时,就执行case后面的语句,若没有与之匹配的常量表达式,则执行default后的执行语句。例3.6switch语句应用实例。
#include<iostream.h>voidmain(){chargrade;cin>>grade;switch(grade){case'A':cout<<"85-100"<<endl;break;case'B
':cout<<"70-84"<<endl;break;case'C':cout<<"60-69"<<endl;break;case'D':cout<<"<60"<<endl;break;default:c
out<<"error"<<endl;}}在本例中,当输入一个字符时如果它与A、B、C、D中的任何一个匹配,则输出其后对应的语句;若不能与它们之中的任何一个匹配,则输出default后的语句,即输出出错提示error。请读者自行分析break语
句的作用。现在我们已经了解了C++的基本语法,下面具体分析几个例子。例3.7判明素数并输出100内中所有的素数。在判断一个数m是否是素数时,就是检验它是否能被2~中的任意一个整数整除。如果存在一个数在范围2~内且能整除m,则m不是素数;否
则,m是素数。#include<iostream.h>#include<math.h>voidprime(longm){intsqrtm;sqrtm=sqrt(m);for(inti=2;i<=sqrtm;i++)if(m%i==0)break;if(sqrtm<i)cout<<
m<<endl;elsecout<<m<<"itisnotaprime"<<endl;}voidmain(){longm;cout<<"inputthenumberm:"<<endl;cin>>m;prime(m);for
(intj=1;j<=100;j++)prime(j);}函数prime()用来判断一个数是否是素数。如果是,则输出该数;如果不是,则输出“itisnotaprime”。在寻找1~100内中的素数时,我们用了一个循环语言来控制输入的数为1
~100,然后判断每个数是否为素数。例3.8猴子吃桃问题。猴子第一天摘下若干桃子,当即吃了一半,还不过瘾,又多吃了一个。第二天早晨又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下来的一
半又多一个。这样,到第10天早晨想再吃时,发现只剩下一个桃子了,问猴子第一天摘了多少个桃子。这个问题用程序能很轻易的解决,只需要用一个循环语句构成的函数就可以实现。#include<iostream.h>
intfn(intm){m=2*m;m++;returnm;}voidmain(){floatsum=1;for(inti=1;i<10;i++)sum=fn(sum);cout<<sum<<endl;}输出结
果为1023,这和用手工计算得到的值是一致的。在解决这个问题时,考虑到从第10天的桃子数起,以后每天的桃子数是后一天的2倍多一个,这样就能用循环语句方便地解决这个问题。3.2.6函数要编好程序,就要合理地划分程序
中的各个程序块,即函数。C++程序是由函数组成的,一个C++程序由一个或若干个函数构成,程序中的函数在程序运行时被调用,虽然main()也是函数,但它并不是普通的函数,它表示程序的入口。程序命令按照它们在源代码中出现的顺序一行行地顺序
执行,直到遇到新的函数调用,然后程序分支去执行函数调用。当函数调用完成时,程序控制立即返回调用函数下一行代码。图3-5C++程序中函数调用层次图mainfun1fun2fun3fun4fun5fun6在C++中,一个
函数必须在声明后才能使用。函数声明的作用是告诉编译器,该函数是存在的。以后编译器在遇到该函数被调用时就不会出错了,同时编译器还对函数调用进行正确性检查。C++函数声明总是由函数原型构成的。C++程序中函数调用的层次如图3-5所示。1.函数原型C++中的函数分为两种:标准库函数和用户自定义的
函数。标准库函数在使用前可以用包含指令#include包含在头文件中,而用户自己定义的函数在使用前必须先声明,即在使用前说明函数的原型。函数原型是一条程序语句,必须以分号结束。它由函数返回类型、函数名和参数构成,其形式为返回类型function(参数表);参数表包含
所有参数的数据类型,各参数之间用逗号分开。函数原型与函数定义在返回类型、函数名和参数表上必须完全一致。函数的返回值也称为函数值,返回的不是函数本身而是一个值。我们经常用return语句来返回函数的值。在使用函数时会涉
及到变量的使用,在C++中变量的作用域和使用规则与C语言中是完全相同的,本书中不再赘述。2.函数的调用机制C++中函数的调用过程就是栈空间操作的过程。在函数调用时,C++会进行如下操作:(1)建立被调用函数的栈空间;(2)保护调用函数的运行状态和返回地址;(3)传递
参数;(4)将控制转交给被调函数。例3.9函数调用实例。voidarea(int,int);voidsum(float,float);voidmain(){inta=4,b=5;area(a,b);sum(a,b);
}在主函数调用两个函数之前,先对这两个函数的原型进行了声明。主函数中调用了两个函数,其调用过程实际上就是一个对栈的操作过程。其他函数调用如递归调用在C语言中也有详细的叙述。关于函数还应注意:任何函数之间不能嵌套定义,调用函数与
被调用函数相互独立,被调用函数和调用函数之间是靠参数传递和返回值来联系的,函数被看作为一个黑盒。3.内联函数函数调用需要建立栈内存环境,进行参数传递,并产生程序执行转移。这些工作都需要一定的时间开销,若多次调用一个代码很短的函数,
就会使程序的运行效率降低。为解决这个问题,应该使用内联函数。内联函数也称为内嵌函数,是能提高程序的效率的一种特殊函数。内联函数用关键字inline来声明,其具体的形式如下:inline返回类型函数名(参数表){//函数体}在编
译时编译器看到关键字inline后就为该函数创建一段代码,在以后每次调用该函数时,都会用这段代码来替代相应的函数。内联函数可以在一开始仅声明一次。内联函数中不能含有复杂的结构控制语句,如switch和w
hile,若含有这些语句,在编译时会将该函数视为普通函数一样产生函数调用代码;内联函数不能用作递归函数来使用;内联函数的返回值类型总是整型。例3.10内联函数举例。#include<iostream.h>inlineintisnumber(c
harch){return(ch>='0'&&ch<='9')?1:0;}voidmain(){charc;while((c=cin.getc())!='\n')//cin.getc()是用于调用输入对象的成员函数{if(isnumber(c))//调用一个小函数c
out<<"youenteredadigit"<<endl;elsecout<<"youenteredanon-digit"<<endl;}}3.2.7函数的重载在C语言中,每个函数都必须有惟一的名字。为功能近似的问题定义函数时,不得
不为这些既近似又有区别的问题定义多个函数,为这些函数起不同的函数名。C++允许这些不同的函数使用相同的函数名。在函数调用时根据给定的实际参数自动匹配,究竟该调用哪一个函数。其匹配是严格按照这些函数定义时的参数类型、个数、顺序来确定的。例
3.11函数重载举例。intabs(int);doubleabs(double);longabs(long);main(){abs(5);//自动匹配调用intabs(int)函数}这三个函数具有相同的名字,但它们的参数类型不相同。重载函数的匹配顺序是靠将实参类型与
所有被调用的具有相同函数名的函数的形参类型一一比较来判定的。它们在匹配时按以下的顺序来寻找被匹配的函数:(1)寻找一个严格的匹配,如果找到了,就用那个函数;(2)通过内部转换寻求一个匹配,只要找到了,就用那个
函数;(3)通过用户定义的转换寻求一个匹配,若能查出有惟一的一组转换,就用那个函数。在大多数情况下,函数的匹配是用寻找一个严格的匹配进行的。对于其它匹配,本书不述及。在匹配函数时,如果调用函数的实际参数与所有的重载函数在参数类型、参数个数、参数顺序上都不相同
,则认为不能匹配。编译器无法分辨两个完全一样的重载函数,因此重载函数应该在参数个数、参数类型或参数顺序上有所不同。C++是C的完善,因此C++继承了C的许多知识点。数组、指针、引用、结构体以及它们的使用范围和使用方法,在C++中与C中是完全相同的。C++除继承C的所有知识外
还有其自身的特点。正如前面章节所述及的,C++是支持多范型的语言。下面重点从实现面向对象的三个主要特征来介绍C++的其他内容。这三个主要特征为:(1)封装;(2)继承;(3)多态。3.2.8C++中的类学习了第1、2章,对类的概念已经有了一个基本的了解,现在我们详细描述C++类的语法,以及
对类的一些简单使用。1.类的建立及其成员C++中的类与C语言中的结构体相似,类的定义方法如下:classClassname{//数据成员//成员函数};其中,class为关键字,Classname为类名,在定义一个类时关键字和类名是必不可少的,一般用具有典型意
义的名字来命名类。例如,定义一个学生类,可以用Student来命名。为编程的需要我们可以将类的成员定义成不同的存取属性(public、private、protected),在后续章节我们将具体介绍各类存取属性的使用范围和使用方法。下面为大家介
绍类的成员定义和类的作用域等。类似于函数声明,类也有声明,其形式如下:classStudent;2.成员的定义在类中可以定义其数据成员和成员函数,下面介绍类的成员的定义和使用。例3.12类的成员的定义和使用实例。#include<iostream.h>classStudent{public:
floatagre(floatmath,floatphy,floatEnglish,floatchinese){agv=(math+phy+English+chinese)/4;returnagv;}private
:floatmath;floatphy;floatEnglish;floatchinese;floatagv;};在该例中我们定义了一个学生类,在类中又定义了一个成员函数agre()和5个成员变量。在定义成员函数和成员变量时,使用了两
个关键字public和private,它们分别将成员函数声明为公有成员,将数据成员声明为私有成员。在定义类的成员时,根据我们的需要可以用关键字private、public和protected来声明成员的存取属性。被private声明后的成员,称为私有成员
,由该关键字声明的成员仅被该类的成员函数调用;被protected声明后的成员,称为被保护成员,该成员除了能被该类的成员函数使用外,还有其他的用途;被public声明后的成员,称为公有成员,由它声明的成员不仅能被该类的成员函数调用,而且还能被类外的函数调用。private声明的成员和protect
ed声明的成员在这里没有太大的区别,但在继承中有较大的差别(详见后续内容)。3.类的作用域类的作用域是指类定义时一对花括号所形成的作用域。在该范围内,类的成员对同一类的成员函数具有访问权。类的所有成员位于这个类的作用域内。类的所有成员是一个相关的整体,而类外的函数访问类的作用域的成
员时受到了限制。这种思想是把一个类的数据结构和功能封装起来,从而使得在类外访问该类时,必须使用专用的接口(接口在这里指类中被public声明为公有的成员函数)。例3.13类的作用域举例。classX{public:voidfn1(){n++;};voidfn2
(){fn1();};intfn(){returnn;};private:intn;};4.类的封装通过第1、2章的学习,我们了解了封装的概念,下面用一个具体的实例来介绍封装是怎样实现的。例3.14对“点”(Point)的封装。classPoint//Point的
类定义{public://外部接口voidset(doubleix,doubleiy);doublexoffset();doubleyoffset();doubleangle();doubleradius();private://内部数据doublex;//横坐标doubley;//纵坐标};//
Point类的成员函数定义#include<math.h>#definepi3.1415926voidPoint::set(doubleix,doubleiy){x=ix;y=iy;}doublePoint::xoffset(){retur
nx;}doublePoint::yoffset(){returny;}doublePoint::angle(){return(180/pi)*atan2(y,x);}doublePoint::radius(){returnsqrt(x*y+y*y);}每个成员函数前都加
了类名。要使用这段代码,应包含两个头文件,一个是point.h,因为所有成员函数的声明都在类的定义中;另一个是math.h文件,因为在成员函数的定义中调用了math.h中声明的函数。在本例中我们将数据和操作结合,构成了一个不可分割的整体,即类。用这个类可生成
具有不同属性值的对象。类中的一些数据成员是私有的,它们被有效的屏蔽,外界不会对其产生干扰和误操作;另一些成员(成员函数)是公共的,它们作为接口提供给外界使用。5.构造函数和析构函数在OOP中,凡是创建的对象都需要作某种形式的初始化。为此需要构造函数(Construct
orFunction),供创建类的实例对象时调用,用以自动完成对象的初始化。析构函数(DestructorFunction)是与构造函数相对应的一个概念,它用于释放对象定义时通过构造函数向系统申请的存储空间以及有关的系统资源,它是在其有
效范围内自动调用的。下面通过一个例子认识构造函数和析构函数。例3.15构造函数和析构函数举例。classStack{chararray[size];int*tos;~Stack();public:Stack();//这是一个构造函数viodpush(char);charpop(c
ahr);};Stack::Stack()//说明函数所属的类且与类名相同{cout<<"constructingastack"<<endl;tos=newint[10];//分配堆空间}Stack::~Stack(){cout<
<"destructingstack"<<endl;deletetos;//释放堆空间}通过该例我们对构造函数和析构函数有了一个初步的认识,下面再来对它们进行详细的讨论。1)构造函数的特性构造函数是类的一
个特殊的成员函数,因此它除具有一般成员函数的受访问限制、可重载等性质外,还具有其自身的许多的特性:(1)构造函数的名称与它所属的类名相同,在类对象被创建时自动调用,它不能用常规的方法调用,不能被继承;(2)构造函数无返回类型(任何返回类型,包括
void,都是非法的),不能带有return语句;(3)一个类可以具有多个构造函数,重载时参数的个数或类型应不一样,两个具有相同参数类型和个数的构造函数是非法的;(4)如果在编程时类中没有显式的定义构造函数,在编译时系统会自动的生成一个不带任何参数的构造函数
;(5)构造函数在调用时,控制成员变量的内存分配,为定义的对象向系统申请内存。2)构造函数的设计与使用类中的私有成员一般不作初始设置,需要在定义对象时初始化。采用显式调用成员函数初始化对象容易遗漏,造成对象无意义,因此,使用构造函数对对象进行初始化是最佳选择。同
时,一个类的不同对象需要有不同的初始化,因此可以采用带参数的构造函数和多构造函数来解决这个问题。下面讨论几种构造函数。(1)带参数的构造函数。例3.16带参数的构造函数举例。classCourse{floatx,y;floatGDP;public:Course(
ifloatox,floatoy);intGDP(floatx,floaty);};Course::Course(intx,inty){//以参数初始化变量x=ox;y=oy;};intCourse::GDP(intx,inty){GDP=(x+y)/2;}voidmain(
){CourseS(90,85);//定义对象时初始化S.GDP(90,85);}如果在定义对象S时不带参数,如CourseS;,那么,构造函数就不能对x、y赋初值。构造函数定义了缺省值,在函数调用时,若无特别指定参数,便以缺省值作初始化,而且也可以防止遗漏初始赋值。缺省参数的构造函数的用法
与一般函数的用法相同。例如将上述函数改为带缺省参数的构造函数如下:classCourse{floatx,y;floatGDP;public:Course(intox=90,oy=85){x=ox;y=oy;}};(2)多构造函数(构造函数重载)。
对于一个类的不同对象,当其需要不同类型和不同个数的初始化参数时,可以在一个类中定义几个构造函数,以适应不同的情况。例3.17多构造函数举例。classCourse{public:Course();Cou
rse(float);Course(char,float);};voidmain(){Courseob1;Courseob2(78.5);Courseob3('M',87.5);}(3)复制构造函数(CopyConstructor)。复制
构造函数是一种特殊的构造函数,它除了具有一般构造函数所具有的性质外,还具有一些特殊的性质:①具有将参数传输对象按位拷贝的功能。②编译系统在编译类时,产生一个缺省拷贝构造函数,例如,在main()中的语句Cou
rseob1('M',65.5);Courseob2(ob1);对定义对象ob2,编译系统自动调用缺省拷贝构造函数,实现对象的按位拷贝。ob2与ob1完全相同,但它们是两个完全独立的对象。③程序员也可以自己定义拷贝构造函数,定义格式如下:classname(constclassname&o
bj){…}其中,obj是引用对象,可以用于初始化其他对象,若有其他参数,则在此后列出,若缺省,则这个引用对象不能省;const是习惯上的限定词(即习惯上将被拷贝对象声明为不可改写),obj对象也可以不是const型。例3.18拷贝构造函数举例。classCourse{charx;fla
oty;public:Course(charox,floatoy){x=ox;y=oy;}Course(constcourse&M){x=M.x;y=M.y;}};voidmain(){Courseob1('M
',67.5);Courseob2(ob1);}注意:如果类中包含有指针成员,那么就要慎用缺省的拷贝构造函数,因为编译器提供的拷贝构造函数只是简单的按位拷贝成员数据,它会使两个对象的指针指向同一片内存区域,而不是复制指针指
向的内容。若该内存区是动态分配的,编程者有义务释放它,但在释放时会遇到麻烦,因为其中一个指针被释放后,另一个指针所指的区域就失去了意义,这可能会导致严重的后果。3)析构函数的特性和用法析构函数是用于当对象离开其有效范围时,释放对象所占用的内存空间的一个特殊函数。析构
函数也是成员函数,它是与构造函数相对应的,命名时在构造函数名前加“~”。析构函数只能是无返回类型,不能带任何参数,不能被重载,一个类只允许有一个析构函数。析构函数的定义一般是由一系列的delete组成的。例3.19析构函数举例。#include<iostream.h>#include<strin
g.h>classsample{private:char*pointer_char;public:sample(){pointer_char=NULL;};sample(char*str){pointer_ch
ar=newchar[strlen(str)+1];strcpy(pointer_cahr,str);};sample(sample&ob){pointer_char=newchar[strlen(ob.pointer_char)+1];strcpy(poin
ter_char,ob.poiner_char);};~sample(){if(pointer_char!=NULL)deletepointer_char;};};3.2.9写C++类代码的相关基础常识写类代码(主要考虑封装性)会涉及到许多C++
的语法知识细节,本节叙述写类代码时应该理解的其中一些基础C++知识。对于在C++中表现继承性和多态性的内容,在后几节将进行讨论。这些是面向对象程序设计的基础,要成为一个好的程序员,应对其有一定了解。1.const对象和const成员函数对对象的最低访问权是良好软件工程的一项基本原
则。下面讲述的就是这一原则在对象上的应用。某些对象是需要修改的,而有些对象是不需要修改的。可以在程序中用关键字const指明那些不允许修改的对象,任何试图修改这种对象的尝试都会导致编译错误。例如:constTimen
oon(12,0,0);声明了类Time的一个const对象,并把该对象初始化成时针为12点的时间。用const声明某个对象可强制实现最低访问权原则。如果程序员不小心修改了这种对象,编译器会发现这种错误,从而避免运行时的错误。尽管有的编译器允许下列错误存在,但考虑到
数据封装的因素,在设计程序时应尽量避免下列错误:(1)定义一个const成员函数,但该函数又修改了对象的数据成员;(2)定义一个const成员函数,但该函数又调用了非const函数;(3)const对象调用非const成员函数;(4)试图修改const对象。cons
t函数的声明或定义规定必须使用关键字const,该关键字插在函数参数列表之后。例如,如下的成员函数被声明为const成员函数,它仅仅返回对象的一个数据成员的值:intgetVuale()const{returnprivateDataMember;}如果在类的定义外部定义const
成员函数,那么该成员函数的定义也必须包含关键字const。可以把非const成员函数重载为const成员函数。编译器根据对象在声明时是否使用了const来自动确定调用哪一个重载函数。由于为了能够正确地初始化对象,构造函数必须允许修改对象,析构函数也必须要能够
做一些撤销对象前的清理工作,因此,const对象的构造函数和析构函数不要用关键字const声明。例3.20把不修改对象值的成员函数都声明成const。classTime{public:Time(int=0
,int=0,int=0);//默认构造函数//"set"函数voidsetTime(int,int,int);//设置时间voidsetHour(int);//设置hour的值voidsetMinute(int);//设置minute的值voi
dsetSecond(int);//设置second的值//"get"函数intgetHour()const;//返回hour的值intgetMinute()const;//返回minute的值intgetSecond()const;//返回second的值//输出函数(通常被声明cons
t成员函数)voidprintMilitary()const;//输出军用格式的时间voidprintStandard()const;//输出标准格式的时间private:inthour;//0—23intminute;//0—59intsecond;//0—59};2.复合一个类可以
把其他类的对象作为自己的成员,这就叫做复合。复合是软件重用的一种形式。在建立对象的时候,其构造函数自动被调用。如果一个对象有多个成员对象,那么这些成员对象应该以它们在该对象中的定义顺序建立。但是,在编写程序的时候,不应该编写依
赖于构造函数调用顺序的代码。下面的程序用类Employee和Date演示了怎样把对象变为其他对象的成员。类Employee包括私有数据成员lastName、firstName、birthDate和hireDate。成员b
irthDate和hireDate是类Date的对象,它们包括私有数据成员month、day和year。程序建立了类Employee的一个对象,初始化并输出了对象的数据成员。注意构造函数头部的语法形式:Employee::Emplooyee(char*fna
me,char*lname,intbmonth,intbday,intbyear,inthmonthinthday,inthyear):birthDate(bmonth,bday,byear),hireDate(hmonth,hday,hyear)这个构造函数有8个参数(f
name,lname,bmonth,bday,byear,hmonth,hday和hyear)。头部的冒号用于把成员初始化值和参数列表分开。初始化值是Employee的构造函数中传递给成员构造函数的参数。因此,bmonth、bday和byear传递给了birth
Date的构造函数,hmonth、hday和hyear传递给了hireDate的构造函数。多个成员初始化值之间是用逗号分隔的。1)类Date的定义classDate{public:Date(int=9,int=1,int=2003);//默认
构造函数voidprint()const;//按格式"月/日/年"输出日期private:intmonth;//1-12intday;//1-31(取决于月份)intyear;//任意年//根据年份和月份测试天数是否正确的工具函数intch
eckDay(int);};2)成员对象初始化值的用法//构造函数:检查月份值的正确性//调用工具函数checkDay核对天数的正确性Date::Date(intmn,intdy,intyr){month=(mn>&&mn<=12)?
mn:1;//检验正确性year=yr;//也可以在这里检验正确性day=checkDay(dy)//检验正确性cout<<"Dateobjectconstructorfordate";print();cout<<endl;}3)检验天数正确性的工具函数intDate::checkDay(
inttestDay){staticintdaysPerMonth[13]={0,31,28,31,30,31,30,31,30,31,30,31};if(month!=2){if(testDay>0
&&testDay<=dayPerMonth[month])returntestDay;}else{//检查是否是闰年intdays=(year%400==0||year%4==0&&year%100!=0)?29:
28);if(testDay>&&testDay<=days)returntestDay;}cout<<"Day"<<testDay<<"invalid.Settoday1.\n";retrun1;//在值有误的情况下让对象处于稳定状态}4)以“月/日/年”的形式输出对象Dat
evoidDate::print()const{cout<<month<<'/'<<day<<'/'<<year;}5)成员对象初始化值的用法之一classEmployee{public:Employee(char*
,char*,int,int,int,int,int,int);voidprint()const;private:charlastName[25];charfirstName[25];DatebirthDate;DatehireDa
te;};6)成员对象初始化值的用法之二Employee::Employee(char*fname,char*lname,intbmonth,intbday,intbyear,inthmonth,inthday,inthyear):birthday(bmonth,bday,byear),h
ireDate(hmonth,hday,hyear){strncpy(firstName,fname,24);firstName[24]='\n';strncpy(lastName,lname,24);lastNa
me[24]='\n';cout<<"Employeeobjectconstructor:"<<firstName<<"<<lastName<<endl;}voidEmployee::print()const{cout<<lastName<<","firstName<<endl;hir
eDate.print();cout<<"Birthday:";birthDate.print();cout<<endl;}7)演示带有成员对象的对象main(){Employeee("Bob","Jones",7,24,49,3,12,88);cout<<endl;e.print()
;cout<<"\nTestDateconstructorwithinvalidvalues:\n";Dated(14,35,94);//非法的日期值Retrun0;}输出结果为:Dateobjectconstructorfordate
7/24/49Dateobjectconstructorfordate3/12/88Employeeobjectconstructor:BobJonesJones,BobHired:3/12/88Birthday:
7/24/49TestDateconstructorwithinvalidvalues:Day35invalid.Settoday1.Dateobjectconstructorfordate1/1/94成员对象不是必须要用成员初始化值初始化。如果没有提供成员初始化
值,那么该成员对象的构造函数将被自动调用。默认构造函数所建立的值(如果有的话)会被“set”函数所建立的新值覆盖。常见的程序设计错误是:既没有为成员对象提供初始化值,也没有为成员对象提供默认的构造函数。遇到这种情况时,编
译器会报错。3.友元函数和友元类一个类的友元函数是在该类作用域之外定义的,但是它有权访问该类的私有成员。一个函数或整个类都可以声明为另一个类的友元。把函数声明为是某个类的友元函数的方法是:在类定义中,把关键字friend放在
函数原形的前面。要将类ClassTwo声明为类ClassOne的友元类,需要用如下形式的声明把ClassTwo作为ClassOne的一个成员:friendClassTwo;成员访问说明符private、protected和public的书写与友元关系的声明无关,因
此,友元关系的声明可以放在类定义中的任何地方。友元关系是“给予”的,而不是“索取”的。也就是说,类B是类A的友元类,类B是类C的友元类,不能就因此认为类A是类B的友元类或类C是类B的友元类,也不能认为类
A是类C的友元类。从对象封装角度看,友元关系在一定程度上破坏了信息的隐藏,降低了面向对象的程序设计的价值。下面的程序演示了如何用友元函数setX设置类Count的私有数据成员x。//友元能够访问类的私有成员//修改类Count的xclassCount{friendvoid
setX(Count&,int);//声明友元函数public:Count(){x=0;}//构造函数voidprint()const{cout<<x<<endl;}//输出private:intx;//数据成员};//因为setX被声明为类Count的私有数据,所以它可以修改类
Count的私有数据voidsetX(Count&c,intval){c.x=val;//合法,因为setX是类Count的友元}main(){Countobject;cout<<"object.xafterinstantiation:";Object
.print();cout<<"object.xaftercalltosetXfriendfunction:";setX(object,8);//用友元函数设置x的值object.print();return0;}输出结果为:Obj
ect.xafterinstantiation:0Object.xaftercalltosetXfriendfunction:8//非友元函数且非本类的成员函数不能够访问私有成员#include<iostream.h>//非友员企图修改类Co
unt的xclassCount{public:Count(){x=0;}//构造函数voidprint()const{cout<<x<<'\n';}//输出private:intx;//数据成员};//试图修改类Count的私有数据的函数。但这种修改是不允许的,因为它不是
类//Count的友元//函数voidcannotSetX(Count&c,intval){c.x=val;//错误:不能访问Count::x}main(){Countobject;cannotSetX(object,3);//cannotSetX不是类Count的友元return0;}
从该程序可以看到,在类的一开始,甚至在public:前面,就进行了友元关系的声明(一般情况下都是如此)。程序用非友元的函数cannotSetX修改类Count的私有数据成员x,结果编译器报错。4.使用this指针每个对象都有一个指向自身的指针,该指针称为this指针。在引用对象内部的成员
的时候,this指针是一个隐含的参数(当然也可以明确地使用this指针)。每个对象都可以通过使用this关键字来确定其自身的地址。this指针隐含地用来引用对象的数据成员和成员函数。this指针的类型取决于对象的类型和使用this指针的成员函数是否是用const声明的。对于类Emplo
yee的非const成员函数来说,this指针的类型为Employee*const(指向类Employee的对象的常量指针);而对类Employee的const成员函数来说,this指针的类型为Employeecons
t*const(指向类Employee的常量对象的常量指针)。下面先给出一个明确使用this指针的例子,然后再举例说明this指针的真正用意。每个成员函数都有权通过访问指向对象的this指针来调用该对象的成员。为了节省存储空间,每个类的
成员函数只有一份拷贝,这些成员函数供类的所有对象使用,而每个对象对类的数据成员都有自己的拷贝。在下面的程序中,类Test的成员函数明确地用this指针输出了该类对象的私有数据x。程序使用了箭头运算符(->)和点运算符(.)。//用this指针引用对象的成员。#i
nclude<iostream.h>classTest{public:Test(int=0);voidprint()const;private:intx;};Test::Test(inta){x=a;}//构造函数voidTest::prin
t()const{cout<<"x="<<x<<"\nthis->x="<<this->x<<"\n(*this).x="<<(*this).x<<'\n';}main(){Testa(12);a.print();return0;}输出结果为:
x=12this->x=12(*this).x=12注意:*this在和圆点运算符一起使用的时候要用括号括起来,因为圆点运算符的优先级高于运算符*。如果不加括号,表达式*this.x就相当于*(this.x)因为成员选择运算符不能和指针一起使用,所以C++编译器认为这种表达式
是一个错误。下面的程序用返回对对象的引用实现了类Time成员函数的连锁调用。类Time的成员函数setTime、setHour、setMinute和setSecond都返回了Time&类型的*this指针。返回对对象引用的*this为什么能起作用呢?这
是因为圆点运算符(.)是自左向右结合的,所以表达式t.setHour(18).setMinute(30).setSecond(22);先计算t.setHour(18),然后返回对对象t的引用并把它作为函数调用的结果,因而余下的表达式被解释为t.setMinute(30).setSe
cond(22);调用t.setMinute(30)也返回对t的引用,余下的表达式被解释为t.setSecond(22);注意:如下的调用也用到了连锁调用的特点:t.setTime(20,20,20).printStandard();因为printStandar
d()并不返回对t的引用,所以这些调用的顺序是不能够改变的。把printStandard()放在调用setTime之前将会导致语法错误。下面的代码是上述类及成员函数的定义。//类Time的定义classTime{public:Time(int=0,
int=0,int=0);//默认构造函数//"set"函数Time&setTime(int,int,int);//设置hour,minute和second的值Time&setHour(int);//设置hour的值Time&setMinute(int);//minute
的值Time&setSecond(int);//设置second的值//"get"函数(通常被声明为const)intgetHour()const;//返回hour的值intgetMinute()const;//返回minut
e的值intgetSecind()const;//返回second的值//输出函数(通常被声明为const)intgetHour()const;//返回hour的值intgetMinute()const;//返回minu
te的值intgetSecond()const;//返回second的值//输出函数(通常被声明为const)voidprintMilitary()const;//输出军用格式的时间voidprintStandard()const;//输出标准格式的时间priva
te:inthour;//0-23intminute;//0-59intsecond;//0-59};//定义类Time的成员函数#include<iostrem.h>//初始化时间数据的构造函数//默认值为零(参看类的定义)Time::T
ime(inthr,intmin,intsec){hour=(hr>=0&&hr<24)?hr:0;minute=(min>=0&&min<60)?Min:0;second=(sec>=0&&sec<60)?Sec:0;}//设置hour、minute和second的值Time&T
ime::setTime(inth,intm,ints){hour=(h>=0&&h<24)?h:0;minute=(m>=0&&m<60)?Min:0;second=(sec>=0&&sec<60)?sec:
0;return*this;//连锁调用的关键环节}//单独设置hour的值Time&Time::setHour(inth){hour=(h>=0&&h<24)?h:0;return*this;//连锁调用的关键环节}//单独设置minute的值Time&Time::s
etMinute(intm){minute=(m>=0&&m<60)?m:0;return*this;//连锁调用的关键环节}//单独设置second的值Time&Time::setSecond(ints){second=(s>=0&&s<60)
?S:0;return*this;//连锁调用的关键环节}//单独读取hour的值intTime::getHour()const{returnhour;}//单独读取minute的值intTime::getMinute()const{returnminute;}//单独读取second的值in
tTime::getTime()const{returnsecond;}//按军用格式HH:MM:SS输出时间voidTime::printMilitaru()const{cout<<(hour<10?"0":"")<<hour<<":"<<(minut
e<10?"0":"")<<minute<<":"<<(second<10?"0":"")<<second;}//按标准格式HH:MM:SSAM(或PM)输出时间voidTime::printStrandard()const{count<<((hour==0||hour==12)?12:h
our%12)<<":"<<(minute<10?"0":"")<<minute<<":"<<(second<10?"0":"")<<second<<(hour<12?"AM":"PM");}//用this指针实现成员函数的连锁调用#include<iostream.h>m
ain(){Timet;t.setHour(18).setMinute(30).setSecond(22);cout<<"Miliatrytime:"t.printMiliatry();cout<<"\nStandardtime:";t.printStand
ard();cout<<"\n\nNewstandardtime:";t.setTime(20,20,20).printStandard();cout<<endl;return0;}输出结果:Mili
atrytime:18:30:22Standardtime:6:30:22PMNewstandardtime:8:20:20PM5.动态内存分配在实现动态内存分配方面,使用运算符new和delete比使用C中的函数malloc和f
ree更好。以如下代码为例:TypeName*typeNamePtr;在ANSIC中,要动态建立一个TypeName类型的对象,应该用下面的语句:typeNamePtr=malloc(sizeof(TypeName));在这个语句中,调用了函数malloc和明确地使用了sizeof运算符。在
ANSIC之前的C版本中,还需要用类型转换运算符(Type*)将malloc函数的类型(void*)转换为Type*类型。在C++中,只需要简单地使用如下语句就可以完成上述工作:typeNamePtr=newTypeName;运算符new自动建立一个大
小合适的对象、调用对象的构造函数(如果有的话)、返回一个具有正确类型的指针。如果new发现没有可用的空间就返回一个0指针。在C++中,释放用new分配的空间要使用运算符delete。例如:deletetypeNamePtr;C++允许用
下面的方法为新建的对象提供一个初始化值:float*thingPtr=newfloat(3.14159);这条语句把新建立的float类型的对象初始化为3.14159。可以用下面的方法建立一个数组并把它赋给int*chessBoardPtr;:ch
essBoardPtr=newint[8][8];下面的语句可以删除该数组:delete[]chessBoardPtr;用new和delete代替malloc和free还有其他好处,特别是使用new会自动调用构造函数,使用dele
te会自动调用类的析构函数。常见的程序设计错误是:把风格不同的new-delete和malloc-free混合使用。用malloc分配的内存是不能用delete释放的,用new建立的对象也不能用free删除。因此,尽管C++程序中也可以用malloc分配和用free释
放内存,但最好还是只使用new和delete。6.类的静态成员类的每一个对象通常都有该类所有数据成员的一份单独的拷贝,有时这是一种浪费。在某些情况下,特定数据成员的一份拷贝应该为类的所有对象共享,静态数据成员就是用于这个目的。静
态数据成员提供的是所有对象的共享信息。静态数据成员的声明以关键字static开头。如果仅有数据的一份拷贝就足以满足要求,使用静态数据成员就可节省内存。虽然静态数据成员看起来有些像全局变量,但它只具有类作用域。静态成员可以是公有的、私有的或受保护的。静态数据成员必须在文件作用域
内初始化。类的公有静态成员既可以用类的对象访问,也可以用作用域运算符“::”通过类名访问。私有的和受保护的静态成员只能用该类的公有成员函数访问。即使不存在类的对象,类的静态成员也是存在的。在这种情况下,
访问公有静态成员的方法很简单,即只要在该静态数据成员的前面加上类名和作用域运算符就可以了,但是在访问类的私有静态数据成员时,必须提供公有的静态成员函数,并且要在调用该函数的时候,在函数名前面依次加上类名和作
用域运算符。下面一条语句是在文件作用域内把数据成员count初始化为0:intEmployee::count=0;数据成员count保存的是类Employee已被实例化的数目。当存在类Employee的对象的时候,对象的任何一个成员函数都可以引用成员count。如果不存在类Employe
e的对象,那就只能像下面那样调用静态成员函数getCount来引用成员count了:Employee::getCount();但是当存在类Employee的对象时,可以用通过其中如何一个对象调用该函数,如:e1Ptr
->getCount();//类EmpolyeeclassEmployee{public:Employee(constchar*,constchar*);//构造函数~Employee();//析构函数
char*getFirstName()const;//返回姓char*getLastName()const;//返回名//静态成员函数staticintgetCount();//返回对象的数目private:char*firstName;c
har*lastName;//静态数据成员staticintcount;//实例化对象的数目};//初始化静态数据成员intEmployee::count=0;//定义返回对象数目的静态成员函数intEmployee::getCount(){retur
ncount;}3.2.10继承在面向对象程序设计时,一个程序的开发可能由多个程序员来完成,每个程序员完成其中的一个部分。由于各程序员采用的概念和模型可能存在着较大的不一致,因此,会导致各个阶段的开发人员之间交流存在着较大的问题,而且也不便于对系统的高效维护,
用继承可以解决这个问题。1.类的层次客观世界中的对象既具有共性,也具有个性,这形成了现实中复杂但层次清晰的层次结构。通过抽象来认识事物,获得主要的、起控制作用的、我们关心的特性,摒弃那些次要的、我们不关心的特性。因
此,抽象的层次不同,分类的层次就不同。抽象的程度是根据研究的复杂度和范围来确定的。不同层次的类之间体现了概括和特化的关系。概括是从某些具有共性的对象或类中抽象出更高一层的类。反之高层次类可以衍生出低层次的类,称为特化。高层次的类
被称为父类,低层次的类被称为子类。子类可以继承父类的一些特性。根据父类和子类的不同关系,继承可以分为四类:替代继承、包含继承、限制继承和特化继承。2.继承性的含义对象类之间的相关关系若存在继承,则具有如下的性质:(1)类之间具有共享特性;(2)类之间具有衍生特性
的个性差别;(3)类之间具有层次关系结构。从模块的角度看,继承是一种关键性的复用技术,一个模块可以向外界提供服务。如果没有继承,每个新的模块都必须定义它所要提供的新的属性和服务,这样就有可能出现代码重复、
资源浪费和程序晦涩的情况。继承机制提供了充分利用资源的方法。继承的好处在于它符合模块设计的开放-闭合原则,一个类是闭合的,因为它可以被编译、被储存在库中、被其他类使用,同时它又是开放的,因为它可以被继承,它的派生类可
以增加新的属性和行为,而它本身并不需要被改动。3.基类和派生类当一个类被其他类继承时,被继承的类称为基类,继承其他类的类称为派生类。基类具有一个类集合中的公共特性,派生类在继承其他类特性的同时也可以加入自己独有的特性。基类与派生类的关系如下:
(1)派生类是基类的具体化。(2)派生类是基类的延迟定义。可以定义一个抽象基类并定义一些操作,使它们服从一定的协议,但许多操作可能并未实现,然后定义非抽象的派生类,实现抽象基类中定义的行为。这是派生类而不是基类的具体化,是抽象类的实现。(3)派生类是基类的结合。当一个派生类有多
于一个基类时,它们组合在一起形成具有所有基类行为的类型。例3.21基类与派生类的说明举例。classA{//定义一个基类,也称为父类intj;public:voidset_j(intn){i=n;}intget_j(intj){returnj;}};c
lassB:publicA{//类B继承类A的公有成员intk;public:voidset_k(intn){k=n;}intnul(){returnk*get_j();}//可调用基类的get_i()};//B不能访问A类
的私有成员jmain(){Bob;ob.set_j(10);//装入j,B可以通过A类的set_j()访问job.set_k(4);//装入B中的k;cout<<ob.nul();//显示40return0;}继承基类的派生类的定
义的一般形式如下:classderived-class-name:access-specifierbase-class-name{};其中,access-specifier可以是3个关键字public、private(默认值)或protecte
d之一。派生类也称为导出类,它具有以下的特点:(1)可在基类所提供的基础上包含新成员;(2)可在自己的类中隐藏基类的任何成员;(3)可为新类重新定义基类中的函数。我们通过下面的程序来看看子类的特点。#include"iostream.
h"#include"conio.h"classbase{//父类inti;//父类中的变量public:intget_i(){returni;}//父类中的函数intset_i(intn){returni=n;}}voidprint()//父类中的函数{cout<<"Hello!C++basec
lass"<<endl;};classderived:publicbase{//子类inti;//子类中的变量隐藏父类中的变量public:intset_i(intn){returni=n;}intget_i(){returni;}//以上两个函数均是子类中的成员函数,用于覆盖父类中的成员函数vo
idprint(){cout<<"Hello!C++inderivedclass!"<<endl;}//父类中的函数被重新定义}voidmain(void){derivedd;d.set_i(3);//子类中的i被设为3,父类中的set_i()函数被覆盖,i未被赋
值cout<<"printinbaseclass:"<<endl;d.base::print();//父类中的打印函数cout<<"printinderivedclass:"<<endl;d.print();//子类中定义的打印函数cout<<"iinderivedclass:"<<
d.get_i();//验证子类中的i并被赋值为3cout<<"iinbaseclass:"<<d.base::get_i();//验证父类中的id.base::set_i(100);//父类中的i被赋值为100cout<<"iinderivedcl
ass:"<<d.get_i();//验证子类中的i,此时i仍为3cout<<"iinbaseclass:"<<d.base::get_i();//验证父类中的i,此时i为100}程序的运行结果为:printinba
seclass:Hello!C++baseclass!printinderivedclass:Hello!C++inderivedclass!iinderivedclass:3iinbaseclass:1234iinderivedclass:3i
inbaseclass:1004.派生类的继承权与访问域派生类并不能继承基类的所有成员,这就涉及到一个继承权的问题,派生类也不能访问基类中所有的成员,这就是派生类作用域的问题。对于基类的私有成员(Private),派生类及派生类的使用者无权访问;对于基类的公有成员(P
ublic)和保护成员(Protected),则按派生类的定义可分为三种情况:(1)私有派生:派生类继承基类的公有成员和保护成员,作为自己的私有成员。关键字是private。(2)公有派生:基类中所有的公有成员,在派生类中都是公有的;基类中所有的保护成员,在派生类中都是保护的。关键字是pub
lic。(3)保护派生:基类的公有成员和保护成员在派生类中都是保护成员。关键字是protected。派生类对基类成员的继承状况见表3-5。表3-5成员访问控制表继承类型基类性质派生类性质publicpublicpublicp
rotectedprotectedprivate不可访问protectedpublicprotectedprotectedprotectedprivate不可访问privatepublicprivatep
rotectedprivateprivate不可访问例3.22私有派生实例。classA{inta;public:intget(){returna;}};classB:AclassB:A{//默认为私有派生int
xb;public:voidmake(){xb=get()+10;}};voidfunc(){Aob1;Bob2;ob2.make();//是B类成员函数,可以访问ob2.get();//错误,不能访问}由于get()在类B中是不可见的,作为类的使用者的f
unc()函数是不能访问get()的,因此ob2.get()是错误的。如要求基类中公有成员在私有派生类者也是公有的,则可以在派生类的公有段特别加以说明,我们可以用“类名::”来说明,例如,例3.22可以改为:classB:A{intxb;public:voidmake(){xb=get()+10;
}A::get();//特别声明,get()也是公有成员};当基类的部分成员需要成为派生类的公有成员时,可以用这种方法分别说明。5.派生类对基类成员直接访问问题派生类不能访问基类的私有成员,若要访问必须使用基类的接口,即通
过成员函数访问。直接访问基类成员,有以下几种方法:(1)在类的定义体中增加保护段(protected),将基类私有成员提供给派生类的访问部分放置在保护段。派生类对基类的保护成员的继承是:若基类的保护成员为公有派生,则在派生类中也处于保护段;若基类的保护
成员为私有派生,则在派生类中变为私有成员。(2)将需要访问基类私有成员的派生类成员函数声明为基类的友元。(3)通过接口。即在基类中定义一个公有成员函数(该函数直接访问基类的私有成员),在公有继承后,派生类通过该公有成员函数间接访问私有成员。6.访问域的调整
规则使用作用域符“::”可以调整访问域,但要注意其在使用时的限制条件,具体的说,有如下的限制:(1)访问声明只能针对变量或函数名,不能说明类型和参数;重载函数只需要一个声明即可。(2)不能对私有段成员作访问声明。(3)只能在相应的段作访问声明
,不能改变所属段。即基类成员被调整后,在派生类中的访问权限既不能扩大也不能缩小,基类中的公有成员只能被调整为公有成员,保护成员只能被调整为保护成员,私有成员不可调整。例3.23调整访问域实例。classbase{publ
ic:inta;protected:intb;private:intc;};classderived:base{public:base::a;//正确,使a成为derived的公有成员base::c;//错误,不允许对基类的私有成员进行访问base::b
;//错误,试图扩大基类保护成员b的访问权限protected:base::b;//正确,使b成为derived的保护成员base::a;//错误,试图限制基类公有成员a的访问权限base::c;//错误,试图阻止对基类的私有成员进行访问};从本例中我们可以清
楚的知道作用域调整的原则。在继承时,如果基类和派生类具有同名成员,则基类的该成员被编译器隐藏起来。若在派生类中调用继承来的被隐藏的成员,用访问声明指定基类类范围,而非派生类的同名成员,派生类在使用继承成员时可以视为在派
生类的范围类,而不必对这些成员重新定义。7.派生类的构造函数和析构函数基类有构造函数和析构函数,在创建和结束派生类对象时,应该遵循如下原则:(1)执行原则。当基类和派生类都具有构造函数和析构函数时,将按顺序(先基类后派生类)执行构造函数,而按相反的顺序执行析构函数。(2)派生类构造函数。当派生类本
身需要构造函数,或在定义派生类对象时,其相应的基类对象需要调用带参数的构造函数,我们就必须定义派生类的构造函数。定义格式如下:derived-constructor(arg-list):base(arg-list){//构造函数执行体}下面的程序中建立了一个变量传递链,首先将
基类和派生类所需的所有变量都传递给派生类构造函数。然后,用派生类构造函数的扩展说明形式(可理解为初始化列表),将某些变量传递给基类构造函数。#include<iostream.h>classbase{intj;public:base
(intn){cout<<"constructingbaseclass"<<endl;j=n;}~base(){cout<<"destructingbaseclass"<<endl;}voidshowj(){cout<<j<<e
ndl;}};classderived:publicbase{intk;public:derived(intn):base(n){//passargtobaseclasscout<<"constructingderivedclass"<<endl;k=n;}~derived(){c
out<<"deconstructingderivedclass"<<endl;}voidshowk(){cout<<k<<endl;}};main(){derivedob(20);ob.showj();ob.showk();return0;}以上程序实现了从派生类向基类传递变量。派
生类首先调用自身的构造函数,将n=20传递给基类,然后通过基类构造函数,将j初始化为20,实现了变量的传递。(3)若基类使用缺省构造函数或不带参数的构造函数,则在派生类中定义的构造函数可略去“:base(arg-list)”,此时若派生类不需
要初始化,则可不定义构造函数。派生类是否要定义析构函数与所属基类无关。若派生类在退出其定义域前需要释放内存等,那么就需要定义析构函数。因为析构函数不带参数,有一定的独立性,所有基类的执行不会受到派生类是否有析构函数的影响。8.多重继承在面向对象程序设计时使用
继承的方法,可以满足对程序的扩展要求。利用继承可以在基类的基础上建立新类。在程序扩展时,继承的层次可能不止是单一的继承和单一的层次,可能会遇到复杂的情况,这就需要使用多重继承。1)多层继承方法多层继承关系是指任何一层派生类都
可以成为其下一层继承的基类。此时,原始基类可以称为第二个派生类的间接基类。多层继承类层次如图3-6所示.多层继承的方法是指所有派生类都只需对其上一层基类负责,用户只要知道哪些是可以继承的内容即可;多重继承是一种抽象机
制,类层次图是说明抽象的工具;多层继承是软件重用机制,把类继承作为软件代码重用和功能扩展的工具。2)直接继承多个基类的方法多基类继承是指一个派生类直接继承多个基类。两个或多个基类联合产生一个派生类,被称为多继承,其类层次如图3-7所示。图3-6多层继承类层次图AB1C1C21C
22B2图3-7多继承类层次A1A2A3B1B2F多继承的定义格式如下:classderived_name:accessbase_class1,accessbase_class2,...{//成员列表};其中,access是访问限定词,各个基类可以不同,
base_class是被继承的基类名,在此继承中构造函数按从左到右的顺序依次构造各个继承的基类的成员,析构函数执行的顺序与构造函数执行的顺序相反。在多重继承中,派生类的基类成员在派生类中的可访问性与单继承的规则相同。3)多继承的构造函数与析构函数在多重继承中构
造函数的定义与单继承类似,只是几个构造函数之间用逗号分隔。在参数个数的设计上必须考虑完成多个基类初始化所需的参数的数目。例3.24建立一个带滚动条的窗口的类。classwindow{public:window(
inttop,intleft,intbottom,intright);~window();};classscrollbar{public:scrollbar(inttop,intleft,intbott
om,itnright);~scrollbar();};classscrollbarwind:window,scrollbar{public:scrollbarwind(inttop,intleft,intbottom,int
right);~scrollbarwind();};scrollbarwind::scrollbarwind(inttop,intleft,intbottom,intright),window(top,left,bottom,right),scrollb
ar(top,right-20,bottom,right){}本例就是一个多重继承的问题,类scrollbarwind继承了类window和类scrollbar,并为scrollbarwind定义了构造函数和析构函数。4)继承成员二义性与虚继承类方法
当一个基类被一个派生类间接继承多次,或者说多条继承链路有公共的基类时,该基类就会存在多个备份,系统无法分辨对基类成员的引用是通过哪个派生类继承来的,这样编译器在编译时就会发出错误信息。这是一种不确定性问题错误。例3.25二义性举例。classbase{inta;public:voidf();};
classderived1:publicbase{};classderived2:publicbase{};classderived12:derived1,derived2{public:voidg(){f();//错误,是从derived1继承还是从derived2继承}};在编译器编译
时它不能分辨类derived12中的函数f()是从哪个基类继承而来的,这样就产生了错误,即二义性错误。二义性检查是在访问控制或类型检查之前进行的。因此当不同基类成员中具有相同名字时就会出现二义性,即使只有一个名字可
以被派生类访问或只有一个名字的类型与要求相符合时也会产生二义性错误。可以采取多种方法来避免二义性问题,以下是避免二义性的几种方法:(1)利用范围运算符指明所要调用的成员函数所属的类;(2)在派生类中重新定义一个与基类中同名的成员函数,使该函数隐蔽基类的同
名成员;(3)将公共基类说明为虚基类,避免在派生类中保留多个基类的备份,而只保存一个实例。下面用虚基类来解决二义性问题。在定义派生类时,要在基类描述前加关键字virtual,这称为虚基类机制。引入虚基类的原因有两点:一是为了防止二
义性,二是为了使派生类中只有公共基类的一个数据副本。例3.26虚基类实例一。#include"iostream.h"#include"conio.h"classbase{protected:intprivate1;public:base(intp){pri
vate1=p;}};classderived1:base{public:derived1(intp1):base(p1){};intget_private(){returnprivate1;}};classderived2:base{public:der
ived(intp2):base(p2){};intget_private(){returnprivate1;}};classderived12:publicderived,publicderived2{public:derived12(intp1,intp2):d
erived1(p1),derived2(p2){};//对类的初始化};voidmain(){derived12d(10,20);cout<<"\nprivate1fromderived1:"<<d.derived1::get_private();//从derived1继承cout<<"\np
rivate1fromderived2:"<<d.derivde2::get_private();//从derived2继承getch();}程序运行结果为:private1fromderived1:10private1fromderived2
:20在本例中我们在初始化derived12时,使用了derived12(intp1,intp2):derived1(p1),derived2(p2){}语句,这样就很清楚地说明了初始化的过程,参数p1传递给derived1(),参数p2传递给der
ived2(),这样在编译时就不会出现问题。当一个基类被声明为虚基类后,即使它成为了多继承链路上的公共基类,最后的派生类中也只有它的一个备份。例如:classL{};classA:virtualpublicL
{};classB:virtualpublicL{};classC:publicA,B{}其中,在类C的对象中,仅有类L的一个对象数据。对于虚基类我们应该注意以下两点:(1)虚基类构造函数的参数必须由最新派生出来的类负责初始
化(即不是直接继承);(2)虚基类的构造函数先于非虚基类的构造函数被执行。例3.27虚基类实例二。#include"iostream.h"#include"conio.h"classbase{protected:intprivate1;publi
c:base(intp){private1=p;}};classderived1:virtualpublicbase{public:derived1(intp1):base(p1){};intget_private(){returnprivate1;}};classderived2:virt
ualpublicbase{public:derived2(intp2):base(p2){};intget_private(){returnprivate1;}};classderived12:publicderived1,publicder
ived2{public:derived12(intp1,intp2,intpb):derived(p1),derived2(p2),base(pb){};//初始化,当初始化表不同时,初始化的结果也不同};voidmain
(){derived12d(10,2,34);cout<<"\nprivate1fromderived1:"<<d.derived1::get_private();cout<<"\nprivate1fromderived2:"<<
d.derived2::get_private();getch();}程序经编译链接后的运行如下:private1fromderived1:34private1fromderived2:34在本例中,类derive
d12的构造函数的初始化表中调用了间接基类base的构造函数,这对于非虚基类是非法的,但对于基类来说是合法的且是必要的。另外,由于虚基类的构造函数先于非虚基类的构造函数被执行,因此在初始化时,将基类的成员private
1初始化为pb,在本例中其值是34。对于派生类derived1和derived2,不论是其内部实现,还是实例化的对象,基类base是否是它们的虚基类是没有影响的,受到影响的是它们的派生类derived12,因为它可以通过两条路径到达基类base。9.实例分析下面通过对下例的分析来说明
继承性技术应用的细节。例3.28图形类。classShape{protected://屏幕上像素点的水平和垂直位置floatX,Y;intfillpat;public://构造函数Shape(floath=0,floatv=0
,intfillpat=0);//X和Y的接口函数floatGetX(void)const;floatGetY(void)const;voidSetPoint(floath,floatv);//图形填充方式的接口函
数intGetFill(void)const;//返回填充方式voidSetFill(intfill);//改变填充方式//纯虚函数,派生类必须定义一个具体的Area、Perimeter和Draw方法vi
rtualfloatArea(void)const=0;virtualfloatPerimeter(void)const=0;}图形类是一个通用类,在该类中定义了点、填充方式和改变这些参数的接口函数。这个类可由具体的派生图形类来继承,在继承的过程中,派生类可以添加具体的计算方法,如Draw,A
rea和Perimeter。在该图形类中,构造函数定义的基点(0,0)在窗口的左上角,缺省填充方式0,一般设为不进行任何填充。GetX和GetY返回基点的X和Y的坐标,SetX和SetY用于设置基点的坐标。同样GetFill和SetFill
用于获得和设置填充方式。Draw方法用于初始化绘图系统,使图形按照一定的填充方式进行绘图。Area和Perimeter是纯虚函数,它们的具体定义一定要在派生类中说明。下面我们将具体介绍构造函数和Draw函数,注意构造函数要求基点坐
标和填充方式,用户程序可以通过SetPoint和SetFill改变它们的值。//构造函数,初始化基点坐标和填充方式Shape::Shape(floath,flaotv,intfill):x(h),y(v),fillpat(fill)voi
dshape::Draw(void)const{SetFillStyle(fillpat);}Draw函数调用了图形处理函数SetFillStyle,当派生类画图形的时候,填充方式将会生效。接下来我们声明圆形类(Circle),圆形类有半径和具体的计算面积和周长的函数(Area
和Perimeter)。半径在创建对象时通过值来传递,这个值不可以外部访问。Draw函数从Shape类中继承了基点和填充方式。//PI是圆周率constfloatPI=3.1415;//Circle类的声明classCircle:publicShape{pro
tected://如果考虑Circle类可能成为某个类的父类,半径应被设计成为可被外部//访问的floatradius;public://构造函数Circle(floath=0,floatv=0,floatr=0,intfill=0);//半径
接口函数floatGetradius(void)const;voidSetradius(floatr);//Circle类的Draw方法virtualvoidDraw(void)const;//测量方法virtualfloatArea(void
)const;virtualfloatPerimeter(void)const;};在给出了声明后,我们给出类Circle类的构造函数和draw函数的实现。//构造函数//参数h和v用于初始化基点//参数fill用于初始化填充方式//r是只可被C
ircle类成员访问的变量//基类对象被构造函数Shape(h,v,fill)初始化Circle::Circle(floath,floatv,floatr,intfill)Shape(h,v,fill),r
adius(r){}//画圆函数drawvoidCircle::Draw(void)const{Shape::Draw();CrawCircle(x,y,radius);}为了说明Circle类的使用方法,用下面的一段
程序来声明两个Circle对象,然后调用一些测量和绘图函数。voidmain(){//声明对象C和D//C采用填充方式7,D采用填充方式0CircleC(1.0,2.0,0.5,7),D(2.0,2.0,0.3
3);chareo1;//用于画图前延迟cout<<"coordinatesofcare"<<C.GetX()<<"and"<<C.GetY()<<endl;cout<<"Circumferenceofcis"<<C.Perimeter()<<endl;cout<<"Ar
eaofcis"<<C.Area()<<endl;cout<<"Type<return>toviewfigures:";cin.get(eol);//初始化画图平面initgraphics()//画圆C,半径为0.5填充方式为7C.Draw();//画圆D,半径为0.3
3,填充方式为0D.Draw();//改变对象C的半径,基点和填充方式C.SetPoint(3.2,1.3);C.SetRadius(0.4);C.SetFill(2);C.Draw();}3.2.11运算符重载1.运算符的重载引言重载运算符是C++的一个特性,它使
得程序员可把C++运算符的定义扩展到运算分量是对象的情况。应该视类对象不同赋予运算符不同的含义。运算符重载是对系统已有预定义的运算符赋予新的含义,用自然的方式将其发展到特殊应用领域,运算符重载的目的是使C
++代码更直观、更易读。由简单的运算符构成的表达式常常比函数调用更简洁、易懂。重载一个运算符时,要满足两个条件:第一,不能改变运算符的优先级和结合方向;第二,不能改变运算符的目数。运算符重载只是增加了一些与定义它的类相关的附加意义。C++中,大部分预定义的运算符都可以重载,见表
3-6。只有以下几个运算符例外:“.”、“::”、“#”、“*”(取指针)、“?”、“:”。表3-6C++中可以重载的部分运算符+−*(乘)/%^&|~!=<>+=−=*=/=%=^=&=!=<<>>>>=<<===<=>=&&||++−−[]()->New
Delete1)重载运算符的需要性在C++中用户定义的类类型在语法上同基本数据类型一样有效。在C语言中,运算符是为基本数据类型而预定义的。在C++中,允许这些运算符对不同的类类型数据进行运算,但相应的运算符应被赋予新的计算含义,使之适用于类类型的对象。例如:c
lassA{public:A(intx){a=x;}};Aa(5),b(6),c;c=a+b;//类对象也应能运算2)如何进行运算符的重载运算符重载的格式如下:return_typeclassname::operator@(opera
nds){//operationtoperformed(操作形式)}其中,classname是重载运算符的类名,operator是运算符重载的关键字,@是要重载的运算符的符号,operands是该运算符所需的操作数。operator是运算符函数名,函数返回类型是return_type。在
类的说明体内声明运算符函数时用如下形式:typeoperator@(operands);例3.29二元运算符的重载程序。#include<iostream.h>classiac{intx,y;//坐标值public:
iac(){x=0;y=0;}//构造函数重载iac(inti,intj){x=i;y=j;}voidget_xy(int&i,int&j){i=x;j=y;}iacoperator+(iacob2);//声明运算重载函
数};iaciac::operator+(iacob2)//定义运算符重载成员函数{iactemp;//定义一个临时对象,存放运算结果temp.x=x+ob2.x;temp.y=y+ob2.y;returntemp
;//临时对象作函数返回值}main(){iaco1(10,10),o2(15,23),o3;intx,y;o3=o1+o2;//调用operator+()进行两对象相加,由于o1+o2仍是iac对象,//因而可以赋值给o3,同理,语句o3=o1+o2+o1+o3
;也是合法的o3.get_xy(x,y);//通过临时对象调成员函数,执行后失效,//用(o1+o2).get_xy(x,y);也是合法的cout<<"(o1+o2)x:"<<x<<",y:"<<y<<endl;return0;}//运行结果:(o
1+o2)x:25,y:333)常用运算符重载讨论下面进一步讨论常用运算符重载的各类情形,分析它们在使用中应注意的技术内涵和方法上的问题。(1)重载“+”运算符是在对象上加一个int类型数的情况。例如:iaciac::operator+(intk){iactemp;temp.x=x+k;temp.
y=y+k;returntemp;}main(){o3=o1+66;//这是“对象+整数”,调用运算符重载函数o3=55+o1;//错误,对象不在运算符左边}//注意,o3=o1+66;语句使o1的x和y都分别加了66(2)重载“-
”和“=”的方法与上述重载“+”运算符类似。例如:iaciac::operator-(iacob2){iactemp;temp.x=x-ob2.x;temp.y=y-ob2.y;returntemp;}iaciac::operator=(iacob2){x=
ob2.x;y=ob2.y;return*this;}//运算符函数返回this指针,即赋值给目标对象(3)关系运算符和逻辑运算符重载。重载运算的返回值不是定义该重载运算符的对象,而是一个代表ture或false的整数。例如:intiac:
:operator==(incob2){if(x==ob2.x&&y==ob2.y)return1;elsereturn0;}intiac::operator&&(iacob2){return(x&&ob2.x)&&(y&&ob2.y);}(4)一元运算符的重载。重载一元前置运算符的成员函数没有参
数,例如--。incinc::operator--(){//前置减:--objx--;y--;return*this;}后置的重载定义,需要一个int参数,仅起区分的作用。例如:inciac::operato--(intx)//形式参数的类型为int
{//后置减:obj--x--;y--;return*this;}其他的一元运算符的重载形式与例子中的形式相似。(5)使用友元进行运算符重载。可以用友元函数重载运算符,但它与成员运算符函数的主要区别在于其参数个数不同。不用友员重载时,成员函数通过this指针传递给运算符左边的操作数,而友元
函数则没有this指针,必须显式传递所有参数。例如:friendincoperator?(iacob1,intk);friendincoperator+(intk,iacob2);2.存储分配的重载通过对new和delete的重载,
能够在其外部定义库函数的通用算法的基础上,提高特定情况下的效率,用户可以定制自己的内存分配方案,其一般的格式如下:void*operatornew(unsignedintsize){//size的值是存放对象所需字节数returnpointer_to_m
emory;}//为对象分配空间时,会自动调用构造函数voidoperatordelete(void*p){//释放指针p所指内存}//释放指针p指向的空间,对象失效时会自动调用析构函数虽然new和delete重载存在两种方式:全局重载方式和一个类的局部重载方式。但一般都是对一个类进行重载,也
就是把重载运算符函数说明为类的成员函数。3.对象赋值与特殊按位拷贝运算对于一般情况下的类对象赋值,使用缺省赋值运算符就可以了,但在有些情况下使用普通赋值运算符会出现错误,需要在类重载时实现“严格”的按位拷贝,将内容和地址传递一起进行。例如,带有指针问题的赋值
,使用普通赋值运算符会出现指针悬挂错误。#include<string.h>classstring{char*contents;intsize;public:voidoperator=(string*);};voidstring::operator=(str
ing*str){if(this==str)return;deletecontents;//释放原来区域contents=newchar[size=str->size];//重新分配空间strcpy(
contents,str->contents);}main(){strings1("1234"),s2("赋值重载");s2=s1;//OK,指针并不覆盖return0;}4.运算符[]和()的重载运算符()为函数调用运算符,运算符[]是下标运算符(变址)。它们的
定义形式如下:typeclass_name::operator[](intindex){…}typeclass_name::operator()(string){…}5.类型转换(1)在基本类型的运算中,不同类型混合运算时,其类型转换是自动进行的。同样,在不同类clas
s的生成对象中,混合运算C++也是允许的,这样将扩大重载运算符的用途,但必须在类中定义转换构造函数。显然,转换的目的是将其他类型变为当前类。所以转换构造函数只能有一个其他类的参数。(2)下面的程序将基本类型double转换成为复数类。classcomplex
{doublerpart,ipart;public:complex(){rpart=ipart=0.0;}complex(doublerp,doubleip){rpart=rp;ipart=ip;}complex(constdoub
le&d){rpart=d;ipart=0;}complexoperator+(constcomplex&com){complextemp(com.rpart+rpart,com.ipart+ipart);retu
rntemp;}};//主调函数main(){complexa,b(1,4),c;c=a+b;c=a+3.0;}说明:上面的程序实际上是用只有一个参数的构造函数来实现double类型向complex类的转换的,称为转换构造
函数。即转换构造函数实质上是一个只带单参数且该参数不是自身类的普通构造函数。但是,要达到不同书写格式的类的转换,需适当考虑上面程序的一些技巧。例如:main(){complexa,b;b=3.0+a;
}//错误此时,编译器会将“b=3.0+a”解释为3.0.operatoor(a)。应当将“+”运算符定义为外部友员。(3)用于转换的构造函数一旦定义,其不仅是作为混合运算时的类转换,而且像初始化、函数返回、对象作为形参到实参等情况都可使用,因为它仍然是一个普通构造函数
。如:complexf(){return1.0;}voidff(complexc);main(){complexa(52.3);complexb;b=f();//赋值号右端是complex类临时对象ff(b);}实际上
,转换构造函数还能进行从任何类类型到该类的转换。例如:classcubed{public:intx,y,z;//...};classcomplex{doublerpart,ipart;public:complex(){rpart=ipart=0
.0;}complex(doublerp,doubleip){rpart=rp;ipart=ip;}complex(constcubed&d){rpart=d.x;ipart=d.y;}//实现cubed到complex的转换complexopera
tor+(constcomplex&com){complextemp(com.rpart+rpart,com.ipart+ipart);returntemp;}};//主调函数main(){cubedb;complexa,
c;c=a+b;}6.转换运算符转换运算符主要用于实现从类对象到基本类型数据的转换。转换运算符无函数类型和参数。例如:classinteger{inti;public:integer(inta){i=a;}
integer(){i=10;}operatorint(){returni;}};//主调函数voidmain(){integerk;intj=k;cout<<j<<endl;}实际上,转换运算符还能进行从任何
类类型到该类的转换。例如:classA{public:intx;};classinteger{inti;public:integer(inta){i=a;}Integer(){i=10;}operatorA()
{Aaa;aa.x=i;returnaa;}};//主调函数voidmain(){integerk;Aj=k;cout<<j.x<<endl;}通过对转换构造函数和转换运算符的分析,对C、C++的以下强制转换格式进行理解就清晰了:inti;integeri
r;C格式:ir=(integer)i;i=(int)ir;C++格式:i=int(ir);ir=integer(i);3.2.12虚函数1.虚函数的需要性在面向对象程序设计中,经常会使用对象的多态性,然而C语言中没有一种良好的支持多态性的机制。在C++中,虚函数就是为满足这种需要而创建的一种支持
动态联编,进而支持多态的机制。2.虚函数的声明和使用虚函数是成员函数,而且是非static的成员函数。说明虚函数的方法如下:virtual<类型说明符>函数名(<参数表>)其中,被关键字virtual说明的函数称为虚函数。如果某类中的一个成员函数被说明为虚函数,这就意味着该成员函数在
派生类中可能有不同的实现。当使用这个成员函数操作指针或引用所标识的对象时,对该成员函数调用采取动态联编方式,即在运行时进行关联。注意:动态联编只能通过指针或引用标识对象来操作虚函数。如果采用一般类型的标识对象来操作虚函数,则将采用静态联编
方式调用虚函数,程序执行的效率会降低。例3.30采用虚函数来实现动态联编。#include<iostream.h>classPoint{public:Point(doublei,doublej){x=i;y=j;}virtualdoubleA
rea()const{return0.0;}//定义了一个虚函数private:doublex,y;};classRectangle:publicPoint{public:Rectangle(doublei,doublej,doublek,double
l);virtualdoubleArea()const{returnw*h;}//定义了一个虚函数private:doublew,h;};Rectangle::Rectangle(doublei,doublej,doublek,doublel):Point(i,j){w=k;h=l;}
voidfun(Point&s){cout<<s.Area()<<endl;//调用虚函数实现动态联编}voidmain(){Rectanglerec(3.0,5.2,15.0,25.0);fun(rec);}通过
这个例子可以看到,派生类中对基类的虚函数进行替换时,要求派生类中说明的虚函数与基类中的被替换的虚函数之间满足如下条件:(1)与基类的虚函数有相同的参数个数;(2)其参数的类型与基类的虚函数的对应参数类型相同;(3)其返回
值或者与基类虚函数的相同,或者返回指针或引用,并且派生类虚函数所返回的指针或引用的基类型是基类中被替换的虚函数所返回的指针或引用的基类型的子类型。满足上述条件的派生类的成员函数自然是虚函数,可以不必加virtaul说明。动态联编的实现需要以下条件:
(1)要有已说明的虚函数;(2)调用虚函数操作的是指向对象的指针或者对象引用,或者是由成员函数调用虚函数;(3)子类型关系的建立。以上结果可用以下程序证实:#include<iostream.h>classA{public:virtualvoidact1();vo
idact2(){act1();this->act1();//这里用指向对象的this指针调用虚函数A::act1();}};voidA::act1(){cout<<"A::act1()called."<<endl;
//由成员函数调用虚函数}classB:publicA{public:voidact1();};voidB::act1(){cout<<"B::act1()called."<<endl;}voidmain(){Bb;b.act2();//由成员函数调用虚函数}构造函数中调用虚函数时,采用静态联
编。即构造函数调用的虚函数是自己类中实现的虚函数,如果自己类中没有实现这个虚函数,则调用基类中的虚函数,而不是任何派生类中实现的虚函数。下面通过一个程序说明在构造函数中如何调用虚函数。#include<iostream.h>classA{public:A()
{}virtualvoidf(){cout<<"A::f()called.\n";}};classB:publicA{public:B(){f();}voidg(){f();}};classC:publicB{public:C(){}vi
rtualvoidf(){cout<<"C::f()called.\n";}};voidmain(){Cc;c.g();//由于C类中没有实现自己的虚函数,所以先调用基类中的虚函数,再调用//自己类中的函数f(),因此就得到下面的输出结果}程序的输出结果为
:A::f()called.C::f()called.在析构函数中调用虚函数同构造函数一样,即析构函数所调用的虚函数是自身类的或者基类中实现的虚函数。要进一步说明的是,当具有虚函数的基类不进行任何实例化时,往往这个基类被说明为
抽象基类。含有一个以上的纯虚函数的基类被称为抽象基类。纯虚函数的书写格式如下:virtualvoidf()=0;这表示在该抽象基类中,不会定义该函数的任何代码(因为该抽象基类不会生成任何实例对象)。其代码留
在该抽象基类被派生后,在派生类中定义。3.2.13模板假设有一个类data,在实际应用时其成员的类型可能是int、char、double,为此需要定义三个类,即classdata_int{intx;};classdata_char{charx;};classda
ta_double{doublex;};这样就会出现代码重复的情况,解决这个问题有两种方法:一种是用typedef或define定义相应的类型或宏来解决;另一种就是使用类模板来解决问题。应用类模板是解决代码重复的最好的方法,下面我们详细讨
论模板。如果一个程序的功能是对某种特定的数据类型进行处理,那么,将所处理的数据类型说明为参数,就可以把这个程序改写为模板。模板可以让程序对其他任何类型进行同样方式的处理。模板的目标是面向对象的另一种高级抽象机制,即类的进一步抽象(有时称为类属类)。1.类模板表示一组类的
模板即类模板,类模板的实例化为类,类的实例化为对象。类模板的一般形式如下:temple<classT>calsstemplatename(类模板名){//类模板的定义}例3.31类模板应用举例。template<classT>classdata{Tx;public:data(Tx1){x=x1;
};Tgetx{returnx;};};voidmain(){data<int>a(10);cout<<a.getx()<<endl;//输出结果为10data<char>b('c');cout<<b.getx()<<endl;//输出结果
是cdata<double>c(10.50);cout<<c.getx()<<endl;//输出结果是10.50}在使用类模板时,应该注意到类模板的特点:(1)实例化了的类模板可以像普通的类一样使用,如要将某函数f()的形参用类实例定义,应写为vio
df(data<double>&d){cout<<d.getx()<<endl;}voidmain(){data<double>a(10.50);f(a);//输出结果是10.50}(2)类模板的成员函数在类的外
部定义时,例如getx()函数,应该写为temple<classT>Tdata<T>::getx(){returnx;}类模板的一个典型应用是用来实现同(异)质链表,更高级的应用是用于建立类模板库(见第6章)。2.函数模板与类模板一样,
可以定义一个函数模板。用类型作参数的函数即函数模板。函数模板实际上是定义了一组类型的函数。函数模板的一般形式如下:template<classT>Tf(Ta,Tb){//函数体}例如:template<classT>Tmax(Ta,Tb){returna>b?a:b;}在使用函数模板时应
该注意以下几点:(1)在函数模板定义中声明的对象或类型不能与模板同名。例如:typedefdoubleType;template<classType>Typemin(Typea,Typeb){//错误,重新声明模
板参数TypeTypedoubleType;Typetemp=a<b?a:b;returntemp;}(2)模板参数名在同一模板参数表中只能被使用一次。例如://错误,模板参数名Type的非法重复使用template<classType,classType>Typemin(Type,Type);(
3)同一模板参数名可以在不同的函数模板声明或定义中被使用。例如://正确,名字Type在不同的模板之间被重复使用template<classType>Typemin(Type,Type);Typemax(Type,Type);(4)如果一个函数模板有一个以
上的模板类型参数,则每个模板类型参数前面都必须有关键字class或typename。例如://正确,关键字typename和class可以混用template<TypenameT,classU>Tminus(T*,U);//错误,必须是<typ
enameT,classU>或<typenameT,typenameU>template<typenameT,U>Tsum(T*,U);3.函数模板实例化函数模板根据一组或多组实际类型或值构造出独立的函数的过程,即是函数
模板的实例化。例3.32函数模板实例化举例。template<classType,intsize>Typemin(Type(&r_array)[size]){Typemin_val=r_array[0];for(inti=1;i<size;++i)if(r_array[
i]<min_val)min_val=r_array[i];returnmin_val;}intia[]={10,5,3,2,1}];doubleda[6]={10.2,2.3,4.5,0.3,4.2,2.1};#include<iostream.h>
intmain(){inti=min(ia);if(i!=3)cout<<"intgermin()failed"<<endl;elsecout<<"ok:intgermin()worked"<<endl;doubled=min(da);if(d!=0.3)cou
t<<"doublemin()failed"<<endl;elsecout<<"doublemin()worked"<<endl;return0;}在本程序中函数min()被初始化了两次:一次是针对5个int型的数组类型,一次是针对6个doubl
e型的数组类型。实例化了的函数模板就是定义了多个不同类型的函数,函数模板的使用使得有些需要函数代码重复的地方不必重复,使得程序简洁易懂。函数模板的高级使用见第6章。3.3Java的基础知识3.3.1Java的发展史Jav
a是由美国Sun公司开发的一种在Internet上具有“硬件/软件独立性”和交互能力的新型程序设计语言。Java程序与平台无关,并且可以分布式地在远地运行。它使得开发者编写的软件可以通过网络提供给用户,用户在自己
的计算机上以WWW页面作为取回和执行软件的平台。3.3.2Java的基本语法1.变量每一个程序都离不开变量,变量也是每一门语言的基础,Java作为一种编程语言,变量也是其最基础的一部分。Java的变量与C++的变量有许多相似之处。下面介绍Java的变量。1)变量类型Java
语言中的所有变量都必须有数据类型。变量的类型决定了变量取值范围和对变量可能的操作。例如,intcount说明count是一个整型数(int),整型数只能取整数(正的和负的),可以使用标准算术运算符(+、-等)来实现标准算术功能。在Java语言中数据类型分为两大
类:简单类型和引用类型。简单类型包括整型,浮点型,字符型和布尔型等。表3-7列出的关键词是Java所支持的所有简单数据类型,包括它们的大小/格式和简短说明。表3-7Java支持的所有简单数据类型类型大小说明整型byte8位字节长整型s
hort16位短整型int32位整型类型大小说明long64位长整型浮点型float32位单精度浮点double64位双精度浮点其他型char16位Unicode单字符booleanN/A布尔2)变量名程序
是通过变量名来存取变量值的。按照习惯,变量名是以一个小写字母开头(类名以大写字母开头)的字符串。在Java中,变量名的命名有一定的规则:(1)必须是由一串Unicode字符组成的合法的Java标识符。Unic
ode是一个字符编码方案,支持当今世界上绝大多数的字符集,这样程序员便可以在他们的程序中使用以他们的母语编写的字符,例如希腊语、俄语和希伯来语等等。Java中的方法Character.isJavaLetter可以判断一个字符是否存在于Unicode字符集。(2)变量名不能与关键词(Java语言的
保留字)和布尔变量的值(true或false)相同。(3)在同一个作用域内,不能有两个同名的变量,但在不同的作用域内可以有同名的变量。此外,在某些条件下,变量可能与另外一个声明在重叠作用域中的变量共享名字。3)作用域变量的作用域是可访问变量的代码块。变量的作用域决定变量何时被创建和撤销,说
明变量时,变量的作用域就建立了。按作用域不同变量可以分为以下4类:(1)成员变量;(2)局部变量;(3)方法参数;(4)异常处理方法参数。成员变量是一个类或对象的成员,与C++一样,成员变量在类中声明(但不是在类的任一方法中,在程序中可以没有成员变量);局部变量在方法中或方法
的一个代码块中声明。一般来说,局部变量从它的声明处到这段代码的结尾处都是可用的,即其作用域是从它的声明处开始到这段代码结束;方法参数和构造方法的参数用来将值传到方法和构造方法中。4)变量初始化和C++一样,Java的局部变量和成员变量
在声明时就可以被初始化。例如:intcount=0。在给这些变量赋值时,应和C++一样注意赋给变量的值必须符合变量的类型。方法参数和异常处理参数不能在声明时初始化,应由调用者来设置参数的值。下面讲解Java语言中不支持的C++语言数据类型:指针、结构和联合。(1)C++指针。Jav
a中的一些数据类型(例如对象和数组)是引用数据类型,引用类型变量的值是对实际值的一个引用。Java并没有一个明确的指针类型,不能建立对匿名存储器的引用,这样使得编程更加容易,可以避免由于指针管理不当引起的普遍错误。(2)C++
的结构和联合。Java既不支持结构也不支持联合,但可以使用类和接口建立复合的类型。在C++中我们可以用以下方法来定义一个包括雇员信息的结构体:structemployee{charname[NAMESIZE];ch
araddress[ADRSIZ];longssn;doublesalary;double(*compute-raise)(double,double);};其中,最后一项是一个指向函数的指针,当为employee结构分配存储区和初始化时,必须为结
构体提供一个指向函数的指针,而且函数必须定义得如同在结构中声明得一样;compute-raise函数必须接收两个double类型数据作为参数,其返回类型为double型。compute-raise函数的定义如下:doublecompute-raise(doublesalary,do
ublepercent){returnsalary﹡percent;}注意:即使salary是结构的一部分,仍需为函数提供这个值。下面创建一个主程序,初始化employee结构并为每人计算10%的增长率。main(){structemployeeGeorge={"George',"NOW
HERE",123456789,45000.00,compute-raise};printf("raise=%f\n",George.compute-raise(George.salary,0.10));}在Jav
a中,类的出现使得建立结构的需求完全过时了。在Java里用类代替了结构体。类提供了一种更清楚的方式将数据和方法结合在一起,同时也提供了一种保持数据对类私有的方法。例如:classEmployee{stringname;stringaddress;longssn;privatedo
ublesalary;doublecompute-raise(doublepercent){returnsalary;}Employee(stringa_name,stringa_address,longa_ssn,d
oublea_salary){name=a_name;addresss=a_address;ssn=a_ssn;salary=a_salary;};}注意:类Employee中包括了compute-raise方法的实现。变量salary被声明为私有的(这意味着只有这个类的实例才有权访
问工资信息),从而保护信息不被泄露。下面的程序可以计算George的工资增长而无需直接获得工资信息。classMainClass{publicstaticvoidmain(stringargs[]){Employeegeorge=newEmpl
oyee("George','NOWHERE",123456789,45000.0);systems.out.println("raise="+george.compute-raise(0.10);}}3.3.3运算符Java
提供了非常丰富的运算符,按运算符结合的操作数的数目可将运算符分为单目运算符、双目运算符和三目运算符。Java的单目运算符可以作为前缀使用,也可以作为后缀使用,这与C和C++单目运算符是一致的。和C一样,Java所有的双目运算符都是中缀的,即运算符在两个操作数中间,如op1operatorop
2除了完成操作外,运算符还可以返回一个值,返回的值及其类型取决于运算符和操作数的类型。例如,算术运算符(实现基本的算术运算,如加、减等)返回一个数,算术运算符返回的数据类型与操作数的类型有关:如果是两个整
数相加,就得到一个整数。这些规则与C中的运算符的规则也是一致的,因此学过C的同学很容易就能掌握Java运算符的使用规则。下面简单的介绍一下Java的运算符。Java运算符分为以下几类:算术运算符、关系和条件运算
符、位操作运算符、布尔逻辑运算符和赋值运算符。1.算术运算符Java支持各种各样的算术运算符,包括+、?、*、/和%,这些运算符适用于所有的浮点数和整数,见表3-8。表3-8算术运算符运算符使用方法说明+op1+op2op1加op2-op1-op2
op1减op2运算符使用方法说明*op1*op2op1乘op2/op1/op2op1除op2%op1%op2op1除以op2的余数表3-9“+”和“?”运算符运算符使用方法说明++op对op无影响--op对op
取负值说明:Java扩展了运算符“+”的定义,以包括字符串的连接。语句System.out.println("Inputhas"+count+"chars.");使用“+”将“Inputhas”、count的值和“chars”连接在一起(注意:这个运算符会自动将count的值转
化为一个字符串)。+和?也可以充当单目运算符来设置操作数的符号,见表3-9。除此之外,还有两个使用方便的算术运算符++和--,它们分别将操作数加1或减1。它的使用规则与C中++和--运算符的使用规则相同,使用后变量的结果也是与C相同的。在
使用时应注意它们的使用方法,见表3-10。表3-10++和??运算符的使用方法及说明操作数使用方法说明++op++先使用op然后op加1++++opop加1--op−−先使用op然后op减1----opop减12.关系和逻辑运算符关系运算符用来比
较两个值,并决定它们之间的关系。例如,如果两个操作数不等,!=返回"真"。表3-11列出了Java的关系运算符。通常,关系运算符和逻辑运算符一起使用,以建立更复杂的判断。&&运算符执行逻辑运算。可以对两
个关系操作使用&&运算符,以判断两个关系是否都是真的。下面的代码中使用&&判断数组下标是否越界,也就是判断下标是否同时大于0和NUM-ENTRIES(这是一个变量,定义数组的大小)。(0<index)&&(index<NUM-ENTRIES)在Java中逻辑运算符的操作方法和规则与
C的一致。表3-12列出常用的逻辑运算符。表3-11关系运算符运算符使用方法说明>op1>op2如果op1>op2,则结果为真,否则为假>=op1>=op2如果op1≥op2,则结果为真,否则为假<op1<op2如果o
p1<op2,则结果为真,否则为假<=op1<=op2如果op1≤op2,则结果为真,否则为假==op1==op2如果op1=op2,则结果为真,否则为假!=op1!=op2如果op1≠op2,则结果为真,否则为假表3-12
常用的逻辑运算符运算符使用方法说明&&Op1&&op2如果op1和op2都为真,则结果为真,否则结果为假||Op1||op2如果op1或op2为真,则结果为真,否则结果为假!!op如果op为假,则结果为真,否则结果为假3.位运算符位运算符可以
实现对数据的位控制,它们的使用方法,与其在C中的使用方法是一致的。表3-13列出了Java中的位运算符。表3-13位运算符运算符使用方法说明>>op1>>op2op1右移op2位<<op1<<op2op1左移op2位>>>
op1>>>op2op1右移op2位,但op2是无符号数&op1&op2逐位相与|op1|op2逐位相或^op1^op2逐位异或~~op逐位取反在一些情况下,位操作对设置布尔标志十分有用。例如,在程序中有几个布尔标志指示程序
中不同元素的状态:是不是可见的,是不是可拖动的,等等。我们可以为每一个标志定义一个布尔变量,可为它们定义一个变量flags。Flags的结果代表某一个的状态,这样就可以用位运算符来设置和读取每一个标志。首先,要为不同的标志设置常量,这些常量的值必须是2的幂,以
保证对标志的操作不会覆盖其他标志;然后,定义一个变量flags,根据每个标志的当前状态来设置它的位。下面的代码将flags初始化为0,意味着所有的标志都为0(没有位被置位)。finalintVISIBLE=1;finalintDRAGGABLE=2;intflags=0;想把某位设置为可见时,可
以用下面的语句设置“VISIBLE”标志。flags=flags^VISIBLE可以用下面的表达式检验可见性。flags&VISIBLE4.赋值运算符赋值运算符用于把一个值赋给一个变量。下面使用“=”初始化count。intcount=0;除了基本的赋值运算符,Java还提
供了几个快捷赋值运算符,使赋值运算符和另一个运算符一起实现算术、逻辑、位操作和赋值操作。例如:i=i+2;可以写为i+=2;表3-14给出了快捷赋值运算符及其等价操作。表3-14快捷赋值运算符及其等价操作运算符使用方法等价于+=op1+=op2op1=op1+op2-=op1-=o
p2op1=op1-op2*=op1*=op2op1=op1*op2/=op1/=op2op1=op1/op2%=op1%=op2op1=op1%op2&=op1&=op2op1=op1&op2|=op1|=op2op1=
op1|op2^=op1^=op2op1=op1^op2<<=op1<<=op2op1=op1<<op2>>=op1>>=op2op1=op1>>op2>>>=op1>>>=op2op1=op1>>>op23.3.4表达式与运算符一样,表达式也是任何一种编程语言都不可缺少的核心基础,表
达式完成Java程序的实际工作。在其他方面,表达式用于计算和给变量赋值,帮助控制程序的执行流程。表达式的工作是双重的:完成表达式元素所指示的计算和返回一些值。表达式的定义,以及一些常用的表达式和它们的使用规则,在C中就已详细介绍过。在Java中,表达式的使
用与C语言是一致的,这里不再介绍,仅详细介绍表达式在Java和C中的不同之处。因为运算符返回一个值,所以运算符的使用就是一个表达式。例如:count++;表达式返回的值的数据类型与表达式中的元素有关,表达式count++返回一个整数是因为+
+返回值的类型与它的操作数一样,count是一个整数。其他表达式返回布尔值、字符串等。例如,表达式System.in.read()!=-1;实际上包括了两个表达式,第一个表达式是方法调用System.in.read(),方法
调用计算并返回方法的值。方法的数据类型与方法返回值的类型一致,System.in.read()方法声明返回整数,故表达式System.in.read()表示一个整数。System.in.read()!=-1包含的第二个表达式是!=运算符,!=用于比较它的两个操作数是否不等
。System.in.read()和-1是两个操作数,!=返回的值根据比较的结果为“true”或“false”。Java允许用户将各种各样的小的表达式组合成表达式语句,只要表达式的一部分要求的数据类型和其他部分的数据类型符合即可。组合表达式中各
运算符的顺序有时会产生问题。例如:x*y*z在这个表达式中,因为相乘的结果与顺序无关,所以表达式中各运算符的顺序并不重要。然而在下面的表达式中不是这样:x+y/100加法和除法运算符的执行顺序不同会有不同的结果,可以使用圆括号明确地告诉Java解释器如何计算表达式。例如为使上句没
有歧义,可将其写为(x+y)/100如果没有明确告诉编译器你希望的操作顺序,则依靠表达式中运算符和其他元素的优先级来决定,具有较高优先级的运算符先执行。例如,除法的优先级高于加法,在表达式x+y/100中,编译
器将先计算y/100,因此x+y/100等价于x+(y/100)。为了使代码容易阅读与维护,应在适当的地方明确地用圆括号“()”指示哪一个运算符优先计算。下面列出的是Java运算符的优先级,运算符的优先级顺序是:上边的运算符优先级比下面的高;同
一层的运算符具有相同的优先级,运算时的顺序为从左到右。·、[]、()++、??、!、~、instanceofnew、(type)*、/、%+、-<<、>>、>>><、>==、!=&^/&&‖?、:=、+=、?=、*=、/=、%=、^=&=、/=、<<=、>>=、>>>=3.3.5流程
控制语句前面已详细介绍了C++流程控制语句。在Java中,其流程控制语句和流程结构与C++中的是完全相同的,在这里就不再赘述。本节只介绍一下在C++中没有提到的那部分控制语句。1.异常处理语句当Java方法中发生错误时,方法可产生一个异常,可以使用thr
ow语句指示有错误发生以及发生了什么错误。调用方法时可以用try、catch和finally去捕获和处理中断。然后利用异常处理错误,就可以得到关于产生和处理异常的信息。2.跳转语句(BranchingStatement)C和C++中使用的跳转语句有goto、break、cont
inue等。前面介绍的switch语句中的break语句导致流程跳到紧跟在当前语句之后的语句,break语句的另一种用法是使控制流程跳到一个标号语句。可以在语句前放置一个合法的Java标志符,后跟一个冒号来标号这个语句,例如:breakToHere:someJavastatement要跳转
到标号为breakToHere的语句,可用break的下列形式:breakbreakToHere标号的break语句是Java所不支持的goto语句的一个替代品。在循环语句中使用continue语句可以从当前语句跳回到循环的顶端,它可以跳过在循环中的continue语句以后的
语句。在Java跳转语句中,可以使用return语句从当前方法中退出并退回到执行原方法调用的语句。return有两种形式:一个有返回值;另一个不返回值。要返回值,只要简单地把值(或计算值的表达式)放在return关键词后,例如:returncount++;return返回
的值的类型必须符合方法申明中返回值的类型,这个规则与C++的规则是一致的。3.3.6数组和字符串像其他编程语言一样,Java允许使用数组对象收集和管理多个值,而且可以使用字符串对象管理由多个字符组成的数据。1.数组下面讲述在Java程序中创建和使用数组所需的知识。像
其他变量一样,在使用数组时必须声明它,而且数组的声明也包括两个基本部分:数组类型和数组名。数组类型指数组中包含的元素的数据类型,例如对一个包括的都是整数的数组,其类型是整型数组。例如:intarrayofInts[];其中,方括号[]表明ar
rayofInts是一个数组,int说明arrayofInts的所有元素都是整数。上面的声明仅说明arrayofInts是一个整型数组,并没有分配任何存储区以存放数组元素。如果在分配存储区之前程序试图访问arraofInts的任何元素,编译器会显示一个错误并拒绝编译程序。例如:testing.
Java:64:VarialearrayofIntsmaynothavebeeninitialized.为给数组分配存储区,必须实例化这个数组,可以使用Java的new运算符完成这项工作。创建一个数组的步骤类似于从类创建一个对象的步骤:声明、实例化和初始化。下面的语句用于为包含1
0个整型数元素的数组arrayofInts分配足够的内存。intarrayofInts[]=newint[10];一般情况下,当创建一个数组时,可以使用new运算符,加上数组元素的数据类型并在方括号[]
内加上所希望的元素数目,即elementTypearrayName[]=newelementType[arraysize];现在数组已被分配了存储区,可以给它赋值并使用它,例如:for(intj=0;j<arrayofInts.length;j++){arrayofInts[j]=j;Sys
tem.out.println("[j]="+arrayofInts[j]);}在该程序中,为了引用一个数组元素,要在数组名后加一方括号,并在方括号内填上想存取元素的下标。注意,Java数组下标从0开始,到数组的总长度减1时结束。在上面的程序
中,for循环遍历arrayofInts的所有元素,给其赋值并打印这些值。注意:arrayofInts.length用来得到数组长度,length是一个所有Java数组都具有的属性。数组可以包含所有合法的Java数组类型,包括引用类型的对象和其他数组。例如,下面声明了一个包括字符串对象的
数组。StringarrayofStrings[]=newString[10];这个数组的类型是引用类型,也就是说,每一个元素包含一个对字符串对象的引用。此时,分配了足够存储区以包含字符串引用,但并没有对字符串本身分配内存。如果想要访问arrayofInts的一个元素,将会得到一个NullPoin
terException,因为数组是空的,没有包含任何String和String对象。这常在Java编程新手中造成一些混乱,因此不得不为String分别分配内存。例如:for(int=0;i<arrayofStrings.length;i++
){arrayofString[i]=newString("Hello"+i);}2.字符串一串字符数据被称作为字符串,在Java环境中用String类(Java.lang组件的一个成员)来实现它:Stringargs[];这行代码明确声明了一个包含String对象的名为args的数组,
空的方括号说明数组的长度在编译的时候是未知的。Java中字符串常量也是作为String类的一个对象来处理的。例如:"Inputhas","chars"编译器在遇到文字串时便隐含地分配一个String对象,因此上面两个
文字串被隐含地分配了两个String对象。字符串对象是不可改变的,也就是说,一旦它们被创建后就不能改变了。Java.lang组件提供了一个不同的类StringBuffer,使用它可以随意创建和管理字符数据。Java允许简单地使用+运算符合并字符串,例如:下
面的代码把三个字符串合并在一起输出。"Input"+count+"chars"合并的两个字符串是文字串"Inputhas"和"chars",第三个字符串(中间的那个)事实上是一个整数,它先被转化为字符串,然后再
和其他字符串合并。3.3.7Java环境的一些特征对Java环境的特征,重点介绍以下几个概念:(1)main()方法;(2)异常;(3)标准输入/输出。1.main()方法在执行Java程序时,运行系统调用程序的main()方法并执行,然后main()方法再调用其它方法
运行这个程序。下例用于从命令行上接收一个文件名作为命名行参数,并能对在命令行上指定的文件中的字符进行计数。例3.33main()方法应用举例。importJava.io.*;classcountfile{publicstaticvoidmain(stringargs
[])throwsJava.io.Ioexception,Java.io.FileNotFoundException{intcount=0;InputStreamis;Stringfilename;if(args.length==1){is=newFileInputStr
eam(args[0]);filename=args[0];}else{is=system.in;filename="Input";}while(is.read!=?1)count++;system.out.pri
ntIn(filename+"has"+count+"class.");}}2.异常在例3.33的程序中,main()方法声明了如下的两个语句:publicstaticvoidmain(Stringargs[])throwJava.io.IOExceptio
n第二个语句表明main()方法可以产生一个名为Java.io.IOException的异常,当程序出现异常时,可以捕获这个异常,并使用名为异常处理器的一段特殊代码处理它。在Java中,方法必须声明非运
行时异常,如果有错误,就产生异常。注意:mian()方法并没有直接产生任何异常,但可以通过调用System.io.read()间接产生一个Java.io.IOException.3.标准输入/输出流System类是Java.bung的一个成员,并提供输入、输出、拷贝数组
、获取当前日期和时间以及系统变量等系统功能。