Empat struktur data yang kita bangun pada bagian 1-4 mengalokasikan node secara dinamis di heap dengan malloc. Tanpa kebalikan free, node yang tidak terpakai tetap di heap dan terakumulasi — memory leak. Pada bagian ini kita memperbaiki keempat modul dengan menambahkan fungsi _destroy konsisten dan mendemonstrasikan realloc untuk resize buffer dinamis. Tujuan bagian ini adalah menetapkan pola manajemen memori yang aman: setiap _init harus dipasangkan dengan _destroy.

Konsep #

Aturan dasar manajemen memori C: setiap malloc harus dipasangkan dengan free pada pointer yang sama. Bila kita malloc tiga node, kita harus free ketiganya. Pola yang kita terapkan: setiap struktur data punya pasangan fungsi _init (set pointer ke NULL, size ke 0) dan _destroy (free semua node, kembalikan ke kondisi kosong).

Mekanisme _destroy untuk struktur berbasis linked list (linked list, stack, queue): traverse node satu per satu, simpan pointer next sebelum free node saat ini. Untuk hash table: iterasi setiap bucket, di setiap bucket traverse linked list dengan pola yang sama. Penting: setelah _destroy, struktur harus dalam kondisi konsisten — semua pointer NULL, size 0 — agar pemanggil tidak tergoda memakai struktur yang sudah di-destroy.

realloc adalah operasi resize: beri pointer ke buffer lama dan ukuran baru, realloc mengalokasikan blok baru, menyalin isi lama, lalu membebaskan blok lama. Pointer yang dikembalikan bisa berbeda dari pointer input — selalu perbarui pointer dengan return value realloc. Bila realloc gagal (return NULL), buffer lama tetap valid — kita bisa mencoba lagi atau mengabaikan resize.

Implementasi #

Fungsi buffer_init, buffer_destroy, dan buffer_resize untuk demo pola manajemen memori:

c
typedef struct {
    int *data;
    int size;
} Buffer;

int buffer_init(Buffer *b, int size) {
    b->data = (int *)malloc(sizeof(int) * (size_t)size);
    if (b->data == 0) {
        b->size = 0;
        return -1;
    }
    b->size = size;
    for (int i = 0; i < size; i++) b->data[i] = i * 10;
    return 0;
}

void buffer_destroy(Buffer *b) {
    if (b->data != 0) {
        free(b->data);
        b->data = 0;
    }
    b->size = 0;
}

void buffer_resize(Buffer *b, int new_size) {
    int *new_data = (int *)realloc(b->data, sizeof(int) * (size_t)new_size);
    if (new_data == 0) return; /* b->data tetap valid */
    b->data = new_data;
    for (int i = b->size; i < new_size; i++) b->data[i] = i * 10;
    b->size = new_size;
}

Demo mandiri:

c
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int *data;
    int size;
} Buffer;

static int buffer_init(Buffer *b, int size) {
    b->data = (int *)malloc(sizeof(int) * (size_t)size);
    if (b->data == 0) {
        b->size = 0;
        return -1;
    }
    b->size = size;
    for (int i = 0; i < size; i++) b->data[i] = i * 10;
    return 0;
}

static void buffer_destroy(Buffer *b) {
    if (b->data != 0) {
        free(b->data);
        b->data = 0;
    }
    b->size = 0;
}

static void buffer_resize(Buffer *b, int new_size) {
    int *new_data = (int *)realloc(b->data, sizeof(int) * (size_t)new_size);
    if (new_data == 0) return;
    b->data = new_data;
    for (int i = b->size; i < new_size; i++) b->data[i] = i * 10;
    b->size = new_size;
}

int main(void) {
    Buffer b;
    if (buffer_init(&b, 3) != 0) {
        fprintf(stderr, "Alokasi gagal\n");
        return 1;
    }
    printf("Awal size=%d: ", b.size);
    for (int i = 0; i < b.size; i++) printf("%d ", b.data[i]);
    printf("\n");

    buffer_resize(&b, 5);
    printf("Setelah resize size=%d: ", b.size);
    for (int i = 0; i < b.size; i++) printf("%d ", b.data[i]);
    printf("\n");

    buffer_destroy(&b);
    printf("Setelah destroy size=%d, data=%p\n", b.size, (void *)b.data);

    long total = 0;
    for (int i = 1; i <= 1000; i++) {
        int *p = (int *)malloc(sizeof(int));
        if (p != 0) {
            *p = i;
            total += *p;
            free(p);
        }
    }
    printf("Total 1000 alokasi: %ld\n", total);
    return 0;
}

Output yang Dihasilkan:

text
Awal size=3: 0 10 20
Setelah resize size=5: 0 10 20 30 40
Setelah destroy size=0, data=(nil)
Total 1000 alokasi: 500500

Pengujian #

Build dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o memory_demo main.c
./memory_demo

Build dengan AddressSanitizer untuk memverifikasi tidak ada leak:

bash
gcc -std=c11 -Wall -Wextra -fsanitize=address -o memory_asan main.c
./memory_asan 2>&1 | tail -3

Output yang Dihasilkan (potongan akhir):

text
Total 1000 alokasi: 500500

Tidak ada pesan leak dari ASan karena setiap malloc (3 + 1000) dipasangkan dengan free. Bila kita menghapus baris free(p) di loop 1000 alokasi, ASan akan melaporkan 1000 leak sebesar 4 byte masing-masing.

Untuk hash table, tambahkan fungsi ht_destroy:

c
void ht_destroy(HashTable *ht) {
    for (int i = 0; i < HT_SIZE; i++) {
        HashNode *cur = ht->buckets[i];
        while (cur != 0) {
            HashNode *next = cur->next;
            free(cur);
            cur = next;
        }
        ht->buckets[i] = 0;
    }
}

Panggil ht_destroy(&ht) di akhir main demo hash table, lalu build dengan ASan — leak sebelumnya (144 byte) hilang.

Mari kita telaah baris-baris kunci pada pola manajemen memori:

  1. Baris b->data = (int *)malloc(sizeof(int) * (size_t)size);: alokasi array dinamis. sizeof(int) * size memberi total byte (4 * size pada sistem 32-bit, 4 * size pada 64-bit — int tetap 4 byte). Cast (size_t) menghindari warning signed-unsigned.
  2. Baris if (b->data == 0) { b->size = 0; return -1; }: penjaga kegagalan alokasi. Bila malloc gagal (return NULL), kita kembalikan kode error ke pemanggil. Penting: set b->size = 0 agar struktur dalam kondisi konsisten meski gagal.
  3. Baris for (int i = 0; i < size; i++) b->data[i] = i * 10;: inisialisasi data setelah alokasi. Tanpa inisialisasi, array berisi sampah — berguna untuk demo pola inisialisasi.
  4. Baris if (b->data != 0) { free(b->data); b->data = 0; } (destroy): penjaga NULL. Bila buffer_destroy dipanggil dua kali (double-free), penjaga ini mencegah crash karena free(NULL) legal (no-op).
  5. Baris b->size = 0; (destroy): reset size agar struktur dalam kondisi valid setelah destroy. Tanpa baris ini, b->size berisi angka lama — pemanggil mungkin tergoda memakai b->data[0] yang sudah di-free.
  6. Baris int *new_data = (int *)realloc(b->data, sizeof(int) * new_size);: resize dengan realloc. Beri pointer lama dan ukuran baru; realloc alokasi blok baru, salin data lama, lalu free blok lama.
  7. Baris if (new_data == 0) return; (resize): kegagalan realloc. Bila realloc gagal, ia mengembalikan NULL tetapi blok lama TETAP VALID. Kita return tanpa modifikasi — buffer lama tetap bisa dipakai.
  8. Baris b->data = new_data; (resize): update pointer ke blok baru. Tanpa baris ini, b->data tetap menunjuk ke blok lama yang sudah di-free oleh realloc — use-after-free.
  9. Baris for (int i = b->size; i < new_size; i++) b->data[i] = i * 10;: inisialisasi elemen baru. realloc hanya menyalin data lama — elemen di [size, new_size) berisi sampah yang perlu diinisialisasi.
  10. Baris HashNode *next = cur->next; free(cur); cur = next; (ht_destroy): pola yang sama dengan linked list destroy — simpan pointer next SEBELUM free.

Pada Bagian 6 kita akan menulis rangkaian test yang memverifikasi keempat struktur data bekerja sesuai kontrak, termasuk edge case boundary.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

hash_table.c

Unduh berkas
c
/* hash_table.c -- simple hash table with chaining. */
#define _POSIX_C_SOURCE 200809L /* for strdup */
#include "hash_table.h"

#include <stdlib.h>
#include <string.h>

/* djb2 hash function by Daniel J. Bernstein. */
static unsigned long hash_djb2(const char *str) {
    unsigned long h = 5381;
    int c;
    while ((c = (unsigned char)*str++)) {
        h = ((h << 5) + h) + c; /* h * 33 + c */
    }
    return h;
}

int ht_init(HashTable *ht, size_t capacity) {
    if (!ht || capacity == 0) return -1;
    ht->buckets = calloc(capacity, sizeof(HTNode *));
    if (!ht->buckets) return -1;
    ht->capacity = capacity;
    ht->size = 0;
    return 0;
}

void ht_destroy(HashTable *ht) {
    if (!ht) return;
    for (size_t i = 0; i < ht->capacity; i++) {
        HTNode *cur = ht->buckets[i];
        while (cur) {
            HTNode *next = cur->next;
            free(cur->key);
            free(cur);
            cur = next;
        }
    }
    free(ht->buckets);
    ht->buckets = NULL;
    ht->capacity = 0;
    ht->size = 0;
}

int ht_put(HashTable *ht, const char *key, int value) {
    if (!ht || !key) return -1;
    unsigned long h = hash_djb2(key) % ht->capacity;
    /* Update if key exists. */
    HTNode *cur = ht->buckets[h];
    while (cur) {
        if (strcmp(cur->key, key) == 0) {
            cur->value = value;
            return 0;
        }
        cur = cur->next;
    }
    /* Insert new node at head. */
    HTNode *node = malloc(sizeof(*node));
    if (!node) return -1;
    node->key = strdup(key);
    if (!node->key) {
        free(node);
        return -1;
    }
    node->value = value;
    node->next = ht->buckets[h];
    ht->buckets[h] = node;
    ht->size++;
    return 0;
}

int ht_get(const HashTable *ht, const char *key, int *out) {
    if (!ht || !key) return -1;
    unsigned long h = hash_djb2(key) % ht->capacity;
    HTNode *cur = ht->buckets[h];
    while (cur) {
        if (strcmp(cur->key, key) == 0) {
            if (out) *out = cur->value;
            return 0;
        }
        cur = cur->next;
    }
    return -1;
}

int ht_remove(HashTable *ht, const char *key) {
    if (!ht || !key) return -1;
    unsigned long h = hash_djb2(key) % ht->capacity;
    HTNode *cur = ht->buckets[h];
    HTNode *prev = NULL;
    while (cur) {
        if (strcmp(cur->key, key) == 0) {
            if (prev) prev->next = cur->next;
            else      ht->buckets[h] = cur->next;
            free(cur->key);
            free(cur);
            ht->size--;
            return 0;
        }
        prev = cur;
        cur = cur->next;
    }
    return -1;
}

size_t ht_size(const HashTable *ht) {
    return ht ? ht->size : 0;
}

linked_list.c

Unduh berkas
c
/* linked_list.c -- singly linked list implementation. */
#include "linked_list.h"

#include <stdio.h>
#include <stdlib.h>

void ll_init(LinkedList *list) {
    if (!list) return;
    list->head = NULL;
    list->size = 0;
}

void ll_destroy(LinkedList *list) {
    if (!list) return;
    LLNode *cur = list->head;
    while (cur) {
        LLNode *next = cur->next;
        free(cur);
        cur = next;
    }
    list->head = NULL;
    list->size = 0;
}

int ll_push_front(LinkedList *list, int value) {
    if (!list) return -1;
    LLNode *node = malloc(sizeof(*node));
    if (!node) return -1;
    node->data = value;
    node->next = list->head;
    list->head = node;
    list->size++;
    return 0;
}

int ll_push_back(LinkedList *list, int value) {
    if (!list) return -1;
    LLNode *node = malloc(sizeof(*node));
    if (!node) return -1;
    node->data = value;
    node->next = NULL;
    if (!list->head) {
        list->head = node;
    } else {
        LLNode *cur = list->head;
        while (cur->next) cur = cur->next;
        cur->next = node;
    }
    list->size++;
    return 0;
}

int ll_pop_front(LinkedList *list, int *out) {
    if (!list || !list->head) return -1;
    LLNode *node = list->head;
    if (out) *out = node->data;
    list->head = node->next;
    free(node);
    list->size--;
    return 0;
}

int ll_get(const LinkedList *list, size_t index, int *out) {
    if (!list || !out || index >= list->size) return -1;
    LLNode *cur = list->head;
    for (size_t i = 0; i < index; i++) cur = cur->next;
    *out = cur->data;
    return 0;
}

int ll_remove(LinkedList *list, size_t index) {
    if (!list || index >= list->size) return -1;
    LLNode *to_free;
    if (index == 0) {
        to_free = list->head;
        list->head = to_free->next;
    } else {
        LLNode *prev = list->head;
        for (size_t i = 0; i < index - 1; i++) prev = prev->next;
        to_free = prev->next;
        prev->next = to_free->next;
    }
    free(to_free);
    list->size--;
    return 0;
}

int ll_find(const LinkedList *list, int value) {
    if (!list) return -1;
    LLNode *cur = list->head;
    int index = 0;
    while (cur) {
        if (cur->data == value) return index;
        cur = cur->next;
        index++;
    }
    return -1;
}

size_t ll_size(const LinkedList *list) {
    return list ? list->size : 0;
}

void ll_print(const LinkedList *list) {
    if (!list) return;
    printf("[");
    LLNode *cur = list->head;
    while (cur) {
        printf("%d", cur->data);
        if (cur->next) printf(", ");
        cur = cur->next;
    }
    printf("]\n");
}

main.c

Unduh berkas
c
/* main.c -- demo of all four data structures. */
#include "linked_list.h"
#include "stack.h"
#include "queue.h"
#include "hash_table.h"

#include <stdio.h>
#include <string.h>

static void demo_linked_list(void) {
    printf("--- Linked List ---\n");
    LinkedList list;
    ll_init(&list);

    ll_push_back(&list, 10);
    ll_push_back(&list, 20);
    ll_push_back(&list, 30);
    ll_push_front(&list, 5);
    printf("After pushes: ");
    ll_print(&list);
    printf("Size: %zu\n", ll_size(&list));
    printf("Find 20: index %d\n", ll_find(&list, 20));

    int v;
    ll_pop_front(&list, &v);
    printf("After pop_front (%d): ", v);
    ll_print(&list);

    ll_destroy(&list);
    printf("\n");
}

static void demo_stack(void) {
    printf("--- Stack ---\n");
    Stack s;
    if (stack_init(&s, 4) != 0) {
        printf("stack_init failed\n");
        return;
    }

    stack_push(&s, 1);
    stack_push(&s, 2);
    stack_push(&s, 3);
    printf("Size: %zu\n", stack_size(&s));

    int v;
    while (!stack_is_empty(&s)) {
        stack_pop(&s, &v);
        printf("Popped: %d\n", v);
    }

    stack_destroy(&s);
    printf("\n");
}

static void demo_queue(void) {
    printf("--- Queue ---\n");
    Queue q;
    if (queue_init(&q, 4) != 0) {
        printf("queue_init failed\n");
        return;
    }

    queue_enqueue(&q, 10);
    queue_enqueue(&q, 20);
    queue_enqueue(&q, 30);
    printf("Size: %zu\n", queue_size(&q));

    int v;
    while (!queue_is_empty(&q)) {
        queue_dequeue(&q, &v);
        printf("Dequeued: %d\n", v);
    }

    queue_destroy(&q);
    printf("\n");
}

static void demo_hash_table(void) {
    printf("--- Hash Table ---\n");
    HashTable ht;
    if (ht_init(&ht, 8) != 0) {
        printf("ht_init failed\n");
        return;
    }

    ht_put(&ht, "satu", 1);
    ht_put(&ht, "dua", 2);
    ht_put(&ht, "tiga", 3);
    printf("Size: %zu\n", ht_size(&ht));

    int v;
    if (ht_get(&ht, "dua", &v) == 0) printf("get(dua) = %d\n", v);
    if (ht_get(&ht, "empat", &v) != 0) printf("get(empat) not found\n");

    ht_remove(&ht, "dua");
    printf("After remove dua, size: %zu\n", ht_size(&ht));

    ht_destroy(&ht);
    printf("\n");
}

int main(void) {
    printf("== Struktur Data Demo ==\n\n");
    demo_linked_list();
    demo_stack();
    demo_queue();
    demo_hash_table();
    printf("== Selesai ==\n");
    return 0;
}

queue.c

Unduh berkas
c
/* queue.c -- FIFO queue (circular buffer). */
#include "queue.h"

#include <stdlib.h>

int queue_init(Queue *q, size_t capacity) {
    if (!q || capacity == 0) return -1;
    q->data = malloc(capacity * sizeof(int));
    if (!q->data) return -1;
    q->capacity = capacity;
    q->head = 0;
    q->tail = 0;
    q->size = 0;
    return 0;
}

void queue_destroy(Queue *q) {
    if (!q) return;
    free(q->data);
    q->data = NULL;
    q->capacity = 0;
    q->head = q->tail = q->size = 0;
}

int queue_enqueue(Queue *q, int value) {
    if (!q || q->size >= q->capacity) return -1;
    q->data[q->tail] = value;
    q->tail = (q->tail + 1) % q->capacity;
    q->size++;
    return 0;
}

int queue_dequeue(Queue *q, int *out) {
    if (!q || q->size == 0) return -1;
    if (out) *out = q->data[q->head];
    q->head = (q->head + 1) % q->capacity;
    q->size--;
    return 0;
}

int queue_is_empty(const Queue *q) {
    return !q || q->size == 0;
}

size_t queue_size(const Queue *q) {
    return q ? q->size : 0;
}

stack.c

Unduh berkas
c
/* stack.c -- LIFO stack (dynamic array). */
#include "stack.h"

#include <stdlib.h>

int stack_init(Stack *s, size_t capacity) {
    if (!s || capacity == 0) return -1;
    s->data = malloc(capacity * sizeof(int));
    if (!s->data) return -1;
    s->capacity = capacity;
    s->size = 0;
    return 0;
}

void stack_destroy(Stack *s) {
    if (!s) return;
    free(s->data);
    s->data = NULL;
    s->capacity = 0;
    s->size = 0;
}

int stack_push(Stack *s, int value) {
    if (!s) return -1;
    if (s->size >= s->capacity) {
        size_t new_cap = s->capacity * 2;
        int *p = realloc(s->data, new_cap * sizeof(int));
        if (!p) return -1;
        s->data = p;
        s->capacity = new_cap;
    }
    s->data[s->size++] = value;
    return 0;
}

int stack_pop(Stack *s, int *out) {
    if (!s || stack_is_empty(s)) return -1;
    if (out) *out = s->data[--s->size];
    return 0;
}

int stack_peek(const Stack *s, int *out) {
    if (!s || stack_is_empty(s) || !out) return -1;
    *out = s->data[s->size - 1];
    return 0;
}

int stack_is_empty(const Stack *s) {
    return !s || s->size == 0;
}

size_t stack_size(const Stack *s) {
    return s ? s->size : 0;
}

tests.c

Unduh berkas
c
/* tests.c -- unit tests for all four data structures.
 *
 * Compile with:
 *   gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined \
 *       tests.c linked_list.c stack.c queue.c hash_table.c -o test_runner
 */
#include "linked_list.h"
#include "stack.h"
#include "queue.h"
#include "hash_table.h"

#include <stdio.h>
#include <string.h>

static int tests_passed = 0;
static int tests_failed = 0;

#define ASSERT(cond, msg) do { \
    if (cond) { tests_passed++; } \
    else { tests_failed++; printf("FAIL: %s (line %d)\n", msg, __LINE__); } \
} while (0)

static void test_linked_list(void) {
    LinkedList list;
    ll_init(&list);
    ASSERT(ll_size(&list) == 0, "ll empty size");

    ll_push_back(&list, 10);
    ll_push_back(&list, 20);
    ll_push_front(&list, 5);
    ASSERT(ll_size(&list) == 3, "ll size after push");

    int v;
    ASSERT(ll_get(&list, 0, &v) == 0 && v == 5, "ll get(0) == 5");
    ASSERT(ll_get(&list, 2, &v) == 0 && v == 20, "ll get(2) == 20");

    ASSERT(ll_find(&list, 10) == 1, "ll find(10) == 1");
    ASSERT(ll_find(&list, 99) == -1, "ll find(99) not found");

    ASSERT(ll_pop_front(&list, &v) == 0 && v == 5, "ll pop_front == 5");
    ASSERT(ll_size(&list) == 2, "ll size after pop");

    ll_remove(&list, 0);
    ASSERT(ll_size(&list) == 1, "ll size after remove");

    ll_destroy(&list);
    ASSERT(ll_size(&list) == 0, "ll size after destroy");
}

static void test_stack(void) {
    Stack s;
    ASSERT(stack_init(&s, 2) == 0, "stack_init ok");
    ASSERT(stack_is_empty(&s), "stack empty initially");

    ASSERT(stack_push(&s, 100) == 0, "stack_push 100");
    ASSERT(stack_push(&s, 200) == 0, "stack_push 200");
    /* Force a grow. */
    ASSERT(stack_push(&s, 300) == 0, "stack_push 300 (grow)");

    ASSERT(stack_size(&s) == 3, "stack size 3");

    int v;
    ASSERT(stack_peek(&s, &v) == 0 && v == 300, "stack_peek == 300");
    ASSERT(stack_pop(&s, &v) == 0 && v == 300, "stack_pop == 300");
    ASSERT(stack_pop(&s, &v) == 0 && v == 200, "stack_pop == 200");
    ASSERT(stack_pop(&s, &v) == 0 && v == 100, "stack_pop == 100");
    ASSERT(stack_is_empty(&s), "stack empty at end");
    ASSERT(stack_pop(&s, &v) == -1, "stack_pop on empty returns -1");

    stack_destroy(&s);
}

static void test_queue(void) {
    Queue q;
    ASSERT(queue_init(&q, 4) == 0, "queue_init ok");
    ASSERT(queue_is_empty(&q), "queue empty initially");

    for (int i = 1; i <= 4; i++) {
        ASSERT(queue_enqueue(&q, i * 10) == 0, "queue_enqueue");
    }
    ASSERT(queue_size(&q) == 4, "queue full size");

    int v;
    ASSERT(queue_dequeue(&q, &v) == 0 && v == 10, "queue_dequeue 10");
    ASSERT(queue_dequeue(&q, &v) == 0 && v == 20, "queue_dequeue 20");
    ASSERT(queue_dequeue(&q, &v) == 0 && v == 30, "queue_dequeue 30");
    ASSERT(queue_dequeue(&q, &v) == 0 && v == 40, "queue_dequeue 40");
    ASSERT(queue_is_empty(&q), "queue empty after drain");
    ASSERT(queue_dequeue(&q, &v) == -1, "queue_dequeue on empty");

    queue_destroy(&q);
}

static void test_hash_table(void) {
    HashTable ht;
    ASSERT(ht_init(&ht, 4) == 0, "ht_init ok");
    ASSERT(ht_size(&ht) == 0, "ht empty");

    ASSERT(ht_put(&ht, "alpha", 1) == 0, "ht_put alpha");
    ASSERT(ht_put(&ht, "beta", 2) == 0, "ht_put beta");
    ASSERT(ht_put(&ht, "gamma", 3) == 0, "ht_put gamma");
    ASSERT(ht_size(&ht) == 3, "ht size 3");

    int v;
    ASSERT(ht_get(&ht, "beta", &v) == 0 && v == 2, "ht_get beta");
    ASSERT(ht_get(&ht, "missing", &v) == -1, "ht_get missing");

    /* Update existing key. */
    ASSERT(ht_put(&ht, "beta", 22) == 0, "ht_put beta update");
    ASSERT(ht_get(&ht, "beta", &v) == 0 && v == 22, "ht_get beta updated");
    ASSERT(ht_size(&ht) == 3, "ht size unchanged after update");

    ASSERT(ht_remove(&ht, "alpha") == 0, "ht_remove alpha");
    ASSERT(ht_get(&ht, "alpha", &v) == -1, "ht_get alpha gone");
    ASSERT(ht_size(&ht) == 2, "ht size 2 after remove");

    ht_destroy(&ht);
}

int main(void) {
    test_linked_list();
    test_stack();
    test_queue();
    test_hash_table();

    printf("\n=== Test Results ===\n");
    printf("Passed: %d\n", tests_passed);
    printf("Failed: %d\n", tests_failed);
    if (tests_failed == 0) {
        printf("Lulus : %d\n", tests_passed);
        return 0;
    }
    return 1;
}