《Effective C++》读书笔记
一、让自己习惯C++
1、视C++为一个语言联邦
1、对内置对象类型而言,pass_by_value比pass_by_reference更高效。对用户自定义对象类型而言,pass-by-reference-to-const往往更好。
2、尽量以const、enum、inline替换#define
3、尽可能使用const
1、如果const出现在星号左边,表示被指物是常量,如果const出现在星号右边,表示指针自身是常量。如果出现在星号两边,表示指针和被指物都是常量。
2、在某些情况下,函数返回值也需要被定义为const,比如书中所说的有理数相乘。
3、及时调用了const修饰的成员函数,也可能改变对象的内容,比如修改了指针指向的内容。
Const int fun();返回值为const
Int fun() const;方法为const
4、确定对象被使用前已被初始化
1、永远在使用对象之前将它初始化。对于无任何成员的内置类型,必须手动完成。
2、C++ 规定,成员变量的初始化动作发生在进入构造函数本体之前,所以在构造函数内部的所谓的“初始化”操作,其实都是赋值操作。
3、规定总是在初始化列表中列出所有的成员变量,以免还得记住哪些成员变量可以无需初值。如果成员函数是const或者references,就必须在初始化列表中进行初始化。
4、为避免(跨编译单元之初始化次序)的问题,要以local static对象替换non-local static对象。
FileSystem & tfs()
{
Static FileSystem fs;
Return fd;
}
二、构造/析构/赋值运算
5、了解C++默默编写并调用了哪些函数
1、编译器暗自为class创建了Default构造函数,copy构造函数,copy assigment操作符以及析构函数。唯有当这些函数被需要(被调用),它们才会被编译器创建出来。
2、打算在一个内含reference成员的class或是含有const成员的class支持赋值操作时,必须自己定义copy assignment操作符。例如:
#include <iostream>
#include <string>
using namespace std;
template<class T>
class NamedObject
{
public:
NamedObject(std::string & name, const T&value) :nameValue(name), objectValue(value) {};
private:
std::string & nameValue;
const T objectValue;
};
void main() {
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s;//此处语法错误,编译出错,尝试引用已删除的函数
return;
}
3、如果某个base class的|copy assignment操作符声明为private,编译器不会为其derivec classes生成一个copy assignment操作符。
6、若不想使用编译器自动生成的函数,就该明确拒绝
1、将父类的拷贝构造函数和拷贝操作符声明为私有,其子类便不可被复制。例如:
class Uncopyable
{
protected:
Uncopyable() {}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable & operator =(const Uncopyable&);
};
class HomeForSale:private Uncopyable{};
7、为多态基类声明virtual析构函数
1、带多态性质的base classes应该声明一个virtual析构函数。反之则不。
2、STL容器都没有virtual析构函数,所以最好不要继承他们,否则可能会造成内存泄漏。
8、别让异常逃离析构函数
1、析构函数不要吐出异常,如果一个被析构函数调用的函数可能会抛出异常,析构函数应该捕捉任何异常,然后吞掉它们或结束程序。
2、如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非析构函数)执行该操作。
9、绝不在构造和析构过程中调用virtual函数
1、在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class。
10、令赋值操作符返回一个reference to * this
11、在operator =中处理“自我赋值”
1、operator =要考虑“自我赋值”和“异常安全性”
2、确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
12、复制对象时勿忘记其每一个成分
1、Copying函数应该确保复制“对象内的所有成员变量”及“所有Base class成分”。
2、不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用。
三、资源管理
13、以对象管理资源
1、像auto_ptr、share_ptr那样管理对象
2、auto_ptr被销毁时会自动删除所指对象,所以不要让多个autp_ptr指向同一个对象。如果以copy构造函数或者copy_assignment操作符复制他们,他们会变为null,而复制所得的新的指针或获得资源的唯一拥有权。STL容器容不得auto_ptr。
3、share_ptr属于RCSP(reference-counting smart pointer 引用型计数指针),但是不能打破环形引用。
4、auto_ptr和share_ptr都在析构函数内做delete操作,但不是delete[],所以不能让他们指向数组。Boost库中拥有可以指向数组的智能指针。
14、在资源管理类中小心copying行为
1、share_ptr允许指定删除器,能够自定义删除操作。
2、复制RAII(Resource Acquisition Is Initialization,资源取得时机便是初始化时机)对象必须一并复制它所管理的对象。
15、在资源管理类中提供对原始资源的访问
1、对原始资源的访问可能经由显示转换或隐式转换,隐式转换更方便但是不安全。
16、成对使用new和delete时要采取相同形式
1、new与delete搭配,new[ ]与delete[ ]搭配,为避免出错,尽量不要对数组进行typedef操作。
17、以独立语句将newed对象置入智能指针
1、以独立语句将newd对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());
执行上述代码段需要完成以下三个操作:
- 调用priority(2)执行new Widget(3)调用tr1::shared_ptr构造函数
但是三者具体顺序未知,但倘若以以下顺序执行
执行
(1)new Widget(2)调用priority(3)调用tr1::shared_ptr构造函数
当priority发生异常,new widget返回的指针就会被丢失,从而造成资源泄露。
合理的代码应该如下:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());
四、设计与声明
18、让接口容易被正确使用
1、如果客户企图使用某个接口而却没有获得他所预期的行为,这个代码不该通过编译;如果代码通过了编译,它的作为就该是客户所想要的。
2、“促进正确使用”的办法包括接口一致性,以及内置类型的行为兼容。
3、“阻止误用”的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
4、shared_ptr支持定制型删除器,可防范 DLL 问题,可被用来自动解除互斥锁。
19、设计class犹如设计type
20、宁以pass-by-reference-to-const替换pass-by-value
1、尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
2、以上规则不适用于内置类型,STL的迭代器和函数对象,对他们而言,pass-by-value往往更适当。
21、必须返回对象时,别妄想返回其reference
1、绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象。
22、将成员变量声明为private
23、宁以non-member、non-friend替换member函数
1、如果non-member或non-friend函数能够提供与member相同的机能,则不要选择member函数。
2、可以将不同类别的的函数定义在同一命名空间的不同头文件中
24、若所有参数皆需类型转换,请为此采用non-member函数
1、比如有理数类Rational,要重载*运算符,使其支持正乘和反乘,就应该用non_member函数来重载。
25、考虑写出一个不抛异常的swap函数
感觉实际用到的地方并不多,暂时没有看大明白,之后有机会再研究。
五、实现
26、尽可能延后变量定义式的出现时间
27、尽量少做转型动作
1、单一对象可能拥有一个以上的地址。例如:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
class Student
{
public:
Student();
~Student();
Student(string name_):name(name_)
{
}
string getName() const {
return this->name;
}
private:
const string name;
};
Student::Student()
{
}
Student::~Student()
{
}
class Person
{
public:
string sex;
Person();
~Person();
private:
};
Person::Person()
{
}
Person::~Person()
{
}
class Worker :public Student ,public Person
{
public:
int number;
};
void main() {
Worker worker;
Worker * work_ptr = &worker;
Person * person_ptr = &worker;
Student * student_ptr = &worker;
cout << work_ptr << endl;
cout << person_ptr << endl;
cout << student_ptr << endl;
}
//003AF9AC
//003AF9C8
//003AF9AC
//请按任意键继续. . .
2、如果可以,尽量避免使用转型,尤其是dynamic_casts,如果有个设计需要转型动作,试着发展无需转型的替代设计。
3、宁可使用C++style转型,不要使用使用旧式转型。
28、避免返回handles指向对象内部成分
1、避免返回handles(包括reference、指针、迭代器),不论不论和这个handle是否为const,不论返回handle的成员函数是否为const,都不能。从而将“虚吊号码牌”(dangling handles)的可能性降至最低。
29、为“异常安全”而努力是值得的
1、异常安全函数及时发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。强烈型保证一般需要copy-and-swap实现出来,但可能会耗费更多的空间和时间资源,可能不具有现实意义。
30、透彻了解inline的里里外外
1、如果friend被实现在class内,将被隐喻声明为inline
2、Inline只是一个建议,编译器有可能忽略,对virtual的inline通常会失败,因为虚函数需要动态决定执行哪个函数。
3、编译器通常不对通过函数指针调用的函数进行inline
inline void fun() {};
void main()
{
void(*g)() = fun;
fun();//这个调用将被inline
g();//这个调用获取不被inline
}
4、如果对库函数进行inline,当库升级时,所有使用库的应用程序都要重新编译。
5、对构造函数和析构函数进行inline可能会使函数非常庞大
31、将文件间的依存关系将至最低
1、相依于声明式,不要相依于定义式。基于此构想的两个手段是Handles classes和Interface classes。
Handles classes的实现Demo如下
//Person.h
#pragma once
#include<string>
class Person
{
public:
Person(const std::string name);
Person() {};
std::string getName();
private:
Person * person_ptr;
};
//Person.cpp
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string name):person_ptr(new PersonImpl(name))
{
}
std::string Person::getName()
{
return person_ptr->getName();
}
//PersonImpl.h
#pragma once
#include "Person.h"
class PersonImpl:public Person
{
public:
std::string getName();
PersonImpl(std::string name);
private:
std::string name;
};
//PersonImpl.cpp
#include "PersonImpl.h"
std::string PersonImpl::getName()
{
return this->name;
}
PersonImpl::PersonImpl(std::string name_):Person(),name(name_)
{
}
//main.cpp
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include "Person.h"
using namespace std;
void main()
{
Person p("Peter");
cout << p.getName() << endl;
}
这样,如果修改了PersonImpl.cpp,只需要重新编译PersonImpl.cpp
2、程序库头文件应该完全且仅有声明式。
六、继承与面向对象设计
32、确定你的public继承塑模出is-a关系
1、public继承意味着is-a,适用于base-class身上的每一件事应同样适用于子类身上。
2、这反映出一个事实,世界上并不存在一个适用于所有软件的完美设计。所谓最佳设计,取决于系统希望做什么事,包括现在与未来。如果你的程序对飞行一无所知,而且也不打算未来对飞行有所知,那么不去区分会飞的鸟和不会飞的鸟,不失为一个完美而有效的设计。
33、避免遮掩继承而来的名称
1、Derived classes内的名称会遮盖Base classed内的名称:
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived :public Base
{
public:
virtual void mf1();
void mf3();
void mf4();
};
int main()
{
Derived d;
int x;
d.mf1(); //调用Derived::mf1
d.mf1(x); //错误!因为Derived::mf1覆盖了Base::mf1
d.mf2(); //调用Base::mf2
d.mf3(); //调用Derived::mf3
d.mf3(x); //错误!因为Derived::mf3覆盖了Base::mf3;
}
2、为了让遮掩的名称再见天日,可使用using声明式或转交函数。转交函数就是在子类的方法体内调用父类的方法,个人表示不推荐。
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived :public Base
{
public:
using Base::mf1;
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
};
int main()
{
Derived d;
int x;
d.mf1(); //调用Derived::mf1
d.mf1(x); //调用Base::mf1
d.mf2(); //调用Base::mf2
d.mf3(); //调用Derived::mf3
d.mf3(x); //调用Base::mf3
}
34、区分接口继承和实现继承
1、纯虚函数只具体指定接口继承
2、非纯虚函数指定接口继承和一份缺省实现
3、非虚函数只能具体接口继承以及强制性实现继承
35、考虑virtual函数以外的其他选择
这节看得不是很明白,之后有用到的话再细看。
36、绝不重新定义继承而来non-virtual函数
37、绝不要重新定义一个继承而来的缺省参数值。
1、因为缺省参数值都是静态绑定,而virtual函数是动态绑定。意思是你可能会在调用一个定义于derived class内的virtual函数的同时,使用的是base class为它只能的缺省参数值。、
2、解决方案可以参考条款35
38、通过复合塑模出has-a或“根据某物实现出”
1、程序中的对象其实相当于你塑造的世界中的某些事物,例如人、汽车、一张张视频画面等等。这样的对象属于应用域(application domain)部分。其他对象则纯粹是实现细节上的人工制品,比如缓冲区,互斥器,查找树等。这些对象相当于软件的实现域(implementation domain)。当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。
39:明智而审慎地使用private继承
- 如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。
- Private继承意味着is-implemented-in-terms-of,它通常比复合的级别低。但是Derived class需要访问Base class的protected成员或者需要重新定义虚函数时,这么设计也是合理的。
- 对与empty base来言,private继承相对复合关系,更能节省空间。
40、明智而审慎地使用多继承
1、多继承比单一继承复杂,它可能导致新的歧义性。对与钻石继承关系,C++默认是对base class执行多份拷贝。但是通过虚继承可以保证只拷贝一份。
2、虚继承会增加大小、速度、初始化复杂度等等成本。如果非要使用,virtual base class不要带有任何数据。
七、模板与泛型编程
41、了解隐式接口和编译期多态
1、classed和templates都支持接口和多态
2、对classed而言,接口是显式的,多态通过虚函数发生于运行期。
3、对template而言,接口是隐式的,多态发生在编译期。
42、了解typename的双重意义
1、声明template参数时,前缀关键字class关键字和typename可互换
2、请使用关键字typename标识嵌套从属类型名称;但不得在base class list和member initialization list内以它作为base class修饰符。
43、学习处理模板化基类内的名称
1、假设B是一个模板化基类,D继承了B,默认情况下,D中不能调用B的方法,因为C++往往拒绝在templatized base classes内寻找继承而来的名称。
2、为解决上述问题,第一种方法,可在derived class templates内通过this指针来调用模板化基类中的方法(相当于明确表示子类将会实现基类的方法)。第二种方法是使用using 声明式。第三种方法是通过::符号明确指出调用的函数位于基类中,但这种方法最差劲,因为如果是调用的虚函数,将会关闭动态绑定行为。
44、将与参数无关的代码抽离templates
1、template生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。、
2、因非类型模版参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
3、因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码(这点不是很懂)。
45、运用成员函数模板接受所有兼容类型
1、使用member function templates生成可接受所有兼容类型的函数
2、如果你声明member templates用于泛化copy构造或泛化assignment操作,你还是需要声明正常的copy构造函数和copy assignment操作符。
46、需要类型转换时请为模版定义非成员函数
1、template实参推到过程中从不将隐式类型转换函数考虑在内。
2、在一个class template内部,template名称可被用来作为“template和其参数”的简略表达方式。(比如可以不写<int>)
3、当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。
47、请使用traits classes表现类型信息
1、STL中有一个名为advance的模版函数,用于将某个迭代器移动给定的某个距离。但是迭代器多种多样,有的只能向前有的只能向后有的两者都可,有的只能单步移动,有的能够多部移动。为了对每一种迭代器都能正确且高效地使用advance模版函数,C++标准库提供了专属的卷标结构体,用于标志迭代器的种类或者能力。
2、其他的感觉用得不太多,价值不大。以后用到再说。
48、认识template元编程
1、模板元编程可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
2、一个模版元编程的示例
template<unsigned n>
struct Factorial
{
enum
{
value=n*Factorial<n-1>::value;
}
}
template<>
struct Factorial<>
struct Factorial<0>
{
enum
{
value=1;
}
}
int main()
{
std::cout << Factorial<5>::value <<endl; //120
std::cout << Factorial<10>::value <<endl; //3628800
}
八、定制new和delete
注意:STL容器所使用的heap内存是由容器所拥有的分配器对象(allocator objects)管理,不是被new和delete直接管理。
49、了解new-handler的行为
1、当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler。
2、当operator new无法满足内存申请时,它会不断调用new-handler直到找到足够的内存。所以一个设计良好的new-handler函数必须做出以下事情:(1)让更多的内存可被使用;(2)安装另一个new-handler;(3)卸除new-handler;(4)抛出bad_alloc的异常;(5)不返回。
3、1993年以前,C++要求operator new必须在无法分配足够内存时返回null。新一代的operator new则应该抛出bad_alloc异常,但很多C++程序是在编译器开始支持新规范前写出来的。所以C++委员会还提供一个nothow方式的new,来返回null;
4、set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
50、了解new和delete的合理替换时机
1、定制new(重载new操作符)要考虑许多细节问题,一般而言我建议你在必要时才试着写写看。很多时候是非必要的,许多编译器都提供了一份良好的实现。
2、有许多理由需要写一个自定义的new和delete,包括改善效能、对heap运用错误进行调试。收集heap使用信息等。
51、编写new和delete时需固守常规
1、operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0bytes申请。Class专属版本还应该处理“比正确大小更大的申请”。
2、operator delete应该在收到null指针时不错任何事。Class专属版本则还应该处理“比正确大小更大的申请”。
52、写了placement new也要写placement delete
1、C++运行期系统有垃圾回收机制,会调用与new相对应的delete版本。
比如:
Widget * pw = new Widget;
如果在Widget的构造函数内发生异常,那么pw并没有接管新new出的内存空间,客户没有能力归还内存。这种情况下,C++运行期系统就会调用与new相对应的delete。如果找不到相应的delete,就会什么都不做,从而造成内存泄露。
- 当你声明placement new和placement delete,请确定不要无意识(非故意)地掩盖了他们的正常版本。
九、杂项讨论
53、不要轻忽编译器的警告
但是实际上公司项目祖传代码有成千上万的警告。。。。
54、让自己熟悉标准程序库
55、让自己熟悉Boost
推荐阅读