Practical Insights on C Language: Pointers to Pointers

Scan the code to follow Chip Dynamics and say goodbye to “chip” congestion!

Practical Insights on C Language: Pointers to PointersPractical Insights on C Language: Pointers to PointersSearch WeChatPractical Insights on C Language: Pointers to PointersChip Dynamics

Imagine opening a Russian nesting doll, and inside is an even smaller doll, and then another… The concept of a pointer to a pointer in C language (double pointer) is like this nesting doll game! Each layer of pointer takes you deeper into the maze of memory.

What is a pointer to a pointer?

A pointer to a pointer sounds a bit convoluted, right? Simply put, it is a pointer, but its “pointing object” is not ordinary data (like int or char), but another pointer (a first-level pointer). In other words, a second-level pointer stores the “address of a first-level pointer”, like a “pointer within a pointer” (commonly known as a double pointer). For example:

int num = 42;      // An integer
int *p = #     // First-level pointer, pointing to the integer
int **pp = &p;     // Second-level pointer, pointing to a pointer

Here, p2 is a second-level pointer:

  • p1 is a first-level pointer, pointing to the address of a;

  • p2 is a second-level pointer, pointing to the address of p1 (i.e., “the address of the pointer”).

Memory layout analysis:

num (42)    <- p    <- pp
0x1000      0x2000   0x3000
  • pp stores the address of p (0x2000)

  • p stores the address of num (0x1000)

  • num stores the actual value 42

Access methods:

  • *pp gets the value of p (0x1000)

  • **pp gets the value of num (42)

Underlying principles of second-level pointers

To thoroughly understand second-level pointers, one must comprehend the “three-layer structure” of memory:

Memory Level

Stored Content

Access Method

Data Layer

Actual data (like int a)

a or *p1

Pointer Layer

First-level pointer (stores data address)

p1 or *p2

Second-level Pointer Layer

Second-level pointer (stores pointer address)

p2 or **p3 (if there is a third-level pointer)

Using the “Russian nesting doll” analogy:

  • The data layer is the “innermost doll” (actual data);

  • The pointer layer is the “middle doll” (containing the address of the inner doll);

  • The second-level pointer layer is the “outer doll” (containing the address of the middle doll).

To access the innermost doll (data), you need to:

  1. Open the outer doll (second-level pointer p2) to get the middle doll (first-level pointer p1);

  2. Open the middle doll (first-level pointer p1) to get the innermost doll (data a).

Code verification: Print addresses at each level

int a = 100;
int* p1 = & a;
int** p2 = & p1; // 0x7ffff7a64550
printf("Data layer (a) address: %p", (void*)& a); // 0x7ffff7a64548
printf("Pointer layer (p1) address: %p", (void*)& p1);  // 0x7ffff7a64540
printf("Second-level pointer layer (p2) address: %p", (void*)& p2); // 100 (equivalent to a)
printf("Data pointed by p1: %d", *p1); // 0x7ffff7a64550 (address of p1)
printf("Pointer pointed by p2 (p1) value: %p", (void*)*p2);  // 100 (equivalent to *p1, i.e., a)
printf("Data indirectly pointed by p2: %d", **p2);  

Use cases for second-level pointersScenario 1: Dynamically modifying pointer direction

If you need to modify a pointer’s direction within a function (for example, to make the caller’s pointer point to newly allocated memory), you must use a second-level pointer—because the function parameter passes a copy of the pointer, directly passing a first-level pointer cannot modify the original pointer’s direction.

For example, “dynamically allocating a string”:

// Function: Modify the passed pointer to point to a newly allocated string
void set_string(char** p_str, const char* new_str) {
    // Free original memory (if any)
    if (*p_str != NULL) {
        free(*p_str);
    }
    // Allocate new memory and copy the string
    *p_str = (char*)malloc(strlen(new_str) + 1);
    strcpy(*p_str, new_str);
}
int main() {
    char* str = NULL;  // Initially a null pointer
    // Call function, passing the second-level pointer (& str) to modify str's direction
    set_string(& str, "hello");
    printf("%s", str);  // Output hello
    set_string(& str, "world");  // Modify direction again
    printf("%s", str);  // Output world
    free(str);  // Free final memory
    str = NULL;
    return 0;
}

Here, the set_string function receives a second-level pointer char** p_str, modifying the direction of str in the caller function through *p_str.

Scenario 2: Operating dynamic two-dimensional arrays

C language does not have native two-dimensional arrays (a two-dimensional array is essentially a combination of several one-dimensional arrays), but can simulate using “pointer to an array of pointers” (i.e., second-level pointers). For example, a 3×3 matrix can be represented as int** matrix, where matrix is a second-level pointer pointing to an array of pointers (each element is a first-level pointer pointing to a row of data).

int main() {
    int rows = 3, cols = 3;
    int** matrix = (int**)malloc(rows * sizeof(int*));  // Second-level pointer pointing to an array of pointers
    // Allocate memory for each row and initialize data
    for (int i = 0; i < rows; i++) {
        matrix[i] = (int*)malloc(cols * sizeof(int));
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j;  // Fill data (0,1,2; 3,4,5; 6,7,8)
        }
    }
    // Print matrix
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("");
    }
    // Free memory (first free each row, then free the array of pointers)
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);
    return 0;
}

Here, matrix is a second-level pointer, matrix[i] is a first-level pointer (pointing to the data of the i-th row), and matrix[i][j] is the data of the i-th row and j-th column—the second-level pointer perfectly simulates the hierarchical structure of a two-dimensional array!

Scenario 3: Implementing insertion operations in a “linked list”

A linked list is a classic data structure in C language, where each node contains data and a pointer to the next node. To insert a new node in a linked list, you need to modify the next pointer of the previous node—at this point, a second-level pointer allows you to directly manipulate the next pointer of the previous node (without traversing the linked list).

For example, a simplified “linked list insertion”:

// Define linked list node
typedef struct Node {
    int data;
    struct Node* next;
} Node;
// Insert new node at specified position in linked list (pos=0 means before head node)
void insert_node(Node** head, int pos, int data) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    new_node->data = data;
    new_node->next = NULL;
    if (pos == 0) {
        new_node->next = *head;  // New node's next points to original head node
        *head = new_node;  // Head pointer points to new node (modifying original head pointer through second-level pointer)
        return;
    }
    // Traverse to find the previous node at the insertion position (simplified logic, assuming pos is valid)
    Node* current = *head;
    for (int i = 0; i < pos - 1; i++) {
        current = current->next;
    }
    new_node->next = current->next;  // New node's next points to original next node
    current->next = new_node;  // Previous node's next points to new node
}
int main() {
    Node* head = NULL;  // Initially empty linked list
    insert_node(&head, 0, 100);  // Insert 100 at head → Linked list: 100
    insert_node(&head, 1, 200);  // Insert 200 at tail → Linked list: 100→200
    insert_node(&head, 1, 150);  // Insert 150 in the middle → Linked list: 100→150→200
    // Print linked list
    Node* current = head;
    while (current != NULL) {
        printf("%d→", current->data);
        current = current->next;
    }
    return 0;
}

Last issue answer example

#include <stdio.h>
int board[8][8] = {0};
int main(void) {
    int startx, starty;
    int i, j;
    printf("Input starting point:");
    scanf("%d %d", &startx, &starty);
    if(travel(startx, starty)) {
        printf("Travel completed!\n");
    }
    else {
        printf("Travel failed!\n");
    }
    for(i = 0; i < 8; i++) {
        for(j = 0; j < 8; j++) {
            printf("%2d ", board[i][j]);
        }
        putchar('\n');
    }
    return 0;
}
int travel(int x, int y) {
    // Corresponding to the eight directions a knight can move
    int ktmove1[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
    int ktmove2[8] = {1, 2, 2, 1, -1, -2, -2, -1};
    // Test the next step's exit
    int nexti[8] = {0};
    int nextj[8] = {0};
    // Record the number of exits
    int exists[8] = {0};
    int i, j, k, m, l;
    int tmpi, tmpj;
    int count, min, tmp;
    i = x;
    j = y;
    board[i][j] = 1;
    for(m = 2; m <= 64; m++) {
        for(l = 0; l < 8; l++)
            exists[l] = 0;
        l = 0;
        // Try the eight directions
        for(k = 0; k < 8; k++) {
            tmpi = i + ktmove1[k];
            tmpj = j + ktmove2[k];
            // If it's a boundary, cannot move
            if(tmpi < 0 || tmpj < 0 || tmpi > 7 || tmpj > 7)
                continue;
            // If this direction is walkable, record it
            if(board[tmpi][tmpj] == 0) {
                nexti[l] = tmpi;
                nextj[l] = tmpj;
                // Increase the number of walkable directions
                l++;
            }
        }
        count = l;
        // If there are no walkable directions, return
        if(count == 0) {
            return 0;
        }
        else if(count == 1) {
            // Only one walkable direction
            // So it is directly the direction with the least exit
            min = 0;
        }
        else {
            // Find the number of exits for the next position
            for(l = 0; l < count; l++) {
                for(k = 0; k < 8; k++) {
                    tmpi = nexti[l] + ktmove1[k];
                    tmpj = nextj[l] + ktmove2[k];
                    if(tmpi < 0 || tmpj < 0 ||
                        tmpi > 7 || tmpj > 7) {
                        continue;
                    }
                    if(board[tmpi][tmpj] == 0)
                        exists[l]++;
                }
            }
            tmp = exists[0];
            min = 0;
            // Find the direction with the least exit from the walkable directions
            for(l = 1; l < count; l++) {
                if(exists[l] < tmp) {
                    tmp = exists[l];
                    min = l;
                }
            }
        }
        // Move in the direction with the least exit
        i = nexti[min];
        j = nextj[min];
        board[i][j] = m;
    }
    return 1;
}

If you find the article good, click Like“, Share“, Recommend“!

Leave a Comment