std::vector内存处理

23

我尝试通过谷歌和搜索来回答我的问题,但是我找不到任何有效的解释,因此我在这里发布我的问题。以下是我的示例代码和输出:

#include <iostream>
#include "vector"
using namespace std;

typedef struct Node{
    int data;
    Node(){
        data = 0;
        std::cout << "Node created. " << this <<'\n';
    }
    ~Node(){
        data = 0;
        std::cout << "Node destroyed. " << this <<'\n';
    }
} Node;

int main() {
    std::vector<Node> vec;
    for(int i = 0; i < 2 ; i++)
       vec.push_back( *(new Node));
    return 0;
}

输出:

Node created. 0x9e0da10
Node created. 0x9e0da30
Node destroyed. 0x9e0da20
Node destroyed. 0x9e0da40
Node destroyed. 0x9e0da44

为什么有一个额外的销毁,以及创建的对象和销毁的对象不同?


18
你没有追踪复制构造函数。此外,C++不同于Java--你只需要使用vec.push_back(Node()); - PaulMcKenzie
34
vec.push_back( *(new Node)); 这段代码也让我感到身体疼痛。 - underscore_d
20
那整个 "typedef struct Node {...} Node" 的东西告诉我你正接受一个 C 语言专家的 C++ 教学,你需要找到一本更好的书或者教练。 - Kate Gregory
4
啊啊啊我的内心已经变得像泥一样了。 - Mateen Ulhaq
2
通常情况下,当您想要跟踪生命周期时,您还需要对隐式提供的构造函数和赋值运算符进行仪器化!也就是说,复制构造函数、默认构造函数(即使在类中没有显式声明)和复制赋值运算符。您可以创建一个可重用的 class noisy,并在需要添加此仪器的地方继承它。 - JDługosz
显示剩余3条评论
4个回答

38

vec.push_back(*(new Node)); 是一种立即的内存泄漏。

首先你动态分配了一个 Node,然后将该 Node 复制到向量中。复制操作是创建新对象的原因,这就是为什么 this 不同的原因。

原始的(动态分配的)Node 永远不会被释放,但是复制品会在向量的析构函数运行时释放(即在函数结束时)。

为什么有三次析构函数调用而不是两次?这是由于当你 push_back 时,向量自动重新分配内存,将它的元素移动/复制到一个新的内存位置,并销毁旧的元素。


请注意,通常情况下,当你只想要一个包含 n 个默认构造元素的向量时,你应该这样做:

std::vector<Node> vec(2);

这会调用Node()(默认构造函数)来为元素vec[0]vec[1]进行初始化,您不需要循环(或动态分配)。


19
如果您添加一个复制构造函数,您会发现答案是什么:
Node created. 0x60200000df90
Node copied: 0x60200000df70  Source: 0x60200000df90
Node created. 0x60200000df50
Node copied: 0x60200000df34  Source: 0x60200000df50
Node copied: 0x60200000df30  Source: 0x60200000df70
Node destroyed. 0x60200000df70
Node destroyed. 0x60200000df34
Node destroyed. 0x60200000df30

当你向向量中添加第二个元素时,存储空间不足,向量必须调整大小并将所有旧元素(在本例中仅有一个这样的元素)复制到新空间。这就是为什么会调用一个额外的复制构造函数。

是的,在C++中,所有标准容器都要求对象应该是可复制或可移动构造的,因此你不必像在Java中那样在堆上创建它们。更正确的代码应该是:

#include <iostream>
#include "vector"


struct Node {
  int data;
  Node() : data(0) {
    std::cout << "Node created. " << this <<'\n';
  }

  Node(const Node &n) : data(n.data) {
    std::cout << "Node copied: " << this << "  Source: " << &n << std::endl;
  }

  ~Node(){
    // You don't need data = 0 in destructor
    std::cout << "Node destroyed. " << this <<'\n';
  }

};

int main() {
  std::vector<Node> vec;
  for(int i = 0; i < 2 ; i++) {
    vec.push_back(Node());
  }
  return 0;
}

这真是太有道理了,非常感谢! - Arup
1
如果我能给这个答案+100分,我一定会的。所有其他答案都试图解释为什么而不是什么(或者至少没有展示出什么)。只有在你理解发生了什么之后,为什么才有用。 - bcrist

12

当你向向量中添加元素时,通过复制构造函数(或移动构造函数)构建一个新的副本。

因此,像vec.push_back(*(new Node));这样的一行代码会执行两个操作:

  • 它通过new动态分配了一个新的Node。
  • 它将新节点复制到向量中。这意味着通过复制构造函数创建了一个新节点。
  • 原始节点永远不会被释放(这称为内存泄漏)。

使用复制构造函数的此版本代码可能会为您提供一些见解:http://ideone.com/ow5YOI


1
你的第一行有误。使用_rvalue_通过push_back添加可以使用移动构造函数。使用emplace_back添加则是就地构造,无需复制。 - underscore_d
如果您担心包含过多信息,那么简化可能是好的选择,但在我看来,只有当完全简化时才是如此,例如“在这种特定情况下,使用了复制构造函数”,而通过省略重要信息部分地简化可能会在以后引起混淆。但我只是在一般情况下说的。 :) - underscore_d
是的,我在发布评论后就发现你是正确的了 :') - Louen

4

您的循环等同于

vec.push_back( *(new Node));
vec.push_back( *(new Node));

发生的情况如下:
  1. new Node 分配内存并创建一个节点 (节点已创建)
  2. push_back 分配新的存储空间
  3. push_back 在向量的存储空间中使用(隐式)复制构造函数创建了一个 Node 对象(没有消息打印)
  4. 你创建的 Node 被泄漏了(存在 2 个节点,其中 1 个不可达)
  5. new Node 分配内存并创建一个节点 (节点已创建)
  6. push_back 分配新的存储空间,并复制/移动现有元素(没有消息打印)
  7. push_back 删除其旧内容 (节点已删除)
  8. push_back 在向量中创建另一个 Node,使用(隐式)复制构造函数(没有消息打印)
  9. 你创建的 Node 被泄漏了(存在 4 个节点,其中 2 个不可达)

当向量超出范围时,它包含的两个副本将被删除。(节点已删除节点已删除


通常应编写以下内容

vec.push_back(Node())

或者

vec.emplace_back()

如果您真正想了解正在发生的事情,您应该创建一个非默认的复制构造函数:


而不是使用默认的复制构造函数。

Node(const Node& other){
    data = other.data;
    std::cout << "Node created. " << this
              << " from " << &other << std::endl;
}

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