Effective C++ 笔记(1)
第一部分: 让自己习惯C++
条款01:视C++为一个语言联邦
请记住
-C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。
条款02,03:尽量以const, enum, inline替换#define,尽可能使用const
对于指针变量的const
对于指针变量的const, 根据const和星号的相对位置,有不同的行为
左定值,右定向
const对于迭代器
//iter的作用像个T* const
const std::vector<int>::iterator iter = vec.begin();
//cIter的作用像个const T*
std::vector<int>::const_iterator cIter = vec.begin();
const与函数声明
class Rational (...);
const Rational operator *(const Rational& lhs, const Rational& rhs);
Rational之前的const表示函数返回一个常量值,用以避免这样的暴行
(a*b)=c;//如果没有const,不报错,有const报错
const成员函数
\(bitwise\) \(const\)声明
在成员函数的括号后面加const,告诉编译器我不会修改这个对象的任何成员变量。
然而有些成员函数不具有const性质,却能通过bitwise测试
class CTextBlock {
public:
...
char &operator [](std::size_t position) const {//bitwise const声明
return pText[position];
}
private:
char *pText;
}
通过了bitwise const 测试,却允许发生这种事
const CTextBlock cctb("Hello");
char *pc = &cctb[0];
*pc = 'J';
\(logical\) \(const\)
一个const成员函数可以修改对象内的某些bits,只要客户端侦测不出来
mutable前缀修饰的变量可以在const成员函数中被修改
class CTextBlock{
public:
...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
//logical const
//虽然有修改,但是修改的是mutable变量,所以通过bitwise测试
std::size_t CTexeBlock::length() const {
if(!lengthIsValid) {
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
在const和non-const中避免重复
对于 non-const 版本的函数可以采取调用 const 版本的函数的方法来实现。(避免代码重复)
class TextBlock {
public:
const char &operator[](int i) const { return text[i]; }
char &operator[](int i) { return const_cast<char &>(static_cast<const TextBlock &>(*this)[i]); }
TextBlock(std::string s) : text(s) {}
private:
std::string text;
};
这里使用的const_cast和static_cast类型转换。
值得注意的是,反向做法是不应该的。const 成员函数承诺绝不改变其对象的逻辑状态,但是 non-const 成员函数没有承诺,在 const 成员函数内调用 non-const 成员函数是有风险的。
请记住
- 将某些东西声明为 const 可帮助编译器侦测出错误用法。const 可被施加于任何作用域的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施 bitwise constness,但编写程序时应该使用“概念上的常量性” (conceptual constness)。
- 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复。
条款04:确定对象被使用前已被初始化
构造函数
对于内置类型,需要自己手动完成初始化
对于内置类型以外的,初始化责任落在构造函数中
值得注意的是,初始化时使用列表初始化替换赋值操作可以提高效率
因为在大括号内赋值的版本,非内置类型会首先调用default构造函数再赋值(内置类型赋值和初始化代价相同)
而列表初始化则避免了default初始化(如果成员变量在"成员初始列"中未被指定初值,则会自动调用default)
ABEntry::ABEntry()
:theName(), //调用theName的default构造函数
theAddress(),
thePhones(),
numTimesConsulted(0)//内置类型,必须显式初始化
{}
即使成员变量是内置类型,有时也一定要使用初值列,当成员变量是const或者reference,它们一定需要初值,不能被赋值
最简单的方法就是总是使用成员初始列
成员初始化次序
C++有着固定的"成员初始化次序",即使成员在成员初始列中的出现顺序可以有很多种。
在成员初始列中条列各个成员时,为了避免错误,最好总以声明次序为次序。
不同编译单元内"non-local static对象"的初始化次序
问题是:如果某编译单元内的non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能还未被初始化,因为C++中对于"定义于不同编译单元内的non-local static对象"的初始化次序并无明确定义。
解决方案:将每个non-local static对象搬到自己的专属函数中(该对象在函数内被声明为static),这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。
这个手法的基础在于:C++保证,函数内的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;
}
这样的方式仍在多线程中带有不确定性,任何一种non-const static对象,不论local或是non-local,在多线程环境下"等待某事发生"都会有麻烦。
处理这个麻烦的一种做法是:在程序的单线程启动阶段手动调用所有的reference-retruning函数,这可消除与初始化有关的"竞速形势"。
请记住
- 为内置型对象进行手工初始化,因为 C++ 不保证初始化它们。
- 构造函数最好使用成员初值列 (member initialization list),而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和他们在 class 中声明次序相同。
- 为免除“跨编译单元之初始化次序”问题,请以 local static 对象替换 non-local static 对象。