Effective C++读书笔记~01 让自己习惯C++
条款:01:视C++为一个语言联邦
View C++ as a federation of languages.
如何理解C++?
将C++视为一个由相关语言组成的联邦而非单一语言。主要的4个次语言(sublanguage):
- C, C++的基础
- Object-Oriented C++, C with Classes:构造函数、析构函数,封装,继承,多态,虚函数等。
- Template C++,C++泛型编程部分
- STL,template程序库,对容器、迭代器、算法、函数对象的规约有极佳的紧密配合与协调。
PS:C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。
[======]
条款02:尽量以const,enum,inline替换#define
Prefer consts, enums, and inlines to #defines.
另一种理解:“编译器替换预处理器”
const等类型可以有编译器进行检查,而#define对编译器是透明的,无法利用编译器检查。
两种替换#define特殊情况:
1)定义常量指针(constant pointers)
如果是用char *定义常量指针,用于替换#define,必须写const两次。
const char *const authorName = "Scott Meyers";
如果是用string(通常比char *要合适)
const std::string authorName("Scott Meyers");
2)class专属常量
为了将常量的作用域(scope)限制于class内,必须让它成为class的一个成员(member)。而为确保此常量至多只有一份实体,必须让它成为static成员:
class GamePlayer {
private:
static const int NumTurns = 5; // 常量声明式
int scores[NumTurns]; // 使用该常量
};
const int GamePlayer::NumTerns; // NumTerns定义
// 为什么没有赋值?
上面定义NumTerns时,为什么没赋值?
因为经常把定义的式子放到实现文件(.cpp, .cxx)中,由于class常量已经在声明时获得初值(5),因此定义时不可再设初值。
当然也可以把初值放在定义中,而声明中不指定初值。
PS:#define无法创建一个class专属常量,因为宏一旦被定义,在其后的编译过程中有效(除非被#undef)
enum hack
1)替换static const
如果编译器必须在编译器就知道数组大小,static const方式无法做到,可以改成使用enum
class GamePlayer {
private:
enum{ NumTurns = 5 }; // enum hack: NumTurns成为常量5的一个记号名称
int scores[NumTurns]; // 使用该常量
};
2)实用主义
很多代码用了它,必须认识。也是模板元编程的基础技术。
用template inline替换宏函数
可以获得宏带来的效率以及一般函数的所有可预料行为和类型安全(type safety)
比如,用template inline函数callWithMax,替换宏函数,以a,b中较大者调用f。
// 不安全的宏函数
// 以a和b的较大者调用f
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
// 兼顾安全与效率的template inline函数
template<typename T>
inline void callWithMax(cosnt T& a, const T& b)
{
f(a > b? a : b);
}
PS:
1)对于单纯常量,最好以const 对象或enum替换#define;
2)对于形似函数的宏(macro),最好改用inline函数替换#define;
[======]
条款03:尽可能使用const
Use const whenever possible.
顶层const:代表指针变量自身无法修改;
底层const:代表指针所指对象无法修改。
char* const p = "hello"; // 顶层const, const pointer, non-const data
const char *p = "hello"; // 底层const, non-const pointer, cosnt data
const与迭代器
迭代器的作用像T指针。
如果希望迭代器本身无法改变,而指向的内容可以改变,可以在迭代器前面加上const,即T const。
如果希望迭代器本身可以改变,而指向的内容不可改变,可以使用const_iterator,即const T*。
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin(); // <=> T* const
*iter = 10; // OK
++iter; // error: iter 是const
std::vector<int>::const_iterator cIter = vec.begin(); // <=> const T*
*cIter = 10; // error: *cIter是const
++cIter; // OK
const成员函数
const成员函数不允许更改对象的任何变量(static除外)。目的在于确认该成员函数可作用于const对象身上。const成员函数很重要,原因:
1)使class接口比较容易被理解。因为很容易得知,哪个函数可以改动对象内容,而哪个函数不行。
2)使“操作const对象”成为可能。对编写高效代码是关键:以pass by reference-to-const方式传递对象(条款20)。??
2个成员函数,如果只是常量性(constness)不同,可以被重载。non-const对象调用non-const function for non-const object, const 对象调用const function for const object。
class TextBlock
{
public:
TextBlock(const char* s) : text(s) {}
const char& operator[] (size_t position) const /* operator[] for const 对象 */
{
return text[position];
}
char& operator[] (size_t position) /* operator[] for non-const 对象 */
{
return text[position];
}
private:
string text;
};
non-const与const成员函数,在使用上的差异:
TextBlock tb("hello"); // non-const 对象
tb[0] = 'x'; // OK: 写一个non-const TextBlock
cout << tb[0] << endl; // OK: 调用non-const TextBlock::operator[]
const TextBlock ctb("hello"); // const 对象
ctb[0] = 'y'; // error: 写一个const TextBlock
cout << ctb[0] << endl; // OK: 调用const TextBlock::operator[]
注意:要修改string text,operator[] 返回值必须是引用类型(char &)或指针类型,不能是值。如果是值, 那么调用者修改的是tb[0]副本,而不是tb[0]本身。
比如,operator[] 返回char,下面句子无法通过编译:
tb[0] = 'x';
成员函数如果是const意味着什么?
这里有2个流行概念:bitwise constness(又称physical constness),logical constness。
1)bitwise constness
成员函数只有在不更改对象任何成员变量(static除外)时,才能说是const。i.e. 它不更改对象内的任何一个bit。编译器只需要寻找成员变量的赋值动作即可。bitwise constness是C++对常量性(constness)的定义,因此const成员函数不能更改对象内任何non-static成员变量。
但是,bitwise测试存在缺陷:如果类对象持有的是指针,bitwise测试能确保const成员函数不修改指针本身,但无法确保指针指向的内容不被修改。
class CTextBlock
{
public:
CTextBlock(const char* s) { pText = new char[strlen(s) + 1]; strcpy(pText, s); }
~CTextBlock() { delete[] pText; }
char& operator[] (size_t position) const /* bitwise const声明, 确保operator[] 不修改pText */
{
return pText[position];
}
...
private:
char *pText;
};
// 以下内容能通过编译器
// 可以通过operator[]返回的指针, 修改pText指向的内容
const CTextBlock cctb("Hello"); // 声明一个常量对象
char *pc = &cctb[0]; // 调用const operator[]取得一个指针, 指向cctb第0个数据
*pc = 'J'; // 更新了cctb[0]内容
2)logical constness
由于bitwise constness并不能完全避免在const成员函数内修改处理对象内的bits,logical constness主张认为一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。
例如,CTextBlock class有可能高速缓存(cache)文本区块的长度,以便应付查询:
class CTextBlock
{
public:
// ...
size_t length() const;
private:
char *pText;
size_t textLength; // 最近一次计算的文本区块长度
bool lengthIsValid; // 目前的长度是否有效
};
size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = strlen(pText); // 错误:不能在const成员函数内修改对象属性textLength和lengthIsValid
lengthIsValid = true;
}
return textLength;
}
// ...
上面length()的实现不是bitwise const,因为textLength和lengthIsValid都可能被修改,因此在用length()取得pText字符串最新长度时,需要重新计算、更新textLength和lengthIsValid的值。
而要在const成员函数内修改对象属性,可以用C++与const相关的摆动场:mutable(可变的)。mutable释放掉non-static成员变量的bitwise constness约束:
class CTextBlock
{
public:
// ...
size_t length() const;
private:
char *pText;
mutable size_t textLength; // mutable表示这些成员变量可能总是会被更改, 即使是const成员函数内
mutable bool lengthIsValid;
};
size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = strlen(pText); // OK
lengthIsValid = true; // OK
}
return textLength;
}
// ...
在const和non-const成员函数中避免重复
虽然上面的例子中,用mutable可以解决在const成员函数中修改对象属性,但不能解决代码重复的问题。比如,const 成员函数和non-const成员函数,operator[] 都可能有边界检验、日志记录数据访问、检验数据完整性这相同的3步。
class TextBlock
{
public:
TextBlock(const char* s) : text(s) {}
const char operator[] (size_t position) const
{
// 边界检验 (bounds checking)
// 日志记录数据访问(log access data)
// 检验数据完整性(verify data integrity)
return text[position];
}
char operator[] (size_t position)
{
// 边界检验 (bounds checking)
// 日志记录数据访问(log access data)
// 检验数据完整性(verify data integrity)
return text[position];
}
private:
string text;
};
能否把这些重复的代码,利用起来呢?编写一次,使用2次。
答案是可以的,可以将常量性转除(casting away constness):利用non-const函数调用cast函数,首先需要转型,然后去掉const修饰:
class TextBlock
{
public:
TextBlock(const char* s) : text(s) {}
const char& operator[] (size_t position) const
{
// 边界检验 (bounds checking)
// 日志记录数据访问(log access data)
// 检验数据完整性(verify data integrity)
return text[position];
}
char& operator[] (size_t position)
{
// 边界检验 (bounds checking)
// 日志记录数据访问(log access data)
// 检验数据完整性(verify data integrity)
return const_cast<char &>(static_cast<const TextBlock&>(*this)[position]);
}
private:
string text;
};
这里有2个转型动作:
1)让non-const operator[]调用其兄弟const operator[],通过static_cast将*this由原始类型TextBlock& 安全转型为const TextBlock&;
2)再从const operator[]的返回值中,通过const_cast移除const。该步当然也可以选择用C风格的强制类型转换,不过并不推荐;
为避免重复,为什么只能是令non-const版本调用const的operator[],而不能是const版本调用non-const的?
因为const成员函数承诺绝不改变其对象的逻辑状态(logical state),non-const成员函数没有这样的承诺。如果在const函数中调用了non-const函数,就是冒了这样的风险。而non-const函数中调用const函数却不会带来风险。
小结
- 将某些东西声明为const,可以帮助编译器检查出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数;
- 编译器强制实施bitwise constness(const成员函数要求不能修改任何对象属性),编写代码时应该使用“概念上的常量性”(conceptual constness)(用mutable在const成员函数修改对象属性);
- 当const和non-const成员函数有着实质等价实现时,可以令non-const版本调用const版本避免代码重复。
[======]
条款04:确定对象被使用前已先被初始化
Make sure thath objects are initialized before they're used.
对于内置型对象
内置类型对象x,在某些语境下保证被初始化为0,不过其他语境并不能保证为0。例如,
// 作为全局变量
int x; // 0
// 作为自动变量
void fun() {
int x; // x不能保证为0
}
// 作为类对象变量的成员
class Point {
int x, y;
};
Point p; // 全局变量p确保x为0
最佳解决办法:手工在使用对象之前现将其初始化。
int x = 0; // 对int手工初始化
const char* text = "A C-style string"; // 对指针手工初始化
double d;
std::cin >> d; // 以读取input stream方式完成初始化
对于类对象
使用构造函数(constructor):确保构造函数都将对象的每个成员初始化。
需要注意:在构造函数中使用member initialization list(成员初值列),才是初始化成员;而构造函数体内使用赋值操作,是赋值(assignment)而非初始化(initialization)。
class PhoneNumber
{
public:
string number;
};
class ABEntry
{
public:
// case1 构造函数体内对class成员进行赋值
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phone)
{
/* 这些都是赋值(assignment), 而非初始化 */
theName = name; // 如果是类对象, 赋值(=)操作会调用拷贝构造函数; 如果是基本类型, 则直接赋值
theAddress = address;
thePhone = phone;
numTiemsConsulted = 0;
}
// case2 使用member initialization list构造ABEntry对象
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phone)
: theName(name), // 调用theName的default构造函数, 从而构造成员对象
theAddress(address),
thePhone(phone),
numTimesConsulted(0)
{}
private:
string theName;
string theAddress;
list<PhoneNumber> thePhone;
int numTimesConsulted;
};
1)case1 构造函数体内对class成员进行赋值,实际上在进入构造函数体之前,会调用构造函数对各对象成员进行初始化。构造函数体内是赋值操作,而非初始化。
2)case2 是推荐的做法,在进入构造函数前,就进行了成员初始化。
因此,case1 相当于下面的代码,可以看出对成员相当于调用了2次赋值操作:
...
// case1 构造函数体内对class成员进行赋值
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phone)
: theName(), // 构造成员对象
theAddress(),
thePhone(),
numTimesConsulted(0)
{
/* 这些都是赋值(assignment), 而非初始化 */
theName = name; // 如果是类对象, 赋值(=)操作会调用拷贝构造函数; 如果是基本类型, 则直接赋值
theAddress = address;
thePhone = phone;
numTiemsConsulted = 0;
}
...
注意:如果成员过多,或者无需初值,可以在成员初值列中忽略该成员,这样它就没有初值。
关于成员初始化次序:
member initialization list中对象成员初始化次序,以防漏掉,或者导致检阅者迷惑,建议与声明的顺序保持一致。
不同编译单元内定义的non-local static对象的初始化次序
static对象,其寿命从被构造出来直到程序结束为止。因此stack和heap-based对象都不是static对象。static对象包括:global对象、定义于namespace作用域内的对象,在class内、函数内、file作用域内被声明为static的对象。函数内的static对象称为local static对象,其他static对象都称为non-local static对象。
编译单元(transitation unit):指产出单一目标文件(sigle object file)的那些源码。通常是单一源码文件(.c, .cpp, .cxx)加上所包含的头文件(#include files)。
问题:如果编译单元A内的某个non-local static对象的初始化动作,使用了另一个编译单元B内的某个non-local static对象,A初始化时,B可能没有被初始化。而C++对定义于不同编译单元内的non-local static对象的初始化次序,并没有明确的规定(事实上也很难做到)。
// FileSystem.h
class FileSystem {
public:
...
size_t numDisks() const;
...
};
extern FileSystem tfs; // 声明global 对象tfs, 属于non-local static对象
// Directory.h
class Directory {
public:
Director(params);
...
};
extern Directory tempDir;
// Directory.cpp
Director::Director(params)
{
...
size_t disks = tfs.numDisks();// 错误:使用tfs对象, 但C++无法保证位于编译单元FileSystem.o中的FileSystem tfs已经初始化完成
}
如何解决这个问题?
可以将B的non-local static对象通过函数包装起来,替换为local static对象,A只需要通过这个包装函数访问B的local static对象即可。类似于单例模式。
// FileSystem.h
class FileSystem { ... };
// reference-returning函数, 返回local static对象引用
FileSystem& tfs() // 用于替换tfs对象, 将non-local static对象tfs替换为local static对象, 并返回其reference
{
static FileSystem fs; // 这里会调用FileSystem构造函数, 确保fs初始化完成
return fs;
}
// Directory.h
class Directory { ... };
// Directory.cpp
Directory::Directory(params)
{
...
size_t disks = tfs().numDisks();
...
}
// reference-returning函数, 返回local static对象引用
Directory& tempDir() // 用于替换tempDir对象
{
static Directory td;
return td;
}
如果这种reference-returning函数,被频繁调用,还可以声明为inline函数。
缺点:
由于函数内含static对象(带有状态),在多线程系统中带有不确定性。解决办法是在程序单线程启动阶段,手工调用所有reference-returning函数,可消除与初始化有关的“竞争条件(race conditions)”。
注意:
绝对要避免这种尴尬状况:对象A初始化依赖于对象B,而对象B的初始化又要依赖于A。
小结
- 为内置型对象进行手工初始化,因为C++不保证初始化它们;
- 构造函数最好使用成员初值列表(member initialization list),而不要在构造函数本地内用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中声明的次序相同;
- 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-static对象;
[======]