大一下学期面向对象程序设计(1)期末复习
期末复习总览
占比分布
期末 70%
作业 20%
考勤 10%
笔试
阅读程序 4题各5分
填空 2题各10分 5空
上机 3*20分
考试要点
构造函数、析构函数
拷贝构造函数
什么时候使用? 传值,返回值
例子(共调用3+2次) :
add(cls x,cls y){
return x+y;
}
add(x,y)
常成员函数
Composition函数初始化
先对成员对象初始化,构造函数初始化列表
static
操作符重载
必须重载为全局函数
虚函数
纯虚函数后加 = 0
栈展开
对象释放
分章复习
Chapter 9 类(1)
条件编译
`#ifndef FILENAME_H`
`#define FILENAME_H`
`#endif`
Sizeof
只包括数据成员的大小
VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。
比较以下例子
class test1{
char c;
} sizeof : 1
class test2{
char a,b,c,d,e;
} sizeof : 5
class test3{
int num; char a;
} sizeof : 8
class test4{
int num; char a,b,c,d,e;
} sizeof : 12
class test5{
char a;int num;char b;
} sizeof : 12
this指针
每一个成员函数都有一个隐藏的指针类型形参:this
<类名> *const this
通过对象调用成员函数时,编译程序会把对象地址作为隐含参数传递给形参this
访问this指向对象的成员时:this-> (也可以省略不写)
接口与实现分离
A.h A.cpp 编译-> A.obj
B.cpp 编译 B.obj
A.obj B.obj 链接 -> B.exe
软件供应商在他们的产品中只需提供头文件和类库(目标模块),而不需提供源代码。
访问函数与工具函数
- 访问(access)函数,public,用来读取或显示数据,如
setName,getName。如vector.empty()
- 工具(utility)函数,private,一般在public函数里面被使用。
构造函数与析构函数
- 缺省构造函数:
- 不带参数的构造函数
- 所有参数都有默认值的构造函数
考虑以下例子为什么编译失败(缺省构造函数重载失败)1
2
3
4
5
6
7
8
9
10
11
12class A{
int x, y;
public:
A(int x1 = 0,int y1 = 0)
x = x1;
y = y1;
}
A(int x1){
x = x1;
y = 0;
}
}; - 只要类中提供了构造函数,即使没有提供缺省构造函数,编译程序将不再提供缺省构造函数。
- 析构函数
- 析构函数是一个特殊的成员函数,它的名字同类名,并在前面加“~”字符,用来与构造函数加以区别。析构函数不接收任何参数,也不可以指定返回类型。
- 一个类中只能定义一个析构函数,析构函数不能重载,且必须是公有的。
- 下面两种情况,析构函数将会被自动调用
- 如果一个对象被定义在一个函数体内,则当这个函数结束时,该对象的析构函数被自动调用;
- 当一个对象是使用new运算符被动态创建的,在使用delete运算符释放它时,delete将会自动调用析构函数
存储类别
- 全局对象:在任何函数(含main)执行前,构造;在程序结束时,析构.
- 局部对象:(栈区)
- 自动变量:对象定义时,构造;块结束时,析构.
- 静态变量:首次定义时,构造?;程序结束时,析构.
- 对象间调用构造函数和析构函数的顺序恰好相反. 全局和静态对象(均为静态存储类别)同理.
- 特例1:调用exit函数退出程序执行时,不调用剩余自动对象的析构函数.
- 特例2:调用abort函数退出程序执行时,不调用任何剩余对象的析构函数.
小陷阱:私有数据成员的引用
考虑以下程序,是否出现类私有数据成员的引用情况?
如何解决该问题?(加const,如何加?)
1 | class A { |
第三行变为 const int & setA()
即可。
赋值运算
- By default, such assignment is performed by memberwise assignment(按成员赋值/逐个成员赋值).
- 拷贝构造函数在对还没存在的对象赋值时会调用;赋值函数在对已经存在的对象赋值会调用;赋值函数初始化前需要清理堆资源等,拷贝构造不需要。
- 注意点
- 有一种特别常见的情况需要自己定义复制控制函数:类具有指针函数。
- 赋值操作符和复制构造函数可以看成一个单元,当需要其中一个时,我们几乎也肯定需要另一个
- 三法则:如果类需要析构函数,则它也需要赋值操作符和复制构造函数
- 如果没定义复制构造函数(别的不管),编译器会自动生成默认复制构造函数
- 如果定义了其他构造函数(包括复制构造函数),编译器绝不会生成默认构造函数
- 即使自己写了析构函数,编译器也会自动生成默认析构函数
- 解决方案
自定义拷贝构造函数:按引用传递
小结
- 条件编译指令
- 访问成员函数的三种方式(句柄+操作符)
- 成员函数的作用域:class scope
- 访问函数和工具函数
- 带默认实参的构造函数
- 构造函数和析构函数被调用的顺序
- 破坏类的封装的一种做法:返回对私有数据成员的引用
- 利用一个对象初始化另一个对象(拷贝构造函数)
Chapter 10 类(2)
常成员函数
- 要求
- 不能修改本对象的数据成员
- 不能调用本对象其它non-const成员函数
- 建议
- 所有不更改object成员的函数均声明为const成员函数
- 注意
- 成员函数是否为常成员函数,不仅取决于它不修改对象、不调用non-const成员函数,而且必须显式地声明为const!
- 构造函数、析构函数不能声明为const
- 可以调用非const成员函数初始化const对象(为什么?)
对象的常量特性体现在初始化(构造)后、析构之前。
- 小结
- 可以定义const常量,具有不可变性;
- 便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患;
- 可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改;
- 可以保护被修饰的东西,防止意外的修改,增强程序的健壮性;
- 提高了效率(编译器对常量进行优化)
构造函数初始化列表
构造函数初始化列表必须使用的情况 注意:C++11支持const直接赋值。
1 | const data member (例外?const static integer)(注:普通const必须一开始初始化) |
组合
- 成员对象的构造和析构顺序
- 成员对象的构造先于宿主对象;
- 成员对象按照类定义中的声明顺序构造
- 成员对象的析构后于宿主对象。
- 总结
- 若无缺省构造函数,则必须使用初始化列表(思考原因)
- 若有缺省构造函数,此时如果成员对象没有显式通过成员初始化列表中初始化,则自动隐含调用缺省构造函数(default constructor). 性能方面?(避免双重初始化)
- 构造函数与析构函数的调用顺序
- 类的成员对象(public or private)
- 把成员对象声明为public不会影响成员对象的封装性
友元函数与友元类
- 为什么使用友元
- 为什么要使用友元函数
- 两个类要共享数据的时候
- 运算符重载的某些场合需要使用友元
- 使用友元函数可提高性能
- 用友元函数可以访问两个或多个类的私有数据,较其它方法使人们更容易理解程序的逻辑关系
- 缺点:破环了类的封装机制
- 总结:
- 类的友元函数在类作用域之外定义,但可以访问类的私有和保护成员
- 尽管类定义中有友元函数原型,友元函数仍然不是成员函数
- 由于友元函数不是任何类的成员函数,所以不能用句柄(对象)加点操作符来调用
- public, private, protected成员访问符与友员关系的声明无关,因此友元关系声明可在类定义的任何位置,习惯上在类定义的开始位置
- 友元关系是指定的,不是获取的,如果让类B成为类A的友元类,类A必须显式声明类B为自己的友元类
- 友元关系不满足对称性和传递性
- 如果一个友元函数想与两个或更多类成为友元关系,在每个类中都必须声明为友元函数
new运算符
- 为什么使用
- 直到运行时才能知道需要多少对象空间;
- 不知道对象的生存期到底有多长;
- 直到运行时才知道一个对象需要多少内存空间
- 使用方法
- 基本数据类型
double *ptr = new double(3.14);
delete ptr
- 类对象
Cls *ptr = new Cls();
delete ptr;
- 数组
int *array = new int[size];
delete [] array;
Cls *clsarray = new Cls[size];
delete [] clsarray;
- 基本数据类型
static
- 初始化
- 显式初始化
- 静态常量整数成员可在类定义中初始化(长整形/短整形/枚举)
const static int i = 0;
//ok- 其它情况必须在类外部定义和初始化。
static int i = 0;
//error- const数组成员的初始化?*
声明为const static数组,并在外部初始化
- 未显示初始化
- 静态基本类型的数据成员缺省初始化为0
- 静态抽象数据类型(类对象)的数据成员默认调用缺省构造函数
- 静态成员的空间分配并不在类的构造函数中完成,也不会在类的析构函数里完成空间回收
- 显式初始化
- 访问
- 可直接通过类名+::访问
- 没有this指针(为什么?)
static 成员函数独立于对象存在。而this指针必须指向类的具体的对象 - 不能加const(为什么?)
const修饰符用于表示函数不能修改成员变量的值,该函数必须是含有this指针 - 可通过对象访问(public),也可直接通过类名+::访问
- 不能访问非静态数据成员,也不能调用非静态成员函数
总结
- 常量对象和常成员函数
- 对象的组合
- 友元函数和友元类
- this指针(隐含参数,作用:级联调用)
- 动态内存分配
- 静态类成员
Chapter 11 运算符重载
运算符重载
- 定义方式:
ClassName operator@([const ClassName& A])
- 调用方式:
- 普通函数
- 全局函数: 函数名(参数列表)
- 类成员函数: 对象.函数名(参数列表)等
- 重载的运算符
使用时以表达式形式出现: HugeIntA + HugeIntB
- 普通函数
- 针对对象,三个不用重载的运算符:
- assignment operator (=)
- address operators (&)
- comma operators (,)
- 限制
- 重载运算符应该仿效其相应的内置对象的功能
- 重载成的成员函数必须是非static的(带着问题思考)
HugeInt operator+(const HugeInt& a); - 不能更改Precedence(优先级), Associativity(结合律) 以及 Number of Operands(操作数数目)
- 仅能重载现有运算符,不能创造新运算符
运算符函数
运算符函数可以是成员函数或者全局函数
- 当重载为类的成员函数时
- 非静态的类成员函数
- 使用this指针隐性获取操作左值
- 左操作数(或唯一的操作数)必须为该类对象(或对象引用)
- 将自动包含该类对象(或其引用)作为操作数,函数参数个数等于运算符目数-1
- 当重载为全局函数时
- 形式
class HugeInt {
friend HugeInt operator+(const HugeInt &, int);
}; - 函数参数个数等于运算符的目数
- 全局函数: 是否访问私有数据
- Friend(可访问私有数据)复习~~~
- Non-friend
- 思考:必须重载为全局函数的情况
- 左操作数必须为不同类型对象或一个基本类型对象
- 使运算符具有可交换性
- HugeInteger + int 和 int + HugeInteger
- 成员函数+参数调换的全局函数
- 形式
- 设计原则
- ( ), [ ], ->和赋值(=, +=, -=等)运算符 必须 重载为成员函数
- >>, <<和需要支持交换律(Commutative)的运算符重载为全局函数
- 其余运算符可以选择重载为成员或全局函数
输入输出流运算符重载
输出流重载 friend ostream &operator<<(ostream&, const cls &)
输入流重载 friend istream &operator>>(istream&)
一元运算符重载
例子:
1 | class String { |
二元运算符重载
- 带有一个参数的成员函数
前提条件是仅当左操作数是该函数所在类的对象 - 带有二个参数的全局函数
- 其中一个参数必须是对象或对象引用
例子:1
2
3
4
5
6
7
8class String {
public:
bool operator<( const String &) const;
};
class String {
friend bool operator<( const String &, const String &);
};
运算符重载小结
const Array &operator=( const Array & );
为何返回值要加&和const?- &为了在a=(b=c)时提高性能
- 参数的&为了防止自我复制
- Const类型的返回值表示对相关对象的保护,实现禁左。禁止(a=b)=c
- 虚悬指针(产生原因)与指针回收问题
关于delete之后的指针使用(有的编译器可以访问,但会有潜在的错误) - 通常会为任何一个使用动态分配内存的类同时提供一组函数(重要!):
- 复制构造函数
- 析构函数
- 重载的赋值运算符函数
- 如何阻止类对象的复制
将重载的赋值运算符以及拷贝构造函数设置为private
类型转换
- 同类对象转换 : 初始化对象,拷贝构造函数
- 非同类对象转换:
- 转换构造函数
- 重载强制类型转换运算符
1
2
3
4
5
6
7
8
9
10
11
12函数声明形式
A::operator int() const; // 不需要返回值,不修改原对象
实现:A -> int
A::operator OtherClass() const;
实现:A-> OtherClass
用户调用:
A s;
static_cast<int>(s);
static_cast<OtherClass>(s);
编译器调用:
s.operator int(); s.operator OtherClass();
- 几个常用的cast
dynamic_cast: 通常在基类和派生类之间转换时使用
const_cast: 主要针对const和volatile的转换.
static_cast: 一般的转换
reinterpret_cast: 用于进行没有任何关联之间的转换,比如一个字符指针转换为一个整形数。 - 小结
- 转换构造函数
- 重载强制类型转换运算符
- 隐式调用原则:
- 先查找符合要求的函数
- 再调用转换运算
- 转换运算不能级联调用(仅能调用一次)
总结
- 哪些运算符可以重载?何时需要重载?有何限制?如何重载?
“.”,”.*”,”::”,”?:”不能重载
“=“,”&”,”,”可以直接使用但有时也需要重载 - 成员函数 vs 全局函数
- ( ), [ ], ->和赋值(=, +=, -=等)
运算符必须重载为成员函数(有时函数需要被重载为常成员函数)
支持交换律的运算符必须重载为全局函数 - 拷贝构造函数和转换构造函数
- 自定义String类 vs 标准string类
Chapter 12 继承
is-a与has-a的区别
- is-a代表继承
- 一个类继承自另一个类
- 属于更加专有的一类对象
- 可以一个类继承一些行为,也可修改甚至创建新的行为
例如vehicle类,有加速、减速等行为,car继承vehicle,同样有这些行为,也可以有新的行为(如打开后备箱等)
- has-a代表组合关系
- 一个对象包含其它的成员对象
例如Employee类中包含firstname、lastname、birthdate、hiredate等对象
- 一个对象包含其它的成员对象
继承
- 作用
- 软件复用;
- 对事物进行分类;
- 支持软件的增量开发;
- 对概念进行组合。
- 声明:
class <派生类名>:<继承方式><基类名> {……};
(多继承在后面添加) - 访问规则
- 派生类吸纳基类的数据成员及成员函数(隐性)
- 派生类的成员函数如何访问基类的数据成员和成员函数?(public继承)
- 可以访问基类中的非private数据成员及成员函数
- 无法直接访问基类的private成员
- 可以通过基类中提供的非private成员函数实现对基类中private数据成员进行修改
- 通常派生类需要重新定义一些成员函数,以实现派生类特有的功能及操作
- 继承中的访问模式
- 基类的public成员能够被程序中所有函数访问
- 基类的private成员只能被基类的成员函数和友元访问
- 基类的protected成员只能被基类的成员和友元以及派生类的成员和友元访问。
- 注意:不能被类的实例(对象)访问。
- 派生类如何访问基类的数据成员?
- 默认情况:派生类成员简单地使用成员名就可以引用基类的public成员和protected成员。
- 当派生类重新定义了基类的成员函数时,访问方式:
base-class name:: + 成员函数
- 只要在派生类中重写基类的函数(函数名相同,即使参数不同),就无法默认调用基类的相关函数,此时基类中的函数需要“基类::函数名”来调用。
派生类与基类的关系
初始化
- 如果派生类的构造函数没有显示调用基类的构造函数,C++将尝试隐式调用默认的构造函数(前提:基类需要有缺省的构造函数)
- 采用成员初始化器列表显示地初始化成员对象和调用基类的构造函数,可以防止重复初始化
头文件:在派生类头文件中使用#include包含基类头文件
- 告诉编译器基类的存在
- 让编译器根据类的定义确定对象的大小,派生类的对象大小取决于派生类显式定义的数据成员和继承自基类的数据成员
- 让编译器能够判断派生类是否正确的使用了基类的成员
注意事项
- 影响数据的有效性检查
派生类可以直接访问基类的protected数据成员 - 派生类依赖于基类的实现
- 基类的数据成员发生改变有可能影响派生类的实现
- 软件“易碎”或“脆弱”,不够健壮
- 基类仅向派生类提供服务,则可使用protected类型声明,其他情况慎用protected
- 影响数据的有效性检查
在派生类中重定义基类成员
- 通过调用基类的public成员函数来访问基类的私有数据成员
- 当功能相同时,尽量调用成员函数,以避免代码拷贝。
- 重定义基类成员函数时,一定要使用“::”访问基类成员,否则会引起无限递归,例如:earnings()
- 注意print()和earnings()的重新定义:调用基类的print()和earnings()函数
符合软件工程要求:使用继承,通过调用成员函数隐藏了数据,保证了数据的一致性。
基类、派生类中的构造与析构函数
- 构造顺序
- 建立派生类的实例对象时、必须调用基类的构造函数来初始化派生类对象的继承成员。
- 派生类的构造函数既可以隐式调用基类的构造函数,也可以在派生类的构造函数中通过给基类提供初始化值显式地调用。
- 基类构造函数->派生类构造函数
- 析构顺序
析构函数的调用顺序和构造函数的顺序相反,因此派生类的析构函数在基类析构函数之前调用。 - 包含成员对象的情况,假设基类和派生类都包含其他类的对象:
- 在建立派生类的对象时,首先执行基类成员对象的构造函数,接着执行基类的构造函数,然后执行派生类的成员对象的构造函数,最后才执行派生类的构造函数。析构函数的调用次序与调用构造函数的次序相反。
基类成员对象初始化—>基类的构造函数—>派生类成员对象初始化—>派生类构造函数 - 建立成员对象的顺序是对象在类定义中的声明顺序。成员初始化器中的顺序不影响建立对象的顺序。
- 在建立派生类的对象时,首先执行基类成员对象的构造函数,接着执行基类的构造函数,然后执行派生类的成员对象的构造函数,最后才执行派生类的构造函数。析构函数的调用次序与调用构造函数的次序相反。
继承小结
- 公有继承
- 基类成员对其对象的可见性:
公有成员可见,其他不可见。这里保护成员同于私有成员。 - 基类成员对派生类的可见性:
公有成员和保护成员可见,而私有成员不可见。这里保护成员同于公有成员。 - 基类成员对派生类对象的可见性:
公有成员可见,其他成员不可见。
一定要区分派生类的对象和派生类中的成员函数对基类的访问是不同的。
- 基类成员对其对象的可见性:
- 私有继承
- 基类成员对其对象的可见性:
公有成员可见,其他成员不可见。(同前) - 基类成员对派生类的可见性:
公有成员和保护成员可见,私有成员不可见。(同前) - 基类成员对派生类对象的可见性:
所有成员都不可见。
保护继承与私有继承类似
- 基类成员对其对象的可见性:
- 总结
不论公有继承还是私有继承,基类成员对于派生类的访问权限是不变的。(可访问公有或保护类型成员)
变化的是派生类的对象以及派生类的派生类对基类成员的访问权限。
基类和派生类的定义
Protected成员
基类和派生类的关系:public,proteced,private
继承关系中构造函数和析构函数顺序
复合(has-a)和继承(is-a)的关系
“使用”和”知道”
Chapter 13 多态
面向对象三大要素
数据封装
继承
多态
多态
- 解释1:同样的消息被类的不同对象接收时导致的完全不同的行为的一种现象。这里所说的消息即对类的成员函数的调用。
- 解释2:通过指向派生类的基类指针,调用派生类的函数; 将不同的派生类对象都当作基类来处理,并根据对象不同产生不同的行为,以屏蔽各派生类对象之间的差异。写出通用的代码,使得程序员可以方便地处理普遍性问题。
- C++语言支持两种类型的多态:
- 编译时的多态(静态多态) ——函数重载
- 运行时的多态(动态多态) ——虚函数
- 多态性提高了软件的可扩展性,使得可以用与接收消息对象类型无关的方式编写
继承中的对象关系
- Invoking Base-Class Functions from Derived-Class Objects(基类指针指向派生类,调用基类函数)
结论: 调用基类还是派生类的函数,取决于句柄的类型,而不是句柄指向的实际对象类型 - Aiming Derived-Class Pointers at Base-Class Objects(派生类指针指向基类,错误)
- Derived-Class Member-Function Calls via Base-Class Pointers(基类指针指向派生类,调用派生类函数,错误)
结论: 通过对象句柄,仅能调用该句柄类型的成员函数
解决办法: downcasting(dynamic_cast)
虚函数
语法
- 调用哪个(基类/派生类)虚函数,由对象类型而不是句柄类型决定.
- 虚函数用于继承结构中的基类和派生类,以实现多态.
- 派生类中覆盖(Overridden)的虚函数和基类中的虚函数必须函数签名和返回值均相同.
包括函数名称、返回值、参数个数、类型、是否const都要一致
调用虚函数的两种方式
- 通过指向派生类的基类指针(或引用)调用,程序会在执行时(execution time)根据对象类型动态选择合适的派生类函数 – 动态绑定( dynamic binding )或延迟绑定( late binding ).
- 通过对象名和点操作符调用,程序在编译时(compile time)即根据对象类型确定函数– 静态绑定( static binding ).
- 只有通过引用或指针来访问对象的虚函数时才进行动态绑定。
- 通过引用或指针访问对象的非虚成员函数,采用静态绑定。(与句柄类型的成员函数代码绑定)
- 通过“类名+::”访问对象成员函数,也采用静态绑定。
- 基类构造函数中对虚函数的调用不采用动态绑定。
- 通过指针访问其他成员函数并调用虚函数时仍需动态绑定。
限制
- 只有类成员才能声明为虚函数
- 静态成员函数不能是虚函数
- 构造函数不能是虚函数
- 析构函数可以是虚函数,并且通常声明为虚函数(注意基类和派生类的析构函数不同名)
例:调用的是basePlusCommissionEmployee的析构函数1
2commissionEmployeePtr = &basePlusCommissionEmployee
delete commissionEmployeePtr;
纯虚函数与抽象类
- 语法 : virtual void function() const = 0;
- Abstract Class(抽象类): 包含一个或者多个纯虚函数的类。无法实例化(但可以声明指针和引用),只能用于继承。
Shape obj; // Error
Rectangle objRectangle;
Shape *ptr = &objRectangle; // OK
Shape &ref = objRectangle; // OK - 作用:为派生类提供一个基本框架或公共接口。
- Concrete Class(具体类): 不包含纯虚函数,可以实例化。
总结
- 成员函数是否声明为虚函数,取决于是否需要多态性支持
- 虚函数是否声明为纯虚函数,取决于该函数对于当前类是否有意义,以及当前类是否需要实例化
Chapter 14 模板
概念
利用一种完全通用的方法来设计函数或类而不必预先说明将被使用的每个对象的类型,利用模板功能可以构造相关的函数或类的系列,因此模板也可称为参数化的类型。——泛型编程(Generic Programming)
分类
类模板(class template)和函数模板(function template)。
函数模板
- 声明
template关键字+尖括号<>括起来的template parameter list(模板参数列表) - 参数列表
- 参数称为formal type parameter(形式类型参数)。在函数调用时替换为基本数据类型或用户自定义数据类型。
- 每个参数都必须以关键词typename (或class )起头,参数和参数之间必须以逗号分隔,如 template < typename T, typename V>
- 理解
- 函数模板只是说明,不能直接执行,需要特化为模板函数后才能执行。
- 处理过程
- 在程序中说明了一个函数模板
- 编译系统发现有一个相应的函数调用
例如:printArray( a, ACOUNT ); - 编译器寻找和使用最符合函数名和参数类型的函数调用
- 根据实参中的类型来确认是否匹配函数模板中对应的形参,然后生成一个重载函数(函数模板特化)。(该重载函数的定义体与函数模板的函数定义体相同)
- 编译这个新建的函数
重载
- 允许有其它同名的函数模板,但参数不同
- 允许有其它同名的函数,但参数不同
- 编译器挑选最佳匹配的函数或模板
- 编译器通过匹配过程确定调用哪个函数。
- 首先,编译器寻找和使用最符合函数名和参数类型的函数调用。如果找不到,则检查是否可以用函数模板产生符合函数名和参数类型的模板函数。
- 如果有多个函数和调用函数相匹配(且匹配程度相同),编译器会认为有二义性,将产生编译错误
- 如果没有找到符合函数,将考虑调用强制类型转换
类模板
- 定义
1、类模板通过允许将泛型类实例化为明确类型的类来实现软件复用
2、类是对问题空间的抽象,而类模板则是对类的抽象,即更高层次上的抽象。
3、程序中可以首先定义一个类模板,然后通过使用不同的实参生成不同的类。
4、类模板的定义格式:5、每个“类型形参”前必须加typename or class关键字,对类模板进行实例化时,代表某种数据类型;也可以是普通数据类型形参,实例化时代表具体数据,如1
2
3
4
5template <typename <类型参数>> //模板声明
class <类名> // 类定义
{
……
};6、类模板中成员函数可以放在类模板的定义体中(此时与类中的成员函数的定义方法一致)定义,也可以放在类模板的外部来定义,格式为:1
2
3
4
5
6
7template < typename arg1, int arg2, typename arg3>
class myclass
{
arg1 buffer[arg2];
arg3 x; //类的定义体
};7、1
2
3
4template <类型形参表>
函数返回类型 类模板名<类型名表>::成员函数(形参)
template < typename arg1, int arg2, typename arg3>
void myclass<arg1,arg2,arg3>::print() {}- 类模板定义只是对类的描述,它本身还不是一个实实在在的类。
- 类模板不能直接使用,必须先特化(specialized)为相应的模板类,定义模板类的对象(即实例)后,才可使用(创建对象等)。
- 可以用以下方式创建类模板的特化及对象。
类模板名<类型实参表> 对象名表;
如:Myclassa;
此处的<类型实参表>要与该模板中的<类型形参表>匹配,要具有同样的顺序和类型,否则会产生错误!
非类型参数和类模板的缺省类型
非类型的模板形参
- 特化时作为常量对待
- 可以有缺省参数
- Example:
1
2
3Template header:
template< typename T, int elements = 10 >
Declaration: Stack< double, 100 > salesFigures;
类型形参也可以有默认实参
Example
Template header: template< typename T = string >
Declaration: Stack<> jobDescriptions;Explicit specializations(显式特化)
- 当某一特殊数据类型不能使用通用类模板时,可以定制处理(即:重定义该类型的类模板。)
- 显式特化:
Stack< Employee > specialization
template<>
class Stack< Employee >{…};
完全代替了通用类模板 Stack< Employee >
没有使用原来类模板的任何内容,甚至可以包含不同的成员 - 优点:可以兼容已有的基于模板的操作
函数模板与友元函数
- 函数声明为类模板的每一个特化的友元
friend void f1();//无参数
f1 is a friend of X< double >, X< string >, etc. - 函数声明为类模板的某一类型参数特化的友元
friend void f2( X< T > & );//带参数
f2( X< float > & ) is a friend of X< float > but not a friend of X< string > - 另一个类的成员函数可以声明为类模板每一个特化的友元(无参数)
friend void A::f3();
f3 of class A is a friend of X< double >, X< string >, etc. - 另一个类的成员函数可以声明为类模板某个类型参数特化的友元(带参数)
friend void C< T >::f4( X< T > & );
C< float >::f4( X< float > & ) is a friend of X< float > but not a friend of X< string > - 另一个类可以声明为类模板的每一个特化的友元
friend class Y;
(无类型参数)
Every member function of class Y is a friend of X< double >, X< string >, etc. - 另一个类可以声明为类模板的某个类型参数特化的友元
friend class Z< T >;
(有类型参数)
Class-template specialization Z< float > is a friend of X< float >, Z< string > is a friend of X< string >, etc.
函数模板与static
- 从类模板实例化的每个模板特化有自己的static数据成员
- 该模板特化的所有对象共享一个static数据成员
- static数据成员必须被定义,在文件范围内被初始化
- 例子
1
2
3
4
5
6
7template <typename T>
class A
{ static int x;
};
template < typename T> int A<T>::x=0;
A<int> a1,a2; // a1和a2共享一个x
A<double> b1,b2; // b1和b2共享一个x
总结
- 模板与继承的区别
当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。
当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。 - 函数模板和函数模板重载
- 类模板
- 定义
- 类型参数和非类型参数
- 实例创建
- 模板和继承
- 模板和友元
- 模板中的静态成员
Chapter 16 异常处理
异常处理思路
- C++提供了一些内置的语言特性来抛出(throw)异常,用以通知“异常已经发生”,然后由预先安排的程序段来捕获(catch)异常,并对它进行处理。
- 捕获并处理异常的程序段
1
2
3
4
5
6
7try
{复合语句}
catch(异常类型声明1)
{复合语句}
catch(异常类型声明2)
{复合语句}
… - 流程解释
- 将可能抛出异常的程序段嵌在try块之中,并通过throw操作创建一个异常对象并抛掷。
- 如果此时没有引起异常,程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。
- 如果存在异常,顺序检查try块后面的catch子句,匹配的catch子句将捕获并处理异常(或继续抛掷异常)。
- 如果匹配的处理器未找到,则默认调用terminate函数,其缺省功能是调用abort终止程序。
标准库异常类
C++标准库提供的逻辑异常:
- invalid_argument异常,接收到一个无效的实参,抛出该异常。
- out_of_range异常,收到一个不在预期范围中的实参,则抛出。
- length_error异常,报告企图产生“长度值超出最大允许值”的对象
- domain_error异常,用以报告域错误(domain error)。
C++标准库提供的运行时异常()
- range_error异常,报告内部计算中的范围错误。
- overflow_error异常,报告算术溢出错误。
- underflow_error异常,报告算术下溢错误。
- 以上三个异常是由runtime_error类派生的。
- bad_alloc异常。当new()操作符不能分配所要求的存储区时,会抛出该异常。它是由基类exception派生的。
exception类接口
1
2
3
4
5
6
7
8
9
10
11
12namespace std{ //注意在名字空间域std中
class exception{
public:
exception() throw() ; //默认构造函数
exception(const exception &) throw() ; //复制构造函数
exception &operator=(const exception&) throw() ;
//复制赋值操作符
virtual ~exception() throw() ; //析构函数
virtual const char* what() const throw() ;
//返回一个C风格的字符串,目的是为抛出的异常提供文本描述
};
}
catch语句详解
- catch子句由三部分组成:关键字catch、圆括号中的异常声明以及复合语句中的一组语句。
- catch子句不是函数,所以圆括号中不是形参,而是一个异常类型声明,可以是类型也可以是对象。
- catch子句的使用:由系统按抛出的异常类型自动在catch子句列表中匹配(找寻能够处理的第一个catch处理器)。(类型相同或是继承类)
- catch子句可以包含返回语句(return),也可不包含返回语句。包含返回语句,则整个程序结束。不包含返回语句,则执行最后一个catch处理器之后的下一条语句(不会回到异常发生点)。
- 如果没有异常发生,继续执行try块中的代码,与try块相关联的catch子句被忽略,程序正常执行
- 注意
- try与catch间不能添加其他代码
- 一个catch处理器只能有一个参数
- 在try语句后两个不同catch处理器中捕获相同异常类型——错误!
- 通过引用捕获异常对象,能够去除表示抛出的异常对象的复制开销
- catch(…){ /代码/ } // catch_all子句
- 任何异常都可以进入这个catch子句。这里的三个点称为省略号。花括号中的复合语句用来执行指定操作。
- catch_all子句可以单独使用,也可以与其它catch子句联合使用。如果联合使用,它必须放在相关catch子句表的最后。
- 如果catch_all子句放在前面进行某项操作,则其它的操作应由catch子句重新抛出异常,逆调用链去查找新的处理子句来处理。
重新抛出
- 当catch语句捕获一个异常后,可能无法处理或不能完全处理异常,完成某些操作后,该异常必须由函数链中更上级(更外层)的函数来处理,这时catch子句可以重新抛出(throw;)该异常,把异常传递给函数调用链中更上级的另一个catch子句,由它进行进一步处理。
- throw;//异常的再抛出
- 由其外层的catch捕获
- 外层无catch处理器时,编译器调用terminate终止程序
异常指定
int someFunction( double value ) throw (a, b, c) {// fun body}
- 异常指定:列出函数可抛出的异常
- 函数可抛出指定异常或派生类型
- 当函数抛出异常不在异常指定中,调用C++标准库的unexpected函数
- 不带异常指定的函数可以抛出任何异常
- void g();
- 如果异常指定为throw(),表示该函数不抛出异常。
- 抛出一个函数异常规格中未声明的异常或者包括空异常规格下抛出异常,将由unexpected()处理
- 虚函数中的异常指定
派生类的虚函数的异常指定必须与基类虚函数的异常一样或更严格。因为当派生类的虚函数被指向基类类型的指针调用时,保证不会违背基类成员函数的异常规范。
例子:
class CBase{
public:
virtual int fun1(int) throw();
virtual int fun2(int) throw(int);
virtual string fun3() throw(int,string);
};
class CDerived:public CBase{
public:
int fun1(int) throw(int);
//错!异常规范不如throw()严格
int fun2(int) throw(int); //对!有相同的异常规范
string fun3() throw(string); }
//对!异常规范比 throw(int,string)更严格
}
unexpected
unexpected()调用set_unexpected() 指定的函数
set_unexpected 定义在 <exception>如果set_unexpected() 未指定被调用函数,默认情况下,terminate()被调用
terminate定义在<terminate>
terminate()调用set_terminate()指定的函数
如果set_terminate()未指定被调用函数,默认调用abort(),退出程序,不会释放内存,导致资源泄漏
函数set_terminate和set_unexpected取函数指针为参数。每个参数指向返回类型为void和无参数的函数。
栈展开(stack unwinding)
- 概念:因发生异常而逐步退出复合语句和函数定义的过程。
- 具体过程
- 当异常被抛出但没有在特定的域内被捕获时,该函数调用堆栈将展开,并试图在下一个外部try-catch语句中处理
- 展开函数调用堆栈意味着在调用链中没有捕获异常的函数将会终止执行,并控制返回到最初调用该函数的语句中
- 如果该调用语句被一个try语句包含,则试图捕获该异常;否则堆栈展开将继续发生
- 如果在main函数中仍没有找到匹配的Handler, 则调用terminate函数(该函数缺省调用abort, 不执行栈展开), 结束程序.
- 每抛出一个异常,首先找到能捕获处理该异常的catch块;
- 利用throw语句中的”实参”对相应的”catch”块的“形参”进行初始化;
- 检查从抛出异常的try块首到throw之间已进行构造但尚未析构的那些处于堆栈中的局部对象,自动进行退栈和析构处理。
- 注意点
- 在栈展开期间,在退出的域中有某个局部量是类对象,栈展开过程将自动调用该对象的析构函数,完成资源的释放。
- 特别说明:由堆栈展开而调用的析构函数抛出了异常,那么terminate将被调用
- 调用terminate函数的情况:
- 对于抛出的异常,异常机制找不到匹配的catch块
- 析构函数试图在堆栈展开时抛出异常
- 在没有异常要处理时试图重新抛出异常
- 调用函数unexpected将默认调用函数terminate
- 在调用terminate函数时,函数set_terminate可以指定被调用的函数。否则默认调用abort函数(不会对自动或静态存储类对象调用析构函数)
构造、析构函数与异常处理
- 异常处理抛出前为try语句块中构造的所有局部对象自动调用析构函数
- 如果一个对象包含成员对象,且在外部对象完全构造前抛出了异常,那么异常出现之前构造的成员对象将被析构
- 如果在异常发生时数组对象只部分构造,则只有已构造的部分被析构
继承与异常处理
如果catch捕获基类类型异常对象的指针或引用,则可以捕获该基类所派生的异常对象的指针或引用。这样允许多态处理错误。
1 | try |
new与异常处理
- Function set_new_handler
- 函数参数为没有参数没有返回值的函数指针
- 一旦注册了new处理器,则不会抛出bad_alloc
- C++标准指出new处理器需要完成以下任务的一个
- 通过释放其他动态分配的内存,再次尝试分配
- 抛出bad_alloc异常
- 调用函数abort或exit结束程序
总结
- 异常的概念
- try…throw…catch模块的语法和处理流程
- 栈展开过程(与构造和析构的关系)
- 对其它异常的处理方法
- new异常的处理
- 动态内存分配异常的处理
Chapter 17 文件
文件的基本概念
- 本节中文件指的是磁盘文件。C++文件(file)分为两类:文本文件和二进制文件。
- 文本文件由字符序列组成,也称ASCII码文件,在文本文件中存取的最小信息单位为字符(character)
- 二进制文件中存取的最小信息单位为字节(Byte),如.obj
- C++把每一个文件都看成一个有序的字节流,每一个文件或者以文件结束符(EOF)结束,或者在特定的字节号处结束。
- Bits (二进制位)
- 0 or 1
- 计算机支持的最小数据项
- 计算机电路执行位处理
- 所有数据项最终由位组成
- Characters(字符)
- 数字、字母和专门的符号称为“字符”
- 能够在特定计算机上用来编写程序和代表数据项的所有字符的集合称为“字符集”
- Char以字节形式存储 (8 bits)
- Wchar_t 占多个字节(用于非英文字符集的表示)
- Fields(字段或数据项)
- 由字符组成
- 代表一定的含义
- Example:姓名,颜色等等
- Records(记录)
- 由多个字段组成
- C++中表现为:类
- Example
An employee’s record might include id#, name, address, etc.
记录的关键字:A record key is a field unique to each record
流
- 当打开一个文件时,该文件就和某个流关联起来了。对文件进行读写实际上受到一个文件定位指针(file position pointer)的控制。
- 输入流的指针也称为读指针,每一次提取操作将从读指针当前所指位置开始,每次提取操作自动将读指针向文件尾移动。
- 输出流指针也称写指针,每一次插入操作将从写指针当前位置开始,每次插入操作自动将写指针向文件尾移动。
- 为了在C++中执行文件处理,必须包含头文件
<iostream>和<fstream>
文件处理
必须包含头文件: #include<fstream>
1 | basic_ifstream (for file input) |
提供了处理字符流的模板特化
1 | ifstream:从文件中读入字符(读文件) |
生成这些流类模板特化的对象,即可打开文件
程序和文件之间通过流对象交互
写数据到文件
- 创建ofstream对象
- 构造函数ofstream(const char* filename,int mode)
filename:文件名由文件的主名和扩展名两部分组成。
mode:
ios::out :缺省模式,覆盖文件中已有数据
ios::app:向文件末尾添加数据
例:ofstream outClientFile( “clients.dat”, ios::out );
建立了一个到文件的“通信通道”
如果该文件名不存在,则新建一个同名文件 - 对于已创建的文件对象,使用成员函数打开文件
- 与构造函数的参数相同,可以先创建再打开
- 例:
ofstream outClientFile;
outClientFile.open("clients.dat“, ios::out);
说明
in :(从文件读取) 打开方式只要含in,如文件不存在则返回失败。在打开为输入输出方式时(同时用out),编程应注意判断是否失败,失败时千万不可再写入文件。
out: (写入文件) 如文件不存在,则建立新文件,如文件存在,如果同时未设定app, in,则文件清空。
trunc:(打开文件,并清空它)文件不存在则建立新文件,与out默认操作相同。但与in配合,文件不存在则返回失败。
app:(写入文件,添加在末尾)原文件内容保留,新数据接在尾部。
ate: (at end,打开文件,文件指针在文件尾) 文件指针可以移动,即新数据可写到任何位置。文件是否清空由其它标识决定。
trunc/app/ate最好配合out、in等一起用,因为不同的C++平台,要求不同,一起用不会出错。如不一起用,至少VC++不认这种格式。
binary标识以二进制方式打开文件。同时用out时,如文件不存在,则建立新文件,并且新文件能用,不必清状态字。
- 既输入又输出
- fstream iofile;
- iofile.open(“myfile.txt”,ios::in|ios::out);
文件的打开和关闭
- 打开文件时应该判断是否成功,若成功,文件流对象值为非零值,不成功为0(NULL),文件流对象值就是指它的地址。
- 说明一个文件流对象,这又被称为内部文件:
ifstream ifile;//只输入用
ofstream ofile;//只输出用
fstream iofile;//既输入又输出用 - 使用文件流对象的成员函数打开一个磁盘文件。
iofile.open(“myfile.txt”,ios::in|ios::out);
也可直接通过构造函数打开文件:
fstream iofile(”myfile.txt”,ios::in|ios::out); - 使用提取(>>)和插入(<<)运算符对文件进行读写操作,或使用成员函数(get\put)进行读写。
- 关闭文件。三个文件流类各有一个关闭文件的成员函数 :
void ifstream::close();
void ofstream::close();
void fstream::close();
使用很方便,如:iofile.close();
文件关闭
- 关闭文件时,系统把该文件相关联的文件缓冲区中的数据写到文件中,保证文件的完整,收回与该文件相关的内存空间,把磁盘文件名与文件流对象之间的关联断开,可防止误操作修改了磁盘文件。
- 关闭文件并没有取消文件流对象,该文件流对象又可与其他磁盘文件建立联系。文件流对象在程序结束时,或它的生命期结束时,由析构函数撤消。它同时释放内部分配的预留缓冲区。
- 调用析构函数也会关闭相应正在打开的文件
在Windows平台下如果以“文本”方式打开文件,当读取文件的时候,系统会将所有的”\r\n”转换成”\n”;当写入文件的时候,系统会将”\n”转换成”\r\n”写入。如果以”二进制”方式打开文件,则读/写都不会进行这样的转换。
文件定位指针:
文件中下一个被读取或写入的字节号
istream、ostream都提供了修改文件定位指针的成员函数
seekg(long pos, int mode)—istream
seekp(long pos, int mode)—ostream
规范化操作
- 在面向对象的程序设计中,信息总是放在对象的数据成员里。这些信息最终需要保存到文件中。
- 读取文件中对象的信息时,必须重新创建对象,把数据读入对象,在运行过程中,对放在对象的数据成员里的信息利用和修改,运行结束时必须把这些信息重新保存到文件中,然后关闭文件
- 在面向对象的C++程序设计中,文件应该在构造函数中打开,并创建对象;而在析构函数中保存和关闭文件,并撤销对象。
总结
文件流分为三种
- 文件输入流(ifstream), 文件输出流(ofstream),文件输入/输出流(fstream)
- 文件处理步骤
- 定义文件流对象
打开文件
void open(const unsigned char *filename, int mode, int access=filebuf::openprot);
关闭文件
- 调用成员函数close()
常用操作
- sfile.unsetf(ios::skipws);//把跳过空格控制位置0,即不跳过空格,否则空格全部未复制
附录
点击下载所有作业建议研究13章(week9)、17章作业