What Is a Pointer?
A pointer represents an address in memory, essentially serving as the identifier for a memory unit. Each byte in memory possesses its own unique address. When you declare a variable, the system allocates a specific block of memory for that variable, and the variable's value is stored within that allocated space.
Integer Pointers
Consider an integer variable a. An integer typically occupies 4 bytes in memory, with each byte having its own address. These addresses increment by 1 sequentially. When you take the address of a using the &a operator, you receive the lowest memory address among the bytes occupied by a. The hexadecimal value displayed (such as 19) converts to decimal 25 in this example.
Pointer Variables
A pointer variable is simply a variable designed to store memory addresses. The syntax int *p declares p as a pointer variable, where int specifies the type of data the pointer is intended to reference. The asterisk * identifies p as a pointer, while the base type (int in this case) indicates what kind of value this pointer should point to.
Pointer Type Semantics
In the declaration int *p, the type is int * (integer pointer type), and p is the variable name. When extending this concept to arrays, you can think of everything except the variable name as representing the type.
The Dereference Operator
The asterisk * serves as the dereference operator (also called the indirection operator). When applied to a pointer, it accesses the value stored at the memory address the pointer references. When dereferencing appears on the left side of an assignment, it designates the memory location for storing a value; otherwise, it retrieves the value at that location.
Size of Pointer Variables
The size of a pointer variable depends on the system's architecture. In a 32-bit environment, all pointers occupy 4 bytes. In a 64-bit environment, pointers consume 8 bytes. This size remains consistent regardless of what type of data the pointer is designed to reference.
Why All Pointers Have the Same Size
Pointer variables store memory addresses, and memory addresses are fundamentally binary numbers. The length of these numbers depends entirely on the system's address bus width. Since all pointers store addresses, they all occupy the same amount of space. This explains why int * and char * pointers both have identical sizes on a 64-bit system (8 bytes).
For printing sizeof results, use the %zd format specifier because sizeof returns a value of type size_t.
The Purpose of Different Pointer Types
If all pointers share the same size, you might wonder why different types exist. The answer lies in dereferencing operations. A pointer's type determines how many bytes can be accessed when dereferencing. For instance, when an int * pointer is stored in a char * variable, the pointer value fits, but dereferencing accesses only a single byte. A char * pointer can only read or modify one byte at a time.
Pointer Arithmetic
Different pointer types increment at different rates. Adding 1 to an int * pointer advances the address by 4 bytes (the size of an integer). Adding 2 advances by 8 bytes. Conversely, a char * pointer advances by only 1 byte per increment. The pointer type dictates the stepping granularity during arithmetic operations.
Void Pointers
The void * type represents a generic pointer capable of holding addresses of any type. This flexibility makes void * useful for functions that must work with pointers of unknown or varying types.
However, void * pointers have limitations. You cannot perform arithmetic operations on them, and you cannot dereference them directly. To use the pointed-to data, you must first cast the void * to an appropriate typed pointer. This design forces explicit type conversion, promoting type safety in generic programming contexts.
Const and Pointers
The const keyword interacts with pointers in nuanced ways, depending on its position relative to the asterisk.
Const Before the Asterisk
When const appears before the asterisk, as in const int *p or int const *p, it qualifies the pointed-to value. The value at the address cannot be modified through this pointer, but the pointer itself can be reassigned to point to a different memory location.
Const After the Asterisk
When const follows the asterisk, as in int * const p, it qualifies the pointer variable itself. The pointer cannot be changed to reference a different address, but the value at the current address can be modified.
Understanding int *p = &a
This declaration creates a pointer variable p that stores the address of a. The expression *p dereferences p to access the value at that address. Since p is itself a variable, it occupies memory space and has its own address, meaning you can create another pointer to store p's address (a pointer to a pointer).
Pointer Operations
Pointer and Array Relationship
Arrrays store elements contiguously in memory, with addresses increasing as the index increases. This contiguous storage enables pointer arithmetic to traverse array elements.
int main() {
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = arr; // Equivalent to &arr[0]
int length = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < length; i++) {
printf("%d ", *(p + i));
}
return 0;
}
The expression *(p + i) accesses the element at position i. Note that incrementing p directly (p++) changes the pointer's base address, which may not be desirable if you need to preserve the original position.
Pointer Subtraction
Pointer subtraction is valid and yields the number of elements between two pointers. Both pointers must point to the same memory block or array. The result represents the element distance: subtracting a higher address from a lower address produces a positive value, while the reverse yields a negative value.
Wild Pointers and Null Pointers
Wild Pointers
A wild pointer is an uninitialized pointer variable containing an indeterminate value. Dereferencing such a pointer leads to undefined behavior, typically causing program crashes. Pointers become wild through:
- Initialization omission
- Array index overflow (accessing beyond array boundaries)
- Returning addresses of local variables that go out of scope
Null Pointers
A null pointer contains a zero address, represented by NULL or 0. Null pointers do not reference any valid memory location and cannot be dereferenced safely. Always initialize pointers explicitly, and check for NULL before dereferencing.
int *ptr = NULL;
if (ptr != NULL) {
// Safe to dereference
}
Assert for Pointer Validasion
The assert macro from <assert.h> provides runtime debugging assistance. It evaluates the given expression and terminates the program if the expression evaluates to false, displaying diagnostic information. This proves valuable for validating pointer parameters and detecting null pointers during development.
#include <assert.h>
void process_data(int *data) {
assert(data != NULL);
// Proceed with processing
}
To disable assertions in release builds, define NDEBUG before including <assert.h>. Visual Studio automatically removes assertions in release configurations.
When Pointers Are Essential
Consider a swap function that fails to modify its arguments:
void swap(int x, int y) {
int z = x;
x = y;
y = z;
}
This function cannot modify the caller's variables because C passes arguments by value. The function receives copies of the arguments, and modifications affect only these copies. Passing pointers to the variables enables actual modification:
void swap(int *x, int *y) {
int z = *x;
*x = *y;
*y = z;
}
Pointers and Arrays
Array Names as Pointers
In most contexts, an array name decays to a pointer to its first element. The array's address and its first element's address share the same numeric value.
Two Exceptions for Address-of Array Name
Two special cases prevent array name decay:
sizeof(arr): When an array name is the sole operand,sizeofreturns the total array size in bytes&arr: Taking the address of an array name yields a pointer to the entire array, not just the first element
int arr[5];
sizeof(arr); // Returns 5 * sizeof(int) = 20
&arr; // Type: int (*)[5], points to entire array
The address values of arr and &arr may appear identical, but their types differ. Adding 1 to arr advances by one element (4 bytes), while &arr + 1 advances past the entire array (20 bytes in this case).
One-Dimensional Array Access Equivalencies
For an array arr and pointer p pointing to arr[0], these expressions are equivalent:
arr[i]*(arr + i)p[i]*(p + i)i[arr](equivalent to*(i + arr))
These equivalencies demonstrate that array subscript notation and pointer arithmetic are fundamentally the same mechanism.
One-Dimensional Array Parameter Passing
When passing an array to a function, only the address of the first element is transmitted. The parameter declaration int arr[] is semantically equivalent to int *arr. Without explicit size information, the function cannot determine the array's dimensions.
void process_array(int *arr, size_t size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2;
}
}
For two-dimensional arrays, passing the array name transmits the address of the first row, requiring an array pointer for proper reception: int (*p)[10].
Double Pointers
A double pointer stores the address of another pointer variable. If int *p holds the address of an integer, then int **pp stores the address of p. The two asterisks represent two levels of indirection: *pp accesses p, and **pp accesses the value p points to.
Pointer Arrays
A pointer array is an array where each element is a pointer:
int *arr[5]; // Array of 5 integer pointers
Simulating Two-Dimensional Arrays with Pointer Arrays
Multiple one-dimensional arrays can be stored in a pointer array to simulate matrix-like access:
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {6, 7, 8, 9, 10};
int arr3[] = {11, 12, 13, 14, 15};
int *ptr_array[] = {arr1, arr2, arr3};
// Access third row, second element
int value = ptr_array[2][1]; // Or: *(*(ptr_array + 2) + 1)
Character Pointers
Character pointers handle string literals and individual characters:
char *str = "Hello World";
The string literal resides in read-only memory, and str holds the address of its first character ('H'). Attempting to modify *str results in undefined behavior because string literals are immutable.
The %s format specifier prints strings by accessing characters sequentially until the null terminator \0 is encountered.
Array Pointers
An array pointer points to an entire array rather than its first element:
int arr[10];
int (*p)[10] = &arr; // p points to entire array of 10 integers
The parentheses are essential—without them, int *p[10] would declare an array of 10 integer pointers instead.
Accessing Elements Through Array Pointers
int arr[5] = {10, 20, 30, 40, 50};
int (*p)[5] = &arr;
// Different ways to access arr[2]
(*p)[2]; // Dereference array, then index
*((*p) + 2); // Array address to element address, then dereference
p[0][2]; // Array pointer as array-like
*(p[0] + 2); // Same as above
Two-Dimensional Arrays
Parameter Passing for Two-Dimensional Arrays
A two-dimensional array is fundamentally an array of one-dimensional arrays. When passing such an array to a function, you transmit the address of its first row. The parameter type must match this structure:
void process_matrix(int (*rows)[5], int row_count) {
for (int i = 0; i < row_count; i++) {
for (int j = 0; j < 5; j++) {
rows[i][j] *= 2; // Or: (*(rows + i))[j]
}
}
}
int main() {
int matrix[3][5] = {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {11, 12, 13, 14, 15}};
process_matrix(matrix, 3);
return 0;
}
Two-Dimensional Array Address Layout
Two-dimensional arrays store elements in row-major order with contiguous memory. This allows accessing elements through pointer arithmetic:
int matrix[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
// Access element at row 1, column 2 (value 7)
*(*(matrix + 1) + 2); // Equivalent to matrix[1][2]
*(*matrix + 6); // Element at offset 6 from start
Function Pointers
Function pointers store addresses of functions, enabling dynamic function invocation and callback mechanisms.
Function Pointer Declaration
int add(int x, int y) {
return x + y;
}
// Function pointer declaration
int (*func_ptr)(int, int) = add; // Points to add function
The declaration breaks down as:
int (*- Pointer to function returning intfunc_ptr- Pointer variable name(int, int)- Parameter types
Calling Through Function Pointers
// Both approaches are valid
int result1 = (*func_ptr)(3, 4); // Explicit dereference
int result2 = func_ptr(3, 4); // Direct call
The dereference syntax is optional—function pointer variables can be called directly with the () operator.
Function Pointer as Parameter
Function pointers frequently serve as callback parameters:
typedef int (*comparator)(const void *, const void *);
void bubble_sort(int *array, size_t size, comparator cmp) {
for (size_t i = 0; i < size - 1; i++) {
for (size_t j = 0; j < size - i - 1; j++) {
if (cmp(&array[j], &array[j + 1]) > 0) {
swap(&array[j], &array[j + 1]);
}
}
}
}
The qsort Function
The qsort function from <stdlib.h> implements quicksort for sorting arrays of any type:
void qsort(void *base, size_t nmemb, size_t size,
int (*compare)(const void *, const void *));
Parameters:
base: Pointer to the array to sortnmemb: Number of elementssize: Size of each element in bytescompare: Comparison function pointer
Comparison Function Requirements
The comparison function must return:
- Negative if first argument is less than second
- Zero if arguments are equal
- Positive if first argument is greater than second
int compare_ints(const void *a, const void *b) {
return (*(int *)a) - (*(int *)b);
}
int main() {
int numbers[] = {5, 2, 8, 1, 9, 3};
size_t count = sizeof(numbers) / sizeof(numbers[0]);
qsort(numbers, count, sizeof(int), compare_ints);
return 0;
}
Implementing qsort from Scratch
int compare_ints(const void *a, const void *b) {
return (*(int *)a) - (*(int *)b);
}
void swap_bytes(void *ptr1, void *ptr2, size_t width) {
unsigned char *p1 = (unsigned char *)ptr1;
unsigned char *p2 = (unsigned char *)ptr2;
for (size_t i = 0; i < width; i++) {
unsigned char temp = p1[i];
p1[i] = p2[i];
p2[i] = temp;
}
}
void my_qsort(void *base, size_t nmemb, size_t width,
int (*cmp)(const void *, const void *)) {
if (nmemb <= 1) return;
unsigned char *arr = (unsigned char *)base;
for (size_t i = 0; i < nmemb - 1; i++) {
for (size_t j = 0; j < nmemb - i - 1; j++) {
void *addr1 = arr + j * width;
void *addr2 = arr + (j + 1) * width;
if (cmp(addr1, addr2) > 0) {
swap_bytes(addr1, addr2, width);
}
}
}
}
Sizeof Versus Arrays
Sizeof with Integer Arrays
int a[] = {1, 23, 4, 2};
sizeof(a); // 16 (entire array: 4 elements * 4 bytes)
sizeof(a + 0); // 8 (address of first element)
sizeof(*a); // 4 (first element: int)
sizeof(a + 1); // 8 (address of second element)
sizeof(a[1]); // 4 (second element)
sizeof(&a); // 8 (address of entire array)
sizeof(*&a); // 16 (entire array)
sizeof(&a + 1); // 8 (address past array)
Sizeof with Character Arrays
char str[] = "abcdef"; // Size 7 (including '\0')
sizeof(str); // 7
sizeof(str + 0); // 8 (address)
sizeof(*str); // 1 (character)
sizeof(str[1]); // 1 (character)
sizeof(&str); // 8 (address of array)
sizeof(&str + 1); // 8 (address past array)
sizeof(&str[0] + 1); // 8 (address of second character)
Sizeof with Char* Pointing to String Literal
const char *p = "abcdef";
sizeof(p); // 8 (pointer size)
sizeof(p + 1); // 8 (address of second character)
sizeof(*p); // 1 (character 'a')
sizeof(p[0]); // 1 (character 'a')
sizeof(&p); // 8 (address of pointer variable)
sizeof(&p + 1); // 8 (address past pointer)
sizeof(&p[0] + 1); // 8 (address of second character)
Sizeof with Two-Dimensional Arrays
int matrix[3][4] = {0};
sizeof(matrix); // 48 (entire array: 3 * 4 * 4)
sizeof(matrix[0]); // 16 (first row: 4 * 4)
sizeof(matrix[0] + 1); // 8 (address of element [0][1])
sizeof(*(matrix[0] + 1)); // 4 (element at [0][1])
sizeof(matrix + 1); // 8 (address of second row)
sizeof(*(matrix + 1)); // 16 (second row)
sizeof(&matrix[0] + 1); // 8 (address of second row)
sizeof(*(&matrix[0] + 1));// 16 (second row)
sizeof(*matrix); // 16 (first row)
sizeof(matrix[3]); // 16 (type-based, not actual access)
Strlen Versus Arrays
Strlen with Character Arrays
char chars[] = {'a', 'b', 'c', 'd', 'e', 'f'};
// No null terminator - undefined behavior
strlen(chars); // Random value (scans until \0 found)
strlen(chars + 0); // Random value
strlen(*chars); // ERROR: dereferences 'a' (97), invalid address
strlen(chars[1]); // ERROR: 'b' (98), invalid address
strlen(&chars); // Random value (starts at array address)
strlen(&chars + 1); // Random value (past array)
strlen(&chars[0] + 1); // Random value (from second character)
Strlen with String Arrays
char str[] = "abcdef";
strlen(str); // 6 (length without \0)
strlen(str + 0); // 6
strlen(*str); // ERROR: 'a' treated as address
strlen(str[1]); // ERROR: 'b' treated as address
strlen(&str); // 6 (array address same as element address)
strlen(&str + 1); // Random value (past array)
strlen(&str[0] + 1); // 5 (from second character)
Strlen with Char* Pointing to String Literal
const char *p = "abcdef";
strlen(p); // 6
strlen(p + 1); // 5 (starting from 'b')
strlen(*p); // ERROR: 'a' as address
strlen(p[0]); // ERROR: 'a' as address
strlen(&p); // Random (address of pointer variable)
strlen(&p + 1); // Random (past pointer)
strlen(&p[0] + 1); // 5 (from second character)
Function Pointer Arrays
A function pointer array stores multiple function addresses:
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }
int main() {
int (*operations[4])(int, int) = {add, sub, mul, div};
operations[0](10, 5); // Calls add(10, 5)
operations[1](10, 5); // Calls sub(10, 5)
return 0;
}
The type of a function pointer array is int (*[4])(int, int), where [4] indicates four function pointers.
Pointer to Function Pointer Array
int (*(*func_ptr_arr)[4])(int, int) = &operations;
// func_ptr_arr points to array of 4 function pointers
This declaration breaks down as:
int (*- Function returning int(*func_ptr_arr)- Pointer variable[4])- To array of 4(int, int)- Function parameters
Callback Functions
A callback function is passed as an argument to another function and is invoked when specific conditions occur:
void apply_operation(int x, int y, int (*op)(int, int)) {
int result = op(x, y);
printf("Result: %d\n", result);
}
int multiply(int a, int b) {
return a * b;
}
apply_operation(5, 3, multiply); // multiply is the callback
The qsort function exemplifies callbacks—the comparison function serves as a callback invoked during sorting.
Type Definitions with typedef
The typedef keyword creates aliases for complex types, simplifying declarations:
Pointer Type Definitions
typedef int* int_ptr;
int_ptr p1, p2; // Both are int pointers
Array Pointer Type Definitions
typedef int (*array_ptr)[10];
array_ptr p; // p is pointer to array of 10 ints
Function Pointer Type Definitions
typedef int (*calc_func)(int, int);
calc_func compute = add; // compute is a function pointer
Note: #define macros for type aliases have limitations when defining multiple variables simultaneously.
Pointer Arithmetic with Converted Types
When converting between pointer and integer types, arithmetic operates on the integer value:
int *ptr = (int *)0x100000;
long converted = (long)ptr + 1; // Numeric: 0x100001
int *new_ptr = (int *)converted; // Address: 0x100001
This differs from pointer arithmetic, which steps by the pointed-to type's size:
char *cptr = (char *)0x100000;
cptr + 1; // 0x100001 (advances by 1 byte)
int *iptr = (int *)0x100000;
iptr + 1; // 0x100004 (advances by 4 bytes)
Structures and Pointers
Structures group related data, and pointers enable efficient manipulation:
struct Person {
char name[50];
int age;
double salary;
};
void update_salary(struct Person *emp, double new_salary) {
emp->salary = new_salary; // Arrow operator for pointer access
}
The arrow operator -> accesses structure members through pointers, equivalent to (*ptr).member.
String Comparison with strcmp
The strcmp functon compares strings lexicographically:
int strcmp(const char *s1, const char *s2);
Return values:
- Negative if
s1<s2 - Zero if strings are equal
- Positive if
s1>s2
if (strcmp(password, "secret123") == 0) {
printf("Access granted\n");
}
Core Concepts Summary
A memory address identifies a unique storage location. Pointers are variables that store these addresses, enabling indirect access to data. The type system ensures correct interpretation of pointed-to data and determines pointer arithmetic behavior. Understanding the relationship between pointers and arrays—where array names often decay to pointers—unlocks efficient memory manipulation and algorithm implementation.
Function pointers enable callbacks and dynamic behavior, while generic pointers (void *) support flexible interfaces. Proper pointer hygiene, including initialization, null checking, and avoiding wild pointers, prevents undefined behavior and security vulnerabilities.