Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 30

Pointer Arithmetic for Multi-Dimensional Arrays

Recommended resources

Hello! Welcome to our lesson on traversing multi-dimensional arrays with pointers.

In our last session, we mastered the "right-left rule" to parse complex C declarations. We can now confidently distinguish between an array of pointers like int *p[10] and a pointer to an array like int (*p)[10]. This distinction, which might have seemed academic, is the absolute key to understanding how C handles multi-dimensional arrays at a low level.

Today, we will build directly on that knowledge. Our goal is to apply pointer arithmetic to traverse multi-dimensional arrays. You'll see precisely why a type like int (*)[10] is not just a syntactic curiosity, but the essential piece of information the compiler needs to navigate a 2D array in memory. This understanding is fundamental for writing efficient and correct code, especially when dealing with the large, structured data buffers common in ADAS applications.

How Multi-Dimensional Arrays are Stored in Memory

The first thing to understand is that computer memory is a single, one-dimensional sequence of bytes. A multi-dimensional array must be "flattened" into this linear space. C, C++, and many other languages do this using row-major order. This means the first row is stored contiguously, followed by the second row, and so on.

The opposite is column-major order, used by languages like Fortran and MATLAB, where columns are stored contiguously. Given your work involves integrating with various systems and possibly analyzing data from different sources (like MATLAB simulations), being aware of these conventions is valuable.

Multidimensional Arrays Layout (C/Java-Like)
This image shows how a `char C[4][3]` array is laid out in memory using row-major order. The elements are stored row-by-row in a contiguous block. The formula shown demonstrates how an element's address can be calculated from its indices and the array's dimensions.

This contiguous, row-by-row layout is the foundation for all multi-dimensional array pointer arithmetic in C.


Pointer Types and Arithmetic in 2D Arrays

Let's connect this to our previous lesson. Consider this declaration:
int arr[3][4];

This is a 2D array. Conceptually, it's an array of 3 elements, where each element is an array of 4 integers.

  • The name arr refers to the entire array. When used in an expression, it decays to a pointer to its first element.
  • What is the first element of arr? It's the first row, arr[0], which is an array of 4 integers.
  • Therefore, arr decays to a pointer to an array of 4 integers. Using the syntax we learned, its type is int (*)[4].

This type is crucial because it tells the compiler the size of the object being pointed to—an entire row. This is how pointer arithmetic scales correctly.

  • arr points to the start of the first row (&arr[0]).
  • arr + 1 points to the start of the second row (&arr[1]). The address is incremented by sizeof(int[4]), which is 4 * sizeof(int) bytes.
  • *arr (which is the same as arr[0]) refers to the first row. In an expression, it decays to a pointer to its first element, &arr[0][0]. Its type is int *.
  • *arr + 1 points to the second integer in the first row (&arr[0][1]). The address is incremented by just sizeof(int).

The following video provides an excellent visualization of these concepts and the different pointer types involved.

Pointers and multidimensional arrays

To see how pointer arithmetic works with multi-dimensional arrays, it's essential to visualize their memory organization and the types of pointers involved. This video from mycodeschool offers a clear, step-by-step explanation.

Watch from the beginning (00:24) to 07:18. Focus on the distinction between the type of B (a pointer to a 1D array) and the type of *B (a pointer to an integer). Notice how adding 1 to B moves the pointer by an entire row's worth of bytes, while adding 1 to *B moves it by only a single element.


The Equivalence of Indexing and Pointer Arithmetic

With this understanding, we can now translate standard array indexing arr[i][j] into its pointer-based equivalent. The compiler effectively performs this transformation for you.

arr[i][j] is equivalent to *(*(arr + i) + j)

Let's break this down piece by piece, using our int arr[3][4] example:

  1. arr: A pointer to the first row (type int (*)[4]).
  2. arr + i: Pointer arithmetic. This yields a pointer to the i-th row. The address is (address of arr) + i * sizeof(int[4]). The type is still int (*)[4].
  3. *(arr + i): Dereferencing this pointer gives us the i-th row itself (arr[i]). When used in an expression, this array name decays to a pointer to its first element, &arr[i][0]. The type is now int *.
  4. *(arr + i) + j: More pointer arithmetic. Since the pointer type is now int *, we add j * sizeof(int) to the address. This gives us a pointer to the j-th element in the i-th row (&arr[i][j]).
  5. *(*(arr + i) + j): The final dereference, which retrieves the actual integer value at that address.

The following document provides a superb textual analysis of this equivalence and explains why the compiler must know the array's column dimension to make this work.

A TUTORIAL ON POINTERS AND ARRAYS IN C by Ted ...

The video provided a great visual demonstration. This classic tutorial by Ted Jensen will now solidify the concepts in text, explaining how the compiler uses the array's dimensions for pointer arithmetic.

Read 'CHAPTER 6' starting from the discussion of char multi[5][10]. Focus on the breakdown of *(*(multi + row) + col). Then, read the beginning of 'CHAPTER 7', which reinforces why the compiler requires the second dimension (COLS) to perform these calculations correctly.

Test your understanding!

Given the following array:
int matrix[2][3] = { {10, 20, 30}, {40, 50, 60} };

What is the value of the expression *(*(matrix + 1) + 2)? Walk through the steps to find the answer.

Show answer
  1. matrix: Pointer to the first row, {10, 20, 30}.
  2. matrix + 1: Pointer to the second row, {40, 50, 60}.
  3. *(matrix + 1): The second row itself, matrix[1]. This decays to a pointer to its first element, 40.
  4. *(matrix + 1) + 2: A pointer to the element at index 2 of the second row (the value 60).
  5. *(*(matrix + 1) + 2): The value at that address, which is 60.

This is equivalent to matrix[1][2].


Generalizing to 3D and Beyond: Performance Matters

The same logic extends naturally to higher dimensions. For a 3D array C[d1][d2][d3], the name C is a pointer to a 2D array of size d2 x d3. The expression C[i][j][k] becomes *(*(*(C + i) + j) + k).

Pointers and multidimensional arrays

The pattern of dereferencing and pointer arithmetic is consistent for arrays of any dimension. Let's briefly see this in action with a 3D array.

Watch from 07:18 to the end (13:24). Note how the logic is simply extended one level deeper for the 3D array. The code example at the end is a good way to verify these concepts on your own machine.

This deep understanding of memory layout is not merely academic; it has critical performance implications. In your work with ADAS, you process large data sets (camera images, RADAR cubes, etc.) where performance is paramount. Traversing this data in the order it's stored in memory (row by row in C) is crucial for CPU cache efficiency.

When you access elements sequentially, you benefit from spatial locality. The CPU can pre-fetch subsequent memory into its fast L1/L2 caches, leading to huge speedups. Accessing elements out of order (e.g., column by column in a row-major array) can lead to constant cache misses, forcing the CPU to wait for data from slow main memory.

Memory layout of multi-dimensional arrays

This article explains the concept of memory layout in detail and uses a benchmark to demonstrate the profound performance impact of traversing arrays in their native order.

First, read the sections 'Row-major vs. column-major' and '2D row-major' to solidify the concept. Then, read 'History: Fortran vs. C' for context. Finally, and most importantly, read the 'Performance' section. This explains why access patterns aligned with memory layout are critical for cache efficiency and shows benchmarks with dramatic results—a concept highly relevant to optimizing data processing in systems like ADAS.


Conclusion

Today we've connected the abstract syntax of pointer-to-array types with the concrete reality of memory layouts and pointer arithmetic. You can now not only read but also understand and apply the pointer logic that underpins C's multi-dimensional arrays.

Key Takeaways:

  • C stores multi-dimensional arrays in a contiguous block using row-major order.
  • The name of a 2D array, T arr[R][C], decays to a pointer of type T (*)[C]—a pointer to an array.
  • Pointer arithmetic on this type scales by the size of a full row (C * sizeof(T)).
  • The indexing expression arr[i][j] is equivalent to the pointer expression *(*(arr + i) + j).
  • Traversing data in its memory-storage order (row-by-row in C) is crucial for performance due to CPU caching and spatial locality.

Preview of the Next Lesson:
We've explored a specific, powerful type: the pointer to an array. In our next lesson, we will generalize this concept and explore pointers to pointers. This will enable us to tackle a classic C problem: how to write a function that can modify a pointer variable belonging to its caller. This technique is essential for managing dynamic resources and implementing more complex data structures.