登录  
 加关注
查看详情
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

秒大刀 博客

好好学习 天天向上

 
 
 

日志

 
 
 
 

读《Effective C++ 中文版 第三版》  

2009-01-19 00:06:22|  分类: 读书 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

读《Effective C++ 中文版 第三版》 - 秒大刀 - 秒大刀的城堡

最深感受:C++语言因为兼容已经背上了沉重的包袱,高效是高效,但确实不太好用,尤其是在貌似朴素的语法背后隐藏着如此之多的高深用法。


条款1:视C++为一个语言联邦

    C++支持的编程形式:面向过程、面向对象、函数编程、泛型编程、元编程

    C++ template机制自身是一步完整的图灵机,可以被用来计算任何可计算的值。模板元编程是一种“在C++编译器内执行并于编译完成时停止执行”的程序

条款2:尽量以const,enum,inline替换#define

    常量字符串的定义应该是:const char* const text = "text"; 最好是const std::string text("text");

    注意一下enum hack的技术

条款3:尽可能使用const

    当要实现同一成员函数的const和非const版本时,为了避免重复可以用非const函数调用const函数(有点坏)

    const char& operator[](std::size_t position) const;

    char& operator[](std::size_t position)

    {

        return const_cast<char&>(static_cast<const T&>(*this)[position]);

    }

条款4:确定对象被使用前已被初始化

    初始化列表的效率要高于构造函数中的赋值,前者属于初始化而后者属于赋值。

    总是使用初始化列表,即使是自定义类型,这样做可以避免总需要记住那些成员需要初始化而导致的遗漏。

    基类的初始化早于子类,成员变量的初始化顺序是声明的顺序而非初始化列表中的顺序。故建议初始化列表中的顺序遵从声明的顺序。

    定义于不同编译单元内的non-local static对象的初始化次序无明确定义

    将non-local static对象移到函数内部形成local static对象则可明确对象的初始化顺序,Singleton的实现即是采用该策略。但多线程情况下可能会有初始化竞争风险,在单线程启动阶段手工调用所有的静态对象的初始化函数可消除该风险。

    non-local static对象的local static包装函数是可以内联的。

条款5:了解C++默默编写并调用了哪些函数

    编译器会自动生成copy构造函数、copy赋值、析构函数,若没有提供构造函数,还将生成默认的构造函数。这些函数都是public inline性质的,只有它们需要被调用时才由编译器创建。

    当类内部包含reference成员,或者const成员,或者基类的copy assignment被设置为private,这时编译器将拒绝自动生成copy assignment。

条款6:若不想使用编译器自动生成的函数,就该明确拒绝

    将不需要的默认函数声明成private而不提供实现,就可以避免外部、friend、以及member的调用。

    class Uncopyable

    {

    protected:

        Uncopyable(){}

        ~Uncopyable(){}

    private:

        Uncopyable(const Uncopyable&);

        Uncopyable& operator=(const Uncopyable&);

    };

    class X : private Uncopyable

    {

    };

条款7:为多态基类声明virtual析构函数

    任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数

    继承任何不带virtual的基类都可能是有风险的,继承STL容器和std::string显示是不明智的。

    在不必要的时候请不要声明virtual析构函数。

    下面的技巧可以防止设计为基类的类被实例化并能保证virtual析构的正确性

  class AbstractBase

    {

    public:

        virtual ~AbstractBase() = 0;

    };

    AbstractBase::~AbstractBase()

    {

    }

    class X : public AbstractBase

    {

   };

条款8:别让异常逃离析构函数

    二次异常会导致程序行为不确定;析构异常可能导致析构不完全

条款9:绝不在构造和析构过程中调用virtual函数

    基类构造期间,虚函数还不虚。子类对象构造时先构造基类对象,此时该对象还不是子类对象。构造和析构过程中的虚函数调用不会下降至子类这一层。

条款10:令operator=返回一个reference to *this

    这样可以实现链式赋值

条款11:在operator=中处理自我赋值

    copy and swap,长见识了

条款12:复制对象时勿忘其每一个成分

    拷贝构造函数和拷贝操作符之间不能相互调用,加入新的函数可以避免代码重复

条款13:以对象管理资源

    auto_ptr被复制后原对象变成null,auto_ptr不适合放入STL容器,因为这些容器是copy性质的

    tr1::shared_ptr是一个添加了引用计数的聪明指针

    auto_ptr和shared_ptr都不要用于数组,否则会郁闷的

    RAII:Resource Acquisition Is Initialization, 资源取得时期便是初始化时期

条款14:在资源管理类中小心coping行为

    或者禁止copy行为,或者采用引用计数的假拷贝,或者采用资源的深度拷贝,或者采用auto_ptr的所有权转移策略,随需而定

条款15:在资源管理类中提供对原始资源的访问

    为了保证资源管理里和其他模块的适配能力有必要在资源管理类中提供对原始资源访问时,有限考虑类型get这样的访问函数,尽量避免使用隐式类型转换。

条款16:成对使用new和delete时要采取相同形式

    new - delete,new[] - delete[]

    尽量不要对数组进行typedef

条款17:以独立语句将newed对象置入智能指针

    编译器对于语句的执行顺序没有调整权,但对同一条语句可能调整执行顺序,比如函数实参代入,不要在这时候写可能的风险语句

    以独立的语句将newed对象存贮于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏

条款18:让接口容易被正确使用,不易被误用

    .net容器的Count和Length确实很让人反感

    cross-DLL problem,对象在一个dll中被new出来,缺在另一个dll中被delete,这可能会导致运行时错误

    “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容

    “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任

条款19:设计class犹如设计type

    能设计出至少像c++内置类型一样好的用户自定义类型是很爽的一件事

条款20:宁以pass-by-reference-to-const替换pass-by-value

    尽量以pass-by-reference-to-const替换pass-by-value,这通常比较高效,并可避免由子类对象拷贝成基类时的对象切割问题

    对于内值类型、STL迭代器和函数对象应该采用pass-by-value的形式。

条款21:必须返回对象时,别妄想返回其reference

    有时候很难避免以拷贝形式返回对象。返回堆对象指针或引用可能带来内存维护问题;返回栈对象指针或引用绝对是错误的;返回局部static对象在多线程环境和同时需要多个对象时也会很郁闷。

条款22:将成员变量声明为private

    请将成员变量声明为private而不是public或者protected,这可赋予客户访问数据的一致性,可细微划分访问控制,允许约束条件获得保证,并提供class作者以充分的实现弹性。

条款23:宁以non-member、non-friend替换member函数

    class设计时应该从OO上考虑是否应该将某些函数作为成员函数。那些并不归属于该类的函数不能以成员函数出现,而应该作为某namespace下的实用全局函数。这样可以尽量的保证类接口的简洁。

条款24:若所有参数皆需类型转换,请为此采用non-member函数

       如果需要对某个函数的所有参数(包括this指针这个隐参数)进行类型转换,哪么该函数必须为non-member函数

条款25:考虑写出一个不抛异常的swap函数

    任何用户自定义加入到std名字空间下的东西都可以认为有不可预期的行为,因为编译器可能对该名字空间做特殊的处理。用户可以全特化std内的templates,但不能添加新的templates。

    std::swap是一个针对值类型的拷贝实现版本,如果要实现引用类型的swap请提供一个特化版本

    当前名字空间中的模板t特化了x名字空间中的模板t后,不指明x::t的调用而采用using x::t; t;的调用会给编译器灵活最佳匹配的权利

条款26:尽可能延后变量定义式的出现时间

    尽量延后变量的定义,这会对理解维护和运行时都有好处

    对于循环中要使用的临时变量,除非构造析构代价很大,并且循环次数足够多,而代码此处对效率要求极高时,可以将其定义在循环外;否则最好定义在循环内部,这样对代码的理解有好处,且不会造成作用域的不必要扩大。

条款27:尽量少做转型动作

    const_cast 用来将对象的常量性移除

    dynamic_cast 用来执行“安全向下转型”,该转型不能被C风格的转型替代,并且执行效率较差

    reinterpret_cast 执行低级转型,动作和实际效果决定于编译器,不可移植

    static_cast 强迫隐式转换

    使用C++风格的转型比C转型好,因为:很容易看出来什么地方用了转型进而跟踪类型系统可能被破坏的地方;转型动作目标变窄,方便编译器诊断错误。

    任何一个转型动作往往会使编译器生成一些运行时代码,而不是啥都不做仅将一种类型看做是另一种类型,转型是需要运行时代价的

    在C++中,复杂继承中的单一对象可能会有多个地址指向自己,这在C、java、C#中都是不可能的,但C++中完全可能。任何时候都应该避免有“对象在C++中如何布局”的假设。

    泛型最好是基于指针或引用来操作,而不是对象本身

    尽可能避免转型,特别是在注重效率的代码中避免dynamic_cast,尝试调整设计来避免转型

    如果转型是必要的,应将这些龌龊的事情隐藏在某个函数背后

条款28:避免返回handles指向对象内部成分

    返回对象内部的数据的引用、指针或者迭代器等这些handles都将严重的降低对象的封装性,因为用户可以用这些handlds去意外的操作对象,并且这些handles的生命期可能会比对象更长。

条款29:为“异常安全”而努力是值得的

    异常安全是指函数抛出异常时 不允许资源泄漏 不允许数据破坏。

    异常安全的层次:

        基本保证:如果异常被抛出,程序内的任何事务仍然保持在有效状态下

        强烈保证:如果异常抛出,程序状态不改变。函数要么完全执行成功,要么在执行失败时恢复到调用函数之前的状态

        不抛异常保证:总是能够完成承诺的功能而不抛出异常

    copy-and-swap可以保证对象的原子修改,实现常采用先把内部数据拷贝一份,修改完成后再更新指向改数据的执行并删除原数据

条款30:彻底了解inlineing的里里外外

    太复杂的inline将会被编译器拒绝,比如循环和递归

    virtual inline一般会被编译器拒绝

    通过函数指针调用的inline会被编译器拒绝

    不要滥用inline,这会使调试和二进制升级更容易,并能缓解代码膨胀和不必要的运行时开销

条款31:将文件件的编译依存关系降至最低

    将一个类拆成内部实现类和接口声明类,然后在接口声明类中以一个指针去指向内部实现类,这可以解决非接口修改带来的重新编译(Handle classes方式,这将带来动态内存管理的负担)。采用interface也可以达到类似的效果(Interface classes方式,这将带来virtual函数调用的负担)。

    让头文件尽可能的自我满足可以降低编译依存

    如果使用object reference或object pointer可以完成任务,就避免使用object,因为编译器需要知道object对象的大小,这会对object对象的修改敏感

条款32:确定你的public继承塑模出is-a关系

    public继承表示is-a关系,即基类的所有行为都适用于子类

条款33:避免遮掩继承而来的名称

    子类的函数声明会覆盖基类相同签名的函数

    using表达式或者inline的调用传递函数可以使private继承的基类的函数重见天日

条款34:区分接口继承和实现继承

    纯虚函数也可以提供默认实现,这样子类即必须实现该方法,也可以通过范围解析来调用基类的默认实现。

条款35:考虑virtual函数以外的其他选择

    藉由Non-Virtual Interface(NVI)手法实现Template Method模式

        virtual函数应该非公有,并添加一个public的方法来转调由子类负责定制的virtual非公有函数。这会将何时工作和怎么工作的问题分开,并能提供很多实现上的灵活性。

    藉由Function Pointers实现Strategy模式

        Function Pointers在.net中抽象为delegate,virtual function只能提供类级别的行为定制,而delegate可以提供对象级别的行为定制和运行时动态的行为替换。但delegate可能稍微降低了对象的封装性。

    藉由tr1::function完成Strategy模式

        tr1::function允许表示函数指针、仿函数对象、成员函数指针等任何接口兼容或者隐式类型转换后兼容的可调用体,就像delegate。

        std::tr1::function<int (int, int)> addFunc(std::tr1::bind(&Calculator::add, calObj, _1)); // 将成员函数绑定到function

    古典的Strategy模式

        Strategy的宿主体系采用继承实现,Strategy的算法实现体系也采用继承实现,这样可以提供更大的灵活性和算法的可重用机会

    将算法实现由class移到外部将无法访问class内的non-public资源

条款36:绝不重新定义继承而来的non-virtual函数

    不要在子类中覆盖基类的non-virtual函数,否则对象将会出现精神分裂症,调用的效果不由对象本身决定反而由调用者决定了。

条款37:绝不重新定义继承而来的缺省参数值

    不能重新定义子类中virtual函数的默认参数值,并且也不要写默认值,即使保持和基类的默认值一致,因为这会带来代码重复。

    通过NVI(non virtual interface)手法在基类包装一个带默认参数non-virtual函数,然后在子类中去实现无默认参数的virtual是个好方法

条款38:通过复合塑模出has-a或“根据某物实现出”(is-implemented-in-terms-of)

    在应用域,复合意味着has-a;在实现域,复合意味着is-implemented-terms-of

条款39:明智而审慎的使用private继承

    private继承在设计层面无意义,仅是一个软件实现上的技巧。

    private继承可以阻止编译器将一个子类对象转换成基类对象,子类仅仅是使用了基类的实现,而摒弃了所有的实现接口。

    聚合和private继承具有类似的设计功效,但应尽量使用聚合,除非为了在子类里访问基类里的protected成员或重写基类里的virtual函数

    C++没有成员的对象大小仍然不能为0。当子类继承自无成员变量类时编译器会进行EBO(empty base optimization 空白基类最优化),这会比使用聚合节省点空间

条款40:明智而审慎的使用多重继承

    virtual继承会增加大小、速度、初始化(及赋值)复杂度等成本

条款41:了解隐式接口和编译器多态

    面向对象编程世界是以显式接口和运行时多态来解决问题的;Template和泛型编程更注重隐式接口和编译器多态

    对class而言接口是显式的,以函数签名为中心。多态则是通过virtual函数发生在运行期

    对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期

    template会比interace实现有更大的灵活性和更好的性能

条款42:了解typename的双重意义

    当需要在template中指涉一个嵌套从属类型名称时需要在前面加上typename关键字以标明这是个类型。但typename关键字不能出现在base classes list内的嵌套从属类型名称之前,也不能出现在member initialization list中作为base class修饰符

条款43:学习处理模板化基类内的名称

    模板类继承时子类不能直接访问基类的方法,解决方法:this->;用using BaseClass<T>::SomeFunction进行方法导出;显式的BaseClass<T>::SomeFunction调用,但如果该该函数是虚函数将会关闭虚函数的绑定行为。

条款44:将与参数无关的代码抽离templates

    Template生成多个class和多个函数,任何template代码的都不该与某个造成膨胀的template参数产生相依关系

    非类型模板参数造成的代码膨胀,往往可以用函数参数或class成员变量代替tempalte的非类型参数而消除

    因类型参数造成的代码膨胀,可以通过共享使用完成相同二进制表述的具现类型代码来消除。比如对所有的指针类型底层统一采用void*来处理

条款45:运用成员函数模板接受所有兼容类型

    模板拷贝构造函数和模板赋值函数并不能组织编译器生成默认的拷贝构造函数和赋值函数,一般还需要定义正常的拷贝构造函数和赋值函数

条款46:需要类型转换时请为模板定义非成员函数

    template在参数推到过程中从不会采用隐式类型转换

    当编写一个“与此template相关的”函数支持“所有参数的隐式类型转换”的class template时,应将这些函数定义为“class template内部的friend函数”

条款47:请使用traits classes表现类型信息

    iterator_traits要求用户自定义的iterator内必须嵌套一个名为iterator_category的typedef

    如何使用traits class:

        建立一组重载函数(身份像劳工)或函数模板,彼此间的差异只在于格子的traits参数。令每个函数实现码与其接受的traits信息相应和

        建立一个控制函数(身份像工头)或函数模板,它调用上述那些“劳工函数”并传递traits class所提供的信息

    traits classes使得“类型相关信息”在编译期可用,以templates和templates特化完成实现

    整合重载技术后,traits classes可以在编译期对类型执行if-else测试

条款48:认识template元编程

    Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率

    TMP可被用来生成“based on combinations of policy choices”的客户定制代码,也可以用来避免生成对某些特殊类型并不适合的代码

条款49:了解new-handler的行为

    malloc在内存分配失败时返回NULL,new在内存分配失败时抛出std::bad_alloc异常,但new (std::nothrow) char[size]这个怪胎在失败时返回NULL而不抛异常,nothrow new只负责内存分配而不管对象构造的

    当operator new无法满足内存申请时,会不断调用new-handler函数知道获得足够的内存。

    new-handler的签名为 typedef void (*new_handler)(); 设置方法为 set_new_handler(MyNewHandler); 卸载方法为 set_new_hanlder(NULL); 得到当前new-handler方法为 lock(); new_handler curHandler = set_new_handler(NULL); set_new_handler(curHandler); unlock();

    new-handler应该让更多的内存可用,需要在new-handler中安装另外的new-hander以尝试得到内存,或者应该卸载new-handler这将抛出bad_alloc异常,自己手工抛出bad_alloc异常或者调用abort、exit结束程序也可以

    可以为类自定义operator new然后在里面set_new_handler定制出针对特定类型的new-handler处理

条款50:了解new和delete的合理替换时机

    自定义new和delete可以检测忘记delete和delete多次的情况,通过多分配内存并写入签名在delete时检测签名可以发现内存的越界访问

    自定义new和delete可以得到程序运行时对动态内存使用的统计数据

    自定义new和delete可以特化以提高性能,因为编译器默认的new和delete必须中庸的适应几乎所有的应用

    自定义new和delete可以降低缺省内存管理器带来的空间额外开销

    自定义new和delete可以控制字节对齐,比如double在8字节对齐时具有最好的性能

    自定义new和delete可以将大量对象成簇集中而减小运行时可能的页切换

    自定义new和delete可以获得某些非传统的行为,比如对只能用C进行操作的共享内存API进行C++封装

条款51:编写new和delete时需要固守常规

    operator new内部应该包含一个无限循环,并在其中尝试分配内存

    operator new必须返回正确的值,内存不足时应该调用new-handler函数,必须对付大小为0的内存分配,需避免不慎覆盖了正常形式的new

    operator new是可以被子类继承的,这往往是错误的,因为子类对象大小一般都比基类大,用在operator new[]上更绝对是错误的。可以通过比对size是否和基类的size相等来判断是否采用自定义的operator new or delete算法,否则采用::operator new or ::operator delete来防止对子类对象的不兼容。

    C++要求operator delete在处理NULL指针时是有效的

条款52:写了placement new也要写placement delete

    标准的placement new在头文件<new>中的定义为:void* operator new(std::size_t, void* pMemory); 

    placement new的第二个参数可以为任何自定义类型,并且也可以有更多的参数

    如果自定义了placement new必须定义一个和自定义placement new参数签名完全一致的placement delete,该函数将在placement new成功但构造函数失败时自动的被运行时框架调用,否则会发生内存泄漏,并应同时定义一个签名为void operator delete(void* pMemory) thorw()的普通placement delete这将用于delete p形式的调用。

    C++缺省在global作用域提供一下形式的operator new:

        void* operator new(std::size_t) throw(std::bad_alloc); // normal new

        void* operator new(std::size_t, void*) throw(); // placement new

        void* operator new(std::size_t, const std::nothrow_t&) throw(); //nothrow new

    当在class内声明任何operator new时,将会覆盖上述的标准形式。可以定义一个基类包装标准new,然后在子类中using导出即可。

条款53:不要轻忽编译器的警告

    严肃对待编译器发出的警告信。努力在你的编译器的最高(最苛刻)警告级别下争取“无任何警告”的荣誉

    不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息可能消失。

条款54:让自己熟悉包括TR1在内的标准程序库

    不熟悉TR1技能而却奢望成为一位高校的C++程序员是不可能的,因为TR1提供的机能几乎对每一个程序库和每一种应用那个程序都带来利益。

    TR1包括14个新组件:

    tr1::shared_ptr:引用计数性质的智能指针。注意不能应用循环引用情况

    tr1::weak_ptr:不对shared_ptr引用计数进行影响的“弱”指针,可以和shared_ptr搭配使用以解决资源的循环引用问题。当shared_ptr因引用计数为0而被释放时,相关的weak_ptr自动标记为无效。

    tr1::function:delegate在C++中的实现,各种函数指针和仿函数已经带来了太多的混乱。

    tr1::bind:是对第一代绑定器bind1st和bind2nd的改进。

    tr1::unordered_set、tr1::unordered_multiset、tr1::unordered_map、tr1::unordered_multimap,以hash为基础,不保证容器中元素的任何次序。

    正则表达式:早该加了,快被郁闷死了

    tr1::tuple:可以持有任意个数对象的变量组,而pair只能持有两个

    tr1::array:对原生数组的STL接口包装

    tr1::mem_fn:类似成员函数指针,是对mem_fun和mem_fun_ref的扩充

    tr1::reference_wrapper:可以将引用伪装成一个对象。可以用来欺骗只能持有对象或者对象指针的容易也用来持有对象引用。

    随机数生成工具:大大超过了rand的能力

    数学特殊函数:包括Laguerre多项式、Bessel函数、完全椭圆积分等

    C99兼容扩充:一大堆的函数和模板,将许多新的C99程序库特性带进C++

    Type traits:可以在编译时获取“萃取”类型的相关信息

    tr1::result_of:用于推导函数调用的返回类型

条款55:让自己熟悉Boost

    http://www.boost.org

    http://www.stlchina.org/


2009-09-09

条款33:避免遮掩继承而来的名称
       using表达式或者inline的调用传递函数可以使private继承的基类的函数重见天日

private继承中的using可见扩展语法是: using BaseClass::function; 如此,所有名称为function的函数都变的可见。该方法无法对重载做细致处理,只能写inline的转接函数了。

  评论这张
 
阅读(3455)| 评论(0)

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018