Pointers, NULL & nullptr

C · Lesson 18 of 32 7 min read Updated

Pointers are the feature that makes C powerful, and the one that worries beginners most. The idea is simple: every variable lives somewhere in memory, and that place has an address, a number. A pointer is a variable that stores an address.

With pointers, a function can change the caller's variables, large data can be shared without copying, and you can build structures such as linked lists. In this lesson you will learn the two pointer operators, use pointers with functions, and learn the safe way to say "points to nothing" with C23's nullptr.

Addresses and the two pointer operators

Think of memory as a long row of numbered boxes, one per byte. When you declare int marks = 87;, the compiler reserves four boxes for marks, say starting at box 1000. Two operators work with addresses:

  • & (address-of) gives the address of a variable: &marks is 1000.
  • * (dereference) follows an address to the value stored there: if p holds 1000, then *p is 87.

A pointer is declared with a * between the type and the name. int *p means "p holds the address of an int".

C
#include <stdio.h>

int main(void)
{
    int marks = 87;
    int *p = &marks;       // p holds the address of marks

    printf("marks    = %d\n", marks);
    printf("&marks   = %p\n", (void *) &marks);
    printf("p        = %p\n", (void *) p);
    printf("*p       = %d\n", *p);

    *p = 92;               // change marks through the pointer
    printf("marks    = %d\n", marks);
    printf("sizeof p = %zu\n", sizeof p);
    return 0;
}

A typical run on 64-bit Linux prints:

Output
marks    = 87
&marks   = 0x7ffc8a2e4b1c
p        = 0x7ffc8a2e4b1c
*p       = 87
marks    = 92
sizeof p = 8

The addresses on your computer will be different, and they change on every run, because modern systems place memory at random positions for security. What matters is that &marks and p are the same, and that writing *p = 92 changed marks itself. %p prints an address, and it expects a void * (a generic address), hence the cast. On 64-bit systems every pointer is 8 bytes, whatever type it points to.

The * has two jobs that look alike. In a declaration (int *p), it says "p is a pointer". In an expression (*p = 92), it means "the thing p points to".

Changing the caller's variables

In Functions & Prototypes you saw that functions receive copies of their arguments. To let a function change a variable, pass its address:

C
#include <stdio.h>

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

int main(void)
{
    int priya_seat = 14;
    int kabir_seat = 27;

    swap(&priya_seat, &kabir_seat);
    printf("Priya: %d, Kabir: %d\n", priya_seat, kabir_seat);
    return 0;
}
Output
Priya: 27, Kabir: 14

swap receives two addresses. The pointers themselves are still copies, but they point at main's variables, so writing through them changes the originals. This also explains scanf("%d", &age): scanf needs to know where to store the number.

Returning several results

A function can return only one value, but output parameters let it hand back more. The caller passes addresses, and the function fills them in:

C
#include <stdio.h>

void min_max(const int values[], size_t n, int *min, int *max)
{
    *min = values[0];
    *max = values[0];
    for (size_t i = 1; i < n; i++) {
        if (values[i] < *min) {
            *min = values[i];
        }
        if (values[i] > *max) {
            *max = values[i];
        }
    }
}

int main(void)
{
    int prices[] = {1299, 899, 2499, 1599, 749};
    int low, high;

    min_max(prices, 5, &low, &high);
    printf("Cheapest: Rs %d, costliest: Rs %d\n", low, high);
    return 0;
}
Output
Cheapest: Rs 749, costliest: Rs 2499

Null pointers: nullptr and NULL

Sometimes a pointer has nothing to point to: a search that found nothing, or a value not yet available. For this C has the null pointer, a special value that never equals the address of any real object. Checking for it is how functions report "not found".

C
#include <stdio.h>

int *find_first_below(int marks[], size_t n, int limit)
{
    for (size_t i = 0; i < n; i++) {
        if (marks[i] < limit) {
            return &marks[i];
        }
    }
    return nullptr;   // nothing found
}

int main(void)
{
    int marks[] = {56, 71, 38, 90};

    int *failed = find_first_below(marks, 4, 40);
    if (failed != nullptr) {
        printf("First failing mark: %d\n", *failed);
        *failed += 2;   // grace marks, written straight into the array
        printf("After grace: %d\n", marks[2]);
    }

    if (find_first_below(marks, 4, 30) == nullptr) {
        printf("Nobody scored below 30\n");
    }
    return 0;
}
Output
First failing mark: 38
After grace: 40
Nobody scored below 30

The function returns the address of the matching element, so the caller can read it and even change it. Before you dereference any pointer that might be null, check it. if (p) is a common short form of if (p != nullptr). Dereferencing a null pointer is undefined behaviour; on Linux it usually stops the program with "Segmentation fault".

Pointers and const

const can protect either the value being pointed to or the pointer itself. Read the declaration from right to left:

C
#include <stdio.h>

int main(void)
{
    int stock = 50;
    int other = 10;

    const int *view = &stock;   // pointer to a const int
    // *view = 40;              // error: assignment of read-only location
    view = &other;              // allowed: view can point elsewhere

    int *const fixed = &stock;  // const pointer to an int
    *fixed = 45;                // allowed: stock becomes 45
    // fixed = &other;          // error: assignment of read-only variable 'fixed'

    printf("stock = %d, *view = %d\n", stock, *view);
    return 0;
}
Output
stock = 45, *view = 10

const int * is the form you will use most, in function parameters such as const int values[] or const char *name. It promises that the function only reads the data, and the compiler enforces the promise.

Dangerous pointers

Three kinds of pointer cause most crashes:

  • Uninitialised (wild) pointers. int *p; *p = 5; writes to a random address. Always initialise pointers, to a real address or to nullptr.
  • Dangling pointers. A pointer to a local variable becomes invalid when the function returns, and a pointer to freed memory becomes invalid after free. Using it is undefined behaviour.
  • Null dereference. Following nullptr crashes. Check before you use a pointer that might be null.

Common mistakes

  • int *p, q; declares only one pointer. p is a pointer but q is a plain int. Declare one pointer per line.
  • Forgetting & or *. Passing age instead of &age, or printing p when you meant *p. -Wall catches many of these.
  • Returning the address of a local variable. GCC warns "function returns address of local variable"; return the value instead, or use allocated memory.
  • Printing an address with %d. Use %p and cast to void *.
  • Mixing pointer types. Storing the address of a double in an int * is an error in modern GCC.

Key points

  • A pointer stores an address; &x gives the address of x, and *p reaches the value p points to.
  • Pass addresses to let a function change the caller's variables or return several results.
  • nullptr (C23) is the null pointer; older code uses NULL or 0. Check before dereferencing.
  • const int *p protects the pointed-to value; int *const p protects the pointer itself.
  • Never use uninitialised, dangling or null pointers; initialise every pointer when you declare it.

Exercise

Four friends in Pune share a dinner bill of Rs 2,350 equally, in whole rupees. Write a function void split_bill(int total, int people, int *share, int *left_over) that stores each person's share and the rupees left over through its pointer parameters. Call it from main and print both results.

Show solution
C
#include <stdio.h>

void split_bill(int total, int people, int *share, int *left_over)
{
    *share = total / people;
    *left_over = total % people;
}

int main(void)
{
    int share = 0;
    int left_over = 0;

    split_bill(2350, 4, &share, &left_over);
    printf("Each pays Rs %d, Rs %d left over\n", share, left_over);
    return 0;
}
Output
Each pays Rs 587, Rs 2 left over

main passes the addresses of share and left_over, so the function can store two results. 4 × 587 = 2348, leaving Rs 2.

Quick Quiz

1. If int x = 5; int *p = &x;, what is *p?

2. Why does scanf("%d", &n) need the &?

3. What is the C23 way to write a null pointer?

4. In int *p, q;, what is the type of q?

Learn C with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call