Memory allocation in linked list

Hi folks, I’m new to zig and trying to build a linked list. I managed to write the following code but it does not work as expected. I think this has something to do with memory allocation.

My code:

const std = @import("std");

pub const LinkedList = struct {
    const Self = @This();
    pub const Node = struct { val: i32, next: ?*Node = null };

    head: ?*Node,
    tail: ?*Node,
    length: u32,

    pub fn new() LinkedList {
        return LinkedList{ .head = null, .tail = null, .length = 0 };
    }

    pub fn insert(self: *Self, v: i32) void {
        var newNode = Node{ .val = v };

        if (self.tail != null) {
            var t: *Node = self.tail.?;
            t.next = &newNode;
            self.tail = &newNode;
        } else {
            self.head = &newNode;
            self.tail = &newNode;
        }
        self.length += 1;
    }

    pub fn traverse(self: Self) void {
        if (self.head == null) {
            std.debug.print("head is null. nothing to do...\n", .{});
            return;
        }

        var h = self.head;
        while (true) {
            if (h == null) {
                break;
            }

            const ptr: *Node = h.?;
            std.debug.print("val :{}\n", .{ptr.val});
            h = ptr.next;
        }
    }

    pub fn len(self: Self) u32 {
        return self.length;
    }
};

pub fn main() !void {
    var list = LinkedList.new();

    list.insert(10);
    list.insert(20);
    list.traverse();
    std.debug.print("len :{}\n", .{list.len()});
}

Output and the error:

zig run main.zig
val :20
val :-1605007032
General protection exception (no address available)
/home/thilina/git/dsa-zig/linked-list/main.zig:46:47: 0x1033a9b in traverse (main)
            std.debug.print("val :{}\n", .{ptr.val});

/home/thilina/git/dsa-zig/linked-list/main.zig:64:18: 0x10338cd in main (main)
    list.traverse();
                 ^
/usr/lib/zig/std/start.zig:511:37: 0x1033864 in posixCallMainAndExit (main)
            const result = root.main() catch |err| {
                                    ^
/usr/lib/zig/std/start.zig:253:5: 0x10333a1 in _start (main)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
Aborted (core dumped)

My questions are,

  1. Why does it print 20 first when I try to traverse the linked list? Head should point to the node with the value 10 because it is added first and I don’t change the value of head after that?
  2. Why does the optional pointer has a garbage value for the second node?

Thank you very much in advance!

The problem is with the insert function you are referencing a stack variable, The reason you get 20, is because, in your main your insert 10 first, but once you insert 20 again, the 20 most likely invalidate the 10, and that’s why in traverse you get a segfault

I’ve sent you a link to a linked list implementation I’ve made, feel free to look at it, if you are confused. But basically Zig is a language where you have to manually manage memory, this includes lifetimes at well, a linked list cannot live on the stack (or at least not in the way you did it), You need to extend the lifetime of those nodes, by allocating memory in one way or another. I would suggest you use a dedicated function to alloc a node, and then insert it. this way the memory won’t be invalid when you will use it.

3 Likes

Hi @thilina, welcome to ziggit! :slight_smile:

In the insert function var newNode = Node{ .val = v }; allocates Node storage from the hardware stack. The stack position is restored when insert returns.
That means that the lifetime of Node ends when function exits.
The pointer to Node returned by &newNode points to an address that is replaced by other contents, depending on what runs next.

In zig an allocator is a programming interface that you can use to request memory allocation (by calling allocator.alloc) and release the memory when it is not used (by calling allocator.free).
There are various allocators, the GeneralPurposeAllocator might be your first choise, but you can also use an ArenaAllocator; in ArenaAllocator you can call alloc and when you are done with all the allocations you can call deinit to free the entire arena with all the allocations.

You can find allocation documentation and examples in the Zig Learning Resources.

3 Likes

This music started playing in my head when I saw that insert function: https://www.youtube.com/watch?v=cOy6hqzfsAs

:slight_smile:

5 Likes

Thank you for the answer, explanation and sending the link to the code. Really appreciate it! I understand what I did wrong here :slight_smile:

Thank you for the detailed explanation on what I did wrong, lifetime of objects and memory allocation. I will take a look at memory allocation and how it works. Really appreciate the help! :slight_smile:

1 Like

Haha, gotta shoot yourself on the foot to learn I guess :man_shrugging:

4 Likes

People think I’m wearing crocs…no, I just wrote alot of C++ back in the day :slight_smile:

image

5 Likes