读书笔记 Effective C++(2)

13、以对象管理资源

将动态分配的资源放进一个资源管理类中,构造函数中获得资源(RAII),析构函数保证了这些资源的释放。
常用的有shared_ptr和auto_ptr,前者常用,后者复制时会把被复制的指针置为null,一块内存只能有一个auto_ptr指向它。

14、在资源类中小心copying行为

考虑下面这么一个管理互斥锁的类:

1
2
3
4
5
6
7
8
9
class Lock{
public:
explicit Lock(Mutex* pm): mutexPtr(pm){
lock(mutexPtr);//获得资源
}
~Lock(){
unlock(mutexPtr);//释放资源
}
}

当Lock类复制时,大多数时候可以有两种选择,第一个是禁止复制:

1
2
3
4
class Lockprivate Uncopyable{
public:
...
}

第二种是用引用计数法来管理复制,复制时,引用加1,当引用为0时,释放资源,使用shared_ptr还可以定制自己的删除器,因为有时候并不希望释放资源,像Lock,析构的时候不释放资源,只是解锁。

1
2
3
4
5
6
7
8
9
class Lock{
public:
explicit Lock(Mutex* pm): mutexPtr(pm, unlock)//指定删除器unlock
{
lock(mutexPtr.get());
}
private:
shared_ptr<Mutex> mutexPtr;//不再需要析构函数
}

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

有些API需要对原始资源进行访问而不是资源管理类,故资源管理类应该提供办法对原始资源进行访问。
有显示转换和隐式转换两种办法。显示转换专门定义一个返回原始资源的函数,例如shared_ptr提供get函数返回原始指针。
隐式转换则是直接将资源管理类转换成原始资源类型,如下:

1
2
3
4
5
6
7
8
9
10
class Font{
public:
...
//隐式转换,Font转换为FontHandle
operator FontHandle() const{
return f;
}
private:
FontHandle f;
}

隐式转换对用户来说很方便,但可能会产生一些问题,显示转换比较安全。

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

new和delete搭配,new[]和delete[]搭配,最好不要对数组做typedef。

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

以单独一条语句将new出来的对象放到智能指针中去。如果不这么做,一旦有异常抛出,可能会导致难以察觉的资源泄漏。

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

让接口正确使用的方法包括接口的一致性,与内置类型的行为兼容。
阻止误用的方法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
shared_ptr支持定制删除器,可以防范DLL问题(即在一个DLL里创建在另一个DLL里释放),可以用来包装互斥锁,实现自动解锁。

19、设计class犹如设计type

设计class就是设计一个新type,要考虑方方面面的问题。

20、宁可以pass by reference to const替换pass by value

对一个类对象传值意味着复制,会调用拷贝构造函数和析构函数,造成了不必要的时间消耗,而传引用则不会调用拷贝构造函数,更高效,如果函数对传入的参数不进行改变,应该加上const。
对于内置类型和STL的迭代器和函数对象,传值比较适合。

21、必须返回对象时,别妄想返回其reference

不要返回对一个局部对象的引用,在函数结束后该对象被释放,引用变成不合法的。
同样不要在函数内返回一个newed对象的引用,因为delete得不到保障。
也不要返回对一个static对象的引用,如果同时需要多个这样的对象,会出问题。
指针同理。

22、将成员变量声明为private

为了封装性,将成员变量声明为private,这可以赋予客户端访问数据的一致性、可以细微划分成员的访问控制(可读可写)、允许约束条件获得保证,并提供class作者充分的实现弹性。
protect的封装性不比public好,因为派生类可以访问protect成员。

23、宁以non-member、non-friend替换member函数

这样做可以增加封装性、包裹弹性和机能扩充性。

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

如果一个函数需要对所有参数进行类型转换,它必须是non-member的,因此调用端不能进行类型转换。如下:

1
2
3
4
5
6
7
8
9
10
class Rational{
public:
Rational(int numerator = 0, int denominator = 1); //无explicit,允许隐式转换
const Rational operator*(const Rational& rhs) const;
...
}
Rational onehalf = Rational(1, 2);
Rational result;
result = onehalf * 2; //正确,2进行了隐式转换
result = 2 * onehalf; //错误,相当于2.operator*(onehalf),2不是Rational类型!

解决方法就是将该函数声明为non-meber的:

1
2
3
4
5
6
const Rational operator*(const Rational& lhs, const Rational& rhs) const;

Rational onehalf = Rational(1, 2);
Rational result;
result = onehalf * 2; //正确,2进行了隐式转换
result = 2 * onehalf; //正确!

25、考虑写一个不抛出异常的swap函数

当std的swap对一个类型效率不高时,提供一个swap成员函数,且这个成员函数不应该抛出异常,往往这个类承担一种指针的角色,指向真正的实现类(pimpl),swap只需要互换指针而不需要互换数据。
如果提供一个成员函数,也该提供一个非成员函数来调用成员函数,对于class,也请特化std的swap。
调用swap时先声明using std::swap然后再调用,且调用不包含任何的命名空间的修饰。
不要在std里加入新东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace WidgetStuff{
class Widget{
private:
WidgetImpl* pImpl;
public:
...
void swap(Widget& other){
using std::swap;
swap(pImpl, other.pImpl);
}
}
void swap(Widget& a, Widget& b){
a.swap(b);//调用成员函数
}
}

分享到