ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

指针数组与数组指针:从内存布局到实战应用全解析

指针数组与数组指针:从内存布局到实战应用全解析 指针数组这块内容我在初学C语言那阵子绕了不少弯。明明概念能背下来一上手写代码就搞混尤其是和“数组指针”放一起的时候脑子直接变浆糊。后来我发现之所以乱是因为大多数人只记了字面定义——“数组里存指针叫指针数组指向数组的指针叫数组指针”——但根本没理解这两者在内存里长什么样、编译器是怎么解释这串声明符号的。这篇笔记就是把我自己的学习路径沉淀下来从最朴素的定义出发先拆内存布局再讲字符串场景、函数指针数组、动态分配最后把“指定位输出字符”这类带指针移动的实际需求揉进去。如果你是刚学到指针这里、被各种声明搞到头疼的人或者想系统梳理一次指针数组操作的开发者这篇内容应该能帮你把思路理顺。需要说明一下这本来就是一份“个人学习”性质的长期笔记所以我会持续往后补充内容。当前版本主要围绕指针数组的基础操作和几个重要变种展开后面遇到新的坑和新的用法我会回来更新这篇。1. 指针数组究竟是什么——先从区分“数组指针”开始1.1 一句话版本和内存语义先给出最本源的结论指针数组是一个数组数组中每个元素都是指针。比如char *name[3]它首先是一个长度为3的数组其次每个元素name[0]、name[1]、name[2]都是char *类型的指针。这个顺序不能反。有人会想“数组里存指针那不就是指针吗为什么不叫指针的数组偏要叫指针数组”其实这是个翻译习惯问题。英文是 array of pointers直译是“指针的数组”中文简化为指针数组。反过来pointer to array直译是“指向数组的指针”中文就是数组指针。两个词的区别在英文里更直观一个中心词是数组array一个中心词是指针pointer。但光靠这个词序还是容易混因为中文里“指针数组”和“数组指针”念起来都有点拗口我见过不少人背反了。真正让人分清的是内存层面的样子指针数组本质是个数组它在内存里是一段连续的空间这段空间每个槽位放的是一个地址值。假设int *pArr[4]pArr 是数组名sizeof(pArr) 在64位系统下是32字节4个元素乘以每个指针8字节如果它确实是数组的话pArr和pArr的值相等但类型不同前者是int *(*)[4]后者是int **。数组指针本质是个指针它只占用一个指针大小的空间8字节指向一个完整的数组。比如int (*p)[4]p 指向一个包含4个int的数组sizeof(p) 是8字节。在实际编码中判断一个声明到底是数组指针还是指针数组最无脑的方法是数“优先级”[]的下标运算符优先级高于*解引用运算符。所以int *p[4]里p 先和[4]结合说明 p 先是一个数组数组元素是int *而int (*p)[4]里括号改变了结合顺序p 先和*结合说明 p 先是一个指针这个指针指向的对象是int[4]类型的数组。1.2 在内存里把它们画出来比看十遍定义都管用我非常推荐学习指针数组时在纸上把内存图画一遍。比如char c A; char *pc c; char *arr[3] { c, c 1, c 2 }; // 这里为了演示实际别这样写其实这个例子不好更常见的初始化方式是char a A; char b B; char c C; char *arr[3] { a, b, c };arr 这个数组在内存里占3个连续槽位每个槽位放的是 a、b、c 的地址。当你写arr[0]时拿到的是 a 的地址写*arr[0]时才拿到字符 A。这个过程可以抽象为先通过下标定位到数组元素一个指针再对指针解引用拿到目标对象。我把这个过程叫“两步取值法”。对新手来说写*arr[0]时容易漏掉括号觉得arr[0]就是字符实际上arr[0]是地址字符还隔着一层。调试的时候如果发现输出的是乱码地址而非字符问题往往就出在这里。对数组指针则是另一种画法int data[3][4] {{1,2,3,4},{5,6,7,8},{9,10,11,12}}; int (*p)[4] data;p 指向的是“一个包含4个int的数组”。这里的 p 不是二级指针它就是数组指针。p1 在内存中跳过的是4个int的宽度16字节而不是4字节。很多人踩过这个坑p之后以为 p 指向下一个int结果发现跳了4个。这就是没有从“p指向的对象是数组”这个角度去推理导致的。这两者虽然名字只差一个词但用法完全不同指针数组常用于“一组独立的数据指针”数组指针常用于“二维数组的行指针操作”。文章后面会重点讲指针数组的实战数组指针只是用来做对比辨析。2. 指针数组最经典的场景存放字符串2.1 为什么字符串适合用指针数组指针数组最常见的应用之一就是存放字符串。我们用两种方式对比看很快就能理解差别。方式一二维字符数组char names[3][20] {Alice, Bob, Cindy};这种方式的问题很明显每一行固定20字节不管存的是3个字符还是10个字符整块内存都占死。3个字符串合计最大60字节但实际上 Alice 只用6字节包含结尾的 \0Bob 只用4字节Cindy 只用6字节加起来16字节就够却占了60字节。如果字符串长度参差不齐且数量很多空间浪费就很吓人。方式二指针数组存放字符串char *names[3] {Alice, Bob, Cindy};这里names[0]指向字符串字面量 Alice 的首地址Alice 在只读数据区占6字节包括结尾的 \0names[1]指向 Bobnames[2]指向 Cindy。每个字符串按实际长度存储指针数组本身只占3个指针大小的空间。遍历的时候直接用names[i]就能拿到字符串首地址用printf(%s, names[i])输出。从编译的角度看字符串字面量是存放在只读区的所以char *names[3]更严谨的声明应该写成const char *names[3]或const char * const names[3]如果指针本身也不变的话。有些编译器对char *指向字面量会报警告所以代码规范里建议直接声明成const char *。2.2 对字符串指针数组做排序的完整思路存放字符串之后最常见的操作是排序。这里有个关键点排序时我们交换的是指针不是整个字符串内容。#include stdio.h #include string.h void sort_names(const char *arr[], int n) { for (int i 0; i n - 1; i) { for (int j i 1; j n; j) { if (strcmp(arr[i], arr[j]) 0) { const char *tmp arr[i]; arr[i] arr[j]; arr[j] tmp; } } } } int main(void) { const char *names[] {Cindy, Alice, Bob}; int n sizeof(names) / sizeof(names[0]); sort_names(names, n); for (int i 0; i n; i) { printf(%s\n, names[i]); } return 0; }为什么交换指针比交换字符串快因为strcmp和strcpy都是按字符逐一操作的遇到很长的字符串拷贝要 O(len) 的时间而交换指针只是交换两个8字节的值是 O(1) 操作。数据量一大性能差距就很明显。这里要注意一个细节sizeof(names) / sizeof(names[0])只能在数组定义的作用域内使用。一旦传入函数在参数里它退化成指针sizeof(names)就变成指针大小这样算出的 n 就是错的。所以我在函数外面算好 n 再传入这是一个很典型的避免入坑习惯。2.3 指定位输出指针移动与偏移量的实战热搜词里有一句“c语言 数组 指针 移动 指定位输出 字符”这说明很多人关注“利用指针移动来定位并输出特定位置的字符”。在指针数组中这个问题通常有两种层次第一层在多个字符串中输出指定的某一个字符串。比如有char *s[] {hello, world, c}要输出第二个字符串“world”直接printf(%s\n, s[1])即可。这里s[1]本质是*(s1)指针先向后移动一个元素宽度再解引用。第二层进一步定位到某个字符串内部的指定字符。比如要输出第2个字符串中的第3个字符索引从0开始即s[1][2]可以用char ch s[1][2]; // r也可以完全用指针移动的方式表达char ch *(*(s 1) 2);这个表达式我看着都绕但拆开就清楚了s是指针数组名s 1让指针移动到第二个元素的位置*(s 1)取出第二个字符串的首地址*(s1) 2把首地址向后移动2个字符位置最后再解引用就拿到目标字符。整个链条对应的是“数组名偏移 → 取字符串首地址 → 字符串内偏移 → 取值”。如果你要写一个循环按指定间隔依次输出字符串内部的字符可以这样操作const char *p s[1]; // 指向第二个字符串的起始位置 while (*p ! \0) { putchar(*p); p 2; // 每次移动2个字符位置注意可能越过\0需要先判断再移动 }这里面有个容易越界的问题指针每次移动2个位置时可能直接跳过\0跑到字符串后面的内存区域。所以实际项目中这种“跳步输出”不能裸奔着写循环要先计算字符串长度再控制循环次数或者用下标和长度判断兜底。3. 指向字符串内容的另一种玩法指针数组与二级指针的纠缠3.1 二级指针其实就是指针数组的“数组名”学指针数组的时候一定会碰见二级指针。它们的关联很紧密指针数组在传给函数时函数参数可以写成char **。原因在前文提过数组名作为实参传递时会退化为指向首元素的指针。而指针数组的首元素是一个指针所以首元素的地址就是“指针的指针”也就是二级指针。于是下面两种函数写法是等价的void print_names(char **names, int n); // 指针形式 void print_names(char *names[], int n); // 数组形式编译器在处理函数参数的时候char *names[]会被调整成char **names。所以你在函数里写sizeof(names)拿到的一定是8字节而不是数组的总大小。这个认识在实战中特别重要。很多初学者写了一个函数要接收指针数组却用int *arr[]做参数结果内部用sizeof(arr)计算长度时发现数据完全对不上。原因就是退化机制不是你代码逻辑错而是 “数组长度信息在传参时根本没被带进函数”。如果你确实需要知道指针数组的元素个数有两条路在主调函数里计算好再作为参数传入。在指针数组末尾放一个约定好的哨兵值例如NULL然后遍历直到碰上NULL。第二种方案在实际工程里非常香。尤其是构造“字符串表”的时候在结尾统一放NULL既当结束标志又避免多传一个长度参数。比如const char *table[] { alpha, beta, gamma, NULL };然后遍历的时候for (int i 0; table[i] ! NULL; i) { printf(%s\n, table[i]); }这种写法在命令行参数处理、配置文件解析、菜单项定义等场景中大量使用安全又直观。3.2 二级指针在函数里修改原数组的可能如果要在函数里操作指针数组的元素比如交换、置空用二级指针作为参数是很顺其自然的事情。上面那个sort_names函数把数组声明成const char *arr[]本质上就等同于const char **arr通过这个二级指针可以直接修改原调用方的数组内容。同理如果你在函数里想释放指针数组里的某些动态内存并希望调用方感知到指针变化也需要二级指针或者更准确地说需要“指向指针的指针”才能修改一级指针本身的值。比如void reset_ptr(int **pp) { *pp NULL; // 把原指针改成NULL }这种写法的本质是想在函数里修改某个变量的值就必须拿到该变量的地址。一级指针本身也是一个变量所以修改指针变量就需要指针即二级指针。想通这一点很多封装函数的设计就顺了。不过要小心一种常见错误有人把指针数组当参数时错误地写成了三级指针。原因是把“数组名”和“指针的地址”搞混了。函数参数里char **p已经能修改char *类型的数组元素没必要再套一层char ***。只有在修改二级指针本身的场景才需要三级指针实战中很少碰到遇到这种需求先停下来想想设计是否绕了远路。4. 实战解析函数指针数组——把代码逻辑变成查表4.1 函数指针数组的声明为什么那么长函数指针数组就是数组元素类型是函数指针的数组。例如int (*handlers[4])(int, int);读法还是按优先级handlers先和[4]结合说明它是个数组再往前元素类型是int (*)(int, int)也就是“接收两个int参数、返回int的函数指针”。如果你用typedef简写一下会清爽很多typedef int (*operation)(int, int); operation handlers[4] { add, sub, mul, div_mod };typedef 的好处是不用每次写一大串声明改起来也方便。工程上强烈建议加 typedef否则一旦参数列表变长整个声明会变得惨不忍睹。4.2 用函数指针数组实现菜单分发函数指针数组最经典的应用是“替代一长串 switch-case”。假设做一个简易计算器菜单用户输入一个选项然后调用对应的函数#include stdio.h int add(int a, int b) { return a b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return b 0 ? 0 : a / b; } int main(void) { typedef int (*op)(int, int); op funcs[] { add, sub, mul, divide }; int idx 2; // 模拟用户选择乘法 int x 6, y 3; int result funcs[idx](x, y); printf(result %d\n, result); return 0; }这比写一长串switch的好处在于逻辑结构变成了数据表。以后要新增一种运算比如取模只需要新写一个函数然后在funcs数组里加一个元素不用去动分发逻辑。这种思想在状态机、命令解析、事件驱动的代码里随处可见。用术语讲叫“表驱动编程”本质就是把“分支选择”固化成“查表索引”。这里有个安全细节数组下标需要做合法性校验。如果用户输入的 idx 超出了数组范围直接调用就是未定义行为。所以生产代码里一定要先检查idx 0 idx (sizeof(funcs)/sizeof(funcs[0]))再执行调用。还有一个容易踩坑的点函数指针数组的元素数量要和初始化列表完全对应如果不确定可以先初始化一部分其余默认填 NULL调用前判断是否为 NULLif (funcs[idx] ! NULL) { funcs[idx](x, y); }这样能避免一个空指针调用导致整个程序崩溃。这套防御式写法在插件化系统里尤为重要。4.3 函数指针数组在状态机里的一个简例状态机的核心就是“当前状态 事件 → 下一个状态 动作”。用函数指针数组实现时一种常见套路是把动作函数按状态编号放进数组比如状态0对应动作0状态1对应动作1状态转移时直接actions[current_state]()。这种设计的好处是状态和行为的绑定一目了然改状态逻辑时只需要维护转移表和动作表两个数据结构。对刚接触状态机的人来说函数指针数组是最好的入门实现方式不用引入复杂框架先感受一下“表驱动”带来的清晰度。缺点是当状态很多、事件很多时二维函数指针数组会变得臃肿此时可以考虑压缩状态空间或用查表映射但这不是本文重点后续持续更新里我再展开。5. 数组指针 vs 指针数组五个维度拆清楚5.1 声明的读法和右左法则我整理了一份对比表按五个维度来拆解两者的差异。先声明示例int *pArr[3]; // 指针数组元素是 int* int (*pArr2)[3]; // 数组指针指向一个 int[3] 数组维度指针数组int *pArr[3]数组指针int (*pArr2)[3]本质类型数组指针内存大小3个指针大小64位下24字节1个指针大小8字节sizeof 结果sizeof(pArr) 24sizeof(pArr2) 8常用场景字符串表、指针集合二维数组的行操作、矩阵p1的移动跨度一个指针的大小8字节整个数组的大小3个int12字节解引用*p后得到一个int*得到一个int[3]数组名读声明的“右左法则”是先找到标识符然后从标识符开始先向右看遇到]或)就停下来处理括号中的内容再向左看遇到*就表示指针。循环往复直到声明结束。int *pArr[3]标识符 pArr先向右看见[3]说明是数组再向左看见*说明数组元素是指针继续向左是 int说明指针指向 int。int (*pArr2)[3]标识符 pArr2先向右看见)括号结束再向左看见*说明 pArr2 是指针往右看括号外面是[3]说明指针指向一个含3个int的数组。这套方法能解决绝大部分复杂声明的辨认问题是值得花10分钟练熟的技能。5.2 二维数组参数传递里数组指针的用武之地数组指针最常见的用途是操作二维数组。比如void print_matrix(int (*m)[4], int rows) { for (int i 0; i rows; i) { for (int j 0; j 4; j) { printf(%4d, m[i][j]); } printf(\n); } }这里的m就是一个数组指针参数它指向每行4个int的一维数组。如果把函数原型写成int **m会出错或者产生误导因为二维数组名和二级指针完全不是同一个东西。二维数组名在传参时退化为“指向首行的数组指针”不是二级指针。这一点是很多人从指针数组跳到二维数组时最容易崩掉的地方。我见过不少代码在函数里用int **m接收二维数组然后m[i][j]编译通过但运行崩溃。原因就在于二维数组int data[3][4]的内存是连续的12个int它没有一个单独的“指针数组”来存放每行的地址而int **期望内存里存放的是“指向指针的指针”两者内存布局根本不一致。这就是“格式不匹配”导致的未定义行为。所以操作二维数组用数组指针int (*)[N]操作指针数组用二级指针int **。这两种搭配才正确。5.3 面试题里最爱考的“移动一个位置”陷阱数组指针和指针数组最容易做错的一类题是“指针移动一个位置”。比如int a[2][3] {{1,2,3},{4,5,6}}; int (*p)[3] a; p; printf(%d\n, **p); // 输出4因为p跳到了第二行如果把 p 换成int *p a[0][0]那么p只会跳4字节到2完全不是同一回事。指针数组也有类似陷阱char *arr[] {hello, world, !}; char **p arr; p; printf(%s\n, *p); // 输出 world因为p跳过了一个指针元素这里p移动了一个指针宽度也就是从指向arr[0]变成指向arr[1]。如果误以为是“指向下一个字符”就会访问到 hello 里面的 e调试时一脸懵。这两个陷阱看似都是“指针加一”实际移动的跨度完全不同取决于指针的类型——指针的类型决定了它指向的对象有多大也就是指针步长。所以学到这里一定要建立条件反射看到某指针 n时第一反应不是“内存地址偏移n字节”而是“偏移 n × sizeof(指针指向的对象类型)”。6. 动态分配指针数组与内存管理6.1 为什么不建议直接定义超大指针数组当你不知道该存多少个指针时写死一个很大的char *arr[10000]看起来省事但实际上有很多问题栈空间可能不够一个线程默认栈在几MB左右10000个指针就是80KB勉强还行但再大几个数量级就会爆栈而且即使空间够用大部分槽位空置也是浪费。更好的方案是用动态内存分配按需分配指针数组本身的大小。最常见的写法是int n 5; char **arr (char **)malloc(n * sizeof(char *)); if (arr NULL) { // 处理分配失败 }这里malloc(n * sizeof(char *))分配的是连续存放5个指针的空间。分配完之后每个元素arr[i]还需要单独初始化为目的地址比如指向某块动态字符串内存。一个完整的例子从文件或命令行收集若干个字符串数量不确定时先读取一个数量值再分配对应大小的指针数组。int count get_count(); // 假设从配置中得到 char **messages (char **)calloc(count, sizeof(char *)); if (!messages) { // 分配失败处理 } for (int i 0; i count; i) { messages[i] (char *)malloc(64); if (!messages[i]) { // 失败处理最好能回滚已经分配的内存 } snprintf(messages[i], 64, message-%d, i); }需要注意两点一是calloc会把内存清零适合初始化指针为 NULL避免出现野指针二是每次malloc都要检查返回值。这块属于“防御式编程”尤其是嵌入式或长时间运行的服务漏一个检查可能在运行时突然崩溃。6.2 释放内存时最容易漏的“二次释放”动态指针数组的释放顺序非常讲究先释放每个指针指向的字符串内存再释放指针数组本身。顺序反了会直接造成内存泄漏或者二次释放崩溃。我见过一个典型的错误释放写法free(messages);这只会释放指针数组本身占用的那小块空间而每个messages[i]指向的动态字符串内存全都漏了。正确写法是for (int i 0; i count; i) { free(messages[i]); // 先释放字符串 } free(messages); // 再释放指针数组释放完之后最好把指针置空防止后面不小心再次引用messages[i] NULL; free(messages); messages NULL;置空不是强迫症。在大型代码里一次 free 后还有其他代码路径可能访问这个指针置空后就可以通过if (p ! NULL)做出安全判断。另外还有一个“二次释放”的坑如果两个指针指向同一个动态内存地址释放一次后再释放另一个就会触发 double free 崩溃。比如char *p malloc(10); char *arr[2]; arr[0] p; arr[1] p; free(arr[0]); free(arr[1]); // 崩溃重复释放同一块内存指针数组允许同一个指针存储多次这是它的灵活之处但也增加了释放管理的复杂度。在项目里遇到共享指针时要么约定单一所有权要么引入引用计数不用引用计数的话就要靠人工保证“谁分配谁释放绝不二次释放”。6.3 常见内存泄漏排查思路排查动态指针数组的泄漏我习惯先看三个点分配了几次释放了几次数量是否一致。释放时是否两层都释放了。某个分支提前 return 时是否绕过了 release 函数。尤其第三个点非常隐蔽。函数中间有个错误分支return -1如果忘记对已经分配的指针数组做清理每次错误路径都是一次泄漏。处理方法是把清理逻辑抽取成统一的 goto cleanup 或者封装成 release 函数函数末尾统一走清理。在 Linux 下我会用 valgrind 检查valgrind --leak-checkfull ./your_program看到 “definitely lost” 就是明确泄漏从分配的调用栈能直接定位到哪个 malloc 没配 free。7. 指针数组的边界问题与工程建议7.1 下标越界不会自动触发错误但后果很严重C语言不检查数组下标越界。arr[5]哪怕数组只有3个元素编译也不报错运行阶段会直接读取相邻内存的数据可能返回垃圾值甚至越界写入破坏其他变量。在某些嵌入式环境下越界写可能把返回地址冲掉导致函数返回时跳到一个随机地址引发硬件异常。所以操作指针数组时建议“越界意识”内化成习惯访问前先想清楚长度。特别是在循环里使用了n和sizeof混搭或者动态分配之后没有记录长度这些都是越界的高发区。一个实用的建议是为指针数组单独维护一个“长度变量”不要把长度隐藏在代码的某个常量里。尤其是动态分配时用int len显式记录元素个数作为所有循环的边界依据。7.2 多层间接访问的可读性陷阱指针数组解决的多字符串、多对象管理问题但是层数一旦增多代码可读性就会断崖式下降。比如char ***ppp;三级指针在C语言里虽然合法能不用就不用。它表达的是“指向指针数组的指针的指针”之类的复杂关系阅读成本很高。真遇到多维数据更好的做法是定义结构体把层级关系显式化typedef struct { const char **items; int count; } StringList;这样既保留了指针数组的灵活性又避免了写成char ***带来的可读性灾难。这个建议我在很多规范里都见到过实践下来确实让代码好维护很多。7.3 现代C代码中的替代选择在现代 C 项目里指针数组并没有被淘汰但一些场景有很多更安全的选择字符串列表可以用const char *arr[]或者封装成StringList结构体避免裸二级指针到处传。动态数组在 C99 以后可以用变长数组VLA做栈上的动态长度数组但要注意 VLA 在栈上分配超大时依然可能爆栈。想实现自动扩容容器C 语言没有内置的vector可以自己封装一个struct vector内部用realloc维护指针数组这是工程里常见的替代方向。我之前自己写过一个小型的字符串向量封装核心就是内部维护一个char **data和count、capacity两个字段追加字符串时如果容量不足就realloc扩容。这套逻辑本质上还是指针数组只是外面包了一层安全接口。所以学指针数组是理解这类容器的地基。8. 实测中的意外情况与避坑心得8.1 “printf 输出字符串却出现乱码”的排查过程我记得第一次用指针数组存字符串时输出全是乱码。我当时的代码大概是char *arr[2]; scanf(%s, arr[0]); // 错误arr[0] 未初始化问题很明显arr[0]是一个未初始化的指针scanf 会往一个随机地址写入字符串。运气好碰上可写的内存区域程序没崩运气不好直接段错误。这种“未初始化的野指针”是初学者最容易掉进去的坑。排查的顺序建议是这样先用printf(%p, (void*)arr[0])打印指针的值如果发现它不是一个可预期的值基本就是野指针。解决办法是给arr[0]分配一块真正的内存或者用一个足够大的字符数组取地址char buf[64]; arr[0] buf; scanf(%63s, arr[0]);有人会问那char *name hello;为什么可以因为字符串字面量会分配在只读内存区编译器安排好了地址name指向那块合法空间。而char *name;只是声明了一个指针变量没有让它指向任何地方这就危险了。一个指向不合法空间的指针就是野指针是很多空指针/段错误的来源。8.2const修饰位置带来的连锁反应指针和 const 的组合有四种经典写法指针数组里同样适用写法含义char *p指向字符串的指针可改字符串内容const char *p指向常量字符串的指针可以改 p 本身但不能改指向内容char *const p常量指针p 不能改但指向内容可改const char *const p两者都不能改在指针数组中如果数组元素是const char *说明可以换字符串比如排序时交换指针但不能修改字符串内容。如果定义成char * const数组则每个指针不可改数组的操作空间就小很多。实际项目中字符串表基本都建议const char *因为字符串字面量往往在只读区强行修改会导致未定义行为。我之前踩过一次把const char *的字符串表传给一个char *参数的函数结果函数想原地改字符串程序在运行时直接崩溃或行为诡异。后面排查发现就是const修饰不一致导致的不安全传参。8.3 局部数组返回的陷阱还有一种非常典型的错误从一个函数返回局部指针数组调用方继续使用。比如char **get_names(void) { char *local[3] { a, b, c }; return local; // 错误local 是数组名返回的指针指向已失效的栈内存 }虽然local中的指针指向字符串字面量没问题但local数组本身在函数返回后就失效了。函数返回时实际返回的是首元素的地址但这个地址所在的栈帧已经被回收之后再访问就是未定义行为。解决办法有三种用static char *local[3]定义成静态数组生命周期延长到整个程序运行期。在函数内用malloc分配指针数组返回后由调用方负责释放注意释放顺序和时机。直接让调用方传入一个数组和长度函数负责填充。第二种方式在工程中比较常见但要承担内存管理职责。我一般倾向第三种调用方传入存储缓冲区函数不负责分配这样可以避免所有权混乱。9. 后续持续更新的方向指针数组在实战中还能怎么用这篇作为第一阶段的学习笔记我先把指针数组的核心概念和操作理清了。往后我想在持续更新中补上的内容包括指针数组在命令行参数处理argv本质上就是char **中的源码剖析。指针数组在 Token 解析器、词法分析里的应用例如把拆出的单词存入指针数组。指针数组配合结构体构造更安全的动态数组容器并实现自动扩容。函数指针数组和状态机配合时事件驱动的组织形式如何从一维查表升级到二维查表。跨平台环境下指针大小差异32位4字节64位8字节对指针数组结构体布局的影响。二分查找法在有序字符串指针数组中的应用以及strcmp在二分边界处理上的细节。这些内容等我有足够实践积累后会继续补进这篇笔记里。我自己学指针数组的最大体会是不要把概念只停留在“指针的数组”这句话上而是要在内存图上反复推演。数组名、元素地址、解引用每一步搞清楚编译器做了什么后面看再多复杂声明都不心虚。日常写代码时遇到不确定的声明就拿“右左法则”走一遍再想想这个声明的sizeof和p1的移动跨度正确答案基本就浮出来了。如果你正在学指针这块建议自己动手把这几个小实验跑一遍定义一个char *arr[3]打印arr、arr[0]、arr1、*(arr1)这些表达式的值和类型再定义一个int (*p)[3]打印p、p1、*p、*(p1)的值。亲手把输出对上内存模型比看十篇教程都管用。
返回列表