返回指针的矢量-理解
Returning vector of pointers - understanding
我正在努力理解以下内容(让我们假设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()
{
storage s;
vector<MyStorageClass*> &vm = s.getItems();
}
现在,vm
保存v
的地址。所以它会受到存储析构函数的影响。
问题:我上面说的是真的吗?
是的,您已经正确理解了这一点。现在,如果你想把它提升到一个新的水平,首先要找到它不好的原因:
- 返回指向类内部的指针或引用会破坏封装
- 一旦对应的
s
-对象被破坏,您得到的引用将变成一个悬空引用,从而打破了引用始终有效的不变量 - 通过返回一个指针向量,你会让调用者怀疑他是否必须删除这些指针
一个更好的解决方案是storage
公开从s
返回相应迭代器的方法begin
和end
。或者,对于需要在s
上操作的算法,可以使用Visitor模式。
此外,在您的情况下,向量s
似乎应该拥有它所包含的对象。这将是使用CCD_ 22的良好指标。
即使向量复制了它的值,但在您的情况下,值是指针,而不是指向的对象。因此,"vm
有自己的副本"并不完全正确:第一段代码中的结果向量将有指针的副本,而不是它们指向的MyStorageClass
对象的副本;因此,实际上,在您的所有3个代码示例中,如果调用Storage析构函数,则存储在vm
中的指针将不再有效!
不过,如果您需要确保Storage
在最后一次访问某个MyStorageClass对象之前没有被破坏,那么,在所提供的方法中,第三种方法将是首选方法,因为矢量数据(即指针(在内存中只存在一次。但是,您应该考虑返回一个const
向量,否则getItems的每个调用方都可以通过返回的引用修改Storage类的v
向量。
正如其他人已经指出的那样,首先暴露矢量本身可能不是一个好主意;您可以考虑使用迭代器或Visitor模式。
此外,指针的使用——在不是绝对需要的地方——在更大的项目中通常是不受欢迎的。考虑使用智能指针,如std::auto_ptr
(尽管由于奇怪的复制语义,但不太推荐(,或更易于理解的boost::shared_ptr
/std::tr1::shared_ptr
。
顺便说一句,第一个和第二个代码示例(至少在大多数现代编译器中(具有完全相同的性能,因为在第一种情况下,编译器可以优化临时返回值(请参阅"返回值优化"(。
我认为你所说的是真的,但我对目的有点困惑。
vector<MyStorageClass*> &vm = s.getItems();
通过引用获取向量。但是向量包含指针,所以超出范围的向量一开始不会导致任何析构函数运行——只有当这些是某种智能指针时,析构函数才会运行(即使这样也取决于情况(。
因此,您可以很高兴地按值传递指针向量,而不会产生太大问题。我相信你通过参考来节省一些效率,但我不认为它像你想象的那么严重。
此外,您的指向对象是用new(动态(创建的,因此您可以以相同的方式按值返回向量,而不必担心丢失指向对象。
所以,我再次认为你的逻辑很好,你的引用方式也更有效率,但我只是想确保你知道它可以双向工作,不会有问题:((由于它是指针的矢量,按值计算也不算太差(。
附言:就参考而言,你确实需要担心悬挂,这可能比比约恩上面指出的你想象的更令人沮丧。如果你曾经使用过string.c_str((,你可能已经尝到了这种味道。如果从字符串中获取.c_str((,而原始字符串超出了作用域,则.c/str((返回的指针是悬空的(指向不再用于此目的的内存(,访问它会导致未定义的行为。因此,by值可能是更好的选择(但它确实取决于您的设计——例如,如果这将是一个在应用程序期间持续存在的单例,那么悬空可能不是问题(。
请注意,如果复制类存储的对象,您的存储类将导致问题。由于您没有提供复制构造函数或赋值运算符,因此将使用默认值。默认情况下,将盲目复制指针的矢量,现在您有两个存储对象,它们都将尝试删除矢量中的指针。
根据我的理解,当
s
返回向量并分配给vm
时,这是作为副本(按值(完成的。因此,如果s
超出范围并调用它析构函数,vm
就有自己的副本,其结构也不会受到影响。
普通指针(T*
(后面没有std::vector
的复制构造函数或赋值运算符(尽管您可以使用一个可以复制的智能指针类(。由于指针指向的内容("已指向的"(不会被复制,因此复制操作是浅层复制。虽然对s.v
的操作不会影响vm
(反之亦然(,但任何影响从一个访问的指向的操作都会影响另一个,因为它们是相同的。
如果要在s.v
而不是MyStorageClass*
中存储一个适当实现的智能指针,则标准的std::vector
复制操作可以产生深度复制,因此可以在不以任何方式影响vm
内容的情况下更改s.v
的内容(反之亦然(。复制指针的复制操作将使用指向类的复制操作来复制指向对象。因此,每个被指向的对象将仅由一个复制指针指向。
或者(正如其他人所提到的(,您可以使用允许共享所有权的智能指针,并让它管理指向的对象,从而取消~storage
中的delete
调用。
然而,传递价值是无效的。
然而,在某些情况下,这是正确的,特别是当容器的一个实例发生变化时,不能影响另一个实例(您提到的情况(,在这种情况下,没有副本就无法逃脱。正确胜过有效。一般来说,您可以使用副本交换习惯用法来减少由于临时创建而导致的效率低下。据我所知,大多数STL实现都不使用std::vector
的副本交换。"写时复制"(Copy on write(也有帮助,它将仅在矢量更改时执行复制。线程化使写时复制变得复杂,并可能导致效率低下。
现在,
vm
保存v
的地址。
vm
没有地址。虽然引用可以在引擎盖下使用指针(它们也可能不使用;根据C++03的§8.3.2-3,它们甚至可能不需要额外的存储(,但在语言级别,引用不是指针。请考虑您可以有空指针,但不能有空引用。更准确的说法是vm
被别名为v
(vm
和v
指同一对象(,这就是为什么对v
的操作会影响vm
(反之亦然(。