Lecture 1 绪论
序
程序设计范型是指设计程序的规范、模型和风格,它是一类程序设计语言的基础。
- 面向过程程序设计范型:程序=过程+调用 或 程序=算法+数据结构
- 函数式程序设计范型:程序被看作“描述输入与输出之间关系”的数学函数。如LISP
- 面向对象程序设计是一种新型的程序设计范型。这种范型的主要特征是:对象=(算法+数据结构)程序=对象+消息
面向对象程序的主要结构特点:
一、程序一般由类的定义和类的使用两部分组成,在主程序中定义各对象并规定它们之间传递消息的规律。
二、程序中的一切操作都是通过向对象发送消息来实现的,对象接收到消息后,启动有关方法完成相应的操作。
基本概念
- 对象 Object
- 类 Class
- 消息 Message
- 方法 Method
对象
在现实世界中,任何事物都是对象。可以使有形的具体存在的事物,也可以是无形的抽象的事件。
对象一般可以表示为:属性+操作
名字:用于区别不同的实体
属性/状态:属性用于描述不同实体的特征状态由这个对象的属性和这些属性的当前值决定。
操作:用于描述不同实体可具有的行为是对象提供给用户的一种服务,也叫行为或方法。
· 对象的操作可以分为两类,一类是自身所承受的操作(private/protected),一类是施加于其他对象的操作(public)。
方法(Method)——就是对象所能执行的操作,即服务。方法描述了对象执行操作的算法,响应消息的方法。在C++中称为成员函数。
属性(Attribute)——就是类中所定义的数据,它是对客观世界实体所具有性质的抽象。C++中称为数据成员。
在面向对象程序设计中,对象是描述其属性的数据及对这些数据施加的一组操作封装在一起构成的统一体。
对象可以认为是:数据+方法(操作)
类
在现实世界中,类是一组具有相同属性和行为的对象的抽象。
类和对象之间的关系式抽象和具体的关系。类是多个对象进行综合抽象的结果,一个对象是类的一个实例。
在面向对象程序设计中,类就是具有相同数据和相同操作的一组对象的集合。是对具有相同数据结构和相同操作的一类对象的描述。
在面向对象程序设计中,总是先声明类,再由类生成其对象。
注意不能把一组函数组合在一起构成类。即类不是函数的集合。
消息
面向对象设计技术必须提供一种机制允许一个对象与另一个对象的交互,这种机制叫消息传递。
在面向对象程序设计中,一个对象向另一个对象发出的请求被称为消息。当对象收到消息时,就调用有关的方法,执行相应的操作。消息是一个对象要求另一个对象执行某个操作的规格说明,通过消息传递才能完成对象之间的相互请求或相互协作。
消息具有三个性质:
(1).同一个对象可以接收不同形式的多个消息,作出不同的响应
(2).相同形式的消息可以传递给不同的对象,所作出的响应可以是不同的。
(3).对消息的响应并不是必需的,对象可以响应消息,也可以不响应。
分为两类:公有消息(其他对象发出),私有消息(向自己发出)。
方法
方法就是对象所能执行的操作。方法包括界面和方法体两部分。
方法的界面(接口)就是消息的模式,它给出了方法调用的协议;
方法体则是实现某种操作的一系列计算步骤,就是一段程序
在C++语言中方法是通过函数来实现的,称为成员函数
消息和方法的关系是:对象根据接收到的消息,调用相应的方法;反过来,有了方法,对象才能响应相应的消息。
面向对象程序设计的基本特征
- 抽象 Abstraction
- 封装 Encapsulation
- 继承 Inheritance
- 多态 Polymorphism
抽象
抽象是通过特定的实例(对象)抽取共同性质以后形成概念的过程。抽象是对系统的简化描述和规范说明,他强调了系统中的一部分细节和特性,而忽略了其他部分。
抽象包括两个方面,数据抽象和代码抽象(或称行为抽象)。前者描述某类对象的属性和状况,也就是此类对象区别于彼类对象的特征物理量;后者描述了某类对象的共同行为特征或具有的共同操作。
在面向对象的程序设计方法中,对一个具体问题的抽象分析结果,是通过类来描述和实现的。
封装
在面向对象程序设计中,封装是指把数据和实现操作的代码集中起来放在对象内部,并尽可能隐藏对象的内部细节。
封装应该具有如下几个条件:
(1)对象具有一个清晰的边界,对象的私有数据和实现操作的代码被封装在该边界内。
(2)具有一个描述对象与其他对象如何相互作用的接口,该接口必须说明消息如何传递的使用方法。
(3)对象内部的代码和数据应受到保护,其他对象不能直接修改。
继承
继承是在一个已经建立的类的基础上再接着声明一个新类的扩展机制,原先已经建立的类称为基类,在基类之下扩展的类称为派生类,派生类又可以向下充当继续扩展的基类,因此构成层层派生的一个动态扩展过程。
派生类享有基类的数据结构和算法,而本身又具有增加的行为和特性,因此继承的机制促进了程序代码的可重用性。
一个基类可以有多个派生类,一个派生类反过来可以具有多个基类,形成复杂的继承树层次体系。
基类与派生类之间本质的关系:基类是一个简单的类,描述相对简单的事物,派生类是一个复杂些的类,处理相对复杂的现象。
继承的作用:
避免公用代码的重复开发,减少代码和数据冗余。
通过增强一致性来减少模块间的接口。
继承分为单继承和多继承。
多态
多态性是指不同的对象收到相同的消息时产生多种不同的行为方式。
C++支持两种多态性:编译时的多态性(重载)和运行时的多态性(虚函数)。
OOP的主要优点
(1)可提高程序的重用性
(2)可控制程序的复杂性
(3)可改善程序的可维护性
(4)能够更好地支持大型程序设计
(5)增强了计算机处理信息的范围
(6)能很好地适应新的硬件环境
C++的优点
C++继承了C的优点,并有自己的特点,主要有:
(1)全面兼容C,C的许多代码不经修改就可以为C++所用,用C编写的库函数和实用软件可以用于C++。
(2)用C++编写的程序可读性更好,代码结构更为合理,可直接在程序中映射问题空间结构。
(3)生成代码的质量高,运行效率高。
(4)从开发时间、费用到形成软件的可重用性、可扩充性、可维护性和可靠性等方面有了很大提高,使得大中型的程序开发项目变得容易得多。
(5)支持面向对象的机制,可方便地构造出模拟现实问题的实体和操作。
C++对C的补充
注释与续行
注释符:/* */
或//
续行符:\
。当一个语句太长时可以用该符号分段写在几行中
note: 其实不加续航符直接换行也可以0.0
E.g.
1 |
|
This program will print hello world
in a line.
输入输出流
C: scanf
和printf
C++: cin>>
和cout<<
(用C的也可以,但是不推荐……)
cout和cin分别是C++的标准输出流和输入流。C++支持重定向,但一般cout指的是屏幕,cin指的是键盘。操作符<<
和>>
除了具有C语言中定义的左移和右移的功能外,在这里符号<<
是把右方的参数写到标准输出流cout中;相反,符号>>
则是将标准输入流的数据赋给右方的变量。
cin和>>
,cout和<<
配套使用
使用cout和cin时,也可以对输入和输出的格式进行控制,比如可用不同的进制方式显示数据,只要设置转换基数的操作符dec、hex和oct即可。
灵活的变量说明
定义变量的位置
在程序中的不同位置采用不同的变量定义方式,决定了该变量具有不同的特点。变量的定义一般可由以下三种位置:
(1)函数体内部
在函数体内部定义的变量称为局部变量。
(2)形式参数
当定义一个有参函数时,函数名后面括号内的变量,统称为形式参数。
(3)全局变量:在所有函数体外部定义的变量,其作用范围是整个程序,并在整个程序运行期间有效。
在C语言中,全局变量声明必须在任何函数之前,局部变量必须集中在可执行语句之前。
C++中的变量声明非常灵活。它允许变量声明与可执行语句交替执行,随时声明。for (int i = 0; i < 10; i++)
结构、联合和枚举名
在C++中,结构名、联合名、枚举名都是类型名。在定义变量时,不必在结构名、联合名或枚举名前冠以struct、union或enum。
如:定义枚举类型boole
: enum boole{FALSE, TRUE};
在C语言中定义变量需写成enum boole done;
,但在C++中,可以说明为boole done;
。
函数原型
C语言建议编程者为程序中的每一个函数建立圆形,而C++要求为每一个函数建立原型,以说明函数的名称、参数类型与个数,以及函数返回值的类型。其主要目的是让C++编译程序进行类型检查,即形参与实参的类型匹配检查,以及返回值是否与原型相符,以维护程序的正确性。
在程序中,要求一个函数的原型出现在该函数的调用语句之前。说明:
(1)函数原型的参数表中可不包含参数的名字,而只包含它们的类型。例如long Area(int, int);
(2)函数定义由函数首部和函数体构成。函数首部和函数原型基本一样,但函数首部中的参数必须给出名字而且不包含结尾的分号。
(3)C++的参数说明必须放在函数说明后的括号内,不可将函数参数说明放在函数首部和函数体之间。这种方法只在C中成立。
(4)主函数不必进行原型说明,因为它被看成自动说明原型的函数。
(5)原型说明中没有指定返回类型的函数(包括主函数main),C++默认该函数的返回类型是int。
(6)如果一个函数没有返回值,则必须在函数原型中注明返回类型为void,主函数类似处理。
(7)如果函数原型中未注明参数,C++假定该函数的参数表为空(void)。
const修饰符
C语言中习惯用#define
定义常量,C++利用const定义正规常数
一般格式 const 数据类型标识符 常数名 = 常量值
采用这种方式定义的常量是类型化的,它有地址,可以用指针指向这个值,但不能修改它。
const必须放在被修饰类型符和类型名前面。
数据类型是可选项,用来指定常数值的数据类型,如果省略了数据类型,那么默认是int。
const的作用于#define
相似,但它消除了#define
的不安全性。
const可以与指针一起使用。
指向常量的指针、常指针和指向常量的常指针。
1)指向常量的指针是指:一个指向常量的指针变量。
2)常指针是指:把指针本身,而不是它指向的对象声明为常量。
3)指向常量的常指针是指:这个指针本身不能改变,它所指向的值也不能改变。要声明一个指向常量的常指针,二者都要声明为const。
说明:
(1)如果用const定义的是一个整型变量,关键词int可以省略。
(2)常量一旦被建立,在程序的任何地方都不能再更改。
(3)与#define
定义的常量有所不同,const定义的常量可以有自己的数据类型,这样C++的编译程序可以进行更加严格的类型检查,具有良好的编译时的检测性。
(4)函数参数也可以用const说明,用于保证实参在该函数内部不被改动。
void型指针
void通常表示无值,但将void作为指针的类型时,它却表示不确定的类型。这种void型指针是一种通用型指针,也就是说任何类型的指针值都可以赋给void类型的指针变量。
void型指针可以接受任何类型的指针的赋值,但对已获值的void型指针,对它在进行处理,如输出或传递指针值时,则必须进行强制类型转换,否则会出错。
1 |
|
内联函数
调用函数时系统要付出一定的开销,用于信息入栈出栈和参数传递等。
C++引进了内联函数(inline function)的概念。在进行程序的编译时,编译器将内联函数的目标代码作拷贝并将其插入到调用内联函数的地方。1
2
3
4
5
6
7
8
9
10
11
12
13
using namespace std;
inline double circle(double r) {
return 3.1416 * r * r;
}
int main() {
for (int i = 0; i < 3; ++i) {
cout << "r = " << i << " area = " << circle(i) << endl;
}
return 0;
}
说明:
(1)内联函数在第一次被调用前必须进行声明或定义。否则编译器无法知道应该插入什么代码
(2)C++的内联函数具有与C中的宏定义#define
相同的作用和类似机理,但消除了#define
的不安全性。
(3)内联函数体内一般不能有循环语句和开关语句。
(4)后面类结构中所有在类说明体内定义的函数都是内联函数。
(5)通常较短的函数才定义为内联函数。
带有缺省参数值的函数
在C++中,函数的参数可以有缺省值。当调用有缺省参数的函数时,如果相应的参数没有给出实参,则自动用相应的缺省参数作为其实参。函数的缺省参数,是在函数原型中给定的。
说明
(1)在函数原型中,所有取缺省值的参数必须出现在不取缺省值的参数的右边。
(2)在函数调用时,若某个参数省略,则其后的参数皆应省略而采用缺省值。
函数重载
函数重载是指一个函数可以和同一作用域中的其他函数具有相同的名字,但这些同名函数的参数类型、参数个数不同。
为什么要使用函数重载?
对于具有同一功能的函数,如果只是由于参数类型不一样,则可以定义相同名称的函数。
调用步骤:
(1)寻找一个严格的匹配,即:调用与实参的数据类型、个数完全相同的那个函数。
(2)通过内部转换寻求一个匹配,即:通过(1)的方法没有找到相匹配的函数时,则由C++系统对实参的数据类型进行内部转换,转换完毕后,如果有匹配的函数存在,则执行该函数。
(3)通过用户定义的转换寻求一个匹配,若能查出有唯一的一组转换,就调用那个函数。即:在函数调用处由程序员对实参进行强制类型转换,以此作为查找相匹配的函数的依据。
注意事项:
重载函数不能只是函数的返回值不同,应至少在形参的个数、参数类型或参数顺序上有所不同。
应使所有的重载函数的功能相同。如果让重载函数完成不同的功能,会破坏程序的可读性。
函数模板
函数模板:建立一个通用函数,其函数类型和形参类型不具体指定,而是一个虚拟类型。
应用情况:凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。template<typename T>通用函数定义
,template<class T>通用函数定义
(class和typename可以通用)
1 | template<typename T> |
与重载函数比较:用函数模板比函数重载更方便,程序更简洁。但应注意它只适用于:函数参数个数相同而类型不同,且函数体相同的情况。如果参数的个数不同,则不能用函数模板。
作用域标识符::
通常情况下,如果有两个同名变量,一个是全局的,另一个是局部的,那么局部变量在其作用域内具有较高的优先权。
在全局变量加上::
,此时::var
代表全局变量。
无名联合
无名联合是C++中的一种特殊联合,可以声明一组无标记名共享同一段内存地址的数据项。如: union {int i; float j;}
在此无名联合中,声明了变量i和f具有相同的存储地址。无名联合可通过使用其中数据项名字直接存取,例如可以直接使用上面的变量i或f。
强制类型转换
C中数据类型转换的一般形式 (数据类型标识符) 表达式
C++支持这样的格式,还提供了一种更为方便的函数调用方法,即将类型名作为函数名使用,是的类型转换的执行看起来好像调用了一个函数。形式为:数据类型标识符 (表达式)。
推荐使用后一种方式。
动态内存分配
作为对C语言中malloc和free的替换,C++引进了new和delete操作符。它们的功能是实现内存的动态分配和释放。指针变量=new 数据类型;
或指针变量=new 数据类型(初始值);
例如:int *a, *b;
a = new int;
b = new int(10);
释放由new操作动态分配的内存时,用delete操作。delete 指针变量;
例如delete a;
,delete b;
。
优点:
(1)new和delete操作自动计算需要分配和释放类型的长度。这不但省去了用sizeof计算长度的步骤,更主要的是避免了内存分配和释放时因长度出错带来的严重后果。
(2)new操作自动返回需分配类型的指针,无需使用强制类型转换。
(3)new操作能初始化所分配的类型变量。
(4)new和delete都可以被重载,允许建立自定义的内存管理法。
说明:
(1)用new分配的空间,使用结束后应该用delete显示的释放,否则这部分空间将不能回收而变成死空间。
(2)使用new动态分配内存时,如果没有足够的内存满足分配要求,new将返回空指针(NULL)。因此通常要对内存的动态分配是否成功进行检查。
(3)使用new可以为数组动态分配内存空间。这时需要在类型后面加上数组大小。指针变量 = new 类型名[下标表达式];
使用new为多维数组分配空间时,必须提供所有维的大小。
(4)释放动态分配的数组存储区时,可使用delete运算符,语法格式为delete []指针变量;
(5)new 可在为简单变量分配内存空间的同时,进行初始化。这时的语法形式为:指针变量 = new 类型名(初始值列表)
引用
引用就是某一变量(目标)的一个别名,这样对引用的操作就是对目标的操作。
引用的声明方法:类型标识符 &引用名=目标变量名;
说明:
(1)&
在此不是求地址运算,而是起标识作用。
(2)类型标识符是指目标变量的类型。
(3)声明引用时,必须同时对其进行初始化。
(4)引用声明完毕后,相当于目标变量名有两个名称。
(5)声明一个引用,不是新定义了一个变量,系统并不给引用分配存储单元。
引用的使用
(1)引用名可以是任何合法的变量名。除了用作函数的参数或返回类型外,在声明时,必须立即对它进行初始化,不能声明完后再赋值。
(2)引用不能重新赋值,不能再把该引用名作为其他变量名的别名,任何对该引用的赋值就是该引用对应的目标变量名的赋值。对引用求地址,就是对目标变量求地址。
(3)由于指针变量也是变量,所以,可以声明一个指针变量的引用。方法是类型标识符 *&引用名=指针变量名
(4)引用是对某一变量或目标对象的引用,它本身不是一种数据类型,因此引用本身不占存储单元,这样,就不能声明引用的引用,也不能定义引用的指针。
(5)不能建立数组的引用,因为数组是一个由若干个元素所组成的集合,所以就无法建立一个数组的别名。
(6)不能建立空指针的引用。
(7)不能建立空类型void的引用。
(8)尽管引用运算符与地址操作符使用相同的符号,但是不一样的。引用仅在声明时带有引用运算符&
,以后就像普通变量一样使用,不能再带&
。其他场合使用的&
都是地址操作符。
用引用作为函数的参数
一个函数的参数可以定义成引用的形式。
在主调函数的调用点处,直接以变量作为实参进行调用即可,不需要实参变量有任何的特殊要求。
用引用返回函数值
函数可以返回一个引用,将函数说明为返回一个引用。
主要目的是:为了将函数用在赋值运算符的左边。要以引用返回函数值。类型标识符 &函数名 (形参列表及类型说明){函数体}
(1)以引用返回函数值,定义函数时需要在函数名前加&
(2)用引用返回一个函数值的最大好处是,在内存中不产生返回值的副本。
在定义返回引用的函数时,注意不要返回该函数内的自动变量(局部变量)的引用,由于自动变量的生存期仅限于函数内部,当函数返回时,自动变量就消失了。
一个返回引用的函数值作为赋值表达式的左值。
一般情况下,赋值表达式的左边只能是变量名,即被赋值的对象必须是变量,只有变量才能被赋值。
Lecture 2 类和对象
类的构成
类是C++对C中结构的扩展。
C语言中的struct是数据成员集合,而C++中的类,则是数据成员和成员函数的集合。
struct是用户定义的数据类型,是一种构造数据类型。类和struct一样,也是一种用户定义的数据类型,是一种构造数据类型。
C结构无法对数据进行保护和权限控制,所以结构中的数据是不安全的。C++中的类将数据和与之相关联的数据封装在一起,形成一个整体,具有良好的外部接口可以防止数据未经授权的访问,提供了模块间的独立性。
类的成员分两部分:一部分对应数据的状态,称为数据成员,另一部分作用于该数据状态的函数,称为成员函数。
private, protected, public
· private
部分称为类的私有部分,这一部分的数据成员和成员函数称为类的私有成员。私有成员只能由本类的成员函数访问,而类外部的任何访问都是非法的。(只能在定义、实现的时候访问)
· public
部分称为类的共有部分,这部分的数据成员和成员函数称为类的公有成员。公有成员可以由程序中的函数访问,它对外是完全开放的。
· protected
部分称为类的保护部分,这部分的数据成员和成员函数称为类的保护成员。保护成员可以由本类的成员函数访问,也可以由本类的派生类的成员函数访问,而类外的任何访问都是非法的。
(1)类声明格式中的3个部分并非一定要全有,但至少要有其中的一个部分。
一般一个类的数据成员应该声明为私有成员,成员函数声明为公有成员。
(2)类声明中的private, protected, public三个关键字可以按任意顺序出现任意次。但是,如果把所有的私有成员、保护成员和公有成员归类放在一起,程序将更加清晰。
(3)private处于类体重第一部分时,关键字private可以省略。
(4)数据成员可以是任何数据类型,但不能用自动(auto)、寄存器(register)或外部(extern)进行声明。
(5)不能在类声明中给数据成员赋值。C++规定,只有在类对象定义之后才能给数据成员赋初值。
成员函数的声明
普通成员函数形式
在类的声明中(.h)只给出成员函数的原型,而成员函数体写在类的外部(.cpp)。
内联函数形式
直接将函数声明在类内部;
在类声明中只给出成员函数的原型,而成员函数体写在类的外部,在成员函数返回类型前冠以关键字inline
。
对象的定义和使用
在C++中,可以把相同数据结构和相同操作集的对象看成属于同一类。
当定义了一个类的对象后,就可以访问对象的成员了。在类的外部可以通过类的对象对公有成员进行访问,访问对象成员要使用操作符.
(称为对象选择符,简称点运算符)。
在定义对象时,若定义的是指向对象的指针,则访问此对象的成员时,要用->
操作符。
在类的内部所有成员之间都可以通过成员函数直接访问,但是类的外部不能访问对象的私有成员。
类成员的访问属性
说明为public的成员不但可以被类中成员函数访问;还可以在类的外部,通过类的对象进行访问
说明为private的成员只能被类中成员函数访问,不能在类的外部,通过类的对象进行访问
说明为protected的成员除了类本身的成员函数可以访问外,该类的派生类的成员也可以访问,但不能在类的外部,通过类的对象进行访问
类的成员对类对象的可见性和对类的成员函数的可见性是不同的。
类的成员函数可以访问类的所有成员,而类的对象对类的成员的访问是受类成员的访问属性的制约的。
一般来说,公有成员是类的对外接口,而私有成员和保护成员是类的内部数据和内部实现,不希望外界访问。将类的成员划分为不同的访问级别有两个好处:一是信息隐蔽,即实现封装;二是数据保护,即将类的重要信息保护起来,以免其他程序不恰当地修改。
对象赋值语句
两个同类型的变量之间可以相互赋值。同类型的对象间也可以进行赋值,当一个对象赋值给另一个对象时,所有的数据成员都会逐位拷贝。
说明:
·在使用对象赋值语句进行对象赋值时,两个对象的类型必须相同,如果对象的类型不同,编译时将出错。
·两个对象之间的赋值,仅仅使这些对象中数据成员相同,而两个对象仍是分离的。
·=
的对象赋值是通过缺省的赋值运算符函数实现的。(复杂的需要重载)
·当类中存在指针时,使用缺省的赋值运算符进行对象赋值,可能会产生错误。
构造函数与析构函数
构造函数和析构函数都是类的成员函数,但它们都是特殊的成员函数,执行特殊的功能,不用调用便自动执行,而且这些函数的名字与类的名字有关。
C++语言中有一些成员函数性质是特殊的,这些成员函数负责对象的建立、删除。这些函数的特殊性在于可以由编译器自动地隐含调用,其中一些函数调用格式采用运算符函数重载的语法。C++引进一个自动完成对象初始化过程的机制,这就是类的构造函数。
对象的初始化
1)数据成员是不能在声明类时初始化
2)类型对象的初始化方法:
·调用对外接口(public成员函数)实现 声明类→定义对象→调用接口给成员赋值
·应用构造函数(constructor)实现 声明类→定义对象→同时给成员赋值
构造函数
构造函数是一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。构造函数具有一些特殊的性质:
(1)构造函数的名字必须与类名相同。
(2)构造函数可以有任意类型的参数,但不能指定返回类型。它有隐含的返回值,该值由系统内部使用。
(3)构造函数是特殊的成员函数,函数体可写在类体内,也可写在类体外。
(4)构造函数可以重载,即一个类中可以定义多个参数个数或参数类型不同的构造函数。构造函数不能继承。
(5)构造函数被声明为公有函数,但它不能像其他成员函数那样被显式地调用,它是在定义对象的同时调用的
·在声明类时如果没有定义类的构造函数,编译系统就会在编译时自动生成一个默认形式的构造函数。
·默认构造函数是构造对象时不提供参数的构造函数。
·除了无参数构造函数是默认构造函数外,带有全部默认参数值的构造函数也是默认构造函数。
·自动调用:构造函数在定义类对象时自动调用,不需用户调用,也不能被用户调用。在对象使用前调用。
·调用顺序:在对象进入其作用域时(对象使用前)调用构造函数。
利用构造函数创建对象的两种方法:
(1)利用构造函数直接创建对象,其一般形式为:类名 对象名[(实参表)];
这里的“类名”与构造函数名相同,“实参表”是为构造函数提供的实际参数。
(2)利用构造函数创建对象时,通过指针和new来实现。其一般语法形式为:类名 *指针变量 = new 类名 [(实参表)];
成员初始化表
对于常量类型和引用类型的数据成员,不能在构造函数中用赋值语句直接赋值,C++提供初始化表进行置初值。
类名::构造函数名([参数表])[:(成员初始化表)]
成员初始化表的一般形式为:数据成员名1(初始值1),数据成员名2(初始值2),…
如果需要将数据成员存放在堆中或数组中,则应在构造函数中使用赋值语句,即使构造函数有成员初始化表也应如此。
类成员是按照它们在类里被声明的顺序初始化的,与它们在初始化表中列出的顺序无关。
拷贝构造函数
拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。其作用是使用一个已经存在的对象去初始化另一个同类的对象。
通过等号复制对象时,系统会自动调用拷贝构造函数
拷贝函数特点:
该函数也是一种构造函数,所以其函数名与类名相同,并且该函数也没有返回值类型
该函数只有一个参数,并且是同类对象的引用
每个类必须有一个拷贝构造函数。可以根据需要定义特定的拷贝构造函数,以实现同类对象之间数据成员的传递。如果没有定义类的拷贝构造函数,系统就会自动生成产生一个缺省的拷贝构造函数
缺省的拷贝构造函数
如果没有编写自定义的拷贝构造函数,C++会自动地将一个已存在的对象复制给新对象,这种按成员逐一复制的过程是由缺省拷贝构造函数自动完成的。
调用拷贝构造函数的三种情况:
(1)当用类的一个对象去初始化该类的另一个对象时。(代入法与赋值法)
(2)当函数的形参是类的对象,调用函数,进行形参和实参结合时。
(3)当函数的返回值是对象,函数执行完成,返回调用者时。
浅拷贝与深拷贝
所谓浅拷贝,就是由缺省的拷贝构造函数所实现的数据成员逐一赋值,若类中含有指针类型数据,则会产生错误。
为了解决浅拷贝出现的错误,必须显示地定义一个自己的拷贝构造函数,使之不但拷贝数据成员,而且为对象1和对象2分配各自的内存空间,这就是所谓的深拷贝。
析构函数
析构函数也是一种特殊的成员函数。它执行与构造函数相反的操作,通常用于撤销对象时的一些清理任务,如释放分配给对象的内存空间等。
析构函数有以下一些特点:
①析构函数与构造函数名字相同,但它前面必须加一个波浪号(~);
②析构函数没有参数,也没有返回值,而且不能重载。因此在一个类中只能有一个析构函数;
③当撤销对象时,编译系统会自动调用析构函数。如果程序员没有定义析构函数,系统将自动生成和调用一个默认析构函数,默认析构函数只能释放对象的数据成员所占用的空间,但不包括堆内存空间。
析构函数被调用的两种情况:
(1)若一个对象被定义在一个函数体内,当这个函数结束时,析构函数被自动调用。
(2)若一个对象是使用new运算符动态创建,在使用delete释放时,自动调用析构函数。
调用构造函数和析构函数的顺序
1) 一般顺序:调用析构函数的次序正好与调用构造函数的次序相反:最先被调用的构造函数,其对应的析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。
2) 全局对象:在全局范围中定义的对象(即在所有函数之外定义的对象),它的构造函数在所有函数(包括main函数)执行之前调用。在程序的流程离开其作用域时(如main函数结束或调用exit函数)时,调用该全局对象的析构函数。
3) auto局部对象:局部自动对象(例如在函数中定义的对象),则在建立对象时调用其构造函数。如果函数被多次调用,则在每次建立对象时都要调用构造函数。在函数调用结束、对象释放时先调用析构函数。
4) static局部对象:如果在函数中定义静态局部对象,则只在程序第一次调用此函数建立对象时调用构造函数一次,在调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用析构函数。
对象的生存期
(1)局部对象:当对象被定义时,调用构造函数,该对象被创建;当程序退出该对象所在的函数体或程序块时,调用析构函数,对象被释放。
(2)全局对象:当程序开始运行时,调用构造函数,该对象被创建;当程序结束时,调用析构函数,该对象被释放。
(3)静态对象:当程序中定义静态对象时,调用构造函数,该对象被创建;当整个程序结束时,调用析构函数,对象被释放。
(4)动态对象:执行new运算符调用构造函数,动态对象被创建;用delete释放对象时,调用析构函数。
局部对象是倍定义在一个函数体或程序块内的,它的作用域限定在函数体或程序块内,生存期较短。
静态对象是被定义在一个文件中,它的作用域从定义时起到文件结束时为止。生存期较长。
全局对象是被定义在某个文件中,它的作用域包含在该文件的整个程序中,生存期是最长的。
动态对象是由程序员掌握的,它的作用域和生存期是由new和delete之间的间隔决定的。
Lecture 3 类和对象
自引用指针this
每一个类的成员函数都有一个隐藏定义的常量指针,称为this指针。
this指针的类型就是成员函数所属的类的类型。
每当调用成员函数时,它被初始化为被调函数所在类的对象的地址。也就是自动地将对象的指针传给它。不同的对象调用同一个成员函数时,编译器将根据成员函数的this指针所指向的对象来确定应该引用哪一个对象的数据成员。
在通常情况下,this指针在系统中是隐含地存在的,也可以显示地表示出来。
this指针是一个const指针,不能在程序中修改它或给它赋值。
this指针是一个局部数据,它的作用域仅在一个对象的内部。
对象数组与对象指针
对象数组
所谓对象数组是指每一数组元素都是对象的数组。
与基本数据类型的数组一样,在使用对象数组时也只能访问单个数组元素,也就是一个对象,通过这个对象,也可以访问到它的公有成员。
如果需要建立某个类的对象数组,在设计类的构造函数时要充分考虑到数组元素初始化的需要:
当各个元素的初值要求为相同的值时,应该在类中定义出不带参数的构造函数或带缺省参数值的构造函数
当各元素对象的初值要求为不同的值时需要定义带形参(无缺省值)的构造函数
定义对象数组时,可通过初始化表进行赋值
对象指针
每一个对象在初始化后都会在内存中占有一定的空间。因此,既可以通过对象名访问一个对象,也可以通过对象地址来访问一个对象。对象指针就是用于存放对象地址的变量。类名 * 对象指针名
用指针访问单个对象成员
初始化指向一个已创建的对象,用->
操作符访问对象的公有成员
用对象指针访问对象数组
对象指针++即指向下一个数组对象元素
指向类的成员的指针
类的成员自身也是一些变量、函数或者对象等,因此也可以直接将它们的地址存放到一个指针变量中,这样就可以使指针直接指向对象的成员,进而可以通过指针访问对象的成员。
指向成员的指针只能访问公有数据成员和成员函数。
使用要先声明,再赋值,然后访问。
指向数据成员的指针
声明:类型说明符 类名::*数据成员指针名
赋值:数据成员指针名 = &类名::数据成员名
使用:对象名.*数据成员指针名
对象指针名->*数据成员指针名
指向成员函数的指针
声明:类型说明符 (类名:: *指针名)(参数表)
赋值:成员函数指针名 = 类名::成员函数名
使用:(对象名.*成员函数指针名)(参数表)
(对象指针名->*成员函数指针名)(参数表)
向函数传递对象
对象可以作为参数传递给函数,其方法与传递其他类型的数据相同。在向函数传递对象时,是通过传值调用传递给函数的。因此,函数中对对象的任何修改均不影响调用该函数的对象本身。
对象指针可以作为函数的参数,使用对象指针作为函数参数可以实现传址调用,即可在被调用函数中改变函数的参数对象的值,实现函数之间的信息传递。同时使用对象指针实参仅将对象的地址值传给形参,而不进行副本的拷贝,这样可以提高运行效率,减少时空开销。
使用对象引用作为函数参数不但具有用对象指针作函数参数的优点,而且用对象引用作函数参数将更简单、更直接。
静态成员
引入目的:实现一个类的不同对象之间数据和函数共享
静态数据成员
用关键字static
声明
该类的所有对象维护该成员的同一个拷贝
必须在类外定义和初始化,用(::)
来指明所属的类
与一般的数据成员不同,无论建立多少个类的对象,都只有一个静态数据的拷贝。从而实现了同一个类的不同对象之间的数据共享。它不因对象的建立而产生,也不因对象的析构而删除。
静态数据成员初始化的格式:<数据类型><类名>::<静态数据成员名>=<值>;
初始化时使用作用域运算符来标明它所属的类,因此,静态数据成员是类的成员,而不是对象的成员。
引用静态数据成员时,采用如下格式:<类名>::<静态成员名>
如何使用静态数据成员?
(1)静态数据成员的定义与一般数据成员相似,但前面要加上static关键词
(2)静态数据成员的初始化与一般数据成员不同。初始化位置在定义对象之前,一般在类定义后,main()前进行
(3)访问方式(只能访问公有静态数据成员)
可用类名访问:类名::静态数据成员
也可用对象访问:对象名.静态数据成员
,对象指针->静态数据成员
(4)私有静态数据成员不能被类外部函数访问,也不能用对象进行访问
(5)支持静态数据成员的一个主要原因是可以不必使用全局变量。静态数据成员的主要用途是定义类的各个对象所公用的数据。
静态成员函数
类外代码可以使用类名和作用域符来调用公有静态成员函数
静态成员函数只能引用属于该类的静态数据成员或静态成员函数。访问非静态数据成员,必须通过参数传递方式得到对象名,通过对象名访问。
可以通过定义和使用静态成员函数来访问静态数据成员。
所谓静态成员函数就是使用static关键字声明函数成员。同静态数据成员一样,静态成员函数也属于整个类,由同一个类的所有对象共同维护,为这些对象所共享。
静态成员函数作为成员函数,它的访问属性可以受到类的严格控制。对公有静态成员函数,可以通过类名或对象名来调用;而一般的非静态公有成员函数只能通过对象名来调用。
静态成员函数可以直接访问该类的静态数据成员和函数成员;而访问非静态数据成员,必须通过参数传递方式得到对象名,然后通过对象名来访问。
定义:static 返回类型 静态成员函数名(参数表);
使用:类名::静态成员函数名(实参表)
对象.静态成员函数名(实参表)
对象指针->静态成员函数名(实参表)
注意:
(1)静态成员函数可以定义成内嵌的,也可以在类外定义,在类外定义时不能用static前缀。
(2)静态成员函数主要用来访问全局变量或同一个类中的静态数据成员。特别是,当它与静态数据成员一起使用时,达到了对同一个类中对象之间共享数据进行维护的目的。
(3)私有静态成员函数不能被类外部函数和对象访问。
(4)使用静态成员函数的一个原因是,可以用它在建立任何对象之前处理静态数据成员。这是普通成员函数不能实现的。
(5)静态成员函数中没有指针this,所以静态成员函数不能访问类中的非静态数据成员,若确实需要则只能通过对象名作为参数访问。
可以通过指针访问静态数据成员和静态成员函数
友元
友元可以访问与其有好友关系的类中的私有成员。友元包括友元函数和友元类。
友元函数
友元函数不是当前类的成员函数,而是独立于当前类的外部函数,但它可以访问该类的所有对象的成员,包括私有、保护和公有成员。
友元函数的声明:
位置:当前类体中
格式:函数名前加friend
友元函数的定义:
类体外:同一般函数(函数名前不能加类名::
)
类体内:函数名前加friend
说明:
(1)友元函数毕竟不是成员函数,因此,在类的外部定义友元函数时,不能在函数名前加上类名::
。
(2)友元函数一般带有一个该类的入口参数。因为友元函数不是类的成员函数,没有this指针,所以不能直接引用对象成员的名字,也不能通过this指针引用对象的成员,它必须通过作为入口参数传递进来的对象名或对象指针来引用该对象的成员。
引入友元机制的原因
(1)友元机制是对类的封装机制的补充,利用此机制,一个类可以赋予某些函数访问它的私有成员的特权。
(2)友元提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据互享的机制。
友元成员函数
一个类的成员函数也可以作为另一个类的友元,这种成员函数不仅可以访问自己所在类对象中的所有成员,还可以访问friend声明语句所在类对象中的所有成员。
这样能使两个类相互合作、协调工作,完成某一任务。
一个类的成员函数作为另一个类的友元函数时,必须先定义这个类。
友元类
一个类也可以作为另一个类的友元。
类Y的所有成员函数都是类X的友元函数
在实际工作中,除非确有必要,一般并不把整个类声明为友元类,而只将确实有需要的成员函数声明为友元函数,这样更安全一些。
友元的关系是单向的而不是双向的。
友元的关系不能传递。
共用数据的保护(const)
const对象的一般形式类型名 const 对象名[(构造实参表列)];
const 类型名 对象名[(构造实参表列)];
常对象必须要有初值。
定义为const的对象的所有数据成员的值都不能被修改。凡出现调用非const的成员函数,将出现编译错误。
对数据成员声明为mutable时,即使是const对象,仍然可以修改该数据成员值。
常数据成员
用const声明的常数据成员,其值是不能改变的。只能通过构造函数的参数初始化表对场数据成员进行初始化。
1 | class Time { |
常成员函数
成员函数声明中包含const时为常成员函数。此时,该函数只能引用本类中的数据成员,而不能修改它们,即成员数据不能作为语句的左值。(mutable可以)类型说明符 函数名(参数表) const;
const
的位置在函数名和括号之后,是函数类型的一部分,在声明函数和定义函数时都要有const关键字。
如果将一个对象声明为常对象,则通过该对象只能调用它的常成员函数,而不能调用普通成员函数。而且常成员函数也不能更新对象的数据成员。
1 | void show_Time() const; |
指向常对象的指针变量
指向常对象的指针变量的一般形式:const 类型 *指针变量名
指向常对象(变量)的指针变量,不能通过它来改变所指向目标对象的值,但指针变量的值是可以改变的。
如果被声明为常对象(变量),只能用指向常对象(变量)的指针变量指向它,而不能非const型指针变量去指向它。
指向常对象(变量)的指针变量除了可以指向常对象(变量)外,还可以指向未被声明为const的对象(变量)。此时不能通过此指针变量改变该变量的值。
指向常对象(变量)的指针变量可以指向const和非const型的对象(变量),而指向非const型变量的指针变量只能指向非const的对象(变量)。
如果函数的形参是指向非const型变量的指针,实参只能用指向非const变量的指针,而不能用指向const变量的指针,这样,在执行函数的过程中可以改变形参指针变量所指向的变量的值。
如果函数形参是指向const型变量的指针,允许实参是指向const变量的指针,或指向非const变量的指针。
1 | void f(Time *pt); |
| Time const t = Time(1,2,3); const Time t = Time(1,2,3);
const int a = 10;
int const a = 10; | t是常对象,其成员值在任何情况下都不能被改变 a是常变量,其值不能被改变 |
| —- | —- |
| void Time::fun() const; | fun是Time类的常成员函数,可以调用该函数,但不能修改本类中的数据成员(非mutable) |
| Time const pt; int const pa; | pt是指向Time对象的常指针,pa是指向整数的常指针。指针值不能改变 |
| const Time pt; const int pa; | pt是指向Time类常对象的指针,pa是指向常整数的指针,不能通过指针来改变指向的对象(值) |
Lecture 4 派生类与继承
继承与派生类
继承目的:代码的重用和代码的扩充
继承方法程序设计思路:一般->特殊
继承种类:单继承、多继承
继承方式:public protected private
继承内容:除构造函数、析构函数、私有成员外的其他成员
保持已有类的特性而构造新类的过程称为继承。
在已有类的基础上新增自己的特性而产生新类的过程称为派生。
被继承的已有类称为基类(父类)。
派生出的新类称为派生类。
继承的访问控制
三种继承方式:public, private, protected
派生类成员的访问权限:inaccessible, public, private, protected
在基类中的访问属性 | 继承方式 | 在派生类中的访问属性 |
---|---|---|
private | public | inaccessible |
private | private | inaccessible |
private | protected | inaccessible |
public | public | public |
public | private | private |
public | protected | protected |
protected | public | protected |
protected | private | private |
protected | protected | protected |
私有继承的访问规则
基类的public成员和protected成员被继承后作为派生类的private成员,派生类的其他成员可以直接访问它们,但是在类外部通过派生类的对象无法访问。
基类的private成员在私有派生类中是不可直接访问的,所以无论是派生类成员还是通过派生类的对象,都无法直接访问从基类继承来的private成员,但是可以通过基类提供的public成员函数间接访问。
通过派生类的对象不能访问基类中的任何成员。
公有继承的访问规则
基类的public成员和protected成员被继承到派生类中仍作为派生类的public成员和protected成员,派生类的其他成员可以直接访问它们。但是,类的外部的使用者只能通过派生类的对象访问继承来的public成员。
派生类的对象只能访问基类的public成员。
1.派生的对象可以赋给基类的对象
1 | Derived d; // Derived public inherit from Base |
2.派生类的对象可以初始化基类的引用
1 | Derived d; |
3.派生类的对象的地址可以赋给指向基类的指针
1 | Derived d; |
通过指针或引用只能访问对象d中所继承的基类成员。
保护继承的访问规则
基类的public成员和protected成员被继承到派生类中都作为派生类的protected成员,派生类的其他成员可以直接访问它们,但是类的外部使用者不能通过派生类的对象来访问它们。
通过派生类的对象不能访问基类中的任何成员。
基类与派生类的关系
派生类是基类的具体化
派生类是基类定义的延续
派生类是基类的组合
派生类的构造函数和析构函数
基类的构造函数和析构函数不能被继承,一般派生类要加入自己的构造函数。
通常情况下,当创建派生类对象时,首先执行基类的构造函数,随后再执行派生类的构造函数;
当撤销派生类对象时,则先执行派生类的析构函数,随后再执行基类的析构函数。
当基类的构造函数没有参数,或没有显示定义构造函数时,派生类可以不向基类传递参数,甚至可以不定义构造函数;当基类含有带参数的构造函数时,派生类必须定义构造函数,以提供把参数传递给基类构造函数的途径。
1 | 派生类名(参数总表):基类名(参数表) |
当派生类中含有内嵌对象成员时,其构造函数的一般形式为:
1 | 派生类名(参数总表):基类名(参数表1),内嵌对象名1(内嵌对象参数表1),内嵌对象名n(内嵌对象参数表n) |
在定义派生类对象时,构造函数的执行顺序如下:
调用基类的构造函数;
调用内嵌对象成员(子对象类)的构造函数(有多个对象成员时,调用顺序由它们在类中声明的顺序确定);
派生类中的构造函数体中的内容
撤销对象时,析构函数的调用顺序与构造函数的调用顺序相反。
当基类构造函数不带参数时,派生类可不定义构造函数,但基类构造函数带有参数,则派生类必须定义构造函数。
若基类使用缺省构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去:基类构造函数名(参数表)
如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类的构造,依次上溯。
由于析构函数是不带参数的,在派生类中是否定义析构函数与它所属的基类无关,基类的析构函数不会因为派生类没有析构函数而得不到执行,基类和派生类的析构函数是各自独立的。
多继承
派生类只有一个基类,这种派生方法称为单基派生或单继承
当一个派生类具有多个基类时,这种派生方法称为多基派生或多继承class 派生类名:继承方式1 基类名1,...,继承方式n,基类名n {}
构造函数的执行顺序同单继承:
先执行基类构造函数,再执行对象成员的构造函数,最后执行派生类构造函数。
必须同时负责该派生类所有基类构造函数的调用。派生类的参数个数必须包含完成所有基类初始化所需的参数个数。
处于同一层次各基类构造函数执行顺序,取决于声明派生类时所制定各基类的顺序,与派生类构造函数中所定义的成员初始化列表的各项顺序无关。
对基类成员的访问必须是无二义性,使用类名限定可以消除二义性。
虚基类
当某一个类的多个直接基类是从另一个共同基类派生而来时,这些直接基类中从上一级基类继承来的成员就拥有相同的名称。在派生类的对象中,这些同名成员在内存中同时拥有多个拷贝。一种分辨方法是使用作用域标示符来唯一表示它们。另一种方法就是定义派生类,使派生类中只保留一份拷贝。class 派生类名:virtual 继承方式 类名 {}
如果在虚基类中定义有带形参的构造函数,并且没有定义缺省形参的构造函数,则整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。
建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。该派生类的其他基类对虚基类构造函数的调用都自动被忽略。
若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类的构造函数。
对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。
对于非虚基类,构造函数的执行顺序仍然是先左后右,自上而下。
若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造函数。
赋值兼容规则
所谓赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。这样,公有派生类实际上具备了基类的所有特性,凡基类能解决的问题,公有派生类也能解决。(在公有派生已提及)
(1)可以用派生类对象给基类对象赋值。
(2)可以用派生类对象来初始化基类的引用。
(3)可以把派生类的地址赋值给指向基类的指针。(这种形式的转换,是在实际应用中最常见到的)
(4)可以把指向派生类对象的指针赋值给指向基类对象的指针
说明
(1)声明为指向基类对象的指针可以指向它的公有派生的对象,但不允许指向它的私有派生的对象
(2)允许将一个声明为指向基类的指针指向其公有派生类的对象,但是不能将一个声明为指向派生类对象的指针指向其基类的一个对象
(3)声明为指向基类对象的指针,当其指向公有派生类对象时,只能用它来直接访问派生类中从基类继承来的成员,而不能直接访问公有派生类中定义的成员。
若想访问其公有派生类的特定成员,可以将基类指针用显示类型转换为派生类指针。
Lecture 5 多态性与虚函数
多态性
所谓多态性就是不同对象收到相同的消息时,产生不同的动作。
C++中的多态性:
通用多态:参数多态,包含多态
专用多态:重载多态,强制多态
参数多态与类属函数和类属类相关联,函数模板和类模板就是这种多态
包含多态是研究类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现的
重载多态如函数重载、运算符重载等。普通函数及类的成员函数的重载多属于重载多态
强制多态是指将一个变元的类型加以变化,以符合一个函数或操作的要求,例如加法运算符在进行浮点数与整型数相加时,首先进行类型强制转换,把整型数变为浮点数再相加的情况,就是强制多态的实例
在C++中,编译时多态性主要是通过函数重载和运算符重载实现的,运行时多态性主要是通过虚函数来实现的
虚函数
虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是在运行时才决定如何动作,即所谓的动态联编。
虚函数是成员函数,而且是非static的成员函数。是动态联编的基础。virtual <类型说明符><函数名>(<参数表>)
如果某类中的一个成员函数被说明为虚函数,这就意味着该成员函数在派生类中可能有不同的实现。当使用这个成员函数操作指针或引用所标识对象时,对该成员函数调用采取动态联编方式,即在运行时进行关联或束定。
动态联编只能通过指针或引用标识对象来操作虚函数。如果采用一般类型的标识对象来操作虚函数,则将采用静态联编方式调用虚函数。
派生类中对基类的虚函数进行替换时,要求派生类中说明的虚函数与基类中的被替换的虚函数之间满足如下条件:
(1)与基类的虚函数有相同的参数个数
(2)其参数的类型与基类的虚函数的对应参数类型相同
(3)其返回值或者与基类虚函数的相同,或者都返回指针或引用,并且派生类虚函数所返回的指针或引用的基类型是基类中被替换的虚函数所返回的指针或引用的基类型的子类型
虚函数的作用
虚函数同派生类的结合可使C++支持运行时的多态性,实现了在基类定义派生类所拥有的通用接口,而在派生类定义具体的实现方法,即常说的”同一接口,多种方法”,它帮助程序员处理越来越复杂的程序
虚函数的定义virtual 函数类型 函数名(形参表){}
派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与其基类中的原型完全相同。
C++规定,如果在派生类中,没有用virtual显示地给出虚函数声明,这时系统就会遵循以下的规则来判断一个成员函数是不是虚函数:
·该函数与基类的虚函数有相同的名称
·该函数与基类的虚函数有相同的参数个数及相同的对应参数类型
·该函数与基类的虚函数有相同的返回类型或满足赋值兼容规则的指针、引用型的返回类型
派生类的函数满足了上述条件,就被自动确定为虚函数
说明:
(1)通过定义虚函数来使用C++提供的多态机制时,派生类应该从它的基类公有派生。赋值兼容规则成立的条件是派生类从其基类公有派生。
(2)必须首先在基类中定义虚函数。在实际应用中,应该在类等级内需要具有动态多态性的几个层次中的最高层类内首先声明虚函数。
(3)在派生类对基类中声明的虚函数进行重新定义时,关键字virtual可以写或不写。
(4)使用对象名和点运算符的方式也能调用虚函数,但是这种调用在编译时进行的是静态联编,它没有充分利用虚函数的特性。只有通过基类指针访问虚函数时才能获得运行时的多态性。
(5)一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。
(6)虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。但是虚函数可以在另一个类中被声明为友元函数。
(7)内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看作是非内联的。
(8)构造函数不能是虚函数。因为虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在产生对象之前运行的,因此虚构造函数是没有意义的。
(9)析构函数可以是虚函数,而且通常声明为虚函数。
虚析构函数
在程序用带指针参数的delete运算符撤销对象时,会发生一种情况:系统会只执行基类的析构函数,而不执行派生类的析构函数。
解决方法:将基类的析构函数声明为虚函数
析构函数设置为虚函数后,在使用指针引用时可以动态联编,实现运行时的多态,保证使用基类类型的指针能够调用适当的析构函数针对不同的对象进行清理工作
虚函数与重载函数的关系
在一个派生类中重新定义基类的虚函数是函数重载的另一种形式,但它不同于一般的函数重载。
普通的函数重载时,其函数的参数个数或参数类型必须有所不同,函数的返回类型也可以不同。
当重载一个虚函数时,也就是说在派生类中重新定义虚函数时,要求函数名、返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同。
若仅仅函数名相同,而参数的个数、类型或顺序不同,系统将它作为普通的函数重载,这时将失去虚函数的特性。
纯虚函数和抽象类
纯虚函数
纯虚函数是一个在基类中说明的虚函数,它在基类中没有定义,但要求在它的派生类中必须定义自己的版本,或重新说明为纯虚函数。virtual <函数类型><函数名>(参数表)=0;
纯虚函数与一般虚函数成员的原型在书写形式上的不同就在于后面加了=0
,表明在基类中不用定义该函数,它的实现部分(函数体)留给派生类去做。
纯虚函数没有函数体
最后面的=0
并不表示函数返回值为0
这是一个声明语句,最后有;
纯虚函数只有函数的名字而不具备函数的功能,不能被调用。在派生类中对此函数提供定义后,它才能具备函数的功能,可被调用。
如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。
一个具有纯虚函数的类称为抽象类
抽象类
如果一个类至少有一个纯虚函数,那么就称该类为抽象类。
抽象类只能作为其他类的基类来使用,不能建立抽象类对象,其纯虚函数的实现由派生类给出。
派生类中必须重载基类中的纯虚函数,否则它仍将被看作一个抽象类。
规定:
(1)由于抽象类中至少包含一个没有定义功能的纯虚函数,因此,抽象类只能作为其他类的基类来使用,不能建立抽象类的对象,纯虚函数的实现由派生类给出
(2)不允许从具体类派生出抽象类
(3)抽象类不能用作参数类型、函数返回类型或显示转换的类型
(4)可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性
(5)抽象类的析构函数可以被声明为纯虚函数,这时,应该至少提供该析构函数的一个实现
(6)如果派生类中没有重定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不是抽象类了,它是一个可以建立对象的具体类
(7)在抽象类中也可以定义普通成员函数或虚函数,虽然不能为抽象类声明对象,但仍然可以通过派生类对象来调用这些不是纯虚函数的函数。
运算符重载
运算符重载是使同一个运算符作用于不同类型的数据时具有不同的行为。运算符重载实质上将运算对象转化为运算函数的实参,并根据实参的类型来确定重载的运算函数。
运算符重载的规则
1.只能重载C++中已有的运算符,不能臆造新的运算符
2.类属关系运算符.
、作用域分辨符::
、成员指针运算符*
、sizeof
运算符和三目运算符?:
不能重载
3.重载之后运算符的优先级和结合性都不能改变,单目运算符只能重载为单目运算符,双目运算符只能重载为双目运算符
4.运算符重载后的功能应当与原有功能相类似
5.重载运算符含义必须清楚,不能有二义性
将运算符重载为类的成员函数
将运算符重载为类的成员函数就是在类中用关键字operator定义一个成员函数,函数名就是重载的运算符。运算符如果重载为类的成员函数,它就可以自由地访问该类的数据成员。<类型><类名>::operator<要重载的运算符>(形参表){}
双目运算
op1 B op2
把B重载为op1所属类的成员函数,只有一个形参,形参的类型是op2所属类。
例如,经过重载后,op1+op2
就相当于op1.operator+(op2)
单目运算
(1)前置单目运算:U op
把U重载为operand所属类的成员函数,没有形参。
例如,++
重载的语法格式为:<函数类型> operator ++();
++op
就相当于函数调用op.operator ++();
(2)后置单目运算:op V
运算符V重载为op所属类的成员函数,带有一个整型(int)形参。
例如,后置单目运算符--
重载的语法格式为:<函数类型> operator --(int);
op--
就相当于函数调用op.operator--(0);
对于++(—)运算符的重载,因为编译器不能区分出++(—)是前置还是后置的,所以要加上(int)来区分。
赋值运算
赋值运算符重载一般包括以下几个步骤,首先要检查是否自赋值,如果是要立即返回,如果不返回,后面的语句会把自己所指空间删掉,从而导致错误;第二步要释放原有的内存资源;第三步要分配新的内存资源,并复制内容;第四步是返回本对象的引用。如果没有指针操作,则没有第二步操作。
赋值运算符与拷贝构造函数在功能上有些类似,都是用一个对象去填另一个对象,但拷贝构造函数是在对象建立的时候执行,赋值运算符是在对象建立之后执行。
运算符重载为友元函数
friend <函数返回类型> operator <二元运算符>(<形参1>,<形参2>);
friend <函数返回类型> operator <一元运算符>(类名 &对象){}
其中,函数返回类型为运算符重载函数的返回类型。operator<重载函数符>
为重载函数名。当重载函数作为友元普通函数时,重载函数不能用对象调用,所以参加运算的对象必须以形参方式传送到重载函数体内,在二元运算符重载函数为友元函数时,形参通常为两个参加运算的对象。
双目运算
op1 B op2
双目运算符B重载为op1所属类的友元函数,该函数有两个形参,表达式op1 B op2
相当于函数调用operator B(op1, op2)
单目运算
(1)前置单目运算 U op
前置单目运算符U重载为op所属类的友元函数,表达式U op
相当于函数调用operator U(op)
(2)后置单目运算 op U
后置单目运算符V重载为op所属类的友元函数,表达式op V
相当于函数调用operator V(op, int)
重载流插入和流提取运算符
istream和ostream是C++的预定义流类,cin是istream的对象,cout是ostream的对象。运算符<<由ostream重载为插入操作,运算符>>由istream重载为提取操作,用于输入和输出基本类型数据。可用重载<<和>>运算符,用于输入和输出用户自定义的数据类型,必须定义为类的友元函数。
输出操作符的重载
ostream & operator <<(ostream &, const 自定义类&);
第一个参数和函数的类型都必须是ostream &
类型,第二个参数是对要进行输出的类类型的引用,它可以是const,因为一般而言输出一个对象不应该改变对象。返回类型是一个ostream引用,通常是输出操作符所操作的ostream对象。
1 | ostream &operator<<(ostream &output,Date &d) |
输入操作符的重载
istream & operator >>(istream &, 自定义类 &)
与输出操作符类似,输入操作符的第一个形参是一个引用,指向要读的流,并且返回的也是同一个流的引用。第二个形参是对要读入的对象的非const引用,该形参必须为非const,因为输入操作符的目的是将数据读到这个对象中。和输出操作符不同的是输入操作符必须处理错误和文件结束的可能性。