读书笔记 effective c++ Item 15 在资源管理类中提供对原生(raw)资源的访问

简介: 1.为什么需要访问资源管理类中的原生资源  资源管理类是很奇妙的。它们是防止资源泄漏的堡垒,没有资源泄漏发生是设计良好的系统的一个基本特征。在一个完美的世界中,你需要依赖这样的类来同资源进行交互,绝不要直接访问原生(raw)资源而玷污你的双手。

1.为什么需要访问资源管理类中的原生资源 

资源管理类是很奇妙的。它们是防止资源泄漏的堡垒,没有资源泄漏发生是设计良好的系统的一个基本特征。在一个完美的世界中,你需要依赖这样的类来同资源进行交互,绝不要直接访问原生(raw)资源而玷污你的双手。但是世界不是完美的,许多API会直接引用资源,所以除非你放弃使用这样的API(这是不实际的想法),你将会绕开资源管理类而时不时的处理原生资源。

2. 如何获取原生资源——通过显示转换和隐式转换

2.1 一个例子

举个例子,Item 13中介绍了使用像auto_ptr或者tr1::shared_ptr这样的智能指针来存放调用createInvestment工厂函数的返回结果:

1 std::tr1::shared_ptr<Investment> pInv(createInvestment()); // from Item 13

 

假设你想使用一个同Investment对象一起工作的函数,如下:

1 int daysHeld(const Investment *pi); // return number of days
2 
3 // investment has been held

你会像下面这样调用它:

1 int days = daysHeld(pInv); // error!

 

代码将不能通过编译:dayHeld想要使用一个原生Investment*指针,你却传递了一个tr1::shared_ptr<Investment>类型的对象。

 

你需要一种方法将一个RAII类对象(在这个例子中是tr1::shared_ptr)转换成它所包含的原生资源类型。有两种常见的方法来实现它:显示转换和隐式转换

2.2 使用智能指针的get进行显示转换

Tr1::shared_ptr和auto_ptr都提供了一个get成员函数来执行显示转换,也就是返回智能指针对象内部的原生指针:

1 int days = daysHeld(pInv.get()); // fine, passes the raw pointer
2 
3 // in pInv to daysHeld

 

2.3 使用智能指针的解引用进行隐式转换

事实上像所有的智能指针一样,tr1::shared_ptr和auto_ptr也重载了指针的解引用运算符(operator->和operator*),这就允许将其隐式的转换成底层原生指针: 

 1 class Investment { // root class for a hierarchy
 2 
 3 public: // of investment types
 4 
 5 bool isTaxFree() const;
 6 
 7 ...
 8 
 9 };
10 
11 Investment* createInvestment(); // factory function
12 
13 std::tr1::shared_ptr<Investment> // have tr1::shared_ptr
14 
15 pi1(createInvestment()); // manage a resource
16 
17 bool taxable1 = !(pi1->isTaxFree()); // access resource
18 
19 // via operator->
20 
21 ...
22 
23 std::auto_ptr<Investment> pi2(createInvestment()); // have auto_ptr
24 
25 // manage a
26 
27 // resource
28 
29 bool taxable2 = !((*pi2).isTaxFree()); // access resource
30 
31 // via operator*

 

2.3 自己实现get进行显示转换

因为有时候获取RAII对象中的原生资源是必要的,一些RAII类的设计者通过提供一个隐式转换函数来顺利达到此目的。举个例子,考虑下面的字体RAII类,字体对于C API来说是原生数据结构:

 1 FontHandle getFont(); // from C API — params omitted
 2 
 3 // for simplicity
 4 
 5 void releaseFont(FontHandle fh); // from the same C API
 6 
 7 class Font { // RAII class
 8 
 9 public:
10 
11 explicit Font(FontHandle fh) // acquire resource;
12 
13 : f(fh) // use pass-by-value, because the
14 
15 {} // C API does
16 
17 ~Font() { releaseFont(f ); } // release resource
18 
19 ... // handle copying (see Item 14)
20 
21 private:
22 
23 FontHandle f; // the raw font resource
24 
25 };

 

假设有大量的字体相关的C API用于处理FontHandles,因此会有频繁的需求将Font对象转换成FontHandles对象。Font类可以提供一个显示的转换函数,比如说:get:

 

 1 class Font {
 2 
 3 public:
 4 
 5 ...
 6 
 7 FontHandle get() const { return f; } // explicit conversion function
 8 
 9 ...
10 
11 };

 

不幸的是,如果它们想同API进行通讯,每次都需要调用get函数:

 1 void changeFontSize(FontHandle f, int newSize); // from the C API
 2 
 3 Font f(getFont());
 4 
 5 int newFontSize;
 6 
 7 ...
 8 
 9 changeFontSize(f.get(), newFontSize); // explicitly convert
10 
11 // Font to FontHandle

一些程序员发现显示请求这些转换是如此令人不愉快以至于不想使用RAII类。但是这会增加泄漏字体资源的机会,这正是设计Font类要预防的事情。

 

2.3 自己实现operator() 进行隐式转换

 一种替代的方法是让Font提供一个隐式转换到FontHandle的函数:

 1 class Font {
 2 
 3 public:
 4 
 5 ...
 6 
 7 operator FontHandle() const // implicit conversion function
 8 { return f; }
 9 
10 ...
11 
12 };

 

这会使C API的调用变得容易并且很自然:

1 Font f(getFont());
2 
3 int newFontSize;
4 
5 ...
6 
7 changeFontSize(f, newFontSize); // implicitly convert Font
8 
9 // to FontHandle

 

缺点是隐式转换增加了出错的机会。举个例子,客户端本来想要一个Font却创建了一个FontHandle:

 1 Font f1(getFont());
 2 
 3 ...
 4 
 5 FontHandle f2 = f1; // oops! meant to copy a Font
 6 
 7 // object, but instead implicitly
 8 
 9 // converted f1 into its underlying
10 
11 // FontHandle, then copied that

 

现在程序拥有一个被Font对象 f1管理的FontHandle,但是直接使用f2也可以获得这个FontHandle。这就不好了。例如:当f1被销毁,字体资源被释放,f2就变成了悬挂指针。

3.隐式转换和显示转换如何选择?

提供从RAII类对象到底层资源的显示转换(通过一个get成员函数)还是提供隐式转换依赖于设计出来的RAII类需要执行的特殊任务以及使用的场景。最好的设计看上去要遵守Item 18的建议:使接口容易被正确使用,很难被误用。通常情况下,像get一样的显示转换函数会是更好的选择,因为它减少了类型误转换的机会。然而有时候,使用隐式类型转换的自然特性会使局面发生扭转。

 

4.访问原生资源和封装背道而驰?

函数返回一个RAII类中的原生资源同封装是背道而驰的,这已经发生了。这不是设计的灾难,RAII类的存在不是用来封装一些东西;他们的存在是用来保证资源的释放会发生。如果需要,资源封装可以在这个基本功能之上进行实现,但这不是必要的。此外,一些RAII类将实现的真正封装同底层资源非常松散的封装组合到一块。举个例子:tr1::shared_ptr封装了所有的引用计数,但是仍然可以非常容易的访问它所包含的原生指针。像一些设计良好的类,它隐藏了客户没有必要看到的东西,但是它提供了客户端确实需要访问的东西。

5.总结

  • API通常需要访问原生资源,所以每个RAII类应该提供一个获得它所管理的原生资源的方法。
  • 访问原生资源可以通过显式转换或者隐式转换来达到。一般情况下,显示转换更加安全,隐式转换对客户端来说更加方便。


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
20天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
30 2
|
26天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
62 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
68 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
78 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
30 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
26 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
25 1
|
2月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
17 0
|
2月前
|
存储 编译器 C语言
深入计算机语言之C++:类与对象(上)
深入计算机语言之C++:类与对象(上)
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)