《Effective C++》学习总结

来源:互联网 发布:芒果tv没有mac版 编辑:程序博客网 时间:2024/05/16 06:06

1.    一个base class的设计通常有两个用途,一个是用于多态用途,另一个是用于非多态用途。

多态用途:base class 必须带有virtual函数,同时含有virtual析构函数。

非多态用途:base class没有virtual函数,也没有virtual析构函数。例如,Uncopyable和标准程序库的input_iterator_tag。

2.    任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。因为带有virtual函数意味着其基类带有多态性性质的,所以,析构函数也应该是多态性质的。多态的目的“经由base class接口处置derived class 对象”。

3.    如果你曾经企图继承一个标准容器或其他“带有non-virtual”的class(比如,string类型),都是不合理的,因为它们的析构函数都是“non-virtual”的。最终会导致派生类对象析构时只会“局部析构”。它们的类的设计目的就是不是为了将来的派生而用。C++没有像Java(final classes)和C#(sealed classes)中有“禁止派生”的机制。

4.    析构函数抛出异常是一件危险的事,原因:

(1)当在try语句中有多个对象析构时,有多个抛出异常或是同类异常时,只有一个异常被捕获了,其他异常不能捕获(这有可能某些资源被泄露),导致异常机制调用terminate()。

(2)当处理函数异常过程中,有出现局部对象的析构时抛出的异常(情形:在函数体中try语句之前的局部对象,在进行“栈解退”时),上层函数无法捕获这两类异常。

解决之道:

(1)将资源对象交给一个管理对象进行管理,使其客户有机会对可能出现的问题作出反应。目的使得发生在析构函数中的异常几率降低,尽可能异常有普通函数捕获异常并处理(而非在析构函数中),这样在客户端可以有机会第一手处理异常问题。

(2)析构函数决定不能抛出异常。如果一个被析构函数调用的函数有可能抛出异常,析构函数应该捕获任何异常,同时可以选择记录此处异常信息,然后吞下它们(不向上抛出,即不传播)或结束程序。

保证对象的析构函数不抛出异常是件合理的事,这是为了使C++异常处理模型得以顺利进行的一项支持。比如保证“栈解退”机制中顺利进行,以解决处理其他异常。

5.    直接调用构造函数就是创建一个匿名对象。

6.    在构造和析构函数期间不能调用virtual函数,因为这类调用从不下降至derived class。但为了实现因类型(派生类对象)的不同处理方式不同,可以通过这条路径模拟:令derived class将必要的构造信息向上传递至base class构造函数。

原因:

(1)    如果此调用的virtual函数下降至derived class阶层,要知道derived 函数几乎必然取用local成员变量,而这些成员变量尚未初始化,这是很危险的行为,所以C++不让你走这条路。

(2)    根本原因:在derived class 对象的base class构造期间,对象的类型是base class,而不是derived class。不只是virtual函数会被编译器解析至base class,若使用运行期类型信息(比如,dynamic_cast和       typeid),也会把对象视为base class类型。

7.    在base class构造期间,virtual函数不是virtual函数。

8.    在operator=函数中,必须返回*this的引用,同时让operator=函数具备“自我复制”,具备“异常安全性”(方法1:只有申请内存成功之后,才释放原先的指针所指的内存。方法2:使用copy-and-swap技术)。

9.    类的静态类成员变量或函数都是在创建对象(调用构造函数)之前已经初始化完毕了。(非静态成员的引用必须与特定的对象相对应)静态成员函数不必通过对象进行调用,因为它属于类而不是属于某个对象的。

10.  “栈解退”过程,会调用局部对象的析构函数对其释放内存,但deleteobject的动作不可逆,也就是在try语句块中有删除语句,回滚之后,对象已经不存在了(不能死而复生)。

11.  获取资源后立刻放进管理对象(managing object)内。实际上“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization; RAII),有时候获取资源被拿来赋值(而非初始化)给某个管理对象,但不论哪种做法,每一笔资源都在获得的同时立刻被放进管理对象中。管理对象运用析构函数确保资源被释放。

12.  基于对象的资源管理办法,是建立在C++对构造函数、析构函数、coping函数的基础上。

13.  引用计数型智能指针(reference-counting smartpointer; RCSP):持续追踪共有多少个对象指向某笔资源,并在无人指向它时自动删除该资源。这种RCSP策略类似于垃圾回收机制,不同的是RCSP无法打破环形引用(recycles of references, 例如两个没被使用的对象彼此互指,因而好像还处在“被使用”状态)。shared_ptr对象就是采用这种策略的,其析构函数内做delete而不是delete[]动作。

14.  为了防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。常见的RAII classes 有tr1::shared_ptr和auto_ptr。前者是较佳选择,因为其coping行为比较直观,若选择auto_ptr,复制动作会使它原来指针指向null(也即所有权转移)。

15.  APIs往往需要访问原始资源(raw resources),所以每一个RAIIclass应该提供一个“取得其所管理的资源”的办法。办法通常有两种:

(1)    显示转换

比如tr1::shared_ptr和auto_ptr都提供了一个get成员函数,用来执行显示转换,也就是它会返回智能指针内部的原始指针(的复件)。

FontHandle get() const { return f; } //显示转换函数

(2)    隐式转换

operator FontHandle () const { return f; } //隐式转换函数

       一般而言,显示转换比较安全,但隐式转换对客户比较方便。

16.  几乎所有智能指针,都重载了指针取值操作符(operator->和 operator *),它们通常允许隐式转换至底部原始指针。

17.  无论是带[ ]还是不带[ ]的delete,都可以用于空指针。

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

原因是C++编译器对函数参数核算的次序有很大的自由。

比如:processWidget(std::tr1::shared_ptr<Widget>(newWidget) , priority() ); C++编译器必须完成三步,执行“new Widget”表达式,调用“tr1::shared_ptr”构造函数,调用priority函数,这三者的操作顺序是不确定的(由C++编译器决定),但,执行“new Widget”表达式,调用“tr1::shared_ptr”构造函数,这两者的相对顺序是确定的。因此,当操作“调用priority函数”排在第二步执行,如果此时发生异常时,“new Widget”返回的指针将会遗失,可能导致对processWidget的调用过程中可能引发资源泄漏。因为,“资源创建”和“资源被转换为资源管理对象”两个时间点之间有可能发生异常干扰。

分离语句:std::tr1::shared_ptr<Widget> pw (new Widget);processWidget(pw, priority() ); 于是,三者的操作顺序就确定了。

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

什么是切割问题:当一个derived class对象以pass-by-value方式传递并被视为一个base class 对象,bass class的copy构造函数会被调用,造成此对象的行为没有了像derived class对象的那些独有的性质,也就是那些性质被切割了,仅仅留下一个base class对象。下面的将会出现切割现象:

void printNameAndDisplay ( Window w ) ;

WindowWithScrollBars wwsb; //WindowWithScrollBars类继承于Window类

printNameAndDisplay(wwsb); //出现切割现象,不能企图在该函数中使用WindowWithScrollBars类独有的性质或功能,比如virtual。

20.  绝不要返回reference指向一个heap-allocated对象;绝不返回pointer或reference指向一个local stack对象;返回pointer或reference指向一个local static 对象,意味着多个pointer或reference指向一个相同的local static对象,这样的操作在多线程下有个竞争问题,那就是初始化local static对象的时候。

21.  任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦,处理这个麻烦的一种做法是:在程序的单线程启动阶段(single-threaded startup portion)手工调用所有的reference-returnning函数,这可消除与初始化有关的“竞速形势(race conditions)”

22.  某些东西的封装性与“当其内容改变时可能造成的代码破坏量”成反比。protected成员变量就像public成员变量一样缺乏封装性,因为在这两种情况下,如果成员变量改变时,都会有不可预知的大量的代码受到破坏。一个使用它所有的derived class都会被破坏,另一个所有使用它的客户代码被破坏。因此,从封装角度观之,其实只有两种访问权限,private(提供封装)和其他(不提供封装)。

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

24.  越多的东西被封装,就越少的人能看到它,而越少的人看到它,我们就有越大的弹性去改变它,因为我们的改变仅仅直接影响看到改变的人事物。

25.  namespace和classes不同,前者可以跨越多个源代码文件,而后者不可以。后者并不适用这种切割机能,因为一个class必须整体定义,不能分割为片片段段。举例:

//头文件“webbrowser.h”这个头文件针对class webbrowser自身及webbrowser核心机能。

namespace WebBrowserStuff

{

        classwebbrowser { …… };

        …  //核心机能,例如所有用户都需要的non-member函数。

}

// 头文件“webbrowserbookmarks.h”

namespace WebBrowserStuff

{

        ……  //与书签相关的便利函数 (non-member函数)

}

// 头文件“webbrowsercookies.h”

namespace WebBrowserStuff

{

        ……  //与cookies相关的便利函数(non-member函数)

}

……

上述的组织形式,允许客户只对他们所用的那一小部分系统形成编译相依。(能降低编译依存性)

将所有客户便利函数放在不同的头文件内但隶属同一个命名空间,意味着可以轻松扩展这一组便利函数。

26.  宁可拿non-member、non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩展性。

27.  通常我们不能够(不被允许)改变std命名空间内的任何东西,但可以(被运行)为标准template(如swap)制造特化版本,使它专属于我们自己的classes。举例:

class Widget

{

public:

        ……

        voidswap(Widget& b)

        {

               usingstd::swap;  //使用STL提供的一般性的swap

               swap(this->pImp,b.pImp); //其中pImp为指针类型

}

}

namespace std

{

        template<>  //提供non-member swap的特化版本,目的在于non-member函数中实现对Widget类型的两个对象进行交换

        voidswap<Widget>(Widget& a, Widget& b)  //对swap偏特化,不使用一般性的swap template

        {

               a.swap(b);//调用成员函数Widget::swap(Widget&)

}

}

上述做法,与STL容器有一致性,因为所有STL容器也都提供了public swap 成员函数和std::swap特化版本

28.  宁可使用编译器替换预编译器。像#define 这种语句不是语言中的一部分,它们是预编译指令,给预编译器处理的。条款2:尽量以const、enum和inline替换#define。注意:当定义class专属常量时,为了确保此常量至多只有一份实体,必须让它成为一个static成员。

#define的局限性:(1)不能用于class专属常量,不能提供任何封装性。(2)取一个#define的地址通常是不合法的。取enum的地址也是不合法的。取const的地址是合法的。

29.  如果你不想让别人使用一个pointer或reference指向某个整型常量,enum可以帮你实现这个约束。

30.  两个成员函数如果只是常量性不同,可以被重载。

31.  const成员函数承诺绝不改变对象的逻辑状态。non-const成员函数却没有这般承诺。

32.  static_cast<>()转换是一种安全的转换,比如将对象加上const属性。如果要去掉const属性,必须使用const_cast<>()进行转换。

33.  const成员函数,有两个流行概念:bitwise constness (physical constness) 和logicalconstness。

bitwise constness的概念表示const成员函数不能改变对象的任何成员变量(static变量除外),也就是说,它不改变对象的任何一个bit。这个概念正是C++对常量性(constness)的定义。也就是说,常量变量或对象,都有bitwise constness约束。

logical constness的概念表示const成员函数允许修改对象中的某些bit,但要保证在客户端侦测不出这种变化。

例如:char & operator[] (std::size_t location) const ; //这就是bitwise constness声明,只能保证*this对象中的每一个bit不被修改,但是,在客户端,允许修改对象中的值。也就是说,该成员函数满足了bitwise constness,但没满足logical constness。为了满足logical constness,这项工作是程序员的责任。而bitwise constness的保证是语法保证了。如果写成这样:

const char & operator[] (std::size_t location) const ,则两则的概念都满足了。

mutable修饰符的作用可以将non-static成员变量的bitwise constness约束释放掉。因为const成员函数对所处理的对象施加bitwise constness约束。

 

34.  编译器对const强制实施bitwise constness(也称physical constness),但在编写应用程序时应该使用“概念上的常量性”(conceptual constness)

35.  为内置类型对象进行手工初始化,因为C++不保证初始化它们。

36.  为了免除“跨编译单元的初始化次序”问题,请以local static对象替代 non-local static 对象。也即是说,函数调用返回一个reference指向的local static对象,替换“直接访问non-local static对象”。

37.  为了实现virtual函数,对象需要额外保存一个指针(称为vptr),该指针指向一个由函数指针构成的数组,该数组称为虚函数表(virtual table,vtbl),每一个带有虚函数(virtual函数)的class都有一个相应的vbtl。当对象调用某一虚函数时,实际被调用的函数取决于该对象保存的指针vptr所指的虚函数表vtbl,编译器在虚函数表中寻找适当的函数指针。

38.  C++支持virtual构造函数,其功能与factory(工厂)函数类似。

39.  接口与实现分离的策略:

(1)    Handle classes

特点:将此类中的所有函数交给一个实现类(implementation classes),并由实现类完成实际的工作。

方法:在Handle class 中定义一个指向实现类的指针(称为,pimpl指针),在其构造函数中对该指针初始化,在成员函数中使用指针调用实现类中的相应的函数。注意,实现类和Handle class的接口保存一致。

(2)    interface classes

特点:将此类定义一种特殊的类,abstract base class (抽象基类),通常不带成员变量,也没有构造函数,只有一个virtual函数,以及一组pure virtual 函数,用来描述整个接口。

用法:为了能够在客户端使用interface class的对象,通常调用一个特殊函数(此函数位于interface class中通常被声明为static,返回一个指针),称为工厂函数(factory)或virtual构造函数,此函数扮演“真正被具现化”的那个derived class的构造函数角色。

40.  对内置类型上的操作绝对不会抛出异常,通常高效的swap几乎总是基于对内置类型的操作(例如pimpl手法的底层指针)。

0 0
原创粉丝点击