如何使用 shared_ptr?

84

你好,我今天在关于如何将不同类型的对象插入到同一个向量数组中的问题上提出了一个问题,我的代码如下:

 gate* G[1000];
G[0] = new ANDgate() ;
G[1] = new ORgate;
//gate is a class inherited by ANDgate and ORgate classes
class gate
{
 .....
 ......
 virtual void Run()
   {   //A virtual function
   }
};
class ANDgate :public gate 
  {.....
   .......
   void Run()
   {
    //AND version of Run
   }  

};
 class ORgate :public gate 
  {.....
   .......
   void Run()
   {
    //OR version of Run
   }  

};      
//Running the simulator using overloading concept
 for(...;...;..)
 {
  G[i]->Run() ;  //will run perfectly the right Run for the right Gate type
 } 

我想使用向量,于是有人告诉我应该这样做:

std::vector<gate*> G;
G.push_back(new ANDgate); 
G.push_back(new ORgate);
for(unsigned i=0;i<G.size();++i)
{
  G[i]->Run();
}

但随后他和许多其他人建议我最好使用Boost Pointer Containers或者shared_ptr。我已经花了过去3个小时阅读这个主题,但是文档对我来说似乎相当高级。

有人能给我一个shared_ptr的小代码示例以及为什么他们建议使用shared_ptr吗?还有类似于ptr_vectorptr_listptr_deque的其他类型吗?

编辑1:我也阅读过一个包含以下内容的代码示例:

typedef boost::shared_ptr<Foo> FooPtr;
.......
int main()
{
  std::vector<FooPtr>         foo_vector;
........
FooPtr foo_ptr( new Foo( 2 ) );
  foo_vector.push_back( foo_ptr );
...........
}

我不理解语法!


2
你不理解哪种语法?main 函数的第一行创建了一个可以包含指向类型为 Foo 的共享指针的向量;第二行使用 new 创建了一个 Foo 并使用共享指针来管理它;第三行将共享指针的副本放入向量中。 - Mike Seymour
7个回答

118

使用shared_ptrvector可以避免因为忘记遍历向量并对每个元素调用delete而导致内存泄漏的可能性。让我们逐行分析稍微修改过的示例。

typedef boost::shared_ptr<gate> gate_ptr;

为共享指针类型创建别名。这可以避免在C++语言中输入 std::vector<boost::shared_ptr<gate> > 时,忘记在闭合“大于号”之间加上空格所导致的丑陋代码。

    std::vector<gate_ptr> vec;

创建一个空的boost::shared_ptr<gate>对象向量。
    gate_ptr ptr(new ANDgate);

为了防止操作抛出异常,需要单独分配一个新的 ANDgate 实例并将其存储到一个 shared_ptr 中。在这个示例中不可能出现此类问题。Boost 的 “最佳实践” 解释了为什么将内存分配到独立的对象中而不是临时对象中是一种最佳做法。

    vec.push_back(ptr);

这将在向量中创建一个新的shared pointer,并将ptr复制到其中。 shared_ptr内部的引用计数机制确保分配给ptr的对象被安全地转移到向量中。

但是,没有解释shared_ptr<gate>的析构函数会确保删除已分配的内存。这就避免了内存泄漏。 std::vector<T>的析构函数确保调用存储在向量中的每个元素的T析构函数。然而,指针(例如gate*)的析构函数不会删除您已经分配的内存。这就是使用shared_ptrptr_vector要避免的问题。


1
这很详细 :) 我的问题是关于第三行代码: gate_ptr ptr(new ANDgate); 它对我来说不太熟悉,一个类型为shared pointer gate的ptr,然后在大括号之间你发送了一个新的ANDgate!这很令人困惑。 - Ahmed
6
@Ahmed:总体表达是一个变量初始化,就像你可能会写int x(5);来初始化变量x的值为5一样。在这种情况下,它被初始化为新创建的ANDgate对象的值;新表达式的值是指向新对象的指针。 - Mike Seymour

43

我想强调一下关于 shared_ptr 的一个重要事项,就是只能使用以下语法来构造它们:

shared_ptr<Type>(new Type(...));

这种方式,指向Type的“真实”指针在你的作用域中是匿名的,只由共享指针持有。因此,你不可能意外使用这个“真实”的指针。换句话说,永远不要这样做:

Type* t_ptr = new Type(...);
shared_ptr<Type> t_sptr ptrT(t_ptr);
//t_ptr is still hanging around!  Don't use it!

尽管这样做是可行的,但是你现在在函数中有一个 Type* 指针 (t_ptr),它存在于共享指针之外。在任何地方使用 t_ptr 都是危险的,因为你永远不知道持有它的共享指针何时会销毁它,从而导致程序崩溃。
对于其他类返回给你的指针也是一样的。如果一个非自己编写的类给了你一个指针,通常不能安全地将其放入 shared_ptr 中。除非你确信该类不再使用该对象。否则,如果你将其放入 shared_ptr 中并且超出范围,则可能在类仍需要该对象时释放它。

8
Ken所说的一切都是正确的,但我认为现在更推荐使用auto t_ptr = make_shared<Type>(...);或者等价的shared_ptr<Type> t_ptr = make_shared<Type>(...);来调用,因为那种形式更加高效。 - Jason Sydes
@KenSimon,shared_ptr<Type> t_sptr ptrT(t_ptr);中的t_sptrptrT之间应该有逗号,吗? - Allanqunzi
除了示例代码中的歧义外,警告很好 - 但遗憾的是你不得不发出警告,因为第一种形式更加简洁,而且更重要的是,任何使用智能指针的人都知道它存在的目的是为了避免危险的裸指针漂浮。最后一段很有趣;幸运的是,我还没有使用过任何强制我使用原始或不清晰类型的库,尽管我肯定会在某个时候遇到这种情况。 - underscore_d

21

在我看来,学习使用智能指针是成为一名合格的C ++程序员最重要的步骤之一。正如您所知道的,每当您新建一个对象时,总有一天您想要删除它。

出现的一个问题是,在使用异常时,很难确保对象始终只在所有可能的执行路径中释放一次。

这就是RAII的原因:http://en.wikipedia.org/wiki/RAII

创建帮助类的目的是确保对象在所有执行路径中始终被释放一次。

这样的类的示例是:std::auto_ptr

但有时您希望与其他人共享对象。只有在没有人再使用它时才应该将其删除。

为了帮助解决这个问题,已经开发了引用计数策略,但您仍然需要手动添加引用和释放引用。本质上,这与new/delete是相同的问题。

这就是为什么boost开发了boost :: shared_ptr,它是引用计数智能指针,因此您可以共享对象而不会意外泄漏内存。

通过C++ tr1的添加,这现在也作为c ++标准的一部分添加到其中,但它的名称为std :: tr1 :: shared_ptr<>。

如果可能,我建议使用标准共享指针。 ptr_list,ptr_dequeue等是针对指针类型的专门化容器。我现在要忽略它们。

所以我们可以从您的示例开始:

std::vector<gate*> G; 
G.push_back(new ANDgate);  
G.push_back(new ORgate); 
for(unsigned i=0;i<G.size();++i) 
{ 
  G[i]->Run(); 
} 

这里的问题是,每当 G 超出作用域时,我们就会泄漏添加到 G 中的 2 个对象。让我们重写它,使用 std::tr1::shared_ptr。

// Remember to include <memory> for shared_ptr
// First do an alias for std::tr1::shared_ptr<gate> so we don't have to 
// type that in every place. Call it gate_ptr. This is what typedef does.
typedef std::tr1::shared_ptr<gate> gate_ptr;    
// gate_ptr is now our "smart" pointer. So let's make a vector out of it.
std::vector<gate_ptr> G; 
// these smart_ptrs can't be implicitly created from gate* we have to be explicit about it
// gate_ptr (new ANDgate), it's a good thing:
G.push_back(gate_ptr (new ANDgate));  
G.push_back(gate_ptr (new ORgate)); 
for(unsigned i=0;i<G.size();++i) 
{ 
   G[i]->Run(); 
} 

当 G 超出范围时,内存会自动回收。

一个练习是让我团队的新手编写他们自己的智能指针类。然后在完成后立即丢弃该类并永远不再使用它。希望您能掌握智能指针在幕后的工作原理,没有任何魔法。


我的导师也给了我类似的建议,让我尝试编写自己的类。谢谢。 - Ahmed
你应该使用迭代器来运行所有的门:for( auto itt = G.begin(); itt != G.end(); ++itt ){ itt->Run(); } - Guillaume Massé
1
或者更好的是C++中的新“foreach” - Just another metaprogrammer

2
boost文档提供了一个相当不错的起步示例:shared_ptr example(实际上是关于智能指针的向量)或shared_ptr doc。 Johannes Schaub的下面回答很好地解释了boost智能指针:smart pointers explained。 ptr_vector背后的想法是它为您处理存储指针后面的内存释放:假设您有一个指针向量,就像在您的示例中一样。当退出应用程序或离开定义向量的作用域时,您必须自己清理(已经动态分配了ANDgate和ORgate),但仅清除向量并不能完成此操作,因为向量存储的是指针而不是实际对象(它不会销毁它所包含的内容)。
 // if you just do
 G.clear() // will clear the vector but you'll be left with 2 memory leaks
 ...
// to properly clean the vector and the objects behind it
for (std::vector<gate*>::iterator it = G.begin(); it != G.end(); it++)
{
  delete (*it);
}

boost::ptr_vector<>可以为您处理上述操作,这意味着它将释放存储指针后面的内存。

shared_ptr是一个智能指针,它是一个光鲜亮丽的“包装器”,用于对普通指针进行一些AI增强。ptr_vector是一个智能指针容器,它是一个“包装器”,用于容纳指针的容器。 - celavek
那么ptr_vector是普通vector的一种替代品吗? - Ahmed
@Ahmed 我想你可以这样想。 - celavek

2
通过 Boost,你可以做到。
std::vector<boost::any> vecobj;
    boost::shared_ptr<string> sharedString1(new string("abcdxyz!"));    
    boost::shared_ptr<int> sharedint1(new int(10));
    vecobj.push_back(sharedString1);
    vecobj.push_back(sharedint1);

>用于在向量容器中插入不同类型的对象。而要访问,您必须使用任何转换操作(any_cast),它类似于动态转换(dynamic_cast),希望它能满足您的需求。


1
#include <memory>
#include <iostream>

class SharedMemory {
    public: 
        SharedMemory(int* x):_capture(x){}
        int* get() { return (_capture.get()); }
    protected:
        std::shared_ptr<int> _capture;
};

int main(int , char**){
    SharedMemory *_obj1= new SharedMemory(new int(10));
    SharedMemory *_obj2 = new SharedMemory(*_obj1);
    std::cout << " _obj1: " << *_obj1->get() << " _obj2: " << *_obj2->get()
    << std::endl;
    delete _obj2;

    std::cout << " _obj1: " << *_obj1->get() << std::endl;
    delete _obj1;
    std::cout << " done " << std::endl;
}

这是shared_ptr的一个示例。_obj2已被删除,但指针仍然有效。 输出为: ./test _obj1: 10 _obj2: 10 _obj2: 10 完成

0
将不同的对象添加到同一容器中的最佳方法是使用make_shared、vector和基于范围的循环,这样你就可以拥有一个漂亮、干净且“可读”的代码!
typedef std::shared_ptr<gate> Ptr   
vector<Ptr> myConatiner; 
auto andGate = std::make_shared<ANDgate>();
myConatiner.push_back(andGate );
auto orGate= std::make_shared<ORgate>();
myConatiner.push_back(orGate);

for (auto& element : myConatiner)
    element->run();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接