C++:在函数内创建并将其作为结果返回新对象时,必须使用new运算符来创建对象吗?

11

我有两个虚拟问题困扰了我一段时间,我在网上搜索并阅读了很多C++教程,但是我无法找到确切的答案。

假设我们有一个名为Node的类,它是单向链表的基本构建块。

class Node
{
   int data;
   Node* next;
}

事实1:局部变量(非静态)在对应函数退出时会被销毁。

问题1:那么以下情况呢:

Node* func()
{ 
    Node n; 
    Node* ptr=&n; 
    return n;
}

节点n会被销毁吗?或者我们必须使用new运算符创建节点并返回指向堆内存的指针。如果两种方法都可以,哪种方法更好?

问题2:如何为节点类编写析构函数?(我在stackOverflow上找到了一些类似的问题,但这些答案侧重于链表的析构函数。我已经明白了那部分内容。我想要的正是一个Node类的析构函数)。

---------------------------------------编辑------------------------------------

感谢所有给我建议或指出我的错误的人。我认为我已经得到了答案。以下是我从你们的答案中摘录的笔记,真的解决了我的困惑。

  1. 不建议从函数返回栈内存地址,因为这会导致未定义行为。
  2. 返回堆内存是可以的,但我们必须注意对象的销毁。
  3. 另一种替代方法是返回一个对象,从复制构造函数中受益。

2
问题1的代码是否能够编译通过? - Kerrek SB
@KerrekSB,感谢您的快速回复。我正在复习C++编程,这个问题突然在我的脑海中浮现,我随手打了一些代码。我刚刚测试了一下,发现它实际上无法编译。已经更新了一个编译后的代码版本,但问题仍然存在。 - user3367047
1
@user3367047,它仍然无法编译。该函数的返回类型为Node*但你正在返回一个Node,即n - eerorika
4个回答

14

问题1

Node* func() { Node n; Node* ptr=&n; return n;}

你的代码创建了一个本地的Node实例(在堆栈上),然后返回它的地址。当函数返回时,由于Node实例是一个局部变量,所以会被销毁。函数返回的地址现在指向一些具有未定义内容的内存,任何试图解引用此指针的尝试都将导致未定义的行为。
为了创建一个节点,你需要调用Node构造函数。如何返回结果与如何调用构造函数相关。
  • You can either return a pointer as you were trying to do, in which case you need to use the new operator:

      Node* func() { 
        Node* n = new Node(10); 
        return n;
      }
    

    However, when you do this, you give func callers the responsibility to destroy the object in question. Since new and delete are symmetrical operations, it is considered better form to put them in symmetrical places in your code, e.g. like this:

      void cnuf(Node* p) { 
        delete p; 
      }
    

    A better alternative altogether may be to use std::shared_ptr which gives you reference counting, like this:

      std::shared_ptr<Node> func() {
        return std::make_shared<Node>(10);
      }
    

    Using this approach, the callers do not need to manually manage each node's lifecycle. Another alternative is using std::unique_ptr instead, which only allows single object ownership.

  • Or you can return the node by value, in which case you create it locally, and then let the function return mechanisms make a copy when you return it:

      Node func() { 
        Node n(10); 
        return n;
      }
    

问题2

你可以在 Node 类的声明中像下面这样声明一个析构函数:

class Node {
  ...
  ~Node();
}

然后,您可以像这样定义它:

Node::~Node() {
  ...
}

然而,实际上最好是让列表来管理其 Node 实例 (next 字段) 之间的连接,只让 Node 类来管理其成员数据的生命周期(data 字段)


2
你可以返回指向局部对象的指针,但它将指向堆栈内存,因此结果可能会令人惊讶。看下面的代码:
#include <iostream>

using namespace std;

class Node { public: int n; };

Node* create(int n) {
    Node node = Node();
    node.n = n;
    cout << "Created " << node.n << endl;
    return &node;
}

int main() {
   Node* n1 = create(10);
   Node* n2 = create(20);
   cout << "Reading " << n1->n << endl;
   cout << "Reading " << n2->n << endl;
   return 0;
}

您不会得到“10”“20”的输出。相反,
Created 10
Created 20
Reading 20
Reading 1891166112

第一个对象被销毁了(当第一个create函数调用结束时)。第二个对象是在已经被销毁的n1上创建的,因此n1地址等于n2地址。

当您返回堆栈地址时,编译器会警告您:

main.cpp: In function Node* create(int):
main.cpp:8:10: warning: address of local variable node returned [-Wreturn-local-addr]
     Node node = Node();

非常棒的回答,附带实用的实验。非常有帮助。谢谢! - user3367047

1
你的意思可能是:“

你所做的应该是:

”。
Node* func()
{ 
    Node n(10); 
    return &n;
}

但这会导致未定义的行为,因为 Node n 将在堆栈上而不是在您的控制下。
Node* func()
{ 
    Node* n = new Node(10); 
    return n;
}

那应该可行,但你需要在析构函数中释放 Node*
如果你可以使用c++11特性,我可能会选择 std::unique_ptr<Node>
class Node
{
   int data;
   std::unique_ptr<Node> next;
}
std::unique_ptr<Node> func()
{ 
    std::unique_ptr<Node> n(new Node(10)); 
    return n;
}

那样,您的Node将会在std::unique_ptr<>的析构函数中被释放。

很棒的答案!std::unique_ptr<>是一个不错的选择。但我认为为其他方面提供一个有效的析构函数是一个好的实践。我是对的吗? - user3367047

0

不,你不必使用 new 来从函数返回一个对象。你可以返回一个本地对象的副本。

Q1: n 节点会被销毁吗?

根据新代码编辑:

函数的返回类型为 Node*,但你正在返回一个 Node,这是无法编译的例子。局部变量 Node n 在函数结束时将被销毁,是的,与所有具有自动存储(非静态局部变量)的变量一样。

根据旧代码的答案:

你有一个 Node*,也就是一个指针。你尝试用整数 10 初始化指针,但那不应该编译。如果代码可以编译,指针的副本将被返回,并且局部指针将在函数结束时被销毁。没有创建任何 Node 实例。解引用返回的指针将具有未定义的行为,除非你绝对确定在初始化指针的内存地址上分配了一个 Node 实例(这在所示的代码中没有发生)。

Q2: 如何为节点类编写析构函数?

这取决于你的设计。如果Node的内存由封闭类(例如List)管理,你可以选择在Node的析构函数中不做任何操作。如果Node负责链接的Node的内存,你需要动态分配实例(使用new)并在析构函数中使用delete。如果使用std::unique_ptr,则不需要手动删除。


非常好的答案。谢谢! - user3367047

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