第一章 让自己习惯C++

C++是一个威力强大的语言,带着众多特性,但是在你可以驾驭其威力并有效运用其特性之前,你必须先习惯C++的办事方式。

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

如今的C++已经是个多重范型编程语言,它同时支持过程形式面向对象形式函数形式泛型形式元编程形式

  • 由于C++的能力使其成为一个无可匹敌的工具,可能会引发某些迷惑:所有“适当用法”似乎都有例外

  • 将其视为一个由相关语言组成的联邦而非当一语言。在其某个次语言中,各种守则与通例倾向简单、直观易懂、并且容易记住。主要的次语言有:

    • C
    • Object-Oriented C++
    • Template C++
    • STL
  • C++高效编程守则视情况而变化,取决于你使用C++哪一部分。

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

通俗的讲,让编译器替换预处理器比较好,因为#define不被视为语言的一部分。

const替换#define

#define ASPECT_RATIO 1.653

# 替换成

const double AspectRatio = 1.653;
  • 这样做的理由:

    • 调试的需要:#define的记号会被预处理器移走,记号名称肯尼没进入记号表内。因此当#define的宏名称获得一个编译错误时,可能会引起困惑,浪费大量时间追踪错误。而AspectRatio肯定会被编译器看到。
    • 更小的代码量:对浮点数而言,使用常量可能比使用#define导致较小量的代码,因为预处理"盲目地将ASPECT_RATIO替换成1.653",这可能导致目标代码出现多份1.653
  • 常量替换的两种特殊情况:

    • 定义常量指针时,由于常量定义式通常被放在头文件内,因此有必要将指针(注意不是指针所指之物)声明为const

      const char* const authorName = "Scott Meyers";
      
      # 更好的做法:string对象比char*更和时宜
      
      const std::string authorName("Scott Meyers");
    • class专属常量需要声明在class内部,并且被class使用。而为确保此常量至多只有一份实体,必须让它成为一个static成员。

      // 通常定义在头文件
      class GamePlayer {
      private:
      static const in NumTurns = 5; // 常量声明式
      int scores[NumTurns]; // 使用该常量
      }; const int GamePlayer::NumTurns; // 某些编译器可能不支持类内初始化,因此需要在类外设初值

enum替换#define

某些编译器可能不支持类内初始化,可改用所谓的"the enum hack"的补偿做法。因为一个属于枚举类型的数值当作int被使用。

class GamePlayer {
private:
enum { NumTurns = 5};
int scores[NumTurns];
};
  • enum hack的行为某方面所比较像#define而不像const
  • enum hack更加实用,它是模板元编程的基础技术。

inline替换#define

#define实现宏看起来像函数,并且不会导致函数调用带来的开销,但是可能引发错误,且可读性降低。

#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被累加一次
  • 使用inline函数可以避免上面的问题,而且inline还能实现一个“class内的private inline函数”。
template<typename T>
inline void callWithMax(const T& a, const T& b) {
f(a > b ? a: b);
}

请记住

  • 对于单纯比变量,最好以const对象或者enums替换#defines
  • 对于形似函数的宏(macros),最好改用inline函数替换#defines

条款03:尽可能使用const

const修饰变量

如果变量本身不应该被修改,那么应该使用const修饰。编译器会强制实施这个约束,告诉编译器和其他程序员某值应该保持不变。

char greeting[] = "Hello";
char* p = greeting; // non-const pointer, non-const data
const char* p = greeting; // non-const pointer, const data
char* const p = greeting; // const pointer, non-const data
const char* const p = greeting; // const pointer, const data
  • 如何区分上面,主要看const的位置:

    • 如果关键字const出现在星号左边,表示被指物是常量。
    • 如果关键字const出现在星号右边,表示指针本身是常量。
    • 如果关键字const出现在星号两边,表示被指物和指针本身都是常量。

const修饰函数

  • 修饰参数时,和修饰一般变量相同。
  • 修饰返回值时,可以降低因客户错误而造成的意外,且又不至于放弃安全性和高效性。
class Rational {};
Rational a, b, c; if(a * b = c) { // 做一个比较操作,但是客户用法错误
// ...
} // 如果a和b都是内置类型,上面的用法当然错误,所以为了避免这种情况,应该: const Rational::Rational operator* (const Rational& lhs, const Rational& rhs);

const修饰成员函数

  • 这样做有两个好处:

    • 增强可读性,使得接口容易被理解,知道哪个函数可以改动对象内容而哪一个不行。
    • const修饰的成员函数可以作用于const对象,可以改善程序效率(根本办法是以pass by reference-to-const方式传递对象)。
  • 一个需要注意的问题:C++对常量性的定义是二进制位常量性(bitwise constness),即const成员函数不应该修改对象的任何成员变量。因此,如果成员变量是一个指针,那么不修改指针所指之物,则符合bitwise constness。但是不从bitwise constness的角度,也算是修改了对象。
class CTextBlock {
public:
char& operator[](std::size_t pos) const { // `bitwise constness`声明,但其实不恰当
return pText[pos];
private:
char* pText;
}
}; const CTextBlock cctb("Hello");
char* pc = &cctb[0]; *pc = 'J'; // cctb此时为"Jello"
  • 解决这个问题的方法是使用mutable,它能释放掉non-static成员变量的bitwise constness约束。
mutable std::size_t textLength;
mutable bool lengthIsValid;

constnon-const成员函数中避免重复

class TextBlock {
public: const char& operator[](std::pos) const {
// ...
return text[pos];
} char& operator[](std::size_t pos) {
// ...
return text[pos];
} private:
std::string text;
}; // 可以发现上面两个函数实现的功能大同小异,但是有很多重复
// 改进:做一个转型动作
class TextBlock {
public: const char& operator[](std::pos) const {
// ...
return text[pos];
} char& operator[](std::size_t pos) { // 只调用const operator[]
return const_cast<char&>(
static_cast<const TextBlock&>(*this)[pos];
)
} private:
std::string text;
};
  • 以上转型动作实际上做了两个操作:

    • *this添加const(使得接下来能调用const operator[]版本)。
    • const operator[]的返回值中移除const
  • 注意: const成员函数承诺绝不会改变其对象的逻辑逻辑状态,non-const成员函数则没有这种承诺。

请记住

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

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

  • 读取未初始化对象的后果: 它会导致不明确的行为,在某些平台上,仅仅只是读取未初始化的值,就可以让你的程序终止运行。更可能的情况是读入一些半随机bits,污染了正在进行读取动作的那个对象,最终导致不可测知的程序行为,以及许多不愉快的调试过程。

按对象的类型划分

  • 对于内置类型的对象,永远在使用前初始化。
  • 对于类类型的对象,初始化责任落在构造函数身上,即确保每一个构造函数都将对象的每一个成员初始化。
  • 重要: 由于类类型成员的初始化动作发生在构造函数本体之前,所以构造函数使用成员初始化列表替换赋值动作更好。因为赋值动作首先会调用default构造函数,然后再调用copy assignment操作符,而使用成员初始化列表之后只调用一次copy构造函数,显然更加高效。
class PhoneNumber {
//...
}; class ABEntry {
public:
ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones); private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
}; ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) {
theName = name; // 这些都是赋值而非初始化
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
} // 改进:使用成员初始化列表
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones):theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) { }

按对象的作用域与生命周期划分

  • non-local static对象:C++对“定义于不同的编译单元内的non-local static对象”的初始化相对次序并无明确定义。

    • global对象
    • 定义于namespace作用内的对象
    • classes内、file作用域内被声明为static的对象
  • local static对象:函数内的local static对象会在“该函数被调用期间、首次遇到该对象的定义式”时被初始化。
    • 函数内被声明为static的对象
  • 因此,如果一个non-local static对象的初始化依赖于另外一个non-local static对象的初始化,那么可能造成错误。
class FileSystem {  // 来自你的程序库
public:
std::size_t numDisks() const; };
extern FileSystem tfs; // 预备给客户使用的对象
class Directory { // 由程序库客户建立
public:
Directory(params); };
Directory::Directory(params) {
std::size_t disks = tfs.numDisks; // 使用tfs对象
} Directory tempDir(params); // 为临时文件而作出的目录
  • 以上的程序存在的一个问题: 除非tfstempDir之前先被初始化,否则tempDir的构造函数会用到尚为初始化的tfs解决方法:local static对象代替non-local static对象(参考单例模式的常见实现方法)。
class FileSystem {  // 来自你的程序库
public:
std::size_t numDisks() const; }; // 用这个函数代替tfs对象,定义并初始化一个local static对象,返回一个reference指向上述对象
FileSystem& tfs() {
static FileSystem fs;
return fs;
}
class Directory { // 由程序库客户建立
public:
Directory(params); };
Directory::Directory(params) {
std::size_t disks = tfs().numDisks; // 使用tfs()
} // 用这个函数代替tempDir对象
Directory& tempDir() {
static Directory td;
return td;
}

请记住

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

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

  1. Effective java第一章引言

    菜鸟一枚,开始读第一本书<Effective Java>(第二版)~ 看引言就有好多名词不懂(>_<) 导出的API由所有可在定义该API的包之外访问的API元素组成.一个包的 ...

  2. Effective c++ 第一章 让自己习惯C++

    条款 01:c++是一个语言联邦而不是一种单一的语言, 它包括: 1.C语言:没有模版.没有异常.没有重载…… 2.Object-Oriented C++:class.析构函数.构造函数.封装.继承. ...

  3. Effective JavaScript :第一章

    第一章 一.严格模式与非严格模式 1.在程序中启用严格模式的方式是在程序的最开始增加一个特定的字符串字面量: ‘use strict’ 同样可以在函数体的开始处加入这句指令以启用该函数的严格模式. f ...

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

    书籍出处:https://www.packtpub.com/web-development/django-example 原作者:Antonio Melé (译者注:本人目前在杭州某家互联网公司工作, ...

  5. MyBatis3.2从入门到精通第一章

    第一章一.引言mybatis是一个持久层框架,是apache下的顶级项目.mybatis托管到goolecode下,再后来托管到github下.(百度百科有解释)二.概述mybatis让程序将主要精力 ...

  6. Nova PhoneGap框架 第一章 前言

    Nova PhoneGap Framework诞生于2012年11月,从第一个版本的发布到现在,这个框架经历了多个项目的考验.一直以来我们也持续更新这个框架,使其不断完善.到现在,这个框架已比较稳定了 ...

  7. 第一章 MYSQL的架构和历史

    在读第一章的过程中,整理出来了一些重要的概念. 锁粒度  表锁(服务器实现,忽略存储引擎). 行锁(存储引擎实现,服务器没有实现). 事务的ACID概念 原子性(要么全部成功,要么全部回滚). 一致性 ...

  8. 第一章 Java多线程技能

    1.初步了解"进程"."线程"."多线程" 说到多线程,大多都会联系到"进程"和"线程".那么这两者 ...

  9. 【读书笔记】《编程珠玑》第一章之位向量&位图

    此书的叙述模式是借由一个具体问题来引出的一系列算法,数据结构等等方面的技巧性策略.共分三篇,基础,性能,应用.每篇涵盖数章,章内案例都非常切实棘手,解说也生动有趣. 自个呢也是头一次接触编程技巧类的书 ...

随机推荐

  1. ARM架构安装Kubernetes集群

    背景 类型 版本 操作系统 CentOS Linux release 7.6.1810 (AltArch) 内核 Linux master 4.18.0-80.7.2.el7.aarch64 硬件配置 ...

  2. C# 9.0新特性详解系列之五:记录(record)和with表达式

    1 背景与动机 传统面向对象编程的核心思想是一个对象有着唯一标识,表现为对象引用,封装着随时可变的属性状态,如果你改变了一个属性的状态,这个对象还是原来那个对象,就是对象引用没有因为状态的改变而改变, ...

  3. 深入理解Java虚拟机(四)——HotSpot垃圾收集器详解

    垃圾收集器 新生代收集器 1.Serial收集器 特点: 单线程工作,收集的时候就会停止其他所有工作线程,用户不可知不可控,会使得用户界面出现停顿. 简单高效,是所有收集器中额外内存消耗最少的. 没有 ...

  4. python 全局变量与局部变量 垃圾回收机制

    掌握L.E.G.B(作用域) 掌握局部作用域修改全局变量 步骤- 1.命名空间和作用域 命名空间:变量名称与值的映射关系作用域:变量作用的区域,即范围. 注意:class/def/模块会产生作用域:分 ...

  5. DataGrid 字体垂直居中

    如果用DataGridTextColumn作为DataGrid的列,字体垂直居中需要这样设置: <Style x:Key="Body_Content_DataGrid_Centerin ...

  6. HBase过滤器:SingleColumnValueFilter和FirstKeyOnlyFilter一起使用的问题

    FirstKeyOnlyFilter是对第一列进行过滤,hbase中的列按照字典序排列,所以如果SingleColumnValueFilter中的过滤列不是第一列的话,FirstKeyOnlyFilt ...

  7. js上 初识JavaScript

    1.JavaScript简介 **JavaScript ** 是什么?(重点) Js是一种专门为网页交互设计的客户端(浏览器端)的脚本语言: Js与html和css有相似之处,都在浏览器端解析: Js ...

  8. BUUCTF 刮开有奖 WriteUp

    题目链接 https://buuoj.cn/challenges#%E5%88%AE%E5%BC%80%E6%9C%89%E5%A5%96 题解 用IDA打开,按F5反编译,双击进入DialogFun ...

  9. 解决Yii ActiveForm监听submit触发2次submit

    今天用yii框架的ActiveForm需要一个奇怪的问题,点击表单提交时会触发两次submit 产生问题的原因: form挂了2次submit事件,一次是yii activeform自带的,一次是我写 ...

  10. Spring Cloud Alibaba基础教程-Nacos(一)

    2019快结束,也有很久没写博客了,今天我们来谈谈Nacos,如果对您有帮助,麻烦左上角点个关注 ,谢谢 ! 嘻嘻 今天先写第一篇 文章目录 为什么要使用Nacos Eureka 闭源 Nacos的优 ...