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:
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.
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 toint." - 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:
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:
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:
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:
#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:
#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/strcatthat don't account for the null terminator's extra byte, overflowing the destination buffer. - Comparing strings with
==(which compares pointer addresses, not content) instead ofstrcmp. - 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.