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

Search WeChat
Chip 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:
-
Open the outer doll (second-level pointer p2) to get the middle doll (first-level pointer p1);
-
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“!