《effective c++》学习笔记

本文深入探讨C++编程中的关键原则,包括构造/析构/赋值运算的优化,合理使用const,确保对象初始化,理解编译器自动生成的函数,资源管理策略,设计与声明的考量,以及实现细节。文章强调了避免异常逃逸析构函数,禁止自我赋值,复制对象时考虑所有成分,使用对象管理资源,以及在资源管理类中谨慎对待copying行为的重要性。

构造/析构/赋值运算

一、尽量用const eunm inline 替代define

  1. 静态类内常量的类内初始化只支持整型(即是整数类型,int/long/uint64_t都可以),如是浮点类型等其他类型,需使用类外初始化,初始化时不需要添加static关键字。

  2. 非const的静态成员,一律需要类外初始化

二、尽可能用const

  1. 例如重载一个对象的*运算符时,如果不是反回一个常量对象(即返回值是const),那么就可以在重载函数外对这个对象赋值,在笔误的情况下有可能完成难以排查的非预期错误

  2. const成员函数,可以读取所有数据成员,但要取地址、写修改时,只能修改静态成员变量,且不允许调用其他non-const成员函数。另外,non-const对象实体,可以调用const成员函数和non-const成员函数,但const对象实体仅能调用const成员函数,这是因为const对象不允许改变对象内的任何一个bit。

  3. 变量声明时,const修饰指针,取决于从右到左,const先结合的部分,const和指针名结合,指针保存的地址不能被修改,const和*结合,说明指针指向的变量的值不能修改。

  4. STL容器中 const T::iterator是一个T* const,其指向的值可以修改,但本身不能做++之类的修改;除非我们使用T::const_iterator,可实现指向的值不能修改,但能++修改,类似一个const T *

  5. mutable修饰成员变量,可以使得这些成员变量能在const成员函数中修改。

  6. 事实上可以通过const_cast<T>()将cosnt特性移除,但不推荐这么做

  7. 类成员函数的常量性不同(和返回值无关,就是相同参数但末尾是否有const),就可以被重载;可以根据对象实体的常量性,会调用不同的重载函数

char & operator[] (std::size_t position) {
    cout << "execute non-const operator[]" << endl;
    return string_base[position];
}

// 如果只是返回值const,将不满足重载条件,需作为const成员函数
const char & operator[] (std::size_t position) const {
    cout << "execute const operator[]" <<endl;
    return string_base[position];
}
output:
cs Text string null
cs Text string : Happy new year
execute const operator[]
y
execute non-const operator[]
ds Text
ds Text

三、确定对象被使用前已先被初始化

  1. 内置类型变量必须手工初始化

  2. 在类构造函数的初始化列表中,列出所有成员变量,以免记忆和遗漏变量初始化,最好初始化的次序和声明的次序一致

  3. 两个编译单元(例如不同文件)中的non-local static对象(函数外面声明的static对象),多个non-local static的初始化次序是无法确定的,如果遇到需要彼此依赖的场合,可以参考Singleton模式的方法:在函数内部声明静态对象,通过reference的方式返回给调用方,以此将non-local static转化为local static对象,该函数称为reference-returning函数。

  4. 任何non-const static对象,在多线程系统环境下都需要特别注意!初始化时,可以在程序的单线程初始化阶段,手工调用reference-returning函数一次。但执行过程中的务必考虑多线程数据访问问题。

四、了解c++默默编写并调用哪些函数

  1. 默认构造、默认拷贝、默认析构(非virtual的)、默认赋值

  2. 基类的析构为virtual的情况下,默认的析构函数也具有virtualness属性;其实这个无所谓,因为原本默认析构函数内也没做任何事,能否调用到问题都不大

  3. 默认拷贝构造、默认赋值构造函数,都是将对象每一个non-static成员拷贝到目标对象

  4. 成员变量有const和reference、base class中的拷贝、赋值构造为private时,编译器无法生产默认的拷贝、赋值构造函数

五、若不想使用编译器自动生成的函数,就该明确拒绝

  1. 拒绝方式,可以将拷贝、赋值构造函数放置在private且不提供实现;如何赋值和拷贝时就能报编译错误,而内部函数、friend函数调用时,在linker时会报错

  2. 额外的,可以写一个Uncopyable作为空的基类,把拷贝、赋值构造函数放置在private且不提供实现;需要屏蔽赋值、拷贝的类继承它即可,boost也提供了类似功能的类:noncopyable

六、为多态基类声明virtual析构函数

  1. 一个类中没有virtual函数,即不需要派生其他类且用作多态使用时,不建议对析构函数virtual,因为虚函数表会增大对象的体积

  2. STL容器都是non-vitrual的,不要傻逼到派生它们

  3. 要多态的基类可以只将析构函数声明为pure virtural,以实现一个抽象类,但我们需要提供一个析构函数的实现,否则linker会报错

七、别让异常逃离析构函数

  1. 异常会导致析构函数非正常退出,中断资源的回收而导致资源泄漏,不一定是指内存,例如于服务端通信的连接等资源

  2. 可以在析构函数中通过try catch(...)转std::abort()中断程序或捕获作后续处理

  3. 较佳的选择是提供一个额外的回收函数如close(),供用户手动关闭,并结合1、2两点作收尾

八、绝不在构造和析构过程中调用virtual函数

事实上base class构造调用的virtual函数就是base class自己的版本,derive class调用的也是自己的版本,如果两边都对virtual做了实现,那么实例化derive对象的时候,两个virtual函数都会调用到。这个似乎符合逻辑,感觉作者过于担心了,一般也不会这么使用。如果调用的函数是pure virtual的函数,那当然会发生链接错误。

class Transaction {
public:
  Transaction() {
    logTransaction();
  }

  virtual void logTransaction() {
    cout << "Transaction log execute!" << endl;
  }
};

class BuyTransaction : public Transaction {
public:
  BuyTransaction() {
    logTransaction();
  }
  virtual void logTransaction() {
    cout << "BuyTransaction log execute!" << endl;
  }
};
BuyTransaction bt;
output:
[root@centos74-dev Demo]# g++ main.cc -o main --std=c++11 && ./main
=============== effective c++ Demo ===============
Transaction log execute!
BuyTransaction log execute!

九、令operator= 返回一个reference to *this

主要是为了方便连锁赋值:a = b = c = 15,STL容器的赋值也是使用这个形式,其实不止=,例如+=、-=等和赋值相关的都可以作这样的标准。

  Transaction& operator+(const Transaction& rhs) {
    return *this;
  }

十、在operator= 中处理“自我赋值”

内置类型的自我赋值一般没什么问题,如果你管理着一些资源,例如堆内存就需要注意。

  1. 使用if (this == &rhs) return *this; 判断自我赋值(主要是这点)

  2. 需要考虑new新的资源失败,是否会影响两个赋值对象的原本值?可以先用一个指针临时保持旧的地址。

  3. 资源的转移,推荐使用copy and swap技术。(说白了是用rhs的资源对象,拷贝构造一个新的资源对象,然后交换这个临时对象)

十一、复制对象时勿忘其每一个成分(重要)

每一个成分,指对象自己和它的base class。即我们自定义拷贝、赋值构造函数时,需要手动调用所有base class的适当copying函数。例如:

class BuyTransaction {
  BuyTransaction(const BuyTransaction &rhs) : Transaction(rhs) {  // 初始化基类
    m_id = rhs.m_id;
  }

  BuyTransaction& operator= (const BuyTransaction &rhs) {
    if (&rhs == this)
      return *this;
    Transaction::operator=(rhs); // 手动调用基类的operator=
    m_id = rhs.m_id;
    return *this;
  }
};

copying函数需确保:

  1. 复制所有local变量

  2. 调用所有baseclasses内适当的copying函数

资源管理

十二、以对象管理资源

不只是堆内存,文件句柄、互斥锁、socket等都是需要不使用需要释放的资源。

  1. 使用工厂函数生成对象资源(暂定,需要细化实现接口)

  2. 资源申请后立即放入管理对象

  3. 依赖管理对象的析构函数释放资源,而不是使用delete操作符

  4. 需要注意析构函数执行过程中可能导致的异常,从而造成资源泄漏

  5. std::auto_ptr不能多个管理一个资源对象,当auto_ptr发送赋值、拷贝时,源ptr会被置null

  6. 使用RCSP(引用计数只能指针),类似shared_ptr替代auto_ptr

  7. 注意std的智能指针在析构时调用的是delete而不是delete[],这在析构数组的时候需要注意,对于可变数组可以用vector替代

十三、在资源管理类中小心copying行为

其实这里就是注意。因为资源管理类的拷贝行为是和业务相关的,行为主要有:

  1. 禁止复制

  2. 对底层资源使用“引用计数法”,对于不需要delete的对象,可以使用shared_ptr的自定义删除器(deleter)

  3. 复制底层资源,即深拷贝

  4. 转移底部资源所有权,类似auto_ptr

别忘了复制管理对象的时候,必须一同处理其管理的资源,无论使用哪种行为。

十四、在资源管理类中提供对原始资源的访问

即使用get方法或隐性转换(可能导致错误,不推荐)方式,面向用于提供原始资源访问的接口。

十五、成对使用new和delete时采取相同形式

即new、delete和new []、delete[]应成对使用。

应当注意使用typedef时,最后注释标注new和delete的使用类型。

十六、以独立语句将newed对象置入智能指针

如要传递智能指针做函数参数时,推荐先在外部独立一条语句,new出对象绑定智能指针,而后再传入函数参数。这是因为一条语句内的调用顺序编译器可能会优化,因此不排除语句内new是否会应异常导致资源泄漏。

设计与声明

十七、让接口容易被正确使用,不易被误用

部分方法:

  1. 保持接口统一一致,例如统一用size表达容器个数

  2. 在创建新类型、限制类型上做一些约束

  3. 使用shared_ptr的定制器deleter,避免夸动态库调用的一系列问题,也可以用来解锁mutex(第十三点

十八、设计class犹如设计type

把class的设计当成是语言设计者最初设计内置类型一样,需要考虑多个方面,主要体现在以下几个问题;

  1. 新的类型如何被创建和析构

  2. 对象的初始化和赋值会有什么差别,毕竟这对应不同的函数调用

  3. 新类型的对象以值传递时,意味着什么?需要记住用copy构造函数定义类型的以值传递

  4. 什么是新类型的合法值,在成员变量、构造函数、赋值操作符和成员变量的set函数时,应该考虑其约束

  5. 新的类型需要配合某个继承体系吗?例如成员函数特别是析构函数的virtual属性

  6. 什么样的操作符、函数对新类型是合理的?(如何定义成员函数这种废话)

  7. 什么样的标准函数应该驳回?(啥函数放private)

  8. 谁该取用新type的成员:设计啥函数是public、protected、private,啥类型和函数是friends

  9. 什么是新类型的“未声明的接口”:它对效率、异常安全性以及资源运用(多任务锁定和动态内存)有什么保障

  10. 你的新类型有多么一般化:如果我的类型是一个类型家族,可以考虑做成类模板

  11. 你真的需要一个新类吗?如果只是定义一个新的derived class 派生类,可以考虑能否通过增加一个成员函数实现

十九、宁以pass-by-reference-to-const替换pass-by-value

即对自定义类型,用于函数参数时尽量使用(const T &t)的方式。

额外小tips:

  1. const T &可以通过base类引用derived类对象,编译器底层貌似把引用转换为指针处理的

  2. 内置类型使用by-value比reference效率更高

注意:内置类型、STL迭代器和函数对象,使value传递更好。

二十、必须返回对象时,别妄想返回其reference

其实就是注意函数返回值为reference或指针指向local变量时,会发生找不到实体的异常,在必要的时候可以考虑以对象的形式返回,不必拘泥于pass-by-reference

二十一、将成员变量声明为private

  1. 将成员变量声明为private,通过函数接口进行访问,可以减少成员变量改变时对类外代码的影响。

  2. protected并不比public更有封装性,因为会影响derived class的代码。

二十二、尽可能用non-member、non-friend替换member函数

尽量使用non-member、non-friend调用组合调用member的方式来实现业务逻辑。整节我能理解的就是,因为类不能跨文件,而命名空间可以,我们就可以针对不同功能模块编写对应功能函数,将这些函数归到相同的命名空间下,使用时,用户针对需要使用的子模块,include对应的头文件即可编译和指定子模块相关的功能函数,方便扩展,降低编译依赖性。按我感觉,就是减少非核心函数在类中的容量。本节并没让我感觉这样做的优势有多大,可能巨型工程代码才能体现优势吧。

至于封装性,能访问成员变量的函数越少,封装性就越高,可能基于此,non-member、non-friend函数的封装性才显得高一些吧。。

二十三、若所有参数皆需要类型转换,请为此采用non-member函数

这里主要指运算符重载时,例如:

class Rational {
    public:
    Rational (int numerator = 0, int denominator = 1); //non-explicit
    int numerator () const;
    int denominator () const;
    const Rational operator* (const Rational & rhs) const;
};
Rational a(1, 8);
Rational b(1, 8);
Rational result = a * b; //ok
result = a * 2; //ok
result = 2 * b; // error

上例a * 2,a可以知道自己是Rational故调用operator* (const Rational & rhs),但2并不知道自己要转换到什么类型中,如发生编译错误。要解决该问题,需要使用non-member函数实现运算符重载:

const Rational operator* (const Rational & rhs, const Rational & lhs);

并且这个函数最好不用声明成Rational的friend吗,而是利用numerator、denominator等函数访问数据成员。

二十四、考虑写出一个不抛异常的swap函数

pimpl(pointer to implementation)手法:在类的内部使用一个指针,指向真实保存数据的impl对象,ceph代码中大量应用。

一个STL提供的swap算法如下:

namespace std {
    template<typename T>
    void swap (T& a, T&b)
    {
        T temp(a);
        a = b;
        b = temp;
    } 
}

以上方法,只要我们的自定义类支持拷贝和赋值操作,就能实现类型的swap。但在某些类内部数据有指针时,如我们自己的拷贝函数,会对指针指向的对象进行拷贝(设计原理上我们应该如此),而标准的swap函数将会带来额外的赋值开销,不符合swap高效的初衷。因此我们可以做swap的一个特化版本,专门为我们的类服务例如Weight,通常我们不能给std命名空间添加任何东西,但可以为标准templates(如swap)构造特化版本:

//首先,在Weight类中,定义一个public的swap函数,用于交换内部指针,然后在std命名空间特化swap,专属Weight服务

namespace std {
    template<>
    void swap<Weight> (Weight& a, Weight& b)
    {
        a.swap(b);
    }
}

如上实现,其实就是STL各个容器中的swap实现原理。

如果默认的std::swap版本对你的class的效率是可接受的,我们不需要做任何事情,直接使用即可。但如类内部使用形如pimpl手法的内部类实现时,就需要考虑用一下方法高效的实现一个定制化的swap:

  1. 写一个public的swap成员函数,让他高效的swap两个对象,且保证不会抛出异常

  2. 在class或template所在的命名空间中提供一个non-member的swap,并调用上面的swap成员函数

  3. 如果我们写的是class而不是class template,为class特化std::swap,并调用上面的swap成员函数

最好,如果我们调用swap,需要包含一个声明:"using std::swap",以便让std::swap在函数内部曝光,而调用swap时不加任何修饰符,这样当编译器找不到你对std::swap的全特化后,可以找到默认的std swap实现

最最后,不要尝试在std namespace重载,这会为std增加新的东西,这是不合法的,重载可以实现在其他namespace中。

感觉,通常只要实现一个成员swap,即可,这样做可能是为了让自定义类型能够在stl容器中套用swap算法吧。。。唉麻烦

实现

二十五、尽可能延后变量定义式的出现时间

尽可能延后对象的定义,直到能给他赋予初值为止,因为通常使用default构造后再赋初值效率比较低下。

二十六、尽量减少做转型的动作(需补充新转型的知识)

C++新提供是4种转换方法:

  1. const_cast<T>:用来将对象的常量性移除

  2. dynamic_cast<T>:执行“向下安全性转型”,用来决定某对象是否归属继承体系中的某个类型。

  3. reinterpret_cast<T>:执行低级转型,例如将一个pointer to int 转型为一个int

  4. static_cast<T>:用来强迫隐式转换,(常用!!)可将non-const转为const,int转double,void*转typed指针,基类指针转到派生类指针,但无法做const转non-const

总结:

  1. 尽可能避免转型,使用其他设计规避转型,在注重效率的代码中避免使用dynamic_cast

  2. 转型必要时,尽量隐藏在函数后,用户调用函数就行

  3. 使用新的c++类型转换,而避免使用旧的,主要是新式容易被识别出来,或者grep

二十七、避免返回handles指向对象内部的成分

其实就是成员函数应该避免传递内部成员变量的引用、指针和迭代器出来,不仅会破坏封装性,更可能因为无法掌握声明周期而导致handles空悬,程序crash。

二十八、为“异常安全”而努力是值得的

程序函数应该考虑出现异常后,对数据、对类内部的影响情况,最好能保证异常抛出后,能回到函数调用前的状态,再不济你应该保证数据回到合法的状态;或者可使用不抛出异常的函数或代码。通常来说,一个函数中如果再调用一个不是异常安全的函数,那么整个函数也都不是异常安全的了。

针对异常抛出后,数据回到调用前的状态,可以使用copy-and-swap方式实现,但并不是所有函数的实现都能靠这样的方法。

总之,就是写代码时,考虑异常的退回机制咯

二十九、透彻了解inlining的里里外外

inline函数注意!inline函数无法随着程序库的升级而升级,因为inline函数是将函数体替换进程序代码中的,inline函数千万不能应用于接口!!否则客户进程也需要重新编译才生效。

inline函数内部,大部分调试器是无法设置断点的,在使用inline时必须考虑使用inline对日后调试的影响。

注意:

  1. 将inlining限制在小型、被频繁调用的函数身上。

  2. 不要只因为function templates出现在头文件,就将它们声明为inline,因为这样的话,它的具象化的函数,也都是inline,容易造成代码体积增大

  3. 类中定义的成员函数隐含inline,如果friend函数也在类内搞,那它也是inline的

三十、将文件间的编译依存关系降至最低

对外接口,尽量提供Impl,即指针或者内部impl的方式,否则修改实现类将使得用户端代码重新编译。当然也可以使用接口类的方式对外提供接口,在派生类中提供具体实现,也能起到分离编译的作用。

总结:

  1. 依赖声明而不是依赖定义式,方法就是上面说的两种

  2. 程序库的头文件应该尽量只有声明

三十一、确定你的public继承塑模出is-a关系

is-a的定义:Derived对象都是一个Base对象,反之则不成立。

每一个适用于Base对象的事情,也同样适用于derived对象。

三十二、避免遮盖继承而来的名称

如果你继承 base class并加上重载函数,而你又希望重新定义或覆写(推翻)其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个 using声明式,否则某些你希望继承的名称会被遮掩。

总结:

  1. derived classes内的名称会遮掩 base classes内的名称。在 public继承下从来没有人希望如此。

  2. 为了让被遮掩的名称再见天日,可使用 using声明式或转交函数( forwarding functions)。

三十三、区分接口继承和实现继承

1,接口继承和实现继承不同。在 public继承之下, derived classes总是继承 base class 的接口

2,pure virtual函数只具体指定接口继承

3,简朴的(非纯) impure virtual函数具体指定接口继承及缺省实现继承

4,non- virtual函数具体指定接口继承以及强制性实现继承。

三十四、考虑virtual函数意外的其他选择

感觉根据需要使用即可,强记这些方法意义不大

  1. non-virtual interface NVI方法:使用public非虚函数,包裹private的虚函数

  2. strategy策略:使用函数指针成员变量传递要调用的方法

  3. function 成员变量来包装一个可调用对象(如2中的某个函数)

  4. 将derived类中的虚函数,替换成另一个dervied类中的虚函数(感觉是两个类的对象进行耦合,需要从实际问题中看,例如每个角色的生命值类,生命值类派生出level1、level99两种类,计算生命值的虚函数方法在levelX中实现,传递levelX进角色类中)

三十五、绝不重新定义继承而来的虚函数

额,伦理问题,其实只需记住,需要重新定义的情况,请记得把base类的函数声明为virtual

三十六、绝不重新定义继承而来的缺省参数值

virtual函数是动态绑定(运行时决定的),而缺省参数却是静态绑定的。意味着,使用Base的指针调用重新定义缺省参数的Dervied类时,是Base类的缺省参数生效。解决办法,使用上面说的NVI方法,用public 非虚函数包裹private的虚函数,用public函数实现缺省参数重新定义。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值