C Pointers: Addresses, Dereferencing, Arithmetic, and the Road to Objective-C Objects
Master C pointer fundamentals — address-of, dereference, pointer types, pointer arithmetic, NULL, void pointers, pointers to pointers, const correctness, and function pointers — and understand precisely why this material is a mandatory prerequisite for Objective-C, where every object variable is itself a pointer.
Pointers are the single most important idea in C, and understanding them thoroughly is not optional groundwork for Objective-C — it is the direct foundation of it. Every Objective-C object you will ever create, message, or pass around is accessed exclusively through a pointer, no exceptions. Person *person = [[Person alloc] init]; is, at the machine level, doing exactly the same kind of thing as int *ptr = &value; — holding the address of something that lives elsewhere in memory, rather than holding the thing itself. If pointers feel shaky, Objective-C object syntax will feel arbitrary and confusing; if pointers are solid, Objective-C's pointer-heavy object model will feel like a natural, almost inevitable extension of what you already know. This lesson goes deep, deliberately, because the spec for this course treats it as mandatory.
Address-Of: Every Variable Lives Somewhere
Every variable in a running C program occupies a specific location in memory, identified by a numeric address — you can think of memory as one enormous array of bytes, and a variable's address is simply its index into that array. The & operator, called 'address-of', retrieves that address: given int value = 42;, the expression &value evaluates to the memory address where value's bytes are stored, not to the value 42 itself.
This address is a real, printable, comparable number — commonly shown in hexadecimal, something like 0x7ffee2a1c05c. Different runs of the same program will typically produce different addresses (modern operating systems randomize stack and heap layout for security, a feature called ASLR — Address Space Layout Randomization), but within a single run, &value always refers to the same fixed location for as long as value exists.
Nearly everything else in this lesson builds on this one primitive: a pointer is nothing more than a variable whose job is to store one of these addresses.
• Every variable has a memory address — a numeric location where its bytes live. • &value — the address-of operator, retrieves that location. • Addresses are typically printed in hexadecimal and vary between program runs (ASLR). • A pointer is simply a variable that stores an address.
Pointer Declaration, Types, and Dereferencing
A pointer variable is declared with the base type followed by an asterisk: int *ptr; declares ptr as 'a pointer to an int' — meaning ptr will hold the address of some int somewhere in memory, not an int itself. Assigning a pointer to point at a real variable uses &: int value = 42; int *ptr = &value; makes ptr hold value's address.
The * operator, used this way in an expression rather than a declaration, means 'dereference' — it means 'go to the address this pointer holds, and give me the value stored there'. So *ptr reads the int that ptr points at, which is 42. Dereferencing can also be used to write: *ptr = 100; does not change what ptr points at, it changes the value at the address ptr already points to — after this line, value itself is now 100, because ptr and value refer to the exact same memory location, just accessed two different ways.
A pointer's declared type matters enormously, even though the address it stores is 'just a number' underneath. The type tells the compiler two critical things: how many bytes to read or write when the pointer is dereferenced (an int * reads/writes 4 bytes at that address; a double * reads/writes 8), and how far to move for pointer arithmetic (covered next). int * and double * are incompatible types precisely because their pointed-to sizes and interpretations differ — you cannot dereference an int * and expect a correct double.
• int *ptr; — declares ptr as a pointer to an int (the * here is part of the declaration). • int *ptr = &value; — ptr now holds value's address. • *ptr (in an expression) dereferences ptr: reads or writes the value at the address it holds. • A pointer's declared type determines how many bytes are read/written on dereference and how arithmetic on it behaves.
The Address-Value-Pointer Relationship
It helps enormously to picture this as two separate boxes in memory: one box holds value's actual data (42), and a second, entirely separate box holds ptr's data — which happens to be the address of the first box. *ptr is the operation of following the arrow from the second box to the first box and reading what's there. This 'one level of indirection' is the entire idea, and everything more advanced (pointers to pointers, Objective-C object references) is just this same picture repeated or extended.
• A pointer variable itself also has its own address, distinct from the address it stores. • Dereferencing follows the stored address to reach the pointed-to data — 'one level of indirection'. • This exact picture — a box holding an address that points at another box — is precisely how an Objective-C object reference works.
Pointer Arithmetic
Pointers support a restricted form of arithmetic that is defined relative to the pointer's pointed-to type, not in raw bytes. Given int *ptr pointing at the first element of an array, ptr + 1 does not add 1 byte to the address — it adds sizeof(int) bytes (typically 4), so that it correctly points at the *next* int in the array. This is precisely why array indexing and pointer arithmetic are so closely related: arr[i] is defined by the C standard to mean exactly *(arr + i) — subscript notation is literally syntactic sugar for pointer arithmetic plus a dereference.
Incrementing a pointer (ptr++) moves it forward by one element of its pointed-to type; ptr-- moves it back by one. Subtracting two pointers of the same type that point into the same array (ptr2 - ptr1) gives the number of elements between them, not the number of raw bytes. Pointer arithmetic is only well-defined within (or one-past-the-end of) a single array — walking a pointer past those bounds, or performing arithmetic between pointers into unrelated arrays, is undefined behavior, exactly the kind of hazard the previous lesson's bounds-checking discussion warned about.
This relationship between arrays and pointer arithmetic is also why an array parameter decays to a pointer so naturally: once you understand that arr[i] and *(arr + i) are the same operation, decay stops looking like a special case and starts looking like the obvious consequence of how array indexing was defined in the first place.
• ptr + 1 advances by sizeof(*ptr) bytes, not 1 byte — arithmetic is scaled by the pointed-to type. • arr[i] is defined as *(arr + i) — subscripting IS pointer arithmetic plus dereference. • ptr2 - ptr1 (same array) gives the element count between them, not a byte count. • Pointer arithmetic outside a single array's bounds (or one-past-the-end) is undefined behavior.
NULL and void *
NULL is a special pointer value meaning 'this pointer intentionally points at nothing'. It is conventionally defined as ((void *)0) and is used to initialize a pointer that doesn't yet have a valid target, or to signal failure from a function that returns a pointer (malloc returns NULL if it cannot allocate memory, for instance). Critically, NULL is a valid value to store in a pointer, but it is never valid to dereference — int *ptr = NULL; *ptr = 5; is one of the single most common crashes in C and Objective-C programming (in Objective-C specifically, sending a message to nil, the object-world equivalent of NULL, is actually safe and simply does nothing — a deliberate, well-known difference worth remembering once you get there).
void * is a special 'generic' pointer type that can hold the address of any type of data, but — precisely because it doesn't know what type of data it points to — it cannot be dereferenced directly and does not support pointer arithmetic (there is no defined 'size' to scale by). void * is used when a function genuinely needs to work with memory regardless of what's stored in it: malloc() returns void * because it has no idea what type you intend to use the allocated memory for, and it must be cast to a concrete pointer type (int *ptr = (int *)malloc(sizeof(int));, or, in C, the cast is technically optional but conventionally included for clarity) before it can be dereferenced meaningfully.
Always check a pointer against NULL before dereferencing it whenever there's any chance it might not point at valid memory — after a malloc call, after a function that can fail and return NULL as a signal, or any pointer that was only conditionally assigned.
• NULL — a pointer value meaning 'points at nothing'; safe to store, never safe to dereference. • Dereferencing a NULL pointer is one of the most common crashes in C (a segmentation fault). • void * — a generic pointer that can hold any address but cannot be dereferenced or arithmetic'd directly without a cast. • malloc() returns void *, cast to the intended concrete pointer type before dereferencing. • Always NULL-check a pointer before dereferencing it if there's any chance it wasn't successfully assigned.
Pointer to Pointer
A pointer can itself point at another pointer — a 'pointer to pointer', declared with two asterisks: int **pptr;. If int value = 42; int *ptr = &value;, then int **pptr = &ptr; makes pptr hold the address of ptr itself (not of value). Dereferencing once, *pptr, gives you ptr (the address of value); dereferencing twice, **pptr, gives you value itself (42) — each * removes one level of indirection.
This pattern is genuinely common, not just an academic curiosity: it's how a function can modify a caller's pointer variable itself (not just the data it points to) — for example, a function that allocates memory and needs to hand the new pointer back to the caller through an out-parameter takes an int ** so it can write *outParam = newlyAllocatedPointer;. It's also how multidimensional dynamic arrays and argv-style arrays of strings (char **argv in main) are represented — an array of pointers, each pointing to its own string.
• int **pptr; — a pointer to a pointer to an int; two levels of indirection. • *pptr — one dereference, gives you back the inner pointer (ptr). • pptr — two dereferences, gives you back the original value. • Common uses: letting a function modify the caller's own pointer variable, and arrays of pointers like char argv.
const Pointer vs Pointer to const
const and pointers combine in ways that trip up nearly everyone at first, because where the const keyword sits changes its meaning entirely. const int *ptr (equivalently int const *ptr) is a 'pointer to a const int' — the *pointed-to data* cannot be modified through this pointer (*ptr = 5; is a compile error), but the pointer itself can be reassigned to point somewhere else (ptr = &otherValue; is fine). Read it right-to-left from the *: 'ptr is a pointer to a const int'.
int *const ptr is a 'const pointer to an int' — the reverse: the pointer variable itself cannot be reassigned once initialized (ptr = &otherValue; is a compile error), but the data it points to CAN be modified through it (*ptr = 5; is fine). And const int *const ptr combines both restrictions: neither the pointer nor the pointed-to data can change through it.
This exact distinction — 'can I change what this points to' versus 'can I change the pointer itself' — is precisely mirrored, conceptually, by how Objective-C strings work: a const char * pointing at a string literal is analogous to holding an immutable NSString *, where the object's contents can't be mutated through that reference, while reassigning the pointer variable to point at a different string entirely is always fine either way.
• const int *ptr — pointer to const data: can't modify *ptr, CAN reassign ptr itself. • int *const ptr — const pointer: CAN modify *ptr, can't reassign ptr itself once initialized. • const int *const ptr — both restrictions apply at once. • Read the declaration right-to-left starting from the pointer name to parse which part is const.
Function Pointers
Functions themselves live at addresses in memory too, and C allows you to take a pointer to a function, store it in a variable, pass it as an argument, and call through it. The declaration syntax is admittedly awkward: int (*operation)(int, int); declares operation as a pointer to a function that takes two ints and returns an int — the parentheses around *operation are required, because without them int *operation(int, int) would instead declare a function named operation that returns an int *.
Assigning a function pointer is as simple as using the function's name (a function name, much like an array name, evaluates to its address in most contexts): int add(int a, int b) { return a + b; } int (*operation)(int, int) = add;. Calling through it looks just like an ordinary call: int result = operation(3, 4); (or, equivalently and more explicitly, (*operation)(3, 4) — both forms are allowed and identical).
Function pointers are the mechanism behind callbacks, sortable comparison functions (qsort's comparator argument is a function pointer), and dispatch tables. This concept, too, has a direct Objective-C echo: under the hood, sending a message to an Objective-C object is ultimately resolved to a function pointer call through the runtime's method dispatch machinery — the same fundamental idea of 'a variable that holds the address of executable code, to be called later,' just wrapped in a much richer, dynamic messaging system.
• int (*operation)(int, int); — a pointer to a function taking two ints and returning an int. • A function's name, in most expressions, evaluates to its address — assign it directly to a matching function pointer. • Call through a function pointer with ordinary call syntax: operation(3, 4);. • Used for callbacks (qsort comparators, event handlers) and dispatch tables; conceptually related to how Objective-C's message dispatch works under the hood.
Bridge to Objective-C: Every Object Is a Pointer
Here is the payoff for everything in this lesson: in Objective-C, Person *person = [[Person alloc] init]; is not a special new kind of syntax to memorize separately — it is exactly the same pattern as int *ptr = &value;, just with a class type instead of int. [[Person alloc] init] allocates a Person object somewhere on the heap and returns the address of that memory; person is a variable that stores that address, i.e. a pointer to a Person. The asterisk in Person *person means exactly what it meant in int *ptr: 'person is a pointer to a Person', not a Person value itself.
This is why you will never see Person person; in correct Objective-C code the way you might see int value; — Objective-C objects are always, without exception, accessed through pointers, because they always live on the heap and are managed by reference. Sending a message, [person setName:@"Ada"], dereferences that pointer under the hood to reach the actual object data and invoke behavior on it, conceptually similar to how *ptr = 100; reaches through a plain C pointer to modify the int it points at.
Every C pointer concept from this lesson carries forward directly: nil (Objective-C's NULL-equivalent for objects) must never be blindly dereferenced-by-message in the sense of relying on undefined results, even though in practice Objective-C makes messaging nil safe by defining it as a no-op — a deliberate design choice built on top of, not instead of, the plain-C pointer model you've just learned. Pointer arithmetic, address-of, and dereferencing are the mental toolkit the rest of this Objective-C course assumes you already have — the next module builds directly on top of this one.
• Person *person = [[Person alloc] init]; — person is a pointer to a Person object on the heap, exactly like int *ptr is a pointer to an int. • The * in Person *person means the same thing it meant for int *ptr — 'this variable holds an address, not the data itself'. • Objective-C objects always live on the heap and are always accessed through pointers — you will never declare a bare Person person;. • nil is Objective-C's NULL-equivalent for objects, but messaging nil is deliberately made safe (a no-op) — a design choice layered on top of the plain-C pointer model, not a replacement for understanding it.
Pointer Declaration and Usage Forms
Read const int *p right-to-left from p: 'p is a pointer to a const int'. A function pointer's declaration parentheses around *name are required, not optional.
int value = 42;
int *ptr = &value; // ptr holds the address of value
printf("%d\n", *ptr); // dereference: prints 42
*ptr = 100; // writes through ptr: value is now 100
int **pptr = &ptr; // pointer to a pointer
printf("%d\n", **pptr); // 100, two dereferences
int *nothing = NULL; // valid to store, never valid to dereference
void *generic = &value; // can hold any address, needs a cast to dereference
int *back = (int *)generic;
const int *toConst = &value; // can't modify *toConst; CAN reassign toConst
int *const constPtr = &value; // can modify *constPtr; can't reassign constPtr
int add(int a, int b) { return a + b; }
int (*operation)(int, int) = add; // function pointer
int result = operation(3, 4); // 7Code Examples
&value and ptr print the same address, proving ptr genuinely holds value's location, not a copy of value itself. Writing through *ptr = 99 changes value directly, demonstrating that ptr and value refer to the exact same storage, just accessed two different ways. Expected result: value = 42 &value = 0x7ffee2a1c05c ptr = 0x7ffee2a1c05c *ptr = 42 after *ptr = 99, value = 99 scores[i] and *(p + i) print identical values at every index because they are, by the C standard's own definition, the same operation — arr[i] is defined as *(arr + i), and pointer arithmetic automatically scales by sizeof(int) so p + i lands exactly on the i-th element. Expected result: scores[0] = 10, *(p + 0) = 10 scores[1] = 20, *(p + 1) = 20 scores[2] = 30, *(p + 2) = 30 scores[3] = 40, *(p + 3) = 40 allocateAndFill needs to modify the CALLER's pointer variable itself (result), not just some data it points to — so it must receive a pointer to that pointer (int **outPtr) and write through it with *outPtr =..., which is exactly why pointer-to-pointer parameters exist. Expected result: result = 77
#include <stdio.h>
int main(void) {
int value = 42;
int *ptr = &value;
printf("value = %d\n", value);
printf("&value = %p\n", (void *)&value);
printf("ptr = %p\n", (void *)ptr);
printf("*ptr = %d\n", *ptr);
*ptr = 99;
printf("after *ptr = 99, value = %d\n", value);
return 0;
}#include <stdio.h>
int main(void) {
int scores[4] = {10, 20, 30, 40};
int *p = scores; // array decays to a pointer to its first element
for (int i = 0; i < 4; i++) {
printf("scores[%d] = %d, *(p + %d) = %d\n", i, scores[i], i, *(p + i));
}
return 0;
}#include <stdio.h>
#include <stdlib.h>
void allocateAndFill(int **outPtr, int value) {
*outPtr = malloc(sizeof(int)); // allocate, then write the caller's pointer through outPtr
if (*outPtr != NULL) {
**outPtr = value;
}
}
int main(void) {
int *result = NULL;
allocateAndFill(&result, 77);
if (result != NULL) {
printf("result = %d\n", *result);
free(result);
}
return 0;
}A Pointer Pointing at an int
ptr does not hold 42 — it holds the ADDRESS of the box that holds 42. This exact shape, box-holding-an-address-of-another-box, is precisely how Person *person points at a Person object on the heap.
int value = 42
Memory
┌─────────────┐
│ 42 │
└─────────────┘
↑
│ address
┌─────────────┐
│ int *ptr │
└─────────────┘
int value = 42;
int *ptr = &value;
printf("%d\n", *ptr); // 42 — follow the arrow, read what's thereImportant Rules
Never dereference a pointer that might be NULL without checking first.
Always initialize a pointer (to a valid address or to NULL) — an uninitialized pointer holds garbage and dereferencing it is undefined behavior.
A void * must be cast to a concrete type before it can be dereferenced or used in arithmetic.
Read const-pointer declarations right-to-left from the variable name to correctly identify what is actually const.
Pointer arithmetic is only well-defined within a single array (or one past its end) — never between unrelated allocations.
Pointers Underneath Every Objective-C Object
Every single Objective-C object reference you will write in this course — NSString *name, UIView *view, Person *person — is, at the machine level, nothing more than a variable holding a heap address, exactly like the plain C pointers in this lesson. The Objective-C runtime adds retain/release memory management, dynamic method dispatch, and message sending on top of this foundation, but the foundation itself never changes: an object variable is a pointer, full stop, which is precisely why this lesson was flagged as mandatory before moving on.
int *ptr; *ptr = 5; // ptr was never initialized — dereferencing garbage int value; int *ptr = &value; *ptr = 5;
An uninitialized pointer holds an indeterminate garbage address, not NULL and not a valid location — dereferencing it is undefined behavior and can crash or, worse, silently corrupt unrelated memory.
int *ptr = NULL; printf("%d\n", *ptr); // dereferencing NULL int *ptr = NULL; if (ptr != NULL) { printf("%d\n", *ptr); } else { printf("ptr has no target\n"); }
NULL is a valid value to store in a pointer specifically to mean 'points at nothing' — dereferencing it anyway is one of the most common crashes in C, always a segmentation fault on a modern OS. Always check before dereferencing.
int *getDangling(void) { int local = 42; return &local; // address of a local that is about to be destroyed } int *getValid(void) { static int persisted = 42; // or heap-allocate with malloc return &persisted; } local lives on the stack frame of getDangling(), which is popped the instant the function returns; the returned address is now dangling — it points at memory that no longer belongs to anything and may be overwritten by the very next function call.
int value = 42; int ptr = value; // forgot & — ptr is not actually a pointer to value int value = 42; int *ptr = &value;
Forgetting the & means ptr (if declared as int, a type mismatch anyway) ends up holding a copy of value's contents rather than its address — a genuine pointer must be assigned an address with &, not the plain variable.
What does the expression *ptr mean when ptr is a pointer variable? A. Multiply ptr by some implicit value B. Dereference ptr: access the value stored at the address ptr holds C. Declare a new pointer named ptr D. Get the address of ptr itself
B. Dereference ptr: access the value stored at the address ptr holds — In an expression context, * is the dereference operator: it follows the address stored in ptr and reads (or, on the left of an assignment, writes) the value found there.
Given int *p pointing into an int array, what does p + 1 actually do to the address? A. Adds exactly 1 byte to the address B. Adds sizeof(int) bytes, moving to the next int element C. Has no defined meaning in C D. Adds 1 to the value being pointed to, not the address
B. Adds sizeof(int) bytes, moving to the next int element — Pointer arithmetic is scaled by the pointed-to type's size — for an int * that's typically 4 bytes — which is exactly why arr[i] is defined as *(arr + i).
What is the practical difference between dereferencing NULL and messaging nil in Objective-C? A. There is no difference — both always crash B. Dereferencing a NULL C pointer is undefined behavior (typically a crash), while sending a message to nil in Objective-C is deliberately defined as a safe no-op C. NULL and nil are the exact same keyword in both languages D. nil can be dereferenced safely in C too
B. Dereferencing a NULL C pointer is undefined behavior (typically a crash), while sending a message to nil in Objective-C is deliberately defined as a safe no-op — Plain C offers no safety net for dereferencing NULL — it is undefined behavior, usually a segfault. Objective-C deliberately layers a safety rule on top of the same underlying pointer concept: sending a message to nil simply does nothing rather than crashing.
Which declaration describes a pointer to a const int (data can't change through it, but the pointer itself can be reassigned)? A. int *const ptr B. const int *ptr C. const int *const ptr D. int **ptr
B. const int *ptr — const int *ptr (or int const *ptr) is a pointer to const data: you cannot modify *ptr, but you can reassign ptr to point elsewhere. int *const ptr is the reverse — a const pointer whose target data is mutable but whose own address binding is fixed.
Why is Person *person the correct way to declare an Objective-C object variable, rather than Person person? A. It is just a stylistic convention with no technical meaning B. Objective-C objects always live on the heap and are always accessed through pointers, exactly like int *ptr points to an int elsewhere in memory C. Person person would be faster but is disallowed for security reasons D. The asterisk is required only for class names that start with a capital letter
B. Objective-C objects always live on the heap and are always accessed through pointers, exactly like int *ptr points to an int elsewhere in memory — Every Objective-C object is allocated on the heap and referenced through a pointer — person holds the object's address, not the object's data directly, which is precisely the same relationship as int *ptr = &value; holding an int's address rather than the int itself.
Write a C function void swap(int *a, int *b) that swaps the values pointed to by a and b using a temporary variable and pointer dereferencing (not array tricks). Call it from main() on two int variables and print both before and after to confirm they were swapped.
Inside swap, dereference both pointers to read and write the underlying values: int temp = *a; *a = *b; *b = temp;. Pass addresses with & when calling.