Effective C++
55 Specific Ways to Impove Your Programs and Designs
改善程序与设计的55个具体做法
1. Accustoming Yourself to C++ 让自己习惯C++
01: View C++ as a federation of languages
视C++为一个语言联邦
C++最初名称 C with Classes
主要的次语言有四个:
- C:区块、语句、预处理、数据类型、数组、指针
- Object-Oritented C++:对象、封装、继承、多态、动态绑定
- Template C++ :泛型编程
- STL:容器、迭代器、算法、函数对象
每个次语言有自己的规约
02: Prefer consts, enums and inlines to #defines
尽量以const,enum,inline替换#define
-
以编译器替换预处理器,因为 #define不是语言的一部分,编译器看不到被#define定义的变量,没有进入记号表(symbol table),编译错误时被替换,难以追踪问题
-
指针类型常量,写两次const:
const char* const authorName = "Scott Meyers";
最好使用
string
类型:const std::string authorName(“Scott Meyers”)
-
类常量,声明时获取初值:static const
-
旧编译器不支持声明获取初值,使用
enum
:enum {Num = 5};
enum类似#define,无法获取常量地址,没有分配存储空间
模板元编程
-
template inline函数替换宏,使用宏注意加括号,但仍无法避免MAX(++a, b)形式调用的不确定结果
-
#incline需求降低了,但没有完全消除。#ifdef/#ifndef控制编译
03: Use const whenever possible.
尽可能使用const
-
const与指针
- 星号左边,const char* p:被指物是常量(*p)
- 星号右边,char* const p:指针自身是常量(p)
- 星号两边,const char* const p:被指物和指针都是常量
- const Widget* pw 与 Widget const *pw相同
-
const与函数
- 返回值 const
- const参数:除非需要修改参数
- const成员函数:不改动对象
-
bitwise/phisical constness与logical constness
class CTextBook { public: ... std::size_t length() const { if (!isLengthValid) { textLength = std::strlen(pText); // 错误:在只读结构中不能有赋值操作 isLengthValid = true; } return textLength; } private: char* pText; std::size_t textLength; bool isLengthValid; };
- logical constness实现方式,使用mutable释放掉non-static成员变量的bitwise constness约束:
class CTextBook { public: ... std::size_t length() const { if (!isLengthValid) { textLength = std::strlen(pText); isLengthValid = true; } return textLength; } private: char* pText; mutable std::size_t textLength; mutable bool isLengthValid; };
-
如果operator[]包含边界检查、访问信息、数据完整性检验,编写const和non-const方法会有大量重复代码
将相同代码移动到同一个成员函数,多了函数调用和两次return
-
常量性转移(casting away constness)
non-const调用const函数
class TextBook { public: ... char& operator[](std::size_t position) const // const对象 { ... // 边界检查 ... // 访问信息 ... // 数据完整性检验 return text[position]; } char operator[](std::size_t position) { return const_cast<char&>( // 将operator[]返回值的const转除 static_cast<const TextBook&>(*this)[position] // 为*this加上const,调用const函数 ); } private: std::string text; };
- 不能反向进行,即让const版本函数调用non-const函数。调用const方法必须保证对象的逻辑状态不被修改,non-const有更改的风险
04: Make sure that objects are initialized before they’re used
确定对象被使用前已先被初始化
读取未初始化的值会导致不明确的行为:某些平台可能会让程序终止运行;读入半随机的bits,导致不可预知的程序行为
-
内置类型,定义时手动初始化/读入
int x = 0;
const char* text = “A C-style string”;
double d;
std::cin >> d;
-
赋值(assignment)与初始化(initialization)
错误做法,构造函数采用了赋值而非初始化:
class PhoneNumber { ... }; class Entry { private: std::string theName; std::string theAddress; std::list<PhoneNumber> thePhones; int numCount; public: Entry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones); } Entry::Entry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) { theName = name; theAddress = address; thePhones = phones; numCount = 0; }
-
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。使用成员初始化列表:
Entry::Entry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) :theName(name), theAddress(address), thePhones(phones), numCount(0) { }
结果与上面相同,但效率更高
default构造函数+调用copy赋值操作符 VS 调用一次copy构造函数:
-
赋值的版本,首先调用默认构造函数,为theName,theAddress,thePhones设初值,然后再赋新的值
-
初始化列表使用copy构造函数,(参数可为空,调用默认构造函数)
-
-
成员变量是const或reference,一定要使用初值
-
成员初始化总是以声明次序被初始化,与初始化列表顺序无关
-
non-local static对象的初始化顺序,C++无明确定义
至少两份源码文件,在其中一次引用静态对象,使用前可能未初始化
解决方法:单例模式
将每个non-local static对象在专属函数内声明为static,函数返回其引用指向的对象。用户调用函数而不是直接使用对象,转化为local static对象。tfs ==> tfs()
class FileSystem { ... }; FileSystem tfs() { static FileSystem fs; return fs; } // 客户使用 class Directory { ... }; Directory::Directory( params ) { ... std::size_t disks = tfs().numDisks(); ... }
额外问题:非const的静态对象,多线程中的竞争条件(race conditions),即多个线程同时访问相同的共享数据,造成数据的不一致性
2.Constructors, Destructors, and Assignment Operators 构造/析构/赋值运算
05: Know what functions C++ silently writes and calls
了解C++默默编写并调用哪些函数