Introduction to Pointers in C
Pointer Definition
A pointer has two core concepts:
- A pointer is the address that uniquely identifies the smallest addressable unit in system memory
- In common programming terminology, the word "pointer" almost always refers to a pointer variable: a variable specifically designed to hold a memory address
In summary: a pointer is an address, and common usage of the word "pointer" almost always means pointer variable.
To get the starting memory address of a variable, we use the & (address-of) operator. We can store this address in a variable, which becomes our pointer variable:
#include <stdio.h>
int main(void)
{
int num = 10; // Allocate memory space for the integer variable
int *ptr = # // Get the starting address of num, store it in ptr
// num takes 4 bytes of space, we store the address of the first of these 4 bytes
// ptr is our pointer variable
return 0;
}
Key takeaways:
- Pointers store addresses, and every address uniquely identifies one block of memory
- Pointers are 4 bytes in size on 32-bit platforms, and 8 bytes on 64-bit platforms, regardless of the pointer type
Pointer Types
Just as ordinary variables have different data types (integers, floats, etc.), pointers also have specific matching types:
char *c_ptr = NULL;
int *i_ptr = NULL;
short *s_ptr = NULL;
long *l_ptr = NULL;
float *f_ptr = NULL;
double *d_ptr = NULL;
The general pattern for pointer declaration is [data type] *, where the data type indicates what type of variable the pointer is intended to point to:
char*type pointers store addresses ofcharvariiblesshort*type pointers store addresses ofshortvariablesint*type pointers store addresses ofintvariables, and so on
Adding/Subtracting Integers from Pointers
#include <stdio.h>
int main(void)
{
int num = 10;
char *byte_ptr = (char*)#
int *word_ptr = #
printf("Address of num: %p\n", &num);
printf("byte_ptr: %p\n", byte_ptr);
printf("byte_ptr + 1: %p\n", byte_ptr + 1);
printf("word_ptr: %p\n", word_ptr);
printf("word_ptr + 1: %p\n", word_ptr + 1);
return 0;
}
The output from this example makes it clear: the type of a pointer determines how large a step it takes when moving forward or backward. In short, adding 1 to a pointer moves it forward by the size of the data type it points to.
Dereferencing Pointers
Dereferencing is the operation of accessing the data stored at the address a pointer holds. The pointer type also determines how much memory can be accessed when we dereference:
#include <stdio.h>
int main(void)
{
int val = 0x11223344;
char *c_ptr = (char*)&val;
int *i_ptr = &val;
*c_ptr = 0;
*i_ptr = 0;
return 0;
}
In the example above, assigning 0 to *c_ptr only modifies 1 byte of the val variable, while assigning 0 to *i_ptr modifies all 4 bytes of val. The pointer type defines how much memory (how many bytes) we have permission to access when we dereference.
Wild Pointers
A wild pointer is a pointer that points to an invalid, unreserved, or random memory location. Using a wild pointer causes undefined program behavior.
Common Causes of Wild Pointers
1. Uninitialized Pointers
Local pointer variables are not automatically set to a valid value. Uninitialized pointers hold random garbage addresses, making them wild:
#include <stdio.h>
int main(void)
{
int *p; // Local uninitialized pointer, stores a random address
*p = 20; // Invalid access to unknown memory
return 0;
}
2. Out-of-Bounds Access
When a pointer moves past the bounds of the array it points to, it becomes a wild pointer:
#include <stdio.h>
int main(void)
{
int arr[10] = {0};
int *p = arr;
for (int i = 0; i <= 11; i++)
{
// Once p moves beyond the last element of arr, it becomes a wild pointer
*(p++) = i;
}
return 0;
}
How to Avoid Wild Pointers
Follow these best practices to avoid wild pointer issues:
- Always initialize pointers on declaration, use
NULLif you do not have a valid address yet - Always check for array bounds when moving pointers through array memory
- Set pointers to
NULLimmediately after the memory they point to is released - Never return the address of a local stack variable from a function
- Always check that a pointer is not
NULLbefore dereferencing
Example of safe pointer usage:
#include <stdio.h>
int main(void)
{
int *p = NULL;
// ... intermediate code ...
int val = 10;
p = &val;
if (p != NULL)
{
*p = 20;
}
return 0;
}
Pointer Operations
There are three common categories of legal pointer operations:
- Adding or subtracting integers
- Subtracting one pointer from another
- Relational comparison between pointers
Adding/Subtracting Integers
This is commonly used to iterate over sequential memory like arrays:
#include <stdio.h>
#define NUM_VALUES 5
int main(void)
{
float values[NUM_VALUES];
float *v_ptr;
for (v_ptr = &values[0]; v_ptr < &values[NUM_VALUES];)
{
*v_ptr++ = 0;
}
return 0;
}
Subtracting One Pointer from Another
When two pointers point to elements in the same array, subtracting them returns the number of elements between the two pointers. This is commonly used to calculate string length:
int my_strlen(char *str)
{
char *start = str;
while (*start != '\0')
start++;
return start - str;
}
Relational Operations on Pointers
Pointers can be compared to check which points to a higher or lower memory address, which is often used to control array iteration:
for (v_ptr = &values[NUM_VALUES]; v_ptr > &values[0];)
{
*--v_ptr = 0;
}
Pointers and Arrays
In C, an array name evaluates to the address of the array's first element in almost all contexts, with only two exceptions:
sizeof(array_name)returns the total size of the entire array, not the size of a pointer&array_namereturns the address of the entire array, not the address of the first element
Becausee array name is the adddress of the first element, array access can be rewritten using pointer arithmetic: arr[i] is exactly equivalent to *(arr + i).
This allows us to iterate through an array using a pointer:
int main(void)
{
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
int *p = arr; // Store the first element address in the pointer
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
Double Pointers
Pointer variables are themselves variables stored in memory, so they have their own address. To store the address of a pointer variable, we need a double pointer (a pointer to a pointer).
Common double pointer operations:
*ppadereferences the double pointer to get the original pointer it points to:
int b = 20;
*ppa = &b; // Equivalent to pa = &b;
**ppadereferences twice to access the original variable pointed to by the first pointer:
**ppa = 30;
// Equivalent to *pa = 30;
// Equivalent to a = 30;
Pointer Arrays
A pointer array is an array that stores pointer variables, rather than ordinary primitive values. Just as we have integer arrays to store integers and character arrays to store characters, we can have an array that stores multiple pointers:
int *ptr_arr[5];
In this example, ptr_arr is an array of 5 elements, and each element is an integer pointer that can hold the address of an integer variable.