青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-
友链 搜索文章 >>

青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-

【C语言学习笔记】十四、多级指针


阅读数: 0次    2026-03-14
字数:4.4k字 | 预计阅读时长:18分钟

1. 多级指针

指针在指向变量地址的同时,它自身也存在一个内存地址中,多级指针就是指向指针的指针,保存的是指针所在的内存地址。

例如:

1
2
3
int a = 10;	// 定义一个整数变量 a
int* p = &a; // 定义一个指针变量 p,指向 a 的地址
int** pp = &p; // 定义一个二级指针变量 pp,指向 p 的地址

2. 多级指针的用途

以一个简单的示例程序为例,可以使用工厂、批发商、零售商来作为初始变量、一级指针、二级指针。

工厂(Factory):生产商品,初始化一个初始的产品id和产品价格;

批发商(Wholesaler):从工厂获取产品,可能会更新产品的id和价格;

零售商(Retailer):从批发商处获取产品,可能会进一步更新产品的id和价格。

工厂只负责生产商品,并初始化产品的属性,批发商和零售商只能接收上级提供的产品,并更新它的属性。

**注意:**在创建产品时,不应该使用函数直接返回局部变量的地址,因为局部变量会在函数执行完毕后销毁,从而导致未定义行为。例如下面的错误示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ★错误示例★

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

// 商品结构体
typedef struct {
uint32_t id;
double price;
} Product;

// 创建商品函数
Product* create_product(uint32_t id, double price);

// 多级指针
int main() {
Product* product1 = create_product(1, 10.0);
printf("ID: %" PRIu32 ", Price: %.2f, Address: %p\n", product1->id, product1->price, (void*)product1);
printf("ID: %" PRIu32 ", Price: %.2f, Address: %p\n", product1->id, product1->price, (void*)product1);

return 0;
}

// 创建商品函数
Product* create_product(uint32_t id, double price) {
Product p;
p.id = id;
p.price = price;
return &p; // 返回局部变量的地址,这会导致未定义行为
}

运行这个错误的程序后可以发现,由于局部变量默认被分配到栈内存上,第一次使用printf打印的数据是正确的,但第二次打印的数据是无意义的。虽然第一次打印碰巧拿到的正确的值,但这个行为本身就是未定义的。

第一次printf之所以"幸运"地打印正确,是因为C语言在函数调用前会先对所有参数求值——值被读取后才执行printf。但printf的执行本身会复用那块栈内存,导致第二次读取时数据已被破坏。

在实际程序开发时必须避免这种行为。

如果需要将局部变量保存下来,需要为其分配堆内存,手动管理其生命周期。

正确示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>

// 商品结构体
typedef struct {
uint32_t id;
double price;
} Product;

// 创建商品函数
Product* create_product(uint32_t id, double price);

// 多级指针
int main() {
Product* product = create_product(1, 10.0);
printf("ID: %" PRIu32 ", Price: %.2f, Address: %p\n", product->id, product->price, (void*)product);
printf("ID: %" PRIu32 ", Price: %.2f, Address: %p\n", product->id, product->price, (void*)product);

free(product);

return EXIT_SUCCESS;
}

// 创建商品函数
Product* create_product(uint32_t id, double price) {
Product* p = (Product*)malloc(sizeof(Product));
if (p == NULL) {
perror("Failed to allocate memory");
exit(EXIT_FAILURE);
}
p->id = id;
p->price = price;
return p;
}

下面是工厂、批发商、零售商链路中,采用多级指针实现商品的传递和商品信息修改的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>

// 商品结构体
typedef struct {
uint32_t id;
double price;
} Product;

// 工厂创建商品
Product* create_product(uint32_t id, double price);

// 批发商更新商品
void wholesaler_update(Product** pp, uint32_t id, double price);

// 零售商更新商品
void retailer_update(Product*** ppp, uint32_t id, double price);

int main() {
// 工厂创建商品(一级指针)
Product* product = create_product(1, 10.0);
puts("工厂创建了一个商品:");
printf("ID: %" PRIu32 ", Price: %.2f\n", product->id, product->price);

// 批发商持有指向工厂指针的指针(二级指针)
Product** wholesaler_product = &product;
wholesaler_update(wholesaler_product, 2, 20.0);
puts("批发商更新了商品:");
printf("ID: %" PRIu32 ", Price: %.2f\n", (*wholesaler_product)->id, (*wholesaler_product)->price);

// 零售商持有指向批发商指针的指针(三级指针)
Product*** retailer_product = &wholesaler_product;
retailer_update(retailer_product, 3, 30.0);
puts("零售商更新了商品:");
printf("ID: %" PRIu32 ", Price: %.2f\n", (**retailer_product)->id, (**retailer_product)->price);

free(product);

return EXIT_SUCCESS;
}

// 工厂创建商品
Product* create_product(uint32_t id, double price) {
Product* p = (Product*)malloc(sizeof(Product));
if (p == NULL) {
perror("Failed to allocate memory");
exit(EXIT_FAILURE);
}
p->id = id;
p->price = price;
return p;
}

// 批发商更新商品
void wholesaler_update(Product** pp, uint32_t id, double price) {
if (pp == NULL || *pp == NULL) {
fprintf(stderr, "wholesaler_update: Invalid product pointer\n");
return;
}
(*pp)->id = id;
(*pp)->price = price;
}

// 零售商更新商品
void retailer_update(Product*** ppp, uint32_t id, double price) {
if (ppp == NULL || *ppp == NULL || **ppp == NULL) {
fprintf(stderr, "retailer_update: Invalid product pointer\n");
return;
}
(**ppp)->id = id;
(**ppp)->price = price;
}

其中需要注意的是,箭头运算符(->)可以解引用指针并访问其成员,也就是p->id完全等价于(*p).id,对于多级指针也是如此,例如(**ppp)->id完全等价于(***ppp).id。

3. 游戏案例:游戏服务器动态玩家列表管理之realloc与多级指针的应用

指针章节中提到,如果要在一个函数中实现修改变量值的功能,需要使用其指针,与这一点类似,在函数中如果需要修改一个指针指向的地址,需要向函数传递指向这个指针的二级指针。

realloc函数可以直接使用来分配内存空间,不必局限于在malloc函数之后使用。

案例:设计一个玩家列表,支持有新玩家加入时动态分配新的内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>

// 玩家结构体
typedef struct {
uint32_t id; // 玩家ID
char name[20]; // 玩家名字
} Player;

// 新增一名玩家到玩家列表中
// 直接传递结构体指针,避免不必要的结构体复制,提高效率
void addPlayer(Player** playerList, uint32_t* playerCount, Player* newPlayer);

// 打印玩家列表
void printPlayerList(Player* playerList, uint32_t playerCount);

int main() {
Player* playerList = NULL; // 初始化玩家列表指针为NULL
uint32_t playerCount = 0; // 玩家数量

// 模拟新增玩家
Player newPlayer1 = { 1, "Alice" };
addPlayer(&playerList, &playerCount, &newPlayer1);

Player newPlayer2 = { 2, "Bob" };
addPlayer(&playerList, &playerCount, &newPlayer2);

printPlayerList(playerList, playerCount); // 打印玩家列表

// 释放玩家列表的内存
free(playerList);

return EXIT_SUCCESS;
}

// 新增一名玩家到玩家列表中
void addPlayer(Player** playerList, uint32_t* playerCount, Player* newPlayer) {
Player* tempList = (Player*)realloc(*playerList, ((size_t)*playerCount + 1) * sizeof(Player)); // 重新分配内存空间以容纳新玩家
// 检查内存分配是否成功
if (tempList == NULL) {
fprintf(stderr, "内存分配失败\n");
// 内存分配失败时,需要保持原有玩家列表不变,不必释放原有内存,避免数据丢失
return;
}
*playerList = tempList; // 更新玩家列表指针
(*playerList)[*playerCount] = *newPlayer; // 将新玩家添加到列表中
printf("新增玩家: ID=%" PRIu32 ", 名字=%s\n", newPlayer->id, newPlayer->name);
(*playerCount)++; // 玩家数量增加
}

// 打印玩家列表
void printPlayerList(Player* playerList, uint32_t playerCount) {
puts("玩家列表:");
for (uint32_t i = 0; i < playerCount; i++) {
printf("玩家ID: %" PRIu32 ", 玩家名字: %s\n", playerList[i].id, playerList[i].name);
}
}

需要注意的是,读取当前列表并添加新玩家时,对当前列表的解引用必须加括号:

1
(*playerList)[*playerCount] = *newPlayer;

必须是(*playerList),而不是*playerList,因为下标运算符([])的优先级高于解引用运算符(*),而它是一个指针,必须先解引用才能访问数组元素,所以需要括号来确保正确的访问顺序。

4. 续上节:卫语句与log编写

上一节的程序还有一些优化空间,例如在函数中可以添加卫语句,提高逻辑严谨性;可以对程序的一些信息做log输出,方便调试。

下面是添加了卫语句与简单的log输出的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <stdio.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <time.h>

#define TIME_STRING_SIZE 26 // 时间字符串大小

// 玩家结构体
typedef struct {
uint32_t id; // 玩家ID
char name[20]; // 玩家名字
} Player;

// 新增一名玩家到玩家列表中
void addPlayer(Player** playerList, uint32_t* playerCount, Player* newPlayer);

// 日志记录函数
void logError(const char* message);
void logInfo(const char* message);
void logMessage(const char* level, const char* message);

// 打印玩家列表
void printPlayerList(Player* playerList, uint32_t playerCount);

int main() {
Player* playerList = NULL; // 初始化玩家列表指针为NULL
uint32_t playerCount = 0; // 玩家数量

// 模拟新增玩家
Player newPlayer1 = { 1, "Alice" };
addPlayer(&playerList, &playerCount, &newPlayer1);

Player newPlayer2 = { 2, "Bob" };
addPlayer(&playerList, &playerCount, &newPlayer2);

printPlayerList(playerList, playerCount); // 打印玩家列表

// 释放玩家列表的内存
free(playerList);

return EXIT_SUCCESS;
}

// 新增一名玩家到玩家列表中
void addPlayer(Player** playerList, uint32_t* playerCount, Player* newPlayer) {
Player* tempList = (Player*)realloc(*playerList, ((size_t)*playerCount + 1) * sizeof(Player)); // 重新分配内存空间以容纳新玩家
// 检查内存分配是否成功
if (tempList == NULL) {
logError("内存分配失败");
// 内存分配失败时,需要保持原有玩家列表不变,不必释放原有内存,避免数据丢失
return;
}
*playerList = tempList; // 更新玩家列表指针
(*playerList)[*playerCount] = *newPlayer; // 将新玩家添加到列表中
char addMsg[64];
snprintf(addMsg, sizeof(addMsg), "新增玩家: ID=%" PRIu32 ", 名字=%s", newPlayer->id, newPlayer->name);
logInfo(addMsg);
(*playerCount)++; // 玩家数量增加
}

// 打印玩家列表
void printPlayerList(Player* playerList, uint32_t playerCount) {
logInfo("玩家列表:");
for (uint32_t i = 0; i < playerCount; i++) {
char playerMsg[64];
snprintf(playerMsg, sizeof(playerMsg), "玩家ID: %" PRIu32 ", 玩家名字: %s", playerList[i].id, playerList[i].name);
logInfo(playerMsg);
}
}

// 日志记录函数
void logError(const char* message) {
logMessage("ERROR", message);
}
void logInfo(const char* message) {
logMessage("INFO", message);
}
void logMessage(const char* level, const char* message) {
time_t now = time(NULL);
char timeString[TIME_STRING_SIZE];
struct tm localTime;
if (localtime_s(&localTime, &now) == 0) {
strftime(timeString, TIME_STRING_SIZE, "%Y-%m-%d %H:%M:%S", &localTime);
printf("[%s] [%s] %s\n", timeString, level, message);
}
else {
printf("[Unknown Time] [%s] %s\n", level, message);
}
}

snprintf是printf家族中专门用于将格式化内容写入字符串缓冲区的函数。与printf直接输出到终端不同,它的第一个参数是目标缓冲区,第二个参数是缓冲区的最大字节数(含\0),后续参数与printf完全一致。正是这个长度限制使它比sprintf更安全——即使格式化后的内容超出缓冲区容量,它也只会写入指定的字节数并自动在末尾补上\0,不会发生溢出。它的返回值是理论上应写入的字符数(不含\0),若返回值大于等于缓冲区大小,说明内容被截断了,可以据此进行检测。

有关time.h与时间戳的使用与处理可以参考第十二章第11节的内容(【C语言学习笔记】十二、常用库函数(数学、时间与错误处理) | 青江的个人站)。

这个程序中只是对log输出做了一个简要的模拟。实际企业开发中,log的输出一般会更加规范,可能用到枚举、宏定义,或者直接使用现成的log库,例如Log4c、Syslog、spdlog等,且一般会输出到专门的log文件中。

5. 三级指针案例:字符串无限追加的应用

案例:创建一个字符串数组,实现一个函数,向函数中传递字符串时,可以将传入的字符串扩充到字符串数组中。

实际上,字符串的类型是char*,因此,字符串数组中的每一个元素都是一个指向一个字符串的指针,因此字符串数组应该是一个二级指针。

此时,为二级指针重新分配内存空间就需要用到三级指针。

需要注意的是,字符串数组中储存的是指针,因此当添加新的指针时,只为指针变量本身分配了空间,但没有为指针指向的数据分配空间。还需要使用malloc函数为这些指针分配内存空间。最后需要将这些内存空间全部释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 向字符串数组中添加一个字符串
void addString(char*** strArray, size_t* size, const char* newString);

// 打印字符串数组中的所有字符串
void printStrings(char** strArray, size_t size);

// 释放字符串数组中的所有字符串和数组本身的内存
void freeStrings(char*** strArray, size_t* size);

// 案例:创建一个字符串数组,实现一个函数,向函数中传递字符串时,可以将传入的字符串扩充到字符串数组中。
int main() {
char** strArray = NULL; // 初始化字符串数组
size_t size = 0; // 字符串数组的大小

// 向字符串数组中添加一些字符串
addString(&strArray, &size, "Hello");
addString(&strArray, &size, "World");
addString(&strArray, &size, "!");

// 打印字符串数组中的所有字符串
printStrings(strArray, size);

// 释放字符串数组中的所有字符串和数组本身的内存
freeStrings(&strArray, &size);

return EXIT_SUCCESS;
}

// 向字符串数组中添加一个字符串
void addString(char*** strArray, size_t* size, const char* newString) {
// 重新分配内存以容纳新的字符串
char** temp = realloc(*strArray, ((size_t)*size + 1) * sizeof(char*));
if (temp == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
*strArray = temp; // 更新字符串数组指针
// 分配内存并复制新字符串
(*strArray)[*size] = (char*)malloc((strlen(newString) + 1) * sizeof(char)); // 注意:这里需要加1来存储字符串的结束符'\0'
if ((*strArray)[*size] == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
strcpy_s((*strArray)[*size], strlen(newString) + 1, newString); // 复制新字符串到数组中
// 增加字符串数组的大小
(*size)++;
}

// 打印字符串数组中的所有字符串
void printStrings(char** strArray, size_t size) {
for (size_t i = 0; i < size; i++) {
printf("%s\n", strArray[i]);
}
}

// 释放字符串数组中的所有字符串和数组本身的内存
void freeStrings(char*** strArray, size_t* size) {
for (size_t i = 0; i < *size; i++) {
free((*strArray)[i]); // 释放每个字符串的内存
}
free(*strArray); // 释放字符串数组的内存
*strArray = NULL; // 将指针置空
*size = 0; // 重置大小
}

6. 案例:动态数据结构的管理

链表是一种动态数据结构,由若干个节点组成,每个节点分为两部分:

  • 数据域:存储实际数据
  • 指针域:存储指向下一个节点的指针

与数组不同,链表的节点在内存中不连续,通过指针串联起来。

案例:创建一个链表,实现一个函数,修改指向结构体的指针,在链表头添加新的节点,不需要返回额外新的头指针,实现动态管理数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>

// 定义一个链表节点结构体
typedef struct Node {
uint32_t data; // 数据域
struct Node* next; // 指针域,指向下一个节点
} Node;

// 在链表头添加新的节点
void addAtHead(Node** head, uint32_t data);

// 打印链表中的所有节点数据
void printList(Node* head);

// 释放链表中的所有节点,防止内存泄漏
void freeList(Node* head);

// 创建一个链表,实现一个函数,修改指向结构体的指针,在链表头添加新的节点,不需要返回额外新的头指针,实现动态管理数据结构。
int main() {
Node* head = NULL; // 初始化链表头指针为NULL

// 在链表头添加一些节点
addAtHead(&head, 1);
addAtHead(&head, 2);
addAtHead(&head, 3);

// 打印链表中的所有节点数据
printList(head);

// 释放链表中的所有节点,防止内存泄漏
freeList(head);

return EXIT_SUCCESS;
}

// 在链表头添加新的节点
void addAtHead(Node** head, uint32_t data) {
// 创建一个新的节点
Node* newNode = (Node*)malloc(sizeof(Node)); // 1. 申请新节点内存
if (newNode == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
newNode->data = data; // 2. 设置新节点的数据域
newNode->next = *head; // 3. 新节点 → 原头节点(必须先执行)
*head = newNode; // 4. 头指针 → 新节点(必须后执行)
}

// 打印链表中的所有节点数据
void printList(Node* head) {
Node* current = head; // 从头节点开始遍历
while (current != NULL) {
printf("%" PRIu32 " -> ", current->data); // 打印当前节点的数据
current = current->next; // 移动到下一个节点
}
printf("NULL\n"); // 链表结束标志
}

// 释放链表中的所有节点,防止内存泄漏
void freeList(Node* head) {
Node* temp; // 临时指针用于保存当前节点
while (head != NULL) {
temp = head; // 保存当前节点的指针
head = head->next; // 移动到下一个节点
free(temp); // 释放当前节点的内存
}
}

在链表中添加新节点时,下面是执行步骤拆解,以链表已有1 -> NULL,插入2为例:

第一步:malloc创建新节点

1
2
newNode → [ data=2 | next=? ]
head → [ data=1 | next=NULL ]

第二步:newNode->next = *head—新节点的next指向原头节点

1
2
newNode → [ data=2 | next=─────────┐ ]
head → [ data=1 | next=NULL ] ←─┘

第三步:*head = newNode—更新头指针指向新节点

1
2
head → [ data=2 | next=─────────┐ ]
[ data=1 | next=NULL ] ←─┘

⚠️步骤顺序不可颠倒:若先执行*head = newNode,原头节点的地址就丢失了newNode->next将无法正确指向它,导致链表断开。

本文来源: 青江的个人站
本文链接: https://hanqingjiang.com/2026/03/14/20260314_C_multiLevelPointer/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
知识共享许可协议
赏

谢谢你请我喝可乐~

支付宝
微信
  • Notes
  • C

扫一扫,分享到微信

微信分享二维码
【C语言学习笔记】十三、动态内存分配
  1. 1. 1. 多级指针
  2. 2. 2. 多级指针的用途
  3. 3. 3. 游戏案例:游戏服务器动态玩家列表管理之realloc与多级指针的应用
  4. 4. 4. 续上节:卫语句与log编写
  5. 5. 5. 三级指针案例:字符串无限追加的应用
  6. 6. 6. 案例:动态数据结构的管理
© 2021-2026 青江的个人站
晋ICP备2024051277号-1
powered by Hexo & Yilia
  • 友链
  • 搜索文章 >>

tag:

  • 生日快乐🎂
  • 新年快乐!
  • 小技巧
  • Linux
  • 命令
  • 语录
  • 复刻
  • Blog
  • Notes
  • Android
  • C
  • FPGA
  • Homework
  • MATLAB
  • Server
  • Vivado
  • Git

  • 引路人-稚晖
  • Bilibili-稚晖君
  • 超有趣讲师-Frank
  • Bilibili-Frank