Skip to content

Pointers, Arrays, and Memory Layout in C

A pointer is a typed value that can designate an object or function. It is not merely an integer address with nicer syntax: validity depends on lifetime, bounds, alignment, and what kind of object exists in the referenced storage.

Address and dereference are inverse operations within a lifetime

c
#include <stdio.h>

int main(void) {
    int score = 41;
    int *pointer = &score;
    *pointer += 1;
    printf("%d\n", score);
    return 0;
}

&score obtains a pointer to score; *pointer is an lvalue designating that object. The pointer remains usable only while the object lives and the access satisfies type and qualifier rules.

NULL is a null pointer constant convention. C23 adds the nullptr keyword with type nullptr_t, improving some generic and variadic uses. A null pointer designates no object and must not be dereferenced.

Arrays are objects, not pointers

c
int values[4] = {2, 4, 6, 8};

values stores four contiguous int objects. In most expressions, an array expression converts—or “decays”—to a pointer to its first element. Important exceptions include operands of sizeof and unary &.

c
#include <stdio.h>

int main(void) {
    int values[4] = {2, 4, 6, 8};
    printf("%zu\n", sizeof values / sizeof values[0]);
    return 0;
}

After an array is passed to a function, the parameter receives a pointer, so sizeof there measures the pointer rather than recovering the array length. Pass the count separately:

c
long total(const int *values, size_t count);

The parameter spelling const int values[] means the same pointer type in a function declaration; brackets document intent but do not carry length.

Pointer arithmetic is bounded by an array object

For a pointer into an array, adding one advances by one element, not one byte. A pointer may point one past the last element for comparison and loop termination, but that one-past pointer cannot be dereferenced.

c
for (const int *it = values; it != values + count; ++it) {
    consume(*it);
}

Creating or using pointer arithmetic unrelated to a common array object is not a portable substitute for integer address arithmetic.

Multidimensional arrays preserve inner extents

c
int grid[3][4];

This is an array of three arrays, each containing four int values. When passed to a function, the outer extent can decay, but the compiler needs the inner extent to compute rows:

c
void clear_grid(size_t rows, int grid[rows][4]);

This C99 variably modified parameter syntax may be unavailable in implementations without VLA support. Because only the outer row count varies here, the same adjusted pointer type can be written without VLA syntax:

c
void clear_grid(size_t rows, int (*grid)[4]);

Actual VLA object support became optional in later standards and toolchain support differs. Flat buffers plus explicit dimensions are often easier at API boundaries.

Structure layout includes alignment and padding

c
struct Record {
    char tag;
    int value;
};

The implementation may insert padding between members and after the last member so each member and array element meets alignment requirements. Do not serialize a struct by writing its raw bytes unless the format deliberately specifies and verifies layout, padding, byte order, and representation.

offsetof reports member offsets, _Alignof in C11 reports alignment requirements, and sizeof includes padding. Reordering fields can change size, but optimize layout only after correctness and measurement.

Bytes may inspect representations under special rules

Character types can inspect an object's representation byte by byte. memcpy is the ordinary way to transfer representations between suitably sized storage without violating typed-access rules.

Casting arbitrary storage to an unrelated pointer type and dereferencing can violate alignment and effective-type/aliasing rules. These rules allow optimizers to assume that incompatible typed pointers do not unexpectedly refer to the same object.

When parsing a wire format, copy bytes into a correctly aligned object and handle byte order explicitly. Do not overlay a struct and hope the host layout matches the protocol.

Lifetime is distinct from storage contents

A pointer to a local object becomes dangling when its block ends:

c
/* invalid design */
int *make_score(void) {
    int score = 42;
    return &score;
}

The bytes may appear unchanged during a test, but the object's lifetime has ended. Similarly, after free(pointer), the allocation's storage is no longer yours to access even if the numeric pointer value remains.

Optional prompts

Explain: Why can a function not use sizeof(parameter) / sizeof(parameter[0]) to recover a caller's array length?

Answer: An array parameter is adjusted to a pointer type. The function receives only the first-element pointer; the array extent is not carried, so it must be passed or encoded separately.

Predict: Is a pointer one past an array valid?

Answer: It is valid for limited arithmetic and comparison as an end sentinel, but dereferencing it is invalid.

Further reference