《C++编程规范:101条规则、准则与最佳实践》——2.9 确保资源为对象所拥有。使用显式的RAII和智能指针

简介:

本节书摘来自异步社区出版社《C++编程规范:101条规则、准则与最佳实践》一书中的第2章,第2.9节,作者:【加】Herb Sutter , 【罗】Andrei,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.9 确保资源为对象所拥有。使用显式的RAII和智能指针

摘要
利器在手,不要再徒手为之:C++的“资源获取即初始化”(Resource Acquisition Is Initialization,RAII)惯用法是正确处理资源的利器。RAII使编译器能够提供强大且自动的保证,这在其他语言中可是需要脆弱的手工编写的惯用法才能实现的。分配原始资源的时候,应该立即将其传递给属主对象。永远不要在一条语句中分配一个以上的资源。

讨论
C++语言所强制施行的构造函数/析构函数对称反映了资源获取/释放函数对比如fopen/fclose、lock/unlock和new/delete的本质的对称性。这使具有资源获取的构造函数和具有资源释放的析构函数的基于栈(或引用计数)的对象成为了自动化资源管理和清除的极佳工具。

这种自动化很容易实现、简洁、低成本而且天生防错。如果不予采用,就需要手工将调用正确配对,包括存在分支控制流和异常的情形,这可是很不容易而且需要注意力高度集中的任务。既然C++已经通过易用的RAII提供了如此直接的自动化,这种C语言式的仍然依赖于对资源解除分配的微观管理方式就是不可接受的了。

每当处理需要配对的获取/释放函数调用的资源时,都应该将资源封装在一个对象中,让对象为我们强制配对,并在其析构函数中执行资源释放。例如,我们无需直接调用一对非成员函数 OpenPort/ClosePort,而是可以考虑如下方法:

class Port {
public:
 Port( const string& destination ); // 调用OpenPort
 ~Port();                       // 调用ClosePort
 // ……通常无法复制端口,因此需要禁用复制和赋值……
};

void DoSomething() {
 Port port1( "server1:80" );
 // ……
}// 不会忘记关闭_port1_;它会在作用域结束时自动关闭

shared_ptr<Port> port2 = /*…*/;    // port2在最后一个引用它的
                               // shared_ptr离开作用域后关闭```
还可以使用实现了这种模式的软件库(参阅[Alexandrescu00c])。

在实现RAII时,要小心复制构造和赋值(见第49条),编译器生成的版本可能并不正确。如果复制没有意义,请通过将复制构造和赋值设为私有并且不做定义来明确禁用二者(见第53条)。否则,让复制构造函数复制资源或者引用计数所使用的次数,并让赋值操作符如法炮制,如果必要,同时还要确保它释放了最开始持有的资源。一个经典的疏漏是在新资源成功复制之前释放了老资源(见第71条)。

确保所有资源都为对象所有。最好用智能指针而不是原始指针来保存动态分配的资源。同样,应该在自己的语句中执行显式的资源分配(比如new),而且每次都应该马上将分配的资源赋予管理对象(比如shared_ptr),否则,就可能泄漏资源,因为函数参数的计算顺序是未定义的(见第31条)。例如:

void Fun( shared_ptr sp1, shared_ptr sp2 );
// ……
Fun( shared_ptr(new Widget), shared_ptr(new Widget) );``
这种代码是不安全的。C++标准给了编译器巨大的回旋余地,可以将构成函数两个参数的两个表达式重新排序。说得更具体一些,就是编译器可以交叉执行两个表达式:可能先执行两个对象的内存分配(通过调用operator new),然后再试图调用两个Widget构造函数。这恰恰为资源泄漏准备了温床,因为如果其中一个构造函数调用抛出异常的话,另一个对象的内存就永远也没有机会释放了!(详细情况请参阅 [Sutter02]。)

这种微妙的问题有一个简单的解决办法:遵循建议,绝对不要在一条语句中分配一个以上的资源,应该在自己的代码语句中执行显式的资源分配(比如new),而且每次都应该马上将分配的资源赋予管理对象(比如shared_ptr)。例如:

shared_ptr<Widget> sp1(new Widget), sp2(new Widget);
Fun( sp1, sp2 );```
另见第31条,了解使用这种风格的其他优点。

例外情况
智能指针有可能会被过度使用。如果被指向的对象只对有限的代码(比如纯粹在类的内部,诸如一个Tree类的内部节点导航指针)可见,那么原始指针就够用了。

参考文献
[Alexandrescu00c] ● [Cline99] §31.03-05 ● [Dewhurst03] §24, §67 ● [Meyers96] §9-10 ● [Milewski01] ● [Stroustrup00] §14.3-4, §25.7, §E.3, §E.6 ● [Sutter00] §16 ● [Sutter02] §20-21 ● [Vandevoorde03] §20.1.4
相关文章
|
25天前
|
存储 编译器 C语言
C++入门: 类和对象笔记总结(上)
C++入门: 类和对象笔记总结(上)
31 0
|
1天前
|
C++
c++的学习之路:7、类和对象(3)
c++的学习之路:7、类和对象(3)
9 0
|
1天前
|
存储 编译器 C语言
c++的学习之路:5、类和对象(1)
c++的学习之路:5、类和对象(1)
8 0
|
5天前
|
编译器 C++
自从学了C++之后,小雅兰就有对象了!!!(类与对象)(中)——“C++”
自从学了C++之后,小雅兰就有对象了!!!(类与对象)(中)——“C++”
|
5天前
|
存储 编译器 C++
自从学了C++之后,小雅兰就有对象了!!!(类与对象)(上)——“C++”
自从学了C++之后,小雅兰就有对象了!!!(类与对象)(上)——“C++”
|
7天前
|
C++
【C++成长记】C++入门 | 类和对象(下) |Static成员、 友元
【C++成长记】C++入门 | 类和对象(下) |Static成员、 友元
|
7天前
|
存储 编译器 C++
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
|
13天前
|
存储 编译器 C语言
C++类与对象
C++类与对象
15 0
|
14天前
|
存储 C++
C++指针
C++指针
|
25天前
|
编译器 C语言 C++
【c++】类和对象(三)构造函数和析构函数
朋友们大家好,本篇文章我们带来类和对象重要的部分,构造函数和析构函数

热门文章

最新文章