Effective C++阅读笔记
一、初始
条款1:视C++为一个语言联邦
C++支持多种风格的编程:
- 面向过程编程
- 面向对象编程
- 函数式编程
- 泛型编程
- 元编程
tip:不同类型的编程模式,遵循不同的高效编程准则即可
条款2:尽量以const,enum,inline 替换 #define
- 如果需要定义常量,请用const / enum
因为#define只是简单的文本替换,并没有类型检查,所以不安全 - 如果需要实现类似宏函数的功能,请用 inline 替换#define
宏函数具有很明显的缺陷:歧义问题,优先级问题
条款3:尽可能使用 const
- 使用 const 可以让程序变得更加安全(函数参数,函数返回值,成员函数,作用域内的对象)
- 使用 const 可以让程序变得更加高效(次要)
可以通过使用volatile关键字来去掉编译器对const的优化
tip:当 const 和 non-const成员函数具有实质等价的实现时,可以在non-const成员函数内部调用const成员函数
条款4:确定对象使用前已经被初始化
注意区分下面两种情况:
第一种情况:
class TestClass{
private:
sting _str;
int _data;
public:
TestClass(string s, int d){
_str = s;
_data = d;
}
}
第二种情况:
class TestClass{
private:
string _str;
int _data;
public:
TestClass(string s, int d)
:_str(s)
,_data(d)
{}
}
注意:只有第二种构造函数才正确的只执行了对类成员的初始化,第一种构造函数先执行了初始化动作,又在构造函数内部执行了赋值操作,相当于浪费了初始化动作。所以,推荐使用第二种方式来书写构造函数。
二、构造/析构/赋值运算
条款5:C++缺省声明和构造的函数
当你写下下面这样的代码时:
class Test{
}
实际上的效果却是这样:
class Test{
public:
Test();
Test(const Test&);
Test& operator=(const Test&);
~Test();
}
default 构造函数、copy 构造函数、copy assignment 操作符、析构函数这些都是编译器缺省声明的,而只有当这些函数被调用,它们才会被编译器创建出来。
条款6:明确拒绝不想使用的编译器缺省生成的函数
- 以前的语法,可以将不想使用的缺省成员函数声明为 private 类型
- C11后,可以这么搞:
class Test{
public:
Test(const Test& t) = deleted; //阻止默认生成拷贝构造函数
}
条款7:为多态基类声明virtual析构函数
- 这样可以使得资源释放的时候不会遗漏掉 derived 类的资源
条款9:不在构造和析构函数中调用virtual函数
为什么不能在构造函数中调用 virtual 函数?
第一个原因,在概念上,构造函数的工作是为对象进行初始化。在构造函数完成之前,被构造的对象被认为“未完全生成”。当创建某个派生类的对象时,如果在它的基类的构造函数中调用虚函数,那么此时派生类的构造函数并未执行,所调用的函数可能操作还没有被初始化的成员,这将导致灾难的发生。
第二个原因,即使想在构造函数中实现动态联编,在实现上也会遇到困难。这涉及到对象虚指针(vptr)的建立问题。在Visual C++中,包含虚函数的类对象的虚指针被安排在对象的起始地址处,并且虚函数表(vtable)的地址是由构造函数写入虚指针的。所以,一个类的构造函数在执行时,并不能保证该函数所能访问到的虚指针就是当前被构造对象最后所拥有的虚指针,因为后面派生类的构造函数会对当前被构造对象的虚指针进行重写,因此无法完成动态联编。
为什么不能在析构函数中调用 virtual 函数?
- 由于在析构函数中,子类的析构函数已经调用过了,在父类的析构函数中也无法访问子类的虚函数表
条款10:令operator=返回一个 reference to *this
条款11:在operator=中处理自我赋值
传统的做法:
Test& Test::operator=(const Test& t){
if(&t == this)
return *this;
delete _pdata;
_pdata = new Type(*t._pdata);
return *this;
}
然而这样做有可能出现的问题:
解决方案:
Test& Test::operator=(const Test& t){
Type* temp = _pdata;
_pdata = new Type(*t._pdata);
delete temp;
return *this;
}
tip:有时候为了效率还可以在第二个版本也加上 if(this == &t),虽然第二个版本也可以处理自我赋值问题。
条款12:copy对象时拷贝每一个成分
三、资源管理
常见的资源有:内存,文件描述符,互斥锁,图形界面中的字型和笔刷,数据库连接,网络sockets
条款13:成对使用 new 和 delete时要采取相同形式
条款14:以对象管理资源
为防止资源泄漏,使用 RAII 对象,通常使用的 RALL classes 是 tr1::shared_ptr 和 tr1::auto_ptr,
建议使用第一种,因为它更直观也不容易出错
条款15:在资源管理类中小心Copying行为
四、设计与声明
条款18:让接口容易被使用,不易被误用
- 好的接口要如标题所言
- “促进正确使用”的方法包括接口的一致性(STL就做的很好),以及与内置类型的行为兼容
- “阻止误用”的办法包括:建立新类型、限制类型上的操作、束缚对象、以及消除客户的资源管理责任
- std::tr1::shared_ptr 支持定制删除器。这可以防止DLL问题,可被用来自动解决互斥锁(自定制一个处理引用计数减为0的删除器,即解锁操作的函数)。
条款19:设计Class如设计Type
条款20:尽量以pass-by-regerence-to-const替换pass-by-value
- 这种方法应该是一种常识,因为它能使你的程序更加高效,并且避免切割问题
- 以上规则并不适用于内置类型,以及STL的迭代器和函数对象,对于这几种类型,应该采用后者
条款22:将成员变量声明为private
- protected并不比public更具有封装性,除非你要写一个包含继承关系的程序,必须要使用protected的性质,否则不要将成员变量声明为protected
- 这样做可以赋予客户访问数据的一致性,可细微划分访问控制,允许约束条件得到保证等
五、实现
条款26:尽可能延后变量定义式出现的时间
- 这样做能提高程序的效率和增加程序的清晰度。
例如:
std::string Test(const std::string& dest){
using namespace std;
string str;
if(dest.length() < MIN_SIZE){
throw logic_error("dest is to short");
}
str = dest;
return str;
}
代码说明:对象str在此函数中并未完全使用,一旦异常抛出,就得额外的承担str的构造成本和析构成本,所以最好延后它的定义:
std::string Test(const std::string& dest){
if(dest.length() < MIN_SIZE){
throw logic_error("dest is to short");
}
std::string str(dest);
return str;
}
代码说明:除了延后对象的定义,还用了一个提高效率的技巧:以dest作为str的初值,跳过了无意义的default构造。正如条款4所说。
条款27:尽量少做转型动作
条款28:inline三两事
- inline 只是一个建议,而不是强制命令
- 慎用 inline
条款29:将文件间的编译依存关系降低
六、继承与面向对象设计
条款32:明智而慎重的使用多重继承
七、模板与泛型编程
八、定制 new 和 delete
九、Else
条款53:让自己熟悉包括TR1在内的STL
条款54:让自己熟悉 Boost