人类的问题求解思路,用计算机语言描述出来,就是程序(program),描述的过程就是编写程序,简称编程(programming)。计算机根据程序的指示按部就班的工作,就是执行程序,通过执行程序我们就可以得到问题求解的最终结果。
程序是人类的问题求解思路,人类有什么样的问题求解思维,计算机语言就应该提供相应的语法工具来支持问题求解思维的描述。针对过程化、面向对象、泛型三种问题求解思维,C++语言提供了过程化、面向对象、泛型编程的系列语法工具。
1. 过程化思维与过程化编程
过程化问题求解思维认为世界是事物状态发展变化的过程,求解问题就是对事物进行加工处理,促使其状态发生一系列变化,从而达到预期的目标状态。
过程化编程,就是在过程化思维下描述人的问题求解思路。因此,过程化编程,就需要描述数据以及数据的加工处理过程,包括:
(1)描述数据:说明求解问题需要哪些数据,这些数据具有什么样的规格;
(2)描述数据处理过程:说明数据如何输入、如何加工处理、以及如何输出处理结果的过程,包括输入数据、处理数据和输出结果三个环节。
1.1 描述数据
描述数据,就是告诉计算机关于待处理数据的一些规格信息,包括:
(1)数据在存储器中占据多大的存储空间;
(2)数据如何转化成存储器要求二进制形式(编码方式);
(3)如何把存储器中二进制形式恢复成数据(解码方式);
(4)数据可以进行哪些运算处理。
1.1.1 数据类型:约定数据规格信息
C++语言通过数据类型的概念来约定数据的规格信息。在C++语言中,提供了一系列预先约定的数据类型,主要包括三类(如图 1所示)。
(1)内置数据类型:描述常用的整型、字符型、浮点型数据;
(2)复合数据类型:描述常变量、数组、指针、引用等复合数据;
(3)定制数据类型:定制自己的数据类型,建立我们自己关于存储空间大小、编码和解码方式、及其可接受运算处理行为的数据规格约定。
C++语言 数据类型 系统 |
1.1.2 描述不变数据:字面量
在C++语言中,根据形式和用途,将字面量分成五种类型:整型字面量、字符型字面量、浮点型字面量、布尔型字面量、字符串型字面量。
不同类型的字面量如何构造:
(1)整型字面量:157、-0655、0xFE、-0x1UL;
(2)字符型字面量:'C'、'$'、'\n'、'\x61'、L'学'、L'a';
(3)浮点型字面量:0.2、0.2f、0.2E-01;
(4)布尔型字面量:true、false;
(5)字符串型字面量:"英语学习"、L"ABC"、"x41\x42\x43"。
1.1.3 描述可变数据:变量
变量是约定了名字和数据类型的存储空间,通俗讲就是放置数据的容器。
变量 = 变量名 + 数据类型 = 数据地址 + 数据类型。
通过变量的名字我们可以方便地对数据进行存取访问。
(1)变量如何定义:数据类型 + 名字。如何给变量取名字。
(2)变量如何初始化:赋值语法、构造函数语法。
(3)访问变量:访问变量的容器、访问变量中的数据值、访问变量的地址。
1.1.4 描述复合数据
在内置数据类型的基础上,C++语言引入复合数据类型。对于这些复合数据类型的变量,我们需要关注:该类型变量的用途和适用场合、如何定义和初始化变量、变量的存储空间映像、如何使用这类变量。
(1)常变量:又称符号常量,描述内容不可修改的变量,用于替代字面量;
const double PI = 3.142; //或者,
const double PI(3.142); //或者,
double const PI = 3.142; //交换const和double的次序
(2)数组变量:用于描述类型相同的一组变量,存储相同类型的一组数据;
n 定义数组变量:元素类型 数组变量名[元素个数];
n 设定数组元素的个数:整型+常量;
n 数组的存储空间映像:sizeof(数组名) = sizeof(数组类型)
= sizeof(数组元素类型)*数组元素个数
n 计算数组元素的地址:表达式中数组变量名求值为数组首元素地址;
n 初始化数组变量:初始化列表{};
n 访问数组元素:下标运算,越界访问;
n 多维数组:元素是数组的数组变量;
n 多维数组的访问:多个下标运算;
(3)指针变量:用于存放其它变量的地址,通过指针间接操作其它的变量;
n 定义指针变量:数据类型 * 指针名;
n 指针变量的存储空间映像:指针变量和指针所指变量的关系;
n 指针的访问和操作:访问指针变量的容器、内容和地址;
n 访问指针所指变量:解引用运算;
n 指针变量与整数的加法和减法运算:求值结果是地址;
n 指针与数组:指针执行数组首元素,指针就会变身数组;
访问数组元素:数组+下标、数组+解引用、指针+解引用、指针+下标。
n 无类型指针:可以存放任何类型变量的地址;
n 指针与动态变量:
定义动态变量:数据类型 * 指针名 = new 数据类型(初始值);
销毁动态变量:delete 指向动态变量的指针;
n 指针与动态数组:
定义动态数组:元素类型 * 指针变量名 = new 元素类型[元素个数]
销毁动态数组:delete[] 指向动态数组首元素的指针;
n 设定动态数组的元素个数:允许通过变量设定。
(4)字符串变量
n 字符数组实现字符串:char str[] = "Hi, Stop";
n 字符指针实现字符串:char *pStr = "Hi, Stop";
n 字符串操作函数:strlen、strcpy、strcat、strcmp、strstr
n string类:#include <string>
(5)引用变量:用于给其它变量取一个的别名,通过引用操作其它变量;
n 引用定义:数据类型 &引用名 = 目标变量;
n 引用作为函数参数:两种作用(a)通过修改形参可以实现对实参的修改;(b)与实参变量共享其存储空间,节省开销;
(6)常量数组,数组元素是常变量的数组;
n const int week[] = {0, 1, 2, 3, 4, 5, 6};
n 枚举更好:enum {Sun, Mon, Tue, Wen, Thur, Fri, Sat};
(7)指针数组,数组元素是指针变量的数组;
n double* *pdpAry = new double*[5];
n 动态数组必须通过delete[]运算销毁掉。
(8)数组指针,指向数组的指针;
n double* (*aryPointer)[5] = &dpAry;
(9)常量指针:指向常变量的指针;
const double PI = 3.142;
const double *pConst = &PI;
(10)指针常量:指针型的常变量;
double dVal = 1.1;
double * const cpVal = &dVal; //不可修改的指针
(11)指向常量的常指针
const double PI = 3.142;
const double * const cpVal = &PI; //指向常变量的常指针
(11)多维数组:数组元素是数组的数组;
(12)多级指针:指向指针的指针;
int iMyAge = 30, *pMyAge = &iMyAge;
int* *ppMyAge = &pMyAge;
int** * pppMyAge = &ppMyAge;
(13)指针引用:给指针起的别名;
int **p2d; //二级指针p2d
int** &refPtr = p2d;
(14)数组引用:给数组起的别名;
int a2d[3][50]; //二维数组a2d
int (&refAry)[3][50] = a2d;
(15)常量引用:给常变量起的别名。
const double PI = 3.142;
const double &refPI = PI;
typedef int* IntPtr;
typedef IntPtr* IntPtr2rd; //二级指针
typedef IntPtr2rd &IntPtr2rdRef; //二级指针的引用
typedef double* AryOfDblPtr[5]; //指针数组
1.1.5 复合数据与函数
(1)数组用作函数参数
n C++语言约定:数组用作函数形参时会退化为数组元素类型的指针,数组元素的个数信息就会损失掉;
n 此时给函数增加一个专门的形式参数,用来传递形参数组的元素个数。
int max(int numbers[], int N );
(2)函数如何返回数组名
数组名求值为数组首元素的地址,返回数组名其实是返回指向数组首元素的指针。C++语言不允许数组作为函数的返回类型,而是用指针替代。
(3)指针函数
指针函数,等同于函数返回数组名。函数返回数组名,实际上返回了指向数组首元素的指针,故指针可以用作为函数的返回类型。
(4)函数指针:指向函数的指针,通过函数指针可以间接调用函数。
n 函数的入口地址:C++语言约定,函数名代表了函数的入口地址;
n 函数的类型信息:由函数的形式参数类型及其返回类型来共同约定;
n 定义函数指针:
int max(int numbers[], int N); //查找最大元素
int (*pMax) (int[], int) = max;
n 调用函数指针:
int nums[] = {25, 32, 41, 5, 78, 44, 31};
int pos = pMax(nums, sizeof(nums)/sizeof(int));
(5)函数指针的数组
n 定义函数指针的数组:
int max(int numbers[], int N); //查找最大元素
int min(int numbers[], int N); //查找最小元素
int absmax(int numbers[], int N); //查找绝对值最大元素
int absmin(int numbers[], int N); //查找绝对值最小元素
int (*findelem[])(int[], int) = {max,min,absmax,absmin};
n 通过函数指针的数组批量调用函数:
int nums[] = {25, 32, 41, 5, 78, 44, 31};
for(int i=0; i<sizeof(findelem)/sizeof(FINDELEM); i++)
int pos = findelem[i](nums, sizeof(nums)/sizeof(int));
n 函数指针用作函数参数,实现通用函数。
1.2 描述数据处理过程
数据的加工处理过程,包括最基本的运算处理,复杂一点的流程控制,数据输入输出处理、以及把很多加工处理过程打包封装起来的函数。
1.2.1 基本的运算处理:运算符和表达式
表达式,是一个或多个操作数通过运算符连接而成的运算序列,用来描述一个简单的运算处理过程。C++语言中表达式的功能有两个:
(1)运算求值产生结果:求值结果是左值或者右值。其中,左值表示一个可解释的存储空间,右值表示一个临时的计算值。
(2)产生副作用:即表达式在运算求值之外的作用,修改变量就是一种副作用。
表达式需要约定操作数和运算符绑定或者结合的先后顺序,C++语言通过运算符的优先级和结合性来规定表达式的结合顺序。其中,优先级高的运算符先结合,优先级相同时,则按照结合性从左向右或者从右向左进行结合。
注意,优先级和结合性只是规定运算符和操作数的结合次序,而非求值次序,求值次序通过时序点来保证。对于多个并列的子表达式,C++语言通常不约定子表达式的求值顺序,逻辑与&&、逻辑或||、条件运算符?:、以及逗号运算符等是例外。
当参与表达式运算的多个操作数的数据类型不一致时,会发生类型转换。C++语言提供隐式类型转换和显式类型转换两种方式。
在C++语言中,提供了丰富的运算符(又称操作符),用于构造各种类型的表达式,描述针对不同类型数据所进行的运算处理。
(1)算术运算:加(+)、减(-)、乘(*)、除(/)、取余(%)、++、--
(2)逻辑运算:逻辑非(!)、逻辑与(&&)、逻辑或(||)
(3)关系运算:等于(==)、>、>=、<、<=、不等于(!=)
(4)位串运算:位非(~)、位与(&)、位或(|)、位异或(^)、左移(<<)、右移(>>)
(5)赋值运算:=、+=、-=、*=、/=、%=、&=、|=、<<=、>>=、^=
(6)其它运算:.、->、[]、()、.*、->*、typeid、const_cast、dynamic_cast、reinterpret_cast、static_cast、sizeof、new、delete、,、?:
1.2.2 控制数据处理流程
C++语言提供了选择、循环和跳转三类语句,用于控制程序的流程逻辑。
(1)选择(“如果……就”):if语句、switch语句;
(2)循环(“如果……就反复执行”):while、do-while、for循环语句;
(3)跳转:break、continue、return、goto。
1.2.3 数据输入输出处理
数据输入,指的是将外部输入设备中的数据放入存储器;而数据输出,则指的是将存储器中的数据写入外部的输出设备。C++语言通过“流”来处理输入输出。
(1)标准输入流:绑定到键盘的输入流对象cin,支持键盘输入;
(2)标准输出流:绑定到显示器的输出流对象cout,支持屏幕输出;
(3)文件输入流ifstream:绑定到特定磁盘文件,支持读文件;
(4)文件输出流ofstream:绑定到特定磁盘文件,支持写文件;
(5)文件输入输出流fstream:绑定到特定磁盘文件,支持读写文件;
可以通过设置格式标记或使用格式操作子,控制输入输出的数据格式。
1.2.4 制造数据处理机器:函数
函数,是将数据处理过程封装成一个逻辑独立、可重用的语法单元,相当于制造一个数据处理机器。
(1)定义函数,就是制造数据处理机器,需要说明函数四要素:
返回类型 函数名(形式参数表) {函数体}
n 函数名,代表函数机器的名字,必须符合标识符的构词规则。
n 形式参数,简称形参,表示将来会输入函数机器的待处理数据。
n 返回类型,函数机器运转产生的结果数据需要通过return语句反馈回去,称为返回值,返回值的数据类型,简称返回类型。
n 函数体,表示函数机器运转的过程,描述函数所封装的数据以及数据处理过程。
(2)调用函数:就是使用数据处理机器,指定机器名和实际参数:
函数名(实参列表)
n 实参,即实际参数,是在使用数据处理机器时指定的实际要处理的数据。
n 形参,即形式参数,是在制造数据处理机器时指定的未来会存在的数据。
(3)函数声明
C++语言允许函数先调用后定义,但是,它要求在调用函数之前,必须进行函数声明以保证函数的存在性。定义包含声明,一次定义,多次声明。
n 函数定义 = 返回类型 + 函数名 + 形参表 + 函数体
n 函数声明 = 返回类型 + 函数名 + 形参表
(4)函数调用的执行流程:实参求值->流程跳转->形参生成->被调函数执行->流程回转->主调函数重启。
(5)形参生成:调用函数,需要根据实参产生形参。两种形参变量的生成方式:
n 克隆方式:根据实参克隆出形参,形参的变化不影响实参;
n 别名方式:形参是实参的别名,修改形参就是修改实参;
(6)函数的嵌套调用和递归调用
(7)函数内联:函数体在调用点展开,节省函数调用的开销。
(8)函数重载:函数的名字可以重复利用,但必须保证形式参数不同,即函数重载。在函数调用时,编译器会选择与实参类型和个数匹配最好的重载函数来调用。
(9)函数的默认参数值:通过函数声明设定参数的默认值。
1.3 结构化编程与程序组织
通过函数,可以比较轻松地应付规模较大问题的编程求解。在求解较大问题时,如果遇到其中一些环节或者子问题暂时难以处理,此时,可以通过函数声明,假设存在某个函数可以解决这些子问题,即使它们暂时还不存在;然后通过调用这个函数完成问题的整体编程求解。之后,再在合适的时机给出这个函数的定义。
自上而下将复杂问题不断细分,产生一系列更易于求解的子问题,然后再针对这些子问题进行编程求解的思维,就是结构化编程思想,或者说,自顶向下、逐步求精的编程思想,或者分治计算思维。
1.3.1 函数声明的组织方式
(1)就地声明,非常适合于编写规模较小的“玩具”程序。
(2)在源文件开始处集中声明。
(3)通过头文件集中声明。
n 定义头文件,生成一个独立的文件,文件扩展名通常为*.h,在其中放置函数或者变量的声明。
n 包含头文件。如果在编写程序时需要声明头文件中的函数,则只需要把该头文件通过#include预编译指令包含进源文件中即可。
#include <头文件名> //包含标准头文件
#include "头文件名" //包含用户头文件
n 避免头文件重复包含:利用#ifndef、#endif和#define进行条件编译。
1.3.2 程序的多文件组织结构
头文件以及独立编译机制,非常有利于团队协作进行程序开发,也导致了将程序文件按功能逻辑进行分割和组织的多文件程序组织结构。为了避免多文件之间程序实体的冲突以及共享,C++语言约定了一系列的语法机制,包括:
(1)作用域:程序实体存在的空间范围,包括局部域、名字空间域、类域、以及文件域。其中,局部域有复合语句的块域、函数域、以及函数声明域。
(2)生存期:程序实体存在的时间范围,包括静态、自动和动态生存期。C++语言允许在不同的作用域定义不同生存期的变量。
n 局部域+静态生存期:局部静态变量
n 局部域+自动生存期:自动变量
n 文件域+静态生存期:全局变量
n 作用域+动态生存期:动态变量
(3)链接属性:多文件之间程序实体的保护和共享;
n 内部链接性:static修饰,不允许共享文件域中的程序实体。
n 外部链接性:extern修饰,允许程序实体延伸到所在文件域的外部。
(4)名字空间:约定了一个命名的作用域,并将其成员隐藏在该作用域中,使其对外不是直接可见的,从而避免了全局程序实体与外界发生冲突。
n 定义名字空间:namespace 名字空间的名字 { }
n 访问名字空间成员:名字空间的名字::成员名
n 使用头文件引入声明
n 使用using指示符:
using 名字空间名::成员名;
using namespace 名字空间名;
n C++标准名字空间:std
2. 面向对象思维与面向对象编程
面向对象问题求解思维认为世界是由一系列对象组成的,每种类型的对象都具有自身的属性和行为能力。找到一组合适的对象,通过对象之间的沟通协作发挥各自的行为能力,就可以实现问题求解。
面向对象编程,就是在面向对象思维下描述人的问题求解思路。因此,面向对象编程,就需要描述问题求解的对象以及对象之间的沟通协作过程:
(1)描述对象:说明求解问题需要哪些对象,这些对象应该具有什么样的规格要求(包括具有哪些属性和行为能力);
(2)描述沟通协作过程:说明如何产生具体的对象,以及这些对象之间如何沟通协作发挥各自的行为能力实现问题求解的过程。
对象之间的沟通协作过程,可以利用过程化编程的语法工具来描述。面向对象编程的核心工作是描述对象以及如何产生具体对象并发挥对象的行为能力。
2.1 描述对象
描述对象就需要完成两方面的工作:(a)定义类,描述一类对象共有的属性和行为特征;(b)产生对象,根据类生成具体的对象实例。
2.1.1 定义类
类描述了具有相同属性和行为能力的一类对象,称为对象类型,简称类。在类中,对象的属性称为类的数据成员,对象的行为能力称为类的成员函数。
(1)定义类的语法
class 类名 {
//1. 数据成员:描述类的属性特征,变量语法(数据类型+名字)
//2. 成员函数:描述类的行为特征,函数语法
}; //注意结尾的分号
(2)类成员封装:三种访问限定符
n private修饰的类成员,私有成员,只有在类自身的成员函数中才能访问对象的私有成员;
n public修饰的类成员,公有成员,在任何函数(包括成员函数和非成员函数)中都可以访问对象的公有成员;
n protected修饰的类成员,保护成员,可以在类自身成员函数及其派生类的成员函数中访问对象的保护成员。
(3)在类体中定义成员函数,在类体外部定义成员函数,成员函数的内联。
(4)类是定制的数据类型,约定了一类对象应该具有的属性和行为特征,同时约定了该类对象的存储空间大小、编码和解码方式以及可以接受的运算和处理行为。
类 = 属性特征 + 行为特征
= 数据成员 + 成员函数
= 公有成员 + 私有成员
2.1.2 产生对象
(1)产生对象的语法,与产生变量类同。
n 变量 = 名字 + 数据类型 = 地址 + 数据类型
n 对象 = 名字 + 类 = 地址 + 类
(2)访问对象成员
n 在外部函数中访问对象成员:只能访问对象的公有成员,可以使用点号或箭头语法进行访问。
对象名.成员名
对象指针->成员名
n 在类成员函数中访问另一个同类对象的成员:可以访问另一个同类对象的公有、私有和保护成员,可以使用点号或箭头语法进行访问。
n 在类成员函数中访问自身对象的成员
成员名
this->成员名
2.1.3 类成员:约定对象的属性和行为
(1)类的数据成员:约定对象的属性特征。
n 常量数据成员:const修饰的数据成员,在对象生命期中不会改变;
n 对象成员:用户定制数据类型的数据成员;
n 静态数据成员:static修饰的数据成员,所有对象共享的数据成员。
n 数据成员的初始化:构造函数。
(2)类的成员函数:约定对象的行为特征。
n 常量成员函数:const修饰的成员函数,描述不修改数据成员的成员函数;
n 静态成员函数:static修饰的成员函数,所有对象共享的成员函数;
n 构造函数:约定对象产生的行为;
n 析构函数:约定对象消亡的行为;
n 操作符重载函数:约定对象运算的行为;
2.1.4 对象产生和消亡的时机
(1)局部域 + 自动变量/对象
在变量/对象的定义点产生(从栈区获得存储空间),在离开局部域时消亡。
(2)局部域 + 静态变量/对象
在程序第一次执行到其定义点时产生(从静态数据区获得存储空间并进行初始化工作),此后持续存在,直到程序执行结束时消亡。
(3)全局变量/对象
在程序开始执行时就产生(从静态存储区获得存储空间并进行初始化工作),此后持续存在,直到程序执行结束时消亡。
(4)动态变量/对象
调用new运算符时产生(从堆区获得存储空间),调用delete运算符时消亡。
2.1.5 构造函数:约定对象产生的初始化行为
(1)构造函数(constructor),是专门用于对象初始化的成员函数,由系统在对象产生时自动调用,并且在该对象从产生到消亡的一生中就只会调用这一次。
(2)定义语法:
类名( 形参列表 ) 《: 成员初始化列表》 { …… }
(3)构造函数分类:
n 默认构造函数:无参数,没有提供任何初始状态信息;例如,
CDate now; //调用默认构造函数
n 普通有参构造函数:有参,提供了一些初始状态信息;例如,
CDate today(2008, 5, 1); //调用有参构造函数
n 拷贝构造函数:单参数,提供了另一个同类对象;例如,
CDate birthday = today; //调用拷贝构造函数,或者
CDate birthday(today);
n 转换构造函数:单参数,提供了另一个非同类对象;例如,
CDate tomorrow = "2005-11-5"; //调用转换构造函数,或者
CDate tomorrow("2005-11-5");
(4)定义默认构造函数。若不提供,系统合成一个默认构造函数。
CDate::CDate( ) { year=1900; month=1; day=1; }
//CDate::CDate( ) : year(1900), month(1), day(1) {}
(5)定义普通有参构造函数
CDate::CDate(int y, int m, int d):year(y),month(m),day(d){}
(6)定义拷贝构造函数
CName(CName &clone_me) { ……} //或者,
CName(const CName &clone_me) { ……} //或者,
CName(CName &clone_me, int n=10) { ……}
(7)默认拷贝构造函数:
n 不提供拷贝构造函数,编译器会合成一个默认的拷贝构造函数。
n 默认拷贝构造函数的行为:递归地为所有基类和对象数据成员调用拷贝构造函数,或者说按成员拷贝的方式进行初始化,等效于用源对象的每个数据成员来初始化目标对象的对应数据成员。
(8)何时调用拷贝构造函数
n 在需要通过同类对象克隆产生新对象的任何时候,调用拷贝构造函数。
n 常见两种情况:(a)函数按值返回一个对象;(b)函数调用时克隆生成形参对象。
(9)转换构造函数:在类型转换时调用。
CDate(string date); //转换构造函数
(10)显式转换构造函数:只有在显式类型转换时才会调用。
explicit CDate(string date); //转换构造函数(显式)
(11)转换成员函数
operator 转换的目标类型名();
2.1.6 析构函数:约定对象死亡的善后行为
(1)析构函数,专门用于约定对象消亡时的善后清理行为。
CDate::~CDate( ) { …… }
(2)对象的深拷贝与浅拷贝:实现深拷贝需要提供4个特殊的成员函数:
n 构造函数:为指针分配存储空间,或者将其置为空(NULL);
n 析构函数:释放动态分配的存储空间;
n 拷贝构造函数:拷贝动态分配的存储空间;
n 赋值操作符重载函数:拷贝动态分配的存储空间。
2.1.7 操作符重载函数:约定对象运算的行为
(1)重载操作符,可以采用成员或者非成员重载方式,其定义语法形式分别为:
返回类型 类名::operator操作符(形参列表) { …… } //成员形式
返回类型 operator操作符(形参列表) { …… } //非成员形式
(2)对于操作符#,将其运算表达式转换为等价的直接函数调用形式,即可反向推演出操作符重载函数的声明原型。以二元操作符减法为例,
n 运算表达式:obj1 - obj2,
n 等价的成员函数调用形式:obj1.operator-(obj2)
n 等价的非成员函数调用形式:operator-( obj1, obj2)
(3)各种操作符的运算表达式及其成员和非成员调用
运算符 | 运算表达式 | 成员调用 | 非成员调用 |
二元 | obj1 # obj2 | obj1.operator#(obj2) | operator#(obj1,obj2) |
一元前置 | #obj | obj.operator#() | operator#(obj) |
一元后置 | obj# | obj.operator#(0) | Operator#(obj,0) |
赋值= | obj1 = obj2 | obj1.operator=(obj2); | 不允许 |
下标[] | obj[i] | obj.operator[](i) | 不允许 |
箭头-> | obj-> | obj.operator->() | 不允许 |
函数调用() | obj(参数表) | obj.operator()(参数表) | 不允许 |
(4)如何选择成员还是非成员方式进行操作符重载
n 成员方式要求操作符的左操作数必须是该类的对象。
n 如果操作符的左操作数可能或者必须是其它数据类型时,我们就应该采用非成员的方式,将操作符重载为全局函数。
n 如果一个类已经定义完成,不希望再向其中添加任何成员函数,此时自然就必须选择非成员方式进行重载。
n C++语言规定,赋值操作符=、下标操作符[]、函数调用操作符( )、指针访问操作符->,必须被定义为类的成员函数。
2.1.8 约定类成员的常量性
可以使用const关键字约定类的常量数据成员和常成员函数。其中,
(1)常量数据成员:约定对象不变的属性,const修饰;
n const数据成员是对象的常量,在对象一生中保持状态不变,必须进行初始化,且只能在构造函数中通过初始化列表显式或者隐式地初始化。
n 引用型数据成员的初始化,与常量数据成员类似。
(2)常成员函数:约定对象不修改状态的行为,const修饰;
n const是常成员函数类型描述的一部分,const可用于重载区分。
n const成员函数中不允许调用非const成员函数,只能调用其它const成员函数。
n const对象只能访问const成员函数,非const对象则可以访问const或者非const成员函数。
2.1.9 约定类成员的静态性
使用static关键字约定静态数据成员和静态成员函数。其中,
(1)静态数据成员:约定类域名字空间中的全局变量,所有对象共享static数据成员的唯一副本。
n 静态数据成员的声明和定义:在类体中只能进行静态数据成员的声明,必须在类体外的文件域中给出静态数据成员的定义和初始化。
n 静态数据成员的访问:
类名::静态数据成员名; //或者,
任意的对象名.静态数据成员名;
(2)静态成员函数:约定类域名字空间中的全局函数。
n 静态成员函数的定义:static修饰。
n 静态成员函数的访问:
类名::静态成员函数名(参数表); //或者,
任意对象名.静态成员函数名(参数表);
n 静态成员函数中不存在自身对象,其this指针无效。
n 在静态成员函数中只能访问静态成员,不能访问非静态成员。
2.2 对象的组合与继承
C++语言提供组合和继承两种工具支持基于组合和类属关系定制新的数据类型。
2.2.1 基于组合关系定制类:组合类
(1)组合类的某个数据成员是另一种定制数据类型,称为对象成员或子对象。
(2)组合类对象的构造和析构:
n 在产生时总是先调用对象成员的构造函数,然后再调用自身的构造函数。
n 消亡时先调用自身的析构函数,然后再析构对象成员。
n 对象成员之间构造和析构的顺序,按照它们在类体中声明的顺序进行。
2.2.2 基于类属关系定制类:继承与派生
(1)语法
class 派生类名 : 继承方式 基类名 { //派生类继承基类成员
//在类体中改造基类成员,或者说明派生类特有的成员
};
(2)通过继承定制派生类,通常需要经过如下的四个步骤:
n 继承基类成员:确定继承的基类,选择从基类继承的方式;
n 改造基类成员:修改基类成员的访问限定或者重定义基类成员;
n 发展派生类成员:定义派生类自身特有的数据成员和成员函数;
n 重写派生类的构造和析构函数,如果有必要,重写赋值操作符。
(3)选择继承方式
n 公有继承,基类的公有和保护成员被继承为派生类的公有和保护成员。
基类的私有成员 派生类的不可访问成员;
基类的公有成员 派生类的公有成员;
基类的保护成员 派生类的保护成员。
n 私有继承,基类的公有和保护成员被继承为派生类的私有成员。
基类的私有成员 派生类的不可访问成员;
基类的公有成员 派生类的私有成员;
基类的保护成员 派生类的私有成员。
n 保护继承,基类的公有和保护成员被继承为派生类的保护成员。
基类的私有成员 派生类的不可访问成员;
基类的公有成员 派生类的保护成员;
基类的保护成员 派生类的保护成员。
(4)改造基类成员
n 修改基类成员的访问限定(using);
在派生类中可以使用using访问声明改变来自基类的成员的访问限定,但是,基类私有成员是派生类的不可访问成员,不能修改其访问限定。
n 修改基类成员的定义(同名隐藏)。
在派生类中可以根据需要重新定义来自基类的成员。此时,重新定义的基类成员,会隐藏原来同名的基类成员。必须注意,隐藏不是重载。
在公有继承的情况下,派生类对象可以被当作基类对象来使用,反过来则是禁止的。具体的赋值兼容规则表现为:
n 派生类对象,可以用来初始化或者赋予基类对象;
n 派生类对象,可以用来初始化基类引用;
n 派生类对象的地址,可以用来初始化或者赋予基类指针。
(6)基类到派生类的动态类型转换
通过dynamic_cast在运行期间进行动态类型转换。此时,dynamic_cast会在运行期间进行类型检查,如果引用或指针所指向的对象不是转换的目标类型对象,则dynamic_cast会失败,且给出相应的错误指示,
n dynamic_cast到指针,则转换失败的结果指针为0值;
n dynamic_cast到引用,则转换失败会抛出一个bad_cast异常。
(7)多态:根据对象类型不同调用不同函数的行为。
n 函数重载:编译器多态
n 虚函数:运行期多态
(8)虚函数
n 定义:virtual修饰的成员函数。
n 覆盖:在派生类中重新定义基类虚函数。
n 引发多态的条件:当且仅当通过基类引用或者基类指针调用虚函数的时候,系统就会根据实际对象类型,在运行期间自动地选择合适的虚函数版本,实现多态行为:
(a)如果基类引用/指针指向基类对象,则绑定虚函数的基类版本;
(b)如果基类引用/指针指向派生类对象,则绑定虚函数的派生类版本;
n 覆盖、重载、同名隐藏
n 虚函数的运行期绑定原理:virtual修饰导致编译器(a)创建虚函数表;(b)添加指向虚函数表的指针成员;(c)改写虚函数调用代码。
n 虚析构函数:如果需要通过基类指针释放派生类对象,将析构函数声明为虚函数。如果类中不包含虚函数,则不要把析构函数声明为虚函数。
(9)纯虚函数和抽象类
n 具体类:可以产生对象的类。
n 抽象类:包含纯虚函数的类。抽象类不允许产生对象,可以定义抽象类的指针和引用。抽象类的派生类,必须为抽象基类的全部纯虚函数提供定义,否则该派生类依然是抽象性。
n 纯虚函数:具有初始式“=0”的虚函数。
n 接口类:没有数据成员,只有纯虚成员函数。
2.3 异常处理
(1)异常检测和异常处理的分离
当函数代码可能发生某种异常且暂时无法处理这种异常时,可以抛出这种异常(即抛出异常);然后,函数调用者就可以检测是否真正发生了异常(即捕获异常),并且说明若发生了这种异常就进行什么样的补救处理(即处理异常)。
(2)C++语言的异常处理机制就提供了如下的语法工具和设施,包括,
n throw语句:用于函数抛出特定异常,通知异常发生;
n try-catch语句:try用于捕获异常,catch用以处理异常。
3. 泛型思维与泛型编程
泛型问题求解思维,认为通过将特殊问题中的某些因素设为可变参数,可以将特殊问题泛化为适用范围更广的一般问题,此后就可以借助于一般问题的求解思路来完成特殊问题的求解。泛型编程,就是在泛型思维下编写程序描述问题求解思路,需要在数据或者对象参数化的情况下描述和应用问题求解思路,包括:
(1)泛化环节:基于泛化的数据或者对象类型来描述通用的问题求解思路;
(2)应用环节:设定通用求解思路的泛化参数,描述特定具体问题的求解过程。
C++语言提供了模板机制来支持数据类型的参数泛化,包括处理过程泛化的函数模板以及处理对象泛化的类模板。另外,C++语言基于模板实现了常用的一些算法和数据结构,形成了一个具有工业级强度、健壮可靠的泛型程序库,称为标准模板库(Standard Template Libaray, STL),是我们进行泛型编程的基础。
3.1 泛化数据类型:定义模板
C++语言的模板语法工具支持函数和类的泛化,即函数模板和类模板。
3.1.1 函数模板
(1)定义函数模板
template<模板参数表>
返回类型 函数名(形式参数表) { …… } //函数体
(2)模板参数有两种:类型参数和非类型参数。其中,
n 类型参数:表示该参数是未知数据类型或对象类型,说明语法:
typename 类型名 //或者:class 类型名
n 非类型参数:表示该参数不是类型,而是未来要指定的常量值。
(3)例如:
template<typename TKey, typename TVal, int SIZE>
TVal find(const TKey keys[], const TVal vals[]) { }
3.1.2 类模板
(1)定义类模板
template<模板参数表>
class 类名 { …… } //类体
(2)例如:
template <class T, int size> //T是类型参数,size是非类型参数
class Array { …… } //类体
3.2 应用环节:模板实例化
函数模板不是函数,类模板也不是类,不能直接使用;必须将函数模板和类模板中的模板参数替换成具体类型或者常量值,才能产生真正的函数代码和类代码,这个步骤称为模板实例化。
(1)模板实例化方法:隐式实例化、显式实例化
(2)模板隐式实例化:在需要时,编译器根据模板定义生成模板实例。
int elem = max<int>(iArray, sizeof(iArray)/sizeof(int));
Array<double> doubleArr(10);
(3)模板显式实例化:程序员直接声明要求模板在此时就完成实例化。
template int max<int>(const int [] , int);
template class Array<double>;
(4)模板参数推理:显式指定、隐式推理
(5)模板特化:在特定或者限定模板参数的情况下为模板提供特殊的定义。
n 完全特化:指定全部模板参数。
n 部分特化:指定或限定部分模板参数。
n 函数模板支持完全特化,类模板支持完全和部分特化。
3.3 C++标准模板库
C++语言利用模板机制,实现了计算机科学领域常用的算法和通用数据结构,形成具有工业级强度、健壮可靠的函数模板和类模板集合,称为标准模板库(STL)。
3.3.1 标准模板库的功能
C++语言的标准模板库可以划分为六种类型的功能组件:容器(container)、迭代器(iterator)、算法(algorithm)、仿函数(function object)、适配器(adapter)、分配器(allocator)。利用这些功能组件,我们可以:
(1)把需要处理的数据或者对象放到容器中,进行有效的组织和存储。容器是实现各种数据结构的类模板集合,如vector、list、map等。
(2)利用迭代器对容器中的元素进行存取访问。C++语言的迭代器是访问容器的泛型指针类模板,每种容器均附带自身的迭代器。
(3)通过迭代器指定容器区间范围,利用算法对该区间范围内的容器元素进行操作和处理。算法是一组常用函数模板,如sort、find、copy等。
(4)仿函数,是重载了operator()的类或类模板,其行为类似于函数,使用仿函数可以对容器元素的操作和处理行为进行定制。
(5)适配器,用于对容器、迭代器、仿函数的行为进行转换。
(6)分配器,用于分配和管理容器的存储空间,标准模板库的容器均使用一种默认的通用分配器。
3.3.2 标准模板库的容器
标准模板库的容器,用于容纳和管理其它的对象,每种容器都附带了自身的迭代器用来访问和操作容器中所包含的元素。这些容器总体上分两类七种:
(1)顺序式容器:向量vector、列表list、双向队列deque。
(2)关联式容器:集合set、多重集合multiset、映射map、多重映射multimap。
特征&操作 | vector | list | deque |
内部结构 | 动态数组 | 双向链表 | 多个数组 |
随机访问 | 支持,很快 | 不支持 | 支持,比vector慢 |
顺序访问 | 支持 | 支持 | 支持 |
查找速度 | 慢 | 很慢 | 慢 |
插入删除 元素 | 支持,在尾部很快,其它位置很慢 | 支持 很快 | 支持,首尾位置很快,其它位置较慢 |
表 2 关联容器set、map、multiset、multimap的特性
特征&操作 | set | multiset | map | multimap |
特征 | 键集合,键不可重复 | 允许重复键的set | 键-值对集合,键不允许重复 | 允许重复键的map |
内部结构 | 二叉搜索树 | 二叉搜索树 | 二叉搜索树 | 二叉搜索树[1] |
随机访问 | 不支持 | 不支持 | 按key随机访问 | 不支持 |
查找速度 | 快 | 快 | 快 | 快 |
插入删除 元素 | 支持 | 支持 | 支持 | 支持 |
(3)顺序容器的适配器:栈stack、队列queue、优先队列priority_queue。
n 栈:将容器元素的插入和访问行为约束转换为后进先出顺序;
n 队列:将容器元素的插入和访问行为约束为元素先进先出顺序;
n 优先队列:将容器元素的插入和访问行为约束为按优先级度量顺序。
3.3.3 迭代器
迭代器,是指针概念的泛化和推广,通过迭代器,可以像指向数组的指针那样访问到容器中的每个位置,进而访问该位置的元素。
(1)五种迭代器:
n 输入迭代器:单步向前迭代 + 读取数据
n 输出迭代器:单步前向+单次写入数据
n 前向迭代器:输入输出迭代器+多次读写
n 双向迭代器:输入输出迭代器+双向单步
n 随机访问迭代器:双向迭代器+任意跳转
(2)容器附带的迭代器:
n 标准模板库的每种容器类型都附带了自己的迭代器。
n vector向量容器提供了三种类型的迭代器:随机访问迭代器、逆向随机访问迭代器、常迭代器。
vector<int>::iterator //随机访问迭代器,可读写
vector<int>::reverse_iterator//逆向随机访问迭代器,可读写
vector<int>::const_iterator //随机访问迭代器,可读不可写
(3)迭代器的适配器
n 通过迭代器适配器,可以为迭代器提供更多的操作功能。
n 三种常用的迭代器适配器:逆向、流和插入适配器。
n 三种类型的插入迭代器:尾插入迭代器(back_inserter)、头插入迭代器(front_inserter)、一般插入迭代器(inserter)。
3.3.4 仿函数
仿函数,又称函数对象(function object),是一种能够以函数语法调用的对象。
(1)两类仿函数:
n 指向一般函数的函数指针;
n 重载了operator()操作符的类对象。
(2)标准库的三类基本仿函数
n Generator(产生器,无参仿函数)
n Unary Function(一元仿函数,有一个参数)
n Binary Function(二元仿函数,有两个参数)
n 谓词(Predicate):返回bool型值的仿函数
(3)标准库中的仿函数
n 算术运算:plus、minus、multiplies、divides、modulus、negate
n 关系运算:equal_to、not_equal_to、greater、less、greater_equal、less_equal
n 逻辑运算:logical_and、logical_or、logical_not
(4)仿函数的适配器
n binder:绑定器,用于将二元函数中的一个参数固定,使之转化为一元函数。标准模板库预定义了两种绑定器:
bind1st:用于绑定二元函数的第一个参数。
bind2nd:用于绑定二元函数的第二个参数。
n negator:取反器,用于将函数取值取反,STL预定义了两种取反器:
not1:用于一元函数对象的取反。
not2:用于二元函数对象的取反。
n ptr_fun:用于将已有函数转换为一个仿函数。
n mem_fun:用于将某个类中的成员函数转换为仿函数。
3.3.5 算法
(1)查找算法:用各种策略去判断容器中是否存在指定元素的一组函数模板。
n 三种类型:在无序容器中查找、在有序容器中查找、序列匹配。
n 在无序容器中查找的算法,有find和count两类算法。
(2)排序和整序算法:
n 排序,是按一定的比较规则对容器元素进行顺序排列。
n 整序,是按一定规律进行分类,如分割算法partition是把容器元素分为两组,一组满足某条件,另一组不满足某条件。
n sort & stable_sort、merge & inplace_merge、nth_element、random_shuffle、rotate & rotate_copy
(3)拷贝、删除和代替算法:
n copy & copy_backward、
n iter_swap、swap & swap_range
n remove & remove_copy & remove_if & remove_copy_if、
n replace & replace_copy & replace_if & replace_copy_if
n unique & unique_copy
(4)排列组合算法:
n next_permutation是按字典序计算给定排列的下一个排列。
n prev_permutation是按字典序计算给定排列的上一个排列。
(5)生成和改变算法
n generate & generate_n
n fill & fill_n
n for_each
n transform
(6)关系比较算法
equal、includes、lexicographical_compare、max & max_element、min & min_element、mismatch
(7)集合算法
set_union、set_intersection、set_difference、set_symmetric_difference
(8)算术算法
accumulate、partial_sum、inner_product、adjacent_difference
(9)堆算法
make_heap、push_heap、pop_heap、sort_heap
[1] 二叉搜索树是一种有序的二叉树数据结构,左子树节点值都小于父节点,右子树节点值都大于根节点,插入、查找的效率都很高,具体细节请参阅有关数据结构的书籍或网站。