C++是一个语言联邦
- C
- Object-Oriented C++
- Template C++
- STL
四大范式:
- 过程式编程PP:Procedural Programming。
- 面向对象编程OO:Object-Oriented Programming。
- 泛型编程GP:Generic Programming。
- 模板元编程:Template Metaprogramming。
确定对象使用前已被初始化
- C++的初始化列表(构造函数初始化列表)初始化顺序只与定义的顺序有关,跟在初始化列表中的顺序无关。
构造/析构/赋值运算
- C++11增加了关键字delete, default等,可以方便地控制默认函数(C++自动生成)。
- 基类析构函数应声明为virtual,否则当对基类指针进行delete操作时,会调用基类析构函数,而不是子类析构函数。如果继承并不是为了多态,则应该不用虚析构函数。
- C++11增加了final关键字,可以防止某个类被继承。
- 析构函数不应该抛出异常
- 异常点后面的程序可能有资源释放,提前抛出异常则资源泄漏
- 异常发生时,C++会通过调用析构函数来释放资源,但如果是析构函数本身抛出异常。。。。然后崩溃
- 构造和析构过程中不要调用virtual函数,虚函数在构造析构过程中并不虚。
- 赋值操作符
- 返回
*this
的引用(处理连续等号) - 处理自我复制(删除当前成员,构造新成员,设置新成员,删除后参数指向的对象也没了)
- 处理异常安全(new失败了,然而当前成员已经删除,这造成该成员指针悬垂)
- 返回
资源管理
- 以对象管理资源:RAII
- 智能指针(使用make_shared和make_unique)
- make_unique在C++14中加入
- 这样内存分配时引用计数控制块和对象在一块儿,一次new也比两次new快
- 还有造成泄露的可能,new对象后,其他函数抛出异常,这时new出来的对象指针遗失
设计与声明
- 将成员变量声明为private
- 实际上只有两种权限:封装与不封装
- 用户视角:private/protect=封装,public=不封装
- 子类视角:private=封装,protect/public=不封装
- 整体来看protect并不具有封装性
- 基类的private成员只能被基类的成员函数或友元函数访问
- https://blog.youkuaiyun.com/scott198510/article/details/129075736
- 封装性意味着改变之后影响的范围尽可能小。
public和private。在用户的视角,public就是public,private和protected被他一同视作了private;在子类的视角,private就是private,public和protected被他一起视作了public。只不过是视角不同而已
- 实际上只有两种权限:封装与不封装
- non-member函数有时比member函数更恰当
- non-member函数不能访问private成员,也因此non-member函数不依赖private成员,更具封装性。
- 如需所有参数都可以类型转换(尤其是一些数学计算之类的),使用non-member
实现
- 尽量延后变量定义出现的时间
- 类型转换
- 尽量避免转型,将转型隐藏于某个函数背后,使用新式转型
- dynamic_cast<>执行效率不高(运行时完成转换)
- 如果不正确会返回空指针
- 依赖运行时类型信息(RTTI),必须具备多态性(父类有虚方法),如果父类没有虚方法则只能使用static_cast
- 避免返回对象内部成分的引用、指针等
- 返回一个私有对象的引用会破坏封装性,退一步也尽量返回一个const类型的
- 例外:operator[]
- 异常处理
- 异常安全:(1)不泄露资源,如上锁后,中间发生异常,锁没有释放;(2)不允许数据破坏,如一系列操作是一起的,某个操作失败,则其他操作会破坏一致性
- 对于new这种操作
- 如果项目有处理,就抛异常,否则最多在最外层打印个日志。
- set_new_hander钩子,new失败后清理资源,然后继续程序或结束程序
- 使用
new(std::nothrow) xxx
,这样new失败会返回null,不抛异常 - 智能指针
- 三种保证
- nothrow:使用nonexcept关键字,不是100%不抛处,而是抛出时遇到了重大问题
- 强烈保证:失败则恢复调用之前状态
- 基本承诺:状态可能改变,但具备一致性
- 减少文件间的编译依赖关系
- pimpl
- 类成员放到struct里面,类中只放一个指针,在类前面前置声明,impl文件中定义该struct。
- 头文件中值包含了“接口信息”,具体实现都在impl类,而且对impl类的引用只有一个指针,所以该头文件并不需要include impl类。故而降低依赖关系。
- 好处是降低依赖关系,一定程度上增加了封装性,减少增量编译时间
- 缺点是可读性变差,运行效率降低,而且接口本身不能被直接构建出来(不能在栈中,需要工厂类)
- 类成员放到struct里面,类中只放一个指针,在类前面前置声明,impl文件中定义该struct。
- 把所有的东西都包含在一起,这样依赖关系扁平化,也可以加快增量编译速度,哈哈哈
- 尽量将实现写进cpp而不是头文件,尽量不要包含不需要的头文件
- pimpl
继承与面向对象设计
- 记住public继承是一种“is a”的关系,不要随便使用继承(比如鸟类的飞行属性,但企鹅并不会飞;比如正方形是矩形,但对矩形的操作并不能全都可以作用在正方形上,例如增加某个方向的长度)要时刻记住is a的面向对象关系并不一定和现实生活中的相同
- 每次继承的时候也要问一句:这是不是is a的关系?
- 注意继承时可能会发生的名称遮盖
- 虚函数也可能被覆盖(参数列表不相同)
- 非虚函数和成员变量则会覆盖(只要同名就会覆盖,不论参数和返回值),这是作用域的原因。如果要访问,直接在名称前面加上
ClassA::
即可- 对于同名函数,如果想要父子实现的函数都可见(以类似重载的关系存在),可以使用using在子类中声明。
- 从“is a”的关系来看,编写代码时不应该出现覆盖
- 接口继承和实现继承
- 纯虚函数指定接口继承,普通虚函数同时指定接口继承和默认实现继承,非虚函数指定接口继承和强制实现继承
- 可以选择将接口继承和实现继承分开,例如定义一个默认实现(protect的非虚函数),然后定义一个纯虚函数。这样就要求子类必须去处理这个功能应该使用什么方式运行(自己定义或者即便不自己定义也要调用一下默认实现)。还有一种形式是把默认实现放在父类的纯虚函数定义中,子类通过
ClassA::fun()
的形式调用默认实现
- 考虑virtual函数以外的选择
- NVI(Non-Vertual Interface)手法实现模板方法设计模式
- 非虚函数规定了框架,调用private的虚函数,子类可以重写某一步骤的具体实现
- 函数指针/function对象实现策略模式
- 可灵活指定具体实现
- 古典策略模式是通过将功能也设计成父子类,并通过多态指针来实现的
- NVI(Non-Vertual Interface)手法实现模板方法设计模式
- 绝不重新定义继承而来的缺省参数值
- 虚函数是动态绑定的,但是虚函数的默认参数值却是静态绑定的
- 就是说如果有一个父类多态指针,其调用某个已经被子类重写的函数,函数虽然确实是子类定义的函数,但是使用的默认参数值却是父类中的,也即“静态绑定”
- 所以不要重新定义缺省参数值,以免程序不符合预期
- private继承和组合
- private继承并不是ia a和关系,而是is-implemented-in-terms of(根据某物实现出)的关系
- 组合表示has a的关系和is-implemented-in-terms of的关系
- 大部分情况下应该使用组合,只在特殊情况可以考虑private继承:
- 需要访问protected成员,或者需要重新定义继承来的虚函数
- 追求对象尺寸最小化(父类没有成员变量,空白基类)
- private继承无法阻止派生类重新定义虚函数,可使用public继承定义一个嵌套类,然后组合在外面的类中
- C++11后增加了final关键字
- 多重继承
- 歧义性
- 如果从多个基类继承到了相同名称,会导致歧义
- 菱形继承导致出现重复成员
- 默认是复制,成员重复。要想只保留一份,基类必须全部是虚继承得来的,如
class basic_ostream : virtual public basic_ios{}
,但虚继承会增加很多开销
- 默认是复制,成员重复。要想只保留一份,基类必须全部是虚继承得来的,如
- 因为其复杂性,所以除非不得已,不要使用多重继承
- 如果要使用多重继承,请不要在基类中放置数据(类似java中的interface)
- 歧义性
模板与泛型编程
- 隐式接口和显式接口
- 如果不用泛型,那么全部的接口都可以通过直接阅读代码得知,这些接口都有具体的声明或定义。多态发生在运行时
- 某个模板函数可能支持传入各种类型,但并不是任意的,这个类型必须能够完成函数内部可能的操作,这种约束是隐式的。多态发生在编译期
- typename与“嵌套从属类型”
- class和typename在模板参数部分可以通用,嵌套从属类型只能用typename
- 嵌套从属类型是类似
C::iter
这样的,这种写法默认是变量,而不是类型,要显式在前面加上typename才说明这是个类型 - 嵌套从属类型在继承列表和初始化列表里面不需要加typename
- 处理模板化基类的名称
- 模板基类的派生类中,并不可直接访问基类成员函数/变量
- 加上
this->
- 使用
using
声明 - 使用
TBase<T>::fun()
来访问。不推荐,因为如果fun是虚函数,那么无法完成多态
- 加上
- 模板基类的派生类中,并不可直接访问基类成员函数/变量
- 尽量减少可能的代码膨胀
- 非类型模板参数可能更容易产生代码膨胀,改成函数参数更好一些
new和delete
- STL容器虽然在堆上分配内容,但并不由new和delete管理,而是自己的分配器管理
- 在一些情况下使用定制的new和delete会提高很多性能