Dynamic memory in C — practice with a grade report that shrinks, an array that grows as students arrive and a table of grades on the heap (and the realloc that nearly left me without data)

Second session with dynamic memory. My grades array already worked with malloc, but new students turned up and I happily wrote grades = realloc(grades, new_n * sizeof(double));. It worked. It worked ten times in a row. Until a classmate asked me: “what if realloc returns NULL?”. I sat there staring at the line. If it failed, I’d have just overwritten my only pointer to the grades with NULL: data lost and a reserved block nobody could free anymore. Since then, realloc always goes with a temporary pointer, and that idea shows up in all three of today’s programs.

In the dynamic memory in C theory you saw malloc, calloc, realloc and free one by one. Here we put them together in three medium-level programs with our usual example, a class’s grades, using data that looks like the real thing: a mistyped grade (11.5), students arriving unannounced and a table with several tests per student. All three programs use fixed data instead of scanf, so you can compare your output with mine; switching them to keyboard input is one of the “Try it yourself” tasks.

Before you start

Make sure you’re clear on:

  • That malloc(n * sizeof *v) reserves n slots and that you have to check for NULL.
  • That realloc may move the block and that its result goes into a temporary pointer.
  • That each block is freed exactly once with free and that the pointer is then set to NULL.
  • How to pass a pointer to a function so that the function changes the caller’s variable (you saw it in functions in C).

If any of these sounds shaky, go over the theory before carrying on.

Compile each program with gcc -Wall -std=c11 program.c -o program and run it with ./program. While you modify it, add -g -fsanitize=address: if you step outside a block or forget a free, it’ll tell you straight away.

Program 1: the grade report that shrinks

The teacher first types how many grades they’ll enter (8) and then the grades. One of them, 11.5, is a typo. The program reserves room for 8, keeps only the valid ones (between 0 and 10), shrinks the array to the real number of grades and computes the average, the passes, the highest, the lowest and which grades are above the average. The tricky part is that the final size (7) isn’t known until everything has been read.

The idea

  1. Read the number of grades n and reserve n slots with malloc.
  2. Walk through the input with an index i, but store using another index k, which only moves on when the grade is valid. That way the valid grades end up packed together at the start of the array, with no gaps.
  3. If k < n, there are spare slots: realloc to shrink to k, with a temporary pointer.
  4. Compute the statistics over the k grades and free.

Before coding, think about which index you use to read and which one to write. If you use the same i for both, the discarded grade leaves a gap full of garbage in the middle of the array.

The code

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

/* What the teacher "types": first how many grades and then the grades (one is 11.5 by mistake) */
const double INPUT[] = {8, 6.5, 4.0, 9.5, 11.5, 5.0, 7.0, 3.0, 8.5};

/* Copies the valid grades (0..10) into v and returns how many it stored */
int read_grades(double *v, int n, const double *input) {
    int k = 0;                              /* k = slots actually in use */
    for (int i = 0; i < n; i++) {
        double x = input[i];
        if (x < 0 || x > 10) {              /* impossible grade: we warn and do not store it */
            printf("Discarded the grade %.1f\n", x);
            continue;
        }
        v[k++] = x;                         /* stores into v[k] and THEN adds 1 to k */
    }
    return k;
}

double average(const double *v, int n) {
    double sum = 0;
    for (int i = 0; i < n; i++) sum += v[i];
    return sum / n;
}

int main(void) {
    int n = (int)INPUT[0];                /* 8: the size is only known at run time */
    double *grades = malloc(n * sizeof *grades);
    if (grades == NULL) { printf("Out of memory\n"); return 1; }

    int k = read_grades(grades, n, INPUT + 1);   /* INPUT + 1: we skip the 8 */

    if (k < n) {                            /* spare slots: we shrink the array */
        double *tmp = realloc(grades, k * sizeof *grades);
        if (tmp != NULL) {                  /* if it failed, the big array would still work */
            printf("Array shrunk from %d to %d slots (%zu -> %zu bytes)\n",
                   n, k, n * sizeof *grades, k * sizeof *grades);
            grades = tmp;
        }
    }

    double m = average(grades, k);
    int passs = 0;
    double max = grades[0], min = grades[0];
    for (int i = 0; i < k; i++) {
        if (grades[i] >= 5) passs++;     /* >= because a 5.0 is a pass */
        if (grades[i] > max) max = grades[i];
        if (grades[i] < min) min = grades[i];
    }
    printf("Valid grades: %d | average %.2f | passed %d\n", k, m, passs);
    printf("Highest %.1f | lowest %.1f\n", max, min);
    printf("Above the average:");
    for (int i = 0; i < k; i++)
        if (grades[i] > m) printf(" %.1f", grades[i]);
    printf("\n");

    free(grades);
    grades = NULL;
    return 0;
}

The heart of the program is read_grades. Let’s look at it on its own:

/* Copies the valid grades (0..10) into v and returns how many it stored */
int read_grades(double *v, int n, const double *input) {
    int k = 0;                              /* k = slots actually in use */
    for (int i = 0; i < n; i++) {
        double x = input[i];
        if (x < 0 || x > 10) {              /* impossible grade: we warn and do not store it */
            printf("Discarded the grade %.1f\n", x);
            continue;
        }
        v[k++] = x;                         /* stores into v[k] and THEN adds 1 to k */
    }
    return k;
}

i walks through the input; k counts the slots really in use. The expression v[k++] = x does two things in this order: it stores x in v[k] and then adds 1 to k. Here’s the trace with the real data:

Round (i)input[i]Valid?Stored ink after
06.5Yesv[0]1
14.0Yesv[1]2
29.5Yesv[2]3
311.5No (> 10)—3
45.0Yesv[3]4
57.0Yesv[4]5
63.0Yesv[5]6
78.5Yesv[6]7
read_grades copies only the valid onesOn top, the 8 input grades; 11.5 is discarded. Below, the 8-slot array: the 7 valid grades are copied into slots 0 to 6 and slot 7 is spare, so realloc trims it.INPUT + 1 (what the teacher types)6.5i=04.0i=19.5i=211.5i=35.0i=47.0i=53.0i=68.5i=7discarded6.5k=04.0k=19.5k=25.0k=37.0k=43.0k=58.5k=6sparek=7grades: malloc of 8 slotsk = 7 → realloc(grades, 7 * sizeof *grades): 64 → 56 bytes
i walks through the input and k only moves on when the grade is valid: that is why 5.0 lands in slot 3.

The drawing shows why 5.0 lands in slot 3 and not in slot 4: 11.5 takes up no space, so everything after it shifts one slot to the left. At the end there’s one spare slot, number 7, and the realloc takes care of it:

if (k < n) {                            /* spare slots: we shrink the array */
    double *tmp = realloc(grades, k * sizeof *grades);
    if (tmp != NULL) {                  /* if it failed, the big array would still work */
        printf("Array shrunk from %d to %d slots (%zu -> %zu bytes)\n",
               n, k, n * sizeof *grades, k * sizeof *grades);
        grades = tmp;
    }
}

Shrinking is the safest way to use realloc, but we still use the temporary: if it failed, the 8-slot array would still be perfectly valid (the 7 grades are in the first 7 slots), so the program just carries on.

Output

Discarded the grade 11.5
Array shrunk from 8 to 7 slots (64 -> 56 bytes)
Valid grades: 7 | average 6.21 | passed 5
Highest 9.5 | lowest 3.0
Above the average: 6.5 9.5 7.0 8.5

Let’s check it by hand. The 7 valid grades add up to 6.5 + 4.0 + 9.5 + 5.0 + 7.0 + 3.0 + 8.5 = 43.5, and 43.5 / 7 = 6.214…, printed as 6.21. Passes (≥ 5): 6.5, 9.5, 5.0, 7.0 and 8.5, which makes 5 (the 5.0 counts thanks to >=). The bytes of the shrink: 8 × 8 = 64 and 7 × 8 = 56. And above 6.21 we have 6.5, 9.5, 7.0 and 8.5, in the order they arrived.

Questions you might ask

**Why INPUT + 1 when calling read_grades?** Because INPUT[0] is the 8 (the number of grades), not a grade. INPUT + 1 is a pointer to the second slot, so inside the function input[0] is already 6.5. Pure pointer arithmetic.

Why reserve 8 if it ends up being 7? Because you don’t know how many will be valid until you’ve read them all. Reserving the maximum and shrinking afterwards is a very common pattern.

**Why do max and min start at grades[0] and not at 0?** If min started at 0, no grade would be lower and the lowest would come out as 0.0. Starting with a real value avoids making up an initial one.

What if all 8 grades were invalid? k would be 0 and average would divide by 0, and realloc(grades, 0) isn’t a good idea either. A real program would check k == 0 before going on: it’s one of the variations below.

**Is the final grades = NULL needed if the program ends right after?** Strictly speaking, no, but it’s a cheap habit that saves you trouble when the code grows and someone adds lines after the free.

Using i to store as well (v[i] = x). Slot 3 would never be written (it’d hold garbage) and the array would have a gap in the middle of the data.

Try it yourself

  • Replace the fixed data with scanf: first read n and then the n grades. Also check that n is greater than 0.
  • Add the k == 0 case: if there are no valid grades, print “No valid grades”, free and finish without computing the average.

Program 2: an array that grows when students arrive

Now we don’t even know how many grades there will be: they arrive one by one until a -1 shows up. We start with room for 2 and, every time the array fills up, we double its capacity with realloc. At the end we shrink it to the exact size. It’s exactly what Python lists or Java’s ArrayList do under the hood.

The idea

You need to keep track of two different numbers:

  • n: how many grades are stored.
  • cap: how many slots are reserved.

When n == cap, the next one doesn’t fit: cap is doubled with realloc. Why double instead of adding one slot each time? Because every realloc may copy the whole array somewhere else. Growing one at a time, 9 grades would need 8 realloc calls; doubling, only 3.

The add function has to be able to change main‘s grades, n and cap, so it receives them by pointer.

Before coding, think about why add takes double **v and not double *v. If realloc moves the block, the new address has to reach main‘s grades variable.

The code

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

/* The grades arrive one by one and we do not know how many: -1 marks the end */
const double INPUT[] = {7.5, 6.0, 8.0, 4.5, 9.0, 5.5, 6.5, 3.5, 10.0, -1};

/* Adds x at the end. If it does not fit, it DOUBLES the capacity. Returns 1 if OK and 0 if not.
   It takes double **v because realloc may move the block and main has to find out. */
int add(double **v, int *n, int *cap, double x) {
    if (*n == *cap) {                                  /* full: time to grow */
        int new_cap = *cap * 2;
        double *tmp = realloc(*v, new_cap * sizeof **v);
        if (tmp == NULL) return 0;                     /* *v is still intact */
        printf("  [grows] capacity %d -> %d (%zu -> %zu bytes)\n",
               *cap, new_cap, *cap * sizeof **v, new_cap * sizeof **v);
        *v = tmp;
        *cap = new_cap;
    }
    (*v)[*n] = x;        /* brackets: first *v (the array), then [*n] */
    (*n)++;              /* (*n)++ adds to the counter; *n++ would move the pointer */
    return 1;
}

int main(void) {
    int n = 0, cap = 2;                          /* we start with room for 2 */
    double *grades = malloc(cap * sizeof *grades);
    if (grades == NULL) return 1;

    for (int i = 0; INPUT[i] != -1; i++) {
        if (!add(&grades, &n, &cap, INPUT[i])) {
            printf("Out of memory: keeping %d grades\n", n);
            break;
        }
        printf("Grade %.1f stored (n=%d, cap=%d)\n", INPUT[i], n, cap);
    }

    double *tmp = realloc(grades, n * sizeof *grades);   /* at the end there is spare room: we shrink */
    if (tmp != NULL) {
        printf("Final shrink: capacity %d -> %d\n", cap, n);
        grades = tmp;
        cap = n;
    }

    double sum = 0;
    for (int i = 0; i < n; i++) sum += grades[i];
    printf("%d grades, average %.2f\n", n, sum / n);

    free(grades);
    return 0;
}

Zoom into add, which is where all the hard stuff is:

/* Adds x at the end. If it does not fit, it DOUBLES the capacity. Returns 1 if OK and 0 if not.
   It takes double **v because realloc may move the block and main has to find out. */
int add(double **v, int *n, int *cap, double x) {
    if (*n == *cap) {                                  /* full: time to grow */
        int new_cap = *cap * 2;
        double *tmp = realloc(*v, new_cap * sizeof **v);
        if (tmp == NULL) return 0;                     /* *v is still intact */
        printf("  [grows] capacity %d -> %d (%zu -> %zu bytes)\n",
               *cap, new_cap, *cap * sizeof **v, new_cap * sizeof **v);
        *v = tmp;
        *cap = new_cap;
    }
    (*v)[*n] = x;        /* brackets: first *v (the array), then [*n] */
    (*n)++;              /* (*n)++ adds to the counter; *n++ would move the pointer */
    return 1;
}

What type you have at each step of the double-asterisk expressions:

ExpressionTypeWhat it is
vdouble **The address of main‘s grades variable
*vdouble *The value of grades: the block’s address
(*v)[*n]doubleSlot number n of the block
**vdoubleThe first slot; only used in sizeof **v (8 bytes)
*nintmain‘s counter

The brackets in (*v)[*n] aren’t decoration: [] binds before *, so *v[*n] would mean “slot *n of v“, which is something else and runs off into other memory. Same with (*n)++: without brackets, *n++ would move the pointer n instead of adding to the counter.

Output

Grade 7.5 stored (n=1, cap=2)
Grade 6.0 stored (n=2, cap=2)
  [grows] capacity 2 -> 4 (16 -> 32 bytes)
Grade 8.0 stored (n=3, cap=4)
Grade 4.5 stored (n=4, cap=4)
  [grows] capacity 4 -> 8 (32 -> 64 bytes)
Grade 9.0 stored (n=5, cap=8)
Grade 5.5 stored (n=6, cap=8)
Grade 6.5 stored (n=7, cap=8)
Grade 3.5 stored (n=8, cap=8)
  [grows] capacity 8 -> 16 (64 -> 128 bytes)
Grade 10.0 stored (n=9, cap=16)
Final shrink: capacity 16 -> 9
9 grades, average 6.72

Here’s the trace of the times the capacity changes:

Grade arrivingn beforecap beforeFull?cap after
7.502No2
6.012No2
8.022Yes4
9.044Yes8
10.088Yes16
(end)916—9 (shrink)
An array that doubles its capacityFive rows: capacity 2 with 2 grades, capacity 4 with 4, capacity 8 with 8, capacity 16 with 9 grades and 7 gaps, and the final shrink to 9 slots. The grades 8.0, 9.0 and 10.0, which forced growth, are highlighted.cap = 27.56.0cap = 47.56.08.04.5cap = 87.56.08.04.59.05.56.53.5cap = 167.56.08.04.59.05.56.53.510.0shrink7.56.08.04.59.05.56.53.510.0coral = the grade that forced growth · dashed = reserved but unused
With 9 grades you only need 3 reallocs to grow and 1 to shrink, instead of one per grade.

The drawing shows each state of the array: in coral, the grade that didn’t fit and forced growth; dashed, the room reserved but unused. With 16 slots and 9 grades there are 7 × 8 = 56 spare bytes, which is why it’s shrunk to 9 at the end. Average check: 7.5 + 6.0 + 8.0 + 4.5 + 9.0 + 5.5 + 6.5 + 3.5 + 10.0 = 60.5, and 60.5 / 9 = 6.72.

Questions you might ask

**Why does the [grows] line come before “Grade 8.0 stored”?** Because add grows first and stores afterwards: grade 8.0 is stored in the 4-slot array.

Isn’t reserving 16 for 9 wasteful? For a while, yes, at most twice what’s needed. In exchange you save lots of copies. And the final shrink sorts it out.

**Why does add return 0 or 1 instead of ending the program?** Because deciding what to do when there’s no memory is the caller’s job. main decides to keep the grades it already has; another program might save to a file and exit.

**What happens to the grades if the growth realloc fails?** Nothing bad: *v isn’t touched and neither is n, so main still has a valid array with all the previous grades.

Mixing up n and cap. If you compute the average dividing by cap (16) instead of n (9), the average comes out as 3.78 and you’re also adding slots that hold garbage.

Try it yourself

  • Change the growth strategy to “add 2 slots” and count how many [grows] lines appear with the same 9 grades.
  • Read the grades with scanf until the user types -1 and try entering 20 grades: what capacity does it reach?

Program 3: a table of grades on the heap

The teacher has several tests per student (labs, midterm and final, weighted 30 %, 30 % and 40 %) and only knows the number of students at run time. We want a students × tests table in dynamic memory that’s used as table[i][j], to compute each student’s final grade and to free everything without leaving anything behind.

The idea

A dynamic table in C is an array of pointers, where each pointer points to a row:

  1. malloc an array of students pointers to double (double **).
  2. One calloc per row, with each student’s 3 grades, starting at 0.0.
  3. If reserving a row fails halfway, free the rows already reserved and the array.
  4. When done, free from the inside out: first each row and then the array of pointers.

Before coding, think about the order of the free calls. If you free the array of pointers first, you have no way left to reach the rows: they become leaks.

The code

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

#define TESTS 3                             /* labs, midterm and final */
const double WEIGHT[TESTS] = {0.3, 0.3, 0.4};
const char *NAME[] = {"Ana", "Bruno", "Carla", "Diego"};
const double DATA[][TESTS] = {{8.0, 6.5, 7.0}, {5.0, 4.0, 3.5},
                                 {9.5, 8.0, 9.0}, {6.0, 7.5, 4.5}};

/* Frees the first 'rows' rows and then the array of pointers (in that order) */
void free_table(double **t, int rows) {
    for (int i = 0; i < rows; i++)
        free(t[i]);                           /* first each row... */
    free(t);                                  /* ...and finally the array that pointed to them */
}

/* rows x cols table on the heap: an array of pointers and one row per student */
double **create_table(int rows, int cols) {
    double **t = malloc(rows * sizeof *t);   /* sizeof *t = sizeof(double *) */
    if (t == NULL) return NULL;
    for (int i = 0; i < rows; i++) {
        t[i] = calloc(cols, sizeof **t);      /* each row starts at 0.0 */
        if (t[i] == NULL) {                   /* if row i fails, we free the i previous ones */
            free_table(t, i);
            return NULL;
        }
    }
    return t;
}

int main(void) {
    int students = 4;                          /* the number of students arrives at run time */
    double **table = create_table(students, TESTS);
    if (table == NULL) { printf("Out of memory\n"); return 1; }
    printf("Reserved: %d pointers (%zu bytes) + %d rows of %zu bytes\n",
           students, students * sizeof(double *), students, TESTS * sizeof(double));

    for (int i = 0; i < students; i++)
        for (int j = 0; j < TESTS; j++)
            table[i][j] = DATA[i][j];        /* table[i] is a row; [j], its slot */

    printf("Student  Labs    Midterm  Final  | Grade\n");
    for (int i = 0; i < students; i++) {
        double grade = 0;
        for (int j = 0; j < TESTS; j++)
            grade += table[i][j] * WEIGHT[j];
        printf("%-8s %5.1f %8.1f %6.1f  | %.2f %s\n", NAME[i], table[i][0], table[i][1],
               table[i][2], grade, grade >= 5 ? "pass" : "fail");
    }

    free_table(table, students);
    table = NULL;
    return 0;
}

The two functions that do the memory work. First, the one that frees:

/* Frees the first 'rows' rows and then the array of pointers (in that order) */
void free_table(double **t, int rows) {
    for (int i = 0; i < rows; i++)
        free(t[i]);                           /* first each row... */
    free(t);                                  /* ...and finally the array that pointed to them */
}

It takes how many rows to free, not always all of them. That lets us reuse it inside create_table when a row fails halfway:

/* rows x cols table on the heap: an array of pointers and one row per student */
double **create_table(int rows, int cols) {
    double **t = malloc(rows * sizeof *t);   /* sizeof *t = sizeof(double *) */
    if (t == NULL) return NULL;
    for (int i = 0; i < rows; i++) {
        t[i] = calloc(cols, sizeof **t);      /* each row starts at 0.0 */
        if (t[i] == NULL) {                   /* if row i fails, we free the i previous ones */
            free_table(t, i);
            return NULL;
        }
    }
    return t;
}

If row i fails, rows 0 to i - 1 do exist: that’s why it calls free_table(t, i). That way a function that fails doesn’t leave reserved garbage behind. The type at each level:

ExpressionTypeWhat it is
tabledouble **Address of the array of pointers
table[i]double *Address of student i’s row
table[i][j]doubleGrade j of student i
sizeof *tsizeof(double *)8 bytes per pointer
sizeof **tsizeof(double)8 bytes per grade
A dynamic table: pointers to rowsThe variable table points to an array of 4 pointers; each pointer points to a row with a student's 3 grades. The numbers show the order of the free calls: first the 4 rows and finally the array of pointers.array of pointersone row per student (calloc of 3)double **tabletable[0]8.06.57.0Ana1freetable[1]5.04.03.5Bruno2freetable[2]9.58.09.0Carla3freetable[3]6.07.54.5Diego4free5free(table)
Free from the inside out: first each row (1 to 4) and finally the array that pointed to them (5).

The drawing shows the full structure with the program’s data and the numbers of the order in which things are freed: the four rows (1 to 4) and the array of pointers last (5).

Output

Reserved: 4 pointers (32 bytes) + 4 rows of 24 bytes
Student  Labs    Midterm  Final  | Grade
Ana        8.0      6.5    7.0  | 7.15 pass
Bruno      5.0      4.0    3.5  | 4.10 fail
Carla      9.5      8.0    9.0  | 8.85 pass
Diego      6.0      7.5    4.5  | 5.85 pass

Let’s check Ana’s grade by hand: 8.0 × 0.3 + 6.5 × 0.3 + 7.0 × 0.4 = 2.40 + 1.95 + 2.80 = 7.15. Bruno’s: 1.50 + 1.20 + 1.40 = 4.10, a fail. And the memory: 4 pointers × 8 = 32 bytes plus 4 rows of 3 × 8 = 24 bytes, 128 bytes in total. The columns aren’t perfectly aligned under the header: that’s down to the printf format, not the memory.

Questions you might ask

**Why not double table[students][3]?** Since C99 there are variable-length arrays (VLAs), but they live on the stack: you can’t return them from a function and, if they’re big, they overflow the stack. The heap has neither problem.

**Why calloc for the rows and not malloc?** Because a freshly created row then holds zeros instead of garbage. If a student missed a test, their slot would be 0.0, which makes sense.

Are the rows next to each other in memory? Not necessarily. Each calloc may hand out a block somewhere else on the heap. That’s why table[i][j] works: first it fetches the row through its pointer and then the slot.

**Why does the array of pointers use malloc and not calloc?** Because we fill it in completely right afterwards, row by row. If a row fails, we already know how many are valid (i) and never read the rest.

**What happens if I call free_table(table, students) twice?** A double free of each row: the program aborts. That’s why we then set table = NULL.

Freeing only table with a free(table). The four rows stay reserved and there’s no pointer left to reach them: 4 leaks of 24 bytes.

Try it yourself

  • Add one more column (a fourth test with its weight) by changing only TESTS, WEIGHT and DATA. Which part of the memory code did you have to touch?
  • Add a function double *final_grades(double **t, int rows) that returns a dynamic array with each student’s final grade. Remember to free it in main.

Common mistakes in this practice

  • One index for reading and writing. When filtering data, the write index (k) must be different from the read index (i).
  • **realloc without a temporary.** Even when shrinking, store the result in tmp and check for NULL.
  • **Mixing up n and cap.** One counts data; the other, reserved slots. Averages and loops use n.
  • **Forgetting the brackets with **.** (*v)[*n] and (*n)++; without them, the program touches other memory.
  • Freeing the table from the outside in. First the rows, then the array of pointers.
  • Not freeing what was reserved when something fails halfway. Every exit path has to leave memory clean.

Watch it step by step in Python Tutor

Open Python Tutor, pick C (gcc) and paste this short version of program 2 (5 grades, no functions). Click Visualize Execution and move forward with Next.

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

int main(void) {
    int n = 0, cap = 2;
    double *grades = malloc(cap * sizeof(double));
    if (grades == NULL) return 1;
    double arriving[] = {7.5, 6.0, 8.0, 4.5, 9.0};

    for (int i = 0; i < 5; i++) {
        if (n == cap) {
            double *tmp = realloc(grades, 2 * cap * sizeof(double));
            if (tmp == NULL) break;
            grades = tmp;
            cap = 2 * cap;
        }
        grades[n] = arriving[i];
        n++;
    }
    printf("n=%d cap=%d\n", n, cap);
    free(grades);
    return 0;
}

What to watch step by step:

  1. After the malloc, the heap block has 2 slots and cap is 2. The first two rounds fill it without growing.
  2. In the third round, n == cap: the realloc runs and a 4-slot block appears on the heap with 7.5 and 6.0 already copied. Look at where tmp and grades point.
  3. In the fifth round it fills up again and the block grows to 8 slots. Notice that slots 5 to 7 stay without a value: reserved, but unused.
  4. Before the free, compare n (5) and cap (8): they’re the two figures you always have to keep separate.

Summary

  • To filter data in an array, use a read index and a write index, and shrink at the end with realloc.
  • A growing dynamic array keeps two numbers, n and cap, and doubles its capacity when it fills up.
  • A function that has to change a caller’s pointer takes a pointer to a pointer (double **).
  • realloc always with a temporary, also when shrinking.
  • A dynamic table is an array of pointers to rows; it’s reserved from the outside in and freed from the inside out.
  • If a reservation fails halfway, free what you’d already reserved before returning NULL.

Checklist: have you practised it?

  • Have you run the three programs and checked the outputs?
  • Can you explain why 5.0 ends up in slot 3 of the report?
  • Could you say in which rounds the array of program 2 grows without looking at the output?
  • Do you understand why add takes double ** and not double *?
  • Do you know in which order the rows and the array of pointers are freed, and why?
  • Have you tried the “Try it yourself” tasks without looking at the theory?

If you answered yes to almost everything, you’re ready for the dynamic memory in C exercises, where I no longer give you the code. See you there!

Next step: exercisesDynamic 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)

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

Sigue aprendiendo

2 Comments

Leave a Reply

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