【问题标题】:When initializing a struct in a loop, why is the address always the same?在循环中初始化结构时,为什么地址总是相同的?
【发布时间】:2021-09-13 02:54:26
【问题描述】:

在 c++17 中,我想初始化一个结构 10 次并将引用它们的 ptr 推入堆栈。我的代码如下所示。

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

stack<TreeNode*> s;
for (int i = 0; i <= 9; i++) {
    TreeNode node(i);
    s.push(&node);
    cout << &node << endl;
}

但是,当我打印在每个循环中初始化的结构的地址时,所有地址都是一样的。

0000003F439BFBA8
0000003F439BFBA8
0000003F439BFBA8
0000003F439BFBA8
0000003F439BFBA8
0000003F439BFBA8
0000003F439BFBA8
0000003F439BFBA8
0000003F439BFBA8
0000003F439BFBA8

这样,就相当于下面的代码

stack<TreeNode*> s;
TreeNode node(-1);
for (int i = 0; i <= 9; i++) {
    node.val = i;
    s.push(&node);
    cout << &node << endl;
}

我知道在 c++ 中,new 用于为堆上的对象分配新的内存块。但是我还是不明白为什么地址是一样的。

如果您能帮助我或给我一些参考,我将不胜感激。

【问题讨论】:

  • 地址总是相同的,因为它是一个局部变量。此外,它的生命周期在循环结束时结束,因此存储其地址不是一个好主意,因为您存储了一个指向不再存在的对象的指针。将来使用该指针是未定义的行为。
  • 每个循环迭代都会创建一个node,它会一直存在到迭代结束。在迭代结束时,它的销毁和它的地址可以被另一个实例重用。每次迭代,您都会保存一个指向 不同 node 的指针,该指针恰好与不再存在的旧 TreeNode 对象具有相同的地址。
  • 谢谢!但是当我在循环中使用new初始化节点时,每个循环的地址都不一样。这是否意味着如果一个变量用new 初始化,如果我从不手动释放它,它的生命周期将一直持续到整个程序结束?
  • @Rinne:正确。我们甚至有一个术语“忘记删除新的对象”,称为内存泄漏
  • @Rinne 是的,new 的目的是当您需要一个比函数范围更长的对象时。但在现代 C++ 中,它很少使用。一旦确定不再需要该对象,您完全有责任记住使用delete 最终销毁该对象。有更安全的替代方案,例如 std::make_uniquestd::make_shared 应该是首选。

标签: c++ loops memory struct c++17


【解决方案1】:

C++ 在循环的每次迭代中构造TreeNode node。但它也通过在每个循环结束时调用(隐式)析构函数来解构node

这意味着内存可以重复使用。而且由于这个例子很简单,内存确实在每次循环迭代中都被重用了。

如果您使用new 而不使用delete,则析构函数不会运行,并且内存不可重复使用。

【讨论】:

    【解决方案2】:

    那里发生的情况是,C++ 足够明智地在循环中只初始化一次局部变量。因为它是一个局部变量,所以它使用相同的内存地址,当node被初始化时,C++分配了node的内存地址,然后当C++检查node是否被初始化时,发现它已经分配了内存并跳过该步骤.

    如果您保留该代码,您可能会遇到问题,因为您将本地 var 的地址推送到 s,但是如果 s 不在此函数中会发生什么情况? s 将继续存在,但您推送的元素是函数的本地元素,因此函数之外的 s 最终会得到一个未初始化的元素。

    你需要做的是创建一个指针,使用new分配更多的内存,初始化指针然后压入指针。

    TreeNode* Node = new TreeNode;
    s.push(Node);
    

    【讨论】:

    • 当然,实际上不要这样做。使用unique_ptr,或者直接存储stack&lt;TreeNode&gt;。避免使用原始的new
    • 谢谢!在某些带有 GC 的语言中,例如 C#,如果我使用 var a = new A() 来初始化一个对象,当不再引用它时,它将被销毁。然后在c++中,如果我使用new分配新内存,如果我从s弹出所有指针会发生什么?它们是否仍然存在于内存中而我只是找不到它们,或者从堆中释放它们?
    • @Rinne 如果您使用原始的new 分配它,那么您负责delete 分配它。如果您丢失了所有指针,它将保持分配状态,并且实际上丢失了。如上,使用unique_ptr,这样所有权语义更清晰。
    • 这个答案是错误的。 C++ 肯定会在循环的每次迭代中初始化局部变量。
    • @Rinne 不,当最后一个指针超出范围时,C++ 不会自动销毁由new 创建的对象。它将保持存在,您无法获得指向它的指针,它只会消耗内存。但是标准库提供了像std::unique_ptrstd::shared_ptr 这样的智能指针,它们将为您实现这种自动销毁。 std::shared_ptr 的行为很像 C# 中的引用。 C++ 中的 auto x = std::make_unique&lt;T&gt;(); 几乎等同于 C# 中的 var x = new T();。在 C++ 中,您必须小心循环引用,这在 C# 中为您处理。
    【解决方案3】:

    cigien 的回答很有帮助!但是有一个说法是,如果变量在循环中只初始化一次,或者在循环中初始化10次。

    所以我对代码做了一点改动,是这样的:

    struct TreeNode {
        int val;
        TreeNode *left;
        TreeNode *right;
        TreeNode(int x) : val(x), left(NULL), right(NULL) {
            cout << x << endl;
        }
    };
    stack<TreeNode*> s;
    for (int i = 0; i <= 9; i++) {
        TreeNode node(i);
        s.push(&node);
        cout << &node << endl;
    }
    

    那么,结果是:

    0
    00000049F28FF518
    1
    00000049F28FF518
    2
    00000049F28FF518
    3
    00000049F28FF518
    4
    00000049F28FF518
    5
    00000049F28FF518
    6
    00000049F28FF518
    7
    00000049F28FF518
    8
    00000049F28FF518
    9
    00000049F28FF518
    

    我想知道这是否表明该结构是在每个循环中创建并在每个循环结束时销毁的。

    【讨论】:

      猜你喜欢
      • 2016-02-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-12-17
      • 1970-01-01
      • 1970-01-01
      • 2011-03-02
      相关资源
      最近更新 更多