Pointers to Structures in C

In the programming paradigm of C, structures (struct) are powerful tools for organizing related data types, while structure pointers serve as the key to efficiently accessing and manipulating these complex data structures. Mastering the use of structure pointers not only aids in writing high-performance code but is also an essential step toward advanced language development. This article will guide you through a deep dive into the core concepts of structure pointers, their memory layout principles, and practical application techniques.
To understand structure pointers, one must first be familiar with their declaration methods and basic arithmetic rules. When we need to point to a variable containing a structure, we simply add an asterisk (*) after the type and use the & operator before the variable name to obtain its address. For example, defining a student information structure that contains two integers:

struct Student {
    int id;
    char name[20];
};

// Declare a pointer to a structure
struct Student *pStudent = &student1; // student1 is an already defined struct Student variable

Through the dereferencing operator ->, we can directly access members of the structure pointed to by this pointer. This differs from accessing ordinary arrays or basic type variables: for regular integers, we use square brackets [ ] subscripts; whereas for structure pointers, we must combine the arrow operator with a dot (.) to extract specific field values.

printf("%d\n", pStudent->id);      // Output the value of id
strcpy(pStudent->name, "Alice");   // Modify the content of name

This operational approach allows us to flexibly read or update any field within a structure without copying the entire block of data, significantly improving memory utilization.

Dynamic Allocation and Pointer Chain Processing

In large-scale projects, data structures are often not statically defined but require extension based on needs at runtime. In such scenarios, functions like malloc(), calloc(), and realloc() become indispensable tools. Through structure pointers, we can easily create new nodes in the heap and construct dynamic collection structures such as linked lists.

Here is an example of inserting a node into a typical singly linked list:

struct Node {
    int data;
    struct Node *next; // Pointer to the next node
};

// Allocate memory for a new node
Node* newNode(int val) {
    Node* node = (Node*)malloc(sizeof(Node));
    if (!node) return NULL; // Check if space was successfully allocated
    
    node->data = val;
    node->next = NULL;     // Initialize to null pointer
    return node;
}

// Insert at the head of the list
void insertAtHead(Node** headRef, int new_data) {
    Node* new_node = newNode(new_data);
    
    if (*headRef == NULL) {
        *headRef = new_node; // Head insertion: directly assign to head pointer
        return;
    }

    new_node->next = *headRef;
    *headRef = new_node;     // Update the linked list head to point to the new node
}

In this example, Node* head serves as the entry point for the linked list. Each insertion operation is implemented by modifying the reference of the head pointer itself. This mechanism makes data structure extension highly flexible but also requires developers to constantly monitor memory leak issues—specifically, calling free() after dynamic allocation has been used; otherwise, it may lead to program crashes or resource exhaustion.

Function Parameter Passing and Return Value Optimization

Structure pointers play a crucial role in function programming, especially when handling large data volumes. Compared with passing an entire structure copy by value (which can result in significant overhead), using pointer parameters allows functions to operate directly on the original data, thereby significantly enhancing performance. Furthermore, returning a pointer to a structure enables callers to obtain the address of newly generated objects without manually allocating memory themselves.

Consider a function that calculates the dot product of two vectors:

struct Vector {
    float x;
    float y;
};

float dot_product(struct Vector *v1, struct Vector *v2) {
    return v1->x * v2->x + v1->y * v2->y; // Directly access members to avoid copying the structure
}

If we were to pass struct Vector as a parameter by value (i.e., copy-by-value), every call would generate two complete vector data copies. In contrast, the implementation above achieves efficient computation using only two pointers. This not only reduces CPU burden but also avoids potential memory waste. Additionally, when modifying structure content within a function, since all operations are performed based on addresses, changes reflect immediately in the original variables, demonstrating the advantage of "reference semantics."

Wild Pointer Prevention and Best Practices

Although structure pointers are powerful, improper usage can easily trigger hard-to-track runtime errors, with dangling pointers (dangling pointer) and null pointer dereferencing being the most common issues. To avoid these problems, developers should adhere to the following guidelines: always initialize pointer variables; check if they are NULL before use; and promptly release memory resources when no longer needed.

// Example of incorrect practice
struct Student *p = (struct Student*)malloc(sizeof(struct Student));
*p->id = 10; // ❌ Error! p is not assigned, leading to wild pointer access of an invalid address

// Correct initialization method
struct Student *p = NULL;
if ((p = malloc(sizeof(*p))) != NULL) {
    p->id = 10;       // ✅ Safe: check before use
} else {
    fprintf(stderr, "Memory allocation failed\n");
}

// Must set the pointer to NULL after freeing memory to prevent accidental misuse later
free(p);
p = NULL;

Additionally, when dealing with nested structures or complex hierarchies, accuracy in accessing deep-level members must be ensured. For example:

struct Address {
    char city[50];
};

struct Person {
    struct Address addr; // Embedded structure
    int age;
};

// Correct access to a nested member: p->addr.city[...]
printf("%s\n", (*p).addr.city);   // ⚠️ While feasible, using the -> operator is recommended for clarity

Summary and Outlook

Structure pointers act as the bridge connecting data organization forms with memory management capabilities in C. They support efficient local field access while possessing the ability to construct complex dynamic data structures, making them one of the core skills in modern system programming. By reasonably utilizing dereferencing, dynamic allocation, and function passing mechanisms, developers can implement high-performance data processing logic while ensuring code safety. With the continuous growth of demands for embedded systems and low-level development, mastering structure pointers will become an essential competency for every C language practitioner.