1. 多级指针
指针在指向变量地址的同时,它自身也存在一个内存地址中,多级指针就是指向指针的指针,保存的是指针所在的内存地址。
例如:
1 | int a = 10; // 定义一个整数变量 a |
2. 多级指针的用途
以一个简单的示例程序为例,可以使用工厂、批发商、零售商来作为初始变量、一级指针、二级指针。
工厂(Factory):生产商品,初始化一个初始的产品id和产品价格;
批发商(Wholesaler):从工厂获取产品,可能会更新产品的id和价格;
零售商(Retailer):从批发商处获取产品,可能会进一步更新产品的id和价格。
工厂只负责生产商品,并初始化产品的属性,批发商和零售商只能接收上级提供的产品,并更新它的属性。
**注意:**在创建产品时,不应该使用函数直接返回局部变量的地址,因为局部变量会在函数执行完毕后销毁,从而导致未定义行为。例如下面的错误示例:
1 | // ★错误示例★ |
运行这个错误的程序后可以发现,由于局部变量默认被分配到栈内存上,第一次使用printf打印的数据是正确的,但第二次打印的数据是无意义的。虽然第一次打印碰巧拿到的正确的值,但这个行为本身就是未定义的。
第一次printf之所以"幸运"地打印正确,是因为C语言在函数调用前会先对所有参数求值——值被读取后才执行printf。但printf的执行本身会复用那块栈内存,导致第二次读取时数据已被破坏。
在实际程序开发时必须避免这种行为。
如果需要将局部变量保存下来,需要为其分配堆内存,手动管理其生命周期。
正确示例如下:
1 |
|
下面是工厂、批发商、零售商链路中,采用多级指针实现商品的传递和商品信息修改的程序。
1 |
|
其中需要注意的是,箭头运算符(->)可以解引用指针并访问其成员,也就是p->id完全等价于(*p).id,对于多级指针也是如此,例如(**ppp)->id完全等价于(***ppp).id。
3. 游戏案例:游戏服务器动态玩家列表管理之realloc与多级指针的应用
指针章节中提到,如果要在一个函数中实现修改变量值的功能,需要使用其指针,与这一点类似,在函数中如果需要修改一个指针指向的地址,需要向函数传递指向这个指针的二级指针。
realloc函数可以直接使用来分配内存空间,不必局限于在malloc函数之后使用。
案例:设计一个玩家列表,支持有新玩家加入时动态分配新的内存空间。
1 |
|
需要注意的是,读取当前列表并添加新玩家时,对当前列表的解引用必须加括号:
1 | (*playerList)[*playerCount] = *newPlayer; |
必须是(*playerList),而不是*playerList,因为下标运算符([])的优先级高于解引用运算符(*),而它是一个指针,必须先解引用才能访问数组元素,所以需要括号来确保正确的访问顺序。
4. 续上节:卫语句与log编写
上一节的程序还有一些优化空间,例如在函数中可以添加卫语句,提高逻辑严谨性;可以对程序的一些信息做log输出,方便调试。
下面是添加了卫语句与简单的log输出的版本。
1 |
|
snprintf是printf家族中专门用于将格式化内容写入字符串缓冲区的函数。与printf直接输出到终端不同,它的第一个参数是目标缓冲区,第二个参数是缓冲区的最大字节数(含\0),后续参数与printf完全一致。正是这个长度限制使它比sprintf更安全——即使格式化后的内容超出缓冲区容量,它也只会写入指定的字节数并自动在末尾补上\0,不会发生溢出。它的返回值是理论上应写入的字符数(不含\0),若返回值大于等于缓冲区大小,说明内容被截断了,可以据此进行检测。
有关time.h与时间戳的使用与处理可以参考第十二章第11节的内容(【C语言学习笔记】十二、常用库函数(数学、时间与错误处理) | 青江的个人站)。
这个程序中只是对log输出做了一个简要的模拟。实际企业开发中,log的输出一般会更加规范,可能用到枚举、宏定义,或者直接使用现成的log库,例如Log4c、Syslog、spdlog等,且一般会输出到专门的log文件中。
5. 三级指针案例:字符串无限追加的应用
案例:创建一个字符串数组,实现一个函数,向函数中传递字符串时,可以将传入的字符串扩充到字符串数组中。
实际上,字符串的类型是char*,因此,字符串数组中的每一个元素都是一个指向一个字符串的指针,因此字符串数组应该是一个二级指针。
此时,为二级指针重新分配内存空间就需要用到三级指针。
需要注意的是,字符串数组中储存的是指针,因此当添加新的指针时,只为指针变量本身分配了空间,但没有为指针指向的数据分配空间。还需要使用malloc函数为这些指针分配内存空间。最后需要将这些内存空间全部释放。
1 |
|
6. 案例:动态数据结构的管理
链表是一种动态数据结构,由若干个节点组成,每个节点分为两部分:
- 数据域:存储实际数据
- 指针域:存储指向下一个节点的指针
与数组不同,链表的节点在内存中不连续,通过指针串联起来。
案例:创建一个链表,实现一个函数,修改指向结构体的指针,在链表头添加新的节点,不需要返回额外新的头指针,实现动态管理数据结构。
1 |
|
在链表中添加新节点时,下面是执行步骤拆解,以链表已有1 -> NULL,插入2为例:
第一步:malloc创建新节点
1 | newNode → [ data=2 | next=? ] |
第二步:newNode->next = *head—新节点的next指向原头节点
1 | newNode → [ data=2 | next=─────────┐ ] |
第三步:*head = newNode—更新头指针指向新节点
1 | head → [ data=2 | next=─────────┐ ] |
⚠️步骤顺序不可颠倒:若先执行*head = newNode,原头节点的地址就丢失了newNode->next将无法正确指向它,导致链表断开。
本文链接: https://hanqingjiang.com/2026/03/14/20260314_C_multiLevelPointer/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
