第五章 实现

条款26:尽可能延后变量定义式的出现时间

只要定义了一个变量而其类型带有一个构造函数或析构函数,那么

  • 当程序的控制流到达这个变量定义式时,你得承受这个构造成本。
  • 当这个变量离开这个作用域时,你得承受这个析构成本。

即使这个变量最终并未被使用,仍然需要耗费这些成本,所以应该尽可能避免这种情形,即延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。

如果变量只在循环内使用,那么把它定义于循环外并在每次循环迭代是赋值给它比较好,还是该把它定义在循环内?

// 方法A:定义于循环外
Widget w;
for(inti i = 0; i < n; ++i) {
w = 取决于i的某个值;
// ...
} // 方法B:定义于循环内
for(int i = 0; i < n; ++i) {
Widget w(取决于i的某个值);
// ...
}

以上的两种做法的成本如下:

  • 做法A:1个构造函数 + 1个析构函数 + n个赋值操作
  • 做法B:n个构造函数 + n个析构函数

从效率上看,如果类的一个赋值成本低于一组构造和析构成本,做法A大体而言比较高效,尤其是当n的数值很大时。否则做法B更好。

从可理解性和维护性上看,做法A造成w的作用域比做法B更大,可维护性和可理解性相对较差。

请记住

  • 尽可能延后变量定义式的出现,这样做可增加程序的清晰度并该改善程序效率。

条款27:尽量少做转型动作

显然,转型(casts)会破坏类型系统(type system),但是有时也不得不这样做。

转型分类包括:

  • 旧式转型(old-style casts):

    • C风格:(T) expression
    • 函数风格:T(expression)
  • 新式转型(C++-style casts):
    • const_cast<T>(expression);

      • 通常被用来将对象的常量性转除(cast away the constness)。
    • dynamic_cast<T>(expression);
      • 主要用来执行"安全向下转型",也就是用来决定某对象是否归属继承体系中的某个类型、即主要用于将基类指针转换成派生类指针(或引用),通常需要知道转换源和转换目标的类型。可能会耗费重大的运行成本。
    • reinterpret_cast<T>(expression);
      • 通常为算术对象的位模式提供较低层次上的重新解释。例如将int*转换为char*,比较危险的转换!
    • static_cast<T>(expression);
      • 用来强制隐式转换,只要不包含底层const,都可以使用。适合将较大算术类型转换为较小算术类型。例如将non-const对象转换为const对象int转为double等等。唯一例外是无法将const转为non-const,这个只要const_cast才能做到。

应该尽可能使用新式转型,主要有两点原因:

  • 它们很容易在代码中被辨别出来(无论是人工还是使用工具如grep等),因而得以简化"找出类型系统在哪个地方呗破坏"的过程。
  • 各转型动作的目标越窄化,编译器越可能诊断出错误的运用。

尽管如此,还是应该尽量少做转型,原因如下:

  • 转型不只是告诉编译器把某种类型视为另一种类型这么简单,任何一个转型动作往往令编译器编译出运行期间执行的代码。

    • int转型为double几乎肯定会产生一些代码,因为在大部分体系结构中,int的底层表述不同于double的底层表述。
    int x, y;
    // ...
    double d = static_cast<double>(x)/y;
    • 会有个偏移量在运行期被实施于Derived*指针上,用以取得正确的Base*地址。
    class Base {};
    class Derived : public Base {};
    Derived d;
    Base* pb = &d;
  • 很容易写出似是而非的代码。

class Window {
public:
virtual void onResize();
// ...
}; // 错误的用法,因为转型并非在当前对象身上调用Window::onResize(),而是当前对象的base class成分的副本上调用Window::onResize()。
class SpecialWindow: public Window {
public:
virtual void onResize() {
static_cast<Window*>(this)->onResize();
// 进行SpecialWindow的专属行为
}
}; // 正确的用法
class SpecialWindow: public Window {
public:
virtual void onResize() {
Window::onResize();
// 进行SpecialWindow的专属行为
}
};
  • 继承中的类型转换效率低:

    • C++通过dynamic_cast实现继承中的类型转换,通常是因为想在一个认定为derived class对象身上执行derived class操作没,但是拥有的是一个指向base的指针或引用。这样做其效率都是相当低的!这种情况下有两种办法可以避免转型:

      • 使用容器并在其中存储直接指向derived class对象的指针
      • derived class中的操作上升到base class内,成为virtual函数,base class提供一份缺省实现

请记住

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
  • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进它们自己的代码内。
  • 宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职责。

条款28:避免返回handles指向对象内部成分

references、指针和迭代器统统都是所谓的handles,即号码牌,用来取得某个对象。

  • 增加封装性:如果成员函数返回handles,那么相当于成员变量的封装性从private上升到public,这显然与条款22:将成员变量声明为private冲突。
  • 使得"通过const修改对象的数据"成为可能:对象只包含指针成员,实际数据通过这个指针指向,const成员函数返回一个这个指针所指对象的引用,并不会造成指针被修改,也就符合bitwise constness,但是通过这个引用却可以改变对象实际的数据。
  • 防止"虚吊"发生:若返回的handles指向一个临时对象,那么返回后临时对象被销毁,handles就会成为"虚吊的",只要handles被传出去,就会面临"handles比其所指对象更长寿"的风险。

请记住

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

条款29:为”异常安全“而努力是值得的

考虑一个例子,有一个菜单类,changeBg函数可以改变它的背景,切换背景计数,同时提供线程安全。

class Menu {
public:
// ...
void changeBg(std::istream& src); private:
Mutex mutex; // 互斥器,提供多种线程互斥访问
Image* bg; // 背景图片
int changeCount; // 切换背景次数
}; void Menu::changeBg(std::istream& src) {
lock(&mutex);
delete bg;
++changeCount;
bg = new Image(src);
unlock(&mutex);
}
  • 异常安全的两个条件:

    • 不泄漏任何资源:即发生异常时,异常发生之前获得的资源都应该释放,不会因为异常而泄漏。在上面的例子中,如果new Image(src)发生异常,那么unlock就永远不会被调用,因此锁资源会泄漏。
    • 不允许数据破坏:如果new Image(src)发生异常,背景图片会被删除,计数也会改变,但是新背景并未设置成功。

对于资源泄漏,条款13:以对象管理资源可以解决;使用智能指针指定删除器也可以解决;

void Menu::changeBg(std::istream& src) {
Lock m1(&mutex);
delete bg;
++changeCount;
bg = new Image(src);
}

对于数据破坏,需要注意异常安全函数的三个保证:

  • 基本承诺:抛出异常后,对象仍然处于合法(valid)的状态,但是不确定处于哪个状态。
  • 强烈保证:如果抛出了异常,状态并不会发生任何改变。就像没调用这个函数一样。
  • 不抛掷保证:这是最强的保证,承诺绝不抛出异常,函数总是能完成它所承诺的事情。

对于前面的例子,可以使用智能指针,以及重排changeBg的语句顺序来满足"强烈保证"。

class Menu {
public:
// ...
void changeBg(std::istream& src); private:
Mutex mutex; // 互斥器,提供多种线程互斥访问
std::shared_ptr<Image> bg; // 背景图片
int changeCount; // 切换背景次数
}; void Menu::changeBg(std::istream& src) {
Lock m1(&mutex);
bd.reset(new Image(src)); // 以new的执行结果设定bg内部指针
++changeCount;
}

可以使用 copy-and-swap策略,它通常能够为对象提供异常安全的"强烈保证"。当我们要改变一个对象时,先把它复制一份,然后去修改它的副本,改好了再与原对象交换。

struct MentImpl {
std::shared_ptr<Image> bg; // 背景图片
int changeCount; // 切换背景次数
}; class Menu {
public:
// ...
void changeBg(std::istream& src); private:
Mutex mutex; // 互斥器,提供多种线程互斥访问
std::shared_ptr<MentImpl> pImpl;
}; void Menu::changeBg(std::istream& src) {
using std::swap;
Lock m1(&mutex); // 获得mutex的副本数据
std::shared_ptr<MenuImpl> pNew(new MenuImpl(*pImpl));
pNew->bg.reset(new Image(src)); // 修改副本
++pNew->changeCount;
swap(pImpl, pNew); // 置换数据,释放mutex
}

请记住

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

条款30:透彻了解inlining的里里外外

使用inline的优劣:

  • 优势:较少函数调用的开销;编译器对inline的优化;
  • 劣势:目标代码的增加,程序体积增大,导致额外的换页行为,降低指令高速缓存装置的命中率。

提出inline的两种方式:

  • 显式提出
  • 隐式提出(类内实现成员函数)

大部分编译器拒绝将太过复杂的(例如带有循环或递归)函数inlining,而所有对virtual函数的调用也会拒绝inlining,因为virtual意味着"等待,直到运行期才能确定调用哪个函数",而inline意味准备"执行前,先将调用动作替换为被调用函数的本体"。

总的来说,一个表面上看似inline的函数是否真是inline,取决于你的环境,主要取决于编译器。幸运的是大多数编译器提供了一个诊断级别:如果它们无法将你要求的函数inline化,会给你一个警告信息。inline函数无法随着程序库的升级而升级。

构造函数和析构函数往往是inlining的糟糕候选人。对于以下代码:

class Base {
public:
// ...
private:
std::string bm1, bm2;
}; class Derived: public Base {
public:
Derived() {}
// ...
private:
std::string bm1, bm2, bm3;
};

虽然看上去Derived构造函数为空,符合一个函数成为inline的特性。但是为了确保C++对于"对象被创建和被销毁时发生什么事"做出的各式各样的保证,编译器会在其中安插代码。因此实际的Derived构造函数可能是这样的:

Derived::Derived() {

    Base::Base();
try {dm1.std::string::string();}
catch() {
Base::~Base();
throw;
} try {dm2.std::string::string();}
catch() {
dm1.std::string::~string();
Base::~Base();
throw;
} try {dm3.std::string::string();}
catch() {
dm2.std::string::~string();
dm1.std::string::~string();
Base::~Base();
throw;
}
}

对于上述代码。显而易见,真正的编译器会以更精细复杂的做法来处理异常。相同理由也使用与析构函数。

80-20经验法则平均而言一个程序往往将80%的执行时间花费在20%的代码上。这个法则提醒我们,作为一个软件开发者,你的目标是找出这可以有效增进程序整体效率的20%代码,然后将它inline或竭尽所能地将它瘦身。但除非你选对目标,否则一切都是徒劳。

请记住

  • 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化。
  • 不要只因为function templates出现在头文件,就将它们声明为inline

条款31:将文件间的编译关系降至最低

C++并没有把"将接口从实现中分离"这件事做的很好,例如:

#include <string>
#include "date.h"
#include "address.h" class Person {
public:
// ...
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};

如果没有引入头文件,那么编译将无法通过。但是这样会在Person定义文件和其含入文件之间形成了一种编译依赖关系如果这些头文件中任何一个被改变,或这些文件多依赖的其他头文件有任何改变,那么每个含有Person class的文件都得重新编译,任何使用Person class的文件也必须重新编译,这样的连串编译依存关系会对许多项目造成难以形容的灾难

可能会想着将实现细目分开:

namespace std {
class string; // 前置声明,但不正确
}
class Data; // 前置声明
class Address; // 前置声明 class Person {
public:
// ...
};

如果这样做,Person的客户就只需要在Person接口被修改过时才重新编译,但是这种想法存在两个问题:

  • string并不是class,它是个typedef,上述前置声明不正确,正确的前置声明较为复杂。
  • 最重要的是,编译器必须在编译期间知道对象的大小:

解决这个问题的方法主要有两种:

  • 第一种方法:handle classes,把Person分割成两个类:一个只提供接口(Person),一个负责实现接口(PersonImpl)。即使用条款25:接口class中只包含一个负责实现接口的class的指针,因此任何改变都只是在负责实现接口的class中进行。这正是接口和实现分离,这种情况下,想Person这样使用pImplclass往往被称为handle classes
#include <string>
#include <memory> class PersonImpl; class Person {
public:
Person(string& name);
string name() const;
private:
std::shared_ptr<PersonImpl> pImpl; // 指针,指向实现物
}; Person::Person(string& name): pImpl(new PersonImpl(name)){}
string Person::name() {
return pImpl->name();
}
  • 第二种方法:Interface classes,令Person成为一种特殊的abstract class
class Person {
public:
virtual ~Person();
virtual std::string name() const = 0; static std::shared_ptr<Person> create(string& name); // 由于客户不能实例化这个类,只能使用它的引用和指针,所以需要提供一种方法来获得一个实例
}; std::shared_ptr<Person> Person::create(string& name) {
return std::shared_ptr<Person>(new RealPerson(name));
} class RealPerson: public Person {
public:
RealPerson(const std::string& name): theName(name) {}
virtual ~RealPerson(){}
std::string name() const;
private:
std::string theName;
}; // 使用
std::shared_ptr<Person> p(Person::create("parzulpan"));
std::cout << p->name() << std::endl;

请记住

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

【C++】《Effective C++》第五章的更多相关文章

  1. [Effective Java]第五章 泛型

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  2. 《Django By Example》第五章 中文 翻译 (个人学习,渣翻)

    书籍出处:https://www.packtpub.com/web-development/django-example 原作者:Antonio Melé (译者@ucag注:大家好,我是新来的翻译, ...

  3. 《Entity Framework 6 Recipes》中文翻译系列 (22) -----第五章 加载实体和导航属性之延迟加载

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 第五章 加载实体和导航属性 实体框架提供了非常棒的建模环境,它允许开发人员可视化地使 ...

  4. 精通Web Analytics 2.0 (7) 第五章:荣耀之钥:度量成功

    精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第五章:荣耀之钥:度量成功 我们的分析师常常得不到我们应得的喜欢,尊重和资金,因为我们没有充分地衡量一个黄金概念:成果.因为我们 ...

  5. 《Linux内核设计与实现》读书笔记 第五章 系统调用

    第五章系统调用 系统调用是用户进程与内核进行交互的接口.为了保护系统稳定可靠,避免应用程序恣意忘形. 5.1与内核通信 系统调用在用户空间进程和硬件设备间添加了一个中间层, 作用:为用户空间提供了一种 ...

  6. Java语言程序设计(基础篇) 第五章 循环

    第五章 循环 5.2 while循环 1.while循环的语法如下: while(循环继续条件){ //循环体 语句(组); } 2.程序:提示用户为两个个位数相加的问题给出答案 package co ...

  7. 读《编写可维护的JavaScript》第五章总结

    第五章 UI层的松耦合 5.1 什么是松耦合 在Web开发中,用户界面是由三个彼此隔离又相互作用的层定义的: HTML是用来定义页面的数据和语义 CSS用来给页面添加样式 JavaScript用来给页 ...

  8. 《Linux内核设计与实现》课本第五章学习笔记——20135203齐岳

    <Linux内核设计与实现>课本第五章学习笔记 By20135203齐岳 与内核通信 用户空间进程和硬件设备之间通过系统调用来交互,其主要作用有三个. 为用户空间提供了硬件的抽象接口. 保 ...

  9. Android深度探索--HAL与驱动开发----第五章读书笔记

    第五章主要学习了搭建S3C6410开发板的测试环境.首先要了解到S3C6410是一款低功耗.高性价比的RISC处理器它是基于ARMI1内核,广泛应用于移动电话和通用处理等领域. 开发板从技术上说与我们 ...

随机推荐

  1. 如何实现 token 加密(来自github每日一题)

    需要一个secret(随机数) 后端利用secret和加密算法(如:HMAC-SHA256)对payload(如账号密码)生成一个字符串(token),返回前端 前端每次request在header中 ...

  2. idea2020.2.x/2020.3.x最新破解版方法教程无限永久重置插件激活码

    idea是一个java开发工件,相信我所有的朋友都用过.本教程教你做到完美,安全,永久.破解 idea2020.2.x和idea2020.3.x的所有版本绝对是100% 激活,支持Windows Ma ...

  3. 中间件面试专题:kafka高频面试问题

  4. li = [11,22,33,44,55,66,77,88,99]分类

    方法一: li = [11,22,33,44,55,66,77,88,99]s = []m = []for i in li: if i <= 55: s.append(i) else: m.ap ...

  5. WPS PDF转Word工具

    WPS PDF转Word工具链接:https://pan.baidu.com/s/1Ijh5MSBWZtsXsm05_6yYvw 提取码:gufy  下载运行后会解压到"D:\Program ...

  6. zstd c++ string 压缩&解压

    zstd 简介 维基百科定义: Zstandard(或Zstd)是由Facebook的Yann Collet开发的一个无损数据压缩算法.该名称也指其C语言的参考实现.第1版的实现于2016年8月31日 ...

  7. db2常用操作

    1. db2建立远程节点编目及删除 db2 catalog tcpip node nodeName remote remoteIp server remotePort db2 list node di ...

  8. Android各版本迭代改动与适配集合

    前言 今天分享的面试题是: Android在版本迭代中,总会进行很多改动,那么你熟知各版本都改动了什么内容?又要怎么适配呢? Android4.4 发布ART虚拟机,提供选项可以开启. HttpURL ...

  9. js 点击按钮下载图片,另存为

    js: 1 $(document).on('click',"#xiazai",function(){ 2 imgurl = $(".img-box").find ...

  10. 小白都能理解的Python多继承

    本文主要做科普用,在真实编程中不建议使用多重继承,或者少用多重继承,避免使代码难以理解. 方法解析顺序(MRO) 关于多重继承,比较重要的是它的方法解析顺序(可以理解为类的搜索顺序),即MRO.这个跟 ...