user2252786
user2252786

Reputation: 1635

Pointers and reference issue

I'm creating something similar to structure list. At the beginning of main I declare a null pointer. Then I call insert() function a couple of times, passing reference to that pointer, to add new elements.

However, something seems to be wrong. I can't display the list's element, std::cout just breaks the program, even though it compiler without a warning.

#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;
}

As you can see, I create new structure element in insert function and save it inside the root pointer. In the first call, while loop isn't even initiated so it works, and I'm able to display root's element in the main function.

But in the second call, while loop already works, and I get the problem I described.

There's something wrong with root->key syntax because it doesn't work even if I place this in the first call.

What's wrong, and what's the reason?

Also, I've always seen inserting new list's elements through pointers like this:

node newElement = new node();
newElement->key = 5;
root->next = newElement;

Is this code equal to:

node newElement = {};
newElement.key = 5;
root->next = &newElement;

? It would be a bit cleaner, and there wouldn't be need to delete memory.

Upvotes: 4

Views: 138

Answers (4)

Matthieu M.
Matthieu M.

Reputation: 299740

It's already been explained that you would have to allocate objects dynamically (with new), however doing so is fraught with perils (memory leaks).

There are two (simple) solutions:

  1. Have an ownership scheme.
  2. Use an arena to put your nodes, and keep references to them.

1 Ownership scheme

In C and C++, there are two forms of obtaining memory where to store an object: automatic storage and dynamic storage. Automatic is what you use when you declare a variable within your function, for example, however such objects only live for the duration of the function (and thus you have issues when using them afterward because the memory is probably overwritten by something else). Therefore you often must use dynamic memory allocation.

The issue with dynamic memory allocation is that you have to explicitly give it back to the system, lest it leaks. In C this is pretty difficult and requires rigor. In C++ though it's made easier by the use of smart pointers. So let's use those!

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.

Note the different declaration of parent and left ? A parent owns its children (unique_ptr) whereas a child just refers to its parent.

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}); }
}

In case you don't know what unique_ptr is, the get() it just contains an object allocated with new and the get() method returns a pointer to that object. You can also reset its content (in which case it properly disposes of the object it already contained, if any).

I would note I am not too sure about your algorithm, but hey, it's yours :)

2 Arena

If this dealing with memory got your head all mushy, that's pretty normal at first, and that's why sometimes arenas might be easier to use. The idea of using an arena is pretty general; instead of bothering with memory ownership on a piece by piece basis you use "something" to hold onto the memory and then only manipulate references (or pointers) to the pieces. You just have to keep in mind that those references/pointers are only ever alive as long as the arena is.

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;
}

Just remember two things:

  • as soon as your arena dies, all your references/pointers are pointing into the ether, and bad things happen should you try to use them
  • if you ever only push things into the arena, it'll grow until it consumes all available memory and your program crashes; at some point you need cleanup!

Upvotes: 0

Peter R
Peter R

Reputation: 2985

The key thing you need to learn here is the difference between stack allocated objects and heap allocated objects. In your insert function your node newElement = {} is stack allocated, which means that its life time is determined by the enclosing scope. In this case that means that when the function exits your object is destroyed. That's not what you want. You want the root of your tree to stored in your node *root pointer. To do that you need to allocate memory from the heap. In C++ that is normally done with the new operator. That allows you to pass the pointer from one function to another without having its life time determined by the scope that it's in. This also means you need to be careful about managing the life time of heap allocated objects.

Upvotes: 1

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726479

The problem is because you are passing a pointer to a local variable out of a function. Dereferencing such pointers is undefined behavior. You should allocate newElement with new.

This code

node newElement = {};

creates a local variable newElement. Once the function is over, the scope of newElement ends, and its memory gets destroyed. However, you are passing the pointer to that destroyed memory to outside the function. All references to that memory become invalid as soon as the function exits.

This code, on the other hand

node *newElement = new node(); // Don't forget the asterisk

allocates an object on free store. Such objects remain available until you delete them explicitly. That's why you can use them after the function creating them has exited. Of course since newElement is a pointer, you need to use -> to access its members.

Upvotes: 2

john
john

Reputation: 87959

Well you have got one problem with your Also comment. The second may be cleaner but it is wrong. You have to new memory and delete it. Otherwise you end up with pointers to objects which no longer exist. That's exactly the problem that new solves.

Another problem

void insert(node *&root, const int key)
{
    node newElement = {};
    newElement.key = key;

    node *y = NULL;
    std::cout << root->key; // this line

On the first insert root is still NULL, so this code will crash the program.

Upvotes: 0

Related Questions