我正在创建类似于结构列表的东西。在main的开头我声明了一个空指针。然后我多次调用insert()函数,传递对该指针的引用,以添加新元素。
然而,似乎有些不对劲。我无法显示列表的元素,std::cout
只是打破了程序,即使编译器没有警告。
#include <iostream>
struct node {
node *p, *left, *right;
int key;
};
void insert(node *&root, const int key)
{
node newElement = {};
newElement.key = key;
node *y = NULL;
std::cout << root->key; // this line
while(root)
{
if(key == root->key) exit(EXIT_FAILURE);
y = root;
root = (key < root->key) ? root->left : root->right;
}
newElement.p = y;
if(!y) root = &newElement;
else if(key < y->key) y->left = &newElement;
else y->right = &newElement;
}
int main()
{
node *root = NULL;
insert(root, 5);
std::cout << root->key; // works perfectly if I delete cout in insert()
insert(root, 2);
std::cout << root->key; // program breaks before this line
return 0;
}
如您所见,我在insert函数中创建了新的结构元素并将其保存在根指针中。在第一次调用中,while循环甚至没有启动,因此它可以工作,并且我能够在main函数中显示root的元素。
但是在第二次调用中,while循环已经有效,我得到了我描述的问题。
root->key
语法有问题,因为即使我把它放在第一次调用中它也不起作用。
出了什么问题,原因是什么?
,我总是看到通过这样的指针插入新列表的元素:
node newElement = new node();
newElement->key = 5;
root->next = newElement;
此代码是否等于:
node newElement = {};
newElement.key = 5;
root->next = &newElement;
?它会更清洁,不需要删除内存。
答案 0 :(得分:2)
问题是因为您正在从函数中传递指向局部变量的指针。取消引用此类指针是未定义的行为。您应该使用newElement
分配new
。
此代码
node newElement = {};
创建一个局部变量newElement
。函数结束后,newElement
的范围结束,其内存被破坏。但是,您将指向已销毁内存的指针传递给函数外部。一旦函数退出,对该存储器的所有引用都将变为无效。
此代码另一方面
node *newElement = new node(); // Don't forget the asterisk
在免费商店分配对象。在明确delete
之前,此类对象仍然可用。这就是为什么你可以在创建它们的函数退出后使用它们。当然,由于newElement
是指针,因此您需要使用->
来访问其成员。
答案 1 :(得分:1)
您需要了解的关键是 stack 分配的对象和 heap 分配的对象之间的区别。在插入函数中,node newElement = {}
是堆栈分配的,这意味着它的生命周期由封闭范围决定。在这种情况下,这意味着当函数退出对象时会被销毁。那不是你想要的。您希望树的根存储在node *root
指针中。为此,您需要从堆中分配内存。在C ++中,通常使用new运算符完成。这允许您将指针从一个函数传递到另一个函数,而不会将其生命周期确定为它所在的范围。这也意味着您需要注意管理堆分配对象的生命周期。
答案 2 :(得分:0)
你的同样评论有一个问题。第二个可能更干净,但这是错误的。你需要新的内存并删除它。否则,您最终会得到指向不再存在的对象的指针。这正是新解决的问题。
另一个问题
void insert(node *&root, const int key)
{
node newElement = {};
newElement.key = key;
node *y = NULL;
std::cout << root->key; // this line
在第一个插入根目录仍为NULL,因此该代码将使程序崩溃。
答案 3 :(得分:0)
已经解释过你必须动态分配对象(使用new
),但这样做会带来危险(内存泄漏)。
有两个(简单)解决方案:
1所有权计划
在C和C ++中,有两种形式可以获取存储对象的内存:自动存储和动态存储。例如,当您在函数中声明变量时,自动就是您使用的,但是这些对象仅在函数的持续时间内存在(因此,之后使用它们时会遇到问题,因为内存可能被其他内容覆盖)。因此,您经常必须使用动态内存分配。
动态内存分配的问题是您必须明确地将其返回给系统,以免泄漏。在C中,这非常困难,需要严谨。在C ++中,虽然通过使用智能指针使其变得更容易。那就让我们用吧!
struct Node {
Node(Node* p, int k): parent(p), key(k) {}
Node* parent;
std::unique_ptr<Node> left, right;
int key;
};
// Note: I added a *constructor* to the type to initialize `parent` and `key`
// without proper initialization they would have some garbage value.
请注意parent
和left
的不同声明?父母拥有其子女(unique_ptr
),而孩子只是指其父母。
void insert(std::unique_ptr<Node>& root, const int key)
{
if (root.get() == nullptr) {
root.reset(new Node{nullptr, key});
return;
}
Node* parent = root.get();
Node* y = nullptr;
while(parent)
{
if(key == parent->key) exit(EXIT_FAILURE);
y = parent;
parent = (key < parent->key) ? parent->left.get() : parent->right.get();
}
if (key < y->key) { y->left.reset(new Node{y, key}); }
else { y->right.reset(new Node{y, key}); }
}
如果你不知道unique_ptr
是什么,get()
它只包含一个用new
分配的对象,get()
方法返回一个指向该对象的指针。您还可以reset
其内容(在这种情况下,它可以正确处理已包含的对象,如果有的话)。
我会注意到我对你的算法不太确定,但是,嘿,这是你的:)
2 Arena
如果这种处理内存的问题让你的头脑变得糊涂,那一开始就很正常,这就是为什么有时竞技场可能更容易使用。使用竞技场的想法非常普遍;而不是一个接一个地打扰内存所有权,你使用“东西”来保持内存,然后只操纵引用(或指针)。你只需要记住,只要竞技场是,那些引用/指针才会存在。
struct Node {
Node(): parent(nullptr), left(nullptr), right(nullptr), key(0) {}
Node* parent;
Node* left;
Node* right;
int key;
};
void insert(std::list<Node>& arena, Node *&root, const int key)
{
arena.push_back(Node{}); // add a new node
Node& newElement = arena.back(); // get a reference to it.
newElement.key = key;
Node *y = NULL;
while(root)
{
if(key == root->key) exit(EXIT_FAILURE);
y = root;
root = (key < root->key) ? root->left : root->right;
}
newElement.p = y;
if(!y) root = &newElement;
else if(key < y->key) y->left = &newElement;
else y->right = &newElement;
}
记住两件事:
arena
,它会一直增长,直到消耗掉所有可用内存并导致程序崩溃;在某些时候你需要清理!