《Effective C++》学习笔记

2  Constructors, Destructors, and Assignment Operators


条款 10:Have assignment operators return a reference to *this.

        为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。

Remember:

  • 令赋值(assignment)操作符返回一个reference to *this。



条款 11:Handle assignment to self in operator =.

        设计良好的面向对象系统(OO-systems)会将对象的内部封装起来,只留两个函数负责对象拷贝(复制),即copy构造函数和copy assignment操作符,不妨称它们为copying函数。

Remember:

  • 确保当对象自我赋值时operator=有良好行为,其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap;
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。



条款 12:Copy all parts of an object.

        设计良好的面向对象系统(OO-systems)会将对象的内部封装起来,只留两个函数负责对象拷贝(复制),即copy构造函数和copy assignment操作符,不妨称它们为copying函数。

Remember:

  • Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”;
  • 不要尝试以某个copying函数实现另一个copying函数,应该将共同机能放进第三个函数中,并由两个copying函数共同调用。




4  Designs and Declarations


条款 18:Make interfaces easy to use correctly and hard to use incorrectly.
        所谓的“cross-DLL problem”,指“对象在动态链接库(DLL)中被new创建,却在另一个DLL内被delete销魂”。在许多平台上,这一类“跨DLL的new/delete成对运用”会导致运行期错误。
Remember:
  • 好的接口很容易被正确使用,不容易被误用,在接口中应努力达成这些性质;
  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容;
  • “阻止误用”的办法包括建立新类型、限制类型上的操作,苏复对象值,以及消除客户的资源管理责任。



条款 19:Treat class design as type design.
        有关class的设计规范:
  1. 新type的对象应该如何被创建和销毁?
  2. 对象的初始化和对象的赋值该有什么样的差别?
  3. 新type的对象如果被pass by value,意味着什么?(记住,copy构造函数用来定义一个type的pass-by-value该如何实现)
  4. 什么是新type的“合法值”?
  5. 你的新type需要配合某个继承图系(inheritance graph)吗?
  6. 你的新type需要什么样的转换?如果希望允许类型T1被隐式转换为类型T2,就必须在class T1内写一个类型转换函数(operator T2)或在T2内写一个non-explicit-non-argument构造函数,如果只允许explicit构造函数存在,则需要写出专门负责执行转换的函数,且不得为类型转换操作符(type conversion operators)或non-explicit-one-argument构造函数。
  7. 什么样的操作符和函数对此新type而言是合理的?
  8. 什么样的标准函数应该驳回?(必须声明为private)
  9. 谁该取用新type的成员?(哪个成员为public,哪个为protected,哪个为private)
  10. 什么是新type的“未声明接口”(undeclared interface)?
  11. 你的新type有多么一般化?(定义一个class还是一个class template)
  12. 你真的需要一个新type吗?(说不定一个或多个non-member函数或templates即可)
Remember:
  • Class的设计就是type的设计,在定义一个新type之前,请确定已经考虑过本条款覆盖的所有讨论主题。




条款 20:Prefer pass-by-reference-to-const to pass-by-value.
        以 by reference 方式传递参数可以避免 slicing(对象切割)问题。当一个 derived class 对象以 by value 方式传递并被视为一个 base class 对象,base class 的 copy 构造函数会被调用,而“造成此对象的行为像个 derived class 对象”的那些特化性质被切割掉了,仅仅留下一个 base class 对象。
        如果窥视C++编译器的底层,可发现 references 往往以指针实现出来,因此 pass by reference 通常意味着真正传递的是指针。因此如果有个对象属于内置类型(如 int ),pass by value 往往比 pass by reference 的效率高些,这个忠告也适用于STL的迭代器和函数对象,因为习惯上它们都被设计为 pass by value,迭代器和对象的实践者有责任看看它们是否高效且不受切割问题的影响。
Remember:
  • 尽量以 pass-by-reference-to-const 替换 pass-by-value,前者通常比较高效,并可避免切割问题;
  • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象,对它们而言,pass-by-value 往往比较适当。
  • (Hunger:pass-by-value+浅copy 会带来意外的bug)



条款 21:Don't try to return a reference when you must return an object.
        在返回一个reference和返回一个object之间抉择时,应该选择行为正确的做法,让编译厂商为“尽可能降低成本”鞠躬尽瘁吧。
Remember:
  • 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。



条款 22:Declare data members private.
        一旦你将一个成员变量声明为public或protected而客户开始使用它,就很难改变那个成员变量所涉及的一切。太多代码需要重写、重新测试、重新编写文档、重新编译。从封装的角度观之,其实只有两种访问权限:private(提供封装)和其它(不提供封装)。
Remember:
  • 切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性;
  • protected并不比public更具封装性。



条款 23:Prefer non-member non-friend functions to member functions.
        如果某些东西被封装,它就不再可见,封装得越深,就越少人看见它,就有越大的弹性去改变它,因为改变仅仅直接影响看到改变的那些人。即封装能够改变事物而只影响有限客户。如果在一个member函数和一个non-member non-friend函数之间做抉择,而且两者提供相同机能,则导致较大封装性的是后者,因为它并不增加“能够访问class内private成分”的函数数量。
        namespace和classes不同,前者可以跨越多个源码文件而后者不能。C++标准程序库并不是拥有单一、整体、庞大的<C++StandardLibrary>头文件并在其中内含std命名空间内的每一样东西,而是有数十头文件(<vector><algorithm><memory>等等),每个头文件声明std的某些机能。

Remember:
  • 以non-member non-firend函数替换member函数,可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。



条款 24:Declare non-member functions when type conversions should apply to all parameter.
        有四个符号(+,-,*和&)既可作一元操作符又可作二元操作符。大多数重载操作符可以定义为普通非成员或类的成员函数。作为类成员的重载函数,其形参看起来比操作数数目少1。作为成员函数的操作符有一个隐含的this形参,限定为第一个操作数。
        member函数的反面是non-member函数,不是friend函数。无论何时如果你可以避免friend函数就该避免,因为就像真实世界一样,朋友带来的麻烦往往多过其价值。

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





条款 25:Consider support for a non-throwing swap.
        有四个符号(+,-,*和&)既可作一元操作符又可作二元操作符。大多数重载操作符可以定义为普通非成员或类的成员函数。作为类成员的重载函数,其形参看起来比操作数数目少1。作为成员函数的操作符有一个隐含的this形参,限定为第一个操作数。
        member函数的反面是non-member函数,不是friend函数。无论何时如果你可以避免friend函数就该避免,因为就像真实世界一样,朋友带来的麻烦往往多过其价值。

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







5  Implementations


条款29:Strive for exception-safe code.

       异常安全函数(Exception-safe functions)提供以下三个保证之一:

  • 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下,没有任何对象或数据结构会因此而败坏,所有对象处于一种内部前后一致的状态,虽然程序的实现状态(exact state)恐怕不可预料;
  • 强烈保证:如果异常被抛出,程序状态不改变,即如果函数成功,就是完全成功,否则,程序回到“调用函数之前”的状态,程序只有两种可能状态:如预期般地到达函数成功执行后的状态,或回到函数被调用前的状态,而只提供基本承诺的函数,若真的出现异常,程序有可能处于任何状态——只要那个状态合法;
  • 不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总能完成原先承诺的功能。
        对大部分函数而言,抉择往往落在基本保证和强烈保证之间。“copy-and-swap”策略是对对象状态做出“全有或全无”改变的一个很好办法,但一般而言它并不保证整个函数有强烈的异常安全性。问题出在“连带影响”(side effects),如果函数只操作局部性状态(local state),便相对容易地提供强烈保证;但当函数对“非局部性数据”(non-local data)有连带影响时,提供强烈保证就困难得多。另一个是效率,copy-and-swap必须为每一个即将被改动的对象做出一个副本,那得耗用你可能无法(或无意愿)供应的时间和空间。

Remember:

  • 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏,这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常性;
  • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备实现意义;
  • 函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全保证”中的最弱者。


条款31:Minimize compilation dependencies between files.

       “接口与实现分离”的关键在于以”声明的依存性“替换”定义的依存性“,那正是编译依存性最小化的本质:实现中让头文件尽可能自我满足,万一做不到,则让它与其它文件内的声明式(而非定义式)相依。

  • 如果使用object references或object points可以完成任务,就不要使用objects;
  • 如果能够,尽量以class声明式替换class定义式,如果能够将”提供class定义式“(通过#include完成)的义务从”函数声明所在“的头文件转移到”内含函数调用“的客户文件,便可将”并非真正必要的类型定义“与客户端之间的编译依存性去掉;
  • 为声明式和定义式提供不同的头文件。

Remember:

  • 支持”编译依存性最小化“的一般构想是:相依于声明式,不要相依于定义式,基于此构想的两个手段是Handle classes和Interface class;
  • 程序库头文件应该以”完全且仅有声明式(full and declaration-only forms)“的形式存在,这种做法不论是否涉及templates都适用。



6  Inheritance and Object-Oriented Design


条款32:Make sure public inheritance model "is-a".

Remember:

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


条款33:Avoid hiding inherited names.

Remember:

  • derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此;
  • 为了让被遮掩的名称再见天日,可使用using声明或转交函数(forwarding functions)。


条款34:Differentiate between inheritance of interface and inheritance of implementation.

Remember:

  • 接口继承和实现继承不同,在public继承之下,derived classes总是继承base class的接口;
  • pure virtual 函数只具体指定接口继承
  • impure virtual 函数具体指定接口继承缺省实现继承
  • non-virtual 函数具体指定接口继承以及强制性实现继承


条款35:Consider alternatives to virtual functions.

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

        这一基本设计,即“令客户通过 public non-virtual 成员函数间接调用 private virtual 函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式的一个独特表现形式,这个non-virtual函数可称为virtual函数的外覆器(wrapper)。

        NVI手法的一个优点隐身在上述代码注释“做一些事前工作”和“做一些事后工作”之中。wrapper确保一个virtual函数在被调用之前设定好适当场景,并在调用结束之后清理场景,如果你让客户直接调用virtual函数,就没有任何好办法可以做这些事。

        NVI手法涉及在derived classes内重新定义private virtual函数,而derived classes并不调用这些函数。这赋予了derived classes“如何实现机能”的控制能力,但base class保留“函数何时被调用”的权利。


藉由Function Pointers实现Strategy模式

        运用函数指针替换virtual函数,其优点是“每个对象可各自拥有自己的计算函数”和“可在运行期改变计算函数”,缺点是必须降低类的封装性,这是唯一能够解决“需要以non-member函数访问class的non-public成分”的办法。例如class可声明该non-member函数为friends,或是为其实现的某一部分提供public访问函数(其它部分则宁可隐藏起来)。


古典的Strategy模式

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




条款36:Never redefine an inherited non-virtual function.

        non-virtual 函数是静态绑定(statically bound)(指针),而 virtual 函数是动态绑定(dynamically bound),任何情况下都不该重新定义一个继承而来的non-virtual函数(overwrite)。




条款37:Never redefine a function's inherited default parameter value.

        virtual 函数是动态绑定(dynamically bound),而缺省参数值确是静态绑定(statically bound)。当你想令 virtual 函数表现出你想要的行为却遭遇麻烦,聪明的做法是考虑替代设计。




条款38:Model "has-a" or "is-implemented-in-terms-of" through composition.

        复合(composition)指某种类型的对象内含其它类型的对象。

Remember:

  • 复合(composition)的意义和public继承完全不同,public继承带有is-a(是一种)的意义;
  • 在应用域(application domain),复合意味has-a(有一个);在实现域(implementation domain),复合意味is-implemented-in-term-of(根据某物实现出)。



条款39:Use private inheritance judiciously.

        C++将public继承视为is-a关系,编译器在必要时刻(为了让函数调用成功)会将派生类暗自转换为基类。如果class之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。

        Empty class可以不是真正的empty,它们没有non-static成员变量,却可以内含typedefs,enums,static成员变量,或non-virtual函数。

Remember:

  • Private继承意味is-implemented-in-term-of(根据某物实现出),它通常比复合(composition)的级别低,但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这设计是合理的;
  • 和复合(composition)不同,private继承可以造成empty base最优化(empty base optimization),EBO一般只在单一继承(而非多重继承)下才行,统治C++对象布局的那些规则通常表示EBO无法被施行于“拥有多个base”的derived class身上,这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。



条款40:Use multiple inheritance judiciously.

       非必要不使用virtual base,平常请使用non-virtual继承;如果必须使用virtual base class,尽可能避免在其中放置数据。

Remember:

  • 多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual继承的需要;
  • virtual继承会增加大小、速度、初始化(赋值)复杂度等等成本,如果virtual base class不带任何数据,将是最具实用价值的情况;
  • 多重继承的确有正当用途,其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合。






8  Customizing new and delete


条款49:Understand the behavior of the new-handler.

        当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,即new-handler(以前它会返回一个null指针,某些旧式编译器目前也还那么做)。为了指定这个“用以处理内存不足”的函数,客户必须调用set_new_handler,一个声明于<new>的标准程序库函数:

namespace std
{
        typedef void (*new_handler)();
        new_handler set_new_handler(new_handler p) throw();
}

  • new_handler是个typedef,定义一个指针指向函数,该函数没有参数也不返回任何东西;
  • set_new_handler是“获得一个new_handler并返回一个new_handler”的函数;
  • throw()是一份异常明细,表示该函数不抛出任何异常。

然后,operator new做以下事情:

  1. 调用标准set_new_handler,告知类的错误处理函数,这会将类的new_handler安装为global new_handler;
  2. 调用global operator new,执行实际的内存分配。如果分配失败,global operator new会调用类的new_handler,因为类的new_handler被安装为global new handler。
  3. 如果global operator new能够分配足够一个类对象所用的内存,类的operator new会返回一个指针,指向分配所得。类的析构函数会管理global new_handler,即自动将类的operator new被调用前那个global new_handler恢复回来。

        传统的C++要求operator new必须在无法分配足够内存时返回null,这个形式被称为nothrow形式。Nothrow new是一个颇为局限的工具,因为它只适用于内存分配,后继的构造函数调用还是可能抛出异常。


条款52:Write placement delete if you write placement new.

        当你写一个new表达式,共有两个函数被调用,一个是用以分配内存的operator new,一个是类的default构造函数。假设其中第一个函数调用成功,第二个函数却抛出异常,则步骤以的内存分配所得必须取消并恢复旧观,否则会造成内存泄漏(memory leak)。此时,客户没有能力归还内存,因为如果类构造函数抛出异常,类指针尚未被赋值,客户手上就没有指针指向该被归还的内存,而取消步骤一并恢复旧观的责任因此落到C++运行期系统身上。运行期系统会高高兴兴地调用步骤一所调用的operator new的相应operator delete版本,前提当然是它必须知道哪一个(因为可能有多个)operator delete该被调用。

        如果operator new接受的参数除了一定会有的那个size_t之外还有其它,这个就是placement new。众多placement new版本中特别有用的一个是“接受一个指针指向对象该被构造之处”,如下:

void* operator new(std::size_t, void *pMemory) throw();
这个版本的new已被纳入C++标准程序库,只要#include<new>就可以取用它。这个new的用途之一是负责在vector的未使用空间上创建对象,而且它是最早的placement new版本(实际上它正是这个函数命名的根据:一个特定位置上的new)。类似于new的placement版本,operator delete如果接受额外参数,便称为 placement delete。如果一个placement new没有对应版的placement delete,则new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用。而当声明placement new和placement delete时,注意不要遮掩了它们的正常版本。
条款 24:Declare non-member functions when type conversions should apply to all parameter.
        有四个符号(+,-,*和&)既可作一元操作符又可作二元操作符。大多数重载操作符可以定义为普通非成员或类的成员函数。作为类成员的重载函数,其形参看起来比操作数数目少1。作为成员函数的操作符有一个隐含的this形参,限定为第一个操作数。
        member函数的反面是non-member函数,不是friend函数。无论何时如果你可以避免friend函数就该避免,因为就像真实世界一样,朋友带来的麻烦往往多过其价值。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值