Effective C++55 笔记

简介: Effective C++55 笔记

1.对于单纯常量 , 最好以const对象或enum替换#define 。对于形似函数的宏(macros),最好用inline函数替换#define。

2. 尽可能使用const

   当进行操作符重载时,返回const对象可以预防那个"没意思的赋值操作".

   将某些东西生命为const可以帮助编译器侦测出错误用法,const可被施加于任何作用域内的对象,函数参数,函数返回类型,成员函数本体。

   编译器强制实时bitwise constness,但你编写程序时应该使用"概念上的常量性"(conceptual constness)

   当const和non-const成员函数有着实质等价的实现时,另non-const版本调用const版本可避免代码复用.

3. 确定对象被使用前已经被初始化

       要确保每一个构造函数都将对对象的每一个成员初始化。 如果成员变量是const或references,它们就一定需要初值,不能被赋值。为了避免需要记住成员变量何时必须在成员初值列表中初始化,合时不需要,最简单的做法就是。总是使用成员初值列。这样做有时候绝对必要,又往往比赋值更高效。

       c++有着十分固定的"成员初始化次序". base classes更早于其derived classes被初始化,而class的成员变量总是以其声明次序被初始化。 注意:初始化数组时需要指定大小,因此代表大小的那个成员变量必须先有初值。    

      关于non-local-static对象的问题:     如果某个编译单元内的某个non-local-static对象(也就是说该对象是global或位于namespace作用域内,或在class内或file作用域内被声明为static) 的初始化动作使用到了另一个编译单元内的某个non-local-static对象,它所用到的这个对象可能没有被初始化,因为c++对"定义于不同编译单元内的non-local-static对象的初始化次序并无明确定义。                    解决此问题的小设计:将每个non-local-static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接涉及这些对象。换句话说non-local-static对象被local-static对象替换了。 ---单例模式的常见做法.这种手法的基础在于: C++保证,函数内的local-static对象会在“该函数被调用期间”时被初始化。这保证了你所获得的那个reference指向了一个经历初始化的对象。

       为内置型对象进行手工初始化,因为C++不保证初始化它们。为免除"跨编译单元之初始化次序"问题,请以local-static对象替换non-local-static对象。

4. 如果你打算在一个内涵"reference成员“的class内支持赋值操作(assignment),你必须自己定义copy assignment操作符,面对"内涵const成员"的classes,编译器的反应也一样。更改const成员是不合法的,所以编译器不知道如何在它自己生成的赋值函数内面对它们。最后还有一种情况:如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。毕竟编译器为derived classes所生的copy assignment操作符想象中可以处理base class成分,但它们当然无法调用derived class无权调用的成员函数。编译器两手一摊,无能为力。

5. 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。(明确拒绝)。

6.polymorphic(待多态性质)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。       Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(ploymorphically),就不应该声明virtual析构函数。

7.析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序。           如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

8.在构造和析构期间不要调用virtual函数,因为这类调用从不会调至derived classes。

9.令赋值操作符重载返回一个reference to *this。

10.确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-ans-swap。

    确保任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

11. 当你编写一个copying函数时,请确保  1)赋值所有local成员变量,2)调用所有base calsses内的适当的copying函数  

     不要尝试以某个copying函数实现另一个copying函数。应该将共同技能放进第三个函数中,并由两个copying函数共同调用。

12. 为防止资源泄漏,请使用RAII 对象,他们在构造函数中获取资源并在析构函数中释放资源。

       两个常被使用的(资源在构造期间获得,析构期间释放)RAII classes分别是tr1::shared_ptr 和 auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向NULL。

13.复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。   普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting) 。不过其他行为也都可能被实现。

14.APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源"的办法,如

get() 。     对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较方便,但隐式转换对客户比较方便。

15. new对应delete    new[] 对应 delete[]。

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

processWidget(std::tr1::shared_ptr<Widget>(new Widget) , priority());
// 由于编译器的优化。可能有如下执行顺序
1。 new Widget
2.  priority
3.  tr1::shared_ptr构造函数
如果第二步抛出异常, 则第一步new出来的内存将泄漏。

17。 好的接口很容易被正确使用,不易被误用。你应该在你的所有接口中努力达成这些性质。

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

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

        shared_ptr支持定制型删除器。这可防范DLL问题(对象在动态连接程序库中被new创建却在另一个动态连接程序库中被delete销毁,可被用来自动接触互斥锁等等。

18. 设计class犹如设置type

问题:

1: 新type的对象应该如何被创建和销毁?

2: 对象的初始化和对象的赋值应该有什么样的差别?

3: 新type的对象如果被passed by value,意味着什么?记住,copy构造函数用来定义一个type的pass-by-value该如何实现。

4: 什么是新type的合法值?

5: 你的新type需要配合某个继承图系吗? 如果你继承自某些既有的classes,你就收到那些classes的设计的束缚,特别是受到”它们的函数是virtual或non-virtual“的影响。如果你允许其他classes继承你的class,那会影响你所声明的函数---尤其是析构函数-----是否为virtual。

6: 你的新type需要什么样的转换?你的type生存于其他一海漂types之间,因而彼此该有转换行为吗?

7: 什么样的操作符和函数对此新type而言是合理的?

8: 谁该取用新type的成员?

9: 什么是新type的”未声明接口? 它对效率、异常安全性以及资源的运用(例如在多任务锁定和动态内存)提供何种保证?

10: 你的新type有多么一般化?或许你其实并非定义一个新type,而是定义一整个types家族。果真如此你就不应该定义一个新class , 而是应该定义一个新的class template。

11: 你真的需要一个新type吗? 如果只是定义新的 derived class 以便为既有的class添加技能,那么说不定单纯定义一个或多个non-member 函数或templates,更能够达到目标。

19:尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。       以上规则并不使用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较合适。

20:绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。

21:切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。    protected并不比public更具封装性。

22: 宁可哪non-member non-friend函数替换member函数。 这样做可以增加封装性、包裹弹性和技能扩充性。

23: 如果你需要为某个函数的所有参数(包括this指针所指的按个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

24: 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。

       如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非template),也请特化std::swap。  

        调用swap时应针对std::swap使用using声明式,然后调用swap并不带任何”命名空间资格修饰“

        为”用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

25: 尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。(减少默认构造函数的调用,调用带初值的构造函数效率比较高)

26:尽量少做转型动作。

旧式转型仍然合法,但新式转型教受欢迎。 1:他们很容易在代码中被辨识出来(不论是人工辨识或者使用工具grep),因而得以简化“找出类型系统在哪个地点被破坏”的过程,2:各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。

如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。

如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码内。

宁可使用c++-style转型,也不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。

27:避免返回handles(包括references,指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并降低"虚吊号码牌“(dangling handles)的可能性。

28:异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型,强烈型、不抛异常型。

     ”强烈保证“往往能够以copy-and-swap实现出来,但”强烈保证” 并非对所有函数都可实现或具备现实意义。

      函数提供的“异常安全保证” 通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者

29:大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调用过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

      不要只因为function templates出现在头文件,就将它们声明为inline。

30:支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes 和 interface classes。

      程序头文件应该以”完全且仅有声明式“的形式存在,这种做法不论是否涉及templates都适用。

31:"public 继承" 以为is-a。适用于base classes身上的每一件事情也一定适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

32:派生类内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。  为了让被遮掩的名称再见天日,可使用using 声明式或转交函数。

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

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

      非纯虚函数具体指定接口继承及缺省继承。

      非虚函数具体指定接口继承以及强制性实现继承。

34:当你为解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方案。

1:使用non-virtual interface(NVI)方法,那是template method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。

2:将virtual函数替换为“函数指针成员变量”,这是strategy设计模式的一种分解表现形式。

3:以std::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是strategy设计模式的某种形式。

4:将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。

35:绝对不要重新定义继承而来的non-virtual函数

36:绝不要重新定义一个继承而来的缺省参数,因为缺省参数值都是静态绑定,而virtual函数---你唯一应该复写的东西却是动态绑定。

37:复合(composition)的意义和public继承完全不同。在应用域,复合以为has-a,在实现域,复合意味(根据实物实现)

38:private继承意味is-implememted-in-terms-of(根据某物实现出)。它通常比复合(composition)的级别低。但是当dervied class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。

    和复合(composition)不同,private继承可以造成empty base 最优化。这对致力于“对象尺寸最小化”的程序开发者而言,可能很重要。

39:多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual继承的需要。

      virtual继承会增加大小,速度,初始化,复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。

      多重继承的确有正当用途。其中一个情节涉及“public继承某个interface class" 和 :"private继承某个协助实现的class"的两相组合。

模板与泛型编程

40:classes和templates都支持接口和多态。

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

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

41:声明template时 ,前缀关键字 typename 和 class 可以互换。

      请使用关键字typename 标识嵌套从属类型名称;但不得在base class list  活 member initialization list 内以它作为base class 修饰符。

42:可在derived class templates 内通过"this->"指涉base class templates内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。

43:请使用member function templates(成员函数模板)生成"可接受所有兼容的类型"的函数

       如果你声明member templates用于“泛化copy构造”或“泛化assignment:"操作,你还是需要声明正常的copy构造函数。

44:当我们编写一个class template,而它所提供之“与此template 相关的”函数支持“所有参数之隐士类型转换” 时 , 请将那些函数定义为:class template内部的friend函数。

45:set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。

      Nothrow new 是一个颇为局限的工具,因为它只适用于内存分配,后续的构造函数调用还是可能抛出异常。

46:operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就应该调用new-handler。它也应该有能力处理0 bytes申请。Class专属版本则还应该处理“比正确大小更大的(错误) 申请。

       operator delete应该在收到null指针时不做任何事情。Class专属版本则还应该处理”比正确大小更大的(错误)申请。

47:当你写一个placement operator new , 请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。

      当你声明placement new和placement delete ,请确定不要无意识(非故意)地遮掩了它们的正常版本。

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

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

目录
相关文章
|
6月前
|
算法 C++
算法笔记:递归(c++实现)
算法笔记:递归(c++实现)
|
6月前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第一章 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第一章 笔记
|
4月前
|
C++ 容器
【C/C++笔记】迭代器
【C/C++笔记】迭代器
28 1
|
4月前
|
存储 安全 程序员
【C/C++笔记】迭代器范围
【C/C++笔记】迭代器范围
73 0
|
5月前
|
C++ Windows
FFmpeg开发笔记(三十九)给Visual Studio的C++工程集成FFmpeg
在Windows上使用Visual Studio 2022进行FFmpeg和SDL2集成开发,首先安装FFmpeg至E:\msys64\usr\local\ffmpeg,然后新建C++控制台项目。在项目属性中,添加FFmpeg和SDL2的头文件及库文件目录。接着配置链接器的附加依赖项,包括多个FFmpeg及SDL2的lib文件。在代码中引入FFmpeg的`av_log`函数输出"Hello World",编译并运行,若看到"Hello World",即表示集成成功。详细步骤可参考《FFmpeg开发实战:从零基础到短视频上线》。
220 0
FFmpeg开发笔记(三十九)给Visual Studio的C++工程集成FFmpeg
|
7月前
|
存储 C++ 容器
黑马c++ STL部分 笔记(7) list容器
黑马c++ STL部分 笔记(7) list容器
|
7月前
|
算法 C++ 容器
黑马c++ STL常用算法 笔记(5) 常用算术生成算法
黑马c++ STL常用算法 笔记(5) 常用算术生成算法
|
7月前
|
算法 C++ 容器
黑马c++ STL常用算法 笔记(4) 常用拷贝和替换算法
黑马c++ STL常用算法 笔记(4) 常用拷贝和替换算法
|
7月前
|
存储 算法 搜索推荐
黑马c++ STL常用算法 笔记(3) 排序算法
黑马c++ STL常用算法 笔记(3) 排序算法
|
7月前
|
算法 C++
黑马c++ STL常用算法 笔记(2) 查找算法
黑马c++ STL常用算法 笔记(2) 查找算法