---
title: 'Arrays and Pointers'
source: 'https://academia.sh/en/courses/c-memory/arrays-and-pointers'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "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'
---

# 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.

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

```c
// 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

```c
// 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

```c
// 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.

```c
// 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**.

```c
// 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]`.

```c
// 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 `i`th **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.
