《Effective C++》读书笔记III

资源管理

条款13:以对象管理资源(Use objects to manage resources.),也就是“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization,RAII

1)标准程序库提供的auto_ptr是个“类指针(pointer-like)对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete

2)对对象管理资源的两个关键想法:(1)获得资源后立刻放进管理对象内;(2)管理对象运用析构函数确保资源被释放。

3)auto_ptrs有个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权!也就是受auto_ptrs管理的资源必须绝对没有一个以上的auto_ptr同时指向它。

4)auto_ptr的替代方案是“引用计数型智慧指针”(reference-counting smart pointer,RCSP),所谓RCSP也是智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSPs提供的行为类似垃圾回收(garbage collection),不同的是RCSPs无法打破环状引用。TR1tr1::shared_ptr就是个RCSP

5)auto_ptrtr1::shared_prt两者都在其析构函数内做delete而不是delete[]动作,就意味着在动态分配而得的array身上使用auto_ptrtr1::shared_prt是个馊主意。

 

条款14:在资源管理类中小心copying行为(Think carefully about copying behavior in resource-managing classes.

1)当一个RAII对象被复制时,可以采取下面几种措施:

(1)禁止复制。

(2)对底层资源祭出“引用计数法”。tr1::shared_ptr允许指定所谓的“删除器”,那是一个函数或函数对象,当引用次数为0时便被调用。

(3)复制底层资源。

(4)转移底层资源的拥有权。

2)复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。

 

条款15:在资源管理类中提供对原始资源的访问(Provide access to raw resources in resource-managing classes.

1)tr1::shared_ptrauto_ptr都提供一个get成员函数,用来执行显示转换,返回智能指针内部的原始指针(的复件)。

2)就像(几乎)所有智能指针一样,tr1::shared_ptrauto_ptr也重载了指针取值操作符(operator->operator*),它们允许隐式转换至底部原始指针。

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

 

条款16:成对使用newdelete时要采取相同形式(Use the same form in corresponding uses of new and delete.

1)当通过new动态生成一个对象,有两件事发生。第一,内存被分配出去(通过名为operator new的函数)。第二,针对此内存会有一个(或更多)构造函数被调用。当使用delete,也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过名为operator delete的函数)。

 

条款17:以独立语句将newed对象置于智能指针(Store newed objects in smart pointers in standalone statements.

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


设计与声明

条款18:让接口容易被正确使用,不易被误用(Make interfaces easy to use correctly and hard to use incorrectly.

1)首先必须考虑客户可能做出什么样的错误。

2)明智而审慎地导入新类型对预防“接口被误用”有神奇疗效。

3)限制类型内什么事可做,什么事不能做。

4)tr1::shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变成0时将被调用的“删除器”。

 

条款19:设计class犹如设计typeTreat class design as type design.

class的设计规范关乎的问题:

1)新type的对象应该如何被创建和销毁?这会影响class的构造函数和析构函数以及内存分配函数和释放函数(operator newoperator new[]operator deleteoperator delete[])的设计。

2)对象的初始化和对象的赋值该有什么样的差别?这决定了构造函数和赋值(assignment)操作符的行为,以及期间的差异。别混淆“初始化”和“赋值”,对应于不同的函数调用。

3)新type的对象如果被passed by value(以值传递),意味着什么?copy构造函数用来定义一个typepass-by-value该如何实现。

4)什么是新type的“合法值”?对class的对象变量而言,通常只有某些数值是有效的。那些数值集决定了class必须维护的约束条件(invariants),也就决定了成员函数(特别是构造函数、赋值操作符和所谓“setter”函数)必须进行的错误检查工作。它也影响函数抛出的异常、以及(极少被使用的)函数异常明细列(exception specifications)。

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

6)新type需要什么样的转换?如果希望允许类型T1之物被隐式转换为类型T2之物,就必须在class T1内写一个类型转换函数(operator T2)或在class T2内写一个non-explicit-one-argument(可被单一实参调用)的构造函数。如果只允许explicit构造函数存在,就得写出专门复制执行转换的函数,且不得为类型转换操作符(type conversion operators)或non-explicit-one-argument构造函数。

7)什么样的操作符和函数对此新type而言是合理的?这决定class声明哪些函数。其中某些该是member函数,某些则否。

8)什么样的标准函数应该驳回?那些正是必须声明为private者。

9)谁该取用新type的成员?这决定哪个成员为public,哪个为protected,哪个为private。也决定哪一个classes/functions应该是friends,以及将它们嵌套于另一个之内是否合理。

10)什么是新type的“未声明接口”(undeclared interface)?它对效率、异常安全性(见条款29)以及资源运用(例如多任务锁定和动态内存)提供何种保证?在这些方面提供的保证将为class实现代码加上相应的约束条件。

11)新type有多么一般化?也许并非定义一个新type,而是定义一整个types家族,应该定义一个新的class template

12)真的需要一个新type吗?

 

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

1)这种传递方式效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。

2)以by reference方式传递参数也可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base classcopy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。

解决切割(slicing)问题的办法,就是以by reference-to-const的方式传递derived class

3)一般而言,可以合理假设“pass-by-value并不昂贵”的唯一对象就是内置类型和STL的迭代器和函数对象。

 

条款21:必须返回对象时,别妄想返回其referenceDon’t try to return a reference when you must return an object.

1)所谓reference只是个名称,代表某个既有对象。任何时候看到一个reference声明式,都应该对它的另一个名称提出疑问。

2)任何函数如果返回一个reference指向某个local对象,都将一败涂地。如果函数返回指针指向一个local对象,也是一样。

3)一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象呗。

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

 

条款22:将成员变量声明为privateDeclare data members private.

1)成员变量应该是private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充实的实现弹性。

2)使用函数可以让你对成员变量的处理有更精确的控制。如果令成员变量为public,每个人都可以读写它,但如果你以函数取得或设定其值,你就可以实现出“不准访问”、“只读访问”以及“读写访问”,甚至可以实现“唯写访问”。

3)封装。如果通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而class客户一点也不会知道class的内部实现已经起了变化。

4)protected并不比public更具封装性。

 

条款23:宁以non-membernon-friend替换member函数(Prefer non-member non-friend functions to member functions.

1)如果要在一个member函数(它不只可以访问class内的private数据,也可以取用private函数、enumstypedefs等等)和一个non-membernon-friend函数(它无法访问上述任何东西)之间做抉择,而且两者提供相同机能,那么,导致较大封装性的是non-member non-friend函数,因为它并不增加“能够访问class内之private成分”的函数数量。

2)将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。他们需要做的就是添加更多non-member non-friend函数到此命名空间内。

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

 

条款24:若所有参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters.

1)只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者。地位相当于“被调用之成员函数所隶属的那个对象”——即this对象——的那个隐喻参数,绝不是隐式转换的合格参与者。

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

 

条款25:考虑写出一个不抛异常的swap函数(Consider support for a non-throwing swap.

1)典型swap算法如下图所示:

namespace std{

template<typename T>

void swap(T& a, T& b)

{

    T temp(a);

    a = b;

    b = temp;

}

}

只要类型T支持coping(通过copy构造函数和copy assignment操作符完成),缺省的swap实现代码就会帮你置换类型为T的对象,你不需要为此另外再做任何工作。

2)“pimpl手法”(pimpl是“pointer to implementation”的缩写)是指“以指针指向一个对象,内含真正数据”这种类型的表现形式。如Widget class:

class WidgetImpl{ //针对Widget数据而设计的class,细节不重要。

public:

  ...

private:

  int a, b, c;

  std:vector<double> v; //可能有很多数据,意味复制时间很长。

  ...

};

class Widget{ //这个class使用pimpl手法

public:

   Widget(const Widget& rhs);

   Widget& operator=(const Widget& rhs) //复制Widget时,令它复制其WidgetImpl对象

   {

       ...

       *pImpl = *(rhs.pImpl);

       ...

}

...

private:

WidgetImpl* pImpl; //指针,所指对象内含Widget数据。

};

我们希望能够告诉std::swap,当Widgets被置换时真正该做的是置换其内部的pImpl指针。一个思路是:将std::swap针对Widget特化。令Widget声明一个名为swappublic成员函数做真正的置换工作,然后将std::swap特化,令它调用该成员函数。

class Widget{

public:

   ...

   void swap(Widget& other)

   {

       using std::swap;

       swap(pImpl, other.pImpl);

   }

   ...

};

namespace std{

   template<>

   void swap<Widget>(Widget& a, Widget& b)

   {

       a.swap(b);

   }

}

3)假设WidgetWidgetImpl都是class templates而非classesC++只允许对class templates偏特化,而function templates身上偏特化是行不通。当打算偏特化一个function template时,惯常做法就是简单为它添加一个重载版本。

namespace std{

template<typename T>

void swap(Widget<T>& a, Widget<T>& b)

{

    a.swap(b);

}

}

std是个特殊的命名空间,其管理规则也比较特殊,客户可以全特化std内的templates,但不可以添加新的templates(或classesfunctions或其他任何东西)到std里头。很简单,将swap置于Widget所在的命名空间中,如下所示:

namespace WidgetStuff{

   ...

   template<typename T>

   class Widget{ ... };

   ...

   template<typename T>

   void swap(Widget<T>& a, Widget<T>& b)

   {

      a.swap(b);

   }

}

4)C++的名称查找法则,如下所示:

template<typename T>

void doSomething(T& obj1, T& obj2)

{

using std::swap; //std::swap在此函数内可用

...

swap(obj1, obj2); //T型对象调用最佳swap版本

...

}

C++的名称查找规则(name lookup rules)确保找到global作用域或T所在命名空间内的任何T专属swap。如果没有T专属之swap存在,编译器就使用std内的swap,这得感谢using声明式让std::swap在函数内曝光。

5)首先,如果swap的缺省实现版对你的classclass template提供可接受的效率,你不需要额外做任何事。

其次,如果swap缺省实现版的效率不足(那几乎总是意味你的classtemplate使用了某种pimpl手法),试着做以下事情:

(1)提供一个public swap成员函数,让它高效的置换你的类型的两个对象值。

(2)在你的classtemplate所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。

(3)如果你正编写一个class(而非class template),为你的class特化std::swap。并令它调用你的swap成员函数。

最后,如果你调用swap,请确定包含一个using声明式,以便std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap

6)成员版swap绝不可抛出异常。因为swap的一个最好应用是帮助classes(和class templates)提供强烈的异常安全性(exception-safety)保障。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值