第三章:多重指针(指针的指针…)
在C语言中,多重指针是一个非常重要且经常使用的概念。通过理解和正确使用多重指针,您可以高效地处理复杂的数据结构并提高程序的灵活性。
1. 指针的指针概念
指针的指针,顾名思义,就是指向一个指针的指针。其通常用于间接访问或修改数据。
基本定义与声明
在C中,定义一个指向指针的指针非常简单。一个指向int
类型数据的指针的指针可以这样定义:
int **ptr_to_ptr;
ptr_to_ptr
是一个二级指针,它可以存储一个指向int
类型数据的指针的地址。例如,您可以有这样一段代码:
int a = 10;
int *ptr = &a;
int **ptr_to_ptr = &ptr;
这里,ptr
是一个指向a
的指针,而ptr_to_ptr
是一个指向ptr
的指针。
示例代码:函数参数中的指针的指针
在函数参数中使用指针的指针可以让函数修改调用者变量的内容。例如,这段代码展示了如何通过指针的指针交换两个整数的值:
#include <stdio.h>void swap(int **x, int **y) { // [1]int *temp = *x; // [2]*x = *y;*y = temp;
}int main() {int a = 20, b = 30;int *ptr_a = &a; // [3]int *ptr_b = &b;printf("Before swap: ptr_a points to %d, ptr_b points to %d\n", *ptr_a, *ptr_b);swap(&ptr_a, &ptr_b); // [4]printf("After swap: ptr_a points to %d, ptr_b points to %d\n", *ptr_a, *ptr_b);return 0;
}
详细讲解
-
swap
函数定义:void swap(int **x, int **y) { // [1]
- 说明:函数
swap
的参数是指向指针的指针int **x
和int **y
,这意味着它可以修改指针变量本身(不仅仅是指针指向的值)。
- 说明:函数
-
交换指针指向的内容:
int *temp = *x; // [2] *x = *y; *y = temp;
- 说明:
- 定义一个临时指针
temp
并让它指向x
指向的内容。 - 然后将
x
指向的内容改为y
指向的内容,将y
指向的内容改为temp
指向的内容,从而实现了交换。
- 定义一个临时指针
- 说明:
-
定义指向变量的指针:
int *ptr_a = &a; // [3] int *ptr_b = &b;
- 说明:
ptr_a
是指向整数变量a
的指针,ptr_b
是指向整数变量b
的指针。
- 说明:
-
调用
swap
函数:swap(&ptr_a, &ptr_b); // [4]
- 说明:传递指针的地址(即指针的指针)给函数
swap
,使得函数可以改变调用者的指针变量,使它们交换所指向的地址。
- 说明:传递指针的地址(即指针的指针)给函数
总结
使用指针的指针作为函数参数,可以让函数直接操作并修改调用者的指针变量,而不仅仅是指向的值。这在需要在函数中改变某个指针所指向的目标时非常有用。在这个示例中,通过传递指针的地址,swap
函数成功地交换了两个指针所指向的地址,实现了交换两个整型值变量的目的。
2. 动态二维数组的实现与访问
动态分配二维数组
在某些情况下,您需要动态分配内存来存储矩阵或其他二维数据结构。我们可以通过指向指针的指针来实现这一点。
示例代码:创建和访问动态二维数组
以下代码展示了如何创建和访问一个动态分配的二维数组:
#include <stdio.h>
#include <stdlib.h>int main() {int rows = 3, cols = 4;int **array = (int **)malloc(rows * sizeof(int *)); // [1]for (int i = 0; i < rows; i++) {array[i] = (int *)malloc(cols * sizeof(int)); // [2]}// 初始化二维数组 [3]for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {array[i][j] = i * cols + j;}}// 打印二维数组 [4]for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {printf("%d ", array[i][j]);}printf("\n");}// 释放内存 [5]for (int i = 0; i < rows; i++) {free(array[i]);}free(array);return 0;
}
这个例子展示了如何动态分配一个二维整数数组,并如何通过指针访问和操作这个数组。
-
[1] 动态分配第一维:使用
malloc
为指针数组分配内存,每个元素将指向二维数组的一行。此处的array
是一个指向指针的指针,分配的内存大小是rows
乘以指针大小(sizeof(int *)
)。 -
[2] 动态分配二维数组的每一行:在循环中为每行分配内存。
array[i]
将指向每行的起始位置,分配大小为列数乘以每个元素的大小(cols * sizeof(int)
)。 -
[3] 初始化二维数组:嵌套的
for
循环遍历数组的每个元素进行初始化。这里的初始化方案是array[i][j] = i * cols + j
,用行号乘以列数加上列号来赋值。 -
[4] 打印二维数组:同样使用嵌套
for
循环遍历并打印二维数组中的每个元素。 -
[5] 释放内存:使用
free
函数首先释放每一行分配的内存,然后再释放指针数组本身的内存。这一步骤非常重要,以防止内存泄漏。
通过这种方式,我们能够在运行时创建大小灵活的二维数组,非常适合在事先不知道确切数组大小的情况下使用。
3. 复杂数据结构中的指针的指针
示例代码:链表中指针的指针用于插入操作
在链表中指针的指针经常用于插入操作,以便在合适的位置插入新节点:
#include <stdio.h>
#include <stdlib.h>// [1]
typedef struct Node {int data;struct Node *next;
} Node;// [2]
void insert(Node **head, int data) {Node *new_node = (Node *)malloc(sizeof(Node));new_node->data = data;new_node->next = *head;*head = new_node;
}void printList(Node *head) {Node *temp = head;while (temp != NULL) {printf("%d -> ", temp->data);temp = temp->next; // [3]}printf("NULL\n");
}// [4]
int main() {Node *head = NULL; insert(&head, 1); insert(&head, 2);insert(&head, 3);printList(head);// 释放内存 Node *temp;while (head != NULL) {temp = head;head = head->next;free(temp);}return 0;
}
在这个例子中,insert
函数通过指针的指针修改链表头指针,使得新节点插入到链表的最前面。
- 作用:用于将新节点插入到链表头部。
- 特点:
- 灵活性高:使用指针的指针可以方便地进行节点插入操作,不需要区分链表是否为空。
- 安全性好:能够有效防止直接操作头指针带来的潜在错误。
- 生命周期:随着链表的增长和变量的作用范围而变化。
详细讲解:
-
节点结构的定义:
typedef struct Node {int data;struct Node *next; } Node;
定义了一个链表节点结构
Node
,包含一个整数数据data
和一个指向下一个节点的指针next
。 -
插入函数:
void insert(Node **head, int data) {Node *new_node = (Node *)malloc(sizeof(Node));new_node->data = data;new_node->next = *head;*head = new_node; }
insert
函数接受指向指针的指针Node **head
和要插入的数据int data
。函数内通过malloc
分配新节点的内存,并将新节点插入到链表的头部。 -
链表的遍历与打印:
void printList(Node *head) {Node *temp = head;while (temp != NULL) {printf("%d -> ", temp->data);temp = temp->next;}printf("NULL\n"); }
printList
函数遍历链表并打印每个节点的值,直到链表末尾。 -
主函数:
int main() {Node *head = NULL;insert(&head, 1);insert(&head, 2);insert(&head, 3);printList(head);// 释放内存Node *temp;while (head != NULL) {temp = head;head = head->next;free(temp);}return 0; }
在
main
函数中,首先定义头指针并初始化为空。然后,通过调用insert
函数插入新节点,并打印链表的所有节点值。最后,释放链表中分配的所有内存。
实际应用场景分析
指针的指针在以下场景中比较常用:
- 多级数据结构:如动态二维数组和多级链表。
- 动态内存管理:在需要动态分配和管理复杂数据结构的情况下,指针的指针提供了灵活性。
- 通过函数传递多个返回值:指针的指针能够让函数同时修改多个变量的值。
多级数据结构
示例:动态二维数组的更高效分配
动态二维数组的分配和管理是一个常见的多级数据结构应用场景。假设我们需要管理一个10x10的二维整数数组:
#include <stdio.h>
#include <stdlib.h>int main() {int rows = 10, cols = 10;int **array = (int **)malloc(rows * sizeof(int *)); // [1]for (int i = 0; i < rows; i++) {array[i] = (int *)malloc(cols * sizeof(int)); // [2]}// 初始化数组 [3]for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {array[i][j] = i * cols + j;}}// 打印数组 [4]for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {printf("%d ", array[i][j]);}printf("\n");}// 释放内存 [5]for (int i = 0; i < rows; i++) {free(array[i]);}free(array);return 0;
}
在这个例子中,使用二级指针(int **array
)来管理动态二维数组,使得我们能够灵活地分配和释放内存。
-
[1]指针的指针:
int **array
表示一个指向int *
类型指针的指针,它用于存储指向多个一维数组(即每一行)的指针。 -
[1][2]内存分配:
- [1]:通过
malloc
函数为指针数组分配内存,存储rows
个int *
类型的指针。 - [2]:为每一行分配
cols
个整数(int
)的内存,每个指针指向一个包含cols
个整数的动态数组。
- [1]:通过
-
[3]初始化数组:
- 嵌套循环遍历每一行和每一列,并将数组元素初始化为
i * cols + j
,即通过行号和列号的计算得到初始值。
- 嵌套循环遍历每一行和每一列,并将数组元素初始化为
-
[4]打印数组:
- 嵌套循环遍历每一行和每一列,逐个打印数组元素。
-
[5]释放内存:
- 循环释放每一行的内存。
- 最后释放存储指针的数组。
这种方法通过分开分配每一行的内存,使得二维数组的管理更加灵活,也便于处理各种动态大小的二维数组。
通过这种动态分配与管理方法,能够有效处理二维数据结构,特别是在内存使用和复杂数据组织上提供了很高的灵活性。
动态内存管理
示例:基于指针的指针的动态内存池
动态内存池是一种高效的内存管理方法,特别适用于频繁的内存分配和释放操作。下面是一个简单的内存池实现:
#include <stdio.h>
#include <stdlib.h>#define POOL_SIZE 1024// [1]
typedef struct MemoryPool {char pool[POOL_SIZE];char *next;
} MemoryPool;// [2]
void MemoryPoolInit(MemoryPool *mp) {mp->next = mp->pool;
}// [3]
void *MemoryPoolAlloc(MemoryPool *mp, size_t size) {if (mp->next + size - mp->pool <= POOL_SIZE) {void *ptr = mp->next;mp->next += size;return ptr;} else {return NULL; // 内存不足}
}// [4]
void MemoryPoolReset(MemoryPool *mp) {mp->next = mp->pool;
}int main() {MemoryPool mp;MemoryPoolInit(&mp);int *a = (int *)MemoryPoolAlloc(&mp, sizeof(int));float *b = (float *)MemoryPoolAlloc(&mp, sizeof(float));if (a && b) {*a = 123;*b = 456.78;printf("a: %d, b: %.2f\n", *a, *b);} else {printf("Memory allocation failed\n");}MemoryPoolReset(&mp); // 重置内存池return 0;
}
这个例子展示了如何使用一个简单的内存池来优化内存管理。通过内存池,我们可以减少对malloc
和free
的调用,提高程序性能。
-
[1] MemoryPool 结构体定义:MemoryPool是一个结构体,包含一个字符数组
pool
和一个指向下一个可用内存位置的指针next
。pool
的大小由宏POOL_SIZE
定义,这里我们设置为1024字节。 -
[2] MemoryPoolInit 函数:该函数初始化MemoryPool。设置
next
指针指向pool
的开始位置。 -
[3] MemoryPoolAlloc 函数:该函数用于从内存池分配指定大小的内存空间。如果内存池剩余的空间足够,则返回指向分配内存的指针,并更新
next
指针的位置,否则返回NULL
。 -
[4] MemoryPoolReset 函数:该函数重置内存池,将
next
指针重新指向pool
的开始位置。这相当于“释放”了所有已分配的内存,使内存池可以重新使用。 -
总结:
- 动态内存池通过减少
malloc
和free
调用频率,提高内存分配和释放的效率。 MemoryPoolAlloc
函数检查内存池是否有足够空间,如果有则分配内存,否则返回NULL
。MemoryPoolReset
函数允许我们快速清空内存池,方便重复使用。
- 动态内存池通过减少
通过函数传递多个返回值
示例:函数返回多个值
通过指针,我们可以让函数返回多个值,例如返回两个整数的最大公约数和最小公倍数:
#include <stdio.h>void gcd_lcm(int a, int b, int *gcd, int *lcm) { // [1]int temp_a = a, temp_b = b;while (temp_b != 0) {int temp = temp_b;temp_b = temp_a % temp_b;temp_a = temp;}*gcd = temp_a; // [2]*lcm = (a * b) / (*gcd); // [3]
}int main() {int a = 12, b = 18;int gcd_val, lcm_val; // [4]gcd_lcm(a, b, &gcd_val, &lcm_val); // [5]printf("GCD of %d and %d is %d\n", a, b, gcd_val);printf("LCM of %d and %d is %d\n", a, b, lcm_val);return 0;
}
在这个例子中,gcd_lcm
函数通过指向整数的指针返回了最大公约数和最小公倍数。这种方法使得函数可以返回多个不同类型的值。
-
[1]函数定义:
void gcd_lcm(int a, int b, int *gcd, int *lcm)
,该函数接受四个参数:两个整数a
和b
,以及两个指向整数的指针gcd
和lcm
。通过这些指针的间接引用,函数可以改变它们指向的变量的值,从而实现返回多个值的效果。 -
[2]计算最大公约数:在函数体内,通过欧几里得算法计算最大公约数,并将结果存储在指针
gcd
所指向的内存位置,即*gcd = temp_a;
。 -
[3]计算最小公倍数:计算最小公倍数的公式是
(a * b) / gcd
,并将结果存储在指针lcm
所指向的内存位置,即*lcm = (a * b) / (*gcd);
。 -
[4]定义变量:在函数
main
中,定义两个整数变量gcd_val
和lcm_val
用于接收gcd_lcm
函数的结果。 -
[5]调用函数:通过传递
a
、b
和指向gcd_val
及lcm_val
的指针,调用gcd_lcm
函数。计算结果通过指针返回,分别存储在gcd_val
和lcm_val
中。
这种通过指针返回多个值的方式在C语言中非常常见和实用,特别是当需要返回多个结果时更加有效。
掌握指针的指针不仅可以帮助您处理复杂的内存管理操作,还可以提升程序的效率和灵活性。通过上述内容的学习,希望读者能够深入理解并灵活应用这一强大的C语言特性。
4. 三重指针的使用
我们来讨论一个引用层数达到三次或更高的真实案例。获知引用层数较高的情况通常出现在多级数据结构的应用中,如多维数组、图、树等。以下是一个堆(Heap)数据结构的简单实现,其中通过引用层数高达三次的指针来追踪堆节点。
示例代码:三重指针的使用
以下代码展示了如何使用三重指针来操作堆节点。堆是一种常见的数据结构,常用于实现优先队列以及各种其他算法。
#include <stdio.h>
#include <stdlib.h>// 堆节点的定义
typedef struct HeapNode {int value;struct HeapNode *left;struct HeapNode *right;
} HeapNode;// 创建新堆节点
HeapNode* createNode(int value) {HeapNode *newNode = (HeapNode *)malloc(sizeof(HeapNode));newNode->value = value;newNode->left = NULL;newNode->right = NULL;return newNode;
}// 插入节点到堆
void insertNode(HeapNode **root, int value) { // [1]if (*root == NULL) {*root = createNode(value);} else if (value < (*root)->value) {insertNode(&(*root)->left, value); // [2]} else {insertNode(&(*root)->right, value);}
}// 打印堆的前序遍历
void printPreOrder(HeapNode *root) {if (root != NULL) {printf("%d ", root->value);printPreOrder(root->left);printPreOrder(root->right);}
}int main() {HeapNode *root = NULL;insertNode(&root, 10); // [3]insertNode(&root, 5);insertNode(&root, 15);insertNode(&root, 2);insertNode(&root, 7);printf("Pre-order Traversal: ");printPreOrder(root); // [4]printf("\n");return 0;
}
- [1]函数参数中的双重指针:在函数
insertNode
中,参数HeapNode **root
是一个指向堆节点指针的指针。这允许我们在函数内部修改实际的堆节点指针。 - [2]递归调用中的三重指针:在递归调用中,
&(*root)->left
和&(*root)->right
是三重指针。它们将当前节点的子节点指针的地址传递给下一层递归调用,从而在子节点为空时创建新节点。 - [3]通过引用层数为二的指针插入节点:在
main
函数中,通过传递&root
给insertNode
函数,我们可以在堆中插入新节点。 - [4]打印堆:调用
printPreOrder
函数,以前序遍历打印堆节点值。
以上示例展示了如何通过递归和双重指针来间接使用三重指针操作复杂的数据结构。如堆、树等多级结构常常需要这种多重引用来管理元素间的关系。了解这些原理对高级数据结构和算法的实现非常有益。