Dynamic memory in C — exercises to raise grades, filter the passes and keep a sorted array that grows (and the double free that knocked out my report)

The week I closed my first “grade report” in C I felt unstoppable: malloc, realloc with a temporary, free… all in order. I wrote a function close_report that computed the average and, to be tidy, freed the array. And in main, to be tidy as well, I freed it again. Result: free(): double free detected in tcache 2 and the program aborted right after printing the average. Two “just in case” free calls are one too many. That bug is the third “What’s wrong here?” snippet, and the idea it leaves (every block has one owner) runs through all the exercises.

Here I no longer give you the code. You’ve got three exercises of increasing difficulty with a class’s grades, three snippets with bugs for you to predict what happens and, at the end, the commented solutions and the cheat sheet. If you get stuck on something basic, go back to the dynamic memory in C theory or to the practice.

How to use this article

  1. Read a level’s statement and write your solution without looking at mine. Compile with gcc -Wall -std=c11 -g -fsanitize=address.
  2. Compare your output with the expected output, character by character.
  3. If it doesn’t work, open the hint. If it still doesn’t, look at the solution, close it and try again from scratch.
  4. Do “What’s wrong here?” before reading the explanations: predicting the bug is the best training.

Level 1 (basic): raise the grades without touching the originals

The teacher wants to see what the grades would look like if everyone got half a point more, but without changing the real grades. Write a function

double *raise_grades(const double *v, int n, double extra)

that returns a new array (reserved with malloc) with each grade raised by extra points, never above 10. In main, with the grades {4.5, 6.0, 9.8, 3.0, 7.5} and extra = 0.5, show the originals and the raised ones, and free whatever needs freeing.

Expected output:

Original: 4.5 6.0 9.8 3.0 7.5
Raised  : 5.0 6.5 10.0 3.5 8.0

The function reserves, but the one using the result is main: where does the free go? And for the cap, an if after adding is enough.

Level 2 (intermediate): filter the passes with just enough memory

Write

double *filter_passed(const double *v, int n, int *k)

that returns a new array with only the passes (grade ≥ 5) and leaves in *k how many there are. It must reserve exactly the memory needed (not one slot more) and return NULL if nobody passed. Test it with two groups: {6.5, 4.0, 9.5, 5.0, 7.0, 3.0, 8.5} and {2.0, 4.5}.

Expected output:

Group A: 5 of 7 passed -> 6.5 9.5 5.0 7.0 8.5
Group B: 0 of 2 passed -> nobody

To reserve just enough you need to know how many passes there are before the malloc. Go through the array twice.

Level 3 (challenge): a sorted array that grows and cleans itself up

The grades arrive unsorted: {6.5, 4.0, 9.5, 5.0, 7.0, 3.0, 8.5}. Write:

  • int insert_sorted(double **v, int *n, int *cap, double x): inserts x in its place so the array is always sorted from lowest to highest. If it doesn’t fit, it doubles the capacity with realloc. Returns 1 if OK and 0 if there’s no memory.
  • void remove_fails(double **v, int *n, int *cap): removes the grades below 5 and shrinks the array to its exact size.

Start with capacity 1 and show the array, n and cap after each insertion and at the end.

Expected output:

+6.5 -> [6.5] n=1 cap=1
+4.0 -> [4.0 6.5] n=2 cap=2
+9.5 -> [4.0 6.5 9.5] n=3 cap=4
+5.0 -> [4.0 5.0 6.5 9.5] n=4 cap=4
+7.0 -> [4.0 5.0 6.5 7.0 9.5] n=5 cap=8
+3.0 -> [3.0 4.0 5.0 6.5 7.0 9.5] n=6 cap=8
+8.5 -> [3.0 4.0 5.0 6.5 7.0 8.5 9.5] n=7 cap=8
No fails -> [5.0 6.5 7.0 8.5 9.5] n=5 cap=5

To insert in order, start from the end and shift every grade bigger than x one slot to the right; the gap left behind is its place. To remove the fails, use a read index and a write index, like in the practice’s report.

What’s wrong here?

Three snippets with one bug each. Read the code, predict what happens and then open the explanation.

Snippet 1: the absences

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

int main(void) {
    int n = 8;                           /* 8 students */
    int *absences = malloc(n);             /* absences of each student */
    if (absences == NULL) return 1;
    for (int i = 0; i < n; i++)
        absences[i] = i % 3;
    printf("Absences of the last one: %d\n", absences[n - 1]);
    free(absences);
    return 0;
}

What do you think happens? Without tools, it’ll most likely print a number and look fine. Compiled with gcc -Wall -std=c11 -g -fsanitize=address, this is the output (the AddressSanitizer report, trimmed to the important lines):

=================================================================
==723==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000018
WRITE of size 4 at 0x502000000018 thread T0
    #0 in main snippet.c:9
0x502000000018 is located 0 bytes after 8-byte region [0x502000000010,0x502000000018)
allocated by thread T0 here:
    #1 in main snippet.c:6
SUMMARY: AddressSanitizer: heap-buffer-overflow snippet.c:9 in main

malloc(n) reserves n bytes: 8 bytes. An int takes 4, so only 2 absences fit. In round i = 2 the program writes 4 bytes right after the block, and AddressSanitizer catches it: WRITE of size 4, 0 bytes after 8-byte region, line 9 (absences[i] = i % 3;), on a block reserved on line 6.

A program with this bug “working” doesn’t mean it’s right. You’re trampling memory that may belong to other data, and the failure will show up when you least expect it.

The fix is to multiply by the size of each slot:

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

int main(void) {
    int n = 8;                                 /* 8 students */
    int *absences = malloc(n * sizeof *absences);  /* 8 int slots = 32 bytes */
    if (absences == NULL) return 1;
    for (int i = 0; i < n; i++)
        absences[i] = i % 3;
    printf("Absences of the last one: %d\n", absences[n - 1]);
    free(absences);
    return 0;
}

Output:

Absences of the last one: 1

The last absence count belongs to student 7: 7 % 3 = 1.

With sizeof *absences you don’t have to remember the type. If tomorrow the absences become short, the line is still correct.

Snippet 2: the miscounted dropouts

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

int main(void) {
    int n = 3;
    double *grades = malloc(n * sizeof(double));
    if (grades == NULL) return 1;
    grades[0] = 7.5; grades[1] = 4.0; grades[2] = 9.0;

    int dropouts = 4;                     /* 4 drop out... but there were only 3 */
    int new_n = n - dropouts;
    printf("Adjusting to %d grades...\n", new_n);
    grades = realloc(grades, new_n * sizeof(double));
    printf("First grade: %.1f\n", grades[0]);
    free(grades);
    return 0;
}

What do you think happens? Real output:

Adjusting to -1 grades...
Segmentation fault (core dumped)

There are two chained bugs. The first is an arithmetic one: there were 3 grades and 4 dropouts are subtracted, so new_n is -1. When multiplying -1 * sizeof(double), the -1 is converted to size_t (an unsigned integer) and turns into a gigantic number: realloc is being asked for almost 18 quintillion bytes. Naturally, it fails and returns NULL.

The second bug is the one that turns an arithmetic slip into a disaster: grades = realloc(grades, ...) without a temporary. Now grades is NULL, the 3-grade block stays reserved with nobody able to reach it, and grades[0] reads address 0: Segmentation fault.

Passing malloc or realloc a size you computed without checking it. A negative number becomes a huge size without any warning.

The fix checks the numbers and uses a temporary:

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

int main(void) {
    int n = 3;
    double *grades = malloc(n * sizeof(double));
    if (grades == NULL) return 1;
    grades[0] = 7.5; grades[1] = 4.0; grades[2] = 9.0;

    int dropouts = 4;
    int new_n = n - dropouts;
    if (new_n < 1) {                 /* we check the numbers before requesting memory */
        printf("Impossible size (%d): keeping %d grades\n", new_n, n);
    } else {
        double *tmp = realloc(grades, new_n * sizeof(double));
        if (tmp != NULL) { grades = tmp; n = new_n; }  /* if it fails, grades is intact */
    }
    printf("First grade: %.1f\n", grades[0]);
    free(grades);
    return 0;
}

Output:

Impossible size (-1): keeping 3 grades
First grade: 7.5

Now the program detects the impossible size, keeps the 3 grades and carries on working.

In a real case, the arithmetic slip would need fixing too (why do 4 drop out if there are 3?). The temporary doesn’t fix the numbers, but it stops a failure from turning into data loss.

Snippet 3: the report that was closed twice

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

void close_report(double *grades, int n) {
    double sum = 0;
    for (int i = 0; i < n; i++) sum += grades[i];
    printf("Report closed: average %.2f\n", sum / n);
    free(grades);                       /* the function frees it... */
}

int main(void) {
    double *grades = malloc(4 * sizeof(double));
    if (grades == NULL) return 1;
    grades[0] = 7.5; grades[1] = 4.0; grades[2] = 9.0; grades[3] = 6.5;
    close_report(grades, 4);
    free(grades);                       /* ...and main frees it again */
    printf("End of program\n");
    return 0;
}

What do you think happens? Real output:

Report closed: average 6.75
free(): double free detected in tcache 2
Aborted (core dumped)

close_report receives a copy of the pointer, not a copy of the array: both grades (the one in main and the parameter) point to the same block. The function frees it and main frees it again. The C library notices the block was already free (double free detected) and aborts the program: “End of program” never gets printed.

Two pointers, one single blockThe pointer grades in main and the parameter grades in close_report point to the same 4-grade block. close_report frees it and then main frees it again, which aborts the program.main()double *gradesclose_report()double *grades7.54.09.06.5already freed1. free(grades) in close_report: the block goes back to the system2. free(grades) in main: the SAME block again → abort
Copying a pointer does not copy the block: two pointers to the same block and two free calls make a double free.

The drawing shows the problem: two pointers, one single block and two free calls.

Freeing “just in case” in two places. Decide who owns the block and free it only there.

The fix: close_report only reads (we mark it with const) and the only free is in main, which is the one that reserved it.

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

void close_report(const double *grades, int n) {   /* const: it only reads, it is not the owner */
    double sum = 0;
    for (int i = 0; i < n; i++) sum += grades[i];
    printf("Report closed: average %.2f\n", sum / n);
}

int main(void) {
    double *grades = malloc(4 * sizeof(double));
    if (grades == NULL) return 1;
    grades[0] = 7.5; grades[1] = 4.0; grades[2] = 9.0; grades[3] = 6.5;
    close_report(grades, 4);
    free(grades);                       /* one single owner, one single free */
    grades = NULL;
    printf("End of program\n");
    return 0;
}

Output:

Report closed: average 6.75
End of program

Average check: (7.5 + 4.0 + 9.0 + 6.5) / 4 = 27 / 4 = 6.75.

If a function frees a block it’s given, say so in its name or in a comment (destroy_report, “frees v”). And the caller should set its pointer to NULL right afterwards.

Solutions

Level 1 solution

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

/* Returns a NEW array with each grade raised by 'extra' points (never above 10).
   The original is not touched: that is why it is const. The caller frees the result. */
double *raise_grades(const double *v, int n, double extra) {
    double *r = malloc(n * sizeof *r);
    if (r == NULL) return NULL;
    for (int i = 0; i < n; i++) {
        r[i] = v[i] + extra;
        if (r[i] > 10) r[i] = 10;           /* cap: nobody gets a 10.3 */
    }
    return r;
}

void show(const char *title, const double *v, int n) {
    printf("%s:", title);
    for (int i = 0; i < n; i++) printf(" %.1f", v[i]);
    printf("\n");
}

int main(void) {
    double grades[] = {4.5, 6.0, 9.8, 3.0, 7.5};
    int n = sizeof grades / sizeof grades[0];

    double *raised = raise_grades(grades, n, 0.5);
    if (raised == NULL) return 1;

    show("Original", grades, n);
    show("Raised  ", raised, n);

    free(raised);                          /* raise_grades reserved it, main frees it */
    return 0;
}

Output:

Original: 4.5 6.0 9.8 3.0 7.5
Raised  : 5.0 6.5 10.0 3.5 8.0

Why it’s done this way:

  • **const double *v**: the function promises not to modify the original grades. If you wrote v[i] += extra by mistake, the compiler would give an error.
  • **A new array with malloc**: returning a local array would repeat the Segmentation fault from the theory. The result has to live on the heap.
  • The cap: 9.8 + 0.5 = 10.3, which is cut down to 10.0. The rest: 4.5 + 0.5 = 5.0, 6.0 + 0.5 = 6.5, 3.0 + 0.5 = 3.5 and 7.5 + 0.5 = 8.0.
  • **free(raised) in main** and never free(grades): grades is a normal stack array, and freeing something that doesn’t come from malloc is a serious bug.
  • **"Raised " with spaces** is just so the two lines line up.

Level 2 solution

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

/* Returns a new array with ONLY the passing grades and leaves its size in *k.
   Two passes: count and then copy, to request exactly the memory needed. */
double *filter_passed(const double *v, int n, int *k) {
    *k = 0;
    for (int i = 0; i < n; i++)             /* pass 1: how many passing grades are there? */
        if (v[i] >= 5) (*k)++;

    if (*k == 0) return NULL;               /* malloc(0) is useless: NULL = empty */

    double *r = malloc(*k * sizeof *r);
    if (r == NULL) { *k = 0; return NULL; }

    int j = 0;                              /* j = next free slot of r */
    for (int i = 0; i < n; i++)             /* pass 2: copy */
        if (v[i] >= 5) r[j++] = v[i];
    return r;
}

void report(const char *group, const double *v, int n) {
    int k;
    double *passed = filter_passed(v, n, &k);
    printf("%s: %d of %d passed ->", group, k, n);
    if (passed == NULL) printf(" nobody");
    for (int i = 0; i < k; i++) printf(" %.1f", passed[i]);
    printf("\n");
    free(passed);                               /* free(NULL) is safe: no if needed */
}

int main(void) {
    double group_a[] = {6.5, 4.0, 9.5, 5.0, 7.0, 3.0, 8.5};
    double group_b[] = {2.0, 4.5};
    report("Group A", group_a, 7);
    report("Group B", group_b, 2);
    return 0;
}

Output:

Group A: 5 of 7 passed -> 6.5 9.5 5.0 7.0 8.5
Group B: 0 of 2 passed -> nobody

Zoom into the function, which is where the decisions are:

/* Returns a new array with ONLY the passing grades and leaves its size in *k.
   Two passes: count and then copy, to request exactly the memory needed. */
double *filter_passed(const double *v, int n, int *k) {
    *k = 0;
    for (int i = 0; i < n; i++)             /* pass 1: how many passing grades are there? */
        if (v[i] >= 5) (*k)++;

    if (*k == 0) return NULL;               /* malloc(0) is useless: NULL = empty */

    double *r = malloc(*k * sizeof *r);
    if (r == NULL) { *k = 0; return NULL; }

    int j = 0;                              /* j = next free slot of r */
    for (int i = 0; i < n; i++)             /* pass 2: copy */
        if (v[i] >= 5) r[j++] = v[i];
    return r;
}

Trace of group A in the second pass (*k is already 5, so 5 × 8 = 40 bytes are reserved):

iv[i]≥ 5?Copied intoj after
06.5Yesr[0]1
14.0No—1
29.5Yesr[1]2
35.0Yesr[2]3
47.0Yesr[3]4
53.0No—4
68.5Yesr[4]5

Why it’s done this way:

  • **Two passes instead of a realloc for each pass**: the first one only counts, so the malloc is exact and there’s no need to grow or shrink.
  • ***k == 0 → NULL**: malloc(0) may return NULL or a pointer you can’t use, depending on the system. Returning NULL on purpose makes it clear the array is empty.
  • **If malloc fails, *k = 0**: that way the caller never walks k slots of an array that doesn’t exist.
  • **free(passed) with no if**: in group B, passed is NULL and free(NULL) does nothing. The line works for both cases.

Level 3 solution

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

void show(const double *v, int n, int cap) {
    printf("[");
    for (int i = 0; i < n; i++) printf(i ? " %.1f" : "%.1f", v[i]);
    printf("] n=%d cap=%d\n", n, cap);
}

/* Inserts x keeping the array SORTED from lowest to highest. Doubles the capacity if needed. */
int insert_sorted(double **v, int *n, int *cap, double x) {
    if (*n == *cap) {
        double *tmp = realloc(*v, 2 * *cap * sizeof **v);
        if (tmp == NULL) return 0;
        *v = tmp;
        *cap *= 2;
    }
    int i = *n - 1;
    while (i >= 0 && (*v)[i] > x) {         /* from the end, we shift the bigger ones to the right */
        (*v)[i + 1] = (*v)[i];
        i--;
    }
    (*v)[i + 1] = x;                        /* the gap left behind is where x goes */
    (*n)++;
    return 1;
}

/* Removes the failing grades by compacting the array and shrinks it to its exact size */
void remove_fails(double **v, int *n, int *cap) {
    int j = 0;
    for (int i = 0; i < *n; i++)
        if ((*v)[i] >= 5) (*v)[j++] = (*v)[i];   /* the passing grades shift to the left */
    *n = j;
    if (j > 0 && j < *cap) {
        double *tmp = realloc(*v, j * sizeof **v);
        if (tmp != NULL) { *v = tmp; *cap = j; }  /* if it fails, the big array is still valid */
    }
}

int main(void) {
    const double arriving[] = {6.5, 4.0, 9.5, 5.0, 7.0, 3.0, 8.5};
    int n = 0, cap = 1;
    double *grades = malloc(cap * sizeof *grades);
    if (grades == NULL) return 1;

    for (int i = 0; i < 7; i++) {
        if (!insert_sorted(&grades, &n, &cap, arriving[i])) break;
        printf("+%.1f -> ", arriving[i]);
        show(grades, n, cap);
    }
    remove_fails(&grades, &n, &cap);
    printf("No fails -> ");
    show(grades, n, cap);

    free(grades);
    return 0;
}

Output:

+6.5 -> [6.5] n=1 cap=1
+4.0 -> [4.0 6.5] n=2 cap=2
+9.5 -> [4.0 6.5 9.5] n=3 cap=4
+5.0 -> [4.0 5.0 6.5 9.5] n=4 cap=4
+7.0 -> [4.0 5.0 6.5 7.0 9.5] n=5 cap=8
+3.0 -> [3.0 4.0 5.0 6.5 7.0 9.5] n=6 cap=8
+8.5 -> [3.0 4.0 5.0 6.5 7.0 8.5 9.5] n=7 cap=8
No fails -> [5.0 6.5 7.0 8.5 9.5] n=5 cap=5

The trickiest function is the insertion:

/* Inserts x keeping the array SORTED from lowest to highest. Doubles the capacity if needed. */
int insert_sorted(double **v, int *n, int *cap, double x) {
    if (*n == *cap) {
        double *tmp = realloc(*v, 2 * *cap * sizeof **v);
        if (tmp == NULL) return 0;
        *v = tmp;
        *cap *= 2;
    }
    int i = *n - 1;
    while (i >= 0 && (*v)[i] > x) {         /* from the end, we shift the bigger ones to the right */
        (*v)[i + 1] = (*v)[i];
        i--;
    }
    (*v)[i + 1] = x;                        /* the gap left behind is where x goes */
    (*n)++;
    return 1;
}

Let’s see what happens when inserting 5.0, when the array is [4.0 6.5 9.5] with n = 3 and cap = 4 (it fits without growing):

Stepi(*v)[i]> 5.0?What it doesArray
Start29.5Yesv[3] = v[2]4.0 6.5 9.5 9.5
216.5Yesv[2] = v[1]4.0 6.5 6.5 9.5
304.0Noleaves the loop4.0 6.5 6.5 9.5
End0——v[i + 1] = 5.04.0 5.0 6.5 9.5
Inserting 5.0 while keeping the orderFour rows. Before: 4.0, 6.5, 9.5 and a gap. 9.5 is copied to slot 3, 6.5 to slot 2, the loop stops at 4.0 and 5.0 is placed in slot 1.before: n=3, 5.0 arrives4.06.59.5i=2: 9.5 > 5.0 → shift4.06.59.59.5i=1: 6.5 > 5.0 → shift4.06.56.59.5i=0: 4.0 is not bigger → stop4.05.06.59.5v[i + 1] = 5.0[0][1][2][3]
The values bigger than x shift one slot to the right; the gap left behind is where x goes.

The drawing shows the “shift”: each grade bigger than 5.0 is copied one slot to the right (which is why, for a moment, a grade appears twice), and the gap left behind is exactly where 5.0 goes.

And the final clean-up:

/* Removes the failing grades by compacting the array and shrinks it to its exact size */
void remove_fails(double **v, int *n, int *cap) {
    int j = 0;
    for (int i = 0; i < *n; i++)
        if ((*v)[i] >= 5) (*v)[j++] = (*v)[i];   /* the passing grades shift to the left */
    *n = j;
    if (j > 0 && j < *cap) {
        double *tmp = realloc(*v, j * sizeof **v);
        if (tmp != NULL) { *v = tmp; *cap = j; }  /* if it fails, the big array is still valid */
    }
}

Why it’s done this way:

  • Grow before shifting: if the array is full, (*v)[i + 1] with i = n - 1 would write one slot outside the block. That’s why the realloc comes first.
  • **i >= 0 before (*v)[i] > x**: && is evaluated left to right and stops as soon as something is false. When x is the smallest grade (3.0), i reaches -1 and the second condition isn’t even looked at: without that order, you’d read v[-1].
  • The capacities: 1 → 2 (when 4.0 arrives) → 4 (9.5) → 8 (7.0). Three realloc calls for seven grades.
  • **remove_fails** uses read (i) and write (j) indexes like the practice’s report: 3.0 and 4.0 aren’t copied and 5 grades remain. Then it shrinks from 8 to 5 with a temporary.
  • **j > 0** avoids realloc(*v, 0) if everyone failed: in that case the array keeps its capacity and n = 0.

Watch it step by step in Python Tutor

Open Python Tutor, pick C (gcc) and paste this short version of level 2, with both passes in main:

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

int main(void) {
    double v[] = {6.5, 4.0, 9.5, 3.0};
    int n = 4, k = 0;

    for (int i = 0; i < n; i++)
        if (v[i] >= 5) k++;

    double *passed = malloc(k * sizeof(double));
    if (passed == NULL) return 1;

    int j = 0;
    for (int i = 0; i < n; i++)
        if (v[i] >= 5) passed[j++] = v[i];

    printf("%d passed, the first one %.1f\n", k, passed[0]);
    free(passed);
    passed = NULL;
    return 0;
}

What to watch step by step:

  1. During the first pass nothing appears on the heap: only k changes on the stack (it ends up as 2).
  2. After the malloc, a block of exactly 2 slots appears on the heap, not one more: that’s what the first pass was for.
  3. In the second pass, watch j: it only moves on when the grade is a pass, while i always moves on.
  4. After free(passed), the block disappears, and with passed = NULL the arrow stops pointing anywhere.

Cheat sheet: dynamic memory in C

The essentials in 30 seconds

  • Everything is in <stdlib.h>. Sizes are in bytes: n * sizeof *v.
  • malloc doesn’t initialise; calloc zeroes; realloc changes the size keeping the data.
  • After every reservation, check for NULL.
  • realloc always with a temporary pointer.
  • Every block, one owner and a single free. Afterwards, p = NULL.
  • A function that returns an array reserves it with malloc; the caller frees it.

Reserving and freeing

I want…CodeResult
n uninitialised doublesdouble *v = malloc(n * sizeof *v);Block of n × 8 bytes or NULL
n counters at zeroint *c = calloc(n, sizeof *c);Block of n × 4 bytes at 0 or NULL
Go to m slotsdouble *tmp = realloc(v, m * sizeof *v); if (tmp) v = tmp;Data kept; if it fails, v intact
Give the memory backfree(v); v = NULL;Block freed, pointer switched off
Know how many slots I haveKeep it in a variable nsizeof(v) is 8: useless

Templates from the exercises

PatternKey codeResult with the exercises’ data
Function returning a new arraydouble *r = malloc(n * sizeof *r); … return r;raise_grades → 5.0 6.5 10.0 3.5 8.0
Reserve just enoughCount → malloc(k * sizeof *r) → copy5 passes in 40 bytes
Growing arrayif (n == cap) { tmp = realloc(v, 2 * cap * sizeof *v); … }cap 1 → 2 → 4 → 8 with 7 grades
Filter in placeif (ok) v[j++] = v[i]; then realloc to j7 grades → 5 passes, cap 5
Dynamic tabledouble **t = malloc(f * sizeof *t); + t[i] = calloc(c, sizeof **t);Free the rows and then t

Symptom and likely cause

SymptomLikely cause
Segmentation fault when using the arraymalloc/realloc returned NULL and it wasn’t checked, or you returned a local array
heap-buffer-overflow (AddressSanitizer)You reserved bytes instead of slots or went past index n - 1
free(): double free detectedTwo free calls on the same block (two owners)
detected memory leaksA free is missing, or v = realloc(v, …) failed and lost the block
Weird numbers in new slotsmalloc and realloc don’t initialise: use calloc or fill them in
warning: function returns address of local variableYou return a stack array: reserve it on the heap

Before handing in, ask yourself

  1. Does every malloc, calloc and realloc multiply by sizeof and check for NULL?
  2. Do all the realloc calls use a temporary pointer?
  3. Do I know who owns each block, and is there exactly one free per block on every path through the program?
  4. Have I compiled with -g -fsanitize=address and does the program end without any report?

With this you’ve wrapped up dynamic memory. The next step in IC2 is structs and strings in C, where everything from today is used to store complete records.

Keep goingAll C (en) articles

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *