第1章 面向对象分析
1.1 结构化技术与面向对象技术
1.结构化技术:以―过程‖为基础,将模型化的功能过程化。 2.人类认知世界的基本方式是:(1)分类认知(2) 整体认知 (3) 推理衍生
面向对象技术的特点:(1)软件复用(2)可维护性“每个组件在做什么(功能问题)
/与系统其他组件有什么关系\"(3)在现有对象基础上构件应用程序(4)纯语言
1.3 面向对象技术基础分析
1. 对象(Object)
对象状态:一个对象在生命周期过程中的一个状态。 对象状态可以用静态属性值来表示。
2.类(Class)
类是面向对象语言必需提供的用户定义的数据类型,它将具有相同状态、操作和访问机制的多个对象抽象成为一个对象类。类给出了属于该类的全部对象的抽象定义,而对象则是符合这种定义的一个实体。所以,一个对象又称作类的一个实例(instance)。
3.数据抽象(Data Abstraction)
数据抽象是从大量的数据中提取出有意义的信息,以便进一步处理。
4.封装性(Encapsulation)
封装是面向对象方法的一个重要原则。它有两个涵义:
把对象的全部属性和全部服务结合在一起,形成一个不可分割的独立单位(对象)。 也称作―信息隐蔽‖(Information Hiding),即尽可能隐蔽对象的内部细节,对外形成一个边界(或者说形成一道屏障),只保留有限的对外接口使之与外部发生联系。这主要是指对象的外部不能直接地存取对象地属性,只能通过几个允许外部使用地服务与对象发生联系。
数据抽象和封装的优点
(1)保护内部数据的安全性
(2)稳定的接口有利于变化系统的扩展升级
5.继承性(Inheritance)
继承是面向对象语言的另一特性。类与类之间可以组成继承层次,一个类的定义(子
1
类,派生类)可以定义在另一个已定义类(父类,基类)的基础上。子类可以继承父类中的属性和操作,也可以定义自己的属性和操作。 继承性的特点
由下而上(抽象继承):提取多个类(子类)的共同特征,泛化和抽象为一个更具普遍意义的类(父类)。
由上而下(派生继承):由一个类(基类)的特征表示更加细化和特化,生成更特殊的类(派生类)。
根类:没有基类(父类)的特殊类。
6.多态性(Polymorphism)
对象的多态性是类的同名方法在不同的情况下具体实现不同。通常有以下两层含义: 对象提供形式不同,但功能相似的服务
指在一般类中定义的属性或操作被特殊类继承之后,可以具有不同的数据类型或表现出不同的行为。这使得同一个属性或操作名在一般类及其各个特殊类中具有不同的语义。
第2章 面向对象设计
2.体系结构框架
1. 系统体系结构的类型
(1)批处理型:对整个输入集进行数据转换。
(2)连续型:随着输入的实时变化,实时进行数据转换。 (3)交互型:由外部的交互控制应用。
(4)事务型:以内关于以存储和更新数据为中心,支持并发访问。 (5)基于规则型:由一定的强制性规则支配应用。 (6)模拟型:应用模拟显示世界对象。 (7)实时型:以严格的定时约束控制应用
第3章 C++基础知识
C++源文件的扩展名为.CPP。
3.2.2 编译
将编辑好的C++源程序通过编译器转换为目标文件(OBJ文件)。即生成该源文件的目标代码。
2
3.2.3 链接
将用户程序生成的多个目标代码文件(.obj)和系统提供的库文件(.lib)中的某些代码连接在一起,生成一个可执行文件(.exe)。
3.2.4 执行
把生成的可执行文件运行,在屏幕上显示运行结果。用户可以根据运行结果来判断程序是否出错。
3.3 C++的词法与规则
3.3.1 C++的字符集
数字:0,1,2,3,4,5,6,7,8,9。 小写字母:a,b,…,y,z。 大写字母:A,B,…,Y,Z。
运算符:+,-,*,/, %,< ,<= ,= ,>= ,> ,!= ,= = ,<< ,>> ,& ,| ,&& ,‖, ∧ ,~ ,( ),[ ],{ },-> ,• , ! , ? , ?: , , , ; , ” , # 。 特殊字符:(连字符或下划线)。
不可印出字符:空白格(包括空格、换行和制表符)。
1. 标识符
标识符是对实体定义的一种定义符,由字母或下划线(或连字符)开头、后面跟字母或数字或下划线(或空串)组成的字符序列,一般有效长度是8个字符(而ANSI C标准规定31个字符),用来标识用户定义的常量名、变量名、函数名、文件名、数组名、和数据类型名和程序等。
2.关键字:关键字是具有特定含义,作为专用定义符的单词,不允许另作它用。
auto break case char class
const continue default do ddefault delete double else enum explicit extern float for friend goto if inline int long mutable new operator private protected
public register return short signed sizeof static static_cast struct switch this typedef union unsigned Virtual void while
3
C++语言的分隔符主要是:空格、制表和换行符。
例:一个简单的C++程序。
#include cout<<”This is my first C++ program.\\n”; //输出This is my first C++ program. /*输出 This is my first C++ program.*/ } 3.4 C++程序的构成 C++语言程序由以下基本部分组成。 1.函数 2.预处理命令 预处理命令以位于行首的符号―#‖开始,C++提供的预处理有宏定义命令、文件包含命令和条件编译命令三种。 3.程序语句 如下是程序控制语句类型: if () else 条件语句 for () 循环语句 while () 循环语句 do while () 循环语句 continue 结束本次循环语句 break 中止循环式switch语句 switch 多分支选择语句 4 goto return 转移语句 从函数返回语句 (5)函数调用语句 函数调用语句是由一次函数调用加一个分号而构成的一个语句。 例如: max(x,y); (6)空语句 空语句: ; 即只有分号―;‖的语句,什么也不做。 3.5 堆与动态创建运算符 3.5.1 堆对象 1. C++程序的内存布局(通常分为四个区) (1)全局数据区(data area):存放全局变量、静态数据、常量。 (2)代码区(code area):存放类成员函数、其他函数代码。 (3)栈区(stack area):存放局部变量、函数参数、返回数据、返回地址。 (4)堆区 (heap area):自由存储区。 2. 堆对象(变量) 堆对象(变量)是在程序运行时根据需要随时可以被创建或删除的对象(变量) ,当创建堆对象(变量)时,堆中的一个存储单元从未分配状态变为已分配状态,当删除堆对象(变量)时,存储单元从分配状态又变为未分配状态,可供其他动态数据使用。 3.5.2 new 和 delete 与对基本数据类型操作一样,new 和 delete算符可以用于创建和删除堆对象。 new T (初始值 );//创建一个T类型对象,返回值为对象首地址 new T [ E ];//创建一个T类型对象数组,返回值为数组首地址 delete 指针变量;//删除一个指针变量所指的对象 delete [ ] 指针变量;删除一个指针变量所指的对象数组 注意:其中 T 是类型,E 是算术表达式。 例1:new与delete 运算符 例2:堆对象数组 例3:函数中的new与delete 运算符 5 3.5.3 new 和 delete注意事项 new运算返回指定类型的指针,如果分配内存失败(如没有足够的内存空间),则返回0; 在程序中对应于每次使用运算符new,都应该相应地使用运算符delete来释放申请的内存。并且对应于每个运算符new,只能调用一次delete来释放内存,否则有可能导致系统崩溃。 delete运算必须删除用new动态分配的有效指针;否则回带来严重问题,如系统崩溃。 new和delete最好成对使用,以避免出现异常。 delete运算释放数组堆对象,必须注明delete了数组有多少个元素。(见例2:堆对象数组) delete只是删除动态内存单元,并不会删除指针本身。 对空指针调用delete是安全的。 C++语言有两个库函数:malloc()与free()。这两个函数也是实现动态内存分配作用的,其功能分别与运算符new和delete相似。但是最好不要将库函数和运算符混合使用,否则可能导致系统崩溃。 3.6 引用、数组与指针 3.6.1. 引用标识符& C++中用& 可派生出一个引用类型,即产生同一变量的另一个名字。引用在C++中非常普遍,主要用途是为用户定义类型指定操作,还可用于函数参数的传递,对引用型参数的操作,就是对实际参数的操作。例如: main() { int num=50; int& ref=num; ref+=10; printf(\"num=%d\} 3.6.2 引用的特点 课本(P183) (1)可以说明任何类型的引用,引用可以使用任何合法变量名。 (2)引用不是变量,说明时必须初始化,并且以后不可以更改为其他变量的引用。 (3)引用不是值,不占存储空间,说明引用时,目标的存储状态不会改变。所以,引用只有说明,没有定义。 (4)引用仅仅在说明时带―&‖符号,此后的引用的用法与变量完全相同。 (5)引用最好做函数形参,其作用效果与指针做函数形参相似,都可以带回 (6)数运算后的结果值;但在函数体内部,引用的用法与变量完全相同,所 (7)比指针简单易懂。(见例4: 引用类型和指针类型做函数形参) 6 3.6.3 const和volatile const 和volatile是类型修饰符。在变量说明语句中,const 用于冻结一个变量,使其在程序中不能被修改。在用const 说明变量时, 必须对该变量进行初始化。用volatile修饰的变量,虽然在一段程序中没有明显被改动,单这个变量的值也会因为程序外部的原因(如中断等)而改变。 3.6.4 数组 2. 一维数组 课本P102 3. 一维数组的初始化 书上P103笔记/书P106那段话 4. 数组的赋值 用―=‖赋值:与数组元素的初始化不同,在给数组元素进行赋值时,必须逐一赋值。 例如:对于下述的数组初始化: int a[3]={1,2,3}; 其等价的赋值形式如下: int a[3];a[0]=1;a[1]=2; a[2]=3; 注意:若要在数组之间进行赋值,也只能一个一个元素地赋值。 例如:将上述数组a的值赋给另一个同样大小的数组b,可以利用下面的循环完成赋值操作:for (i=0;i<3;i++) b[i]=a[i]; 用流命令赋值 其语法格式为:cin>>数组名; 或 cin>>数组名[下标]; 例如:对一个大小为5的字符型数组a赋值,可以用下列两种方式:char a[5];cin>>a; 用scanf()函数 其语法格式为:scanf(―类型标识‖,数组名); 或 scanf(―类型标识‖,数组元素地址); 用C++库函数中的strcpy( )函数(字符串拷贝函数) 其常见语法格式为: strcpy(数组名,字符串);//将一个字符串赋值到一个字符数组中 例如: char str1[10]; strcpy(str1,‖hello‖); 注意,此例不能写为:str1=‖hello‖;//不合法 另一种常见的语法格式为: strcpy(数组名1,数组名2);//将数组2中的字符串赋值到数组1中 例如: strcyp(str1,str2); 7 注意,上例不能写为:str1=str2;//不合法 5. 数组越界 在给数组元素赋值或对数组元素进行引用时,一定要注意下标的值不要超过数组的范围,否则会产生数组越界问题。因为当数组下标越界时,编译器并不认为它是一个错误,但这往往会带来非常严重的后果。 例如:定义了一个整型数组a:int a[10]; 数组a的合法下标为0 9。如果程序要求给a[10]赋值,将可能导致程序出错,甚至系统崩溃。 常用下面的式子确定数组的大小,预防数组越界情况的发生 。 假定对于一个整型数组a,它的大小为: sizeof(a)/sizeof(int) sizeof(a)表示求数组a在内存中所占字节数,sizeof(int)表示求整型数据在内存中所占字节数。使用上面这个式子,可以使数组大小计算在16位机器和32位机器之间移植。 8.数组与函数 数组也可以作为函数参数,将数组中数据传送到另一个函数中。传递可以采用两种方法: (1)数组元素作为函数的参数 (2)数组名作为函数的参数 当用数组名作为函数的实参和形参时,传递的是数组的地址。这时实参数组和形参数组应该分别在它们所在的函数中定义。此时采取的不是―值传送‖方式,而是―地址传送‖方式,即把实参数组的起始地址传送给形参数组,这样形参数组就和实参数组共占同一段内存单元,当形参值发生变化时,实参值也发生变化。 此时应该注意: (1)实参数组与形参数组类型要一致。 (2)形参数组的长度不要超过实参数组的长度。实参数组必须定义为具有确定长度的数组,而形参数组可以不定义长度,只在数组名后跟一个空的方括号,同时在被调用的函数中另设一个参数用来传递元素的个数。 (3)可以在被调用函数中采用降维处理,即用单重循环来遍历二维数组中的所有元素。此时调用函数中的数组不要用数组名表示,而要用第一个元素的地址表示。 3.6.5 指针 课本P170 1.指针是一类特殊的变量(对象),存储的是程序中另一个对象在内存中的地址。 8 2.定义格式: <数据类型> *<指针变量(对象)名>; 3.指针初始化 指针对象可以被一个相同类型的对象初始化; 例如:int i;int *p=&i; 同一类型的指针之间可以相互赋值; 例如:int i,*p1,*p2;p1=&i;p2=p1; 通过直接分配内存地址初始化; 例如:int *p1;p1=new int; 直接赋值空指针;例如:int *p1=NULL,*p2=0; 通用指针赋值; 例如:void *p1; float* p2; p1=p2; p2=(float* )p1; 4.C++中有两个有关指针的特别运算符: & 运算符:为取地址运算符,&x的值为x的地址。 * 运算符:指针运算符,或指向运算符,也称间接运算符,*p代表p所指向的变量。 在指针变量的定义和指针变量的引用中都有*p。但引用指针时的*p与定义指针变量时用的*p是有区别的,它们形式上有些相似,而含义是不同的。 注意区分下面三种表示方法所具有的不同意义。例如,有一个指针px , px -- 指针变量,它的内容是地址量。 *px -- 指针的目标变量,它的内容是数据。 &px -- 指针变量占用的存储区域的地址。 5.指针运算 指针运算的实质是地址的计算,它只能进行算术运算、关系运算和赋值运算。 指针的算术运算 指针与整数的加减运算:(px+n,px-n) 指针加1、减1运算:(px++,++px,px--,--px) 指针的相减运算:(px-py) 指针的关系运算 在两个指向相同类型变量的指针之间可以进行各种关系运算。两指针之间的关系运算表示它们指向的地址位置之间的关系。 (p1==p2)结果为1表示相同类型指针变量p1,p2指向同一地址空间;结果为0反之; (p1>p2)结果为1表示相同类型指针变量p1的地址在p2的地址的前面;结果为0反之; (P==0)和(P!=0)判断指针是否为空; 9 注意:指向不同数据类型的指针之间的关系运算是没有意义的,指针与非0整数之间的关系运算也是没有意义的。 函数指针(课本P178) 函数指针就是指向函数的指针。 定义函数指针的语法格式为: <数据类型> (*函数指针名)(<参数表>); 其中,数据类型是指函数指针所指向函数的返回值的类型,参数表中指明该函数指针所指向函数的形参类型和个数。 例如:int (*p)(int,int); 就定义了一个函数指针p,它指向一个返回整型值,有两个整型参数的函数。 在定义了指向函数的指针变量后,在使用此函数指针之前,必须先给它赋值,使它指向一个函数的入口地址。由于函数名是函数在内存中的首地址,因此可以赋给函数指针变量。赋值的一般语法格式为: 函数指针名=函数名; 例如,对上面刚定义的函数指针p,可以给它赋值如下: p=func1; 其中,函数名所代表的函数必须使一个已经定义过的,和函数指针具有相同返回类型的函数。并且等号后面只需写函数名而不要写参数, 当函数指针指向某函数以后,可以用下列形式调用函数: (*指针变量)(实参表列) 例如:(*p)(a,b),它相当于funl(a,b)。 注意:指针的运算在这里是无意义的。因为指针指向函数的首地址。当用指针调用函数时,程序是从指针所指向的位置开始按程序执行,若进行指针运算,程序的执行就不是从函数的开始位置执行,这就会造成错误。 与定义一般变量指针数组一样,C++语言中也可以定义具有特定返回类型和特定参数类型的函数指针数组。函数指针数组也可以是多维的,不过在实际编程中多只用到一维函数指针数组。定义它的语法格式如下: <数据类型> (*函数指针名[常量表达式])(参数表); 例如:int (*p[5])(int,int); 就定义了一个含有5个元素的函数指针数组,其中的每个元素都是一个指向函数的指针,且指向的函数都是返回值类型为整型,带两个整型参数的函数。 3.6.6 常引用与常指针 10 1.用const修饰说明引用为常引用。常引用所引用的对象不能被更新。常引用的说明形式如下: const <类型说明符> & <引用名>; 例如:int a=10; const int &n=a; 其中,n是一个常引用,它所引用的对象不会被更新。如果出现: n=123; 则是非法的。 常引用作函数形参,在函数中不能更新它所引用的对象,因此对应的实参不会被破坏。 2.用const修饰定义指针为常指针。常指针的定义有两种: <类型> *const <指针变量名>;//表示常量指针,const修饰的是指针变量,该指针变量本身不能更新其中存储的地址值,但指针所指向的地址中所存储的值可以更新。 const<类型> *<指针变量名>;//表示指针常量,const修饰的是<类型>*,该指针变量可以更新其中存储的地址值,但指针所指向的地址中所存储的值不能更新。 (见例6:常量指针与指针常量) 3.7 函数基础扩展 一个函数定义中的<参数表>可以被省略,表明该函数为无参函数,若<参数表>用void取代,则也表明是无参函数,若<参数表>不为空,同时又不是保留字void,则称为带参函数。 * 函数定义说明 注意: 在一个函数体内允许有一个或多个return语句,一旦执行到其中某一个return语句时,return后面的语句就不再执行,直接返回调用位置继续向下执行。 C++中不允许函数定义嵌套,即在函数定义中再定义一个函数是非法的。一个函数只能定义在别的函数的外部,函数定义之间都是平行的,互相独立的。 常见的函数调用方式有下列两种: 方式一:将函数调用作为一条表达式语句使用,只要求函数完成一定的操作,而不使用其返回值。若函数调用带有返回值,则这个值将会自动丢失。例如:max(3,5); 方式二:对于具有返回值的函数来说,把函数调用语句看作语句一部分,使用函数的返回值参与相应的运算或执行相应的操作。例如: int a=max(3,5); int a=max(3,5)+1; cout< #include int t; t=a; if(b>t) t=b; if(c>t) t=c; return t; } 11 3.函数调用时的参数传递 (1)按值传递(2)地址传递(3)引用传递:引用传递方式是在函数定义时在形参前面加上引用运算符―&‖。 3.7.4 特殊函数 1.内联函数(课本P137) 内联扩展(inline expansion)简称为内联(inline),内联函数也称为内嵌函数。当在一个函数的定义或声明前加上关键字inline则就把该函数定义为内联函数,它主要是解决程序的运行效率。 内联函数可以在一开始仅定义或声明一次,但必须在函数被调用之前定义或声明。内联函数中不能含有任何循环以及switch和goto语句;内联函数中不能说明数组;递归函数(自己调用自己的函数)不能定义为内联函数。 2.重载函数 函数重载又称为函数的多态性,是指同一个函数名对应着多个不同的函数。所谓―不同‖是指这些函数的形参表必须互不相同,或者是形参的个数不同,或者是形参的类型不同,或者是两者都不相同,否则将无法实现函数重载。例如,下面是合法的重载函数: int func1(int,int); int func1(int); double func1(int,long); double func1(long); 重载函数的类型,即函数的返回类型可以相同,也可以不同。但如果仅仅是返回类型不同而函数名相同、形参表也相同,则是不合法的,编译器会报―语法错误‖。 除形参列表外都相同的情况 ,编译器不认为是重载函数 ,只认为是对同一个函数原型的多次声明 。 在调用一个重载函数func1()时,编译器必须判断函数名func1到底是指哪个函数。它是通过编译器,根据实参的个数和类型对所有func1()函数的形参一一进行比较,从而调用一个最匹配的函数。 注意: 函数名必须相同/函数返回值可以不同/参数列表必须不同(参数类型或参数个数不同) 3.带默认形参值的函数 当一个函数既有定义又有声明时,形参的默认值必须在声明中指定,而不能放在定义中指定。只有当函数没有声明时,才可以在函数定义中指定形参的默认值。 默认值的定义必须遵守从右到左的顺序,如果某个形参没有默认值,则它左边的参数就不能有默认值。 例如:void func1(int a, double b=3.75, int c=3); //合法 void func1(int a=1, double b, int c=3); //不合法 在进行函数调用时,实参与形参按从左到右的顺序进行匹配,当实参的数目少于形参时,如果对应位置形参又没有设定默认值,就会产生编译错误;如果设定了默认值,编译器将为那些没有对应实参的形参取默认值。 12 注意:形参的默认值可以是全局常量、全局变量、表达式、函数调用,但不能为局部变量。 4.函数的嵌套调用 由前述可知,C++函数不能嵌套定义,即一个函数不能在另一个函数体中进行定义。但在使用时,允许嵌套调用,即在调用一个函数的过程中又调用另一个函数。 例如: func1(int a, float b) { float c; c=func2(b-1,b+1); } int func2(float x, float y) { 函数体 } func1和func2是分别独立定义的函数,互不从属。 5.递归函数(课本P140) 一个函数直接或间接地调用自身,这种现象就是函数的递归调用。 递归调用有两种方式: 直接递归调用:直接递归调用即在一个函数中调用自身; 间接递归调用:即在一个函数中调用了其他函数,而在该其他函数中又调用了本函数。 利用函数的递归调用,可将一个复杂问题分解为一个相对简单且可直接求解的子问题(―递推‖阶段);然后将这个子问题的结果逐层进行回代求值,最终求得原来复杂问题的解(―回归‖阶段)。 #include \"iostream.h\" long f(int n) {if(n<0) {cout<<“error!“< else return (n*f(n-1)); } 6. 带参数的main( )函数 C++编写的源程序经过编译、链接生成可执行文件,在执行该文件时,如果需要带命令行参数,则该源文件的主函数main()需要带参数。 格式:void main(int argc, char *argv[]){ } 其中: 参数argc表示存放命令行参数的个数; 参数argv存放命令行各个参数和命令字的字符串; 13 (见例10:带参数的main( )函数) 3.8 变量的作用域问题 1.变量的作用域指程序中变量有效的区域。 2.变量的作用域分为三种: 文件域(全局域)/局部域/类域:类的定义范围内。 注意:不同作用域中的标识符可以重名,且局部域中定义的变量回隐藏同名的全局变量。 3.程序的生命周期:从程序的运行开始到结束。 (1)永久变量(全局变量): (2)临时变量(局部变量):变量的存储空间在程序执行到达其作用域时产生,随作用域结束而消亡。 (3)静态变量:综合以上两种变量的特点。将局域变量说明为静态变量(static),系统为静态局域变量分配固定的存储空间,只能在第一次执行时初始化。这样静态局域变量既在局部域作用有效,又可以永久存在。 注意: (1)全局变量可以显式初始化,也可以由系统隐式初始化为0。 (2)全局变量也可以是静态的(static),但静态全局变量只在定义它的文件中使用,在其他文件中不可使用。 (3)静态全局变量的优点是实现信息隐藏,并可以在不同文件中使用意义不同而同名的变量。 (4)静态函数的作用与静态全局变量相同,只能在定义它的文件中使用。 (5)内联函数和定义为const 的常量和函数都是隐含的静态类型。 4.域运算符:: 域运算符::提供对全局变量的访问,使被局部变量隐藏的全局变量显现出来。 例如:int var;//全局变量 void fun() { int var;//局部变量 var=::var;//将全局变量的值赋给局部变量 } 5.外部变量和外部函数(extern) (1)静态全局变量只能在定义它的文件中使用。 (2)非静态全局变量在其他文件说明为外部变量(extern),才能使用。 (3)外部非静态全局函数同上。 14 变量定义,而不是变量说明。 6.自动变量(auto):限制在某一范围内使用,采用堆栈分配内存,相当于局部变量。 当程序执行超出自动变量的作用域时,就释放其内存,其值也消失。(?) 7.寄存器变量(register):目的是加快速度,若寄存器满,系统则自动按auto变量处理。 4种存储类型的变量特征 8.预处理 宏定义命令:#define <宏名> <定义内容> 文件包含命令:#include <系统目录文件名> #include ―当前目录文件名‖ 条件编译命令:同一源程序在不同的编译条件下得到不同的目标代码。 (1)第一种形式: #ifdef <标识符> //判断<标识符>是否被编译过了 <程序段1> //是,则编译<程序段1> [#else <程序段2>] // 否,则编译<程序段2> #endif (2)第二种形式: (与上相反)#ifndef <标识符> //判断<标识符>是否被编译过了<程序段1> //否,则编译<程序段1> [#else <程序段2>] // 是,则编译<程序段2> #endif (3)第三种形式: (嵌套条件编译命令) #if <表达式1> //判断<表达式1 >是否为真 <程序段1> //是,则编译<程序段1> [#elif <表达式2> <程序段2>] // 否,判断<表达式2 >是否为真 :[#else //以上条件都为假 <程序段n>] #endif 3.9 名字空间 C++支持名字空间的特征。名字空间允许几个开发组使用相同的类名、变量名和函数名,可以避免源代码层次上的命名冲突。 名字空间提供了一个全局标识符和全局变量所在的作用域。 名字空间定义格式: namespace <名字空间名> { //该空间中的全局标识符和全局变量; } 名字空间的使用方法: 15 using namespace <名字空间>; //表示以下代码使用的全局标识符和变量是定义在该<名字空间>中。 <名字空间>::<标识符和变量> //限定使用<名字空间>中定义的全局标识符和变量。 注意: 对标准名字空间的使用: using namespace std; 第4章 类和对象 基本概念 4.1.1 类的定义 2.类定义的相关说明: (见例1:类与对象的定义) class是定义类的关键字。 <类名>是一个标识符,用于惟一标识一个类。 一对大括号{ }内是类的定义体,说明该类的所有成员。 类的成员包括: * 数据成员:静态属性 * 成员函数:动态属性 * 类成员的访问控制权限: 公有的(public):类的公有成员可以被类内成员函数和类外对象访问,是类与外部的接口;保护的(protected):类的保护成员可以被类内成员函数和派生类内成员函数访问; 私有的(private):类的私有成员可以被类内成员函数,是默认访问控制权限; 4.1.2 类的成员函数 在类定义体外定义类的函数成员的格式如下: 返回类型 类名::成员函数名(参数说明) { 函数体; }//类的成员函数对类的数据成员进行操作; 成员函数的定义体可以在类的定义体中格式如下: class Class_Name { 返回类型 成员函数名(参数说明) { 函数体; }//类的成员函数对类的数据成员进行操作; 16 }; 注意: 在类定义体外定义成员函数时,需在函数名前加上类域标记(::),因为类的成员变量和成员函数属于所在的类域,在域内使用时,可直接使用成员名字,而在域外使用时,需在成员名外加上类对象的名称。 成员函数的函数体在类的定义体内部---内联函数 成员函数的函数体在类的定义体外部---非内联函数 在类的定义体中不能对所定义的数据成员进行初始化。 类的数据成员可以是任意一种初等数据类型或复合数据类型。 类的数据成员可以是另一个类的对象(子对象)或自身类的指针或引用,但不能是自身类对象。如果子对象的定义在后,则需要提前说明。 4.3 构造函数和析构函数(课本P209) 构造函数(constructor)和析构函数(destructor)都是类的成员函数,但它们是特殊的成员函数,不用显式调用,但由系统自动执行,而且这些函数的名字与类的名字有关。 4.3.1 构造函数 课本P210 4.3.2 析构函数 课本P211 当一个对象消失,或用new创建的对象用delete删除时,由系统自动调用类的析构函数。析构函数名字为符号― ‖加类名,析构函数没有参数和返回值。一个类中只可能定义一个析构函数,所以析构函数不能重载。 析构函数是用于取消对象的成员函数,当一个对象作用域结束时,系统自动调用析构函数。 析构函数的作用:进行撤消对象,释放内存等。 当对象超出其定义范围时(即释放该对象时),编译器自动调用析构函数。在以下情况下,析构函数也会被自动调用; 4.3.5 重载构造函数 构造函数可以像普通函数一样被重载,C++根据说明中的参数个数和类型选择合适的构造函数。若类 X 具有一个或多个构造函数,创建类 X 的对象时,C++会根据参数选择调用其中一个。 构造函数可以使用默认参数,但谨防二义性。 使用构造函数的限制:不能被继承,不能说明为虚函数,不能显式调用,不能取构造函数的地址。 (见例5:重载构造函数与析构函数) 17 4.3.6 拷贝构造函数 课本P243 1. 拷贝构造函数的表示 当构造函数的参数为自身类的引用时,这个构造函数称为拷贝构造函数。拷贝构造函数的功能是用一个已有对象初始化一个正在建立的同类对象。例如: class A { public : A( int ) ; A ( const A & , int = 1 ) ; „ }; „ A a ( 1 ) ; A b ( a , 0 ) ; A c = b ; 2. 拷贝构造函数的调用 (1)用已有对象初始化创建对象。 (2)当对象作函数参数时,因为要用实参初始化形参,也要调用拷贝构造函数。 (3)函数返回值为类类型时,情况也类似。(按引用返回则不调用) 4.4 类的初始化 1.C++允许以下3 种数据初始化方法: (1)初始值表:适用于结构和数组的初始化。 例如: struct conf { char *month; int day; int year; } cpp[ ]={“Nov.” , 12 , 1994 , “Oct.” , 4, 1999, “April” , 6 , 2000 } (2)赋值表达式:适用于简单变量或指针类型的初始化。 例如: int i = 1 ; char *p = “No. 1” ; 18 (3)表达式表:与方法(2)语义相同,风格不一样。 例如: int i (1) ; char *p ( “No. 1” ) ; 构造函数的初始化主要采用表达式表的方法。 2. C++中,类的初始化分为两种情况: (1)对仅有公有段成员,而没有构造函数或基类的类对象,用初始值表来表示。典型例子是结构。 (2)带有构造函数的类的初始化。 例如: class X { public: X ( ) ; X ( int i ) ; private: int i ; }; 3.初始化时,构造函数可用两种方式把值赋给成员: (1)接受该值作为参量,并在构造函数体内赋给其成员。 例如: class X { int a, b ; // 默认为 private 成员 public: X( int i , int j ) { a = i ; b = j ; } } (2)使用函数体前的初值表(初始化列表)。 例如:代替函数体内对一般成员赋值示例。 class X { int a, b ; public: X( int i ,int j ) : a ( i ),b ( j ){ }; } 19 类的静态成员函数 课本P224 4.5 复杂的对象表示 1.对象数组 (见例7:类的实例对象数组) 定义格式:〈类名〉 〈数组名〉[〈size〉] 为了创建对象数组,构造函数的形式必须是: (1)使用默认构造函数; (2)若有用户自定义构造函数,则必须含有一个不带参数或具有一个带默认参数的构造函数。 2.对象指针: 〈类名〉 *〈对象指针名〉 对象指针作函数形参与指针变量形参的效果相同,实现传地址调用,无须将实参copy给形参,可以降低时空开销,提高效率。 注意:当指针 +1 或 –1 时,它的增加或减少方式会使指针指向其类类型的下一对象或上一对象。 3. 对象引用作函数参数 C++中,对象引用可以作为函数的参数,作用与变量引用做函数参数相同。 * 类成员指针:C++中,可以定义指向类中成员的指针。 (1)类数据成员指针: 定义格式:<数据类型> <类名>::*<指针名> 赋值格式:<指针名> = <类名>::<数据成员名> 调用格式: <对象名>.(*<指针名>)(<实参表>); (2)类成员函数指针: 定义格式:<数据类型> (<类名>::*<指针名>)(<参数列表>) 赋值格式:<指针名> = <类名>::<成员函数名> 调用格式: <对象名>.(*<指针名>)(<实参表>); (见例8:类成员指针 ) 注意: 类成员指针所指向的类中成员必须是public属性; 类成员指针所指向的类中成员不能是静态成员; 4.6 this指针(自指向指针) 课本(P209) 20 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个函数成员所在的对象的指针。成员函数中可以用this关键字来引用该指针。this指针的类型就是成员函数所属的类的类型。当对象调用成员函数时,编译器先将该对象的地址赋给this指针,然后调用成员函数。 注意: this指针只能在类的成员函数中使用,它指向该成员函数被调用的对象。成员函数访问数据成员,隐含使用this->数据成员。 *this一般用于成员函数返回当前对象自身。 。 this指针大量用于运算符重载成员函数设计中。 静态成员函数没有this指针。因为类只有一个静态成员函数实例,所以使用this指针没有什么意义。在静态成员函数中使用this指针会引起编译错误。 4.7 对象的常类型 常对象是指对象常量,定义格式如下: <类名> const <对象名>; 或者 const <类名> <对象名>; 注意: 在定义常对象时必须进行初始化,而且不能被更新其数据成员。 常对象只能调用常成员函数,不能调用非常成员函数。 常对象成员:包括常成员函数和常数据成员。 1. 常成员函数 使用const关键词说明的函数为常成员函数,常成员函数说明格式如下: <类型> <函数名>(<参数表>) const; 注意: (1)const是函数类型的一个组成部分,因此在实现部分也要带const关键词。 (2)常成员函数不更新对象的数据成员,也不能调用该类中的非常成员函数。 (3)常对象只能调用常成员函数,而不能调用其他成员函数。 (4)const关键词可以参与区分重载函数。例如,如果在类中有说明: void print(); void print() const; 则这是对print的有效重载。 4.9 子对象和堆对象 子对象(对象成员):类中有数据成员是对象。 例如: class A; class B {„„ A obj; 21 „„. } 堆对象:通过new动态创建的对象。 第5章 友元 课本P229 友元提供了在不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。通过友元,一个普通函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。友元的正确使用能提高程序的运行效率,但破坏了类的封装性和数据的隐蔽性。 5.1 友元函数 1.定义友元函数的方式是在类定义中用关键词friend说明该函数,其格式如下: friend <类型> <友元函数名> (<参数表>); 2.注意: 类的友元函数可以直接访问该类的所有成员; 类的友元函数不是类的成员函数,它可以是一个普通函数,也可以是其他类的成员函数,在其函数体中通过对象名访问这个类的私有或保护成员; 友元函数说明可以放在类定义体内,不受访问控制权限限制,但其函数体一般放在类定义体外部; (见例1:友元函数求对象的成员变量的平方) (见例6:类的成员函数是另一个类的友元函数) 5.2 友元类 1. C++允许说明一个类为另一个类的友元类(friend class)。 如果A是B的友元类,则A中的所有成员函数可以像友员函数一样访问B类中的所有成员。定义格式如下: class B { friend class A;//A的所有成员函数均为B的友元函数 //… } 2. 注意: 友元关系不可以被继承。假设类A是类B的友元,而类C从类B派生,如果没有在类C中显式地使用下面的语句: friend class A; 那么,尽管类A是类B的友元,但这种关系不会被继承到类C,也就是说,类C和类A没有友元关系,类A的成员函数不可以直接访问类C的受保护成员和私有成员。 22 不存在―友元的友元‖这种关系。假设类A是类B的友元,而类B是类C的友元,即是说类B的成员函数可以访问类C的受保护成员和私有成员,而类A的成员函数可以访问类B的受保护成员和私有成员;但是,类A的成员函数不可以直接访问类C的受保护成员和私有成员,即友元关系不存在传递性。 (见例2:友元类) (见例3:节点类和栈类的友元关系) 5.3 友元应用实例 例4:求两数的平方差。 #include private: int a,b,max,min; public: Myclass(int i, int j):a(i),b(j) { max=(a>b)?a:b; min=(afriend int Result(Myclass& x); }; 第6章 模板 模板(template)是C++支持参数化多态性的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意数据类型。 6.1 模板的概念 模板(template)是一种使用数据类型形式参数来产生一系列函数集合或类集合的机制。集合中不同的函数或类可以操作不同的数据类型,从而避免需要为每一种数据类型编写一个单独的类或函数,实现代码重用。 模板的实质:将数据类型参数化,以便在具体情况下选择特定数据类型。 模板的优点:可以构造相类似的函数或类的序列。 C++程序由类和函数组成,模板也分为函数模板(function template)和类模板(class template) 。 23 例如:设计一个求两参数最大值的函数,不使用模板时,需要定义四个函数; int max(int a,int b){return(a>b)?a,b;} long max(long a,long b){return(a>b)?a,b;} double max(double a,double b){return(a>b)?a,b;} char max(char a,char b){return(a>b)?a,b;} 若使用模板,则只定义一个函数: Template type max(type a,type b){return(a>b)?a,b;} 调用时: max(10,20); 6.2 函数模板 课本P300 函数模板可以定义一个对任何数据类型的变量进行操作的函数,增强了函数设计的通用性。使用函数模板的方法是先说明函数模板,然后实例化成相应的模板函数进行调用执行。 1. 函数模板说明 函数模板的一般说明形式如下: template < 模板形参表> <返回值类型> <函数名>(模板函数形参表) { //函数定义体; } 2.格式说明: <模板形参表>可以是基本数据类型形参、类类型形参和变量形参(非数据类型)。类型形参需要加前缀class(跟随类型形参)。如果类型形参多于一个,则每个类型形参都要使用class。 <模板函数形参表>中的参数必须唯一,而且在<函数定义体>中至少出现一次。 函数模板定义是一组重载函数的逻辑描述,编译系统不为其产生任何执行代码。 3.使用函数模板 函数模板只是说明,不能直接执行,需要实例化为模板函数后才能执行。 当编译系统发现有一个函数调用: <函数名>(<实参表>); 时,将根据<实参表>中的类型实例化为一个重载函数(模板函数)。 注意: 24 模板的实参是隐式地传递给模板形参,该模板函数的定义体与函数模板的函数定义体相同。 函数模板仍然符合先定义,后调用(同时实例化为模板函数)的原则。 注意: 对模板函数的说明和定义必须是全局作用域。模板不能被说明为类的成员函数。 虽然模板参数T可以实例化成各种类型,但定义函数模板中由模板参数T定义的各变量之间必须保持完全一致的类型。模板类型并不具有隐式的类型转换。 可以在函数模板形参表中和模板函数的调用中使用类和用户自定义类型。 4.重载模板函数 模板函数与普通函数一样,也可以重载。 编译器在处理时,首先匹配重载函数,然后再寻求模板的匹配。 (编程验证,并总结特点) 6.3 类模板 类模板与函数模板类似,它可以为各种不同的数据类型定义一种模板化的类,在引用时使用不同的数据类型实例化该类模板,从而形成一个类的集合。 类模板实际上是函数模板的推广。可以用相同的类模板来组建任何类型的对象集合。在传统C++中,可能有一个浮点数类或者一个整数类,如果使用类模板,可以定义一个对两者都适用的类number。 1.类模板定义 类模板定义的一般形式是: template <类型形参表> class <类名> { //类说明体 }; template <类型形参表> <返回类型> <类名> <类型名表>::<成员函数1>(形参表) { //成员函数定义体 } template <类型形参表> <返回类型> <类名> <类型名表>::<成员函数2>(形参表) { //成员函数定义体 } … template <类型形参表> <返回类型> <类名> <类型名表>::<成员函数n>(形参表) { //成员函数定义体 } 25 注意事项: 类模板的<类型形参表>与函数模板中的意义一样。 在类外定义的成员函数定义中,<类型名表>是类型形参的使用。 这样的一个说明(包括成员函数定义)不是一个实实在在的类,只是对类的描述,称为类模板(class template)。类模板必须用类型参数将其实例化为模板类后,才能用来生成对象。一般地,其表示形式为: 类模板名 <类型实参> 对象名(值实参表); 其中<类型实参表>将显式传递给类模板的形参,完成将类模板实例化为模板类。 类模板参数列表决不能是空的,如果其中有一个以上的参数,则这些参数必须要用逗号分开。 <类型形参表>也可以是表达式参数。表达式参数经常是数值。对模板类进行实例化时,给这些参数所提供的变量必须是常量表达式。 如: template // }; 类模板someclass的第二个参数是表达式形参,而第一和第三个参数是类型形参。 类模板的成员函数的体外,每个前面都必须用与声明该类模板一样的表示形式加以声明,其他部分同一般的成员函数定义。 2.使用类模板 与函数模板一样,类模板不能直接使用,必须先实例化为相应的模板类,定义该模板类的对象后才能使用。 建立类模板后,可用下列方式创建类模板的实例: <类名> <类型实参表> <对象表>; 其中,<类型实参表>应与该类模板中的<类型形参表>匹配。<类型实参表>是模板类(template class),<对象>是定义该模板类的一个对象。 使用类模板可以说明和定义任何类型的类。这种类被称为参数化的类。如果说类是对象的抽象,那么类模板可以说是类的抽象。 注意:类模板与模板类的区别 。 6.4 模板应用实例 课本P303 第7章 运算符重载 26 课本(P142) 运算符重载的规则如下: (1)C++中的运算符除了少数几个以外,几乎全部可以重载,而且只能重载已有的这些运算符。 (2)重载之后运算符的优先级和结合性都不会改变。 (3)运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。一般来讲,重载的功能应当与原有功能类似。 (4)运算符重载不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型。 7.2 运算符重载的实现 运算符的重载形式有两种:重载为类的成员函数和重载为类的友元函数。 1.运算符重载为类的成员函数的语法形式如下: <函数类型> operator <运算符>(<形参表>) { <函数体>; } friend <函数类型> operator <运算符>(<形参表>) { <函数体>; } 其中: <函数类型>指定了重载运算符的返回值类型; operator是定义运算符重载函数的关键词; <运算符>给定了要重载的运算符名称,是C++中可重载的运算符; <形参表>中给出重载运算符所需要的参数和类型; 对于运算符重载为友元函数的情况,还要在函数类型说明之前使用friend关键词来说明。 注意事项: 当运算符重载为类的成员函数时,函数的参数个数比原来的运算数个数要少一个(后缀++、--除外);当重载为类的友元函数时,参数个数与原运算数的个数相同。 单目运算符最好重载为成员函数; 双目运算符则最好重载为友元函数; 运算符重载的含义必须清楚; 运算符重载不能有二义性; 7.3 单目运算符重载 类的单目运算符可重载为一个没有参数的非静态成员函数或者带有一个参数的非成员函数,参数必须是用户字定义类型的对象或者是对该对象的引用。 在C++中,单目运算符有++和--,它们是变量自动增1和自动减1的运算符。在类中可以对这两个单目运算符进行重载。 27 如同―++‖运算符有前缀、后缀两种使用形式,―++‖和―--‖重载运算符也有前缀和后缀两种运算符重载形式,以―++‖重载运算符为例,其语法格式如下: <函数类型> operator ++(); //前缀运算 <函数类型> operator ++(int);//后缀运算 使用前缀运算符的语法格式如下: ++<对象>; 使用运算符前缀时,对对象(操作数)进行增量修改,然后再返回该对象。所以前缀运算符操作时,参数与返回的是同一个对象。这与基本数据类型的运算符前缀类似,返回的也是左值。 使用后缀运算符的语法格式如下: <对象>++; 使用运算符后缀时,必须再增量之前返回原有的对象值。为此,需要创建一个临时对象,存放原有的对象,以便对操作数(对象)进行增量修改时,保存最初的值。运算符后缀操作时返回的时原有对象值,不是原有对象,原有对象已经被增量修改,所以,返回的应该是存放原有对象值的临时对象。 (见例1:++运算符重载) 7.4 双目运算符重载 将双目运算符B重载为类的成员函数:使之能够实现表达式―oprd1 B oprd2‖,其中oprd1为A类的对象,则应当把B重载为A类的成员函数,该函数只有一个形参,形参的类型是oprd2所属的类型。经过重载之后,表达式oprd1 B oprd2就相当于函数调用―oprd1.operator B(oprd2)‖。 将双目运算符B重载为类的友元函数:这样,它就可以自由地访问该类的任何数据成员。这时,运算符所需要的运算数都需要通过函数的形参表来传递,在参数表中形参从左到右的顺序就是运算符运算数的顺序。 (见例2:双目运算符重载) 7.5 ->运算符重载 ―->‖运算符是成员访问运算符,这种一元的运算符只能被重载为成员函数,所以也决定了它不能定义任何参数。 1.成员访问运算符―->‖函数重载的一般形式为: class_name* class_name::operator->(); 2.成员访问运算符的调用形式是: 对象->成员;//与对象指针调用成员比较 注意: ―->‖成员重载运算符不能是静态成员函数; 通过对象->成员;实现访问的成员应该是public型; (//例10:―->‖运算符重载为成员函数) 7.6 赋值运算符重载 28 在C++中有两种类型的赋值运算符:一类是―+=‖和―-=‖等先计算后赋值的运算符,另一类是―=‖即直接赋值的运算符。下面分别进行讨论。 1.运算符―+=‖和―-=‖的重载 对于标准数据类型,―+=‖和―-=‖的作用是将一个数据与另一个数据进行加法或减法运算后再将结果回送给赋值号左边的变量中。对它们重载后,使其实现其他相关的功能。 (见例4:复合赋值运算符重载) 2.运算符―=‖的重载 赋值运算符―=‖的原有含义是将赋值号右边表达式的结果拷贝给赋值号左边的变量,通过运算符―=‖的重载将赋值号右边对象的私有数据依次拷贝到赋值号左边对象的私有数据中。在正常情况下,系统会为每一个类自动生成一个默认的完成上述功能的赋值运算符,当然,这种赋值只限于由一个类类型说明的对象之间赋值。 如果一个类包含指针成员,采用这种默认的按成员赋值,那么当这些成员撤消后,内存的使用将变得不可靠。 (见例5:赋值运算符―=‖的重载) 7.7 下标运算符重载 下标运算符―[ ]‖通常用于在数组中标识数组元素的位置,通过下标运算符重载可以实现数组数据的赋值和取值。 下标运算符实质是求地址运算。 下标运算符重载函数只能作为类的成员函数,不能作为类的友元函数。 下标运算符―[ ]‖函数重载的一般形式为: type class_name::operator[ ](int arg); 其中arg为该重载函数的参数。重载了的下标运算符只能且必须带一个参数,该参数给出下标的值。重载函数operator[ ]的返回值类型type是引用类型。 (见例6:下标运算符重载 ) 7.8 运算符new与delete重载 C++提供了new与delete两个运算符用于内存管理,但有些情况下用户需要自己管理内存,为自己所定义的类体系建立一种新的动态内存管理算法,以克服new与delete的不足。这就要重载运算符new与delete,使其按照要求完成对内存的管理。 注意: new和delete只能被重载为类的成员函数,不能重载为友元。 重载了的new和delete均默认为类的静态成员函数。 运算符new重载的一般形式为: void *class_name::operator new(size_t , void *class_name::operator delete(void *, 29 可以带有两个参数,若有第二个参数,则其第二个参数的类型必须为size_t。 (见例7:运算符new与delete重载) 7.9 逗号运算符重载 逗号运算符是双目运算符,和其他运算符一样,也可以通过重载逗号运算符。逗号运算符构成的表达式为―左运算数,右运算数‖,该表达式返回右运算数的值。如果用类的成员函数来重载逗号运算符,则只带一个右运算数,而左运算数由指针this提供。 (见例8:逗号运算符重载) 7.10 类型转换运算符重载 类型转换运算符重载函数的格式如下: operator <类型名>() { <函数体>; } 类型转换运算符重载函数没有返回类型,因为<类型名>就代表了它的返回类型,而且也没有任何参数。在调用过程中要带一个对象实参。 类型转换运算符的实质将对象转换成类型名规定的类型。转换时的形式就像强制转换一样。如果没有转换运算符定义,直接用强制转换是不行的,因为强制转换只能对标准数据类型进行操作,对类类型的操作是没有定义的。 类型转换运算符重载的缺点是无法定义其类对象运算符操作的真正含义,因为只能进行相应对象成员数据和一般数据变量的转换操作。 (见例9:类型转换运算符重载) 第8章 继承和派生 8.1 基类和派生类 继承是类之间定义的一种重要关系。定义类B时,自动得到类A的操作和数据属性,使得程序员只需定义类A中所没有的新成分就可完成在类B的定义,这样称类B继承了类A,类A派生了类B,A是基类(父类),B是派生类(子类)。这种机制称为继承(Inheritance) 。 派生类可以具有基类的特性,共享基类的成员函数,使用基类的数据成员,还可以定义自己的新特性,定义自己的数据成员和成员函数。 在C++语言中,一个派生类可以从一个基类派生,也可以从多个基类派生。从一个基类派生的继承称为单继承;从多个基类派生的继承称为多继承。图8-1反映了类之间继承和派生关系。 1.派生类的定义格式 单继承的定义格式如下: class <派生类名> :<继承方式> <基类名> { public: //派生类新定义成员 members; 30 members; 其中,<派生类名>是新定义的一个类的名字,它是从<基类名>中派生的,并且按指定的<继承方式>派生的。 <继承方式>有三种关键字给予定义: public:表示公有继承; private:表示私有继承,可默认声明; protected:表示保护继承。 多继承的定义格式如下: class <派生类名> : <继承方式1> <基类名1>,<继承方式2> <基类名2>,… { public: //派生类新定义成员 members; 2.派生类的三种继承方式 (1) 公有继承(public) 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态;而基类的私有成员仍然是私有(不可访问)的。 (2) 私有继承(private) 私有继承的特点是基类的公有成员和保护成员作为派生类的私有成员,并且不能被这个派生类的子类访问;而基类的私有成员仍然是私有(不可访问)的。 (3) 保护继承(protected) 保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问;而基类的私有成员仍然是私有(不可访问)的。 (见例0:继承关系的访问控制权限) 表8-1 不同继承方式的基类和派生类特性 (1)在公有继承时,派生类的对象可以访问基类中的公有成员;派生类的成员函数可以访问基类中的公有成员和保护成员。这里,一定要区分清楚派生类的对象和派生类中的成员函数对基类的访问是不同的。 (2)在私有继承时,基类的非私有成员只能由直接派生类访问,而无法再往下继承。 (3)对于保护继承方式,这种继承方式与私有继承方式的情况相同。两者的区别仅在 31 于对派生类的成员而言,对基类成员有不同的可访问性。 (4)对于基类中的私有成员,只能被基类中的成员函数和友元函数所访问,不能被其他的函数访问。 3. 访问控制 类通过派生定义,形成类的等级,派生类中用―类名 :: 成员‖访问基类成员。在建立一个类等级后,通常创建某个派生类的对象来使用这个类等级,包括隐含使用基类的数据和函数。 派生类对基类成员可以有不同的访问方式: 课本P253图标 注意事项 1.定义与派生类同名的成员 如果派生类定义了与基类同名的成员,称派生类的成员覆盖了基类的同名成员,若要在派生类中使用基类同名成员,可以显式地使用类名限定符: 基类名 :: 成员 2.派生类不能访问基类私有成员 3. 私有继承和保护继承 私有继承:派生类对基类的公有继承使用关键字private 描述(可缺省),基类的所有公有段和保护段成员都成为派生类的私有成员。 保护继承:派生类对基类的公有继承使用关键字 protected 描述,基类的所有公有段和保护段成员都成为派生类的保护成员,保护继承在程序中很少应用。 4. 派生类中的静态成员 不管公有派生类还是私有派生类,都不影响派生类对基类的静态成员的访问,派生类对基类静态成员必须显式使用以下形式: 基类名 :: 成员 总结:基类与派生类的关系 任何一个类都可以派生出一个新类,派生类也可以再派生出新类,因此,基类和派生类是相对而言的。一个基类可以是另一个基类的派生类,这样便形成了复杂的继承结构,出现了类的层次。一个基类派生出一个派生类,它又做另一个派生类的基类,则原来的基类为该派生类的间接基类。 1. 派生类是基类的具体化。 2. 派生类是基类定义的延续。 3. 派生类是基类的组合。 派生类将其本身与基类区别开来的方法是添加数据成员和成员函数。因此,继承的机制将使得在创建新类时,只需说明新类与已有类的区别,从而大量原有的程序代码都可以复用。 派生类的生成过程: (1)吸收基类成员 派生类首先―全盘‖继承基类的所有成员(构造和析构除外)。 (2)改造基类成员 32 对继承来的基类所有成员的访问权限的改造。 对基类成员的覆盖。 (3)添加新成员 定义派生类的构造函数和析构函数。 定义派生类的新数据成员和成员函数,以新增功能。 8.2 单继承 单继承是指派生类有且只有一个基类的情况,在单继承中,每个类可以有多个派生类,但是每个派生类只能有一个基类,从而形成树形结构。 8.2.1 构造函数 构造函数不能够被继承,C++提供一种机制,使得在创建派生类对象时,能够调用基类的构造函数来初始化基类数据 也就是说,派生类的构造函数必须通过调用基类的构造函数来初始化基类子对象。所以,在定义派生类的构造函数时除了对自己的数据成员进行初始化外,还必须负责调用基类构造函数使基类的数据成员得以初始化。 派生类构造函数的一般格式如下: <派生类名>(<派生类构造函数总参数表>) :<基类构造函数>(<参数表1>), <子对象名>(<参数表2>) { <派生类中数据成员初始化> }; 派生类构造函数的调用顺序如下: (1)调用基类的构造函数,调用顺序按照它们继承时说明的顺序。 (2)调用子对象类的构造函数,调用顺序按照它们在类中说明的顺序。 (3)调用派生类构造函数。 8.2.2 析构函数(课本P255) 由于析构函数也不能被继承,因此在执行派生类的析构函数时,基类的析构函数也将被调用。执行顺序是先执行派生类的析构函数,再执行基类的析构函数,其顺序与执行构造函数时的顺序正好相反。 (见例1:在继承关系中,子对象、基类与派生类的构造和析构) 8.2.4 应注意的问题 33 在实际应用中,使用派生类构造函数时应注意如下几个问题: (1)派生类构造函数的定义中一般省略对基类构造函数的调用,由系统自动完成调用基类构造函数的工作。(注意:基类构造函数的定义形式) (2)当基类的构造函数使用一个或多个参数时,则派生类必须定义构造函数,提供将参数传递给基类构造函数途径。在有的情况下,派生类构造函数体可能为空,仅起到参数传递作用。 (见例4:在继承关系中,基类与派生类的构造函数的参数传递) 8.3 多继承 8.3.1 多继承的概念 可以为一个派生类指定多个基类,这样的继承结构称为多继承。多继承可以看作是单继承的扩展。所谓多继承是指派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个继承。 多继承下派生类的定义格式如下: class<派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,… { <派生类类体> }; 其中,<继承方式1>、<继承方式2>、…是三种继承方式:public,private和protected之一。 8.3.2 多继承的构造函数 在多继承的情况下,多个基类构造函数的调用次序是按基类在被继承时所声明的次序从左到右依次调用,与它们在派生类的构造函数实现中的初始化列表出现的次序无关。 派生类的构造函数格式如下: <派生类名>(<总参数表>):<基类名1>(<参数表1>),<基类名2>(<参数表2>),…<子对象名>(<参数表n+1>),… { <派生类构造函数体> } 其中,<总参数表>中各个参数包含了其后的各个分参数表。 派生类构造函数执行顺序: (1)先执行所有基类的构造函数; (2)再执行派生类本身构造函数 (3)处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指定的各基类顺序,与派生类构造函数中所定义的成员初始化列表的各项顺序无关。 多继承下派生类的构造函数与单继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。同时,派生类的参数个数必须包含完成所有基类初始化所需的参 34 数个数。 (见例1:在继承关系中,子对象、基类与派生类的构造和析构) 8.3.3 二义性和支配原则 一般说来,在派生类中对基类成员的访问应该是惟一的。但是,由于多继承情况下,可能造成对基类中某个成员的访问出现了不惟一的情况,则称为对基类成员访问的二义性问题。 1.同名成员的二义性 在多重继承中,如果不同基类中有同名的函数,则在派生类中就有同名的成员,这种成员会造成二义性。 如:class A { public: void f(); }; class B { public: void f(); void g(); }; class C: public A,public B { public: void g(); void h(); }; C obj; 二义性解决方法: 1.使用基类名可避免这种二义: obj.A::f();//A中的f(); obj.B::f();//B中的f(); C类的成员访问f()时也必须避免这种二义。 以上这种用基类名来控制成员访问的规则称为支配原则。 例如: obj.g();//隐含用C的g() obj.B::g();//用B的g() 35 以上两个语句是无二义的。 2.如果同一个成员名在两个具有继承关系的类中进行了定义,那么,在派生类中所定义的成员名具有支配地位。在出现二义性时,如果存在具有支配地位的成员名,那么编译器将使用这一成员,而不是给出错误信息。 以上面的代码为例,在类A和类B中都定义了具有相同参数的成员函数a,这样,尽管类D中可以以两种方式来解释成员函数名a——即来自类B的成员函数a和来自类C的成员函数a,但是,按照刚才所说的规则,类B的成员名a相比类A的成员名a (即是类C中的成员名a)处于支配地位,这样,编译器将调用类B的成员函数a,而不产生二义性的错误。 3.同一基类被多次继承(直接或间接)而产生的二义性 由于二义的原因,一个类不能从同一类直接继承二次或更多次。如: class derived:public base,public base {…} 是错的。 如果必须这样,可以使用中间类。 8.3.4 赋值兼容规则 赋值兼容规则是指:在公有派生的情况下,一个派生类的对象可用于基类对象适用的地方。赋值兼容规则有三种情况(假定类derived由类base派生) : 1.派生类的对象可以赋值给基类的对象。(反之不然) 如: derived d; base b; b=d; 2.派生类的对象可以初始化基类的引用。(反之不然) 如: derived d; base& br=b; 3.派生类的对象的地址可以赋给指向基类的指针。 (反之不然) 如: derived d; base *pb=&d; 8.4 虚基类 当某类的部分或全部直接基类是从另一个共同基类派生而来时,这些直接基类中从上一级基类继承来的成员就拥有相同的名称,也就是说,这些同名成员在内存中存在多个副本。而多数情况下,由于它们的上一级基类是完全一样的,在编程时,只需使用多个副本的任一个。 C++语言允许程序中只建立公共基类的一个副本,将直接基类的共同基类设置为虚基类,这时从不同路径继承过来的该类成员在内存中只拥有一个副本,这样有关公共基类成员访问的二义性问题就不存在了。 (见例2:二义性问题与虚基类) 36 8.4.1 虚基类的引入 引进虚基类的真正目的是为了解决二义性问题。当基类被继承时,在基类的访问控制保留字的前面加上保留字virtual来定义。 3.实现机制: 如果基类被声明为虚基类,则重复继承的基类在派生类对象实例中只好存储一个副本,否则,将出现多个基类成员的副本。 4.虚基类说明格式: class <派生类名>:virtual<继承方式><基类名> { } 说明: 其中,virtual是虚基类的关键字。虚基类的说明是用在定义派生类时,写在派生类名的后面。 引进虚基类后,派生类(即子类)的对象中只存在一个虚基类的子对象。当一个类有虚基类时,编译系统将为该类的对象定义一个指针成员,让它指向虚基类的子对象。该指针被称为虚基类指针。 注意事项: 1.要解决有共同基类而产生二义性,必须采用虚基类。 2.类之间的继承关系可以用一个有向无环图(DAG)表示,称为类格。一个类在类格中,既可以被用作虚基类,也可以被用作非虚基类。在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而每个非虚基类产生各自的子对象。虚基类的构造函数按被继承的顺序构造,建立虚基类的子对象时,虚基类构造函数仅被调用一次。 8.4.2 虚基类的构造函数 为了初始化基类的子对象,派生类的构造函数要调用基类的构造函数。对于虚基类来讲,由于派生类的对象中只有一个虚基类子对象。为保证虚基类子对象只被初始化一次,这个虚基类构造函数必须只被调用一次。由于继承结构的层次可能很深,规定将在建立对象时所指定的类称为最直接派生类。虚基类子对象是由最直接派生类的构造函数通过调用虚基类的构造函数进行初始化的。如果一个派生类有一个直接或间接的虚基类,那么派生类的构造函数的成员初始列表中必须列出对虚基类构造函数的调用,如果未被列出,则表示使用该虚基类的默认构造函数来初始化派生类对象中的虚基类子对象。 C++规定,在一个成员初始化列表中出现对虚基类和非虚基类构造函数的调用,则虚基类的构造函数先于非虚基类的构造函数执行。 (见例3:在继承关系中,虚基类、基类与派生类的构造和析构) 从虚基类直接或间接继承的派生类中的构造函数的成员初始化列表中都要列出这个虚基类构造函数的调用。但是,只有用于建立对象的那个派生类的构造函数调用虚基类的构造函数,而该派生类的基类中所列出的对这个虚基类的构造函数调用在执行中被忽略,这样便保证了对虚基 37 类的子对象只初始化一次。 8.5 应用实例 例5:在继承关系中,虚基类、基类与派生类的构造和析构 例6:在继承关系中,虚基类与派生类的构造函数的参数传递 第9章 多态性与虚函数 (课本P272) 多态性(Polymorphism)是面向对象程序设计的重要特征之一。所谓多态性是指当不同的对象收到相同的消息时,产生不同的动作。C++的多态性具体体现在运行和编译两个方面,在程序运行时的多态性通过继承和虚函数来体现,而在程序编译时多态性体现在函数和运算符的重载上。 (1)联编(或绑定,binding):把函数调用与适当的函数代码相对应的操作。 (2)静态联编:在编译或链接阶段完成联编。 (3)动态联编:在运行阶段完成联编。 (4)静态多态性:定义在一个类或全局域中的同名函数,根据参数表(类型和数目)区别语义。 (5)动态多态性:定义在一个类层次的不同类中的同名函数,具有相同的参数表,根据指针指向属于某类的对象来区别语义。 (见例1:静态联编,动态联编与虚函数) 9.1 普通成员函数重载 在C++语言中,只有在声明函数原型时形式参数的个数或者对应位置的类型不同,两个或更多的函数就可以共用一个名字。这种在同一作用域中允许多个函数使用同一函数名的措施被称为重载(overloading)。函数重载是C++程序获得多态性的途径之一。 9.1.1 函数重载的方法 函数重载要求编译器能够唯一地确定调用一个函数时应执行哪个函数代码,既采用哪个函数实现。确定函数实现时,要求从函数参数的个数和类型上来区分。这就是说,进行函数重载时,要求同名函数在参数个数上不同,或者参数类型不同。否则,将无法实现函数重载。 main() 例:给出以下程序的运行结果。 { #include } 38 周长。 #include return 2*PI*r; } double length(float x,float y) { return 2*(x+y); } 9.1.2 函数重载的表示形式 普通成员函数重载可表达为两种形式: 1. 在一个类说明中重载 例如: Show ( int , char ) ; Show ( char * , float ) ; 2. 基类的成员函数在派生类重载。有3种编译区分方法 (1)根据参数的特征加以区分 例如: Show ( int , char ) 与 Show ( char * , float ) 不是同一函数,编译能够区分 (2)使用― :: ‖加以区分 例如: A :: Show ( ) 有别于 B :: Show ( ) (3)根据类对象加以区分 例如:Aobj.Show ( )调用 A Show ( ) Bobj.Show ( )调用 B Show ( ) :: :: 9.1.3 函数重载的注意事项 在C++语言中,编译程序选择相应的重载函数版本时函数返回值类型是不起作用的。不能仅靠函数的返回值来区别重载函数,必须从形式参数上区别开来。例如: void print(int a); void print(int a,int b); int print(float a[]); 这三个函数是重载函数,因为C++编译程序可以从形式参数上将它们区别开来。 但: int f(int a); double f(int a); 这两个函数就不是重载函数,编译程序认为这是对一个函数的重复说明,因为两个函数的形式参数个数与相应位置的类型完全相同。 由typedef定义的类型别名并没有真正创建一个新的类型,所以以下程序段: typedef double money; double calculate(double income); money calculate(money income); 也是错误的函数重载。 39 同样道理,不同参数传递方式(如引用形参和变量形参)也无法区别重载函数,如: void func(int value); void func(int &value); 也不能作为重载函数。 在程序中不可滥用函数重载,不适当的重载会降低程序的可读性。C++语言并没有提供任何约束限制重载函数之间必须有关联,程序员可能用相同的名字定义两个互不相关的函数。实际上函数重载暗示了一种关联,不应该重载那些本质上有区别的函数,只有当函数实现的语义非常相近时才应使用函数重载。 9.1.4 函数重载的二义性 函数重载的二义性(ambiguity)是指C++语言的编译程序无法在多个重载函数中选择正确的函数进行调用。函数重载的二义性主要源于C++语言的隐式类型转换与默认参数。 在函数调用时,编译程序将按以下规则选择重载函数: 如果函数调用的实际参数类型与一个重载函数形式参数类型完全匹配,则选择调用该重载函数; 如果找不到与实际参数类型完全匹配的函数原型,但如果将一个类型转换为更高级类型后能找到完全匹配的函数原型,编译程序将选择调用该重载函数。所谓更高级类型是指能处理的值域较大,如int转换为unsigned int,unsigned int转换为long,long转换为unsigned float等。 例如:int func(double d); … count< 派生类指针:指向基类和派生类的指针是相关的。 例如: A * p ; A A_obj ; B B_obj ; p = & A_obj ; p = & B_obj ; // 指向类型 A 的对象的指针 // 类型 A 的对象 // 类型 B 的对象 // p 指向类型 A 的对象 // p 指向类型 B 的对象, 40 //它是 A 的派生类 利用 p,可以通过 B_obj 访问所有从 A_obj 继承的元素 ,但不能用 p访问 B_obj 自身特定的元素 (除非用了显式类型转换)。 注意:可以用一个指向基类的指针指向其公有派生类的对象。但却不能用指向派生类的指针指向一个基类对象。希望用基类指针访问其公有派生类的特定成员,必须将基类指针用显式类型转换为派生类指针。例如((B_class *)p)-> show_phone();一个指向基类的指针可用来指向从基类公有派生的任何对象,这一事实非常重要,它是C++实现运行时多态的关键途径。 9.3 虚函数 课本(p278) 9.3.1 虚函数的概念 虚函数是在基类中冠以关键字 virtual 的成员函数。它提供了一种接口界面。虚函数可以在一个或多个派生类中被重定义。 动态联编:在C++语言中,是通过将一个函数定义成虚函数来实现运行时的多态。如果一个函数被定义为虚函数,那么,使用指向基类的对象指针或对象引用来调用该成员函数,C++也能保证所调用的是正确的特定于实际对象的成员函数。 如果类c1,c2…由基类base派生而来,base有一个用virtual修饰的公有或保护函数成员f(),而在c1,c2…中的一些类中重新定义了成员函数f(),而对f()的调用都是通过基类的对象引用或对象指针进行的,在程序执行时才决定是调用c1还是c2或其他派生类中定义的f(),这样的函数f()称为虚函数。 一旦一个函数在基类中第一次声明时使用了virtual了关键字,那么,当派生类重载该成员函数时,无论时否使用了virtual 关键字,该成员函数都将被看作一个虚函数,也就是说,虚函数的重载函数仍是虚函数。(传递性) (见例2:虚函数) 9.3.2 使用虚函数时应注意: (1)在类体系中访问一个虚函数时,应使用指向基类类型的指针或对基类类型的引用,以满足运行时多态性的要求。 (动态联编)当然也可以像调用普通成员函数那样利用对象名来调用一个函数(静态联编)。(例2:虚函数) (2)在派生类中重新定义虚函数时,必须保证该函数的值和参数与基类中的说明完全一致,否则就属于重载(参数不同)或是一个错误(返回值不同)。(例5:虚函数与重载函数) (3)若在派生类中没有重新定义虚函数,则该类的对象将使用其基类中的虚函数代码。 (4)虚函数必须是类的一个成员函数,不能是友元函数,但虚函数可以是另一个类的友元。 (5)静态成员函数不能是虚函数。 (6)内联函数不能是虚函数。 (7)构造函数不能是虚函数。 (8)析构函数可以是虚函数,而且通常是虚函数。(例4:虚析构函数) 41 9.3.3 虚函数与重载函数的比较 (1)重载函数要求函数有相同的返回值类型和函数名称,并有不同的参数序列;而虚函数则要求这三项(函数名、返回值类型和参数序列)完全相同; (2)重载函数可以是成员函数或友元函数,而虚函数只能是成员函数; (3)重载函数的调用是以所传递参数序列的差别作为调用不同函数的依据;虚函数是根据对象的不同去调用不同类的虚函数; (4)虚函数在运行时表现出多态功能,这是C++的精髓;而重载函数则在编译时表现出多态性。 9.4 纯虚函数与抽象类 9.4.1 纯虚函数 在许多情况下,在基类中不能给出有意义的虚函数定义,这时可以把它说明成纯虚函数,把它的定义留给派生类来做。定义纯虚函数的一般形式为: class 类名{ virtual 返回值类型 函数名(参数表) = 0; }; 纯虚函数是一个在基类中说明的虚函数,它在基类中没有定义,要求任何派生类都定义自己的版本。纯虚函数为各派生类提供一个公共界面。由于纯虚函数所在的类中没有它的定义,在该类的构造函数和析构函数中不允许调用纯虚函数,否则会导致程序运行错误。但其他成员函数可以调用纯虚函数。 (见例6:纯虚函数与抽象类) 下面的代码在类Creature中将虚函数 KindOf 声明为纯虚函数: class Creature { public: virtual char *KindOf()=0; }; char *Creature::KindOf() { return \"Creature\"; } 使用下面的格式也是可以的: class Creature { public: virtual char *KindOf()=0 { return \"Creature\"; } 42 }; 9.4.2 抽象类 如果一个类中至少有一个纯虚函数,那么这个类被成为抽象类(abstract class)。抽象类中不仅包括纯虚函数,也可包括虚函数。抽象类中的纯虚函数可能是在抽象类中定义的,也可能是在抽象类的派生类中重定义的。 抽象类有一个重要特点,即抽象类必须用作派生其他类的基类,而不能用于直接创建对象实例。抽象类不能直接创建对象的原因是其中有一个或多个函数没有定义,但仍可使用指向抽象类的指针支持运行时多态性。 一个抽象类不可以用来创建对象,这只能用来为派生类提供了一个接口规范,派生类中必须重载基类中的纯虚函数,否则它仍将被看作一个抽象类。如果要直接调用抽象类中定义的纯虚函数,必须使用完全限定名,如上面的示例,要想直接调用抽象类Creature中定义的纯虚函数,应该使用下面的格式: cout< 注意: 抽象类只能用作其他类的基类,抽象类不能创建对象。 抽象类不能用作函数参数类型、函数返回值类型或显式转换的类型。可以声明抽象类的指针和引用。 如果在抽象类的构造函数中调用了纯虚函数,那么,其结果是不确定的。 由于抽象类的析构函数可以被声明为纯虚函数,这时,应该至少提供该析构函数的一个实现。一个很好的实现方式是的抽象类中提供一个默认的析构函数,该析构函数保证至少有析构函数的一个实现存在。如下面的例子所示: class classname { // 其他成员 public: virtual classname()=0 { // 在此添加析构函数的代码 } }; 由于派生类的析构函数不可能和抽象类的析构函数同名,因此,提供一个默认的析构 43 函数的实现是完全必要的。这也是纯虚析构函数和其他纯虚成员函数的一个最大的不同之处。一般情况下,抽象类的析构函数是由派生类的实例对象释放时由派生类的析构函数隐式调用。 抽象类的主要作用是取若干类的共同行为,形成更清晰的概念层次。使用抽象类符合程序设计中的单选原则(single choice principle)。 从基类继承来的纯虚函数,在派生类中仍是虚函数。 抽象类的实例 例:编写一个程序计算正方体、球体和圆柱体的表面积和体积。(例8:纯虚函数与抽象类) 设计思路: 设计一个抽象类为公共基类container 成员函数surface_area( )//求表面积 成员函数volume( )//求体积 数据成员radius 三个派生类cube,sphere,cylinder 第10章 C++流和文件流 课本p325 C语言中没有提供专门的输入输出语句,同样,C++语言中也没有专门的输入/输出(I/O)语句,C++中的I/O操作是通过一组标准I/O函数和I/O流来实现的。C++的标准I/O函数是从C语言继承而来的,同时对C语言的标准I/O函数进行了扩充。C++的I/O流不仅拥有标准I/O函数的功能,而且比标准I/O函数功能更强、更方便、更可靠。 10.1 C++流的概念 1.在C++语言中,数据的输入和输出(简写为I/O)包括三方面: 标准I/O:针对标准输入设备(键盘)和标准输出设备(显示器)。 文件I/O:针对外存磁盘上的文件。 串I/O:针对内存中指定的字符串存储空间。 2.C++中把数据之间的传输操作称作流。 输出流:表示数据从内存传送到某个载体或设备。 输入流:表示数据从某个载体或设备传送到内存缓冲区变量。 3.I/O操作流程: 首先打开操作,使流和文件发生联系; 建立联系后的文件才允许数据流入或流出; 输入或输出结束后,使用关闭操作使文件与流断开联系。 44 4.C++为实现数据的输入和输出定义了一个庞大的类库,其继承关系如下图: 5.C++系统中的I/O类库,其所有类被包含在iostream.h,fstream.h和strstrea.h这三个系统头文件中,如下表: 6.C++还为用户进行标准I/O操作定义了四个全局类对象,它们分别是cin,cout,cerr和clog: cin为istream_withassign流类的对象,代表标准输入设备键盘,也称为cin流或标准输入流; cout,cerr和clog为ostream_withassign流类的对象; cout代表标准输出设备显示器,也称为cout流或标准输出流, cerr和clog含义相同,均代表错误信息输出设备显示器。 因此当进行键盘输入时使用cin流,当进行显示器输出时使用cout流,当进行错误信息输出时使用cerr或clog。 5.C++的流通过重载运算符―<<‖和―>>‖执行输入和输出操作。输出操作是向流中插入一个字符序列,因此,在流操作中,将运算符―<<‖称为插入运算符。输出操作是从流中提取一个字符序列,因此,将运算符―>>‖称为提取运算符。 必要时可以对―>>‖和―<<‖进行重载。 (例4:提取运算符和插入运算符重载) (1) cout:在ostream输出流类中定义有对插入操作符<<重载的一组公用成员函数,函数的具体声明格式为: ostream& operator<<(简单类型标识符); 在istream流类中声明简单类型标识符有:char, signed char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, float, double, long double, char*, signed char*, unsigned char*等,另外还增加一个void* 类型,用于输出任何指针(但不能是字符指针,因为它将被作为字符串处理,即输出所指向存储空间中保存的一个字符串)的值。 (2) cin:在istream输入流类中定义有对提取操作符>>重载的一组公用成员函数,函数的具体声明格式为: istream& operator>>(简单类型标识符&); (3) cerr:cerr类似标准错误文件。cerr与cout的差别在于: cerr是不能重定向的; cerr不能被缓冲,它的输出总是直接传达到标准输出设备上。 错误信息是写到cerr的项。即使在各种其他输出语句中,如果使用下列语句,则错误信息―Error‖总能保证在显示器上显示出来: cerr << ―Error‖ << ―\\n‖; (4)clog:clog是不能重定向的,但是可以被缓冲。在某些系统中,由于缓冲,使用clog代替cerr可以改进显示速度: clog << ―Error‖ << ―\\n‖; 45 10.2 格式化I/O 格式化I/O可以准确控制数据(特别是整数、浮点数与字符串)的I/O格式。 通常有两种方法: 使用ios类的成员函数 使用I/O操纵符 10.2.1 ios类中的枚举常量 在根基类ios中定义有三个用户需要使用的枚举类型,由于它们是在公用成员部分定义的,所以其中的每个枚举类型常量在加上ios::前缀后都可以为本类成员函数和所有外部函数访问。 1.一个无名枚举类型:其中定义的每个枚举常量都是用于设置控制输入输出格式的标志使用的。该枚举类型定义如下: enum { skipws,left,right,internal, dec,oct,hex,showbase, showpoint,uppercase,showpos,scientific, fixed,unitbuf,stdio }; 各枚举常量的含义如下: (1)skipws :设置从流中输入数据时跳过当前位置及后面的所有连续的空白字符,从第一个非空白字符起读数,否则不跳过空白字符。空格、制表符‗\‘、回车符‗\\r‘和换行符‗\\n‘统称为空白符。默认为设置。 (2)left,right, internal: left在指定的域宽内按左对齐输出, right按右对齐输出, internal使数值的符号按左对齐、数值本身按右对齐输出。域宽内剩余的字符位置用填充符填充。 默认为right设置。在任一时刻只有一种有效。 (3)dec, oct, hex : 设置dec表示数值按十进制输出, 设置oct后按八进制输出, 设置hex后则按十六进制输出。 默认为dec设置。 46 (4)showbase :设置对应标志后使数值输出的前面加上―基指示符‖ 八进制数的基指示符为数字0, 十六进制数的基指示符为0x, 十进制数没有基指示符。 默认为不设置,即在数值输出的前面不加基指示符。 (5)showpoint :强制输出的浮点数中带有小数点和小数尾部的无效数字0。默认为不设置。 (6)uppercase :使输出的十六进制数和浮点数中使用的字母为大写。默认为不设置。即输出的十六进制数和浮点数中使用的字母为小写。 (7)showpos :输出的正数前带有正号―+‖。默认为不设置。即输出的正数前不带任何符号。 (8)scientific,fixed:进行scientific设置后使浮点数按科学表示法输出,进行fixed设置后使浮点数按定点表示法输出。只能任设其一。缺省时由系统根据输出的数值选用合适的表示输出。 (9)unitbuf,stdio:这两个常量很少使用,所以不予介绍。 2.ios中定义的第二个枚举类型为: enum open_mode { in, out, ate, app,trunc, nocreate, noreplace, binany }; 其中的每个枚举常量规定一种文件打开的方式,在定义文件流对象和打开文件时使用。 3.ios中定义的第三个枚举类型为: enum seek_dir {beg, cur, end}; 其中的每个枚举常量用于对文件指针的定位操作上。 10.2.2 ios类中的成员函数 ios类提供成员函数对流的状态进行检测和进行输入输出格式控制等操作,每个成员函数的声明格式如下表: 因为所有I/O流类都是ios的派生类,所以它们的对象都可以调用ios类中的成员函数和使用ios类中的格式化常量进行输入输出格式控制。下面以标准输出流对象cout为例说明输出的格式化控制。 (见例1:输出格式) 10.2.3 格式控制操作符 数据输入输出的格式控制还有更简便的形式,就是使用系统头文件iomanip.h中提供的操纵符。使用这些操纵符不需要调用成员函数,只要把它们作为插入操作符<<(个别作为提取 47 操作符>>)的输出对象即可。这些操纵符及功能如下: dec oct hex ws endl ends flush setiosflags(long f) resetiosflags(long f) setfill(int c) setprecision(int n) setw(int w) 在上面的操纵符中,dec,oce,hex,endl,ends,flush和ws除了在iomanip.h中有定义外,在iostream.h中也有定义。所以当程序或编译单元中只需要使用这些不带参数的操纵符时,可以只包含iostream.h文件,而不需要包含iomanip.h文件。 下面以标准输出流对象cout为例,说明使用操作符进行的输出格式化控制。 (见例2:输出格式) 10.3 检测流操作的错误 1.在I/O流的操作过程中可能出现各种错误,每一个流都有一个状态标志字,以指示是否发生了错误以及出现了哪种类型的错误,这种处理技术与格式控制标志字是相同的。ios类定义了以下枚举类型: enum io_state { goodbit=0x00, //不设置任何位,一切正常 eofbit=0x01, //输入流已经结束,无字符可读入 failbit=0x02, //上次读/写操作失败,但流仍可使用 badbit=0x04, //试图作无效的读/写操作,流不再可用 hardfail=0x80 //不可恢复的严重错误 }; 2.对应于这个标志字各状态位,ios类还提供了以下成员函数来检测或设置流的状态: int rdstate(); //返回流的当前状态标志字 int eof(); //返回非0值表示到达文件尾 int fail(); //返回非0值表示操作失败 int bad(); //返回非0值表示出现错误 int good(); //返回非0值表示流操作正常 int clear(int flag=0); //将流的状态设置为flag 为提高程序的可靠性,应在程序中检测I/O流的操作是否正常。当检测到流操作出现错误时,可以通过异常处理来解决问题。 10.4 文件流 10.4.1 文件的概念 在磁盘上保存的信息是按文件的形式组织的,每个文件都对应一个文件名,并且属于 48 某个物理盘或逻辑盘的目录层次结构中一个确定的目录之下。一个文件名由文件主名和扩展名两部分组成,它们之间用圆点(即小数点)分开,扩展名可以省略,当省略时也要省略掉前面的圆点。文件主名是由用户命名的一个有效的C++标识符,为了同其他软件系统兼容,一般让文件主名为不超过8个有效字符的标识符,同时为了便于记忆和使用,最好使文件主名的含义与所存的文件内容相一致。 文件扩展名也是由用户命名的、1至3个字符组成的、有效的C++标识符,通常用它来区分文件的类型。如在C++系统中,用扩展名h表示头文件,用扩展名cpp表示程序文件,用obj表示程序文件被编译后生成的目标文件,用exe表示连接整个程序中所有目标文件后生成的可执行文件。对于用户建立的用于保存数据的文件,通常用dat表示扩展名,若它是由字符构成的文本文件则也用txt作为扩展名,若它是由字节构成的、能够进行随机存取的内部格式文件则可用ran表示扩展名。 要在程序中使用文件时,首先要在开始包含#include 10.4.2 文件的打开与关闭 1.流可以分为3类:输入流、输出流以及输入/输出流,相应地必须将流说明为ifstream、ofstream以及fstream类的对象。例如: ifstream ifile; //说明一个输入流 ofstream ofile; //说明一个输出流 fstream iofile; //说明一个输入/输出流 2.通过流对象可调用函数open()打开文件(即是在流与文件之间建立一个连接)。Open()的函数原型为: void open(const char * filename, int mode, int prot=filebuf::openprot); 其中filename是文件名字,它可包含路径说明。 prot决定文件的访问方式,取值为: 0 普通文件 1 只读文件 2 隐含文件 4 系统文件 一般情况下,该访问方式使用默认值0。 mode说明文件打开的模式,它对文件的操作影响重大,mode的取值必须是以下值之一: ios::in //打开文件进行读操作 ios::out //打开文件进行写操作 ios::ate //打开时文件指针定位到文件尾 49 ios::app //添加模式,所有增加都在文件尾部进行 ios::trunc //如果文件已存在则清空原文件 ios::nocreate //如果文件不存在则打开失败 ios::noreplace//如果文件存在则打开失败 ios::binary //二进制文件(非文本文件) 注意: 对于ifstream流,mode的默认值为ios::in; 对于ofstream流,mode的默认值为ios::out。 与其他状态标志一样,mode的符号常量可以用位或运算―|‖组合在一起,如ios::in|ios::binary表示以只读方式打开二进制文件。 注意: 打开文件操作并不能保证总是正确的,如文件不存在、磁盘损坏等原因可能造成打开文件失败。如果打开文件失败后,程序还继续执行文件的读/写操作,将会产生严重错误。在这种情况下,应使用异常处理以提高程序的可靠性。 如果使用构造函数或open()打开文件失败,流状态标志字中的failbit、badbit或hardbit将被置为1,并且在ios类中重载的运算符―!‖将返回非0值。通常可以利用这一点检测文件打开操作是否成功,如果不成功则作特殊处理。 3.每个文件流类中都提供有一个关闭文件的成员函数close(),当打开的文件操作结束后,就需要关闭它,使文件流与对应的物理文件断开联系,并能够保证最后输出到文件缓冲区中的内容,无论是否已满,都将立即写入到对应的物理文件中。文件流对应的文件被关闭后,还可以利用该文件流调用open成员函数打开其他的文件。 关闭任何一个流对象所对应的文件,就是用这个流对象调用close()成员函数即可。如要关闭fout流所对应的a:\\xxk.dat文件,则关闭语句为: fout.close(); 10.4.3 文件的读写 1. 文件读写方法 (1)使用流运算符直接读写:文件的读/写操作可以直接使用流的插入运算符―<<‖和提取运算符―>>‖,这些运算符将完成文件的字符转换工作。 (见例3:插入运算符重载) (见例4:提取运算符和插入运算符重载) (2)使用流成员函数 输出流成员函数为:put函数、write函数 输入流成员函数如下:get函数、getline函数、read函数 2. 文本文件的读写 文本文件只适用于那些解释为ASCII码的文件。处理文本文件时将自动作一些字符转 50 换: 输出时将换行字符0x0A时将转换为回车0x0D与换行0x0A两个字符存入文本文件, 输入时也会将回车与换行两个字符合并为一个换行字符, 这样内存中的字符与写入文件中的字符之间就不再是一一对应关系。文本文件的结束以ASCII码的控制字符0x1A表示。 (见例5:文件流) (见例6:文件流,将abc.txt的内容copy到xyz.txt文件中) 3.二进制文件的读写 二进制文件不同于文本文件,它可用于任何类型的文件(包括文本文件),读写二进制文件的字符不作任何转换,读写的字符与文件之间是完全一致的。 对二进制文件的读写可采用两种方法: 使用get()和put(); 使用read()和write()。 (见例7:文件读写(二进制读写)) 4.文件的随机读写 (1)输出流随机访问函数。 输出流随机访问函数有seekp和tellp。 (2)输入流随机访问函数。 输入流随机访问函数有seekg和tellg。 (见例8:文件读写(文本读写)) 10.5 字符串流 1.字符串流类包括: 输入字符串流类istrstream, 输出字符串流类ostrstream, 输入输出字符串流类strstream。 它们都被定义在系统头文件strstream.h中。只要在程序中带有该头文件,就可以使用任一种字符串流类定义字符串流对象。每个字符串流对象简称为字符串流。 2.三种字符串流类的构造函数声明格式分别如下: istrstream(const char* buffer); ostrstream(char* buffer, int n); strstream(char* buffer, int n, int mode); 对字符串流的操作方法通常与对字符文件流的操作方法相同。 课本(p405 ) 51 第11章 异常处理 11.1 异常处理概述 程序可能按编程者的意愿终止,也可能因为程序中发生了错误而终止。例如,程序执行时遇到除数为0或下标越界,这时将产生系统中断,从而导致正在执行的程序提前终止。 程序的错误有两种: 编译错误:即语法错误。如果使用了错误的语法、函数、结构和类,程序就无法被生成运行代码。 运行时发生的错误:分为不可预料的逻辑错误和可以预料的运行异常。 异常处理机制(Exception Handling)是用于管理程序运行期间错误的一种结构化方法。所谓结构化是指程序的控制不会由于产生异常而随意跳转。异常处理机制将程序中的正常处理代码与异常处理代码显式区别开来,提高了程序的可读性。 11.2 异常处理的基本思想 11.3 C++异常处理的实现 C++语言异常处理机制的基本思想是将异常的检测与处理分离。当在一个函数体中检测到异常情况存在,但无法确定相应的处理方法时,将引发一个异常,并由函数的直接或间接调用检测并处理这个异常。这一基本思想用3个保留字实现:throw、try和catch。其作用是: try:标识程序中异常语句块的开始。 throw:用来创建用户自定义类型的异常错误。 catch:标识异常错误处理模块的开始。 在一般情况下,被调用函数直接检测到异常条件的存在并使用throw抛掷一个异常(注意,C++语言的异常是由程序员控制引发的,而不是由计算机硬件或程序运行环境控制的);在上层调用函数中使用try检测函数调用是否引发异常,检测到的各种异常由catch捕获并作相应处理。 11.3.1 异常处理的语法 在C++程序中,任何需要检测异常的语句(包括函数调用)都必须在try语句块中执行,异常必须由紧跟着try语句后面的catch语句来捕获并处理。因而,try与catch总是结合使用。 try 、throw和catch语句的一般语法如下: 异常处理的执行过程如下: (1)控制通过正常的顺序执行到达try语句,然后执行try块内的保护段。 (2)如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行,程序从异常被抛掷的try块后跟随的最后一个catch子句后面的语句继续执行下去。 (3)如果在保护段执行期间或在保护段调用的任何函数中有异常被抛掷,则从通过throw运算数创建一个异常对象。编译器从能够处理抛掷类型的异常的更高执行上下文中寻找一个catch子句(或一个能处理任何类型异常的catch处理程序)。catch处理程序按其在try块后出现的顺序被检查。如果没有找到合适的处理程序,则继续检查下一个动态封闭的try块。此处理继续下去直到最外层的封闭try块被检查完。 52 11.3.2 异常处理的规则 (1)try分程序必须出现在前,catch紧跟出现在后。catch之后的圆括号中必须含有数据类型,捕获是利用数据类型匹配实现的。 (2)如果程序内有多个异常错误处理模块,则当异常错误发生时,系统自动查找与该异常错误类型相匹配的catch模块.查找次序为catch出现的次序。 (3)如果异常错误类型为C++的类,并且该类有其基类,则应该将派生类的错误处理程序放在前面,基类的错误处理程序放在后面。 (4)如果一个异常错误发生后,系统找不到一个与该错误类型相匹配的异常错误处理模块,则调用预定义的运行时刻终止函数,默认情况下是abort。 (见例1:异常处理) 11.4 标准C++库中的异常类 标准C++库中包含9个异常类,它们可以分为运行时异常和逻辑异常: length_error //运行时长度异常 domain_error //运行时域异常 out_of_range_error//运行时越界异常 invalid_argument//运行时参数异常 range_error //逻辑异常,范围异常 overflow_error //逻辑异常,溢出异常 标准C++库中的这些异常类并没有全部被显式使用 11.5 多路捕获 很多程序可能有若干不同种类的运行错误,它们可以使用异常处理机制,每种错误可与一个类,一种数据类型或一个值相关。这样,在程序中就会出现多路捕获。 注意: catch 段出现的顺序很重要。因为在一个try块中,异常处理程序按照catch出现的顺序来匹配其异常类型,后面的子句被忽略。 catch(…)是捕获所有类型的异常,所以通常放在其他catch子句之后。 * 带有异常说明的函数原型 格式:<函数返回值> 函数名(<参数表>)throw(<异常类型表>) (见例2:异常处理(函数原型带异常说明)) 注意: 如果函数的原型中没有异常说明throw部分,则该函数可引发任意类型的异常; 如果函数原型中的throw部分只有空表,则表明该函数不引发任何类型的异常; 11.6 异常处理中对象的构造与析构 程序执行过程中,在找一个匹配的catch异常后,参数传递方式如下: (1)如果catch子句的异常类型说明是一个值参数,则其初始化方式是复制被throw的异常对象; 53 (类似值传递) (2)如果catch子句的异常类型说明是一个引用,则其初始化方式是使该引用指向异常对象; (类似地址传递) 当catch 子句的异常类型说明参数被初始化后,便开始展开栈的过程。其中包括从对应的try块开始到异常被throw处之间构造(且尚未析构)的所有自动对象进行析构。 最后,程序从最后一个catch处理之后开始恢复执行。 11.7 含有异常的程序设计 11.7.1 何时避免异常 异常并不能处理所发生的所有问题。实际上若对异常过分的考虑,将会遇到许多麻烦。下面的段落指出异常不能被保证的情况。 1. 异步事件,2. 普通错误情况,3. 流控制,4. 不强迫使用异常,5. 新异常,老代码 11.7.2 异常的典型使用 1. 随时使用异常规格说明,2. 起始于标准异常,3. 套装用户自己的异常,4. 使用异常层次,5. 多重继承,6. 用―引用‖而非―值‖去捕获,7. 在构造函数中抛出异常,8. 不要在析构函数中导致异常,9. 避免无保护的指针 54 因篇幅问题不能全部显示,请点此查看更多更全内容