effective_c++读书笔记

条款1:视C++为一个语言联邦

C++是一个庞大复杂的语言,它看上去更像是多个语言的联合,所以可以将它分为几个次语言

  • C
  • Object-Oriented C++
  • Template C++
  • STL

每个部分都有自己的规约,用什么规则取决于用哪部分。

条款2:尽量以const, enum, inline 替换 #define

#define 是在预处理阶段完成的,预处理器只是将define的部分以字符的形式替换掉代码的内容,所以在编译期间是看不到define的东西的,这对于调试很不好。
enum申明常量,很有意思。

条款3:尽可能使用const

声明类的时候,能用const的地方都尽量加上去。

条款4:确定对象被使用前已先被初始化

这条很明显,能避免很多错误。
全局对象的初始化,在主函数执行之前。
尽量在初始化列表中对所有成员进行初始化。
类中的const 和 reference 成员必须在初始化列表中进行初始化。
类中成员的初始化顺序就是声明的顺序,和初始化列表的顺序咩有关系。
C++中,存在全局区的数据(global, static)的初始化和C有点不一样。non-local(全局,类中)的对象在main函数之前初始化,但不同编译单元之间的顺序无法确定。local(函数内的)的对象在首次遇到该对象的定义式时进行初始化,之后在遇到直接跳过初始化。(通过在对象内存的前后加一个标记来表示)。
所以尽量避免使用non-local的全局对象,更好的方式是包装一个函数,使其变成local的,这样能保证它的初始化时间。
C中,存在全局区的数据都是在main函数之前初始化的。

条款5:了解C++默默编写并调用哪些函数

对象的成员变量是在构造函数执行前完成的,也就是通过初始化列表完成的,在函数体内的只能叫赋值,不是初始化。这也是为什么如果类中存在没有默认构造函数的成员时,一定要在初始化列表中完成初始化,const 成员也是一样的道理,因为 const 成员不能修改,所有在构造函数中赋值是非法的。另外,完成基类的构造也应该在初始化列表中。

编译器生成的4个默认函数都是 public 且 inline 的,而且只有在这些函数被需要(被调用)它们才会被编译器创建出来。也就是当你没有声明他们,且某个模块又调用了她们的时候。什么叫被需要才创建呢?也就是如果没有地方调用他们,他们是不存在的,只有调用了,才在调用的地方产生一个 inline,这也是为什么是 inline 的原因。

默认的析构函数是 non-virtual 的,除非这个 class 的 base class 自身申明有 virtual 析构函数,那么它会继承 base 的虚属性。

编译器并不是什么情况下都会产生默认的函数。特别是 copy assignment ,当类中含有引用成员或const 成员时,编译器拒绝产生 copy assignment。对于构造函数,如果类中的成员或基类没有默认的构造函数,当前类也不会产生默认构造函数。还有一点,对于在基类中被限定为 private 的函数,在派生类中都不会产生默认的对应函数,因为默认函数的要调用基类对应的函数。

条款6:若不想使用编译器自动生成的函数,就该明确拒绝

对于 copy constructor 和 copy assignment ,如果不想类被复制,可以将这两个函数声明为 private,且不需要实现,因为有申明就能通过编译,而找到具体的实现,是在链接阶段的事情,如果不调用,就不用去找它。当然,这种方法对于类别调用和 friend 会失效。

另外,还有一种方法,让当前类继承一个基类,这个基类是空类,只有两个private 的拷贝构造和赋值,这种方法对于内部调用和friend一样有效。因为编译器生成版的函数会调用基类对应的兄弟。

条款7:为多态基类声明virtual析构函数

当需要多态时,基类的析构函数必须为 virtual,如果不是,在多态情况下,用基类指针调用析构,调用的是基类的析构,只能释放基类那一部分的数据,派生类那一部分的成员都没有处理。

另一方面,如果一个类确定不用作多态,那就不要声明 virtual,节约空间。

纯虚析构也需要实现。其他的纯虚不实现,是因为永远不可能调用到(因为类不能实例化),但是析构会被派生类的析构调用。

条款8:别让异常逃离析构函数

从语法上,构造和析构都能抛异常,但析构会有问题,构造不会有问题。

创建对象分两步:分配内存和调用构造函数。若分配内存出错,默认会抛出bad_alloc异常;若在调用构造函数时抛出异常,会在 new 的过程中清理掉相应的内存。

析构抛出异常,会导致出错的地方以后的代码无法执行,也就是析构没有执行完,程序会直接跳到对应的异常处理程序的地方。解决的办法是在析构内部解决掉异常,要么终止程序,要么忽略异常。另一个办法是将可能出现异常的部分写成接口,让用户自己来处理,不要放在析构里面做。

条款9:绝不在构造和析构过程中调用 virtual 函数

这个问题体现在有继承关系的多态情况下。当一个派生类构造时,是先构造基类的部分,也就是先调用基类的构造函数,此时构造了基类的部分,它是一个基类,而不是派生类,包括虚表指针,所以在构造函数里面的虚函数执行的是基类的函数,而不可能是派生类的。如果是在一般的函数中调用,那么派生类的虚表指针指向的是自己的虚表,所以执行的是自己的函数。
同理,析构也是一样的,先析构子类的部分,当到基类时,对象已经变成一个基类,所以只能执行基类的函数。

条款10:令operator=返回一个reference to *this

这个好理解,为了连续的赋值。(当然,这条也适用于 +=-=*=等带有赋值性质的操作符)
另外,连续赋值的执行是从右向左的,赋值表达式的值等于左值。

条款11:在operator= 中处理“自我赋值”

主要是担心自己赋值给自己时,先把自己删除了,导致为定义的行为。

一个比较安全的写法是这样的:

1
2
3
4
5
6
7
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig=pb; // pb是类 Widget 的一个成员,指向Bitmap的一个指针
pb=new Bitmap(*rhs.pb); // 这个代码就是在删除前,将原来的保存起来,既
delete pOrig; // 保证了不会删除自己,也保证了 new 的异常安全
return *this;
}

当然,合理的写法还有很多,记住不要在赋值之前把自己给删除了。

条款12:复制对象时勿忘其每一个成分

写 copy constructor 和 copy assignment 时,确保复制“对象内的所有成员变量”及“所有base class 成员”,调用基类对应的兄弟。

资源,最常用的就是内存,其他的包括文件描述符,互斥锁,数据库连接,sockets。
为保证申请资源后,能合理释放,最好用对象来管理资源。现代C++不应该在代码中显式出现new, delete。


条款13:以对象管理资源

  • 获得资源后立即放进管理对象(资源获取即初始化-RAII)
  • 管理对象运用析构函数确保资源被释放

C++11提供了三个智能指针,unique_ptr, shared_ptr, weak_ptr。
unique只能通过移动语义来转移控制,不能有多个指针指向同一个对象。
shared_ptr使用引用计数,在内部通过一个计数类(代理模式)去记录有多少对象指向同一个资源,当计数为0时,删除资源。shared_ptr有一个陷阱:循环引用,自己指向自己。
weak_ptr是shared_ptr的一个弱实现,不能增加和减少计数,需要转换成shared_ptr才能用。

条件14:在资源管理类中小心coping 行为

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  • 普遍而常见的RAII class copying 行为是:抑制copying,施行引用计数。

条款15:在资源管理类中提供对原始资源的访问

因为在某些情况下,要使用原始接口,所以只能使用原始资源。

条款16:成对使用new和delete时要采取相同形式

删除数组和单一数据时要统一

条款17:以独立语句将newed对象置入智能指针

以独立语句将newed对象存储于智能指针内。如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。

让接口容易被正确使用,不容易被误用。

条款18:让接口容易被正确使用,不容易被误用

促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容。
阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。

这部分没实际经验没办法理解。

条款19:设计Class犹如设计type

无法理解

条款20:宁以pass-by-reference-to-const替换pass-by-value

引用的底层实现是指针。

用传引用替代传值,可以省去复制产生的开销,以及切割问题。
切割,就是传值的时候,将一个实参的派生类传给一个基类的形参,其实是用一个派生类构造了一个基类,在函数中得到的是一个基类,不会出现多态的效果(虚表指针是不在复制范围内的)。
示例:
错误的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Window {
public:
std::string name() const;
virtual void display() const;
};
class WindowWithScrollBars: public Window {
public:
virtual void display() const;
};

void printNameAndDisplay(Window w)
{
std::cout<<w.name();
w.display(); //这里调用的是基类的函数
}

正确的写法:

1
2
3
4
5
void printNameAndDisplay(const Window& w)
{
std::cout<<w.name();
w.display();
}

对于内置类型,STL的迭代器和函数对象,传值更好。

条款21:必须返回对象时,别妄想返回其reference

绝不要返回pointer或reference指向一个local stack对象(函数返回时就被析构),或返回reference指向一个heap-allocated对象(一旦没有用变量接这个引用,就会造成内存泄漏),或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象(对个对象是同一个对象)。

条款22:将成员变量声明为private

protected和public的封装性一样差。
封装性与改变一个属性需要改变的代码量成反比。

条款23:宁用non-member non-friend函数替换member函数

提高封装性,使性能单一。
个人认为,一个类如何设计接口,很多时候是凭一个程序员的直觉,真没有固定的定律。

条款24:若所有参数皆需类型转换,请为此采用non-member函数

如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
考虑运算符重载。

条款25:考虑写出一个不抛异常的swap函数

没看懂。

条款26:尽可能延后变量定义式的出现时间

直到要用的时候再定义,减少不必要的构造和析构。循环内的定义尽量写在循环内。

条款27:尽量少做转型动作

C++ 的四种转型:

  • const_cast:通常被用来将对象的常量性转除。它也是唯一有此能力的C++-style转型操作符。
    主要用于去掉 const,volatile

  • dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
    主要用于多态下的基类转向派生类,运行时进行类型检查

  • reinterpret_cast:意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int 转型为一个int 。
    二进制层面的转换,就是将二进制重新解释。

  • static_cast:用来强迫隐式转换,例如将non-const对象转为const对象,或将int 转为double等等。它也可以用来执行上述多种转换的反向转换,例如将void* 指针转为typed指针,将pointer-to-base 转为pointer-to-derived。
    功能类似于C 风格的转换,在兼容类型直接,不进行运行检查。

类型转换的时候会产生一些额外代码。特别是多态时,有多继承的时候,子类指针转成父类的时候,会有一个偏移。

尽量避免使用dynamic,效率太低。

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。
  • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码内。

条款28:避免返回handles指向对象内部成分

避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const。

条款29:为“异常安全”而努力是值得的

异常安全,
使用对象来管理资源
copy-and-swap

条款30:透彻了解inlining的里里外外

inline 可能会导致代码膨胀
inline不是强制命令,只是对编译器的申请
将函数定义在class内,就是隐式的申请为inline,这里friend也有同样的效果
inline函数通常一定要被置于头文件内,因为在编译过程要替换。
template通常也被置于头文件内,因为一旦被使用,编译器为了将它具现,需要知道它张什么样子
编译器会拒绝几种情况下的inline申请:函数内带有循环和递归、virtual函数(因为在运行期才能确定调用那个函数)
当你对一个已经inline的函数取地址时,编译器还会生成一个非inline的函数
为了C++的安全,编译器会在构造和析构里面加入很多代码,取决于具体的类,所以将构造申明为inline可能不是一个好的选择。
inline的缺点:增加代码量,无法调试,修改就需要重新编译整个项目

条款31:将文件间的编译依存关系降至最低

支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes
尽量用声明,而不是定义。

条款32:确定你的public继承塑模出is-a关系

以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味”is-a”(是一种)的关系。

“public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived clases身上,因为每一个derived class对象也都是一个base class对象。

条款33:避免遮掩继承而来的名称

  • derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
  • 为了让被遮掩的名称再见天日,可使用using声明式或转交函数。

C++的名称查找规则是从局部一层层往外找的。如果在派生类中有和基类同名的函数,那么会遮掩基类中所有这个名字函数的重载,无关参数,无关类型,无关virtual,只在名称在查找。
要想调用基类的函数,必须显式调用。

条款34:区分接口继承和实现继承

pure函数也可以提供定义,在派生类中被调用。其意义在于提供接口,同时提供一个默认实现。
pure virtual函数的继承只是继承了接口,在派生类中要实现,但是实现可以调用基类提供的默认实现(Base::virtualFun())。这点不同于virtual函数,virtual函数提供了接口,也提供了默认实现,允许派生类重写实现。
non-virtual的函数提供了一份强制实现,在派生类中不应该重写non-virtual函数。它表示所有派生类都支持的相同的实现。

条款35:考虑virtual函数以外的其他选择

当你为解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方案:

  • 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virutal成员函数包裹较低访问性的virtual函数。

  • 将virutal函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。

  • 以std::function成员变量替换virtual函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。

  • 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Straregy设计模式的传统实现手法。

条款36:绝不重新定义继承而来的non-virtual函数

如题,真理。

条款37:绝不重新定义继承而来的缺省参数值

绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数–你唯一应该覆写的东西–却是动态绑定。
不懂没关系,真用到的时候在回头看书。

条款38:通过复合塑模出has-a或“根据某物实现出”

  • 复合的意义和public继承完全不同
  • 在应用域,复合意味has-a。在实现域,复合意味is-implemented-in-terms-of。

在一个类中包含另一个类。

条款39:明智而审慎地使用private继承

  • private继承意味is-implemented-in-terms of。它通常比符合的级别低。但是当derived class 需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
  • 和复合不同,private继承可以造成empty base 最优化。这对致力于”对象尺寸最小化“的程序库开发者而言,可能很重要。

private继承,编译器不会将派生类转为基类。
所有基类成员都变成private。

条款40:明智而审慎地使用多重继承

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等成本。如果virutal base classes不带任何数据,将是最具有实现价值的情况(类似Java中的接口继承)。
  • 多重继承的确有正当用途。其中一个情节涉及”public继承某个Interface class“和”private继承某个协助实现的class“的两相组合。

条款41:了解隐式接口和编译器多态

模板没有具现化以前,并不知道具体的实现是什么。直到编译的时候才能确定。
所以这是一种多态。

条款42:了解typename 的双重意义

有嵌套类型名称的时候用。

条款43:学习处理模板化基类的名称

在derived class template内通过“this->”指涉base class template内的成员名称,或借由一个明白写出的”base class资格修饰符”完成。
基类有可能特化,所以不一定提供统一的接口,所以派生类中不能直接用基类的接口。

条款44:将与参数无关的代码抽离template

不懂

条款45:

补充

平时我们申请堆内存用的 new 和 delete 是 C++ 的操作符。

new 会被编译器翻译成两部部分:申请内存和构造对象。delete也是两部分:析构和回收内存。

申请内存的函数是 ::operator new(size_t size),operator new 会申请 size 大小的内存,如果申请不成功,会抛出异常,异常处理程序会 调用一个 new_handler的函数来处理(反复调用),直到没法再申请了会抛出一个 bad_alloc异常。

条款49:了解new-handler的行为

在 <new> 中有如下声明:

1
2
3
4
namespace std {
typedef void (*new_handler) ();
new_handler set_new_handler (new_handler P) throw();
}

new_handler是一个函数指针,下面的函数是设置 operator new 分配失败时要调用的函数。

一个类可以设计它自己的 new_handler。
就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using namespace std;

class QQQ {
private:
class NewHandlerHolder {
public:
explicit NewHandlerHolder(new_handler nh) : handler(nh) {}
~NewHandlerHolder() {
set_new_handler(handler);
}

private:
new_handler handler;
};

public:
QQQ (){
cout<<"I am constructor\n";
}

static std::new_handler set_new_handler(std::new_handler p) throw() {
new_handler oldHandler=currentHandler;
currentHandler=p;
return oldHandler;
}

static void* operator new(size_t size) throw(std::bad_alloc) {
NewHandlerHolder h(std::set_new_handler(currentHandler));
cout<<"This is my new\n";
return ::operator new(size);
}

private:
static std::new_handler currentHandler;
};

std::new_handler QQQ::currentHandler=nullptr;


void outMem() {
cout<<"Memory out of limit\n";
}

int main()
{
QQQ::set_new_handler(outMem);
QQQ* qqq=new QQQ;

return 0;

}

书中的那个代码写得更健壮一些。
从代码中可以看出,当类中有 operator new 的时候,编译器就不会去用全局的那个 operator new 了。
据说 operator new 还可以重载,只要返回值和第一个参数不变就行。

书中那个模板继承的方式没看懂

条款50:了解new 和delete 的合理替换时机

很多时候需要定制程序的new和delete,这里指的应该是 operator new 和 operator delete,因为C++ 规定 new 和 delete 的行为是不能改变的。
定制的理由很多,编译器自带的版本因为是一个通用的版本,所以在很多方面性能不佳,具体看书上。

条款51:编写new 和 delete 时需固守常规

写 operator new 和 operator delete 是有一定规定的(C++规定)
operator new 是内含一个无穷循环,分配内存,如果内存不足,调用 new-handler。它也应该有能力处理 0 bytes 的申请。
class专属版本的申请还要考虑有继承的情况,因为往往派生类的对象比基类的对象大。数组的情况也有一些特别的地方,比如要用一定的空间来保持数组大小。

operator delete 应该在收到null指针时不做任何事情。

条款52:写了placement new 也要写placement delete

new 分为两部分,一部分申请空间,一部分构造。
在申请空间阶段抛出异常的情况之前说了,那么在构造阶段抛出异常会如何。此时,空间已经申请了,抛出异常,C++的运行环境会去调用和operator new 匹配的operator delete 释放那部分空间。

之所以对应的,是因为operator new 和 operator deete 都可以有多个版本。

正常版本(也就是编译器提供的全局版本)是这样的:

1
2
3
4
5
6
void* operator new (std::size_t ) throw (std::bad_alloc);
void* operator delete(void* rawMemory) throw();

除此之外,可以自己实现其他版本,带有其他参数,这样的版本称为 placement new 和 placement delete。
在new 的时候要加上其他参数。
<new\> 中已经实现了三个:

void operaotor new(std::size_t) throw(std::bad_alloc);
void
operator new(std::size,void) throw();
void
operaotr new(std::size_t, const std::nothrow_t&) throw();
~~~
第一个是正常的;第二个是平常一般说的的placement new ,接受一个指针,在指针的地方构造;第三个是兼容老的程序的版本。

声明了一个placement new 就必须要申明一个对应参数的placement delete。因为在发生异常的时候会去找它,找不到就会内存泄漏。但是你自己显式delete的时候,调用的还是正常的那个。

在class中的声明的会覆盖全局的,只要有一个,它就只会来找累里定义的,不会去找全局的。

条款53:不要轻忽编译器的警告

  • 严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉。
  • 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失。

条款54:让自己熟悉包括TR1在内的标准程序库

直接看c++11的文档更好。

条款55:让自己熟悉Boost

boost