💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## scoped_ptr ### 头文件: `"boost/scoped_ptr.hpp"` `boost::scoped_ptr` 用于确保动态分配的对象能够被正确地删除。`scoped_ptr` 有着与`std::auto_ptr`类似的特性,而最大的区别在于它不能转让所有权而`auto_ptr`可以。事实上,`scoped_ptr`永远不能被复制或被赋值!`scoped_ptr` 拥有它所指向的资源的所有权,并永远不会放弃这个所有权。`scoped_ptr`的这种特性提升了我们的代码的表现,我们可以根据需要选择最合适的智能指针(`scoped_ptr` 或 `auto_ptr`)。 要决定使用`std::auto_ptr`还是`boost::scoped_ptr`, 就要考虑转移所有权是不是你想要的智能指针的一个特性。如果不是,就用`scoped_ptr`. 它是一种轻量级的智能指针;使用它不会使你的程序变大或变慢。它只会让你的代码更安全,更好维护。 下面是`scoped_ptr`的摘要,以及其成员的简要描述: ``` namespace boost { template<typename T> class scoped_ptr : noncopyable { public: explicit scoped_ptr(T* p = 0); ~scoped_ptr(); void reset(T* p = 0); T& operator*() const; T* operator->() const; T* get() const; void swap(scoped_ptr& b); }; template<typename T> void swap(scoped_ptr<T> & a, scoped_ptr<T> & b); } ``` ### 成员函数 ``` explicit scoped_ptr(T* p=0) ``` 构造函数,存储`p`的一份拷贝。注意,`p` 必须是用`operator new`分配的,或者是null. 在构造的时候,不要求`T`必须是一个完整的类型。当指针`p`是调用某个分配函数的结果而不是直接调用`new`得到的时候很有用:因为这个类型不必是完整的,只需要类型`T`的一个前向声明就可以了。这个构造函数不会抛出异常。 ``` ~scoped_ptr() ``` 删除被指物。类型`T`在被销毁时必须是一个完整的类型。如果`scoped_ptr`在它被析构时并没有保存资源,它就什么都不做。这个析构函数不会抛出异常。 ``` void reset(T* p=0); ``` 重置一个 `scoped_ptr` 就是删除它已保存的指针,如果它有的话,并重新保存 `p`. 通常,资源的生存期管理应该完全由`scoped_ptr`自己处理,但是在极少数时候,资源需要在`scoped_ptr`的析构之前释放,或者`scoped_ptr`要处理它原有资源之外的另外一个资源。这时,就可以用`reset`,但一定要尽量少用它。(过多地使用它通常表示有设计方面的问题) 这个函数不会抛出异常。 ``` T& operator*() const; ``` 返回一个到被保存指针指向的对象的引用。由于不允许空的引用,所以解引用一个拥有空指针的`scoped_ptr`将导致未定义行为。如果不能肯定所含指针是否有效,就用函数`get`替代解引用。这个函数不会抛出异常。 ``` T* operator->() const; ``` 返回保存的指针。如果保存的指针为空,则调用这个函数会导致未定义行为。如果不能肯定指针是否空的,最好使用函数`get`。这个函数不会抛出异常。 ``` T* get() const; ``` 返回保存的指针。应该小心地使用`get`,因为它可以直接操作裸指针。但是,`get`使得你可以测试保存的指针是否为空。这个函数不会抛出异常。`get`通常在调用那些需要裸指针的函数时使用。 ``` operator unspecified_bool_type() const ``` 返回`scoped_ptr`是否为非空。返回值的类型是未指明的,但这个类型可被用于Boolean的上下文中。在if语句中最好使用这个类型转换函数,而不要用`get`去测试`scoped_ptr`的有效性 ``` void swap(scoped_ptr& b) ``` 交换两个`scoped_ptr`的内容。这个函数不会抛出异常。 ### 普通函数 ``` template<typename T> void swap(scoped_ptr<T>& a,scoped_ptr<T>& b) ``` 这个函数提供了交换两个scoped pointer的内容的更好的方法。之所以说它更好,是因为 `swap(scoped1,scoped2)` 可以更广泛地用于很多指针类型,包括裸指针和第三方的智能指针。\[2\] `scoped1.swap(scoped2)` 则只能用于它的定义所在的智能指针,而不能用于裸指针。 > \[2\] 你可为那些不够智能,没有提供它们自己的交换函数的智能指针创建你的普通swap函数。 ### 用法 `scoped_ptr`的用法与普通的指针没什么区别;最大的差别在于你不必再记得在指针上调用`delete`,还有复制是不允许的。典型的指针操作(`operator*` 和 `operator-&gt;`)都被重载了,并提供了和裸指针一样的语法。用`scoped_ptr`和用裸指针一样快,也没有大小上的增加,因此它们可以广泛使用。使用`boost::scoped_ptr`时,包含头文件`"boost/scoped_ptr.hpp"`. 在声明一个`scoped_ptr`时,用被指物的类型来指定类模板的参数。例如,以下是一个包含`std::string`指针的`scoped_ptr`: ``` boost::scoped_ptr<std::string> p(new std::string("Hello")); ``` 当`scoped_ptr`被销毁时,它对它所拥有的指针调用`delete` 。 ### 不需要手工删除 让我们看一个程序,它使用`scoped_ptr`来管理`std::string`指针。注意这里没有对`delete`的调用,因为`scoped_ptr`是一个自动变量,它会在离开作用域时被销毁。 ``` #include "boost/scoped_ptr.hpp" #include <string> #include <iostream> int main() { { boost::scoped_ptr<std::string> p(new std::string("Use scoped_ptr often.")); // 打印字符串的值 if (p) std::cout << *p << '\n'; // 获取字符串的大小 size_t i=p->size(); // 给字符串赋新值 *p="Acts just like a pointer"; } // 这里p被销毁,并删除std::string } ``` 这段代码中有几个地方值得注明一下。首先,`scoped_ptr`可以测试其有效性,就象一个普通指针那样,因为它提供了隐式转换到一个可用于布尔表达式的类型的方法。其次,可以象使用裸指针那样调用被指物的成员函数,因为重载了`operator-&gt;`. 第三,也可以和裸指针一样解引用`scoped_ptr`,这归功于`operator*`的重载。这些特性正是`scoped_ptr`和其它智能指针的用处所在,因为它们和裸指针的不同之处在于对生存期管理的语义上,而不在于语法上。 ### 和auto_ptr几乎一样 `scoped_ptr` 与 `auto_ptr`间的区别主要在于对拥有权的处理。`auto_ptr`在复制时会从源`auto_ptr`自动交出拥有权,而`scoped_ptr`则不允许被复制。看看下面这段程序,它把`scoped_ptr` 和 `auto_ptr`放在一起,你可以清楚地看到它们有什么不同。 ``` void scoped_vs_auto() { using boost::scoped_ptr; using std::auto_ptr; scoped_ptr<std::string> p_scoped(new std::string("Hello")); auto_ptr<std::string> p_auto(new std::string("Hello")); p_scoped->size(); p_auto->size(); scoped_ptr<std::string> p_another_scoped=p_scoped; auto_ptr<std::string> p_another_auto=p_auto; p_another_auto->size(); (*p_auto).size(); } ``` 这个例子不能通过编译,因为`scoped_ptr`不能被复制构造或被赋值。`auto_ptr`既可以复制构造也可以赋值,但这们同时也意味着它把所有权从`p_auto` 转移给了 `p_another_auto`, 在赋值后`p_auto`将只剩下一个空指针。这可能会导致令人不快的惊讶,就象你试图把`auto_ptr`放入容器内时所发生的那样。\[3\] 如果我们删掉对`p_another_scoped`的赋值,程序就可以编译了,但它的运行结果是不可预测的,因为它解引用了`p_auto`里的空指针`(*p_auto)`. > \[3\] 永远不要把`auto_ptr`放入标准库的容器里。如果你试一下,通常你会得到一个编译错误;如果你没有得到错误,你就麻烦了。 由于`scoped_ptr::get`会返回一个裸指针,所以就有可能对`scoped_ptr`做一些有害的事情,其中有两件是你尤其要避免的。第一,不要删除这个裸指针。因为它会在`scoped_ptr`被销毁时再一次被删除。第二,不要把这个裸指针保存到另一个`scoped_ptr` (或其它任何的智能指针)里。因为这样也会两次删除这个指针,每个`scoped_ptr`一次。简单地说,尽量少用`get`, 除非你要使用那些要求你传送裸指针的遗留代码! ### scoped_ptr 和Pimpl用法 `scoped_ptr`可以很好地用于许多以前使用裸指针或`auto_ptr`的地方,如在实现pimpl用法时。\[4\]pimpl 用法背后的思想是把客户与所有关于类的私有部分的知识分隔开。由于客户是依赖于类的头文件的,头文件中的任何变化都会影响客户,即使仅是对私有节或保护节 的修改。pimpl用法隐藏了这些细节,方法是将私有数据和函数放入一个单独的类中,并保存在一个实现文件中,然后在头文件中对这个类进行前向声明并保存 一个指向该实现类的指针。类的构造函数分配这个pimpl类,而析构函数则释放它。这样可以消除头文件与实现细节的相关性。我们来构造一个实现pimpl 用法的类,然后用智能指针让它更为安全。 > \[4\] 这也被称为Cheshire Cat 用法. 关于pimpl用法更多的说明请见 [www.gotw.ca/gotw/024.htm](http://www.gotw.ca/gotw/024.htm) 和 Exceptional C++ 。 ``` // pimpl_sample.hpp #if !defined (PIMPL_SAMPLE) #define PIMPL_SAMPLE class pimpl_sample { struct impl; // 译者注:原文中这句在class之外,与下文的实现代码有矛盾   impl* pimpl_; public: pimpl_sample(); ~pimpl_sample(); void do_something(); }; #endif ``` 这是`pimpl_sample`类的接口。`struct impl` 是一个前向声明,它把所有私有成员和函数放在另一个实现文件中。这样做的效果是使客户与`pimpl_sample`类的内部细节完全隔离开来。 ``` // pimpl_sample.cpp #include "pimpl_sample.hpp" #include <string> #include <iostream> struct pimpl_sample::impl { void do_something_() { std::cout << s_ << "\n"; } std::string s_; }; pimpl_sample::pimpl_sample() : pimpl_(new impl) { pimpl_->s_ = "This is the pimpl idiom"; } pimpl_sample::~pimpl_sample() { delete pimpl_; } void pimpl_sample::do_something() { pimpl_->do_something_(); } ``` 看起来很完美,但并不是的。这个实现不是异常安全的!原因是`pimpl_sample`的构造函数有可能在`pimpl`被构造后抛出一个异常。在构造函数中抛出异常意味着已构造的对象并不存在,因此在栈展开时将不会调用它的析构函数。这样就意味着分配给`pimpl_`指针的内存将泄漏。然而,有一样简单的解决方法:用`scoped_ptr`来解救! ``` class pimpl_sample { struct impl; boost::scoped_ptr<impl> pimpl_; ... }; ``` 让`scoped_ptr`来处理隐藏类`impl`的生存期管理,并从析构函数中去掉对`impl`的删除(它不再需要,这要感谢`scoped_ptr`),这样就做完了。但是,你必须记住要手工定义析构函数;原因是在编译器生成隐式析构函数时,类`impl`还是不完整的,所以它的析构函数不能被调用。如果你用`auto_ptr`来保存`impl`, 你可以编译,但也还是有这个问题,但如果用`scoped_ptr`, 你将收到一个错误提示。 要注意的是,如果你使用`scoped_ptr`作为一个类的成员,你就必须手工定义这个类的复制构造函数和赋值操作符。原因是`scoped_ptr`是不能复制的,因此聚集了它的类也变得不能复制了。 最后一点值得注意的是,如果pimpl实例可以安全地被多个封装类(在这里是`pimpl_sample`)的实例所共享,那么用`boost::shared_ptr`来管理pimpl的生存期才是正确的选择。用`shared_ptr`比用`scoped_ptr`的优势在于,不需要手工去定义复制构造函数和赋值操作符,而且可以定义空的析构函数,`shared_ptr`被设计为可以正确地用于未完成的类。 ### scoped_ptr 不同于 const auto_ptr 留心的读者可能已经注意到`auto_ptr`可以几乎象`scoped_ptr`一样地工作,只要把`auto_ptr`声明为`const`: ``` const auto_ptr<A> no_transfer_of_ownership(new A); ``` 它们很接近,但不是一样。最大的区别在于`scoped_ptr`可以被`reset`, 在需要时可以删除并替换被指物。而对于`const auto_ptr`这是不可能的。另一个小一点的区别是,它们的名字不同:尽管`const auto_ptr`意思上和`scoped_ptr`一样,但它更冗长,也更不明显。当你的词典里有了`scoped_ptr`,你就应该使用它,因为它可以更清楚地表明你的意图。如果你想说一个资源是要被限制在作用域里的,并且不应该有办法可以放弃它的所有权,你就应该用 `boost::scoped_ptr`. ### 总结 使用裸指针来写异常安全和无错误的代码是很复杂的。使用智能指针来自动地把动态分配对象的生存期限制在一个明确的范围之内,是解决这种问题的一个有效方法,并且提高了代码的可读性、可维护性和质量。`scoped_ptr` 明确地表示被指物不能被共享和转移。正如你所看到的,`std::auto_ptr`可以从另一个`auto_ptr`那里窃取被指物,那怕是无意的,这被认为是`auto_ptr`的最大缺点。正是这个缺点使得`scoped_ptr`成为`auto_ptr`最好的补充。当一个动态分配的对象被传送给`scoped_ptr`, 它就成为了这个对象的唯一的拥有者。因为`scoped_ptr`几乎总是以自动变量或数据成员来分配的,因此它可以在离开作用域时正确地销毁对象,从而在执行流由于返回语句或异常抛出而离开作用域时,也总能释放它所管理的内存。 在以下情况时使用 `scoped_ptr` : * 在可能有异常抛出的作用域里使用指针 * 函数里有几条控制路径 * 动态分配对象的生存期应被限制于特定的作用域内 * 异常安全非常重要时(总应如此!)