Pointers & Arrays

Address-of and dereference, pointer arithmetic, array decay, and strings as char arrays, with a real swap example.

Addresses, and the & operator

Every variable lives somewhere in memory, at a specific address. The & ("address-of") operator gives you that address:

C
int score = 90;
printf("%p\n", (void*)&score); // e.g. 0x7ffee3a1b45c — the address where score lives

Pointers, and the * operator

A pointer is a variable whose value is a memory address — specifically, the address of another variable of a declared type.

C
int score = 90;
int *scorePtr = &score; // scorePtr holds the address of score

printf("%d\n", score);        // 90       — the value itself
printf("%p\n", (void*)&score); // an address — where score lives
printf("%p\n", (void*)scorePtr); // the SAME address — what scorePtr holds

printf("%d\n", *scorePtr); // 90 — *scorePtr means "the value AT this address" (dereferencing)

*scorePtr = 100;           // writes through the pointer
printf("%d\n", score);     // 100 — score itself changed

* is doing two different jobs depending on context, and this is a common source of early confusion:

  • In a declaration (int *scorePtr), * means "this variable is a pointer to int."
  • In an expression (*scorePtr), * means "dereference — give me the value stored at this address."

Pointer arithmetic

Adding 1 to a pointer doesn't add one byte — it advances by sizeof(the pointed-to type), so pointer arithmetic naturally walks element-by-element through an array:

C
int numbers[5] = {10, 20, 30, 40, 50};
int *p = numbers; // an array name decays to a pointer to its first element

printf("%d\n", *p);       // 10
printf("%d\n", *(p + 1)); // 20 — advances by sizeof(int), not by 1 byte
printf("%d\n", *(p + 2)); // 30

for (int i = 0; i < 5; i++) {
    printf("%d ", *(p + i)); // 10 20 30 40 50 — equivalent to p[i]
}

p[i] is actually defined in terms of pointer arithmetic — it's shorthand for *(p + i). This is why array indexing and pointer arithmetic are so closely related in C.

Arrays decay to pointers

When an array is passed to a function (or otherwise used where a pointer is expected), it "decays" into a pointer to its first element — the function has no way to know the original array's length:

C
void printArray(int *arr, int length) { // arr here is really just a pointer
    for (int i = 0; i < length; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void) {
    int nums[5] = {1, 2, 3, 4, 5};
    printArray(nums, 5); // must pass the length separately — arr has forgotten it
    printf("%zu\n", sizeof(nums));           // 20 (5 ints * 4 bytes) — the real array
    // printf("%zu\n", sizeof(arr)) inside printArray would print 8 (a pointer's size), NOT 20
}

This is why every C function that takes an array parameter also needs a separate length parameter — the array itself doesn't carry that information once it has decayed to a pointer.

Strings as char arrays

C has no built-in string type — a string is just an array of char, terminated by a null byte ('\0') marking the end:

C
char greeting[] = "Hello"; // actually 6 bytes: 'H','e','l','l','o','\0'

printf("%s\n", greeting);        // Hello — %s prints until it hits '\0'
printf("%zu\n", strlen(greeting)); // 5 — strlen does NOT count the '\0'
printf("%zu\n", sizeof(greeting)); // 6 — sizeof DOES count the '\0'

Because strings are just char arrays, common string operations are ordinary library functions operating on that array, not built-in language features:

C
#include <string.h>

char dest[20];
strcpy(dest, "Hello");      // copies "Hello\0" into dest
strcat(dest, ", World!");   // appends onto the existing content in dest
printf("%s\n", dest);       // Hello, World!

if (strcmp("abc", "abc") == 0) {
    printf("equal\n");      // strcmp returns 0 when strings are equal (not true/false!)
}

A real example: swap using pointers

Because C passes arguments by value, a function can't modify the caller's variables directly unless it receives their addresses and writes through pointers:

C
#include <stdio.h>

void swapByValue(int a, int b) { // does NOT work — a and b are local copies
    int temp = a;
    a = b;
    b = temp;
}

void swapByPointer(int *a, int *b) { // works — writes through the given addresses
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 1, y = 2;

    swapByValue(x, y);
    printf("%d %d\n", x, y); // 1 2 — unchanged! swapByValue only swapped its own local copies

    swapByPointer(&x, &y);
    printf("%d %d\n", x, y); // 2 1 — actually swapped
    return 0;
}

This pattern — passing a pointer so a function can modify the caller's variable — is how C simulates "pass by reference," which it doesn't have as a built-in language feature the way C++ does.

Common mistakes

  • Forgetting the length of an array is lost once it decays to a pointer — always pass the length alongside a pointer parameter.
  • Off-by-one errors with strcpy/strcat that don't account for the null terminator's extra byte, overflowing the destination buffer.
  • Comparing strings with == (which compares pointer addresses, not content) instead of strcmp.
  • Confusing * in a declaration ("this is a pointer") with * in an expression ("dereference this pointer").

Interview questions

Q: What does it mean that "arrays decay to pointers"? When an array is used in most expressions (passed to a function, assigned to a pointer variable), it's automatically converted to a pointer to its first element — the array's total length information is lost at that point, which is why array-processing functions always need a separate length parameter.

Q: Why doesn't swap(int a, int b) swap the caller's variables, but swap(int *a, int *b) does? C passes arguments by value — swap(int a, int b) receives copies of the caller's values, so any changes inside the function only affect those local copies. Passing pointers (int *a, int *b) instead gives the function the addresses of the caller's variables, so dereferencing and writing through those pointers (*a = ...) modifies the original variables directly.