Hello! Welcome to the third lesson in our module on Pointer Arithmetic and Arrays.
In our last lesson, we demystified the relationship between arrays and pointers, establishing that while they are distinct types, an array name "decays" into a pointer to its first element in most contexts. We saw how this explains why arr + 1 points to the second element, while &arr + 1 points just past the end of the entire array.
Today, we will put that theory into direct practice. Our goal is to master the two primary ways of accessing array elements: the familiar array subscript notation ([]) and the powerful technique of pointer arithmetic. Understanding the equivalence and appropriate use of these methods is a hallmark of a proficient C programmer and is fundamental to manipulating data efficiently, especially in the performance-critical world of embedded systems.
The Equivalence of Subscripting and Pointer Arithmetic
The C standard defines a simple but profound equivalence: for any array a and index i, the expression a[i] is interpreted by the compiler as *(a + i).
Let's break this down:
a + i: Because of array decay (which we covered last lesson), the array nameais converted to a pointer to its first element. The expression then becomes pointer arithmetic: it calculates the memory address of thei-th element,iunits of the element's size away from the beginning.*(...): The dereference operator*then accesses the value stored at that computed memory address.
This relationship is the bridge between the high-level abstraction of an "index" and the low-level reality of memory addresses.

To see this principle built up from scratch with clear code examples, let's watch a video that walks through the process.
Pointer Arithmetic, Array Offsets, and exposing how brackets dereference | Modern Cpp Series Ep. 26
This video from Mike Shah provides an excellent, step-by-step demonstration of how array subscripting is just 'syntactic sugar' for pointer arithmetic and dereferencing.
Please watch from 02:47 to 10:10. As you watch, focus on how each of these concepts is demonstrated: A pointer is initialized to an array's start address, and dereferencing it accesses the first element (*p is like array[0]). Incrementing the pointer (p++) moves it by sizeof(type), advancing it to the next element. Accessing elements using an offset from the base pointer (*(p + i)). The final reveal: how the bracket notation array[i] is simply a convenient way to write *(array + i).
Iterating Through Arrays: Two Styles
Now that we know both methods access the same elements, we can use them to iterate through arrays. You are already familiar with the standard index-based for loop. The pointer-based approach offers an alternative that can sometimes be more direct or efficient.
Let's compare the two styles for iterating through an array of N elements:
Style 1: Array Subscripting
This is the most common and often most readable method, especially when you need the index i for other calculations.
#define N 10
int numbers[N];
// Initialize the array
for (int i = 0; i < N; i++) {
numbers[i] = i * 3;
}
Style 2: Pointer Arithmetic
This method creates a pointer to the start of the array and increments it until it passes the end of the array. This can be very efficient for purely sequential access.
#define N 10
int numbers[N];
int *iptr;
// Initialize the array
iptr = numbers; // or iptr = &numbers[0];
for (int i = 0; i < N; i++) {
*iptr = i * 3;
iptr++; // Move to the next integer
}
A more common pointer-based loop combines the initialization, condition, and increment into the for statement itself:
// A more idiomatic pointer loop
for (int *p = numbers; p < numbers + N; p++) {
// *p refers to the current element
// Do something with *p
}
To see these patterns in a complete program and read a detailed explanation, the following resource is very helpful.
The article 'Pointer Arithmetic' from Dive Into Systems provides clear code examples that directly contrast using subscripting and pointer arithmetic for both initializing and printing array contents.
Read the sections that demonstrate the two iteration styles. Start from the code block under 'Then, using pointer dereferencing...' and read until the end of the code block under 'You can see how the code above modified array elements...'. This will show you complete loops for both writing to and reading from an array using both techniques.
Readability vs. Performance: A Professional's Dilemma
Given that both methods achieve the same result, the natural questions for a software engineer are:
- Is one faster than the other?
- Which one should I use in my code?
In early C programming, it was often claimed that pointer arithmetic was faster because it avoided the "multiplication and addition" (base + index * size) inherent in subscripting. An iterating pointer just required a simple increment.
However, in modern systems, this is an oversimplification. Compilers are incredibly sophisticated. For a simple for loop, a modern compiler can often recognize the access pattern and generate identical, highly optimized machine code for both the subscript and pointer versions.
This Stack Overflow thread provides an excellent discussion on this topic, including assembly output, which should be right up your alley given your experience with debuggers and low-level code.
Accessing array values via pointer arithmetic vs. subscripting in C
This discussion on Stack Overflow delves into the practical differences between array subscripting and pointer arithmetic, focusing on performance and readability.
Read the top three answers (by Mecki, moonshadow, and TheMarko). Pay special attention to: Mecki's explanation of the theoretical operation count and the accompanying assembly code, which shows how compilers can optimize these operations. The consensus that readability and clarity should be the primary concern, a principle you'll recognize from standards like MISRA C and ASPICE.
The key takeaway for you as a professional is this: Prioritize clarity.
- Use array subscripting (
arr[i]) when access is random or when the indexihas a clear logical meaning. - Use pointer iteration (
*p++) for purely sequential processing (e.g., searching, copying) if it makes the logic simpler and more direct. Never sacrifice readability for a micro-optimization that the compiler likely provides anyway.
Application with Complex Types
This equivalence holds true for any data type, which is where its power becomes apparent in real-world applications. Consider a scenario from your domain, like processing a buffer of CAN messages.
#include <stdio.h>
#include <stdint.h>
// A typical structure for a CAN message
typedef struct {
uint32_t id;
uint8_t dlc; // Data Length Code
uint8_t payload[8];
} CAN_Message;
// Function to find a message by its ID, using subscripting
CAN_Message* find_message_by_id_subscript(CAN_Message buffer[], int count, uint32_t target_id) {
for (int i = 0; i < count; i++) {
if (buffer[i].id == target_id) {
return &buffer[i]; // Return address of the found message
}
}
return NULL; // Not found
}
// Same function, using pointer arithmetic
CAN_Message* find_message_by_id_pointer(CAN_Message buffer[], int count) {
CAN_Message *end = buffer + count; // Pointer to one past the last element
for (CAN_Message *p = buffer; p < end; p++) {
if (p->id == 0x7DF) { // Note the -> operator for pointer to struct
return p; // Return the pointer to the found message
}
}
return NULL; // Not found
}
When we execute p++ in the second function, the compiler automatically adds sizeof(CAN_Message) to the address, correctly advancing the pointer to the next message in the buffer. Both functions are perfectly valid; the choice between them is a matter of style and clarity.
Test your understanding!
Imagine you have a function that needs to clear (zero-out) a specific portion of an integer array. The function signature is void clear_range(int arr[], int start_idx, int end_idx);.
Here is an implementation using array subscripting:
void clear_range_subscript(int arr[], int start_idx, int end_idx) {
for (int i = start_idx; i <= end_idx; i++) {
arr[i] = 0;
}
}
Your task: Rewrite this function into an equivalent version named clear_range_pointer that uses only pointer arithmetic and does not use the [] operator inside the loop.
Show answer
Here is one possible solution. There can be other correct variations.
void clear_range_pointer(int arr[], int start_idx, int end_idx) {
// Create a pointer to the start of the range
int *p_start = arr + start_idx;
// Create a pointer to the end of the range
int *p_end = arr + end_idx;
// Loop from the start pointer until it passes the end pointer
for (int *p_current = p_start; p_current <= p_end; p_current++) {
*p_current = 0;
}
}
This solution directly translates the indices into start and end pointers and then iterates between them, demonstrating a clear understanding of pointer-based iteration.
Conclusion
In this lesson, we've solidified the practical connection between arrays and pointers. You can now confidently use both array subscripts and pointer arithmetic to access and manipulate array data.
Key Takeaways:
- Array subscripting
a[i]is functionally identical to the pointer expression*(a + i). - You can iterate through an array using a standard index-based loop or a pointer-based loop.
- Pointer arithmetic automatically scales with the size of the pointed-to data type, making it suitable for arrays of any type, including complex structs.
- With modern compilers, the performance difference between the two methods is often negligible. Therefore, you should choose the method that makes your code clearer and more maintainable.
In our next lesson, we will continue exploring pointer arithmetic by learning how to calculate the difference between two pointers that point to elements of the same array, a technique useful for determining distances and ranges within memory blocks.