10

列表的标准模板库文档说:

无效 push_back ( const T& x );

在末尾添加元素 在列表的末尾添加一个新元素,紧跟在其当前最后一个元素之后。这个新元素的内容被初始化为 x 的副本。

这些语义与 Java 语义有很大不同,让我感到困惑。我缺少 STL 中的设计原则吗?“一直在复制数据”?这让我害怕。如果我添加对对象的引用,为什么要复制对象?为什么不只是对象通过?

这里必须有一个语言设计决定,但我在 Stack Overflow 和其他网站上找到的大多数评论都集中在与所有这些对象复制都可能引发异常的事实相关的异常引发问题上。如果您不复制,而只处理引用,那么所有这些异常问题都会消失。很迷茫。

请注意:在我使用的这个遗留代码库中,boost 不是一个选项。

4

5 回答 5

11

STL 总是准确地存储您告诉它存储的内容。list<T>始终是一个列表,T因此所有内容都将按值存储。如果您想要一个指针列表,请使用list<T*>,这将类似于 Java 中的语义。

这可能会诱使您尝试list<T&>,但这是不可能的。C++ 中的引用与 Java 中的引用具有不同的语义。在 C++ 中,引用必须初始化为指向一个对象。一个引用被初始化后,它会一直指向这个对象。你永远不能让它指向不同的对象。这使得在 C++ 中不可能有一个引用容器。Java 引用与 C++ 指针更密切相关,因此您应该使用list<T*>, 代替。

于 2012-10-16T14:12:52.600 回答
6

它被称为“价值语义”。C++ 通常被编码为复制值,与 Java 不同的是,除了原始类型之外,您复制引用。它可能会吓到你,但我个人觉得 Java 的引用语义更让我害怕。但是在 C++ 中,您可以选择,如果您想要引用语义,只需使用指针(最好是智能指针)。那么你会更接近你习惯的Java。但请记住,在 C++ 中没有垃圾收集(这就是您通常应该使用智能指针的原因)。

于 2012-10-16T14:07:50.057 回答
4

您不添加对对象的引用。你通过引用传递一个对象。那不一样。如果您没有通过引用传递,甚至在实际插入之前可能已经制作了额外的副本。

它会复制一份,因为您需要一份副本,否则代码如下:

std::list<Obj> x;
{
   Obj o;
   x.insert(o);
}

会留下一个无效对象的列表,因为o超出了范围。如果您想要类似于 Java 的东西,请考虑使用shared_ptr. 这为您提供了您在 Java 中习惯的优势 - 自动内存管理和轻量级复制。

于 2012-10-16T14:09:10.507 回答
4

实际上,Java 的工作方式相同。请允许我解释一下:

Object obj = new Object();
List<Object> list = new LinkedList<Object>();
list.add(obj);

是什么类型的obj?它是Object. 实际的对象在堆上的某个地方浮动——在 Java 中您唯一能做的就是传递对它的引用。您将对该对象的引用传递给列表的add方法,列表本身就存储了该引用的副本。您可以稍后修改命名引用obj,而不会影响存储在列表中的该引用的单独副本。(当然,如果您修改对象本身,您可以通过任一引用看到该更改。)

C++ 有更多选择。您可以模拟 Java:

class Object {};
// ...
Object* obj = new Object;
std::list<Object*> list;
list.push_back(obj);

是什么类型的obj?它是一个指向Object. 当您将它传递给列表的push_back方法时,列表本身会存储该指针的副本。这与 Java 具有相同的语义。

但是,如果您从效率的角度考虑…… C++ 指针/Java 引用有多大?4 字节或 8 字节,具体取决于您的架构。如果您关心的对象大约是那个大小或更小,为什么还要费心把它放在堆上,然后到处传递指向它的指针呢?只需传递对象:

class Object {};
// ...
Object obj;
std::list<Object> list;
list.push_back(obj);

现在,obj是一个实际的对象。您将它传递给列表的push_back方法,该方法本身存储该对象的副本。在某种程度上,这是一个 C++ 习语。它不仅对指针是纯开销的小对象有意义,而且在非 GC 语言中它也使事情变得更容易(堆上没有任何可能意外泄漏的东西),并且如果对象的生命周期自然绑定到列表中(即,如果它从列表中删除,那么在语义上它应该不再存在),那么您不妨将整个对象存储在列表中。它还具有缓存局部性优势(std::vector无论如何,当在 中使用时)。


你可能会问,“那为什么push_back要使用引用参数呢?” 有一个足够简单的理由。每个参数都按值传递(同样,在 C++ 和 Java 中)。如果你有一个std::listof Object*,那很好——你传入你的指针,然后复制那个指针并传递给push_back函数。然后,在该函数内部,制作该指针的另一个副本并将其存储到容器中。

这对指针来说很好。但是在 C++ 中,复制对象可以任意复杂。复制构造函数可以做任何事情。在某些情况下,将对象复制两次(一次复制到函数中,再复制到容器中)可能会导致性能问题。所以push_back通过 const 引用获取它的参数——它从原始对象直接复制到容器中。

于 2012-10-16T14:21:23.853 回答
2

如果没有引用计数,就无法维持共享所有权,因此通过复制来维持单一所有权。

考虑一个常见的情况,你想将一个堆栈分配的对象添加到一个比它更长寿的列表中:

void appendHuzzah(list<string> &strs) {
  strs.push_back(string("huzzah!"));
}

该列表无法保留原始对象,因为该对象在超出范围时将被销毁。通过复制,列表获得了自己的对象,其生命周期完全在自己的控制之下。如果不是这样,那么这种直接的用法就会崩溃并且毫无用处,我们将始终不得不使用指针列表。

Java 区分原始类型和引用类型。在 C++ 中,所有类型都是原始类型。

于 2013-11-23T06:26:48.827 回答