Skip to content
Informatics

Pointers and dynamic memory in C

Addresses, pointers and dereferencing, pass by reference, pointer arithmetic and arrays, malloc and free, classic mistakes, memory on microcontrollers, function pointers.

Pointers are C’s most powerful feature and the one that scares learners most. The idea is actually simple: a pointer is a variable that holds a memory address. With pointers a function can change the caller’s variables, arrays are walked through, memory is allocated at run time and hardware registers are accessed directly. In this lesson we take them step by step, together with dynamic memory and the mistakes to avoid.

Addresses and pointers

Each variable occupies one or more memory cells, and the & operator gives its address. A pointer is declared with an asterisk and holds an address; the * operator applied to the pointer gives access to the variable pointed to (dereferencing).

int x = 42;
int *p = &x;          /* p holds the address of x: p "points to" x */

printf("%d\n", *p);   /* 42: the value of the variable pointed to */
*p = 7;               /* changes x through the pointer */
printf("%d\n", x);    /* 7 */

The pointer’s type (int *, double *, char *) says what kind of data is at the address, and therefore how many bytes to read. The null pointer NULL points to nothing: it is the value used to say “no object”, and dereferencing it crashes the program.

Pass by reference

To let a function change a caller’s variable, pass it the variable’s address:

void swap(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

int main(void)
{
    int x = 3, y = 8;
    swap(&x, &y);             /* now x is 8 and y is 3 */
    return 0;
}

This way a function can also return several results, for example the magnitude and phase of a complex number, and it is why scanf wants &x: it must write into the caller’s variable.

Pointers and arrays

An array’s name, in almost every expression, converts to the address of its first element. And you can do arithmetic on pointers: p + 1 is not the next byte address, but that of the next element.

An array of integers in memory with addresses and a pointer to the third element
An array of five 4-byte integers starting at address 0x1000. The pointer p holds 0x1008, the address of a[2]: so *p is 33, and p + 1 points to the next element, at address 0x100C.

The equivalences a[i] == *(a + i) and &a[i] == a + i hold. You can walk an array with a pointer:

double average(const double *v, int n)
{
    double s = 0.0;
    for (const double *p = v; p < v + n; p++) {
        s += *p;
    }
    return s / n;
}

Dynamic memory

Arrays have a fixed size, decided when the program is written. If the size is known only at run time (how many samples to read from a file, how many nodes a circuit has) you ask the system for memory with malloc, in the area called the heap, and give it back with free when it is no longer needed.

#include <stdlib.h>

int n = read_sample_count();
double *samples = malloc(n * sizeof *samples);
if (samples == NULL) {                /* malloc can fail: always check */
    fprintf(stderr, "Out of memory\n");
    return 1;
}
/* ... use samples[0] ... samples[n - 1] like an array ... */
free(samples);
samples = NULL;                       /* avoids reusing it by mistake */

The classic mistakes

  • Uninitialised pointer: int *p; *p = 5; writes to a random address.
  • Dereferencing NULL: for example not checking the result of malloc.
  • Memory leak: forgetting free. In a program that runs for months, such as a router’s firmware, memory runs out little by little.
  • Use after free: using memory after freeing it. Its content may have been reassigned to something else.
  • Double free: freeing the same block twice corrupts the allocator’s internal structures.
  • Pointer to a local variable returned from a function: when the function ends, the variable no longer exists.

These mistakes account for a large share of the vulnerabilities in software written in C and C++: that is why newer languages such as Rust check memory use at compile time. Tools such as valgrind and AddressSanitizer find them during testing.

On microcontrollers dynamic allocation is almost always avoided: with a few kilobytes of RAM, repeated allocations and frees fragment the memory until malloc fails after hours or days of operation. Guidelines for critical software, such as MISRA C used in automotive, forbid it after initialisation: static arrays sized for the worst case are used instead.

Function pointers

A pointer can also hold the address of a function, which is then called through it. It is used to choose behaviour at run time: the standard library’s qsort receives a pointer to the function that compares two elements, and microcontrollers’ interrupt vector tables are arrays of pointers to the functions to run when an interrupt arrives.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x