1. 让自己习惯 C++

条款 01:视 C++ 为一个语言联邦

为了理解 C++,必须认识其主要的次语言:

  • C:C++ 仍以 C 为基础。很多时候,C++ 对问题的解法起始是较高级的 C 的解法(如,条款 02、条款 13)。
  • Object-Oriented C++:C with Classes,classes、封装、继承、堕胎、虚函数等等。这部分是面向对象设计之古典守则在 C++ 上最直接的实施。
  • Template C++:泛型编程
  • STL:STL 是个 template 程序库,即 C++ 标准库。

C++ 并不是也给带有一组守则的一体语言:它是从四个次语言组成的语言联邦,每个次语言都有自己的规约。

请记住:

  • C++ 高效编程守则视状况而变化,取决于你使用 C++ 哪一部分。

条款 02:尽量以 const,enum,inline 替代 #define

此条例可以看作,使用编译器代替预处理器,因此 #define 不被视为语言的一部分。

#define ASPECT_PATIO 1.653

记号 ASPECT_PATIO 可能不会被编译器看见,导致 ASPECT_PATIO 可能没有进入记号表中。当运用此常量发生编译错误时,这个错误信息可能提到的是 1.653 而不是 ASPECT_PATIO,此时你可能会为了追踪 1.653 的源头浪费大量的时间。解决这一问题的方法是使用一个常量代换上述的宏:

const double AspectRatio = 1.653;  // 大写名称常用于宏,因此这里改变名称的写法

作为一个语言常量,AspectRatio 可你的那个会被编译器看到,并进入记号表内。

无法利用 #define 创建一个 class 专属常量

我们无法利用 #define 创建一个 class 专属常量,因为 #define 并不重视作用域。一旦宏被定义,它就在其后编程过程中有效(除非在某处被 #undef)。这意味着 #define 不仅不能用来定义 class 专属常量,也不能提供任何封装性(private)。

利用 enum 表达常量

当编译器不允许 “static 整数型 class” 完成 “inclass 初值设定(类内初始化)”,可以使用 enum 来解决,例如:

class GamePlayer {private:enum { NumTurns = 5 };int scores[NumTurens];
}

enum 的这个行为类似于 #define 而不是 const,例如 取一个 const 的地址是合法的,但取一个 enum 的地址就不合法,取一个 #define 的地址通常也不合法。enum 和 #define 一样绝不会导致非必要的内存分配。

不要用形似函数的宏

#define 实现的宏看起来像函数,但不会有函数调用带来的额外开销。下面这个宏带着宏实参,调用函数 f:

// 以 a 和 b 的较大值调用 f
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

当使用这种宏是,必须为宏中所有实参加上小括号,否则会出现不可思议的事情:

int a = 5, b = 0;
CALL_WITH_MAX(++a, b);  // a 被累加两次
CALL_WITH_MAX(++a, b + 10);  // a 被累加一次

为了获得宏所拥有的效率,以及一般函数的所有可预料行为和类型安全性,我们可以使用 template inline 函数(条款 30):

// 由于我们不知道 T 是什么,所以采用 pass by reference-to-const 见条款 20
template<typename T>
inline void callWithMax(const T& a, const T& b) {if(a > b ? a : b);
}

这里不需要在函数本体中为参数加上括号,也不需要操行参数被核算多次等待。

有了 const、enum 和 inline,我们对预处理器(尤其是 #define)的需求降低了,但并非完全消除。

请记住:

  • 对于单纯常量,最好以 const 对象或 enum 替换 #define。
  • 对于形似函数的宏,最好改用 inline 替换 #define。

条款 03:尽可能使用 const

const 允许指定一个于一约束(指定不被改动的对象),而编译器会强制执行这项约束。它允许你告诉编译器和其他程序员某值应该保持不变。

如果关键字 const 出现在星号左边,表示被指物事常量(底层);如果出现在星号右边,表示指针自身事常量(顶层);如果出现在星号两边,表示被指物和指针两者都是常量。

STL 迭代器是以指针为根据得到的,所以迭代器的作用就像哥 T* 指针。生命迭代器为 const 就像声明指针为 const 一样(即,声明一个 T* const 指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值可以改动。

令函数返回一个常量值,往往可以降低因客户错误而造成的以外,而又不至于放弃安全性和高效性,例如:

const Rational operator* (const Rational& lhs, const Rational& rhs);

可以阻止如下操作:

Rational a, b, c;
(a * b) = c;  // 在 a * b 的成果上调用 operator=

如果 a 和 b 都是内置类型,这样的代码直截了当就是不合法。而一个”良好的用户自定义类型“的特征是它们避免无端地与内置类型不兼容。将 operator* 的返回值声明为 const 可以预防那个没有意义的赋值动作。

至于 const 参数,除非你有需要改动参数或 local 对象,否则请将它们声明为 const。

const 成员函数

将 const 实施于成员函数的目的,是为了确认该成员函数可作用于 const 对象身上。这类成员函数重要原因如下:

  • 它们使 class 接口比较容易被理解。因为,这样可以知道哪些函数可以改动对象内容,哪些不行。
  • 它们使”操作 const 对象“成为可能。

需要注意一个事实:两个成员函数如果只是常量性不同,那么它们可以被重载

bitwise constness(physical constness) 和 logical constness

成员函数如果是 const 意味着什么?有两个流行概念: bitwise constness(又称 physical constness)和 logical constness。

bitwise constness(physical constness)

bitwise const 认为,成员函数只有在不更改对象的任何成员变量(static 除外)时才可以说是 const。也就是说它不更改对象内部的任何一个 bit。这种论点的好处是很容易发现错误点:编译器只需寻找成员变量的赋值动作即可。bitwise constness 正是 C++ 对常量性的定义,因此 const 成员函数不可以更该对象内任何 non-static 成员变量

但是很多成员函数虽然没有完全具备 const 性质但是却可以通过 bitwise 测试。如果一个更改了”指针所指物“的成员函数虽然不能算是 const,但是如果只有指针(所指物不属于对象)隶属于对象,那么 bitwise const 不会引发编译错误。

参考下面的例子:

class CTextBlock {public:char& operator[](std::size_t position) const {return pText[position];}
private:char* pText;
};

这个 class 不适当的将 operator[] 声明为 const 成员函数,而该函数去返回一个 reference 指向对象内布置(条款 28)。在此例中 operator[] 并不更改 pText。因为是 bitwise const,所以编译器可以通过,但是如果发生下面的操作:

const CTextBlock cctb("Hello");  // 声明一个常量对象
char *pc = &cctb[0];  // const operator[] 区的一个指针,指向 cctb
*pc = 'J';  // cctb 变成了 "Jello"

创建了一个常量对象并设以某值,而且只对它调用 const 成员函数,但是还是改变了它的值。

logical constness

这种情况产生了 logical constness。这一派主张,一个 const 成员函数可以修改它所处理的对象内的某些 bits,但只有在客户端检测不出的情况下才行:

class CTextBlock {public:std::size_t length() const;
private:char* pText;std::size_t textLength;  // 最近一次计算的文本区块长度bool lengthIsValid;  // 目前的长度是否有效
};std::size_t CTextBlock::length() const {if (!lengthIsValid) {textLength = std::strlen(pText);  // 错误 在 const 内不能给其他成员赋值lengthIsValid = true;}
}

由于 length 会修改 textLength 和 lengthIsValid,所以编译器会发出错误。此时就需要一个 C++ 与 const 相关的摆动场:mutable。mutable 会释放掉 non-static 成员变量的 bitwise const 约束。此时编译器就不会发出错误信息了。

在 const 和 non-const 成员函数中避免重复

假设 TextBlock 内的 operator[] 不但只是返回一个 reference 指向某个字符,也执行一些其他的检测。把所有这些同时放进 const 和 non-const operator[] 中,会出现代码重复、回鹘、代码膨胀等待问题:

class TextBlock {public:const char& operator[](std::size_t position) const {...  // 边界检测...  // 志记数据访问...  // 检验数据完整性return pText[position];}char& operator[](std::size_t position) {...  // 边界检测...  // 志记数据访问...  // 检验数据完整性return pText[position];}
private:std::string text;
};

虽然可以将这些代码放到另一个成员函数中,并令两个版本的 operator[] 调用它,但是还是重复了一些代码,如函数调用、两次 return 语句。我们应该实现 operator[] 的功能并使用它两次。

本例中 const operator[] 完全实现了 non-const 版本该做的一切,唯一的不同是返回类型多了一个 const 修饰。这时候让 non-const 版本调用 const 版本是一个避免代码重复的安全做法:

class TextBlock {public:const char& operator[](std::size_t position) const {...  // 边界检测...  // 志记数据访问...  // 检验数据完整性return pText[position];}char& operator[](std::size_t position) {return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);  // 将 op[] 返回值的 const 移除,为 *this 加上 const,调用 const op[]}
private:std::string text;
};

这个代码有两个类型转换操作,第一次用来为 *this 添加 const(使接下来的调用使用 const 版本,而不是 non-const 版本),第二次则是从 const operator[] 的返回值中移除 const。

第一次类型转换,通过 static_cast 强制类型转换 添加了 const;第二次类型转换,移除 const,只能通过 const_cast 来实现(static_cast 和 const_cast 见条款 27)。

不应该令 const 版本调用 non-const 版本,const 成员函数承诺不改变其对象的逻辑状态,non-const 成员函数没有这样的承诺。所以如果这样做,有可能会让你承诺不改动的对象被改动了。这也是为什么“const 成员函数调用 non-const 成员函数”是一种错误行为。

请记住:

  • 将某些东西声明为 const 可帮助编译器检测出错误用法。const 可被施加于任何作用内的对象、函数参数、函数返回类型、成员函数本体。
  • 编译器强制实施 bitwise constness,但你编写程序时应该使用“概念上的常量性”。
  • 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复。

条款 04:确定对象被使用前已先被初始化

读取未初始化的值会导致不明确的行为。读取未初始化的值可能会导致程序终止运行,但更可能的情况是读入一些“半随机”bits,污染了正在进行读取动过的那个对象,最终导致不可预测的程序行为。

通常如果你使用 C part of C++ 而且初始化可能招致运行期成本,那么就不保证发生初始化。而 non-C parts of C++,规则会有所不同。这样解释了为什么 array(来自 C part of C++)不保证其内容被初始化,而 vector(来自 non-C parts of C++)却有此保证。

表面上这是无法决定的状态,最佳处理方法是:永远在使用对象之前将它初始化。

对于内置类型以外的任何东西,初始化责任落在构造函数身上。但是不要混淆赋值和初始化。在构造函数里,应该使用成员初始值列表来标识初始化。

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) : theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) { }

C++ 有十分固定的“成员初始化次序”。是的,次序总是相同:base classes 更早于其 berived classes 被初始化,而 class 的成员变量总是以其声明次序被初始化。

跨编译单元初始化

编译单元是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件。

解决问题:如果某编译单元内的某个 non-local static 对象的初始化动作使用了另一个编译单元内的某个 non-local static 对象,它所用到的这个对象可能尚未被初始化,因为 C++ 对“定义于不同比那一单元内的 non-local static 对象”的初始化次序并无明确定义。

例如:

class FIleSystem {public:...std::size_t numDisks() const;...
};
extern FileSystem tfs;  // 预备给客户使用的对象

假设某些客户建立了一个 class 用以处理文件系统内的目录,此时他们的 class 会用上 FileSystem 对象:

class Directory {public:Sirectory(params);
};
Directory::Directory(params) {std::size_t disks = tfs.numDisks();  // 使用 tfs 对象
}

此时初始化次序的重要性出现了:除非 tfs 在 tempDir 之前先被初始化,否则 tempDir 的构造函数会用到尚未初始化的 tfs。但是 tfs 和 tempDir 是不同的人在不同的时间于不同的源码文件建立起来的。换句话说,C++ 对“定义于不同编译单元内的 nonlocal static 对象”的初始化相对次序并无明确定义。

解决这个问题的方法是:将每个 non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为 static)。这些函数返回一个 reference 指向它所含的对象。然后用户调用这些函数,而不直接使用这些对象。换句话说,non-local static 对象被 local static 对象替换了。

也就是说,C++ 保证,如果以 函数调用 替换直接访问 non-local static,函数内的 local static 对象会在“函数被调用时”或“首次遇到该对象定义时”已经被初始化了。

class FileSystem { ... };
FileSystem& tfs() {static FileSystem fs;return fs;
}class Directory { ... };
Directory::Directory(params) {std::size_t disks = tfs().numDisks();
}
Directory& tempDir() {static Directory td;return td;
}

这么修改后,这个系统程序完全可以像以前一样使用它,唯一不同的是他们现在使用 tfs() 和 tempDir() 而不再是 tfs 和 tempDir。也就是说他们使用函数返回的“指向 static 对象”的引用,而不再使用 static 对象自身。

请记住:

  • 为内置型对象进行手工初始化,因为 C++ 不保证初始化它们。
  • 构造函数最好使用成员初始化列表,而不要再构造函数体内使用赋值操作。初始值列表列出的成员变量,其排列次序应该和它们在 class 中的声明次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以 local static 对象替换 non-local static 对象。

Effective C++ 01 让自己习惯 C++相关推荐

  1. More Effective C++: 01基础议题

    01:仔细区别 pointers 和 references 1:没有所谓的null reference,但是可以将 pointer 设为null.由于 reference 一定得代表某个对象,C++ ...

  2. 阿龙的学习笔记---Effective C++---第一章:习惯C++

    条款1:视C++为一个语言联邦 C++发展至今已经不只是C with classes的概念了,他是一个多重范式的编程语言.可将其视为一个多重语言的联邦. C++的次语言被总结为以下4个: C语言.说到 ...

  3. Effective C++ --1 让自己习惯C++

    1.视C++为一个语言联邦 C++主要的次语言有四种:C.Object-Oriented C++.Template C++和STL. 2.尽量以const,enum,inline替换#define ( ...

  4. Effective C++ -- 零散知识点整理

    Effective C++ --1让自己习惯C++ Effective C++ --2构造/析构/赋值运算 Effective C++ --3资源管理 Effective C++ --4设计与声明 E ...

  5. Effective C++ --2 构造/析构/赋值运算

    上一部分Effective C++ --1 让自己习惯C++ 5. 了解C++默认编写并调用哪些函数 (1)   编译器暗自为类创建默认构造函数.拷贝构造函数.拷贝赋值函数和析构函数. (2)   拷 ...

  6. 【C#】Whisper 离线语音识别(微软晓晓语音合成的音频)(带时间戳、srt字幕)...

    语音合成&语音识别 用微软语音合成功能生成xiaoxiao的语音. 用Whisper离线识别合成的语音输出srt字幕. 一.语音合成 参考这个网址:https://www.bilibili.c ...

  7. [存档]Martin Fowler在UMLChina交流实录

    北京时间2002年1月23日(星期三)上午9:30-11:30 嘉宾:Martin Fowler.现为ThoughtWorks首席科学家.Martin Fowler目前著有4本书籍: Analysis ...

  8. 为何2020年,生鲜电商领域会迎来市场的大爆发?

    生鲜一直属于居民的刚性需求,这也决定了生鲜电商的光明前景. 但由于生鲜产品不耐储存.民众的购买习惯等问题,生鲜电商的事业发展一直充满波折,饱经磨难. 不少人都在思考:生鲜电商何时才能迎来黎明? 今年上 ...

  9. Vue3+TypeScript实现网易云音乐WebApp(解析歌词,并实现自行匹配滚动)

    前言 最终实现效果gif: 这篇文章实现了gif里的其他功能 1. 实现思路 解析歌词 拿到歌词数组 -> lyricArr = [{time: 0, lyric: '给我你的爱', uid: ...

最新文章

  1. 不只是华为/阿里/百度/小米/京东,AIoT已然成为资本与新兴企业都认可的赚钱方向...
  2. linux下修改/dev/shm tmpfs文件系统大小
  3. Linux C编程--进程介绍3--进程终止和等待
  4. 参与势力战是不可多得的zhajinhua2012
  5. python读音播报-基于python GUI开发的点名小程序(语音播报)
  6. 【SICP练习】31 练习1.37
  7. 工业设备数据采集系统-采集精灵
  8. 计算机教学提问的观课量表,观课议课|如何使用观察和记录量表
  9. Md5是什么?MD5怎么校验?Md5校验工具怎么用?
  10. 模电 2个NPN管组成的恒流源电路分析
  11. 职场的5个时间管理技巧
  12. html传参,css接受?What 弄啥嘞?
  13. css常用的属性(边框三角形,文本省略号)
  14. vue实现考勤排班日历(备忘)
  15. 每日好店——淘宝店铺推荐系统实践
  16. Android——地图
  17. 【ThreeJS】场景后处理增加圆形暗角效果
  18. 当我们谈BIM的时候,我们在谈什么?
  19. 英语语法汇总(2.冠词)
  20. 不用写算法的机器视觉外观检测软件——让自动化检测更加简便

热门文章

  1. 吉他入门教程之演奏技巧—推弦的练习拓展
  2. 吐血整理Python体系练手项目500例(附源代码),练完可就业
  3. 保定学院计算机二级证书领取时间,专接本各科目考试发、收试卷及答题纸(卡)时间...
  4. 微服务框架 SpringCloud微服务架构 服务异步通讯 51 死信交换机 51.3 延迟队列 51.3.1 延迟队列 51.3.2 延迟队列插件
  5. 8x8点阵c语言程序,8X8点阵取模软件下载
  6. 手机如何升降鸿蒙系统,鸿蒙系统现身,搭配升降式镜头和鸿鹄芯片,你以为是手机?...
  7. 计算机及应用中级职称,计算机中级职称考试试题及答案
  8. Python:list和dict的数据类型基础操作
  9. 如何提高总结和归纳的能力
  10. 阿里首次公开量子通信技术 为未来而准备