Exceptional C++ 读书笔记
Item 1 Iterator:
(1): 注意当前迭代器是否有效,如果无效则解引用产生程序错误;
(2):注意当前迭代器生命期,某些容器经过某些操作后将重新分配内部存储空间,则当前迭代器无效;
(3) : 有效范围, 类似find(first, last, value)时, 迭代器first 必须在last之前,必须保证指向同一个容器;
(4): 有效的内部操作;
Item 2, 3: Case-Insensitive String
(1) 1 关于std::string: std::string实际是
template<class charT, class traits = char_traits<charT>, class Allocator = allocator<charT> >
class basic_string;
typedef basic_string<char> string;
要实现case-insensitive string需要改变string的compare方式,因为char_traits<char>类提供compare等方式,所以
就需要对char_traits<char>作相应的变动;
char_traits::eq()和lt()提供相等和小于的对比方式,compare()和find()提供对比和搜索字符串序列的功能;更改这四个成员函数 就可以实现case-insensitive string;
(2)关于实际应用中,最好选用一个单一的函数去接受两个相应的std::string对象,而非用更改traits的方法来实现一个ci_string;
例如 bool CaseInsensitiveString(std::string &a, std::string &b)
(3)类似ci_char_traits : public char_traits<char>,
用于变换traits或者iterator功能等情形,而非当作传统的OO方式来使用时,可以当作合理的派生,不必考虑virtual
destructor等;
Item 4, 5 Max Reusable Generic Containers
(1) template constructor永远不会是一个copy constructor, 因为永远存在一个最适合的copy
constructor, 因此overload resolution决不会将template constructor 当作copy
constructor
(2) 善于使用模板成员函数
Item 6, 7 Temporary Objects
(1) a: 用传递const T& 代替传值;
b: 用预先创建的对象代替不需要的重复创建的对象,类似(container.end()每次被调用将返回一个临时的对象,这是不必要的);
c: 用++T 代替 T++ (因为T++ 一般先制造一个临时对象,然后更新本身,最后返回临时对象);
d: 用++T来实现T++;
e: 避免做无用的隐式转换;
(2) 注意对象生命期,绝不准让函数返回一个函数内部的auto变量的指针或者引用;
(3) 多用标准库的代码;
Item 8 -- 17 Writing Exception-Safe Code
(1)当一个函数不能处理内部的异常时,应当将它抛给函数调用者去处理;
(2) 当异常出现时,请让资源正确的被释放,数据处于一个稳定的状态;
(3)绝不能让从destructor或者被重载的delete中抛出,写析构函数和资源分配函数时应当写上throw();
(4)先将所有可能抛出异常的操作和安全的的操作分离出来,当可能抛出异常的操作执行成功时,再用no throw操作改变对象状态;
(5)让每一个代码片段(类,模块,函数)负责单一的任务,类似STL stack中的pop和top是分离的
(6): exception-safe基本准则:
A: 基本安全: 当异常抛出时不产生资源泄漏;
B: 健壮的安全性:如果某个操作因为异常的抛出而终止,则对象当前状态不应改变;
C: 不抛出异常: 约定这个函数决不抛出异常;
(7): 将资源管理单独的封装成一个类类似书上的例子 StackImpl封装了内存管理功能
(8): 多用聚合,少用继承(包括这种聚合 pivate继承 -- 因为这将导致在constructor中无法控制base class
(9): 用资源申请既初始化来隔离类的实现和资源的管理与所有权
(10): 什么时候应当用private继承代替containment?
a: 需要存取某个类的protected成员
b: 需要覆写虚函数
c: base 对象需要在其他类对象建构之前建构
(11): 析构函数中决不允许抛出异常:
a: 对象本身所申请的的资源不能完全释放;
b:考虑 T t[20];的情况;析构某个T时抛出异常会产生未定义情况
Item 18, 19 Code Complexity
(1): 提供强异常安全需要损失一定程度的性能;
(2): 如果一个函数存在几个不相关的副作用,那么它不可能实现异常安全, 否则应当把这几个函数分割为不同的函数
(3): 不是所有的函数都需要强异常安全;
Item 20 Class Mechanics
(1): 设计时首先考虑标准库是否存在相关的类
(2): 用explicit constructor代替隐式转换
(3): 如果提供一个常规的operator op(类似+),那么应该同样提供一个operator op=(+=),
operator op(类似的 + - * /)不应该改变对象值, 它应当返回一个 + 操作后的临时对象;
(4): 多用a op= b 代替 a = a op b; 一般 const T operator op(T, T)产生一个临时变量;
(5) 一元运算符是成员函数
=,(),[]和->必须是成员函数
+=,-=,/=,*=(等等)是成员函数
所有其它的二元运算符是非成员函数
(6) ++T 应当返回一个引用, T++应当返回一个const T,以防止 T++++这种情况发生;T++应当由++T来实现;
(7) 避免使用前导下划线来命名,前导下划线是留给编译器使用的
Item 21 override 虚函数
(1): 除非确定某个类不被派生,否则都应当提供一个虚函数;
(2): 关于 overload, override和 hide
a: overload意思是提供一个相同名称但是不同参数的函数, 编译时由编译器决定哪个是最匹配的函数
b: override意思是在派生类提供一个相同名称且相同参数不同实现的虚函数,动态调用时将从派生类指针调用这个函数
c: hide 某个函数的意思是有一个函数在某个范围内(派生类,嵌套类,名称空间等),隐藏同名函数
(3) 当派生类提供一个和基类的某个成员函数同名的函数时,如果你不想隐藏基类的同名函数,请在派生类声明基类的同名函数,
例如: using Base::f;
(4) 永远不要在派生类override virtual function中改变默认参数;
Item 22, 23 Class Relationships
(1): 什么时候不应该选用public 继承
a: 如果某个类为提供虚函数,这个类本身不打算实施多态;
b: 如果某个类未提供protected 成员
c: 类型为不是IS-A, WORKS-LIKE-A or USABLE-AS-A;
d: 派生类只用了基类的public 成员;
(2): 只有当需要存取某个类protected 成员或者override 虚函数的时候才应该选用private继承;不是IS-A, WORKS-LIKE-A or
USABLE-AS-A的时候决不应当用public 继承;
(3): 避免提供public virtual function, 使用template method 设计模式代替;从算法中提取出那些每次都要进行的步骤,将其抽象出 来,只把一些因地制宜的细节通过virtual function 留给派生类来实现。
(4): 应用compiler-firewall 方法隐藏实现细节,使用一个不透明的指针(指向一个声明但未定义的类
(struct xxxImpl; xxxImpl *pimpl_)来存private 成员,包括状态变量和成员函数);
例如: clas Map { private: struct MapImpl; MapImpl *pimpl_; };
Item 24 Uses and Abuses of Inheritance
(1): 多用聚合,(containment, composition, layering HAS-A, delegation),当关系模型为 IS-IMPLEMENTED-IN-TERMS-OF时,请多考虑聚合,最好不用继承;
(2): 非public 继承的条件:
a: 当需要override virtual function时候;
b: 需要存取基类protected成员时候
c: 需要保证初始化顺序的时候(基类必须建构在派生类之前,析构在派生类之后等情况);
d: 需要某种多态的访问控制时;
(3): 用public继承时应当避免的问题:
a: 如果非public继承可以做,那么决不要用public继承;绝不能用public继承表述一个依据什么实现的关系模型;
b: 绝不能用public继承表述一个 IS-ALMOST-A关系模型(类似正方形是一个矩形等关系模型);
(4): 应当总是先确定 public继承关系模型为 IS-A或者WORKS-LIKE-A
Item 25 Object-Oriented Programming
略;
Item 26-28 Minimizing Compile-Time Dependencies
(1): 决不#include无用的头文件;
(2): 当前向声明某个stream时候用<iosfwd>足够了;
(3): 如果composition足够的话,决不能用继承;
Item 29 Compilation Firewalls
(1): 放置所有的非virtual函数 private member 到Pimpl类中去; 客户程序员无论如何也不应当看到这些private成员;
Pimpl类中某些函数可能需要类中某些成员的支持,所有需要一个back pointer指向类本身,back pointer通常命名为self_
Item 30 The "Fast Pimpl" Idiom
(1):绝不能放置对象到一个buffer里面,例如
char buffer[100];
new (&buffer[0]) T;
a: 不能保证对齐规则;
b: 脆弱
c: 难于维护;
Item 31 - 34 Name Lookup and Interface Principle
(1): 对于一个class X,任何提及并且含有X(例如来自同一头文件或者名称空间)的函数(括成员函数,static函数和自由函数),逻辑上都属于class X的一部分,因为它们是X接口的一部分
任何class X的成员函数都一定提及X;
任何class X的成员函数都一定含有X(隐式的this);
例如:
class X{...};
ostream& operator<<(ostream&, const X&)//肯定提及X;
如果operator<<含有X ,例如在同一个头文件或者名称空间,那么它逻辑上属于X的一部分,因为他建构了X的一部分接口;
否则反之;
(2): 关于 supplied with;
struct _iobuf{...};
typedef struct _iobuf FILE;
FILE* fopen(const char *filename, const char* mode);
int fclose(FILE* stream);
int fseek(FILE* stream, long offset, int origin);
......
这些函数都是非成员,但是它们确实是 FILE接口的一部分;(part of the interface of FILE);
class File
{
public:
int fseek(long offset, int origin);
......
};
int fseek(FILE* stream, long offset, int origin);
例如上例: int fseek(FILE* stream, long offset, int origin);
提及并且同File都来自于头一个头文件或者名称空间,此函数一样属于File接口的一部分;
(3):如果在某个作用域内调用了一个接受另一个作用域内类型参数的函数,那么编译器不止在当前作拥域内查找该函数,同时也会到接受的的 类型所在的作用域去查找该函数,而且不用特别声明;类似
void f()
{
std:string hello = "hello world";
std::cout << hello; //正确调用std::operator<<;
}
如果没有Koenig
lookup,则编译器无法查找到operator<<是哪一个我们想要的;(这个函数封装在std::string内,是string接口
的 一部分);如果希望正确调用的operator<<,我们需要 std::operator<<(std::cout,
hello);
所以,如果在同一个名称空间内定义了一个类和一个提及这个类的函数, 那么编译器将强制关联这个类和这个自由函数;
(4): namespace A { class X {}; void f(X x) {} }
namespace B { void f(A::X x) {} void g(A:: xx) { f(xx); } }
这将导致B::g出现二义性而无法编译;原因在于g涉及A::X,所以编译时编译器同样将名称空间A内的函数也考虑进来,此时出现两个参 数为A::X的函数f
类的成员函数将被优先考虑,例如
namespace A { class X {}; void f(X x) {} }
class B
{
void f(A::X x) {}
void g(A:: xx) { f(xx); } //此时不会出现二义性问题;
};
如果编译器发现一个类成员函数为f(A::X),则不再使用Koenig lookup去搜索自由函数;
(5):结论为名称空间不像想象的那样完全隔离函数声明,它只能属于部分独立;
(6): 根据Interface Principle,如果一个函数提及了两个类,class X和class Y, 并且此函数同X包含在同一个名称空间或者头文件之中,那么,首先,此函数逻辑上是X的界面接口的一部分;由于此函数依赖Y,因此,X同样依赖于Y;
(7): 如果A,B是类,f(A,B)是自由函数, 那么
a: 如果A和f在同一头文件或名称空间内,那么f逻辑上是A的一部分,所以,A也依赖于B;
b: 如果A,B和f在同一头文件或名称空间内,那么f逻辑上是A的一部分也是B的一部分,故此,A,B互相依赖;
如果 ,A,B是类,A::g(B)是一个A的成员函数,那么:
a: 因为A::g(B),所以,A,依赖于B;
b:如果A,B被声明在一起(同一个头文件或名称空间),那么A,B互相依赖,A::g(B)逻辑上同样属于B的一部分;
实际上,上述情况的实际含义是,类型A,B通常将被放在一起使用,,如果修改了一个,那么将影响另一个;
(8):关于名称隐藏:
struct B
{
int f(int);
int f(double);
int g(int);
};
struct D : public B
{
private:
int g(std::string, bool);
};
D d;
int i;
d.f(i) //调用B::f(int);
d.g(i); //错误,提示应当提供两个参数;
实际上,编译器查找匹配函数名称时候,先查找当前范围内的名称匹配函数,例如这里是D,然后列出一个编译器所能找到的函 数名称 相同的函数的列表,但
是并不考虑存取权限和所携带参数是否正确,如果它一个名称匹配的函数都没找到的话,那么将向外一层 继续查找,如果编译器找到一个或者多个候选的函数,
编译器将停止查找,然后提交函数的重载决议;
两种解决方法是
a: d.B::g(i);//编译器自然会考虑B的范围;
b:
struct D : public B
{
using B::g;
private:
int g(std::string, bool);
};
(9): 使用名称空间,如果写一个类在名称空间N中,那么请将所有的协助函数和操作符都都写在N中;
Item 35, 36 Memory Management
(1):
[常量数据(const data)区:]
常量数据区存储字符串等在编译期间就能确定的值。类对象不能存在于这个区域中。在程
序的整个生存周期内,区域中的数据都是可 用的。区域内所有的数据都是只读的,任何企图修改本区域数据的行为都会造成无法预料的后果。之所以会如此,是
因为在实际的实现 当中,即使是最底层的内部存储格式也受制于所实现的特定的优化方案。例如,一种编译器完全可以把字符串存放在几个重叠的对象里 面
——只要实现者愿意的话。
[栈(stack)区:]
栈区存储自动变量(automatic
variables)。一般来说,栈区的分配操作要比动态存储区(比如堆(heap)或者自由存储区(free
store))快得多,这是因为栈区的分配只涉及到一个指针的递增,而动态存储区的分配涉及到较为复杂的管理机制。栈区中,内存一 旦被分配,对象
就立即被构造好了;对象一旦被销毁,分配的内存也立即被收回(译注:这里作者用了“去配(deallocate)”一词, 鄙人一律翻译为“回收”)。
因此,在栈区中,程序员没有办法直接操纵那些已经被分配但还没有被初始化的栈空间(当然,那些通过 使用显式(explicit)析构函数
(destructor)和new运算符而故意这么做的情况不算在内)。
[自由存储区(free store):]
自由存储区(free
store)是C++两个动态内存区域之一,使用new和delete来予以分配和释放(freed)。在自由存储区(free
store)中,对象的生存周期可以比存放它的内存区的生存周期短;这也就是说,我们可以获得一片内存区而不用马上对其进行初始 化;同时,在对
象被销毁之后,也不用马上收回其占用的内存区。在对象被销毁而其占用的内存区还未被收回的这段时间内,我们可以 通过void*型的指针访问这片区域,
但是其原始对象的非静态成员以及成员函数(即使我们知道了它们的地址)都不能被访问或者操 纵。
[堆(heap)区:]
堆(heap)区是另一个动态存储区域,使用malloc、free以及一些相关变量来进行分配和回收。要注意,虽然在特定的编译器里缺省的 全局运
算符new和delete也许会按照malloc和free的方式来被实现,但是堆(heap)与自由存储区(free
store)是不同的——在某 一个区域内被分配的内存不可能在另一个区域内被安全的回收。堆(heap)中被分配的内存一般用于存放在使用new的构
造过程中和显 式(explicit)的析构过程中涉及到的类对象。堆中对象的生存周期与自由存储区(free store)中的类似。
[全局/静态区(Global/Static):]
全局的或静态的变量和对象所占用的内存区域在程序启动(startup)的时候才被分配,而且可能直到程序开始执行的时候才被初始 化。比如,函数
中的静态变量就是在程序第一次执行到定义该变量的代码时才被初始化的。对那些跨越了翻译单元(translation
unit)的全局变量进行初始化操作的顺序是没有被明确定义的,因而需要特别注意管理全局对象(包括静态类对象)之间的依赖关系。 最后,和前面
讲的一样,全局/静态区(Global/Static)中没有被初始化的对象存储区域可以通过void*来被访问和操纵,但是只要是 在对象真正的生存
周期之外,非静态成员和成员函数是无法被使用或者引用的。
[关于“堆(heap)vs.自由存储区(free store)”]:
本条款中我们将堆(heap)和自由存储区(free store)区分开来,是因为在C++标准草案中,关于这两种区域是否有联系的问题一直 很谨慎的没有予以详细说明。比如当内存在通过delete运算符进行回收时,18.4.1.1中说道:
It
is unspecified under what conditions part or all of such reclaimed
storage is allocated by a subsequent call to operator new or any of
calloc, malloc, or realloc, declared in <cstdlib>.
[关于在何种情况下,这种内存区域的部分或全部才会通过后续的对new(或者是在<cstdlib>里声明的calloc,malloc, realloc中的任何一个)的调用来被分配的问题,在此不作详细规定,不予详述。]
同样,在一个特定的实现中,到底new和delete是按照malloc和free来实现的,或者反过来malloc和free是按照new和
delete来实现 的,这也没有定论。简单地说吧,这两种内存区域运作方式不一样,访问方式也不一样——所以嘛,当然应该被当成不一样的两个东西
来使用了!
Item 37: AUTO_PTR
略
Item 38 Object Identity
(1): 千万别写一个依赖于检测自身赋值达到正确操作的operator=(),一个拷贝赋值操作符应当使用create-a-temporary-and-swap
方法来自动维持强异常安全和安全的自身赋值;
(2): 自身赋值是个可行的优化去避免无用的工作;
Item 39 Automatic Conversions
(1): 避免使用隐式转换操作符:
a: 隐式转换会影响重载决议;
b: 隐式转换会让错误的代码安静的编译通过;
(2): C++标准保证「对指向同一个对象的多个指针的比较之结果必须是“相等(equal)” 」,但却并不保证「对指向不同对象的多个指针 的比较之结果必须是“不相等(unequal)”」。
Item 40,41 Object Lifetimes
(1): void f()
{
T t(1);
T& rt = t;
//#1 do something;
t.~T();
new (&t) T(2);
//#2 do something;
};
a: #2是合法的;
b: #2是不安全的;
请总是尝试写异常安全级别的代码, 总是尝试正确的组织代码让资源正确的申请和释放当异常抛出时;
(2): 避免C++ 语言级别定义不太明确的技巧,应该多用清晰简单的编程技巧;
(3): 用同一个成员函数完成两种拷贝操作(拷贝构造和拷贝赋值)的注意是很好的:这意味着我们只需在一个地方编写和维护操作代码。本条 款问题中的惯用法只不过是选择了错误的函数来做这件事。如此而已。
其实,惯用法应该是反过来实现的:拷贝构造操作应该以拷贝赋值操作来实现,不是反过来实现。例如:
T::T( const T& other ) {
/* T:: */ operator=( other );
}
T& T::operator=( const T& other ) {
// 真正的工作在这里进行
// (大概可以在异常安全的状态下完成,但现在
// 其可以抛出异常,却不会像原来那样产生什么不良影响
return *this;
}
这段代码拥有原惯用法的所有益处,却不存在任何原惯用法中存在的问题。[注5] 为了代码的美观,你或许还要编写一个常见的私有辅 助函数,利用其做真正的工作;但这也是一样的,无甚区别:
T::T( const T& other ) {
do_copy( other );
}
T& T::operator=( const T& other ) {
do_copy( other );
return *this;
}
T& T::do_copy( const T& other ) {
// 真正的工作在这里进行
// (大概可以在异常安全的状态下完成,但现在
// 其可以抛出异常,却不会像原来那样产生什么不良影响
}
如果需要的话,请编写一个私有函数来使拷贝操作和拷贝赋值共享代码;千万不要利用「使用显式的析构函数并且后跟一个
placement
new」的方法来达到「以拷贝构造操作实现拷贝赋值操作」这样的目的,即使这个所谓的技巧会每隔三个月就在新闻组中出现几次。 (也就是说,决不要
编写如下的代码:)
T& T::operator=( const T& other )
{
if( this != &other)
{
this->~T(); // 有害!
new (this) T( other ); // 有害!
}
return *this;
}
Item 42: Variable Initialization -- Or is it?
(1): 关于 T u; T t = u;
这时,编译器总会是调用 copy-construct而不是调用默认constructor之后调用operator=(),这只是一个从C继承来的语法, 并没有赋值操作
(2): 用 T t(u);代替 T t = u;
Item 43 Const-Correctiness;
(1): 避免在某个函数参数使用 const by value
(2): 如果是非内部类型的return by value,那么优先考虑返回一个const value;
(3): 如果某个类数据成员的改变并不影响到类状态的改变,例如class X { private: char buffer[1024]; ...};
那么当某函数只改变这个类数据成员,而不改变其他成员时,也应当把这个函数为const ,并将这个被改变但无关类状态的成员声明为
mutable;
Item 44 Casts
(1): 请优先使用新转型风格cast;
(2): 避免用const_cast消除const ,请优先使用mutable;
(3): 避免动态的向下转型;
Item 45: BOOL
略
Item 46 Forwarding Functions;
(1): 多考虑传递对象的const &;
(2): 避免语言的隐晦角落,多用简单的技巧;
(3): 如果不认为性能上绝对需要,请避免inline或者内部优化;
Item 47 Control Flow
(1): 避免使用全局或者静态变量;如果必须如此,那么请注意它们的初始化顺序(因为多个头文件内引入的静态变量的声明顺寻并不是标准定 义的);
(2): 将基类的constructor在初始化器列表中的顺序和类声明的顺序是一样的,基类的初始化顺序是依据类定义时的顺序的;
(3): 将类的数据成员的初始化顺序与初始化器列表中声明的顺序是一样的,同样依据类定义;
(4): 努力去实现代码的异常级安全;
(5): 绝不能写依赖函数参数赋值顺序的代码;