Skip to content
academia.sh

Lesson 03 / 15

Arrays and Pointers

How an array's name decays into the address of its first element when it is passed to a function; why that decay loses the length information; and how ownership of a dynamically allocated array passes to the caller.

Published Updated

In this lesson

  • Shows with an example that an array's name decays into the address of its first element when passed to a function
  • Explains why the length information lost through decay must be carried as a separate parameter
  • Measures with a counter that ownership of a dynamically allocated array passes to the caller
Contents

Two lessons ago, the core’s allocate and free row allocated a block with count_alloc and freed it with count_free; its class was caller, because the side requesting the memory was obligated to free it. This lesson deepens that row, first by measuring how array names reach functions — because freeing a dynamic array correctly first requires knowing what it arrives at a function as.

Decay: When an Array Name Is Passed to a Function

// decay.c — an array name decays into the address of its first element in a function parameter
#include <stdio.h>

static void show(int *p, int length) {
    int total = 0;
    for (int i = 0; i < length; i++) {
        total += p[i];
    }
    printf("%s %d\n", "total inside function", total);
    printf("%s %d\n", "first element inside function", *p);
}

int main(void) {
    int array[4] = {2, 4, 6, 8};

    printf("%s %d\n", "first element outside array", *array);
    printf("%s %d\n", "does array equal the first element's address", (int)(array == &array[0]));

    show(array, 4);

    int *pointer = array;
    printf("%s %d\n", "does the array assigned to a pointer equal the first element", (int)(pointer == &array[0]));
    return 0;
}
first element outside array 2
does array equal the first element's address 1
total inside function 20
first element inside function 2
does the array assigned to a pointer equal the first element 1

PO11. The comparison array == &array[0] gives 1: when an array name is used, that name carries the same value as the address of its first element. In the call show(array, 4), array passes to the function’s int *p parameter as this address — the notation p[i] inside the function reaches the same elements as array[i], and the total comes out 20 (2+4+6+8). The assignment pointer = array shows the same thing: when an array name is assigned to a pointer variable, it is not the array itself that gets copied, but the address of its first element. This is called decay: an array name loses its own type (array) in a function parameter or a pointer assignment and turns into a pointer type.

The Lost Length

// length_loss.c — decay does not carry the array's element count
#include <stdio.h>

static int sum(int *p, int length) {
    int total = 0;
    for (int i = 0; i < length; i++) {
        total += p[i];
    }
    return total;
}

int main(void) {
    int small[3] = {1, 1, 1};
    int large[7] = {1, 1, 1, 1, 1, 1, 1};

    printf("%s %d\n", "small array total", sum(small, 3));
    printf("%s %d\n", "large array total", sum(large, 7));
    printf("%s %d\n", "large array total with wrong length", sum(large, 3));
    return 0;
}
small array total 3
large array total 7
large array total with wrong length 3

PO12. The sum function can never work out how many elements long the array is from the pointer it receives — this is why length is carried as a separate parameter. In the last call, the large array (7 elements) passes to the function, but length is given as 3; the function trusts this wrong information and only sums the first three elements, giving a result of 3 — with no relation to the array’s actual size at all. This is the bounding consequence of decay: when an array is passed to a function, the length information must be carried by hand as a separate value at the source; it can never be recovered from the pointer itself.

Carrying the length as a separate parameter is not the only method used in C. Another common solution is to place a terminator value inside the array itself — the '\0' at the end of a string does exactly this job: a loop walking over a string learns the length not from a separate parameter, but by advancing until it reaches the terminator. What the two methods have in common: the information lost through decay must be recovered some other way at the source — either carried alongside as a number, or found through a mark embedded inside the array. There is no third way; the pointer itself never carries this information.

Ownership of a Dynamic Array

// dynamic_array.c — the owner of an allocated array is the side that requested it
#include <stdio.h>
#include <stdlib.h>

static int *create_array(int length, int start) {
    int *array = malloc((size_t)length * sizeof *array);
    if (array == NULL) {
        return NULL;
    }
    for (int i = 0; i < length; i++) {
        array[i] = start + i;
    }
    return array;
}

int main(void) {
    int length = 5;
    int *array = create_array(length, 10);

    if (array == NULL) {
        printf("%s\n", "allocation failed");
        return 1;
    }

    int total = 0;
    for (int i = 0; i < length; i++) {
        total += array[i];
    }
    printf("%s %d\n", "dynamic array total", total);
    printf("%s %d\n", "last element", array[length - 1]);

    free(array);
    array = NULL;
    printf("%s %d\n", "is it a null pointer after being freed", (int)(array == NULL));
    return 0;
}
dynamic array total 60
last element 14
is it a null pointer after being freed 1

PO13. create_array allocates an array but never frees it itself — it returns the block to main. The total inside main is 60 (10+11+12+13+14) and the last element is 14, showing the array filled correctly. Ownership crosses the function boundary here: create_array allocates the memory, but main, which requested and uses it, is obligated to free it — which is why the class is caller. Right after the free(array) call, array is assigned NULL, and the comparison array == NULL gives 1 — this prevents the dangerous condition, the topic of the next lesson, of a freed pointer.

How Far a Wrong Length Stays Silent

The length_loss.c example showed a wrong length giving an incomplete total, but that was only the “undercounting” case. What happens when the wrong length is a value that exceeds the array’s actual bound? The following example measures this question by placing a call that stays within bounds and a call that exceeds it side by side.

// bound_overrun.c — a correct length stays within bounds, a wrong length can exceed it
#include <stdio.h>

static int largest(const int *p, int length) {
    int large = p[0];
    for (int i = 1; i < length; i++) {
        if (p[i] > large) {
            large = p[i];
        }
    }
    return large;
}

int main(void) {
    int array[4] = {3, 2, 9, 7};

    printf("%s %d\n", "largest with correct length", largest(array, 4));
    printf("%s %d\n", "largest with a short length", largest(array, 2));
    return 0;
}
largest with correct length 9
largest with a short length 3

PO14. Called with the correct length (4), largest scans the whole array and finds 9. Called with a short length (2), it only looks at the first two elements and returns 3 — the value 9 exists in the array but is never seen because it sits third. This lesson deliberately includes only the short length call; giving a value that exceeds the actual length (something like largest(array, 6)) would try to read memory past the end of the array, and that is undefined behavior — so it is never run here. What the two cases have in common: the largest function trusts the length value passed to it unconditionally — it can detect neither a short nor a long value on its own, because all it has is a starting address and a number.

Returning the Length Along With It

create_array took the length as a value main already knew, and never computed it itself. If a function determines the length of the array it allocates itself — say, it produces a filtering result, an array shorter than its input — how does it report that length back to the caller? Since the pointer itself does not carry it, the only way is an output parameter.

// returns_length_too.c — an allocating function reports the length back through an output parameter
#include <stdio.h>
#include <stdlib.h>

static int *select_evens(const int *source, int length, int *result_length) {
    int *result = malloc((size_t)length * sizeof *result);
    if (result == NULL) {
        *result_length = 0;
        return NULL;
    }
    int count = 0;
    for (int i = 0; i < length; i++) {
        if (source[i] % 2 == 0) {
            result[count] = source[i];
            count++;
        }
    }
    *result_length = count;
    return result;
}

int main(void) {
    int source[6] = {1, 2, 3, 4, 5, 6};
    int length = 0;

    int *evens = select_evens(source, 6, &length);
    printf("%s %d\n", "number of elements selected", length);

    int total = 0;
    for (int i = 0; i < length; i++) {
        total += evens[i];
    }
    printf("%s %d\n", "total of the selected elements", total);

    free(evens);
    evens = NULL;
    return 0;
}
number of elements selected 3
total of the selected elements 12

PO15. select_evens knows the source array can produce at most length elements, so it allocates the block at that size, but it only learns how many of them are actually even after scanning — it writes this information back to the caller through *result_length. After this call, main holds both the new array (evens) and its actual length (length, now 3); the total, 12 (2+4+6), proves the two match up correctly. Ownership is again with the caller, just as in the create_array example: select_evens allocates the memory but never frees it, main uses it and frees it with free. Unlike create_array, this example measures the case where the length comes not from the input but from the function’s own count — so the length is no longer an input parameter but an output parameter. result_length itself passes as an int * — a separate usage showing that pointers can be used to write a single value “back to the caller,” not only to reach arrays.

Decay of a Two-Dimensional Array

A one-dimensional array decayed into int * when passed to a function. A two-dimensional array’s decay works one level short: int matrix[3][4] does not become int ** in a function parameter — the row size (4 elements) stays as part of the type, and it decays into int (*)[4].

// matrix_decay.c — int matrix[3][4] passes to a function as int (*)[4], not int **
#include <stdio.h>

static int row_total(int row[4]) {
    int total = 0;
    for (int i = 0; i < 4; i++) {
        total += row[i];
    }
    return total;
}

static int grand_total(int (*matrix)[4], int row_count) {
    int total = 0;
    for (int i = 0; i < row_count; i++) {
        total += row_total(matrix[i]);
    }
    return total;
}

int main(void) {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    printf("%s %d\n", "first row total", row_total(matrix[0]));
    printf("%s %d\n", "grand total of the matrix", grand_total(matrix, 3));
    return 0;
}
first row total 10
grand total of the matrix 78

PO16. The grand_total function’s parameter is int (*matrix)[4] — it carries the type “the address of an array of four elements.” The notation matrix[i] here means *(matrix + i), and it gives the address of the first element of the ith row; that address decays once more (this time to int *) when it passes to row_total. The first row total is 10 (1+2+3+4), and the grand total of the matrix is 78 (the sum of all elements from 1 to 12) — proving the two-layer decay reaches the correct elements. If the parameter had been written as int **matrix, the compiler would reject it: int (*)[4] and int ** are the same size (one address) but are different types, because the first carries the information “the address of an array whose row size is 4,” while the second means only “a pointer to a pointer” — this lost information is exactly what the compiler needs to route a matrix[i][j] access to the correct row.

The Bounding Measurement

Decay only operates at a single level: an int array[4] becomes int * when passed to a function, but an int matrix[3][4] does not become int ** — the matrix_decay.c example above already measured this through row totals. This lesson stops at multi-dimensional arrays here; the rule that gets fixed is: decay always produces an address, and that address never carries the information of “how many elements a block it points to.” This information must always be carried explicitly at the source, either through a separate parameter (like length above) or a terminator value (like the '\0' at the end of a string). In matrix_decay.c, this information traveled by a third route: because the row size (4) stays embedded in the type itself (int (*)[4]), no separate parameter was needed for it, but the row count (3) still had to be passed to grand_total as a separate row_count parameter — because the type only carries the column size, not the row count.

Summary

  • An array name decays into the address of its first element when passed as a function parameter or assigned to a pointer; array == &array[0] proves this decay.
  • Decay does not carry the array’s element count — given a wrong length parameter, a function cannot detect it; it only accesses however many elements it was given.
  • The owner of a dynamically allocated array is the side that requested and uses it (class caller); the allocating function can hand the block back and pass on the obligation to free it.
  • Assigning NULL to a pointer right after freeing a block explicitly marks at the source that the pointer no longer points to a valid block.
  • The length of a multi-element block cannot be recovered from the pointer itself; it must be carried by a separate value, an output parameter, or a terminator — an input parameter if the length comes from the input, an output parameter if the function computes it itself, but ownership stays with the caller in both cases.
  • A two-dimensional array decays into type int (*)[N] when passed to a function, not int ** — the row size is carried as part of the type, the row count is not.

The three new measurements in this lesson all tie back to the same observation from different angles: an array’s decay into a pointer carries no information about the array itself — not its length, not its row count, not how far it is safe to read. This information always has to come through a separate channel: either as a parameter (length in largest), embedded in the type itself (int (*)[4] in matrix_decay.c), or as an output parameter (result_length in returns_length_too.c). All three are different solutions to the same bound; none removes the bound, they only make it visible and portable.

Next Step

In the previous example, array was assigned NULL right after the free(array) call — but what happens if that step is skipped? The next lesson, Null and Dangling Pointers, measures what a pointer that still points at a freed block means, why that condition is never run here, and why assigning NULL moves it out of the unowned class into a condition that can be checked.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close