第1章 面向对象程序设计概述
面向过程程序设计
面向对象程序设计
面向对象的软件开发
图书馆图书借阅管理系统的面向对象分析与设计
Coding Conventions (编码规范)
Google开源项目风格指南
英文原版:https://github.com/google/styleguide
中文版:https://github.com/zh-google-styleguide/zh-google-styleguide
其中C++ 规范5 万多字,过于庞大
geosoft.no的编码风格指南(英文版)
https://geosoft.no/development/cppstyle.html
4.9 版本有94 条规则。规则相对简明
第2章 面向过程程序设计概述
从C语言到C++
简单C++程序
C++对C语言的扩充
C++程序的编写和实现
C++关键字
第3章 类与对象
类的声明和对象的定义
类的成员函数
对象成员的访问
构造函数与析构函数
构造函数的作用
带参数的构造函数
类是一种 抽象 的 自定义 数据类型,它并 不占用 内存空间
所以不能在类内对 数据成员 进行 初始化
这点,以后有时间从类加载机制上,理解。不要局限在记住这些规定,这些是可以通过原理解释的
先加载类
还有了解到,程序执行(或者说编译,应该就是说执行时),并不是main函数先加载执行,而是先加载类信息(体会、理解一下)
class Box {
...
private:
int length = 0; //错误,不能在类内对数据成员进行初始化
}
构造函数与参数初始化表
对数据成员 初始化 的方法:
1、在 构造函数 的 函数体 内通过 赋值语句 对 数据成员 进行 初始化
2、参数初始化表
class Box{
public:
// 使用参数初始化表的构造函数
// 就是: 后面中,(L)中的L,赋给length,(W)中的W,赋给width, (H)中的H,赋给height
Box(float L, float W, float H) : length(L), Width(W), heigth(H) {}
float Volume() {
return length * width * height;
}
private:
float length, width, height;
}
构造函数重载
构造函数和析构函数的调用次序
1)在全局范围中定义的对象(即在函数之外定义的对象),
其 构造函数 在文件中的 所有函数(包括 main
函数)执行之前调用
但如果一个程序中有 多个文件,而不同的文件中都定义了 全局对象,则这些对象的 构造函数 的 执行顺序 是 不确定 的
当 main
函数执行完毕 或 调动 exit()
函数时(此时程序终止),调用 析构函数
2)如果定义的是局部自动对象(如在函数中定义对象),
则在创建对象时调用其 构造函数。
如果程序被 多次 调用,则在每次创建对象时,都要调用 构造函数
在函数调用结束、释放对象时,先调用析构函数
3)如果在函数中定义 静态(static
)局部对象,
则只在程序第一次调用此函数创建对象时,调用构造函数一次,在调用结束时,队形并不释放,
因此也不调用析构函数,只在 main
函数结束或调用 exit()
函数结束程序时,才调用析构函数
对象数组
对象数组初始化的几种方法
可以说,只有一种初始化方法,完整初始化赋值
Box box[3] = { {1, 3, 5}, {2, 4, 6}, {3, 6, 9} };
#include <iostream>
using namespace std;
class Box {
private:
float length, width, height;
public:
Box() {
length = 1; width = 1; height = 1;
cout << "Box(" << length << "," << width << "," << height << ")的构造函数,被调用" << endl;
}
Box(float L, float W, float H) {
length = L; width = W; height = H;
cout << "Box(" << length << "," << width << "," << height << ")的构造函数,被调用" << endl;
}
};
int main() {
Box box[3] = { Box(1, 3, 5), Box(2, 4, 6), Box(3, 6, 9) };
// 1.经测试,下面这种赋值方法,也可以
//Box box[3] = { {1, 3, 5}, {2, 4, 6}, {3, 6, 9} };
// 2.下面这种方式,VS报错,测试不了,提示木马。Dev-C++测试成功,可以
//Box box[3] = { Box(1, 3,5), Box(), Box(3, 6, 9) };
// 3.下面这种方式,VS也测试不了,提示木马
//Box box[3] = { {1, 3, 5}, {}, {3, 6, 9} };
return 0;
}
对象指针
对象与 const
对象的动态创建与释放
对象的赋值与复制
向函数传递对象
图书馆图书借阅管理系统中类的声明和对象的定义
第4章 继承与派生
继承与派生的概念
派生类的声明
派生类的构成
派生类中基类成员的访问属性
公用继承
基类的公用成员和保护成员在派生类中保持原有访问属性,
其私有成员仍为基类私有
公用基类成员在派生类中的访问属性
公用基类成员 | 在公用派生类中的访问属性 |
---|---|
公用成员 | 公用 |
私有成员 | 不可访问 |
保护成员 | 保护 |
私有继承
基类的公用成员和保护成员在派生类中成了私有成员,其私有成员仍为基类所有
私有基类成员在派生类中的访问属性
私有基类的成员 | 在私有派生类中的访问属性 |
---|---|
公用成员 | 私有 |
私有成员 | 不可访问 |
保护成员 | 私有 |
受保护的继承
基类的公用成员和保护成员在派生类中成了保护成员,其私有成员仍为基类所有
受保护的意思:不能被外界访问,但可以被派生类的成员访问
基类中的成员 | 在保护派生类中的访问属性 |
---|---|
公用成员 | 保护 |
私有成员 | 保护 |
保护成员 | 不可访问 |
派生类的构造函数和析构函数
多重继承
多重继承是指派生类具有两个或两个以上的直接基类(direct class)
多重继承派生类是一种比较复杂的类构造形式
声明多重继承的方法
多重继承派生类的构造函数与析构函数
多重继承引起的二义性问题
虚基类
格式
class 派生类名: virtual 继承方式 基类名
例子
class Person {
...
};
// 虚继承于Person类
class Student: virtual public Person {
...
};
// 虚继承与Perosn类
class Employee virtual public Person {
...
};
虚基类并不是在声明基类时声明的,而是在声明派生类时的指定继承方式时声明的
因为一个基类可以在派生一个派生类时,作为虚基类,而在派生另一个派生类时,不作为虚基类
基类与派生类对象的关系
聚合与组合
包含子对象派生类构造函数的执行顺序:
1、最先调用基类的构造函数,对基类数据成员初始化。
当派生类有多个基类时,各基类构造函数的调用顺序按照他们在继承方式中的声明次序调用,而不是按派生类构造函数参数初始化列表中出现的次序调用
2、再调用子对象的构造函数,对子对象数据成员初始化。
当派生类有多个子对象时,各子对象构造函数按派生类声明中子对象出现的次序调用,而不是按派生类构造函数参数初始化列表中出现的次序调用
3、最后执行派生类构造函数的函数体,对派生类新增的一般数据成员初始化
包含子对象派生类析构函数的执行顺序与其构造函数的执行顺序相反
1、最先执行派生类析构函数的函数体,对派生类新增的一般数据成员所涉及的额外内存空间进行清理
2、再调用子对象的析构函数,对子对象所涉及的额外内存空间进行清理。
当派生类有多个子对象时,按派生类声明中子对象出现的逆序调用
3、最后调用基类的析构函数,对基类所涉及的额外内存空间进行清理,多个基类则按照派生类声明时列出的逆序、从右到左调用
图书馆图书借阅管理系统中继承与聚合的应用
第5章 多态性与虚函数
什么是多态性
多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作
从而可以使用相同的调用方式来调用这些具有不同功能的同名函数
C++中多态性分为4类
1、参数多态
2、包含多态
3、重载多态
4、强制多态
前面2种称为通用多态,后面2中称为专用多态
向上类型转换
功能早绑定和晚绑定
多态从实现的角度分为2类
1、编译时多态
2、运行时多态
绑定工作在编译连接阶段完成的情况称为功能早绑定
绑定工作在程序运行阶段完成的情况称为功能晚绑定
一般而言
编译型语言(C、PASCAL)都采用功能早绑定
解释型语言(LISP、Prolog)都采用功能晚绑定
实现功能晚绑定-虚函数
虚函数的定义和作用
虚函数的定义是在基类中进行的
在成员函数原型的声明语句之前冠以关键字 virtual ,从而提供一种接口
virtual 函数类型 函数名(参数表) {
函数体
}
虚函数的作用
允许在 派生类 中重新定义 与基类同名的函数,
并且可以通过 指向 基类对象的 指针或 基类对象的 引用来访问 基类和 派生类中的 同名函数
虚析构函数
C++规定,如果在派生类中,没有用virtual显示地给出虚函数的声明,这时系统会遵循以下的规则来判断一个成员函数是不是虚函数
- 该函数与基类的虚函数有相同的名称
- 该函数与基类的虚函数有相同的参数个数及相同的对应参数类型
- 该函数与基类的虚函数有相同的返回类型或者满足赋值兼容规则的指针、引用型的返回类型
派生类的函数满足了上述条件,就被自动确定为虚函数。因此,在本程序的派生类MotorVehicle, Car, Truck 中 message() 仍为虚函数
虚函数的定义说明
1、通过定义虚函数来使用C++提供的多态性机制时,派生类应该从它的基类公用派生。
之所以有这个要求,是因为是在赋值兼容性规则的基础上来使用虚函数的,而赋值兼容规则成立的前提是派生类从其基类公用派生
2、必须首先在基类中定义虚函数。
由于 基类 和 派生类 是相对的,因此,这项说明并不表明必须在类等级的最高层类中声明虚函数。
在实际应用中,应该在类等级内需要应具有动态多态性的几个层次中的最高层类内首先声明虚函数
3、在派生类中对基类声明的虚函数进行重新定义时,关键字 “virtual” 可以写,也可以不写。
但为了增强程序的可读性,最好在对派生类的虚函数进行重新定义时,也加上关键字 “virtual”
如果在 派生类 中没有对基类的 虚函数 重新定义,则派生类简单地继承其 直接基类 的虚函数
4、虽然使用对象名和点运算符的方式也可以调用虚函数,
如语句 “c.message()” 可以调用虚函数 Car::message(),但是这种调用是在编译时进行的功能早绑定,
它没有充分利用虚函数的特性
只有通过指向基类对象的指针或基类对象的引用访问虚函数时,才能获得运行时的多态性
5、一个虚函数无论被公用继承多少次,它仍然保持其虚函数的特性
6、虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,
因为下滑神女湖调动要靠特定的对象来决定该激活哪个函数。
但是虚函数可以在另一个类中被声明为友元函数
7、內联函数不能是虚函数,因为內联函数是不能在运行中动态确定其位置的。
即使虚函数在类的内部定义,编译时仍将其看做是非內联的
8、构造函数不能是虚函数,因为虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,
因此徐构造函数是没有意义的
9、析构函数可以是虚函数,而且通常说明为虚函数
虚函数和重载函数的比较
在一个派生类中重新定义基类的虚函数不同于一般的函数重载
1、函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是同一类族中不同派生层次上的同名函数问题,
前者是横向重载,后者可以理解为纵向重载。
但与重载不同的是,同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)
2、重载函数可以是成员函数或普通函数,而虚函数只能是成员函数
3、重载函数的调用是以所传递参数序列的差别作为调用不同函数的依据;
虚函数是根据对象的不同去调用不同类的虚函数
4、虚函数在运行时表现出多态功能,这是C++的精髓;
而重载函数则在编译时表现出多态性
纯虚函数和抽象类
纯虚函数
抽象类是带有纯虚函数的类
有时,基类往往表示一种抽象的概念,它并不与具体的事物相联系
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义,
但要求在它的派生类中必须定义自己的版本,或重新说明为纯虚函数
纯虚函数的定义
class 类名 {
...
virtual 函数类型 函数名(参数表)=0;
...
}
抽象类
如果一个类中至少有一个纯虚函数,那么就成该类为抽象类
抽象类使用规定
1、由于抽象类中,至少包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类来使用,不能建立抽象类对象,它只能用来为派生类提供一个接口规范,其纯虚函数的实现由派生类给出
2、不允许从具体类派生出抽象类,所谓具体类,就是不包含纯虚函数的普通类
3、抽象类不能用作参数类型、函数返回类型或显式转换的类型
4、可以声明抽象类类型的指针,此指针可以指向它的派生类对象,进而实现动态多态性
5、如果派生类中没有重新定义纯虚函数,则派生类只是简单继承基类的纯虚函数,则这个派生类仍然是一个抽象类。
如果派生类中给出了基类所有纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以创建对象的具体类
6、在抽象类中,也可以定义普通成员函数或虚函数,虽然不能为抽象类声明对象,但仍然可以通过派生类对象来调用这些不是纯虚函数的函数
C++中,还有一种情况是函数体为空的虚函数,请注意它和纯虚函数的区别。
纯虚函数一般没有函数体,而空的虚函数的函数体为空。
前者所在的类是抽象类,不能直接进行实例化,而后者所在的类是可以实例化的。
它们共同的特点是都可以派生出新的类,然后在新类中给出新的虚函数的实现,而且这种新的实现可以具有多态特征
图书馆图书借阅管理系统中的多态性
第6章 友元与静态成员
封装的破坏-友元
友元函数不是当前类的成员函数,而是独立于当前类的外部函数
但它可以访问该类对象的任何成员,包括私有成员、保护成员和公有成员
友元函数
友元类
对象机制的破坏-静态函数
静态数据成员
说明:
1、如果只声明了类而未定义对象,则类的一般数据成员是不占内存空间的,只有在定义对象时,才为对象的数据成员分配空间。
但是静态数据成员不属于某一个对象,在为对象分配的空间中不包括静态数据成员所占的空间。
静态数据成员是在所有对象之外单独开辟空间。只要在类中定义了静态数据成员,即使不定义对象,也为静态数据成员分配空间,它可以被访问。
在一个类中可以有一个或多个静态数据成员,所有的对象共享这些静态数据成员,都可以访问它
2、静态数据成员不随对象的建立而分配空间,也不随对象的撤销而释放(一般数据成员是在对象建立时分配空间,在对象撤销时释放)。
静态数据成员是在程序编译时,被分配空间的,到程序结束时才释放空间
3、静态数据成员可以初始化,但只能在类体外进行初始化
int Student::stu_count = 0;
不必在初始化语句中加 static
注意:静态数据成员要实际地分配空间,故不能在类声明中初始化。
类声明只声明一个类的“尺寸与规格”,并不进行实际的内存分配,所以在类声明中写"static int stu_count = 0;" 是错误的
如果未对静态数据成员赋初值,则编译系统会自动赋予初值0
4、静态数据成员即可以通过对象名访问,也可以通过类名来访问
这些现在看起来,很多规定,这些理解,是应该理解一下程序编译执行的内存状态来理解的,而不是这样靠记忆
静态成员函数
静态成员函数的作用:为了能处理静态数据成员
说明:
1、静态成员函数与非静态成员函数的根本区别是:非静态成员函数有 this指针,而静态成员函数没有this指针,因而决定了静态成员函数不能默认本类中的非静态成员
当调用一个对象的非静态成员函数时,系统会把该对象的起始地址赋给成员函数的 this指针。而静态成员函数并不属于某一对象,它与任何对象都无关,因此静态成员函数没有this指针。
既然它没有指向某一对象,就无法对一个对象中的非静态成员进行默认访问(即在访问数据成员时,不指定对象名)
2、静态成员函数可以直接访问本类中的静态数据成员,因为静态成员同样是属于类的,可以直接访问。
在C++程序中,静态成员函数主要用来访问静态数据成员,而不访问非静态成员。
假如在一个静态成员函数中有以下语句:
cout << age << endl;
cout << score << endl;
但是,并不是绝对不能访问本类中的非静态成员,只是不能进行默认访问,因为无法知道应该去找哪个对象。
如果一定要访问本类的非静态成员,应该加对象名和成员运算符"." .
图书馆图书借阅管理系统中友元与静态成员的应用
第7章 运算符重载
为什么要进行运算符重载
运算符重载的方法
重载运算符的规则
1、C++不允许用户自己定义新的运算符,只能对C++中已有的运算符进行重载
2、运算符重载针对新类型数据的实际需要,对原有运算符进行适当的改造。一般来讲,重载的功能应当与原有的功能相类似
3、C++允许重载的运算符包括C++中几乎所有的运算符,只有5个运算符不允许重载
4、坚持 “4个不能改变”
不能改变运算符操作数的个数;
不能改变运算符原有的优先级;
不能改变运算符原有的结合性;
不能改变运算符原有的语法结构
5、重载的运算符必须和用户定义的自定义类型对象一起使用,其参数至少应有一个是类对象(或类对象的引用),
也就说,参数不能全部是C++的标准类型,以防止用户修改用于标准类型数据的运算符的性质
6、重载运算符的函数不能有默认的参数,否则就改变了运算符参数的个数
7、用于类对象的运算符一般必须重载,
但又2个例外,"=" 和 “&” 可以不必用户重载
地址运算符 &,也可以不必重载,它能放回类对象在内存中的起始地址
运算符重载函数作为类的成员函数和友元函数
几种常用运算符的重载
不同类型数据间的转换
图书馆图书借阅管理系统中的运算符重载
第8章 泛型编程
函数模板
函数模板的定义
泛型编程 和 面向过程编程以及面向对象编程一起,是C++所包含的三种编程风格
面向过程的编程,可以将常用代码封装在一个函数中,然后通过函数调用来表达目标代码重用的目的
面向对象的编程,可以通过 类的继承 以及 类的聚合或组合 来实现代码的重用。
而泛型编程,是以独立于任何特定类型的方式编写代码,使代码具有更好的重用性
模板是泛型编程的基础,传统C++的泛型编程,仅仅局限于简单的模板技术-函数模板 和 类模板。
随着C++的发展,标准C++引入STL(标准模板库)的内容,才真正进入泛型编程 的广阔天地
C++是一种强类型语言,强类型语言所使用的数据都必须明确地声明为某种严格定义的类型,并且在所有的数值传递中,编译器都强制进行类型相容性检查。
虽然强类型语言有力地保证了语言的安全性和健壮性,但有些时候,强类型语言对于实现相对简单的函数似乎是个障碍
虽然上述程序代码中的Max函数的算法很简单,但是强类型语言迫使我们不得不为所有希望比较的类型都显式定义一个函数,显得既笨拙又效率低下
在模板出现之前,有一种方法可以作为这个问题的一种解决方案,那就是使用带参数宏。
但是这种方法也是很危险的,因为宏的工