Effective C++ 笔记
Sec0 Introduction
- 本书的目的:
如何有效运用C++,使软件易理解、易维护、可移植、可扩充、高效、并有预期行为 - 提出的忠告分两类:
- 一般性的设计策略,带有具体细节的特定语言特性
- 如何在两个不同做法中择一完成某项任务?
- inheritance还是templates?
- public还是private?
- private继承还是composition?
- 选择member函数还是non-member函数?
- 选择pass-by-value还是pass-by-reference?
- 更多的细节:
- assignment操作符的适当返回类型
- 何时该令析构函数为virtual?
- 当operator new无法找到足够内存该怎么办?
- 一般性的设计策略,带有具体细节的特定语言特性
- 阅读本书的方式:
- 从感兴趣的items开始
Sec1 Accustoming Yourself to C++
Item01: View C++ as a federation of languages
-
现在的C++: Multiparadigm programming language
一个同时支持procedural, object-oriented, functional, generic, metaprogramming的语言 -
如何理解这样一个语言?
将C++视为一个由相关语言组成的联邦而非单一语言。在其某个次语言(sublanguage)中,各种守则与通例都倾向于简单、直观易懂并且容易记住。
主要的次语言有4个:- C
blocks、statements、preprocessor、built-in data types、arrays、pointers等统统来自C - Object-Oriented C++
即C with Classes 所诉求的:classes(析构函数和构造函数)、encapsulation、inheritance、polymorphism、virtual函数(动态绑定)等等。 - Template C++
即C++的泛型编程(generic programming) 部分。带来了崭新的编程范型(programming paradigm),也就是所谓的template metaprogramming(TMP)。 - STL
template程序库。定义了containers、iterators、algorithms和function objects。
- C
-
Tips:
C++高效编程守则视状况而变化,取决于你使用C++的哪个部分。
Iterm02: Prefer consts, and inlines to #defines
-
使用常量来替换宏定义
-
使用
#define
的坏处:#define ASPECT_RATIO 1.653
- 可能编译器开始处理源码之前就被预处理器移走了。于是记号名称
ASPECT_RATIO
有可能没有进入记号表(symbol table) - 所以运用此常量但是获得编译错误信息的时候,难以定位。
- 可能编译器开始处理源码之前就被预处理器移走了。于是记号名称
-
解决之道:用常量来替换宏
const double AspectRatio = 1.653;
- 可以进入记号表(symbol table)内。
-
-
替换的两种特殊情况:
-
定义常量指针(constant pointers)
要在头文件定义一个常量cahr*-based字符串。必须写 const两次:
const char* const authorName = "Scott Meyers";
其实string对象更合适
const std::string authorName("Scott Meyers");
-
class专属常量:
为了将常量的作用域(scope)限制于class内,必须让它成为class的一个成员(member):而为确保此常量最多只有一份实体,你必须让它称为一个static成员:class GamePlayer { private: static const int NumTurns = 5; // 常量声明式 int scores[NumTurns]; ... };
-
注1:
变量定义:用于为变量分配存储空间,还可为变量指定初始值。程序中,变量有且仅有一个定义。
变量声明:用于向程序表明变量的类型和名字。
定义也是声明:当定义变量时我们声明了它的类型和名字。
extern关键字:通过使用extern关键字声明变量名而不定义它。
-
注2:
1.定义也是声明,extern声明不是定义,即不分配存储空间。extern告诉编译器变量在其他地方定义了。
2.如果声明有初始化式,就被当作定义,即使前面加了extern。只有当extern声明位于函数外部时,才可以被初始化。
例如:extern double pi=3.1416; //定义
3.函数的声明和定义区别比较简单,带有{ }的就是定义,否则就是声明。
4.除非有extern关键字,否则都是变量的定义。
-
注3:程序设计风格:
1. 不要把变量定义放入.h文件,这样容易导致重复定义错误。
2. 尽量使用static关键字把变量定义限制于该源文件作用域,除非变量被设计成全局的。
3. 可以在头文件中声明一个变量,在用的时候包含这个头文件就声明了这个变量。
只要不取地址,可以声明并使用它们而无须提供定义式。
但如果取某个class专属常量的地址,要看到定义式的话,需要这么写:const int GamePlayer::NumTurns; // 定义
因为class常量在声明时获得初值。因此定义时不可以再设初值。
-
-
也可以用enum来代替const或者define。但是enum行为上更像一个define
-
-
宏作为函数需要注意的地方:
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) int a = 5, b = 0; CALL_WITH_MAX(++a, b); // 递增两次 CALL_WITH_MAX(++a, b+10); //递增一次
所以建议用template inline函数:
template<typename T> inline void callWithMax(const T& a, const T& b) { f(a > b ? a : b); }
这是一个函数,所以遵守作用域和访问规则。】
-
Tips:
- 对于单纯常量,最好以const对象或者enums替换
#defines
- 对于形似函数的宏(macros),最好改用inline函数替换
#defines
- 对于单纯常量,最好以const对象或者enums替换
Item03: Use const whenever possible
-
const的一些知识点:
const出现在星号左边,表示被指物是常量。星号右边则表示指针自身为常量。- 对应到迭代器:
因为迭代器其实就是像T*指针。所以声明迭代器为const就像声明指针为const一样,表示指针不能变。但是想要迭代器指向的数据不变,就得用const_iteraotr
- 对应到迭代器:
-
const面对函数声明时的应用:
- 令函数返回一个常量值
避免无意义的赋值动作
- 令函数返回一个常量值
-
const成员函数:
是为了确认该成员函数可作用于const对象身上。- 它们使class接口比较容易被理解
- 操作const对象成为可能
一个小知识点:两个成员函数如果只是常量性的不同,可以被重载!
TextBlock tb("Hello"); std::cout << tb[0]; // 调用non-const TextBlock::operator[] const TextBlock ctb("Hello"); std::cout << ctb[0]; // 调用const TextBlock::operator[]
mutable
关键字:
可以在const成员函数也可以更改成员变量!
-
在const和non-const成员函数中避免重复(常量性转移 casting away constness)
即:让non-const operator[]调用其const兄弟。可以避免代码重复!
class TextBlock { public: ...; const char& operator[](std::size_t position) const { ...; return text[position]; } char& operator[](std::size_t position) { return const_cast<char&>( static_cast<const TextBlock&>(*this) [position] ); } }
第一次转型:用
static_cast<const TextBlock&>
将*this转换。以可以调用const类型的[]
。第二次是从const operator[]
的返回类型值中移除const。- 注:不应该用const类型调用non-const类型!
-
总结:
- 将某些东西声明为const,可以帮助编译器侦测出错误用法,const可以被施加于作用域中的任何对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施bitwise constness,但写程序的时候应该使用概念上的常量性 conceptual constness。
- 注意实现non-const和const版本的代码重复!
Item04: Make sure that objects are initialized before they're used
-
array(来自C part of C++) 不保证其内容被初始化,但是vector(来自STL part of C++)就保证初始化
-
所以建议永远都使用初始化!
- 对于构造函数,确保将每个成员初始化。
而注意,在构造函数的{}里面进行赋值,并不是初始化!而是assignment!赋值!
C++中,对象的成员变量的初始化动作发生在进入构造函数本体之前。最佳写法是:使用所谓的member initialization list。成员初始列替换赋值动作。从而构造函数本体不需要进行任何动作。
- 对于构造函数,确保将每个成员初始化。
-
成员初始化次序:
base classes总是更早于derived classes被初始化的。
而class的成员变量总是以其声明次序被初始化。 -
不同编译单元内定义之 non-local static 对象的 初始化次序:
- 注:local static对象指的是在函数里面的static对象。其他的,global对象、定义于namespace作用域内的对象、在classes内、在file作用域内被声明为static的对象称为 local static对象。
non-local static对象,会在main()结束的时候调用析构函数自动销毁。
客户机和本机的non-local static的初始化顺序很难决定,所以建议:
- 将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些寒湖是返回一个reference指向它所含的对象,然后用户调用这些函数,而不涉及对象。即non-local static对象被local static对象替换了。
这其实是Singleton模式的一个常见实现手法。可以保证获得的reference的对象指向一个历经初始化的对象。
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; }
-
这种函数第一行定义并初始化一个local static对象,第二行返回它。挺适合inlining的。
但是在多线程下可能有麻烦!解决方法是,在单线程启动阶段(single-threaded startup portion) 手工调用所有reference-returning 函数。
- 注:local static对象指的是在函数里面的static对象。其他的,global对象、定义于namespace作用域内的对象、在classes内、在file作用域内被声明为static的对象称为 local static对象。
-
总结:
- 为内置型对象手工初始化
- 使用成员初值列。少在构造函数里卖弄使用赋值操作。注意次序!
- 用local-static对象替代non-local static对象。
Sec2 Constructors, Destructors, and Assignment Operators
Item05: Know what functions C++ silently writes and calls
-
default 构造函数和析构函数
编译器产生的构造函数是个non-virtual。除非这个class的base class自身声明有virtual析构函数。 -
如果在一个内含reference成员的class内支持赋值操作(assignment),必须自己定义copy assignment操作符!内含const成员也一样。
或者,如果某个base classes将copyassginment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。
-
总结:
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符和析构函数
但是要注意特殊情况。
Item06: Explicitly disallow the use of compiler-generated functions you do not want
-
所有编译器产出的函数都是public。为组织创建,我们得自行声明它们。所以我们可以将copy构造函数或者copy assignment函数声明为private。借此可以组织自动创建。但是这也不太安全!
-
更好的做法:
class HomeForSale { public: ...; private: ...; HomeForSale(const HomeForSale&); // 只有声明 HomeForSale& operator=(const HomeForSale&); };
这里就不需要写函数参数的名称了。反正也不会用。有了这样的定义,如果客户企图拷贝对象,编译器会报错,如果member函数或friend函数这么做,连接器会报错。
这里可以将连接器错误移动到编译器,只需要将这些声明移动到private里面就行。
-
-
为了阻止HomeForSale对象被拷贝,我们也可以将它继承Uncopyable:
class HomeForSale : private Uncopyable { ...; }
这里,class就不需要声明copy构造函数或者class assign操作符了。 相对更简单一些。
-
总结:
为驳回编译器自动生成的情况,可以将相应的成员函数声明为private并且不予实现。使用像Uncopyable 这样的base class也是一种做法