PART0、前言
- TOPIC
- 运用c++进行高效编程
- 收获
- 了解c++如何行为
- 为什么那样行为
- 如何运用其行为形成优势
PART1、导读
定义式的任务
- 对象:为此对象拨发内存的地点
- 函数:提供代码本体
- 类:列出他们的成员
c++没有接口的关键字,定义接口就是定义一个全是纯虚函数的类
PS:自定义构造函数后,编译器就不会自动构造默认构造函数
PART2、让自己习惯C++
视c++为一个语言联邦
- 联邦组成
- c
- Object-Oriented C++
- Templete C++
- STL
- 内置类型值传递通常比引用传递高效的原因
- 传递效率
- 引用传递本质传递一个指针:不管何种类型指针,默认是4字节
- 执行效率
- 直接寻址&间接寻址
尽量以const、enum、inline替换#define
- 即“宁以编译器替换预处理器”
- 单纯常量:以const或enum替换#define
- 形似函数的宏,最好改用inline函数替换#define
尽可能使用const
const语法
- 星号左侧出现const:被指物是常量
const int *ptr - 星号右侧出现const:指针本身是常量
int * const ptr - 右侧两式等价
const int * ptr
int const * ptr
const成员函数
- void fun() const;
- 声明const的理由
- 让编译器知道,该函数不改变类成员变量
- 提高代码健壮性
- 两个函数如果只是常量性不同,可以被重载
- const char& operator [] (int position) const;
- char& operator [] (int position);
确定对象被使用前已先被初始化
- c++对对象的初始化反复无常,不能保证始终初始化
- 策略
- 内置类型:使用对象前手工完成初始化
- 引用类型:初始化责任落在构造函数上
- 构造函数中使用形如:myName = name,并非初始化,而是赋值
- 因为调用带参构造函数前,首先调用默认构造函数
- 正确初始化:使用成员初始化列表的做法
- 构造函数中使用形如:myName = name,并非初始化,而是赋值
PART3、构造、析构、赋值运算
条款05、了解C++默默编写并调用哪些函数
- 如无声明,编译器会为类默默声明
- copy构造函数
- copy assignment 操作符
- 析构函数
条款06、若不想使用编译器自动生成的函数,就该明确拒绝
- 如何拒绝
- 主动声明,且将其声明为private
- 为防止友元或成员函数调用,故意不实现函数
- 使用像Uncopyable这样的base class
该方式是上述两点的综合
- 用了上述方式
- 企图拷贝、赋值时,编译器报错
- 友元、成员函数调用时,链接器报错
条款07、为多态基类声明virtual析构函数
- class只有有虚函数,一般也要有一个虚析构函数
- 原因:防止只销毁base部分,derived部分没被销毁
- 注意:把string、vector、list等标准库中不带virtual析构函数的类当做base class
- c++没有类似java中final或c#中sealed的关键字
条款08、别让异常逃离析构函数
- 最好不要在析构函数中抛异常
- 实在需要在析构函数中执行动作,该如何避免
- 调用abort
- 吞下异常
- 提供普通函数执行动作
条款09、绝不在构造、析构函数中调用virtual函数
- 因为这样不能实现多态,只会调用base的函数
- 派生类构造函数,一般先调用父类构造函数
- base class构造期间,virtual函数不是virtual函数
- 在derived class对象的base class构造期间,对象的类型是base class,而不是derived class
- derived class析构函数开始执行时,相应的derived class成员变量便呈现未定义值,派生类先执行派生类析构函数,再执行基类析构函数
*条款10、令operator= 返回一个reference to this
- 原因:实现连续赋值
- 例子:x = y = z = 15
- 该条款只是个协议,无强制性
条款11、在operator=中处理“自我赋值”
- 例子:int a; a = a;
- 例子:p =q(两个指针指向同一变量)
条款12、复制对象时勿忘其每一个成分
- class每添加一个成员变量
- 修改拷贝构造函数
- 修改copy assignment操作
- copy构造和copy assignment操作符有相近的代码
- 可建立新的成员函数给两者调用
PART4、资源管理
条款13、以对象管理资源
-
资源
- 资源由系统给予,用完须还给系统
- 常使用的资源
1、内存(动态分配内存)
2、文件描述器
3、互斥锁
4、数据库连接
5、网络sockets
-
有new,就要delete,有malloc,就要free
- 但这还不够,因为代码可能因种种原因没运行到delete或free
- 出现异常的情况
1、区域内有一个过早return语句
2、delete位于循环内,程序过早break或goto,跳出了循环
3、区域内抛出异常 - 上述异常情况,如果是个人开发,谨慎一点依然可以防止错误,但团队开发不行
- 单纯依赖“对象总是会执行delete语句的”是行不通的
-
策略:把资源放进对象内
- 依赖c++的析构函数自动调用机制来确保资源被释放
- 在析构函数中对对象调用delete
- 关键想法
1、获得资源后立刻放进管理对象
2、运用析构函数确保资源被释放 - 存放资源的对象即是智能指针
- auto_ptr
- 切记不能让多个auto_ptr同时指向同一个对象,防止野指针
- 不同寻常的性质
- 通过拷贝构造函数或opertor=复制的话,原指针变成null,复制所得指针取得资源唯一拥有权
- 这一性质使得其并非动态分配资源的最佳对象
- STL容器要求其元素有正常的复制行为,因此这些容器不能* 用auto_ptr
- shared_ptr
持续追踪共有多少对象指向资源
无人指向时自动删除资源
引用计数型 - c++中没有特别针对“动态分配数组”而设计的智能指针
- auto_ptr和shared_ptr在析构函数中做delete而不是delete[]
- 原因是c++中有vector和string对象取代数组
- 实在要用的话,可求助boost
- boost::scoped_array
- boost::shared_array
- auto_ptr
- 为防止资源泄漏,使用智能对象,在构造函数中获得资源,在析构函数中释放资源
- 依赖c++的析构函数自动调用机制来确保资源被释放
条款14、在资源管理类中小心copy行为
小心copy行为的几种做法
- 禁止复制
继承ucopyable,详见条款6 - 对底层资源使用“引用计数法”
shared_ptr的做法 - 复制底部资源
深度拷贝 - 转移底部资源的拥有权
autor_ptr的做法
条款15、在资源管理类中提供对原始资源的访问
- 智能对象应该提供一个取得原始资源的方法
- 对原始资源的访问经由显式转换(安全)或隐式转换(方便客户)
条款16、成对使用new和delete时要采取相同形式
- 使用new生成对象,有两件事会发生
1、内存被分配出来
2、对此内存,有一个以上的构造函数被调用 - 使用delete释放对象,也有两件事发生
1、对此内存,有一个以上的析构函数被调用
2、内存被释放 - new时使用[],delete时也使用[]
- ps:尽量不要对数组形式做typedef动作
条款17:以独立语句将newed对象置入智能指针
- 以独立语句将newed对象存储于智能指针内,防止异常抛出,导致资源泄漏
- 原型:std::tr1::shared_ptr
pw(new Widget)
PART5、设计与声明
条款18、让接口容易被正确使用,不易被误用
- 尽量让你的type与内置type一致
- 接口一致性
- 阻止误用的方法
- 建立新类型
- 限制类型上的操作
- 束缚对象的值
- 消除客户的资源管理责任
- 支持定制型删除器:防止DLL问题
- 引用计数为0是调用删除器
条款19、设计class犹如设计type
高效设计type的规范
- 新type对象应该如何被创建和销毁
- 构造函数(内存分配函数)设计
- 析构函数(内存释放函数)设计
- 对象初始化和赋值有什么差别
- 什么是新type的合法值
- 新type的对象如果被pass by value,意味着什么
- 详见P84-85
条款20、宁以常量引用传递替换值传递
- 该规则不适用于内置类型以及STL中的迭代器和函数对象
- 对于这些小型对象,执行效率与传递效率均比常量引用传递高
- 对于引用类型,则适用该规则
- 常量引用传递减少构造对象的成本
- 还可避免对象切割问题
- 但能调用derived class中的成员吗?
条款21、必须返回对象时,别妄想返回其reference
- 函数返回一个引用或者指针
- 如果指向local对象,将发生内存泄露
- 如果指向动态对象(存于heap)
- 对象的delete操作不明确,同样会造成资源泄漏
- 如果指向static对象,则可能发生线程安全
- 正确策略:返回对象
- 构造与析构成本代价高,但一般编译器有做优化,可消除这一代价
条款22、将成员变量声明为private
- 为什么不是public?
1、一致性:统一函数均为public,变量为private
这样用户使用该类均是调用函数,不用犹豫该不该加()
2、精确控制:可实现不准访问,读写访问,只读访问
3、封装:成员变量改变时,外部代码不会受到破坏 - protected成员封装性不一定高于public~
- 取决于代码破坏量
条款23、宁以non-member、non-friend替换member函数
- 误解:写成member函数可以提高封装性,其实反而降低了封装性
- 例子
- 使用non-member、non-friend,增加
- 封装性
- 包裹弹性
- 机能扩充性
条款24、若所有参数皆需类型转换,请为此采用non-member函数
条款25、考虑写出一个不抛异常的swap函数
- swap传入的两个参数是
- 对象形式的话,使用临时变量方法无可厚非
- 指针的话,依然使用临时变量方法效率太低
- 策略:自己写一个成员函数
PART6、实现
实现须知
- 太快定义变量
- 造成效率上的拖延
- 过度使用转型
- 代码变慢又难维护
- 招来难以理解的错误
- 返回对象内部数据的地址
- 破坏封装
- 留给客户虚吊号码牌(野指针)
- 未考虑异常带来的冲击
- 资源泄漏
- 数据败坏
- 过度inlining
- 引起代码膨胀
- 过度耦合
- 不尽人意的冗长build时间
条款26、尽可能延后变量定义式的出现时间
- 变量定义式与真正使用该变量的间隔要尽可能短
- 防止间隔中代码抛异常,白白付出了变量的构造和析构成本
条款27、尽量少做转型动作
转型语法
c风格(旧式转型)
1、( T )expression
2、T( expression )-
c++风格
- const_cast
( exp ) - 常量性转除
- 唯一有此能力c++风格方式
- dynamic_cast
( exp ) - 安全向下转型
- 决定某对象是否归属继承体系中的某个类型
- 旧式转型无法执行
- 运行成本高
- 应用场合
- 在derived类对象上执行操作函数,但手上只有base的指针或引用
- reinterpret_cast
( exp ) - 执行低级转型
- 低级转型:int指针转型为int
- 实际效果取决于编译器
- 执行低级转型
- static_cast
( exp ) - 强迫隐式转换
- 可执行上述多种转换的反向转换
- 例子
- non-const对象转const对象
- int转double(向上转)
- void*指针 转 typed指针
- pointer2base 转 pointer2derived
- 无法将const转non-const,只有const_cast能
- const_cast
旧式转型仍合法
新式转型较受欢迎的原因
1、易辨识,易排bug
2、转型动作目标窄化,编译器更容易诊断错误错误观念:
转型什么也没做,只是告诉编译器把某类型视为另一类型-
c++中,如果派生类中虚函数第一个动作是调用base class的对应函数,其原型:
- base::vir();
- 错误做法
- 类型转换:static_cast
(*this).vir() - Java后遗症:super();
- 类型转换:static_cast
优秀的c++代码很少使用转型
条款28、避免返回handles指向对象内部成分
- 形式:返回handles(reference、指针、迭代器)指向private内部数据
- 危害:造成调用者可通过引用更改内部数据
- 预防:加const
- 但依然有后遗症:导致悬垂指针、handle
条款29、为异常安全而努力是值得的
- 带有异常安全性的函数会
- 不泄漏任何资源
- 不允许数据破坏
- 异常安全函数保证
- 基本承诺
- 异常被抛出,程序内任何事物仍保持有效状态
- 强烈保证
- 异常抛出,程序状态不改变
- 函数要么完全成功,要么完全失败,没有部分成功
- 实现形式:copy and swap
- 类似数据库事务的roll back
- 类似怀孕,要么怀了,要么没怀,木有部分怀孕的说法
- 不抛掷保证
- 承诺绝不抛出异常
- 即是什么都不做?
- 基本承诺
条款30、透彻了解inlining的里里外外
- inline函数
- 看着像函数,动作像函数,但又不需承受函数调用的开销
- 编译器会对inline函数有特殊照顾
- 对inline函数执行最优化
- 比宏好的多
- 缺点
1、增加目标代码大小
2、代码膨胀导致额外的换页行为,降低高速缓存的命中率
- 错误观念:函数模板一定必须是inline
- template的具现化与inline无关
- 如果想让template内联化,请显示声明inline
- 一般虚函数会使inlining落空
- virtual意味着等待,直到运行期才确定调用那个函数
- inline意味着执行前,先将调用动作替换为被调用函数的本体
- 使用函数指针进行的调用,不会inlining
- 构造/析构函数不适合inlining,特别是成员变量特多的类
- inline函数无法调试
- vs2010下是可以的~~
- inline应用场合
- 小型、被频繁调用的函数
相关细节
- 许多环境中,template定义式通常只能置于头文件内,无法分离编译
- 另有些环境支持分离编译,但很少
- 如果编译器支持export关键字,也可实现分离编译
- 但支持的编译器也很少
PART7、继承与面向对象设计
条款31、将文件间的编译依存关系降至最低
- 使用引用对象、指针对象可以完成任务,就不要直接使用对象
- 尽量以class声明式替换class定义式
- 使编译依存性最小化的方式
- handle class
- interface class
条款32、确定你的public继承塑模出is-a关系
- 适用于base class的事情一定也适用于derived class
- 每个derived class 对象也都是一个base class对象
条款33、避免遮掩继承而来的名称
- derived class内实现虚函数写不写virtual关键字
- 派生类的名称会遮掩基类的名称
- 让基类被遮掩的名称再见天日的方法
- using 声明式
- 转交函数
条款34、区分接口继承、实现继承
- 纯虚函数只具体指定接口继承
- 虚函数具体制定接口继承和缺省实现继承
- 一般函数具体指定接口继承以及强制性实现继承
条款35、考虑virtual函数以外的其他选择
- Non-Virtual-Interface实现
- 将原来的虚函数改为普通public成员函数
- 普通成员函数调用私有虚函数
条款36、绝不重新定义继承而来的non-virtual函数
条款37、绝不重新定义继承而来的缺省参数
- 静态绑定又称前期绑定
- 动态绑定又名后期绑定
- 基类和派生类中同一函数均有缺省参数值的后果
- 基类和派生类各出一半力
- 缺省参数值是静态绑定,而虚函数要求的是动态绑定
- 原因:程序执行速度和编译器实现的简易度
条款38、通过复合塑模处has-a或根据某物实现出
- 复合两意义
- has-a
- is-implemented-in-terms-of
- 例子:set和list的关系
条款39、明智而审慎的使用private继承
- private继承的话,编译器不会自动将一个派生对象转换为一个base对象
- private继承是为了采用base已经准备的某些特性,不意味base和derived之间有什么关系
- private是一种实现技术
- 无设计层面上的意义
- 其意义只及于软件实现层面
条款40、明智而审慎的使用多继承
- 多继承会造成多种后患
- 继承多个base,如果base中有相同的名称,会导致歧义
- 钻石型多重继承
多个base又都继承同一个base
virtual继承会增加成本