How Pointers to Structures Work in C

4

You can point to almost anything in C. The language lets you create pointers for primitive types, arrays, functions, and user-defined types. But pointers to structures are where things get interesting. They are extremely common. If you are writing C code, you will likely encounter them often.

Here is a basic example. It shows how you define a structure and then create a pointer to it.

This snippet does more than just assign a memory address. It establishes a relationship between the variable ptr and the struct p1. The pointer holds the address of the structure. It allows you to access the members x and y indirectly. This is a fundamental pattern. You see it in linked lists, dynamic memory allocation, and function arguments.

Why Use Pointers to Structures?

Copying entire structures is expensive. If your struct contains a lot of data, passing it by value to a function means the compiler makes a full copy. This wastes time and memory. Using a pointer avoids the copy. You pass the address instead. The function can modify the original data without overhead.

There is also the question of dynamic sizing. Arrays have fixed sizes at compile time. Structures don’t necessarily solve the dynamic problem on their own. But when you combine a struct with malloc, you get flexible data storage. You can allocate a structure on the heap. A pointer to that structure lets you manage its lifetime manually.

Accessing Members via Pointers

Once you have a pointer, you need to access the data inside. You cannot use the dot operator. The expression ptr.x will fail to compile. ptr is an address, not the structure itself. You must dereference it first.

There are two ways to do this. The first is verbose.

The parentheses are mandatory here. The dot operator has higher precedence than the dereference operator. Without them, the compiler tries to access ptr.x, which doesn’t exist.

The second way is cleaner. C provides the arrow operator ->. It combines dereferencing and member access into one step.

This syntax is standard. It is what you will see in real codebases. It reads naturally as “go to the object pointed to by ptr and get its x member.” It is concise. It reduces visual clutter.

Common Pitfalls

Null pointers are a risk. If you declare a pointer but don’t initialize it, it points to garbage. Accessing ptr->x on a null pointer causes a segmentation fault. You must check for null before using the pointer.

This check is not optional in robust code. It prevents crashes.

Another issue is memory leaks. If you allocate memory with malloc and lose the pointer, that memory stays reserved until the program ends. You need to keep track of every allocation. free() is your responsibility. Forget it

Pointers to structures in C often trip up developers who are comfortable with basic pointers but get tangled in syntax. Consider a simple record definition for a user profile.

typedef struct {
char name[21];
char city[21];
char state[3];
} Rec;

typedef Rec *RecPointer;

Here, RecPointer is just an alias for a pointer to a Rec structure. When you declare a variable of this type, you are dealing with a memory address, not the data itself.

RecPointer r;

The variable r occupies four bytes on a 32-bit system (or eight on 64-bit). It is just a pointer. It does not contain the name, city, or state. To store actual data, you must allocate memory on the heap.

r = (RecPointer)malloc(sizeof(Rec));

This malloc call reserves 45 bytes. That is 21 bytes for the name, 21 for the city, and 3 for the state, plus one byte for padding or alignment to meet memory boundaries. Now r points to a valid block of memory that behaves exactly like a Rec structure.

Accessing Members via Dereference

To interact with the data, you must dereference the pointer. This is where precedence errors creep in. You can access members using the standard dot notation, but you must wrap the dereference in parentheses.

strcpy((r).name, “Leigh”);
strcpy((
r).city, “Raleigh”);
strcpy((*r).state, “NC”);

Notice the syntax. (*r).name is correct. If you write *r.name, it fails to compile. Why? Because the dot operator has higher precedence than the dereference operator. The compiler interprets *r.name as *(r.name). Since r is a pointer, r.name is invalid syntax, and the expression collapses. The parentheses force the dereference *r to happen first, yielding the structure, and then the dot operator accesses the name field.

It is tedious to type. It looks cluttered. It invites errors.

The Arrow Notation

C provides a cleaner way to handle this. The arrow operator -> is syntactic sugar for (*pointer).member. It is not a different mechanism. It is not a new operator that changes how memory is accessed. It is simply a shorter way to write the dereference and member access in one step.

strcpy(r->name, “Leigh”);

This is identical to strcpy((*r).name, "Leigh"). It saves two characters. It removes the need for nested parentheses. It is the standard way most C developers interact with structure pointers.

Memory Management Implications

The free(r) call is mandatory. The memory was allocated from the heap. If you do not release it, it leaks. The pointer r itself, the four-byte variable holding the address, is local to the stack frame or global scope, but the data it points to lives elsewhere.

When you use r->name, you are modifying the data at the address stored in r. The pointer r remains unchanged. The address does

Allocating memory for arrays on the fly is a staple of C programming, but it requires understanding how pointers interact with raw memory blocks. When you need a fixed-size array that isn’t known at compile time, standard stack allocation won’t cut it. You have to reach for the heap.

The code snippet below demonstrates one common pattern:

Here, malloc reserves space for ten integers. The cast to (int *) ensures the pointer matches the expected type, even though modern C compilers often warn against explicit casts for void*. The loop then initializes each element to zero using subscript notation. Finally, free releases the memory back to the system.

But this isn’t the only way to write it.

You can achieve the exact same result by swapping out p[i] for pointer arithmetic:

Why does this matter? Because p[i] is just syntactic sugar for *(p+i). The compiler treats them identically. If you’re working with embedded systems or writing tight loops where every cycle counts, knowing that these are interchangeable helps you read legacy code—and write your own without confusion.

Still, there’s a subtle trap.

If you declare a pointer to an array type directly—like int (*p)[10] —you’re dealing with a different beast entirely. That pointer points to the entire array, not just the first element. Incrementing it moves the pointer by the size of the whole array, not a single integer. Most developers stick with int * because it’s simpler and more flexible.

When to Use Which Approach

Choosing between subscript notation and explicit pointer arithmetic often comes down to readability and intent.

  • Use p[i] when you want to emphasize index-based access. It’s clearer for most readers.
  • Use *(p+i) when you’re doing low-level memory manipulation or need to avoid array decay quirks in complex expressions.

Both approaches require careful memory management. Forget free, and you leak. Call it too early, and you’ll hit undefined behavior. And while malloc is straightforward here, always verify the return value isn’t NULL before dereferencing.

“Pointers to arrays are powerful, but they demand discipline. One misstep and you’re reading garbage—or worse, corrupting another variable’s memory.”

In practice, most developers rarely need to write raw pointer arithmetic for simple arrays. Libraries like std::vector in C++ or higher-level abstractions in other languages handle this automatically. But in C? You’re on your own.

And that’s why understanding the mechanics matters. Not just for passing interviews or writing textbooks, but for when the code breaks at 2 AM and you need to know exactly what’s happening in memory.

When you declare a pointer to an integer array, you are not creating anything exotic. It is just a standard pointer to an int. The magic happens with malloc. You allocate a block of memory large enough for however many integers you need. The pointer then targets the first element of that block.

C does not care how you access it. You can use square brackets like p[5] or use pointer arithmetic like *(p + 5). The compiler treats them as identical. This flexibility is why dynamic arrays are so useful for strings. You do not guess the size. You allocate exactly enough storage for the string length plus the null terminator.

Arrays of Pointers vs. Arrays of Structures

Why use an array of pointers when you could just use an array of structs? Space. Or rather, the lack of it.

Consider a structure Rec with three character arrays of 81 bytes each. That is 243 bytes per record. If you declare Rec records[10], you instantly reserve 2,430 bytes in memory. All of it. Even if you only ever use one record.

An array of pointers changes the math.

The array a itself only holds 10 pointers. On a 64-bit system, that is 80 bytes. That is a fraction of the memory required for the full structures. The memory for the actual records stays unused until you need it.

You can allocate a single record on demand.

This pattern solves memory-intensive problems by deferring allocation. You only pay for what you use. When you are done with the record, you call free. The pointer becomes a dangling reference, but the memory is returned to the system.

Structures Containing Pointers

Structures can hold pointers. This allows you to mix fixed-size data with variable-size data in the same object.

Take an address book entry. The name, city, and phone number might have reasonable maximum lengths. A comment, however, could be anything from a single word to a novel. You do not want to waste space allocating a huge buffer for every entry just in case one person writes a long comment.

The Addr struct itself is small

How Comment Fields Handle Empty vs. Populated Records

Not every record in the database carries a comment. When a field is left blank, it doesn’t sit empty. It holds a pointer. Specifically, a 4-byte pointer that points to nothing of substance. The system treats this absence as a valid state. The record is still complete. The metadata is intact.

But what happens when a user actually types something?

The allocation changes. The database doesn’t reserve a fixed buffer for these comments. It doesn’t guess at a maximum length and pad the rest with null bytes. That would waste space. Instead, the system calculates the exact string length. Then it allocates exactly that many bytes.

This dynamic allocation is efficient. It prevents fragmentation from bloating. A short note takes a few bytes. A long essay takes more. The pointer in the blank record points to a null reference, keeping the footprint minimal. The records with content stretch to fit the data. Nothing is wasted. Nothing is forced.

Is this the only way to store text? No. But it is a clever way to balance speed and space. You get the responsiveness of fixed-size headers with the flexibility of variable-length data. It’s a small detail. A low-level mechanic. But it adds up when you’re managing millions of records. The database breathes easier. The disk usage stays lean.

And the user? They never see the pointer. They just see their comment. Or the lack of one. The complexity is hidden. The storage is optimized. The result is a system that feels light, even when the data grows.