《Effective C++》精简总结——18~31条款



四、设计与声明

软件设计的步骤通常为:从一般性的构想开始,逐步细化实现细节,以允许特殊接口的开发。接口设计的准则为:让接口容易被正确使用,不容易被误用。


18. 让接口容易被正确使用,不容易被误用

  • 好的接口应当是很容易被正确使用,不容易遭误用;
  • 实现正确使用的方法有:保证接口的一致性(如STL容器的push(),pop(),size()接口等),与内置类型的行为兼容(ints,a*b不可被赋值)等;
  • 实现防止误用的方法有:建立新类型(资源管理对象),限制类型的动作(private),束缚对象值(const),以及消除客户的资源管理责任;
  • tr1::shared_ptr 支持定制删除器(custom deleter),其构造函数接受两个实参:一个是被管理的指针(必须是个指针,可以通过cast进行强制类型转换见:C++强制类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast),第二个参数是引用次数变为0时将调用的删除器。其可防范cross-DLL问题,可用来自动解除互斥锁(mutexes)等待;

19. 设计 class 犹如设计 type

  • 设计 class 的时候,就相当于定义一个新的 type。c++开发中大多数时间是在扩张类型系统的,class 类的重载函数和操作符、控制内存的分配和归还、定义对象的初始化和析构都是设计时候应当考虑到的;
  • 具体需考虑的内容详见p84;

20. 尽量用 pass-by-reference-to-const 替换 pass-by-value

  • 尽量使用常量引用传递代替值传递有以下好处:
    ①省去拷贝构造和析构的调用和时间,更为高效;
    ②可以解决值传递过程中会发生的slicing(对象切割)问题,即将派生类作为实参传递给以基类作为形参的情形,导致实际调用的内容为基类函数,而不是派生类的特化性质全被切割的问题;
  • c++编译器的底层,references往往以指针来实现,所以pass by value通常意味着传递的是指针。所以对于内置类型的对象而言,值传递往往比引用传递的效率更高,对于STL的迭代器和函数对象,习惯上实现选择的是值传递,但是只是对于内置类型的泛型而言;

21. 返回 reference 并不能代替返回 object/value

单纯追求返回引用或指针以省去返回对象时调用构造函数和之后析构函数的想法是错误的,有些情形就应当返回对象。

  • 绝不要返回 pointer 或 reference 指向一个 local stack 对象,因为local stack对象在区块结束,即函数结束之时该对象即被销毁,返回的将是无定义;
  • 绝不要返回 reference 指向一个 heap-allocated 对象,因为在函数中获取的使用堆区内存的对象,容易遗漏或是难以找到进行释放delete的时机,会导致资源的泄漏;
  • 绝不要返回 pointer 或 reference 指向一个可能需要多个该对象的 local static 对象;其一难以保证多线程安全,其二类内静态成员变量独一份,会导致相关运算与所想大相径庭;

22. 将成员变量声明为 private

  • 请将成员变量声明为 private。这可赋予客户访问数据的一致性(都通过函数访问而不是直接访问)、可细微划分访问控制(通过public函数提供只读,只写,可读可写,不可访问等访问控制)、提供封装性,给予类维护者更大的施展空间(通过函数访问成员变量,日后可方便地使用某个计算替换该变量);
  • 若有一个public成员变量,取消了该成员变量,则所有使用它的代码都将不可用;若有一个protected成员变量,取消了该成员变量,则所有使用它的derived class都变得不可用。所以protected并不比public更具有封装性,对于封装而言,只有两种访问权限:private(提供封装)和其它(不提供封装);

23. 尽量使用 non-member 和 non-friend 代替 member 函数

  • 尽量以非成员非友元函数替换成员函数。这样做可以增加封装性、包裹弹性和功能扩充性;
  • 对于成员函数和友元函数,它们可以访问类中的任何成员,而非成员非友元函数无法访问前述的任何东西,若是两者提供同样的功能,则后者明显具有较大的封装性;
  • 为提高封装性而将函数设为非成员的,并不意味其不可以是其它class的成员;
  • c++中,对于为提高封装性而设为非成员的函数的做法是,将非成员函数与所调用/作为参数的class的类型定义放在同一个namespace当中
  • 对于上述做法,其实c++标准程序库也是这么组织的。即标准程序库并不是单一的一个<c++StandardLibrary>头文件并在其中内含std命名空间的所有东西,而是有数十个头文件(<vector>、<algorithm>、<memory>等),在每个功能性头文件中定义std的相关功能,例子见 namespace命名空间和head files头文件;这一思想即是将所有功能性函数按照功能分类放在不同的头文件中但是同属于一个命名空间,可以方便进行功能扩展,同时减少编译依赖;

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

  • 如果需要为某个参数的所有参数(包括被this指针所指的隐喻参数)进行类型转换,那么这个函数必须是个 non-member (例如维护有理数类Rational,operator* 左乘 int 和右乘 int 的一致性)(只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者);
  • 对于隐式类型转换,分为两种:一是构造函数的隐式类型转换(构造函数不指定是 explicate 的);二是 operator return_type (),有 operator 运算参与的隐式类型转换,见:条款15:在资源管理类中提供对原始资源的访问,对于隐式类型转换的介绍见:operator算子&隐式类型转换
  • 对于函数而言,member 函数对应的反面是 non-member 函数,而不是 friend 函数;假设如果一个“与某 class 相关”的函数不该成为一个 member ,就该是个 friend 是错误的,而应当是个非成员函数;

25. 考虑写出一个不抛出异常的 swap 函数

  • 当std::swap对自定义类型的效率不高时(即std提供的缺省的swap函数,是基于拷贝构造函数和拷贝赋值函数的,可能会抛出异常),提供一个swap成员函数,并确定这个函数不抛出异常(例如pimpl(pointer to implementation)手法,基于对内置类型的操作,不会抛出异常);
  • 如果提供了一个 member swap(一般将变量声明为private,缺省swap不可访问),也该提供一个 non-member swap 用来调用前者(将对象作为实参传入,调用高效的成员函数);对于 classes(而非 templates),也应当特化 std::swap;
  • c++只允许对 template class 的偏特化,而不允许对 template function 的偏特化,通常做法是对 template function 进行重载;
  • c++的名称查找法则(name lookup rules)确保找到 global 作用域或 T 所在的命名空间内的任何 T 专属的 swap;
  • 由以上,结合先调用专属swap,没有命中再调用 default swap的原则,所以调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何"命名空间资格修饰";
  • 为“用户定义类型”进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言是全新的东西;
  • 该条款讨论了default swap, member swaps, non-member swaps, std::swap total specification版本和对 swap 的调用:
    如果 default swap 提供了对你的 class 或 class templates 可接受的效率,可以不再做任何事;如果效率不够好(往往意味这使用了某种pimpl手法,即内置类型转换,省去构造和析构步骤)则做法为:
     ①提供一个 public swap 的成员函数,其实现高效置换自定义类型的两对象值;
     ②在该自定义类型所在的命名空间提供一个 non-member swap,并令其调用上述的 public swap 成员函数;
     ③若是该自定义类型不是模板类型,则再为该自定义类型特化 std::swap,并令其调用上述的 public swap 成员函数;
    如果调用该自定义类型为实参的swap函数,确保包含 using std::swap 声明,然后裸调用 swap 函数即可;
  • copy-and-swap 策略的思想是“修改对象数据的副本,然后在一个不抛异常的函数中将修改后的数据和原件置换”,是对对象状态做出“全有”或“全无”改变的很好方法,在条款29:为“异常安全”而努力中就有应用;


五、实现

在对class或class template进行设计和考虑之后,相应的接口实现,程序组织都会简单有序的多。但是,太快定义变量可能造成效率上的拖延;过度使用转型(cast)可能导致代码变慢且难维护,会招致微妙难解的错误;返回对象"内部数据之号码牌(handles)",可能会破坏封装并六个客户虚吊号码牌(dangling handles);未考虑异常带来的冲击则可能导致资源泄漏和数据败坏;过度使用inlining可能引起代码膨胀;过度耦合(coupling)则可能导致冗长的建置时间(build times);


26. 延后并以具明显意义初值定义变量

  • 延后变量的定义时机,可以避免变量定义后未被使用即被收回的情况(如返回一个string类型,但是在为该string赋值或返回之前即遭遇异常退出),改善程序效率;
  • 以具有明显意义的初始定义变量,则可以避免无意义的default构造行为,直接以copy assignment或 copy constructor对变量进行定义并初始化同样可以改善程序效率,并使得程序可读性提高;
  • 对于在循环中使用的变量,其定义在循环中或是循环外取决于其 赋值操作的成本 与 构造函数+析构函数 的成本
    在循环中定义,成本为 n 次 构造函数+析构函数; 在循环外定义,成本为 1次 构造函数+析构函数 和 n次 赋值操作。
    在一定的循环次数n下,选择总成本较低的方案即可;

27. 尽量少做转型动作

  • 两种旧式转型和四种新式转型见:C++强制类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast
  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_cast,如果有个设计需要转型动作,试着发展无需转型的替代设计;
  • 如果转型是必要的,试着将其隐藏于某个函数之后背后,客户可随后调用该函数,而不需要将转型放进他们自己的代码内;
  • 宁可使用c++ - style转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有专职分类;
  • 对于dynamic_cast的替代方法有两种:①使用类型安全容器;②将virtual函数往继承体系上方移动,但绝须避免连串(cascading) dynamic_casts,因为这样的代码通常又大又慢,且一旦类继承体系改变,则代码就需要重新检验;

28. 避免返回 handles 指向对象内部成分

  • references、pointers、iterators 统统是所谓的 handles(号码牌,用于取得某个对象),而返回一个“代表对象内部数据”的 handle,伴随着降低对象封装性的风险;
  • 对于二次封装的 class 中 private 声明的 struct/class 或 function 成员,const 成员函数若是返回一个指向该成员的 handle,则会造成其 private 声明失效,即可以通过该 handle 对内部成员进行涂改。所以避免返回 handle 指向对象内部可以帮助 const 成员函数行为像个 const;
  • 在调用函数时可能会产生临时对象 temp,若是将 temp 当作 reference 或 pointer 返回,则在函数区块结束后,所返回的 handle 所指代的对象是败坏的,也即造成了 虚吊号码牌(dangling handles) 的现象,因为 handle 比起所指对象的寿命更长;

29. 尽量保证所写函数代码是异常安全的

  • 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源(使用资源管理对象解决)或允许任何数据结构遭破坏(在异常安全保证中选择最合适的进行实施)。这样的函数区分为三种可能的保证:基本型(如有异常,程序状态仍是合法状态)、强烈型(如有异常抛出,程序会恢复到函数调用之前的状态)、不抛异常型;
  • 强烈保证通常能够以 copy-and-swap 技术实现,但强烈保证并非对所有函数都可实现或具备现实意义;同时,若是使用了该技术的同时,函数中有调用没有提供任何异常安全保证的函数,则该函数的实现也不能够提供任何异常安全保证(从木桶短板效应来理解);
  • 由上,函数提供的“异常安全 保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱的保证
  • copy-and-swap 策略的思想是“修改对象数据的副本,然后在一个不抛异常的函数中将修改后的数据和原件置换”,是对对象状态做出“全有”或“全无”改变的很好方法;

30. inline函数剖析

  • 条款2. 最好以编译器替代预处理器 中介绍到,用inline函数代替宏,可以调用内联函数在享受函数封装带来的确定性和封装性的情况下,不需带来函数调用的额外开销;
  • inline函数的实际实现中,是以函数代码替换对此函数的每一个调用,在编译之后会增加目标码的大小,若是过度使用inlining技巧会造成程序体积过大,而且代码膨胀之后会导致额外的换页(paging)行为,降低指令高速缓存装置的击中率(instruction cache hit rate),以及效率损失;
  • inline 只是对编译器的一个请求,并不是强制命令
    该请求可以是隐喻提出:①成员函数定义于 class 定义式内,即类内定义成员函数;②类内定义友元函数;
    该请求也可以明确提出:在函数定义式前加上 inline 关键字;
  • 一个看起来是 inline 的函数是否真的是 inline,取决于建置环境,主要取决于编译器
    编译器通常不对通过函数指针而进行的调用实施 inlining,同时构造函数和析构函数最好不要设为 inline(特别是继承体系浅层的 class 的 ctor 和 dtor,会造成较深层次的派生类的 ctor 和 dtor 代码的膨胀);
  • 将大多数 inlining 限制在小型,被频繁调用的函数身上。可使得日后的调试过程和二进制(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化;
  • inline 函数通常放置在头文件内,因为 inlining 在多数c++程序中是编译期行为,即大多数建置环境在编译过程进行 inlining,为将一个函数调用替换为被调用函数代码,编译期需要知道函数代码;
  • templates 也通常放在头文件,因为其一旦被使用,编译期为了具现,需要知道其具体实现;
  • 由上👆两条得出结论,不要只因为 function templates 出现在头文件,就将其声明为 inline。也即 template 的具现化和 inlining 无关

31. 将文件间的编译依存关系降至最低

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

    ① Handle classes:成员函数必须通过 implementation pointer 取得对象数据,也即是 pimpl(pointer to implementation) idiom设计,将一个类分割为两个 classes,一个只提供接口,还一个Imlp类负责实现接口。 这样会为每次访问增加一层间接性。而每个对象消耗的内存数量必须增加 implementation pointer 大小。最后,implementation pointer 必须在Handler class构造函数内初始化,指向一个动态分配而来的 implementation object,所以将承受动态内存分配及其后释放动作所带来的额外开销,以及遭遇 bad_alloc 异常(内存不足)的可能性;该方式在STL GNU 容器实现当中广泛使用;

    分析发现该设计方法实现编译分离的关键在于以 声明依存性 替代 定义依存性,使得编译依存性最小化:
      1.如果使用object references/pointers 可完成任务,就不要使用 objects。只需要一个类型声明式就可定义出指向该类型的 references/pointers,但如果定义某类型的 objects,就需要该类型定义式,会造成一处修改,多次编译(除了修改的实现需编译,客户端文件也需编译)的情况;
      2.如果能够,尽量以 class声明式替换 class定义式。当所声明的函数用到某个class时,并不需要定义某类型的object,即使函数是以by value 方式传递实参或返回值;
      3.由以上两点,不难想到为某个类型的声明式和定义式提供两个不同的头文件。使用#include适当的内含声明式的头文件来替代手工前置声明的方式;

    ② Interface classes:作为实现派生类而设计的接口基类,它通常没有成员变量,也没有构造函数,且除了析构函数是virtual的,其它函数都是 pure virtual 的。 所以每次函数调用都会涉及间接跳跃(indirect jump)成本(若是使用 interface class,因为其是抽象类只能使用指针或引用,一般使用工厂模式对其非抽象派生类进行具化再使用派生类所重写的接口函数,所以造成了间接跳跃),同时每个派生类中会包含一个vptr(virtual table pointer)(见 条款7:为多态基类(polymorphic base classes)声明 virtual 析构函数C++类空间大小),该指针会增加存放对象所需内存——实际取决于该对象除了 Interface class 之外是否还有其它 virtual 函数来源;

    实现 Interface class 两种最常见机制 :
      1.从 Interface class public 继承接口规格,然后实现出接口所覆盖的函数;
      2.多重继承,"public 继承某个 Interface class"再"private 继承某个协助实现的 class"以实现,见条款40:明智审慎地使用多重继承/ p.197;

  • 以上两种手段解除了接口和实现之间的耦合关系,从而降低了编译依存性(compilation dependencies);
  • 程序库头文件应该以“完全且仅有声明式”的形式存在,不论是否涉及 templates 都适用;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Effective C++(编程的50个细节) Effective C++(编程的50个细节)着重讲解了编写C++程序应该注意的50个细节问题,书中的每一条准则描述了一个编写出更好的C++的方式,每一个条款的背后都有具体范例支持,书中讲的都是C++的编程技巧和注意事项,很多都是自己平时不太注意但又很重要的内容,绝对经典,作者Scott Meyers是全世界最知名的C++软件开发专家之一。 电子书PDF格式下载:http://www.yanyulin.info/pages/2013/11/effective.html 1、从C转向C++ 条款1:尽量用CONST和INLINE而不用#DEFINE 条款2:尽量用而不用 条款3:尽量用NEW和DELETE而不用MALLOC和FREE 条款4:尽量使用C++风格的注释 2、内存管理 条款5:对应的NEW和DELETE要采用相同的形式 条款6:析构函数里对指针成员调用DELETE 条款7:预先准备好内存不够的情况 条款8:写OPERATOR NEW与OPERATOR DELETE要遵循常规 条款9:避免隐藏标准形式的NEW 条款10:如果写了OPERATOR NEW就要同时写OPERATOR DELETE 条款11:为需要动态分配内存的类声明一个拷贝构造函数和一个赋值函数 条款12:尽量使用初始化而不要在构造函数里赋值 条款13:初始化列表中成员列出顺序和它们在类中的声明顺序相同 条款14:确定基类有虚析构函数 条款15:让OPERATOR=返回*THIS的引用 条款16:在OPERATOR=中对所有数据成员赋值 条款17:在OPERATOR=中检查给自已赋值的情况 3、类和函数:设计与声明 条款18:争取使类的接口完整并且最小 条款19:分清成员函数,非成员函数和友元函数 条款20:避免PUBLIC接口出现数据成员 条款21:尽可能使用CONST 条款22:尽量用传引用而不用传值 条款23:必须返回一个对象时不要试图返回一个引用 条款24:在函数重载与设定参数默认值间慎重选择 条款25:避免对指针与数字类型的重载 条款26:当心潜在的二义性 条款27:如果不想使用隐式生成的函数要显示的禁止它 条款28:划分全局名字空间 4、类和函数:实现 条款29:避免返回内部数据的句柄 条款30:避免这样的成员函数,其返回值是指向成员的非CONST指针或引用 条款31:千万不要返回局部对象的引用,也不要返回函数内部用NEW初始化的指针 条款32:尽可能推迟变量的定义 条款33:明智的使用INLINE 条款34:将文件间的编译依赖性阡至最低 5、继承与面向对象设计 条款35:使公有继承体现是一个的函义 条款36:区分接口继承与实现继承 条款37:绝不要重新定义继承而来的非虚函数 条款38:绝不要重新定义继承而来的缺省参数值 条款39:避免向下转换继承层次 条款40:通过分层来体现有一个和用...来实现 条款41:区分继承和模板 条款42:明智的使用私有继承 条款43:明智的使用多继承 条款44:说你想说的,理解你说的 6、杂项 条款45:弄清C++在幕后为你所写、所调用的函数 条款46:宁可编译与链接时出错,也不要运行时出错 条款47:确保非局部静态对象在使用前被初始化 条款48:重视编译器警告 条款49:熟悉标准库 条款50:提高对C++的认识
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值