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
- The program creates an object such as
int score. That object has storage, a type, a value, and a lifetime. - The expression
&scoreproduces an address value whose type isint *. - 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.
- When the program evaluates
*p, it first reads the address stored inp, then accesses the object located there as the pointed-to type. - 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. - For pointer arithmetic such as
p + 1, the compiler scales the movement bysizeof *p. With anint *, the next pointer value is the nextintposition.
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
NULLbefore 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
constfor 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, orscore_ptrinstead of vague names when pointer roles differ.
Practice Exercises
- Write a function
void swap_ints(int *a, int *b)that swaps two integers. Test it frommainby printing the values before and after the call. - 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 whencountis zero. - Create an array of five prices and use two pointers,
currentandend, 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.
constmakes pointer interfaces safer by separating read-only access from writable access.
