类
class
类:包含值、相应操作集的模板,将相关信息片段组织成复合值, 使得可以整体对其进行操作
概念
实体
object:对象,属于一个类的所有值
instance:实例,单个对象
field/instance variable:域/实例变量,类的分量
method:应用于类实例的操作
- C++中方法的使用和传统函数类似,新名称只是为强调其同 所属的类紧密联系
- 传统函数称为free function,不被约束于特定类
权限
private
:私有,声明在private section中的域仅对该类 本身可见(默认)public
:公有,声明在public section中的域对所有用户 可见- 现代面向对编程不鼓励在类中声明public实例变量
protected
:受限,声明在protected section中的成员对 所有子类都可以访问,但用户不能访问
类&结构体
C++中:结构类型和类基本上以同样方式实现,类是struct
的扩展
- 结构体:默认访问权限是
public
,类默认为private
- 类:允许高级继承、方法,成员默认
private
- 对一般成员变量,类和结构体在内存布局上完全一致,如: 顺序、内存布局
消息传递模型
面向对象程序设计中,对象间通过信息发送、请求实现对象间通信, 将传递的这些信息称为消息
1 | receiver.name(arguments) |
- 对象间消息发通常理解为一个对象调用另一个对象的方法
- sender:发送方,初始化方法的对象
- receiver:接收方,消息的目标对象
接口实现分离
C++中类接口、实现相分离时,类自身定义仅存在与其
.h
文件实现放在
.cpp
中作为独立方法定义,需要以类名作为限定符、::
作为分隔的方式表明自己所属的类
类继承
1 | class subclass: public superclass{ |
子类
subclass
继承父类superclass
所有public
、protected
成员,private
成员仍然保持私有特性可以继承模板类
- 子类甚至可以不包含任何代码,仅为简化类型名
1
2class StringMap: public Map<string, string>{}
// 创建不包含代码的子类,简化代码
- 子类甚至可以不包含任何代码,仅为简化类型名
子类局限
子类对象是其所属父类的实例,但是有其局限
将子类对象赋值给父类对象会导致子类特有实例变量值被丢弃 (因为父类对象在栈中空间较小)
常用解决方法是使用指针,指针变量大小相同
- 但使用指针会使内存管理变得复杂
- 为类定义析构函数可以管理内存,但是指针越界时,不能 保证析构函数会在合适的时间调用
最好方法是避免使用类继承,并创建独立的类管理自身堆内存, 否则
- 完全禁止拷贝:定义私有拷贝构造函数、重载赋值操作符, 但这会使得对象难以嵌入大型数据结构
- 执行深拷贝:用户需要承担内存管理,重载拷贝构造函数、 重载赋值操作符
Multiple Inheritance
多重继承:类可以继承自多个父类
- 多重继承在实际编程过程中可能导致程序复杂、模糊不清
- 多重继承的多个父类可能拥有多个相同方法名、数据域名
- C++中单继承已经足够复杂,最好避免使用多重继承
final
final
:指定虚函数不能被派生类覆盖、类不能被继承
语法
- 虚函数:在声明符之后
- 类:紧跟类名后
1
2
3
4
5
6
7struct A final{}
struct B{
virtual void foo();
}
struct C: B{
void foo() final;
}
override
override
:指定派生类虚方法覆盖基类虚方法
语法:在成员函数声明之后 (其他情况下
override
甚至不是关键字)用途:有助于防止代码出现意外继承行为
- 即不是强制性关键字,是辅助性、可选
- 非虚函数可以通过类型转换调用基类方法,不算覆盖???
Iterator
迭代器:指向集合中一个特定元素,每次可以通过单步递进方式访问 其他元素
- 迭代器使用
*
操作查找其指向的值
迭代器层次结构
InputIterator
:允许读值OutputIterator
:允许给解析的迭代器赋新值ForwardItertor
:结合InputIterator
、OutputIterator
,允许读写值BidirectionIterator
:在ForwardIterator
基础上允许向后 迭代,增加--
操作符RandomAccessIterator
:在BidirectionIterator
基础上 允许向前、向后移动任意元素,包含全部关系符
指针作为迭代器
指针类型已经实现了
RandomAccesIterator
提供的所有操作符 ,所以可以使用指针作为迭代器很多实现已经将
iterator
作为类中嵌套迭代器的类型,要用typedef
重命名指针类型为iterator
1
typedef ValueType * iterator;
- 如:编译器内部利用迭代器,将基于范围
for
展开
- 如:编译器内部利用迭代器,将基于范围
- 编译器内部
iterator
类型指针变量参见 cppc/basics/intro
类方法
Constructor
构造函数:创建对象
构造函数名与类名完全一样
构造函数无法被子类继承,只能在初始化列表中调用
- 缺省编译器调用父类默认构造函数
- 若没有构造函数,则必须显式在初始化列表中初始化
执行阶段
初始化阶段:初始化列表
调用父类构造函数初始化父类成员变量
- 缺省调用默认构造函数
- 若父类没有默认构造函数,必须在初始化列表中显式 调用构造函数
初始化类自身数据域
- 对一般类型,缺省类似于普通未赋值初始化
- 对类类型成员变量,缺省没有显式初始化则调用默认 构造函数
计算阶段:执行构造函数体
- 在函数体中执行的都是赋值,不是初始化
Initializer List
初始化列表:初始化父类、自身数据域
- 位于的构造函数体花括号前、参数列表之后,用
:
和参数列表 分隔
初始化父类
父类名后用括号括起来的参数列表,参数列表必须和父类 某一构造函数原型相匹配
1
Foo(string name, int id):(name, id){};
父类数据域后用括号括起的该数据域的初始化值(此方法 也可以用于初始化类自身数据域)
1
Foo(string name, int id): name(name), id(id){};
调用父类构造函数
1
Derived(string name, int id):Base(name, id){};
初始化自身数据域
类类型成员变量最好使用初始化列表初始化,可以避免 构造函数初始化阶段调用成员变量默认构造函数
必须放在初始化列表中成员变量
- 常量成员:常量只能初始化,不能赋值
- 引用类型:引用必须在定义时初始化,不能重新赋值
- 没有默认构造函数的类类型
成员变量按照其在类中声明顺序初始化,而不是在初始化 列表中的顺序
- 为避免因成员变量初始化依赖导致的未定义,应该按照成员 变量声明的顺序初始化
Default Constructor
默认构造函数:没有参数的构造函数
- 若类没有默认构造函数,编译器会自行定义
Copy Constructor
1 | Foo(const Foo &f){}; |
拷贝构造函数:使用同类实例初始化创建新对象
用途
- 复制对象
- 传值调用时,隐式调用拷贝构造函数构造新对象传参
类中没有定义拷贝构造函数时,编译器会自行定义一个
- 若类中有指针变量、并有动态内存分配,则必须定义拷贝 构造函数
explicit
epxplicit
关键字:声明为explicit
构造函数不能在隐式转换中
使用
- 用途
- 希望函数参数只能是给定类型,可以禁止隐式类型转换
Destructor
析构函数:类的对象消亡时,析构函数被自动调用
- 析构函数可以完成各种清理操作
- 最重要:释放对象所创建的所有堆内存
- 关闭对象打开的任何文件
- 析构函数名称:类名前加上
~
- 析构函数特点
- 没有返回类型
- 不能重载
- 每个类只有一个无参的析构函数
- 良好设计的C++应用中,每个类都需要堆其对内存负责
Operator Overloading
操作符重载:扩展标准操作符以适应新的数据类型
编译器遇到操作符时,会根据操作符操作数的类型确定其 操作语义
重载操作符函数名由关键字
operator
后跟操作符构成- 操作符的左右操作数类型、传值方式
- 操作符返回值
1
2
3ostream & operator<<(ostream & os, Point pt);
// 流对象不能拷贝,必须引用传递
// 返回依然是流引用
有些类型根本没有定义过某些操作符,虽然实际上是扩展该类型 操作符,但是也成为重载操作符
- overload:重载,使用相同名字的不同版本类方法
重载方式
C++提供两种机制用以重载内置操作符使得可以适用于新定义类
类方法:在类中用方法重载操作符
1
2
3
4
5
6
7
8
9
10
11// .h
Class Point{
Public:
bool operator==(Point rhs);
// 类中声明操作符重载方法
}
// .c
bool Point::operator==(Point rhs){
// 实现中限定为`Point`方法
}- 左操作数为该类型对象(声明、实现省略)
- 右操作数作为形参传递
- 编译器把左操作数视为接收者,将右操作数作为形参传递
free function:在类外使用自由函数重载定义操作符
1
2
3
4
5
6// .h
bool operator==(Point pt1, Point pt2);
// .c
bool operator==(Point pt1, Point pt2){
}- 二元操作符的两个操作数都作为形参传递
- 操作符重载自由函数一般需要声明为类的友元函数,使其 能够访问类的私有实例变量
++
/--
重载++
/--
时,必须指明是重载前缀还是后缀形式
- 额外传入无意义整形参数说明重载后缀形式
- 常用于枚举类型,方便遍历枚举类型
1 | Direction operator++(Direction & dir){ |
=
赋值操作符被定义为返回其左值,所以重载赋值操作符时注意 返回值
赋值操作符开头往往检查左、右操作数地址是否一样
- 避免不必要拷贝操作、逻辑错误
()
- 重载函数调用操作符
()
将可以像函数一样调用对象
1 | class AddFunction{ |
- 重载此方法类称为function class(函数类),其实例称为 function object/functor(函数对象、函数子)
const
方法
C++允许在方法参数表后增加关键字const指定方法,声明 其不改变其对象的状态
- 在接口原型、实现中都应该使用
const
关键字约束
1 | // .h |
Friend
友元:允许访问类中私有实例变量的元素(不是类方法)
友元函数:允许访问类中私有变量的自由函数
1
2
3
4
5friend prototype;
// 在类定义中声明友元函数
class Point{
friend bool operator==(Point p1, Point p2);
}友元类:允许访问类中私有实例变量的类
1
2friend class cls_name;
// 在类中定义中声明友元类- 友元类的声明不是双向的,需要两个类都显式声明另一个类 为友元类,才能相互对方私有变量
典型方法
Accessor/Getter:访问器/读取器:获取实例变量值的函数
- 为方便,读取器的命名通常以
get
为前缀
- 为方便,读取器的命名通常以
Mutator/Setter:设值方法/设值器,为特定实例变量 设置值的方法
- 为方便,设置器的命名通常以
set
为前缀 - 将实例变量设为私有就是为了阻止用户不受限制访问变量, 所以读取器的设置更为常见
- 事实上,immutable设计风格将类设置为完全不可变
- 为方便,设置器的命名通常以
类内存结构
- 类的方法地址不会存储在实例的内存空间中
非虚函数
编译时
- 编译器根据变量类型(指针、引用类型)在调用函数 处写入类方法的地址
- 编译时即确定执行的函数
- 可以认为是普通函数,只是默认包含参数
this
指针
运行时
- 访问方法相应内存地址,传入
this
指针调用方法
- 访问方法相应内存地址,传入
(Pure)Virtual Method
- 虚方法/虚函数:
virtual
修饰的方法,基类中有定义- 纯虚方法:没有函数体的虚函数,基类中无定义,实现只能由 其子类提供
- 派生类中覆盖方法总为虚方法,无论是否有
virtual
关键字- 建议后代虚函数加上
virtual
以提升可读性
- 建议后代虚函数加上
1 | virtual double getpay(); |
编译时
- 编译器根据类的声明创建虚表,每个类维护一张虚表
- 对象被构造时,虚表地址在实例内存的首个字中
- 编译器在调用函数处不直接写入函数地址(因为不确定 调用的函数)
执行时
- 从对象首读取虚表,在虚表中查询需要执行方法地址
- 访问相应方法地址,传入
this
指针调用方法 - 实现多态:subtyping polymorphism,同一语句具体调用 方法动态决定
- 多态:参见program/program_design/language_design
- vTable:虚表,存储一个类中所有虚函数地址
- 虚表是依次存储当前类中所有虚方法,不是继承序列 中同一个虚方法
Abstract Class
抽象类:包含纯虚方法的类
抽象类主要作用
将有关类组织在继承层次结构中
刻画一组子类的操作接口的通用语义,只描述派生类共同 操作接口,完整实现留给子类
抽象类只能作为基类派生新类使用,不能创建抽象类对象
所以抽象类也不能做参数类型、函数返回类型、显式转换 类型
可以定义指向抽象类的指针、引用,可以指向其派生类, 进而实现多态
此特性本身就是为了解决基类生成对象不合理的问题, 抽象类就是为了抽象设计目的而建立
实际中,为了强调类是抽象类,可以将类的构造函数 放在
protected
区域其派生类必须实现基类中所有纯虚函数,才能成为非抽象类 ,否则仍然是抽象类
构造/析构函数内不能使用纯虚函数(一般成员方法内可以)
因为派生类构造/析构函数内会调用基类构造/析构函数
纯虚函数没有函数体,不能被调用
Override
覆盖/重置:在派生类中签名一样的方法会覆盖父类的方法
非虚方法:调用方法取决于指针、引用类型
- 将派生类对象赋给基类指针、引用后,调用方法是基类方法 ,表现父类的行为
虚方法:动态检查,调用方法取决于对象实际类型
- 将派生类对象赋给基类指针、引用后,调用方法是派生类 方法
- 即指针(引用)根据指向值决定行为,实现多态
Template
模板:parameterized class参数化类,包含base type规格说明 的类
模板是C++多态的一种实现
编译器遇到函数模板调用,会自动生成相应版本函数拷贝
- 因此模板必须能实现,即不能将模板接口、实现分开,
否则编译器无法生成相应版本函数拷贝
(当然可以通过
#include
强行将接口、实现分开) - 模板不能节省空间
- 因此模板必须能实现,即不能将模板接口、实现分开,
否则编译器无法生成相应版本函数拷贝
(当然可以通过
模板使用
在函数、类前添加
1 | template <typename ValueType> |
template
关键字:表示此行后整个语法单位是模板模式一部分ValueType
:类型占位符,生成函数拷贝时被替换为相应类型
模板函数
1 | template <typename ValueType> |
模板类
1 | // .h |
Template Specialization
模板特化:指定一个或多个有具体模板参数的模板
- 全特化:给出所有模板参数
- 偏特化:给出部分模板参数
模板实例化时,优先使用模板参数最匹配的模板版本
通过特化模板,可以对特定模板参数集合自定义当前模板
- 最好特化模板的接口和普通模板一致
类模板特化
类模板可以全特化、偏特化
- 特化的模板参数从模板参数列表中移除
- 在类名后给出完整参数列表
1 | template <typename T2> |
函数模板特化
函数模板只能全特化
- 若编译器可以通过返回值类型推断模板实参类型,可以省略 函数后模板参数列表,否则引起歧义报错
- 函数模板“偏特化”可以通过函数模板重载实现
1 | template <> |