Skip to main content

Welcome to a Grasp lesson

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

Lesson 32

Struct Pointer Arithmetic

Recommended resources

Hello! Welcome to our next session.

In our last few lessons, we've taken a deep dive into C structs. We've seen how to calculate their size including padding, how to optimize memory usage by reordering members, and how to force a specific memory layout using packing directives like #pragma pack.

Today, we will connect all of these concepts to answer a critical interview question: how does pointer arithmetic work with structs? This lesson directly addresses how to predict the memory addresses that result from these operations, which hinges entirely on understanding a struct's total size in memory.

1. The Fundamental Rule of Pointer Arithmetic

Before we focus on structs, let's quickly recall the universal rule of pointer arithmetic in C. When you have a pointer p of type T* and you perform an operation like p + 1, the compiler doesn't just add 1 to the raw memory address. Instead, it scales the addition by the size of the type the pointer points to.

The new address is calculated as:

This is why incrementing an int* moves the address by sizeof(int) bytes (typically 4), and incrementing a char* moves it by sizeof(char) byte (always 1). The same exact logic applies to structs.

To see this in action, let's look at a straightforward explanation.

Pointer arithmetic for structs - Stack Overflow

This Stack Overflow thread addresses the core of today's topic. The question is simple: if a struct pointer p has a value, what is the value of p++? The accepted answer is clear and concise.

Please read the initial question and the first two answers. The first one gives the direct formula, and the second one (by user 'nos') states the general principle of scaling by sizeof *p.

As the resource confirms, for a pointer p of type struct my_struct*, the expression p++ increments the address held by p by exactly sizeof(struct my_struct) bytes. The pointer doesn't "know" about the individual members inside; it only knows the total size of the entire object it points to.


2. Connecting Padding, Size, and Pointer Arithmetic

Here is where our previous lessons become crucial. The value of sizeof(struct) is not merely the sum of its members' sizes; it includes all padding bytes inserted by the compiler for alignment. This means that any changes to a struct's layout—either through reordering or packing—will directly change the outcome of pointer arithmetic.

Let's use the two struct layouts we've discussed before. Assume a 64-bit system where sizeof(double) is 8 and sizeof(int) is 4.

// Layout 1: Unoptimized ordering
typedef struct {
    char   c;  // 1 byte
    // 7 bytes of internal padding
    double d;  // 8 bytes
    int    s;  // 4 bytes
    // 4 bytes of trailing padding
} struct_unoptimized; // sizeof is 24 bytes

// Layout 2: Optimized ordering
typedef struct {
    double d;  // 8 bytes
    int    s;  // 4 bytes
    char   c;  // 1 byte
    // 3 bytes of trailing padding
} struct_optimized; // sizeof is 16 bytes

To be absolutely sure about these sizes, you can quickly refer back to the reasoning we studied previously.

Structure Member Alignment, Padding and Data Packing

Let's use the analysis from GeeksforGeeks to confirm the sizes of our example structs. This will reinforce why the padding rules lead to these specific total sizes.

In the section "Structure Padding in C", quickly review the analysis for structc_t (our struct_unoptimized) and structd_t (our struct_optimized). Focus on how the final sizes of 24 and 16 bytes are calculated, including the role of trailing padding for array alignment.

Now, let's apply pointer arithmetic. Imagine we have an array of each struct type, and a pointer to the first element, which starts at memory address 0x2000.

Scenario 1: Unoptimized Struct

struct_unoptimized arr_u[2];
struct_unoptimized *p_u = &arr_u[0]; // p_u holds address 0x2000

p_u++; // Move to the next element

What is the new address in p_u?
The compiler calculates: 0x2000 + sizeof(struct_unoptimized)

The pointer now points to the beginning of arr_u[1] at address 0x2018.

Scenario 2: Optimized Struct

struct_optimized arr_o[2];
struct_optimized *p_o = &arr_o[0]; // p_o holds address 0x2000

p_o++; // Move to the next element

What is the new address in p_o?
The compiler calculates: 0x2000 + sizeof(struct_optimized)

The pointer now points to the beginning of arr_o[1] at address 0x2010.

This directly shows how a simple code change—reordering struct members—has a tangible effect on memory layout and pointer arithmetic. This is a key insight to articulate in an interview.


3. A Practical Code Example

Let's consolidate this with a complete, runnable C program. This demonstrates not only incrementing a pointer to iterate forward but also pointer subtraction, which also operates in units of sizeof(struct).




#include <stdio.h>
#include <stddef.h> // Required for ptrdiff_t

// Using the optimized struct from our example
typedef struct {
    double d;
    int    s;
    char   c;
} data_packet; // On a 64-bit system, sizeof is 16

int main() {
    data_packet packets[3] = {
        {1.1, 10, 'a'},
        {2.2, 20, 'b'},
        {3.3, 30, 'c'}
    };

    // Initialize a pointer to the first element of the array.
    // Note: The array name 'packets' decays to a pointer to its first element here.
    data_packet *ptr = packets;

    printf("Base address of array:     %p\n", (void*)packets);
    printf("Initial ptr value:         %p\n", (void*)ptr);
    printf("sizeof(data_packet):       %zu bytes\n\n", sizeof(data_packet));

    // Access the first element
    printf("packets[0]: d=%.1f, s=%d, c='%c'\n", ptr->d, ptr->s, ptr->c);
    
    // --- Pointer Increment ---
    // Move the pointer to the next element in the array
    ptr++;

    printf("\nAddress after ptr++:       %p\n", (void*)ptr);
    printf("packets[1]: d=%.1f, s=%d, c='%c'\n", ptr->d, ptr->s, ptr->c);
    
    // --- Pointer Subtraction ---
    // The difference between two pointers is the number of elements, not bytes.
    data_packet *start = &packets[0];
    data_packet *end = &packets[2];
    ptrdiff_t diff = end - start;

    printf("\nPointer subtraction (end - start): %td elements\n", diff);

    return 0;
}

Expected output (addresses will vary, but offsets will be consistent):

Base address of array:     0x7ffc1...
Initial ptr value:         0x7ffc1...
sizeof(data_packet):       16 bytes

packets[0]: d=1.1, s=10, c='a'

Address after ptr++:       0x7ffc1... (16 bytes higher than initial)
packets[1]: d=2.2, s=20, c='b'

Pointer subtraction (end - start): 2 elements

This code explicitly shows that ptr++ advances the address by sizeof(data_packet), allowing you to correctly access the members of the next element in the array. Furthermore, end - start gives 2, because &packets[2] is two data_packet-sized chunks away from &packets[0].


Conclusion

You now have a complete picture of how struct pointers behave. The logic is consistent with all other pointer types in C, but the "gotcha" that interviewers look for is whether you connect it to the struct's total size, including padding.

Key Takeaways:

  • Pointer arithmetic on a struct pointer is scaled by the total size of the struct (sizeof(struct)), including all internal and trailing padding.
  • The expression ptr + n, where ptr is a struct S*, computes the address (address of ptr) + n * sizeof(struct S).
  • As a result, optimizing a struct's layout by reordering members changes the step size for pointer arithmetic.
  • Subtracting two struct pointers (ptr2 - ptr1) yields the number of elements between them, not the byte difference.

In our next lesson, we will transition to the final module on low-level data manipulation. We'll start with bit-fields, which allow you to define struct members that are smaller than a byte. This technique is indispensable for modeling hardware registers from a datasheet, a common task in your line of work with AUTOSAR and microcontroller-level development.