第1、2章 绪论与C++对C的扩充

1.类与对象的基本概念:
在面向对象程序设计中,对象是描述其属性的数据以及对这些数据施加的一组操作封装在一起构成的统一体。
类是对一组具有相同属性(或称状态,用数据表示)和行为(或称操作、方法,用函数实现)的对象的抽象,一个类所包含的方法和数据描述了一组对象的共同属性和行为。对象则是类的具体化,是类的实例。
在面向对象的程序设计中,总是先声明类,再由类生成其对象,类是建立对象的“模板”,按照这个模板所建立的一个个具体的对象,就是类的实际例子,通常称为实例。
2.面向对象程序设计方法的3个主要特点:
(1)封装性:在面向对象程序设计中,封装是指把数据和实现操作的代码集中起来放在对象内部,并尽可能隐藏对象的内部细节。面向对象程序设计是通过类的说明实现封装的。程序在使用一个对象时,只能通过对象与外界的操作接口来操作它。在C++中,成员函数名就是对象的对外接口。外界可以通过函数名来调用这些函数实现某些行为。
(2)继承性:体现在类的层次关系中,派生的子类拥有父类中定义的数据和方法。子类直接继承父类的全部描述,同时可修改和扩充,并且继承具有传递性。
(3)多态性:对象根据所接收的消息而做出动作。同一消息为不同的对象接受时可产生完全不同的行动,这种现象称为多态性。利用多态性,用户可发送一个通用的信息,而将所有的实现细节都留给接受消息的对象自行决定,这样,同一消息即可调用不同的方法。
3.C++对C的补充
(1)输入输出头文件:

#include<iosteam>
using namespace std;//使用命名空间std

(2)main()函数
标准C++要求main函数必须声明为int类型。main函数执行完毕通常返回给操作系统一个值。该值是一个状态指示器,操作系统一般通过main()的返回值来确定程序是否执行完毕。
int main(){ …
return 0;
}
(3)C++中的输出流
cout输出流可向输出设备输出若干个任意类型的数据。
cout必须配合插入操作符“<<”使用,用于向输出流中插入数据,引导待输出的数据输出到屏幕上。
格式:cout<<输出项1<<输出项2<<….<<输出项n;、
“输出项”是需要输出的一些数据,可以是变量、常量或表达式。
在cout中使用endl或转义字符′\n′作为输出项,可实现数据的换行功能。
(4)C++中的输入流
在C++中,数据的输入通常采用输入流对象cin来完成。
格式:cin>>变量1>>变量2>>……>>变量n;
cin必须配合抽取操作符“>>”使用,用于从输入数据流中抽取数据传送给“>>”后面的变量,从而实现在程序运行时为变量赋值。
(5)const修饰符
①C++:使用const定义常值变量。
格式:const 类型标识符 常值变量名=常量值;
声明const变量时必须通过初始化获得初值
②指向常量的指针:指针指向空间的内容是不能改变的

const int *p;
int *u;
*p=10; //╳ 因为p指向的是常量,其所指空间的内容不可修改
p=u; //√
③常指针:定义时必须初始化,且一旦初始化,就不能再指向其他变量
int d = 1;
int* const w = &d;
*w=2; // OK
w=p; //╳ 因为p是一个常指针,其所指空间地址不可修改

④指向常量的常指针:定义时必须初始化,不能再指向其他变量,且指针指向空间的内容是不能改变的。

int d = 1;
const int* const w = &d;
*w=2; // ╳ 因为w指向的是常量,其所指空间的内容不可修改
w=p; //╳ 因为w是一个常指针,其所指空间地址不可修改
⑤const修饰形参:表示此参数在其所在的函数内为常量,即不可修改。

(6)函数的使用
①形参带默认值:在调用函数时,可以不用给带默认值的形参传递实参。
如:

void myfunc(double d=0.0{ }
myfunc(198.234); // pass an explicit value
myfunc( ); // let function use default

默认参数通常在函数名第一次出现时指定,一般是在函数原型声明中设置,在函数定义时不再写出。若程序中只有函数定义而没有函数声明,则默认参数可出现在函数定义中。
如果一个函数中有多个默认参数,则在形参分布中,默认参数应按从右至左的顺序依次定义,即在带默认值的形参的右边不能出现不带默认值的形参。
②内联/内嵌函数:在函数声明或定义的前面加上关键字“inline”,该函数就被声明为内联函数。inline限定符只用于经常使用的短函数。
③函数重载
两或两个以上的函数使用同一个函数名,称为函数重载。
函数重载的条件: 函数参数的类型或个数不同,即函数原型的参数列表不同
只有函数返回值类型不同,不能重载
函数重载与参数带默认值的函数一起使用时,有可能引起二义性,使用时要谨慎。
(7)引用和引用参数
①概念:引用(reference)就是一个变量(目标)的别名,对引用的操作就是对目标的操作。
②语法格式: 类型标识符 &引用名=目标变量名;
例如:int a; int &aRef=a;
③说明:
系统并不为引用分配存储单元;
除了用作函数的参数或返回值类型外,在声明引用时,必须对其进行初始化。
为引用提供的初值,可以是一个变量或另一个引用。
一旦引用被声明为一个目标变量的别名,就不能把该引用名再作为其他变量的别名。
可以声明一个指针变量的引用:
不能建立数组、空指针、空类型(void)的引用。
不能建立引用的引用,不能建立指向引用的指针。
指针是通过地址间接访问某个变量(要使用*),而引用是通过别名直接访问某个变量。因而使用引用可以简化程序。
如果不希望通过引用改变原始变量的值,可声明常引用
const 类型标识符 &引用名=目标变量名;
④用引用作为函数的参数
定义函数时,函数的形参可以是基本数据类型的变量、指针变量、数组名,也可以是引用的形式。
调用该函数时,实参对应为主调函数中的变量。这样,被调函数可以通过引用对主调方的实参进行访问,甚至修改其值。
(8)作用域运算符::
C允许在不同的作用域内声明同名的变量,在作用域的重叠区域内,全局变量被局部变量屏蔽。
在C++中,可以通过使用作用域运算符(::)来访问重叠作用域内与局部变量同名的全局变量。
(9)new和delete运算符
①new运算符:用于向系统申请动态存储空间,并返回正确类型的指针。申请失败时,则返回0指针,使用new的语法格式:指针变量=new 数据类型标识符(初值)/[元素个数]
②delete运算符:使用new申请得到的空间必须使用delete运算符进行释放,使用格式:
delete []指针变量名

第3章 类与对象

1.类的定义:
类的定义包括类的说明和类的实现两大部分。说明部分提供了对该类所有数据成员和成员函数的描述;实现部分则提供了所有成员函数的实现代码。
(1)类定义的格式:
class <类名>
{ private:
<数据成员或成员函数>
protected:
<数据成员或成员函数>
public:
<数据成员或成员函数>
};
<各成员函数的实现代码>

(2)说明:
①class是定义类的关键字,类名由用户自己定义,但应满足标识符的命名规则。
②与struct相比,类的说明部分添加了新内容:成员函数的说明以及访问权限控制符:public、protected、private。
private部分:该部分成员只允许被本类的成员函数访问或调用,在类外不能被直接访问。一般情况下,类的数据成员多声明为private成员;
public部分:该部分成员允许被本类或其他类的成员函数(通过对象)访问或调用。public成员多为成员函数,用来向外界提供一个接口,外界只能通过这个接口才可以实现对private成员的访问;一个类的定义中应该含有public权限的成员,否则该类的定义无意义。
protected部分:该部分成员不能在类外访问,只允许被本类的成员函数及其子类(派生类)的成员函数访问或调用。
当类成员未指明是哪部分时,默认为private成员。
类中的每个成员只能被指定一种特定的访问控制权限。
③公有部分、私有部分和保护部分可以以任意顺序出现,且并非同时出现。
④类的说明部分以分号结束。
(3)成员函数的定义:
①在类的定义体外部定义成员函数,在类的定义体中声明成员函数定义时,需在函数名之前加上其所属的类名和作用域限定符“::”,没加类名的函数默认为非成员函数(全局函数)。
②在类的定义体内部定义成员函数,系统自动使该成员函数成为内联函数。在类定义体外部定义的成员函数若为内联函数,需用inline关键字指明。
(4)类成员的访问:
①类的数据成员和成员函数属于该类的类作用域(即使成员函数定义在类体之外),非成员函数在文件作用域中定义。
②在类作用域中,类成员可由该类的所有成员函数直接访问;在类作用域外,访问类成员是通过一个对象的句柄实现的,可以是对象名、对象引用或对象指针。利用对象的句柄访问类成员的运算符与访问结构体成员的运算符相同。
2.对象
对象是类的一个实例,当定义一个类时,只定义了这种数据类型,当程序中定义了数据类型的变量时,该变量就是该类的对象(或类的实例)。
(1)类对象的定义方法:
定义类的同时定义对象;定义类后定义对象,单独定义对象的一般格式: 类名 对象名表;
(2)对象的指针与引用:
①指向对象的指针变量的定义格式: 类名 *变量名;
②对象引用的声明格式: 类名 &引用名=对象名;
(3)访问对象成员的方法:
①格式1:对象名.公有成员名;
②格式2:指针变量名->公有成员名;
注意:在类外不能对类的private成员和protected成员进行直接访问,可以通过其他public成员函数访问。
3.this指针
this指针是一个特殊的隐含指针,它指向当前对象,代表了对象的地址。当对象调用类的非静态成员函数时,编译器会自动将对象本身的地址作为一个隐含参数传递给该函数,这个地址被一个隐含的形参this指针获取,函数调用中对对象成员的访问都隐含地被加上了前缀this->。
显式使用this指针的语法格式: this->成员名;
4.构造函数
(1)功能:创建对象时,实现对数据成员的初始化。
(2)定义格式: 类名(形参列表) { 函数体 }
(3)特点:
①构造函数是类的成员函数,可以直接访问类的所有数据成员。
②构造函数与类同名,可以是无参函数,也可以是有参函数。
③构造函数不允许有返回值类型,即使是void也不可以。
④构造函数可以重载,为对象提供不同的初始化方法。
⑤构造函数应为公有函数(public)。
⑥声明类对象时,可以在对象名后面的括号中提供初始化值。这些初始化值作为参数传递给类的构造函数。
⑦在创建类对象时,系统将自动调用构造函数以实现对数据成员的初始化。
⑧若类中没有显式定义构造函数,则编译时系统为该类提供一个默认的构造函数,它仅负责创建对象,不做任何初始化工作。在用默认构造函数创建对象时,若创建的是全局对象或静态局部对象,则对象的默认值是0,否则对象的初始值不确定。如果用户自定义了构造函数,则编译系统将不会再添加默认的构造函数。
⑨构造函数可以包含默认参数,这样即使对象定义时不提供初始值,对象也能利用默认参数进行初始化。一个类只能有一个带默认参数的构造函数,在使用时要防止二义性。
(4)拷贝构造函数
①功能:拷贝构造函数是一种特殊的构造函数,用于完成基于对象的同一类其他对象的构造及初始化。
②定义格式:
类名(const 类名 &参数名)
{ 函数体 }
③说明:
拷贝构造函数只有一个参数,即该类对象的引用;
拷贝构造函数的功能是用于实现对象值的拷贝,以完成对新对象的初始化,即用一个对象去构造另一个对象。
如果定义类时没有定义拷贝构造函数,编译系统会自动为类添加一个默认的拷贝构造函数,用以完成浅拷贝。
如果将对象作为函数的参数,或将对象作为函数的返回值,调用这两类函数时也会自动调用拷贝构造函数。
(5)对象的赋值:与变量的赋值类似,同类型的对象之间也可以进行赋值,对象的赋值是指对其中的数据成员赋值,不包括成员函数的赋值。
5.析构函数
(1)功能:
析构函数是类的一个特殊的成员函数,在类对象的生命期即将结束时由系统自动调用,用来在对象被删除前做一些清理善后工作和数据保存工作。其函数名是在类名前面加上代字符(~)。
(2)定义格式:
~类名()
{ 函数体 }
(3)特点:
①析构函数没有返回值,也没有参数。
②一个类中只能有一个析构函数,没有重载。
③如果定义类时没有提供析构函数,系统会自动创建一个默认的析构函数。当类对象中分配有动态内存时,则必须为该类提供适当的析构函数,以完成清理工作。
6.构造函数和析构函数的调用顺序
构造函数和析构函数是由系统自动调用的,函数的调用顺序取决于过程进入和离开对象范围的顺序。一般来说,析构函数的调用顺序与构造函数相反。
对象的存储类型可以改变析构函数的调用顺序。
全局对象的构造函数在程序开始执行之前调用,在程序结束时调用其析构函数;
自动局部对象的构造函数在执行到对象定义时被调用,其析构函数则在离开定义对象的块时被调用;
静态局部对象的构造函数在程序执行首次到达对象定义时调用一次,其析构函数在程序结束时被调用。
7.字符串类
(1)标准C++类库预定义了string字符串类,它封装了字符串的属性,并提供了访问属性的成员函数。利用string可以直接声明字符串变量,并能进行字符串的赋值、相加、比较、查找、插入、删除、取子串等操作。
(2)使用string类时必须包含头文件string,并使用命名空间std。
(3)常用方法和运算符。

8. 对象数组与对象指针

(1)对象数组
①概念:是指每一个元素都是相同类型的对象的数组。每个数组元素都是一个对象,不仅具有数据成员,还有成员函数。
②一维对象数组的定义格式: 类名 数组名[常量表达式];
③说明:
建立数组时需要调用构造函数,调用次数与数组长度一致。
使用对象数组时只能访问单个数组元素的成员,访问形式:数组名[下标].成员名
初始化对象数组:若构造函数只有一个参数,定义数组时可以直接在等号后面的花括号内给出初值列表。考虑初始化的各种需要,当各对象元素的初值相同时,可在类中定义无参构造函数或带默认参数的构造函数;当各对象元素的初值要求不同时,需定义带参(无默认值)构造函数。
带多个参数的构造函数初始化对象数组:定义对象数组时,只需在花括号内分别写出构造函数并指定实参即可初始化各对象元素。
当对象数组生命期结束时,系统会自动调用析构函数来完成扫尾工作。
(2)对象指针
①对象指针:是存放对象地址的变量,通过对象指针可以间接访问它所指向对象的成员。
②使用指针访问单个对象成员:
a.定义对象指针: 类名 *对象指针名;
b.给对象指针的赋值: 对象指针名=&对象名;
c.使用对象指针访问对象成员:
(*对象指针名).成员名 或 对象指针名->成员名
③使用对象指针访问对象数组的方法:
a.定义对象指针: 类名 *对象指针名;
b.为对象指针赋值:对象指针名=对象数组名;
c.通过对象指针访问对象数组各元素的成员:
对象指针名[下标].成员名 或 对象指针名->成员名 (对象指针名++;)
*

9. 向函数传递对象


①传值调用:调用函数时,系统创建形参对象,并自动调用拷贝构造函数完成实参对象向形参对象的值复制工作。函数调用过程中只能访问形参对象,函数中对形参对象的任何修改均不影响调用该函数的实参对象本身——单向传递。
②传址调用:调用函数时,创建形参指针变量,并将实参对象的指针传递给形参变量,函数调用过程中,通过形参可间接访问其所指对象(即实参对象),则对其所指对象的修改就是对实参的修改。
③引用调用:调用函数时,为实参对象声明引用(别名),不会为形参重新分配空间,也不会进行参数传递。函数调用过程中,通过形参可对其引用的对象(即实参对象)进行直接访问,则对其引用的对象的修改就是对实参对象的修改。可见,引用调用既具有传址调用可改变实参的特点,还具有调用时节省时间和空间、操作简单直接的特点。

10.常(const)类型

①常引用
声明常引用:在声明引用时用const修饰,则被声明的引用为常引用。如果用常引用作为函数形参,则不会产生对实参对象不希望的修改操作(避免对实参对象的更改),保证了数据的安全性,其作用效果与传值调用相同,却在调用中节省的时间和空间的消耗。
语法格式: const 类型标识符 &引用名=对象名;
②常数据成员
声明const数据成员: const 类型标识符 成员名;
说明:除构造函数外,任何其他函数不能对常数据成员进行赋值,并且在构造函数中只能利用初始化列表向常数据成员提供初始值。含有常数据成员的类的构造函数的定义格式为:
类名::类名(形参表): 数据成员名(值),……
{ 函数体 }
另外,对于类的引用数据成员,也应通过初始化表进行数据的初始化。
③常成员函数
声明常成员函数:需在函数声明和函数定义中分别指定其const属性,在调用时不必加const。
函数声明中指定const的限定格式:
类型标识符 函数名(参数表)const;
函数定义中指定const的限定格式:
类型标识符 函数名(参数表)const
{ 函数体 }
说明:
const成员函数的定义中,不允许修改类对象的数据成员。
常成员函数可以访问本类中的数据成员(普通/常),但不能修改其值,也不能调用本类的普通(非const)成员函数——保证常成员函数不会改变数据成员的值。
通过常对象只能调用它的常成员函数,而不能调用其普通成员函数——常成员函数是常对象唯一的对外接口。
关键字const可以用于区分重载函数。常成员函数可以用普通成员函数重载,编译器根据对象是否为const自动选择所用的重载函数。
常对象的构造函数和析构函数不需要const声明。将构造函数和析构函数声明为const是个语法错误。在常对象的构造函数中调用非const成员函数是合法的。
④常对象
声明常对象:声明对象时使用const修饰,其声明格式:
const 类名 对象名(初值表); 或 类名 const 对象名(初值表);
如: const CPoint point1(12, 0);
说明:
常对象必须通过构造函数进行初始化;
常对象的数据成员在对象的生存期内不能被修改,试图修改则会产生编译时错误
C++不允许常对象调用普通(非const)的成员函数。

11.静态成员

(1)静态数据成员
同一个类的不同对象都有自己的数据成员的空间,它们可以具有不同的属性值(实例属性),当类的所有对象需要共享一个成员副本时(类属性),可以通过声明静态数据成员来实现。
①定义格式:static 类型标识符 成员名;
②说明:
类的静态数据成员是同一类的所有对象共有的。不管有多少个对象,静态数据成员只有一个,同时可被任何一个同类对象访问。
在一个类对象的空间内不包含静态数据成员的空间,其所占的空间不会随对象的产生而分配,或随对象的撤销而消失。它是在程序开始时被分配的,即使没有创建类的一个对象。
静态数据成员不具体属于哪一个对象,不能在构造函数或其它成员函数中初始化。必须初始化且在类外进行,其语句不属于任何类、任何函数,其在文件范围内只能初始化一次,格式为:
类型标识符 类名::静态数据成员名=初值;
对于在类的public部分声明的静态数据成员,可以不使用成员函数而通过对象直接访问,若未定义类的对象,需用类名指明。访问格式为:类名::成员名
在private和protected部分声明的静态数据成员只能通过类的成员函数访问。若未定义类的对象,则需提供一个类的静态成员函数,并在调用时使用类名和作用域运算符指定。
(2)静态成员函数
①定义格式:其定义与一般成员函数的定义相同,只是在其前面需加上static修饰符。
②说明:
即使在类没有实例化任何对象时,类的静态数据成员和静态成员函数就已经存在并可使用。
与静态数据成员一样,静态成员函数是独立于类对象而存在的,它不与类的对象相关联。静态成员函数没有this指针。访问静态成员函数时不需对象,但需在函数名前加上类名和作用域运算符。
一个静态成员函数不与任何对象关联,因此,它不能对类中的非静态成员进行默认访问。解决办法:将对象作为静态成员函数的参数,在函数中通过对象访问其非静态成员。

12.对象成员(组合类)

(1)定义:
组合类是指一个类将其他类对象作为自己的数据成员。其定义格式为:
class X
{ 类名1 成员名1;
类名2 成员名2;
……
};
(2)组合类构造函数的定义形式:
如果其成员对象的构造函数为有参函数时,组合类对象的构造函数参数表需指定传递给成员对象的参数,并利用初始化列表的方式显式调用对象成员的构造函数。其定义形式为:
类名::类名(对象成员形参,基本类型成员形参):对象成员1(形参表1),对象成员2(形参表2),…
{ 基本类型成员的初始化 }
(3)说明:
①组合类对象的构造函数首部中,各成员对象的初值表均来自于构造函数的形参表。
②组合类对象按照由内到外的顺序构造,按照由外到内的顺序删除。
③若一个类在定义中将它的成员对象指定为public,将不会破坏该成员对象的private成员的封装和隐藏。
④构造组合类对象的顺序:系统首先调用内嵌成员对象的构造函数,依次初始化成员对象;然后再执行组合类对象构造函数的函数体,对其他的非对象数据成员进行初始化。若该组合类对象含有多个内嵌成员对象,则按类定义时的声明顺序构造成员对象,而不是按照初始化表中的顺序。如果成员对象不需要提供初始值,系统将隐式调用其默认构造函数。
(4)组合类的拷贝构造函数:
如果没有为组合类定义一个拷贝构造函数,编译系统会自动为组合类添加一个默认的拷贝构造函数,其功能是:自动调用内嵌对象的拷贝构造函数,对各内嵌对象成员进行初始化。组合类拷贝构造函数的定义格式如下例:
C::C(C &c):a(c.a)
{ … }

第4章继承与派生

1.继承的基本概念
继承是面向对象程序设计的一个重要特性,它允许在已有类的基础上创建新的类。新类继承了现有类的属性和方法(不必重新编写代码),同时又添加了自己的新特性,从而在继承的基础上实现扩充。
2.基类与派生类:
在继承关系中,被继承的原有类称为基类(base class),又称为父类或超类;通过继承关系定义出来的新类被称为派生类(derived class),又称为子类。
(1)单继承派生类的定义格式:
class 派生类名 : [继承方式] 基类名
{ 派生类中新成员的声明 };
(2)说明:
①“继承方式”可以是public、pretected或private方式,若缺省,则默认为private。通过它可以实现派生类对继承自基类的成员的访问控制,通常多使用public继承方式。
②基类必须是已定义的一个类。
③派生类首部列出的是派生类的直接基类。
④派生类中成员:
继承基类的成员;
增加新的成员;
重新定义继承自基类的成员。
3.派生类对基类成员的访问控制
(1)说明:
①无论是哪种继承方式,基类的private成员在派生类中都是不可见的。这符合数据封装和信息隐藏的思想。
②派生类虽然不能直接访问基类的private成员,但可以通过基类的public或protected成员函数间接访问。
③不同继承方式的影响主要体现为:
派生类成员对基类成员的访问控制(类域内内部访问)
派生类对象对基类成员的访问控制(类域外对象访问)
(2)私有继承的访问规则:
基类中的成员 公有成员 保护成员 私有成员
访问方式 内部访问 可访问 可访问 不可访问
对象访问 不可访问 不可访问 不可访问
(3)公有继承的访问规则:
基类中的成员 公有成员 保护成员 私有成员
访问方式 内部访问 可访问 可访问 不可访问
对象访问 可访问 不可访问 不可访问
(4)保护继承的访问规则:
基类中的成员 公有成员 保护成员 私有成员
访问方式 内部访问 可访问 可访问 不可访问
对象访问 不可访问 不可访问 不可访问
4.同名成员:同名覆盖
在派生类中可以对从基类中继承的成员函数进行重新定义,使之满足派生类的具体需要。——同名覆盖
通过派生类对象调用一个被重定义过的基类成员函数,被调用的是派生类的成员函数,此时若想调用基类的成员函数,必须使用基类名和作用域运算符加以限定。
5.派生类的构造函数和析构函数
派生类不能继承基类的构造函数和析构函数。由于派生类继承了基类成员,在建立派生类的对象时,必须先调用(隐式或显式)基类的构造函数来初始化派生类对象的基类成员。
(1)派生类构造函数:
①定义格式:
派生类名::派生类构造函数名(形参表):基类构造函数(参数表)
{ 函数体 }
②说明:
调用基类构造函数的参数表来自于派生类构造函数的参数表中。
若在基类中没有定义任何构造函数,则派生类的构造函数定义时可省略对基类构造函数的调用,在创建对象时,系统将隐式调用基类的默认构造函数;如果基类构造函数存在,且为有参函数,则必须定义派生类的构造函数。
③构造函数的调用顺序:
a)在创建派生类对象时,系统首先调用基类的构造函数,初始化派生类中的基类成员;
b)若派生类包含对象成员,则调用对象成员的构造函数。若包含多个成员对象,其调用顺序按照它们在类中的声明顺序;
派生类名(参数总表):基类名(参数表0),对象成员名1(参数表1),….,对象成员名n(参数表n)
c)调用派生类的构造函数,初始化派生类中的新增数据成员;
(2)派生类析构函数
①定义方法与普通类的析构函数相同:
~派生类类名() { 函数体 }
②派生类析构函数的工作:完成派生类对象的清理工作
③析构函数的调用顺序与构造过程相反:
a)调用派生类的析构函数,清理普通成员;
b)调用对象成员的析构函数,清理对象成员;
c)调用基类的析构函数,清理自基类继承来的成员。
6.基类与派生类对象之间的赋值兼容关系
(1)规则:在需要基类对象的任何地方,都可以用公有派生类的对象替代,只能使用从基类继承来的成员,但反之则不被允许。
(2)具体体现:
①派生类对象可以向基类对象赋值,即用派生类对象中从基类继承来的数据成员逐个赋值给基类对象的数据成员。
②派生类对象可以初始化基类对象的引用。
③派生类对象的地址可以赋给指向基类对象的指针,即指向基类的指针也可以指向派生类对象。
(3)注意:
通过基类对象名、指针访问派生类对象时,只能访问从基类继承的成员。
(4)基类指针、派生类指针、基类对象、派生类对象的混合匹配有以下四种情况:
①直接用基类指针指向基类对象。
②直接用派生类指针指向派生类对象。
③用基类指针指向其派生类对象。这种方式是安全的,但基类指针仅能访问派生类对象的基类部分。
④用派生类指针指向基类对象。这种方式不被允许,会导致语法错误。可以通过强制类型转换将基类指针转换为派生类指针,实现对基类对象的访问,但不安全,程序员需正确使用指针。

7.多继承

(1)多重继承派生类(多基派生类)的定义格式:
class 派生类名 : 继承方式 基类名1,继承方式 基类名2,…,继承方式 基类名n
{ 派生类中新成员的声明 };
首部出现的每一个“继承方式”,只用于限制对紧随其后的基类的继承。
首部出现的基类均为该派生类的直接基类。
派生类定义中的基类顺序决定了基类成员的构造顺序
生类的成员:继承的各基类成员+派生类增加的新成员
(2)多基派生类构造函数的定义格式:
派生类名::派生类名(形参表):基类名1(参数表1),
基类名2(参数表2),……,基类名n(参数表n)
{ 函数体 }
说明:当基类中定义有缺省参数的构造函数或未定义构造函数时,派生类构造函数的定义中可以省略对基类构造函数的调用。当基类中定义了有参构造函数时,派生类必须定义构造函数,建议是带参构造函数,提供将参数传递给基类构造函数的途径。
(3)多基派生类构造函数的调用顺序
多重继承派生类构造函数的调用顺序与单继承相同:
①先执行各基类的构造函数,处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指的各基类顺序。
②然后执行对象成员的构造函数;
③最后执行派生类的构造函数。
(4)多基派生类析构函数的调用顺序:与构造函数严格相反。
(5)二义性
①同名成员:
当一个派生类的多个直接基类声明了同名的成员时
解决方法:用基类名和作用域运算符来限定
②多基类为同一基类派生:
当派生类从多个基类派生,而这些基类又是从同一个基类派生而得,则在访问此共同基类的成员时
解决方法:用基类名和作用域运算符来限定、采用虚基类来解决
③虚基类
虚基类并不是一种新的类型的类,而是一种派生的方式。如果采用虚基类的方式定义派生类,在创建派生类对象时,类层次结构中某个虚基类的子对象只被保留一个。
将一个基类声明为虚基类,必须在定义各直接派生类时,在指定基类名前面加上关键字virtual,定义格式:
class 派生类名:virtual 继承方式 基类名
{ 声明派生类成员 };
第5章 多态性和虚函数
1.多态性
(1)概念:
是指同样的消息被不同对象接收时导致完全不同行为的现象。这里的“消息”即对类成员函数的调用。(以相同的指令却调用了不同的函数)
(2)种类:
①编译时的多态性:静态联编
不论用类对象、对象指针或对象引用哪种形式访问成员函数(非虚函数),编译系统都根据它们的定义类型,在编译阶段进行静态绑定。其方式为:
通过在类中重载成员函数实现
通过在派生类中重定义基类成员函数实现
②运行时的多态性:动态联编
运行时的多态性是指在程序运行时根据运行所产生的信息决定调用哪个函数,它是在运行过程中发生的,编译系统在编译时是无法确定的,通过虚函数实现。
2.虚函数的定义与调用
(1)虚函数实现多态性的过程:
首先将基类中的某成员函数声明为虚函数,然后在每个派生类中重新定义此虚函数。当客户通过基类指针(或引用)请求调用虚函数时,C++会在程序运行时根据所指对象的不同,自动地选择与之关联的派生类函数,实现调用。
(2)基类中声明虚函数的方法:
class 类名
{ ……
virtual 类型 函数名(参数表);
……
};
(3)说明:
①只有类的成员函数才能被声明为虚函数,其他全局函数和静态成员函数、类的构造函数不能声明为虚函数。
②若虚函数在类外定义,则定义时不能再加virtual关键字
③虚函数可以在多个派生类中被重定义,它属于函数重载的情况。但与一般函数重载不同的是,它要求在派生类中重定义时的虚函数必须与基类定义中的虚函数原型完全相同。否则,按普通函数重载看待。
④一旦一个基类成员函数被声明为虚函数,它从该点之后的继承层次结构中都是虚函数。若派生类需重定义该虚函数,声明时可省略virtual关键字。
⑤没有重定义虚函数的派生类简单地继承其直接基类的虚函数。
⑥C++只对调用的虚函数进行动态绑定,而普通函数则是静态绑定。
3.虚析构函数
(1)虚析构函数解决的问题:
若基类指针指向的派生类对象是使用new运算动态生成的,如果没有将析构函数定义为虚函数,则在使用delete运算删除该派生类对象时,只会调用基类析构函数,而没有调用派生类的析构函数,从而无法完成正确的对象清理工作。
(2)说明:
如果将基类的析构函数定义为虚函数,由该基类所派生的所有派生类的析构函数也都自动成为虚函数。
4.纯虚函数与抽象类
(1)纯虚函数:
只在基类中声明为虚函数,但未给出具体的函数定义。纯虚函数的声明形式:
virtual 类型 函数名(参数表)=0;
(2)抽象类:包含纯虚函数的类称为抽象类。
(3)说明:
不能建立抽象基类的实例化对象,但可以声明指向抽象基类的指针变量或引用,用以实现虚函数的运行时多态。
抽象类中可以有多个纯虚函数。
抽象类也可以定义其他非纯虚函数。
如果在派生类中没有重新定义纯虚函数,则必须再次将该虚函数声明为纯虚函数,此时这个类仍是一个抽象类。
从抽象类中可以派生出具体类或抽象类,但不能从具体类派生出抽象类。