返回指针的向量 - 理解

时间:2011-12-01 13:35:52

标签: c++ memory-management pointers

我正在尝试理解以下内容(让我们假装MyStorageClass很大):

class MyStorageClass
{
public:
  string x;
  string y;
  string z;
};

class storage
{
public:
  storage();
  ~storage()
  {
    vector<MyStorageClass *>::iterator it = myVec.begin(); it != myVec.end(); it++)
    {
      delete *it;
    }

  vector<MyStorageClass*> getItems()
  {
    for(int i = 0; i < 10; ++i)
    { 
      v.push_back(new MyStorageClass());
    }
    return v;
  }

private:
  vector<MyStorageClass*> v;

};



main()
{
  storage s;
  vector<MyStorageClass*> vm = s.getItems();
}

从我的描述中,当s返回向量并分配给vm时,这将作为副本(按值)完成。因此,如果s超出范围并将其称为析构函数,vm具有自己的副本,其结构不受影响。但是,通过价值传递效率不高。因此,如果您将其更改为通过引用传递:

vector<MyStorageClass*>& getItems()
{
  for(int i = 0; i < 10; ++i)
  { 
    v.push_back(new MyStorageClass());
  }
  return v;
}

您传递v的内存位置(在类Storage中)。但您仍然使用=运算符将其副本分配给Main类中的向量vm。因此,vm独立于v,如果调用Storage析构函数vm不受影响。

最后,如果getItems返回了引用,则在main中您有以下内容:

main()
{
  storage s;
  vector<MyStorageClass*> &vm = s.getItems();
}

现在,vm保留v的地址。因此它受Storage Destructor的影响。

问题:我上面所说的是真的吗?

5 个答案:

答案 0 :(得分:7)

是的,您已正确理解这一点。现在,如果你想把它提升到一个新的水平,首先找出原因,这是坏事:

  1. 返回指针或对类内部的引用会破坏封装。
  2. 一旦相应的s - 对象被销毁,您获得的引用将成为悬挂引用,从而打破引用始终有效的不变量。
  3. 通过返回一个指针向量,你会让调用者想知道天气他是否必须删除这些指针。
  4. 更好的解决方案是storage公开从begin返回相应迭代器的方法ends。或者,您可以将Visitor - 模式用于需要在s上运行的算法。

    另外,在你的情况下,似乎向量s应该拥有它包含的对象。这将是使用boost::ptr_vector的一个很好的指标。

答案 1 :(得分:0)

我认为你所陈述的是真实的,但我对此目的感到有些困惑。

vector<MyStorageClass*> &vm = s.getItems();

通过引用获取向量。但是向量包含指针,因此超出范围的向量不会导致任何析构函数首先运行 - 析构函数只有在这些是某种类型的智能指针时才会运行(即使它依赖于它)。

因此,您可以愉快地通过值传递指针向量而不会出现问题。我相信你会通过引用来节省一些效率,但我认为它并不像你想象的那么严重。

此外,您的指向对象是使用new(动态)创建的,因此您可以以相同的方式按值返回向量,而不必担心丢失指向的对象。

所以,再次,我认为你的逻辑很好,你的参考方式更有效率,但我只是想确保你知道它可以双向无问题地工作:)(并且因为它的一个指针的矢量,按值也不算太差。)

PS:就参考而言,你必须担心悬挂可能比你在Bjorn上面指出的更令人沮丧。如果您曾经使用过string.c_str(),那么您可能已经习惯了这一点。如果从字符串中获取.c_str()且原始字符串超出范围,则.c_str()返回的指针悬空(指向不再用于此目的的内存)并访问它会导致未定义的行为。因此,by值可能是更好的选择(但它确实取决于你的设计 - 例如,如果在应用程序的持续时间内这将是一个单独的持久性,那么悬挂可能不是问题。)

答案 2 :(得分:0)

即使向量复制了它的值,你的例子中的值也是指针,而不是指向的对象。所以“vm拥有它自己的副本”并不完全正确:第一段代码中的结果向量将包含指针的副本,但不包含MyStorageClass的副本他们指向的对象;所以,实际上在你的所有3个代码示例中,如果调用存储析构函数,vm中存储的指针将不再有效!

但是,如果你需要确保Storage在上次访问其中一个MyStorageClass对象之前没有被销毁,那么,在所提出的方法中,第三种方式将是首选方法,因为矢量数据(即指针)仅在存储器中存在一次。您应该考虑返回const向量,但是,getItems的每个调用者都可以通过返回的引用修改Storage类的v向量。

正如其他人已经指出的那样,暴露载体本身可能并不是一个好主意;您可以考虑使用迭代器或访问者模式。

此外,指针的使用 - 不是绝对需要 - 在较大的项目中通常相当不赞成。考虑使用智能指针,例如std::auto_ptr(虽然由于奇怪的复制语义而不是很值得推荐),或者更容易理解boost::shared_ptr / std::tr1::shared_ptr

正如旁注,第一个和第二个代码示例(至少在大多数现代编译器中)具有完全相同的性能,因为在第一种情况下,编译器可以优化临时返回值(请查看“返回”价值优化“)。

答案 3 :(得分:0)

请注意,如果复制了类存储的对象,您的存储类将导致问题。由于您不提供复制构造函数或赋值运算符,因此将使用默认值。默认情况下会盲目地复制指针向量,现在你有两个存储对象,它们都试图删除向量中的指针。

答案 4 :(得分:0)

  

根据我的理解,当s返回向量并分配给vm时,这将作为副本(按值)完成。因此,如果s超出范围并将其称为析构函数,则vm具有自己的副本,其结构不受影响。

普通指针(T*)后面没有std::vector的复制构造函数或赋值运算符(尽管你可以使用一个可复制的智能指针类)。由于指针指向(“指向”)的内容未被复制,因此复制操作是浅拷贝。虽然s.v上的操作不会影响vm(反之亦然),但影响从一个访问的指针的任何内容都会影响另一个,因为它们是相同的。

如果要在s.v而不是MyStorageClass*中存储经过适当实施的智能指针,标准std::vector复制操作可能会生成深层副本,因此{{1}的内容可以改变而不会以任何方式影响s.v的内容(反之亦然)。复制指针的复制操作将使用指向类的复制操作来复制指向的对象。因此,每个尖头对象只能由一个复制指针指向。

或者(正如其他人提到的那样),您可以使用允许共享所有权的智能指针,让它管理指向的对象,取消vm中的delete调用。

  

但是,通过值传递效率不高。

但是,在某些情况下,这是正确的,特别是当变异容器的一个实例不得影响另一个(你提到的情况)时,在这种情况下,如果没有副本,你就无法逃脱。正确胜过王牌。一般来说,您可以使用copy-swap idiom来减少由于临时创建而导致的低效率。据我所知,大多数STL实现不使用~storage的复制交换。仅在更改向量时执行副本的Copy-on-write也可以提供帮助。线程化使copy-on-write变得复杂,并且会导致效率低下。

  

现在,std::vector保留vm的地址。

v没有地址。虽然引用可能使用引擎盖下的指针(它们也可能不会;它们甚至可能不需要额外的存储,根据C ++ 03的§8.3.2-3),在语言级别引用不是指针。考虑您可以使用空指针,但不能使用空引用。更准确的说法是vm别名为vmvvm引用相同的对象),这就是v上的操作会影响的原因v(反之亦然)。