Chapter 5: Pointers: The Heart of C
Welcome to the most distinctive and powerful feature of C: pointers. While intimidating for beginners, mastering pointers is fundamental to truly understanding C and low-level programming. Pointers allow you to directly interact with memory addresses, enabling advanced memory management, efficient data manipulation, and direct hardware interaction.
In this chapter, we will demystify pointers by exploring:
- What memory addresses are and how variables are stored.
- How to declare, initialize, and use pointers.
- The concepts of dereferencing and indirection.
- Pointer arithmetic and its applications.
- How pointers enable “pass-by-reference” in functions.
- Special types of pointers like
NULLandvoidpointers.
Let’s confront the “dreaded” pointer head-on!
5.1 Memory Addresses and Variables
Every byte in your computer’s RAM (Random Access Memory) has a unique numerical address. When you declare a variable, the compiler reserves a chunk of memory for that variable, and that chunk has a starting address.
Consider a variable int num = 10;.
numis the name of the variable.10is the value stored in the memory location.- The variable
numalso has a memory address where its value is stored.
In C, we use the address-of operator (&) to get the memory address of a variable.
Example:
#include <stdio.h>
int main() {
int num = 10;
float pi = 3.14f;
char grade = 'A';
printf("Value of num: %d\n", num);
printf("Address of num: %p\n", &num); // %p is the format specifier for printing addresses
printf("\nValue of pi: %.2f\n", pi);
printf("Address of pi: %p\n", &pi);
printf("\nValue of grade: %c\n", grade);
printf("Address of grade: %p\n", &grade);
return 0;
}
When you run this, you’ll see hexadecimal numbers (e.g., 0x7ffee5134c6c). These are the memory addresses. They will likely be different every time you run the program and across different systems.
5.2 What is a Pointer?
A pointer is a variable whose value is the memory address of another variable. Instead of holding actual data (like an int or float), it holds the location where that data is stored.
Think of it like this: A regular variable is a box holding a value. A pointer is a box holding a sticky note with the address of another box.
5.2.1 Declaring Pointers
To declare a pointer, you use the asterisk (*) operator between the data type and the pointer variable name. This asterisk signifies that you are declaring a pointer to a variable of that type.
Syntax:
dataType *pointerName;
dataType: The type of data the pointer will point to. This is crucial, as the compiler needs to know how many bytes to interpret starting from the address the pointer holds.*: The dereference operator (used in declaration, and later for dereferencing).pointerName: The name of your pointer variable.
Examples:
int *ptr_to_int; // A pointer to an integer
float *ptr_to_float; // A pointer to a float
char *ptr_to_char; // A pointer to a character
double *ptr_to_double; // A pointer to a double
5.2.2 Initializing Pointers
After declaring a pointer, it must be initialized. You typically initialize a pointer with the address of an existing variable of the matching type.
dataType var;
dataType *ptr_var = &var; // ptr_var now holds the address of 'var'
If you don’t initialize a pointer, it will contain a “garbage” address, potentially pointing to an invalid or unknown memory location. This is known as a wild pointer and can lead to dangerous crashes or unpredictable behavior. Always initialize pointers, even if it’s to NULL.
5.2.3 The NULL Pointer
The NULL macro (defined in <stddef.h>, <stdio.h>, <stdlib.h>) is a special value that a pointer can hold, indicating that it does not point to any valid memory location. It’s good practice to initialize pointers to NULL if they aren’t immediately assigned a valid address.
C23 nullptr: C23 introduces nullptr as a keyword for a null pointer constant and nullptr_t as its type, similar to C++. This provides a more type-safe way to represent null pointers than NULL.
Example:
int *ptr_empty = NULL; // A pointer that doesn't point anywhere (yet)
// In C23:
// int *ptr_empty_c23 = nullptr;
You can check if a pointer is NULL using if (ptr == NULL) or if (!ptr).
5.3 Dereferencing Pointers
Once a pointer holds a memory address, you can access the value stored at that address using the dereference operator (*). This is often called indirection.
Syntax:
*pointerName
This expression refers to the value at the address stored in pointerName.
Example:
#include <stdio.h>
int main() {
int value = 42;
int *ptr_value; // Declare a pointer to an int
ptr_value = &value; // Initialize the pointer with the address of 'value'
printf("Value of 'value': %d\n", value); // Output: 42
printf("Address of 'value': %p\n", &value); // Output: (some address)
printf("Value of 'ptr_value' (the address it holds): %p\n", ptr_value); // Same address as &value
printf("Value at the address 'ptr_value' points to: %d\n", *ptr_value); // Output: 42 (dereferencing)
// You can also change the value using the dereference operator:
*ptr_value = 100; // Changes the value of 'value' to 100
printf("\nAfter changing value via pointer:\n");
printf("New value of 'value': %d\n", value); // Output: 100
printf("New value at the address 'ptr_value' points to: %d\n", *ptr_value); // Output: 100
return 0;
}
Notice how & gives you the address, and * (when used on a pointer variable) gives you the value at that address. They are inverse operations.
5.4 Pointer Arithmetic
Pointers can be used in arithmetic expressions, but only certain operations are valid and make sense. Pointer arithmetic works based on the size of the data type the pointer points to.
When you increment a pointer (e.g., ptr++), it doesn’t just add 1 to the memory address. Instead, it adds the size of the data type it points to. This makes it easy to move between elements in an array.
Valid operations:
++(increment): Moves the pointer to point to the next element of its type.--(decrement): Moves the pointer to point to the previous element of its type.+(addition with an integer):ptr + nmoves the pointernelements forward.-(subtraction with an integer):ptr - nmoves the pointernelements backward.-(subtraction of two pointers): If two pointers point to elements within the same array, their difference gives the number of elements between them. The result type isptrdiff_t(defined in<stddef.h>).
Example:
#include <stdio.h>
int main() {
int numbers[] = {10, 20, 30, 40, 50}; // An array of 5 integers
int *ptr = numbers; // ptr points to the first element (numbers[0])
printf("Initial ptr points to: %d (address: %p)\n", *ptr, ptr);
ptr++; // Increment ptr - moves to the next int (numbers[1])
printf("After ptr++: %d (address: %p)\n", *ptr, ptr); // Output: 20
ptr += 2; // Add 2 to ptr - moves 2 ints forward (numbers[3])
printf("After ptr += 2: %d (address: %p)\n", *ptr, ptr); // Output: 40
ptr--; // Decrement ptr - moves to the previous int (numbers[2])
printf("After ptr--: %d (address: %p)\n", *ptr, ptr); // Output: 30
// Accessing elements using pointer arithmetic
printf("numbers[0] using pointer arithmetic: %d\n", *(ptr - 2)); // Go back 2 elements
printf("numbers[4] using pointer arithmetic: %d\n", *(ptr + 2)); // Go forward 2 elements
// Pointer subtraction (difference between two pointers)
int *ptr1 = &numbers[0]; // Points to 10
int *ptr2 = &numbers[3]; // Points to 40
ptrdiff_t diff = ptr2 - ptr1;
printf("Difference between ptr2 and ptr1: %td elements\n", diff); // Output: 3
return 0;
}
Important: Pointer arithmetic is only reliably defined for pointers that point to elements within the same array or just past the end of an array. Performing arithmetic on pointers to unrelated memory locations or standalone variables leads to undefined behavior.
5.5 Pointers and Functions: Pass By Reference
As you saw in Chapter 4, C functions typically pass arguments by value. This means a copy of the argument is made, and the original variable in the caller remains unchanged.
However, sometimes you need a function to modify a variable in the calling function. This is achieved using pass-by-reference with pointers. Instead of passing the variable’s value, you pass its address. The function then uses this address (the pointer) to directly access and modify the original variable.
Example:
#include <stdio.h>
// Function to swap two integer values using pointers
void swap(int *a, int *b) { // Parameters are pointers to integers
printf(" Inside swap: Before swap, *a = %d, *b = %d\n", *a, *b);
int temp = *a; // temp gets the value at the address 'a' points to
*a = *b; // The value at address 'a' points to is changed to value at 'b'
*b = temp; // The value at address 'b' points to is changed to temp
printf(" Inside swap: After swap, *a = %d, *b = %d\n", *a, *b);
}
int main() {
int x = 10;
int y = 20;
printf("In main: Before swap, x = %d, y = %d\n", x, y); // Output: x=10, y=20
// Call swap, passing the ADDRESSES of x and y
swap(&x, &y);
printf("In main: After swap, x = %d, y = %d\n", x, y); // Output: x=20, y=10
return 0;
}
Explanation:
- In
main,xandyhold values 10 and 20. swap(&x, &y)passes the addresses ofxandyto theswapfunction.- Inside
swap,areceives the address ofx, andbreceives the address ofy. *arefers to the value ofx, and*brefers to the value ofy.- By dereferencing and assigning to
*aand*b, theswapfunction directly modifies the originalxandyvariables inmain.
This is a critically important use of pointers for modifying multiple values from a function, or for returning more data than a single return statement allows.
5.6 void Pointers (Generic Pointers)
A void pointer (declared as void *) is a generic pointer that can hold the address of any data type. It’s often called a typeless pointer.
Characteristics:
- It does not “know” what type of data it points to, so you cannot dereference a
void *directly. - To dereference a
void *, you must first type cast it to a specific data type pointer. - Pointer arithmetic is not allowed directly on
void *because the compiler doesn’t know the size of the data type to increment/decrement by. You must cast it first.
void pointers are useful when writing generic functions that operate on different data types, like memory management functions (malloc, free) or sorting algorithms.
Example:
#include <stdio.h>
int main() {
int i = 10;
char c = 'A';
float f = 3.14f;
void *ptr_generic; // Declare a void pointer
ptr_generic = &i; // ptr_generic now holds the address of 'i'
// printf("%d\n", *ptr_generic); // ERROR: cannot dereference void* directly!
printf("Integer value via void pointer: %d\n", *(int*)ptr_generic); // Cast to int* then dereference
ptr_generic = &c; // ptr_generic now holds the address of 'c'
printf("Character value via void pointer: %c\n", *(char*)ptr_generic); // Cast to char* then dereference
ptr_generic = &f; // ptr_generic now holds the address of 'f'
printf("Float value via void pointer: %.2f\n", *(float*)ptr_generic); // Cast to float* then dereference
// Pointer arithmetic on void* is not allowed directly
// ptr_generic++; // ERROR
// (char*)ptr_generic++; // This would work by casting first
return 0;
}
5.7 Pointers to Pointers (Double Pointers)
A pointer can point to another pointer. This creates a “pointer to a pointer” or a double pointer. You use two asterisks (**) to declare it.
Syntax:
dataType **ptr_to_ptr_name;
This is often used in situations where you need to modify a pointer itself from a function (e.g., in functions that allocate memory dynamically). We’ll see practical examples later.
Example:
#include <stdio.h>
int main() {
int val = 100;
int *ptr_val = &val; // Pointer to an int
int **ptr_to_ptr_val = &ptr_val; // Pointer to an int pointer
printf("Value of val: %d\n", val); // 100
printf("Address of val: %p\n", &val); // 0x...A
printf("Value of ptr_val: %p\n", ptr_val); // 0x...A (address of val)
printf("Address of ptr_val: %p\n", &ptr_val); // 0x...B
printf("Value of ptr_to_ptr_val: %p\n", ptr_to_ptr_val); // 0x...B (address of ptr_val)
// Dereferencing a double pointer:
printf("Value at *ptr_to_ptr_val: %p\n", *ptr_to_ptr_val); // Dereferences to ptr_val (0x...A)
printf("Value at **ptr_to_ptr_val: %d\n", **ptr_to_ptr_val); // Dereferences to val (100)
return 0;
}
Exercise 5.1: Pointer Basics
Write a C program that:
- Declares an
intvariablemy_numberand initializes it to25. - Declares an
intpointerptr_my_number. - Assigns the address of
my_numbertoptr_my_number. - Prints the value of
my_number. - Prints the address of
my_number. - Prints the value stored in
ptr_my_number(which should be the address ofmy_number). - Prints the value that
ptr_my_numberpoints to (using dereferencing). - Uses the pointer to change the value of
my_numberto50. - Prints the new value of
my_numberand the valueptr_my_numberpoints to, to confirm the change.
Exercise 5.2: Simple Calculator with Pointers (Mini-Challenge)
Create a function void perform_operation(int *num1, int *num2, char operator) that takes two pointers to integers and a character representing an arithmetic operator (+, -, *, /).
The function should:
- Dereference the pointers to get the actual values.
- Perform the specified operation.
- Crucially: Instead of returning the result, store the result back into the memory location pointed to by
num1. - Handle division by zero (print an error and do not modify
num1).
In your main function:
- Declare two integer variables, e.g.,
a = 10,b = 5. - Call
perform_operationwith the addresses ofaandband an operator. - After the function call, print the value of
ato see the result.
Example:
int a = 10, b = 5;
perform_operation(&a, &b, '+'); // a should become 15
printf("a = %d\n", a);
a = 10; b = 5;
perform_operation(&a, &b, '*'); // a should become 50
printf("a = %d\n", a);
a = 10; b = 0;
perform_operation(&a, &b, '/'); // Should print error, a remains 10
printf("a = %d\n", a);
This chapter introduced you to the core concept of pointers, their declaration, initialization, dereferencing, and fundamental use cases like pass-by-reference. This knowledge is paramount for understanding how C manages memory and will be foundational for the next chapters on arrays, strings, and dynamic memory allocation. You’ve taken a significant step into true low-level programming!