Skip to main content

Welcome to a Grasp lesson

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

Lesson 1

Pointers and Arrays in C/C++

Recommended resources

Hello! Welcome to the first lesson in our course on high-performance computing. We're starting with the absolute bedrock of systems programming in C and C++: pointers and their relationship with arrays. Your goal to master GPU kernel engineering begins here. In the world of CUDA, you are in direct control of memory, and manipulating pointers to navigate buffers in global, shared, or register memory is a daily task. A deep, intuitive understanding of pointer arithmetic is non-negotiable.

This lesson will equip you to perform pointer arithmetic and explain the fundamental connection between pointers and arrays. We will explore how the compiler helps you navigate memory, how array indexing is simply a convenient syntax for pointer operations, and how these concepts extend to more complex data structures. Let's begin our trip down memory lane.

1. From Machine Code to Typed Pointers

At the lowest level, a computer's memory is a vast, linear sequence of bytes, each with a numerical address. Early machine code programmers worked with these raw addresses directly. There were no data types—a value at a memory location was just a number, and it was up to the programmer to interpret it correctly.

C and C++ introduced a crucial abstraction: the typed pointer. A pointer still holds a memory address, but it also "knows" the type of data it points to (e.g., int*, char*, MyStruct*). This type information is a powerful tool for the compiler.

When you add 1 to an int*, you aren't just adding one byte to the address. The compiler knows an int occupies a certain number of bytes (typically 4), so it automatically scales the addition. pointer + 1 effectively means "give me the address of the next integer". This automatic scaling is the core principle of pointer arithmetic.

Let's explore this idea further with a short reading that traces this evolution from raw memory to typed pointers.

A trip down memory lane... (C++) - Hacks - The Crucible

This article from The Crucible provides a nice narrative on how memory is handled, moving from the untyped world of machine code to the typed pointers of C/C++. This will help build your intuition from first principles.

Please read from the beginning to the end of the integer example. As you read, focus on two key points: The distinction between an "address register" in assembly (which is just a number) and a "pointer" in C (which has a type). The difference in behavior when incrementing a char* versus an int*. Notice how the compiler uses the type information to change the underlying address by the correct number of bytes.


2. Pointer Arithmetic in Action

Now that you have the core concept, let's see it demonstrated. Pointer arithmetic isn't limited to incrementing (++). You can add or subtract any integer value to move forward or backward through a block of memory, and you can even subtract two pointers (that point into the same array) to find the number of elements between them.

This becomes especially powerful when working with arrays, which are by definition contiguous blocks of memory containing elements of the same type—a perfect match for pointer arithmetic. The following video provides a clear, practical guide.

Master Pointer Arithmetic In 10 Minutes (The Ultimate Guide For C Programmers).

This video from HuwsTube, "Master Pointer Arithmetic In 10 Minutes," gives a concise and practical demonstration of these concepts.

Please watch the following segments: The first example shows pointer arithmetic on a simple integer array. Pay close attention to how adding 1 to the pointer moves its value by 4 bytes (the size of an int). The follow-up explanation solidifies the idea that array indexing syntax, like a[4], is just "syntactic sugar" for the pointer operation *(a + 4). Finally, watch the example with structs. This is particularly relevant for complex data types you'll encounter in AI. Notice how pointer arithmetic works seamlessly, with the compiler using sizeof(struct) to calculate the correct byte offset, even accounting for complex memory layouts and padding (a topic we'll cover in detail soon).


3. The Interchangeability of Pointers and Arrays

As the video highlighted, there is an intimate relationship between pointers and arrays. In most contexts, when you use the name of an array, it decays into a pointer to its first element.

This means if you have an array int arr[5], the expressions arr and &arr[0] are equivalent; both yield the memory address of the first integer in the array.

This equivalence is why array indexing and pointer arithmetic are interchangeable:

  • arr[i] accesses the i-th element.
  • *(arr + i) first calculates the address of the i-th element and then dereferences it to get the value.

These two forms produce identical machine code. The choice between them is a matter of style and clarity.

This diagram illustrates the relationship. The pointer `ptr` is initialized with the base address of the array `arr`. Incrementing the pointer (`ptr + 1`, `ptr + 2`, etc.) moves it to the address of subsequent array elements, making `*(ptr + i)` equivalent to `arr[i]`.

4. A Common Pitfall: Don't Do the Compiler's Job

Given that pointer arithmetic is about scaling by sizeof(type), a common beginner's mistake is to try to perform this scaling manually. For example, to get to the next element in a struct person array, one might incorrectly write:

p_person = p_person + sizeof(struct person);

This is wrong and will lead to bugs or crashes. The C++ compiler is already aware that p_person is a pointer to a struct person. When it sees p_person + 1, it automatically multiplies the 1 by sizeof(struct person). If you add sizeof(struct person) yourself, the compiler will multiply that value by sizeof(struct person), advancing the pointer far beyond your intended location.

The next video does an excellent job of demonstrating this common error and, fitting your interest in low-level details, looks at the generated assembly code to prove how the compiler handles this for you.

you will never ask about pointer arithmetic after watching this video

The channel "Low Level" provides a fantastic demonstration of this common mistake in "you will never ask about pointer arithmetic after watching this video."

Watch these key parts: The incorrect attempt shows the bug in action. The presenter tries to increment the pointer by sizeof(struct person) and explains why this results in a crash. The correct method demonstrates the simple, correct syntax: p_person += 1. The most interesting part for you will be the analysis of the assembly code around the add instruction, which proves that the compiler automatically uses the correct byte offset (0x44, which is 68 decimal).


5. Extension to Multi-dimensional Arrays

The principles of pointer arithmetic also apply to multi-dimensional arrays, though the syntax can appear more complex. A 2D array, like int arr[3][4], is stored in memory in row-major order. This means Row 0 is stored first, followed immediately by Row 1, and then Row 2, forming one contiguous block of 12 integers.

You can think of arr not as a simple pointer-to-int, but as a pointer to the first element of an array of arrays.

  • arr points to the first row, arr[0].
  • arr + i points to the i-th row, arr[i].
  • *(arr + i) dereferences this pointer, yielding the i-th row itself. Since an array name decays to a pointer to its first element, *(arr + i) is equivalent to &arr[i][0].
  • *(arr + i) + j takes the address of the first element of the i-th row and adds j to it, yielding the address of arr[i][j].
  • *(*(arr + i) + j) dereferences this final address to get the value of the element arr[i][j].

While arr[i][j] is far more readable, understanding the pointer notation *(*(arr + i) + j) is crucial for comprehending how memory is being accessed, a skill that is paramount when optimizing memory access patterns in a CUDA kernel.

You can explore this in more detail in the following resource.

Relationship Between Pointer and Array in C

This article from GeeksforGeeks provides a clear breakdown of pointer arithmetic for 2D arrays.

Please read the section titled "Pointers and 2D Arrays". Focus on the derivation of the *(*(arr + i) + j) expression and how it relates to the row-major memory layout shown in the diagram.


Conclusion

In this lesson, we have established the fundamental principles of pointer arithmetic and its deep connection to arrays in C/C++. This is the first and most critical step in building the low-level programming skills required for high-performance computing.

Here are the key takeaways:

  • Typed Pointers: Pointers in C/C++ know the type of data they point to, which allows the compiler to perform "magic" on your behalf.
  • Automatic Scaling: When you add an integer n to a pointer ptr, the compiler calculates the new address as address_of(ptr) + n * sizeof(*ptr). You should always let the compiler do this math for you.
  • Arrays are Pointers (Mostly): An array name typically decays into a pointer to its first element.
  • Syntactic Sugar: Array-style indexing, arr[i], is simply a more readable alternative to pointer arithmetic, *(arr + i). Both compile to the same machine code.

In our next lesson, we will build directly on this foundation. Now that we understand how to navigate blocks of memory with pointers, we will ask: where does this memory come from? We'll explore the concepts of stack vs. heap memory allocation, which are central to managing program resources efficiently.