第一章 让自己习惯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. XML文件格式

    首行是需要对xml版本声明<?xml version="1.0" ecoding="UTF-8"?> 语言可根据需要修改 在编写xml的几个注意点 ...

  2. elastic-job分布式调度与zookeeper的简单应用

    一.对分布式调度的理解 调度->定时任务,分布式调度->在分布式集群环境下定时任务这件事 Elastic-job(当当⽹开源的分布式调度框架) 1 定时任务的场景 定时任务形式:每隔⼀定时 ...

  3. C++ 虚函数表与多态 —— 多重继承的虚函数表 & 内存布局

    多重继承的虚函数表会有两个虚表指针,分别指向两个虚函数表,如下代码中的 vptr_s_1.vptr_s_2,Son类继承自 Father 和 Mather 类,并且改写了 Father::func_1 ...

  4. DVWA各等级XSS

    xss原理及基本介绍 XSS,全称Cross Site Scripting,即跨站脚本攻击,某种意义上也是一种注入攻击,是指攻击者在页面中注入恶意的脚本代码,当受害者访问该页面时,恶意代码会在其浏览器 ...

  5. [OI笔记]每周刷题记录

    一些题库: bzoj.uoj.luogu(洛谷).CF.loj.hdu.poj.51nod 下面是一些近期的做题记录 省选爆炸-然后大概就先这样了,要回去读一段时间文化课,如果文化课还不错的话也许还会 ...

  6. 是的,你没看错!Python可以实现自动化办公

    是的,你没看错!Python可以实现自动化办公 公众号[伤心的辣条],如今越来越多的人加入到学习Python的队伍当中,尤其是对于很多职场人来说,不管你是程序员还是非程序员,Python已经为很多职场 ...

  7. 赶紧收藏!Spring MVC 万字长文笔记,我愿奉你为王者笔记!

    Spring MVC Spring MVC是目前主流的实现MVC设计模式的企业级开发框架,Spring框架的一个子模块,无需整合Spring,开发起来更加便捷. 什么是MVC设计模式? 将应用程序分为 ...

  8. 物联网打工人必备:LiteOS Studio图形化调测能力

    摘要:本文会给大家介绍下LiteOS Studio的调测的几个知识点,包括: 调测配置,监视变量,反汇编代码同步展示,数值进制切换,跨平台编译调测,Qemu模拟器调测,多核调测,远程设备调测等. 掌握 ...

  9. P4735 最大异或和 01 Trie

    题目描述 给定一个非负整数序列 \(\{a\}\),初始长度为\(n\). 有 \(m\) 个操作,有以下两种操作类型: \(A\ x\):添加操作,表示在序列末尾添加一个数 \(x\),序列的长度 ...

  10. 解决uiautomator截取不到手机App界面信息

    今天在使用uiautomatorviewer进行安卓app控件定位的时候,出现以下异常,(用的是真机测试Android版本是10,据说是Android 8以后sdk自带的uiautomator直接打开 ...