C Pointers

A pointer is a variable whose value is the address of another object in memory. Pointers matter in C because they let code work directly with storage: functions can modify caller variables, arrays can be traversed efficiently, and later topics such as strings, dynamic memory, and data structures depend on them.

The key idea is simple: a normal variable stores a value, while a pointer stores where a value lives. The power and risk of pointers both come from that extra level of indirection.

Overview: How C Pointers Work

Every object in a running C program occupies some bytes in memory. An int variable might occupy four bytes, a double might occupy eight, and an array occupies a sequence of element objects. The address-of operator, &, asks for the memory address of an object. If score is an int, then &score has type int *, read as “pointer to int”.

A pointer variable stores one of these address values. The pointer’s type is not decoration; it tells the compiler what kind of object is expected at that address. When you dereference a pointer with *p, C uses the pointer type to know how many bytes to read or write and how to interpret them. Dereferencing an int * accesses an int. Dereferencing a double * accesses a double.

Pointers are separate variables. If int *p = &score;, then p stores the address of score. The expression *p refers to the actual score object. Changing *p changes score, because both names reach the same storage.

A pointer can also hold a special value, NULL, meaning it points to no object. A null pointer is useful for “not available” or “not found” states, but it must not be dereferenced. Dereferencing a null pointer, an uninitialized pointer, or a pointer to an object whose lifetime has ended causes undefined behavior: the C standard gives no reliable result, so the program might crash, print nonsense, or appear to work until later.

Pointer arithmetic is defined in units of the pointed-to type. If p is an int *, then p + 1 means the next int object, not the next byte. This is why pointers work naturally with arrays: in most expressions, an array name converts to a pointer to its first element.

Syntax

type *name;
type *name = &object;
*name
name = NULL;
Syntax Meaning
int *p; Declares p as a pointer to int. Its value is uninitialized until assigned.
p = &x; Stores the address of x in p. The pointed-to type must match or be compatible.
*p Dereferences p, accessing the object it points at.
NULL A null pointer constant, used when a pointer intentionally points to no object.
const int *p Pointer to a read-only int through this pointer.
int *const p Constant pointer to a modifiable int; the address in p cannot be changed.

The * character appears in both declarations and expressions, but it has different roles. In int *p, it is part of the declaration: p is a pointer. In *p = 10, it is the dereference operator: use the object that p points to.

Examples

Pointing to a Variable

#include <stdio.h>

int main(void)
{
    int score = 72;
    int *score_ptr = &score;

    printf("score = %d
", score);
    printf("*score_ptr = %d
", *score_ptr);

    *score_ptr = 90;

    printf("score after change = %d
", score);
    return 0;
}

Output:

score = 72
*score_ptr = 72
score after change = 90

score_ptr stores the address of score. The expression *score_ptr reaches the same int object as the name score, so assigning through the pointer changes the original variable.

Using a Pointer Parameter

#include <stdio.h>

void add_bonus(int *points, int bonus)
{
    if (points != NULL) {
        *points += bonus;
    }
}

int main(void)
{
    int quiz_score = 18;

    add_bonus(&quiz_score, 2);

    printf("Quiz score: %d
", quiz_score);
    return 0;
}

Output:

Quiz score: 20

C passes function arguments by value, so a function normally receives a copy. Passing &quiz_score gives the function a copy of the address instead. That copied address still points to the caller’s variable, allowing add_bonus to modify it. The null check makes the function safe to call even when no valid object is available.

Traversing an Array with Pointers

#include <stdio.h>
#include <stddef.h>

int main(void)
{
    int readings[] = {4, 7, 9, 10, 6};
    size_t count = sizeof(readings) / sizeof(readings[0]);
    int total = 0;

    for (int *p = readings; p < readings + count; p++) {
        total += *p;
    }

    printf("Total: %d
", total);
    printf("Average: %.1f
", (double)total / count);
    return 0;
}

Output:

Total: 36
Average: 7.2

In the loop initializer, readings converts to a pointer to the first element. Each p++ moves to the next int. The loop stops at readings + count, which is the legal one-past-the-end pointer used only for comparison, not dereferencing.

Const and Pointer Meaning

#include <stdio.h>

void print_label(const char *label)
{
    if (label != NULL) {
        printf("Label: %s
", label);
    }
}

int main(void)
{
    int value = 5;
    int other = 8;
    const int *read_only = &value;
    int *const fixed_pointer = &value;

    printf("Read through pointer: %d
", *read_only);
    read_only = &other;
    printf("Now reading: %d
", *read_only);

    *fixed_pointer = 12;
    printf("Changed value: %d
", value);

    print_label("ready");
    return 0;
}

Output:

Read through pointer: 5
Now reading: 8
Changed value: 12
Label: ready

const int *read_only means the value cannot be modified through that pointer, but the pointer may point somewhere else. int *const fixed_pointer means the pointer keeps the same address, but the object may be changed. Function parameters such as const char *label tell callers that the function will read the data, not modify it.

How It Works Step by Step

  1. The program creates an object such as int score. That object has storage, a type, a value, and a lifetime.
  2. The expression &score produces an address value whose type is int *.
  3. A pointer variable stores that address value. The pointer itself also has storage, usually large enough to hold any ordinary object address on the platform.
  4. When the program evaluates *p, it first reads the address stored in p, then accesses the object located there as the pointed-to type.
  5. For assignment such as *p = 90, C writes into the pointed-to object. Any other expression referring to that same object observes the new value.
  6. For pointer arithmetic such as p + 1, the compiler scales the movement by sizeof *p. With an int *, the next pointer value is the next int position.

The compiler does not automatically know whether a pointer value is valid. That responsibility belongs to the programmer. A pointer should point to a live object of a compatible type, or it should be NULL and checked before use.

Common Mistakes

Dereferencing Before Assigning

A declaration such as int *p; creates a pointer variable, but it does not make it point anywhere useful. Writing through *p before assigning p a valid address is undefined behavior. Correct code assigns the pointer first, as in int value = 10; int *p = &value;.

Confusing the Pointer with the Pointed-To Value

p is the stored address. *p is the object at that address. If you want to change the original int, write *p = 25;, not p = 25;. Assigning an integer to a pointer is a type error or a serious bug unless the value is a valid null pointer constant such as NULL.

Returning Addresses of Local Variables

A local variable inside a function stops existing when that function returns. Returning &local leaves the caller with a dangling pointer. Instead, return the value itself, have the caller pass in storage through a pointer parameter, or use dynamic memory in lessons where allocation is appropriate.

Moving Outside an Array

Pointer arithmetic is only defined inside the same array object, plus the one-past-the-end position used for comparison. Dereferencing array + count is out of bounds. Stop before that position, as the array traversal example does.

Best Practices

  • Initialize pointers when you declare them, either to a valid address or to NULL.
  • Check for NULL before dereferencing a pointer that might not point to an object.
  • Keep pointer types accurate. Avoid casts that silence warnings unless you understand the exact object representation and aliasing rules involved.
  • Use const for pointer parameters that only read data. This documents intent and prevents accidental writes.
  • Prefer array indexing when it is clearer; use pointer traversal when it makes the relationship to addresses or ranges clearer.
  • Never return the address of a local automatic variable.
  • Keep pointers within the lifetime and bounds of the object they refer to.
  • Use descriptive names such as current, end, or score_ptr instead of vague names when pointer roles differ.

Practice Exercises

  1. Write a function void swap_ints(int *a, int *b) that swaps two integers. Test it from main by printing the values before and after the call.
  2. Write a function int max_value(const int *values, size_t count) that returns the largest element in an array. Decide how your function should behave when count is zero.
  3. Create an array of five prices and use two pointers, current and end, to print only the prices greater than 10.

Summary

  • A pointer stores an address; dereferencing accesses the object at that address.
  • The address-of operator & creates a pointer to an object, and the dereference operator * follows a pointer.
  • Pointer types tell C how to read, write, and move through memory.
  • Passing pointers to functions lets those functions modify caller-owned objects.
  • Arrays and pointers are closely related, but pointer arithmetic must stay within array bounds.
  • Invalid, uninitialized, null, dangling, or out-of-bounds pointers cause undefined behavior when dereferenced.
  • const makes pointer interfaces safer by separating read-only access from writable access.