【C++】《Effective C++》第五章
第五章 实现
条款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风格:
- 新式转型(
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才能做到。
- 用来强制隐式转换,只要不包含底层const,都可以使用。适合将较大算术类型转换为较小算术类型。例如将
应该尽可能使用新式转型,主要有两点原因:
- 它们很容易在代码中被辨别出来(无论是人工还是使用工具如
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这样使用pImpl的class往往被称为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 classes和Interface classes。 - 程序库头文件应该以"完全且仅有声明式"(
full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。
【C++】《Effective C++》第五章的更多相关文章
- [Effective Java]第五章 泛型
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- 《Django By Example》第五章 中文 翻译 (个人学习,渣翻)
书籍出处:https://www.packtpub.com/web-development/django-example 原作者:Antonio Melé (译者@ucag注:大家好,我是新来的翻译, ...
- 《Entity Framework 6 Recipes》中文翻译系列 (22) -----第五章 加载实体和导航属性之延迟加载
翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 第五章 加载实体和导航属性 实体框架提供了非常棒的建模环境,它允许开发人员可视化地使 ...
- 精通Web Analytics 2.0 (7) 第五章:荣耀之钥:度量成功
精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第五章:荣耀之钥:度量成功 我们的分析师常常得不到我们应得的喜欢,尊重和资金,因为我们没有充分地衡量一个黄金概念:成果.因为我们 ...
- 《Linux内核设计与实现》读书笔记 第五章 系统调用
第五章系统调用 系统调用是用户进程与内核进行交互的接口.为了保护系统稳定可靠,避免应用程序恣意忘形. 5.1与内核通信 系统调用在用户空间进程和硬件设备间添加了一个中间层, 作用:为用户空间提供了一种 ...
- Java语言程序设计(基础篇) 第五章 循环
第五章 循环 5.2 while循环 1.while循环的语法如下: while(循环继续条件){ //循环体 语句(组); } 2.程序:提示用户为两个个位数相加的问题给出答案 package co ...
- 读《编写可维护的JavaScript》第五章总结
第五章 UI层的松耦合 5.1 什么是松耦合 在Web开发中,用户界面是由三个彼此隔离又相互作用的层定义的: HTML是用来定义页面的数据和语义 CSS用来给页面添加样式 JavaScript用来给页 ...
- 《Linux内核设计与实现》课本第五章学习笔记——20135203齐岳
<Linux内核设计与实现>课本第五章学习笔记 By20135203齐岳 与内核通信 用户空间进程和硬件设备之间通过系统调用来交互,其主要作用有三个. 为用户空间提供了硬件的抽象接口. 保 ...
- Android深度探索--HAL与驱动开发----第五章读书笔记
第五章主要学习了搭建S3C6410开发板的测试环境.首先要了解到S3C6410是一款低功耗.高性价比的RISC处理器它是基于ARMI1内核,广泛应用于移动电话和通用处理等领域. 开发板从技术上说与我们 ...
随机推荐
- 获取浏览器URL中查询字符串中的参数
//http://www.runoob.com/index.html?name=xiaoming&age=23function showWindowHref(){ var sHref = wi ...
- 【SPOJ QTREE4】Query on a tree IV(树链剖分)
Description 给出一棵边带权(\(c\))的节点数量为 \(n\) 的树,初始树上所有节点都是白色.有两种操作: C x,改变节点 \(x\) 的颜色,即白变黑,黑变白. A,询问树中最远的 ...
- Android全面解析之Context机制
前言 很高兴遇见你~ 欢迎阅读我的文章. 在文章Android全面解析之由浅及深Handler消息机制中讨论到,Handler可以: 避免我们自己去手动写 死循环和输入阻塞 来不断获取用户的输入以及避 ...
- 本地安装yum源脚本
rpm -qa|grep yum //检查是否安装了yum. 如果没有安装就执行下面的文件 创建一个以xxx.sh结尾的文件 #!/bin/bash #创建两个文件用于挂载文件 mkdir /mn ...
- Jmeter(5)JSON提取器
Jmeter后置处理器-JSON提取器 JSON是一种轻量级数据格式,以"键-值"对形式组织数据. JSON串中{}表示对象,[]表示对象组成的数组.对象包含多个"属性& ...
- 20201205-3 HTML环境搭建与文件基本结构
HTML环境搭建与文件基本结构 HTML的基础 HTML环境搭建 Pycharm 包含全部环境 编写(代码) → 运行浏览器 → 代码检查 Sublime 只是用来编写 HTML 代码: ...
- django 取出数据库的时间与当前时间相加减
1 转换时区utc比北京时间慢八个小时 from datetime import tzinfo, timedelta, datetime ZERO = timedelta(0) class UTC(t ...
- 为了Java微信支付V3开发包,我找出了微信支付文档至少六个错误
1. 前言 最近忙的一批,难得今天有喘气的机会就赶紧把最近在开发中的一些成果分享出来.前几日分享了自己写的一个微信支付V3的开发包payment-spring-boot-starter,就忙里偷闲完善 ...
- python 安装相关
一.安装python 1.官网下载python 1.1 可下载绿色版 2.2 也可下载安装版,安装时可自动安装pip 和 自动配置环境变量 2.手动配置环境变量,我的电脑>属性>高级> ...
- IphoneX适配正确姿势
IphoneX适配正确姿势 写在前面 距离18年9月iphonex发布以来已经快两年了(所以对于iphonex机型的头部刘海(sensor housing)和底部小黑条(Home Indicator) ...