Effective CPP 学习笔记
1.Cpp 是一个语言联邦
2.以const,enum,inline替换 #define
预处理器会带来诸多问题,但还是有很大的用途,所以这条仅适用于能用以上三种的情况。
3.尽可能使用const
high-level const :从右至左的第一个const (* 右边 指针自身是const)
low-level const :从右向左看的第二个const(* 左边 指针所指对象是const)
const成员函数:const的成员函数中是不能对类本身进行改变的所以说是bitwish的,mutable可解决这个问题。
使用const成员函数调用重载的non-const函数:
char & operand[] (std::size_t position){
return const_cast<char&>(
static_cast<const TextBlock&>(*this)
[position];
)
}
4.保证使用前初始化
对inner type 和object都要在使用前初始化。
使用initializer-list对类进行初始化,而不是适用赋值操作。
跨单元编译的时候应该使用local static 替换 non-local static否则会出现初始化次序的问题。
FileSystem & tfs(){
static FileSystem fs;
return fs;
}
5.编译器的自动构造
default-constructor | copy-constructor | copy assignment
6.明确拒绝不需要的自动构造
将不需要的函数private化,或者使用一个private的base class
7.virtual 析构函数
这节的说法有点奇怪,其实重点在于通过base class 的指针删除derived class object 时如果base class存在non-virtual 析构函数,就会导致derived data 删除错误,就是这个原因而已。
virtual 析构函数意味着Class需要支持多态,并作为base class.
virtual 的执行是从子到父的,一个non-virual析构函数的base class可能会因为未实现而产生错误。
基本上STL库的东西都没有non-virtual 析构函数,所以也都不能继承。
pure-virtual析构函数使之成为抽象类。
8.不建议在析构函数抛出异常
使用try {...} catch {...}
进行捕获,然后使用std::abort();
提前结束,或者是吞掉异常,当然,重新设计接口会更好。
9.不在构造和析构过程中调用virtual函数
构造和析构函数期间derived class尚未形成,会被当成是base class这样子调用一个p virtual 或是一个 imp virtual函数都是会发生错误的。可以靠更改接口,便成一个non-virtual 函数,然后靠传入数据去修改。
10.operator= 返回& *this
ClassName & operator= (const ClassName & rhs){
...
return *this;
}
11.operator= 中处理自我赋值
=的重载可能会遇到不小心出现的给自己赋值的情况,这里就会发生重复删除的错误。
if (this == &pointer) { return this;} // 认同测试
// CAS 操作 copy and swap
XXX & operator= (XXX llll) // 注意是pass by value
XXX & operator= (XXX & llll) {
XXX temp(llll);// copy
swap(llll);// swap
return *this;
}
12.拷贝构造函数要包含所有的值
13.以对象管理资源
不推荐零散的delete方法,使用包括但不限于shared_ptr & auto_ptr 其中后者不支持多份拷贝(null),对于数组建议使用STL的vector等容器,以上的ptr不支持数组的delete [],注意不会报错的问题。
14.资源管理类的Copying
资源管理类的对象的拷贝,要复制所有的资源,抑制拷贝,施行引用计数。
shared_ptr接受删除器设定函数。
15.为资源管理类提供原始类型的转换
类似shared_ptr.get()的是显示转换。
operator xxx() const {
return f;
}
是隐式转换。推荐第一种。
16.new ([]) 和delete([])配对
17.独立语句将newd对象放入智能指针
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
都写在一起,语句调用顺序可能有问题。
18.接口更易被使用,更难被调用
促进正确使用:接口一致性,内置类型的行为兼容
阻止误用:建立新内置类型,限制类型上的操作,束缚对象值。
19.Class是类型系统的扩充
1.如何创建和销毁 #16
2.初始化和赋值 #4
3.值传递 copy-constructor
4.数值合法 setter进行检查
5.继承
6.类型转换 operator
7.操作符和函数的合理性
8.标准函数的权限?pub/pri/pro
9.undeclared-interface
10.一般化 你可能需要一个模版
11.需要这个类? 添加字段和方法可能是好办法
20. pass-by-reference-to-const to pass-by-value
降低无谓的拷贝构造开销。
不适用于STL迭代器/函数对象/内置类型。
21.如果一定要返回对象,不要返回引用
const XXX & operator * (...){
XXX xxx();
return xxx;
}
// local-object 已经被回收掉了
const XXX * operator * (){
XXX * xxx = new XXX(...);
return xxx;
}
// 看起来可以 但是不方便调用者析构
XXX x(1) ,y(2),z(3);
x * y * z; // 中间生成的指针没办法被回收
const XXX operator * (...){
Static XXX xxx;
...
return xxx;
}
// 如果这个函数只用一次,或者说是分开用的当然没问题
// 但是如果 x * y * z XXX会被进行反复覆盖
// 而且如果我们用一个static-array就好了
在这种尴尬的时候,就尽量进行 pass-by-value吧
22.将成员变量声明为private
protect的封装性和public是一样的。
23.以non-member non-friend函数替换member函数
class WebBrowser {
void clearEverything();
}
void clearEverything(const WebBrowser & browser){
...
browser.clearCache();
}
后者拥有更好的封装性。
24.如果某个函数的所有参数都要进行隐式转换那就写成non-member function比较好
Retional temp(2);
result = 2 * temp;// 试图调用2.operator*(temp) 不可能啊
result = temp * 2;// 需要对2 进行隐式转换
最好写成。
class Rational {
...
}
// 写在类外作为一个独立的函数
const Rational operator*(const Rational & lhs,... rhs){
return Rational(...);
}
25.考虑写出一个不抛异常的Swap函数
STL默认实现的swap函数是一个拷贝,但是我们日常中有很多的类实现是通过携带一个私有指针来实现一些功能的。
pointer to implementation
存储一个指针指向真实数据,我们在swap的时候只需要进行交换指针的操作就好了。
1.类提供交换的函数,毕竟是private的指针
class Widget {
void swap (Widget & other){
using std::swap; // 通过声明通知编译器去找swap函数 (STL & local-namespace).
swap(this.pImpl, other.pImpl);
}
}
namespace std{
template<>
void swap<Widget>(Widget & a, Widget & b){
a.swap(b);
}
}
再写这样的一个全特化就好了。
刚才的那个是class没有范型,如果有泛型的话就会麻烦一点,毕竟STL库可是不能做偏特化,因为std是STL的命名空间你不能再往里注入了。
而且CPP只支持对于模版类的偏特化而不支持对模版函数的偏特化,所以写的时候需要注意一下。
template<typename T>
void swap<Widget<T>>(Widget<T> & a, Widget<T> & b){
...
}// 这样就明显是错误的
正确的写法,并且写在了一个和class相同的namespace里面:
namespace Fuck {
template<typename T>
void swap(Widget<T> &a, Widget<T> &b){
a.swap(b);
}
}
这样子在using std::swap;
的时候就会搜索到local-namespace的偏特化函数。
26.延后定义式出现的位置
构造/析构函数都有一定的使用的消耗,所以说一个变量的定义应该拖延到开始使用它的时候(另一种角度讲,是我们能给他一个确定的初值用俩初始化的时候)。
27.减少使用类型转换
C++ style-cast
const_cast / static_cast / dynamic_cast / reinterpret_cast 低级转型
C style-cast => C++ style-cast
dynamic_cast => virtual interface
28.避免返回Handle指向class的内部成分
struct RectData {
Point ulhc;
Point lrhc;
}
class Rectangle {
public:
Point & upperLeft() const {
return pData->ulhc;
}
Point & lowerRight() const {};
private:
std::shared_ptr<RectData> pData;
}
class Rectangle {
public:
const Point & upperLeft() const{
return pData->ulhc;
}
const Point & lowerRight() const { };
private:
std::shared_ptr<RectData> pData;
}
避免返回handles,通过*/&指向程序的内部,handle可能回比对象的生命周期更长,如果一定要的话,返回值const。
29.为异常安全努力是值得的
当异常被抛出的时候,带有异常安全性的函数:
1.不泄露任何资源
2.不允许数据败坏
example:
void changeBackground(){
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
unlock(&mutex);
}
只要Image抛出异常,以上两者就都被违反了。
// Lock ml(&mutex); // 改成这个就能保证互斥器被及时释放
异常安全函数提供一下三个保证之一:
- 基本承诺:异常被抛出,程序内的任何事物仍然保持在有效状态下。(
状态也是不可以预料
)
强烈保证:调用成功就成功,不成功就退回到调用之前的状态。
不抛掷保证:承诺不抛掷异常。
class PrettyMenu { std::shared_ptr<Image> bgImage; } void PrettyMenu::changeBackground(std::istream& imgSrc){ Lock ml(&mutex); bgImage.reset(new Image(imgSrc)); // 取消了delete 通过 shared_ptr来实现 // ++ 操作 在成功绑定了之后 再增加 ++imageChanges; }
CAS 策略 + pimpl idiom:
struct PMImpl { shared_ptr<Image> bgImage; int imageChanges; } class PrettyMenu { ... private: Mutex mutex; shared_ptr<PMImpl> pImpl; // 将数据放进指针中,类中存储指针 } void PrettyMenu::changeBackground(std::istream & imgSrc){ using std::swap; Lock ml(&mutex); shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); pNew->bgImage.reset(new Image(imgSrc)); swap(pImpl, pNew); // 交换指针 }
CAS由于可能造成不必要的开销,所以说不一定对所有的情况都成立。
选择对我们整个系统都最为强烈的异常安全等级。
30.不要轻率的使用inline
inline 的整体观念,对于每一个inline的函数调用都以函数本体替换,会增加目标码的大小。当然如果比较简单也会相应减小。
inline只是对编译器的申请而非强制调用,可以显示申请,也可以隐式申请,可以通过写在函数中进行申请。
- inline 适合小型的直接调用型的函数,以便于二进制升级(binary upgrade)
- function template 不要因为它们是写在h文件里就inline
- 构造函数不要轻率的使用inline 构造函数经过编译器的填充会增加很多我们无法控制的内容,如果日后需要进行修改,会造成所有创建对象的地方全都会被重新编译
31.将文件间的编译依存关系降到最低
依存关系会造成依赖某个类的改变会造成依赖的所有类被重新编译。
- pimpl idiom ( pointer to impletment )接口与实现分离
- 使用object references | object pointers 可以就不要使用 objects
如果能够尽量以class 声明式替换class 定义式
class Data; Data today(); void clearAppointments(Data data);
分开提供包含定义式和声明式的头文件。
- 使用interface的方式去实现接口分离(包含虚析构函数和纯虚的实现函数)
这个地方我的理解是这样的,如果有一个类的实现变了,依赖他的类就会进行重新编译,但是如果我们把接口和实现分离,那includes的可能就只有框而没有具体实现,那么如果实现真的变了,也只是实现类引发的接口类重新
编译,而不是将这个继续传递下去。
32.确定你的public继承塑造出is-a关系
is-a
其实是在说正确的继承关系。
33.避免遮掩继承而来的名称
继承添加函数会遮掩父类的同名函数,为了解决遮掩问题,可以使用using Base::xxx();
或者使用forward functions,通过一层转接调用之前的函数。
不过如果是virtual 函数的话,为什么不使用override关键字呢?
34.区分接口继承和实现继承
pure virtual functions’ feature:
- drived class must redefine them.
- non-definition in base class.
声明impure virtual functions的目的:让derived classes 继承该函数的接口和缺省实现。
class Airplane {
public:
virtual void fly(...);
}
virtual void Airplane::fly(){
}
class ModelA : public Airplane {
public:
}
virtual void ModelA::fly(...){
}
class Airplane {
public:
virtual void fly(...) = 0;
protected:
void defaultFly(...);
}
virtual void Airplane::defaultFly(...){
}
class ModelA : public Airplane {
public:
}
virtual void ModelA::fly(...){
Airplane::defaultFly(...);
}
class Airplane {
public:
virtual void fly(...) = 0;
}
virtual void Airplane::fly(...){
}
class ModelA : public Airplane {
public:
}
virtual void ModelA::fly(...){
Airplane::fly(...);
}
最后一种相比于第二种,当然是为了怕多加一个函数发生变量名污染的情况。其实在现代cpp中使用override
和final
可以解决这些问题的。
- 纯虚函数只继承接口;
- 虚函数既继承接口,也提供了一份默认实现;
- 普通函数既继承接口,也强制继承实现。
35.考虑virtual函数以外的其他选择
Non-Virtual Interface实现的Template Method模式
class GameCharater { public: int healthValue() const { ... // do something first int retVal = doHealthValue(); ... // do something later return retVal; } private: virtual int doHealthValue() const { ... } }
Wrapper 重点在于first/later,driven class 会调用新的类所复写的函数体。
std::function / Function Point 实现Strategy模式
class GameCharacter; int defaultHealthCalc(const GameCharacter & gc); class GameCharacter { public: typedef std::function<int(const GameCharacter&)> HealthCalcFunc; // typedef int (*HealthCalcFunc)(const GameCharacter&); explicit GameCharacter(HealthCalcFunc hcf = defaultHealthClac) : hearthFunc(hcf) { } int healthValue() const { return healthFunc(*this); } private: HealthCalcFunc healthFunc; }
策略模式,类似的想法就是替换执行方法。
36.绝不重新定义继承而来的non-virtual函数
破坏is-a
37.绝不重新定义继承而来的缺省参数值
dynamic type / static type 的概念
virtual-method depend on dynamic type
default-params —> static binding
但是直接写两次virtual函数(base & driven)是很不合理的还带着依赖,所以说那个NVI解决这个问题:
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw (ShapeColor color = Red) const {
doDraw(color);
}
private:
virtual void doDraw(ShapeColor color) const = 0;
}
class RectShape : public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const;
}
这样defalut就依靠一个non-virtual function保护了。
38.通过复合塑造出has-a
class Address { ...}
class PhoneNumber { ... }
class Person {
public:
...
private:
std::string name;
Address address;
PhoneNumber voiceNumber;
PhoneNumber faxNumber;
}
复合的含义has a
39.明智而审慎的使用private
继承
private继承不意味着is-a
结构,而意味着implemented-in-terms-of
(根据某物具象出)。
为了采用base class的某些实现了的特性(所谓实现部分被继承,接口部分被省略)。
尽量使用复合,必要时才使用private继承(所谓必要时:virtual & protected方法被牵扯进来的时候)。
还有就是组合模式无法重新定义继承的virtual函数这个问题。
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;
}
class Widget: private Timer {
private:
virtual void onTick() const;
}
bad implement~
class Widget {
private:
class WidgetTimer : public Timer {
public:
virtual void onTick() const;
}
WidgetTimer timer;
}
40.明智而审慎的使用多继承
virtual-base-class:
- 非必要不使用virtual bases
- 如果要使用不要包含成员变量(类比interface)
可用来处理钻石型继承的模式
class IPerson { public: virtual ~IPerson(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; }
class DatabaseID { ....}
class PersonInfo { public: explicit PersonInfo (DatabaseID pid); virtual ~PersonInfo(); virtual const char* theName() const; virtual const char* theBirthDate() const; virtual const char* valueDelimOpen() const; virtual const char* vauleDelimOpen() const; }
class CPerson : public IPerson , private PersonInfo { public: explicit CPerson(Database pid) : PersonInfo(pid) { } virtual std::string name() const { return PersonInfo::theName(); } virtual std::string birthDate() const { return PersonInfo::theBirthDate(); } private: const char* valueDelimOpen() const { return ""; } const char* vauleDelimClose() const { return ""; } }
组合模式,IPerson类似Interface实现必须实现的接口,PersonInfo提供有用的virtual-method,使用多重继承的一个优势。
41.了解隐式接口和编译期多态
- class 和 template 都支持接口和多态
- 对classes而言接口是显式的,以函数签名为中心,多态通过virtual函数发生在运行期
- 对template参数而言接口式隐式的,奠基于有效表达式。多态则是通过template具现化和函数冲在解析发生于编译期
42.了解typename
的双重含义
template<typename T> class Widget;
template<class T> class Widget; // 其中的 class 和 typename 没有任何区别
template<typename C>
void print2nd(const C & container) { // 这段cpp代码是有错的
if(container.size() >= 2) {
C::const_iterator iter(container.begin()); // C:const_iterator 从属性类型(依赖C)
++iter;
int value = *iter; // int 非从属性类型 (不依赖C)
std::cout << value;
}
}
编译器没办法分析出C::const_iterator
是一个类型,所以说要让我们通过手动置顶的方式去给compiler提供这个消息。
正确的写法:
if(container.size() > 2) {
typename C::const_container iter(container.begin());
}
//////////
template<typename C>
// 提供类型信息 (不允许使用typename ,并非从属类型) (允许使用typename)
void print2nd(const C container , const typename C::const_container iter);
另外:
template<typename T>
class Derived : public Base<T> :: Nested { // base class list 中不允许typename
public:
explicit Derived(int x)
: Base<T> :: Nested(x) { // mem.init.list 中不允许 typename
typename Base<T>::Nested temp; // 作为一个base class的修饰符加上typename
}
}
typedef的例子:
template<typename T>
void workWithInterator(IterT iter) {
// iterator 的item类型
typedef typename std::iterator_traits<IterT>:: value_type value_type;
value_type temp(*iter);
}
43.学习处理模版化基类内的名称
example:
class CompanyA {
public:
void sendClearText(... );
void sendEncrypted(... );
}
class CompanyB {
public:
void sendClearText(... );
void sendEncrypted(... );
}
class MsgInfo { ... };
template<typename Company>
class MsgSender {
public:
...
void sendClear(const MsgInfo & info) {
std::string msg;
// info ===> message
Company c;
s.sendClearText(msg);
}
void sendSecret(const MesgInfo & info) {
...
}
}
template<typename Company>
class LoggingMegSender : public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo & info) {
/// msg to log
sendClear(info); // 调用base 函数无法编译通过
/// msg to log
}
}
因为泛型类有可能被以某种形式特化,所以compiler 没办法确定是不是真的可以调用这个方法。
对于这个问题有三种解决问题的方法:
this->sendClear(info);// 1
using MsgSender<Company>::sendClear;// 通知compile假设可以使用这个方法 2
MsgSender<Company>::sendClear(info);// 假设这个方法被继承下来了 3
3对virtual-method很不利。
44.将参数无关代码迁出模版
核心就是防止无谓重复的编译期特化
template<typename T, std::size_t n>//T为数据类型,n为矩阵大小
class SquareMatrix{
public:
……
void invert();//求逆运算
};
SquareMatrix<double,5> sm1;
sm1.invert();//调用SquareMatrix<double,5>::invert
SquareMatrix<double,10> sm2;
sm2.invert();//调用SquareMatrix<double,10>::invert
对多个size_t都进行了泛化。
template<typename T>
class SquareMatrixBase{
protected:
SquareMatirxBase(std::size_t n,T* pMem)
:size(n), pData(pMem){}
void setDataPtr(T* ptr) {pData=ptr;}
……
private:
std::size_t size;
T* pData;
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
public:
SquareMatrix()
:SquareMatrixBase<T>(n, data){}
……
private:
T data[n*n];
};
这么改动之后,就会有多个类型基于一个Base类的泛化类型。
45.运用成员函数模版接受所有兼容类型
模版的泛化类型,base & driver 之间并不存在继承关系,例如原生被shared_ptr包装之后就很难办。
使用泛化的转化函数:
template<typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U> & other) : heldPtr(other.get()) { ... }
T* get() const { return heldPtr; }
private:
T* heldPtr;
}
支持从T->U的类型转换,并且通过heldPtr(other.get())
的隐式类型转换保证了不会乱转换。
模版泛化的拷贝构造和赋值和原生的没有关系。
46.需要类型转换时请为模版定义非成员函数
首先参考T24
template<typename T>
class Rational {
public:
...
friend const Rational operator* (const Rational & lhs,
const Rational & rhs) {
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
}
通过友元函数进行类型推倒,当Rational
这里友元函数的意义就不是访问私有变量了,而是为了提前被特化出来。
47.请使用traits class
表现类型信息
主要含义是编译期的类型检查
针对于类型信息在编译时的类型检查,靠的是模版的详细特化。
template<typename IterT, typename DisT>
void doAdvance(IterT& iter, Dist d, std::random_access_iterator_tag)
{
iter+=d;
}
template<typename IterT, typename DisT>
void doAdvance(IterT& iter, Dist d, std::bidirectional_iterator_tag)
{
if(d>=0)
while(d--) ++iter;
else
while(d++) --iter;
}
template<typename IterT, typename DisT>
void doAdvance(IterT& iter, Dist d, std::input_iterator_tag)
{
if(d<0)
throw std::out_of_range("Negative distance");
while(d++) --iter;
}
template<typename IterT,typename DistT>
void advance(IterT& iter,DistT d)
{
doAdvance(iter,d,typename::std::iterator_traits<IterT>::iterator_category();
}