尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C语言数组深度解析:从内存布局到指针退化与越界防护

C语言数组深度解析:从内存布局到指针退化与越界防护 数组这玩意儿凡是写过两行 C 代码的人基本都觉得自己会。但我在实际写程序、面试新人、带实习生的时候发现十个候选人里至少有六七个一聊到数组名和指针的区别、二维数组传参、越界保护这些话题就开始含糊。说句实在话数组确实是 C 语言里最基础的数据结构但它背后牵扯的内存布局、指针运算、函数的形参退化才是真正决定你这门语言学得扎不扎实的地方。这篇博文不是从零开始的教材而是把你日常会用到的“C 语言数组”知识重新过一遍重点放在那些代码能跑但心里没底、笔试容易错、面试爱追问的细节上。无论你是刚准备计算机二级的初学者还是在 VSCode 里折腾环境、刷 C 语言题目的在校生又或者做嵌入式、和 PLC 打交道的工程师这篇文章都值得收藏下来慢慢看。1. 数组的基本盘内存连续性与索引的真相1.1 数组在内存里长什么样为什么索引必须从 0 开始先看一段最简单的声明int arr[5] {10, 20, 30, 40, 50};这段代码干了什么它告诉编译器给我在内存里找一块能放下 5 个 int 的连续空间。假设每个 int 占 4 字节那么这块空间就是连续的 20 个字节。这听起来太基础了但“连续”这两个字才是数组的灵魂。因为连续所以我们可以用首地址加偏移量就能访问任意一个元素因为连续所以缓存对数组友好也正因为连续你一旦越界破坏的就是紧挨着数组的别的数据。为什么索引从 0 开始因为数组元素的访问本质是第 n 个元素的地址 首地址 n * 每个元素占用的字节数如果索引从 1 开始公式就变成了首地址 (n-1) * 元素大小每次访问都要多一次减法运算。C 语言的设计哲学是“贴近机器”索引从 0 开始直接对应偏移量编译器不用做多余的变换硬件也喜欢这种简单直接的寻址方式。现在很多新语言也沿用从 0 开始就是受了 C 的影响。1.2 数组名到底是不是指针sizeof 和 arr 到底算什么这是面试中出现频率极高的问题也是很多人写代码时容易糊涂的地方。先说结论数组名在大多数场合会“退化”为指向首元素的指针但它本身并不是指针变量而是一个地址常量。看这段代码#include stdio.h int main(void) { int arr[5] {1, 2, 3, 4, 5}; printf(arr %p\n, (void*)arr); printf(arr %p\n, (void*)arr); printf(arr 1 %p\n, (void*)(arr 1)); printf(arr 1 %p\n, (void*)(arr 1)); printf(sizeof(arr) %zu\n, sizeof(arr)); return 0; }如果arr的地址假设是0x1000那么输出大致是arr和arr的数值一样但类型完全不同。前者是int*后者是int(*)[5]。arr 1指向0x1004因为它按int的大小跳一次前进 4 字节。arr 1指向0x1014它按整个数组的大小跳一次前进 20 字节。sizeof(arr)返回 20而不是 8说明 sizeof 在数组名上不会触发退化。简单记忆就是arr代表“这个数组”而arr代表“指向这个数组的指针”。绝大多数情况下两者数值一样但指针运算的步长完全不同。我见过不少线上崩溃本质上就是对arr 1之后的内存做了越界读写。注意当数组作为函数参数传递时它会被“调整”成指针sizeof自然也就失效了。这就是后面要说的形参退化。1.3 数组越界为什么是 C 语言里最阴险的错误C 语言不会检查数组越界。很多从 Java、Python 转过来的朋友特别不适应这一点——越界访问在那些语言里会立刻抛出异常但在 C 语言里它可能什么都不发生也可能几小时后才在完全无关的地方崩溃。原因就是数组是一片连续内存越界访问只是去读写了这块内存旁边的东西。如果你的数组在栈上旁边可能存着其他局部变量、栈帧指针、甚至函数的返回地址。非法改写这些内容就会出现“明明没动那行代码但变量莫名其妙变了”的现象。我踩过最深的一个坑是在一个循环里把 64 长度的缓冲区写到了 65 个元素。程序跑起来一切正常只有在特定输入下循环次数稍微变多一个完全无关的标志位被改成 1整个功能就悄悄失效了。调试了整整一天最后用 AddressSanitizer 编译才抓出来。所以我的建议是只要条件允许就把越界检测放在编译期和运行期都打开。用 GCC 或 Clang 编译时加上这些选项能救你很多次gcc -fsanitizeaddress -g -O1 program.c -o program ./programAddressSanitizer 能在运行到越界访问的那一刻直接告诉你“哪个函数、哪一行、越界了多少字节”比你自己人肉 diff 效率高太多。平时刷题、写作业可能不觉得但到了项目阶段这个工具几乎是救命稻草。2. 多维数组、指针数组与函数传参的底层逻辑2.1 二维数组在内存中的布局你以为的“行列”其实是线性地址二维数组在逻辑上是“有行有列”但在物理内存里它仍然是一段线性连续空间并且 C 语言按行优先顺序存储int matrix[3][4];它在内存中的排列顺序是matrix[0][0] matrix[0][1] matrix[0][2] matrix[0][3] matrix[1][0] ...也就是说matrix[1]的地址就是matrix[0]的地址往后跳 4 个 int 的地址。理解这一点极其重要因为很多数据批量处理的场景二维数组可以一次性读写比如用fwrite(matrix, sizeof(int), 12, fp)一次把整个 3x4 矩阵写入文件就是因为它在内存里连续排布。访问二维数组时matrix[i][j]其实等价于*(*(matrix i) j)。编译器会先根据i计算行地址再根据j计算列偏移。这个表达式的本质也是“地址 偏移量”。2.2 行指针、指针数组和二级指针到底怎么区分这是我在面试里非常喜欢问的一个点因为能一眼看出候选人有没有真正理解多维数组。看这两行声明int *p1[4]; // 指针数组p1 是一个数组数组里有 4 个 int* int (*p2)[4]; // 数组指针行指针p2 是一个指针指向一个长度为 4 的 int 数组p1和p2从字面上看只差一个括号含义却完全不同。p1的每个元素是独立的指针可以分别指向不同的地址常用于“字符串数组”或“指针数组”场景p2则是一个指向“整行”的指针它天然适合配合二维数组使用。还有二级指针int **pp;它可以指向指针的指针但不代表它等于“二维数组”。二维数组的元素类型是int [4]而int**指向的元素类型是int*两者的内存布局根本不同。有人试图把int a[3][4]直接赋给int**编译器会报警告强行转过去运行访问时大概率崩溃。区分它们有一个快速方法看[]和*的优先级。[]的优先级高于*所以int *p1[4]先结合成“大小为 4 的数组”元素类型是int*而int (*p2)[4]用括号打破了优先级p2 先是指针再指向int[4]这种类型。2.3 函数传参的三种等价写法与维度丢失问题当一维数组作为函数实参传递时它退化成了指针。下面这三种函数签名在编译层面完全等价void print_array(int arr[4]); void print_array(int arr[]); void print_array(int *arr);不管你怎么写编译器看到的都是int *arr。所以函数内部对数组使用sizeof(arr)得到的只会是指针的大小而不是数组的大小。二维数组传参更需要注意。下面这几行代码很容易让人懵void print_matrix(int matrix[3][4]); void print_matrix(int matrix[][4]); void print_matrix(int (*matrix)[4]);这三种写法等价核心原因是 C 语言要求数组形参必须能推导出每一行的长度。丢掉第一维行数是可以的因为编译器扫描数组时只需要知道每行有多长再加上首地址就能定位所有元素列数必须写出来否则无法计算matrix[i][j]的地址偏移。常见的错误是把二维数组传给int**类型的形参这在第 2.2 节已经说过类型不兼容。正确做法是传行指针或者把二维数组手动“压平”成一维数组再传。2.4 二维字符数组和指针数组存字符串该怎么选字符串在 C 语言里本质就是char数组。存放多个字符串有两种常见方式// 方式一二维字符数组每个字符串固定占 20 字节 char names[3][20] {zhangsan, lisi, wangwu}; // 方式二指针数组每个指针指向字符串常量或动态分配的内存 const char *ptrs[3] {zhangsan, lisi, wangwu};两者的差别很现实。char names[3][20]会在栈上分配 60 字节每行固定 20 字节即使某个字符串只有 5 个字符剩下的空间也浪费了好处是内存是连续的、可修改的读写效率高。const char *ptrs[3]则只占 3 个指针的空间字符串本身存放在只读数据段里不能通过指针去修改。这种方案适合只读的字符串列表比如菜单、配置项、错误消息等。如果要做大量字符串的插入、排序、删除我更推荐用指针数组加动态分配。排序时只需要交换指针不需要搬动字符串内容性能和代码简洁度都会好很多。3. 字符串数组与高频实战操作拆解3.1 C 语言里没有字符串类型一切都是 char 数组现代语言里字符串是一种内置类型但在 C 语言中根本没有“字符串变量”这种东西。所谓字符串就是一个以\0结尾的char数组。hello这种字符串常量在内存里的实际形态是 6 个字节h e l l o \0。因为字符串就是数组所以字符串相关的操作全部可以归约成数组操作遍历、复制、拼接、比较、逆序、截取等等。明白这一点你以后看到任何字符串函数都不会觉得它是什么神秘魔法无非就是在数组上做循环。一个新手经常犯的错字符数组初始化后忘记留\0的位置char str[5] hello; // 错误需要 6 个字节5 个字符 1 个 \0如果编译器允许这么写结果就是\0写到数组外面打印字符串时可能出现乱码甚至越界。老实的写法是char str[] hello; // 编译器自动算长度包括 \0共 6 字节3.2 字符串复制、拼接函数的安全隐患只要写过几年代码的人对strcpy和strcat大概率都有心理阴影。这两个函数从设计上就是“不带边界检查”的目标缓冲区多大它不管只负责往里面写直到源字符串结束。安全替代品是strncpy和strncat但它们也各有各的坑。strncpy在源字符串长度小于 n 时会补\0到 n 个字节如果源字符串长度不小于 n它不会给你加\0。这导致手动加\0几乎是必须的char dst[8]; strncpy(dst, src, sizeof(dst) - 1); dst[sizeof(dst) - 1] \0;最后一行就是“安全红线”。我见过很多代码用了strncpy却漏了结尾补\0结果越界读的问题变成了不必要的长字符串输出。把这个写法看成固定套路写熟练了就不会出错。3.3 字符串逆序、数组转字符串等笔试题实战字符串逆序是高频的入门题PTA、计算机二级、各大厂笔试都爱出。经典双指针一次遍历就能搞定#include stdio.h #include string.h void reverse_string(char *s) { int left 0; int right (int)strlen(s) - 1; while (left right) { char tmp s[left]; s[left] s[right]; s[right] tmp; left; right--; } } int main(void) { char s[] hello world; reverse_string(s); printf(%s\n, s); return 0; }输出是dlrow olleh。核心思路是首尾互换、往中间逼近。这道题虽然简单但考察的点不少会不会用strlen、会不会处理奇数长度、会不会写原地交换逻辑。再说“数组转字符串”这类需求。比如把一个整数数组[1, 2, 3, 4, 5]拼成1,2,3,4,5。我常用的做法是先在char缓冲区里用snprintf逐个格式化中间手动加分隔符char buffer[128] {0}; int nums[] {1, 2, 3, 4, 5}; int len sizeof(nums) / sizeof(nums[0]); int pos 0; for (int i 0; i len; i) { int written snprintf(buffer pos, sizeof(buffer) - pos, i 0 ? %d : ,%d, nums[i]); if (written 0 || written sizeof(buffer) - pos) { break; // 缓冲区不足停止拼接 } pos written; }这里每次都用size - pos去限制剩余空间就是防止越界的保险丝。很多新手直接一个sprintf(buffer, ...)反复拼接一旦数据变长就把缓冲区打爆。3.4 数组去重的三种思路与复杂度对比数组去重也是笔试题常客。根据数据规模和是否允许额外空间可以选择三种方案方案一双重循环暴力去重int remove_duplicates(int arr[], int n) { int m 0; for (int i 0; i n; i) { int duplicate 0; for (int j 0; j m; j) { if (arr[j] arr[i]) { duplicate 1; break; } } if (!duplicate) { arr[m] arr[i]; } } return m; }思路简单但时间复杂度是 O(n²)适合小规模数组。方案二先排序再去重借助快速排序将数组排好序然后一次遍历把重复元素跳过保留不重复的连续序列。时间复杂度是 O(n log n)空间复杂度取决于排序实现。方案三哈希表或标记数组如果数组元素的值域范围有限比如都是 0 到 100 的整数可以开一个标记数组第一次遇到的数字就标记下次再遇到直接跳过。时间复杂度 O(n)空间换时间是最快的。实际开发里我首选排序方案因为快排库函数成熟、泛化能力强而且排序后去重代码写起来极短。哈希表方案则适合对时间有硬性要求的场景。4. 动态数组、内存生命周期与初始化细节4.1 静态数组不够用何时该上 malloc/realloc栈上声明的数组大小必须在编译期确定。有些编译器支持变长数组但那是 C99 的可选特性跨平台性和编译器兼容性都一般。真正问题在于栈内存很有限大数组容易栈溢出。我见过有人定义了一个 10MB 的局部数组一运行就段错误。动态数组就是把内存从堆上分配大小可以在运行期计算不受栈大小限制用完手动释放。常用的函数是malloc、calloc、realloc。分配一个长度为 n 的 int 数组int n 100; int *data (int*)malloc(n * sizeof(int)); if (data NULL) { // 内存分配失败返回 NULL必须处理 return -1; } memset(data, 0, n * sizeof(int)); // 如果希望初始化为 0也可以用 callocmalloc不初始化内存分配完里面是“脏”的随机值必须在用之前自己清零或逐个赋值。calloc会帮你清零但性能稍差适合对初始化语义要求清晰的时候。4.2 动态二维数组的两种实用创建法二维数组的起始行数、列数都可能在运行期变化这时就要用动态创建。常用的有两种方式。方式一指针数组 每行单独分配int rows 3, cols 4; int **matrix (int**)malloc(rows * sizeof(int*)); for (int i 0; i rows; i) { matrix[i] (int*)malloc(cols * sizeof(int)); }这种方法每行是独立分配的访问时matrix[i][j]很自然但释放时必须逐行释放稍麻烦。它的优点是每行可以有不同的长度适合做不规则二维结构。方式二一次性分配整块内存int *matrix (int*)malloc(rows * cols * sizeof(int)); #define AT(i, j) matrix[(i) * cols (j)]这种方式只有一个malloc也只要一个free内存连续缓存友好适合图像、矩阵这类规则数据。访问时用宏或者函数包装一下代码可读性也不差。从工程角度我偏好第二种。内存连续意味着一轮遍历可以预取性能打分比碎片化的方式高一个档次。对于嵌入式、工控这些资源受限的环境减少 malloc 次数本身就是在降低堆碎片风险。4.3 free、野指针、realloc 的数据搬迁问题动态内存管理最怕的就是内存泄漏和野指针。free一次之后指针还保留着原来的地址值这块地址已经归还给堆管理器再通过它读写就是典型的野指针操作。安全的习惯是 free 后立刻把指针置空free(data); data NULL;这样以后不小心再用data至少会因为解引用空指针而崩溃而不是访问到不知道被谁改写的堆内存——前者问题暴露得快后者真的找不到原因。realloc也容易踩坑。它可能原地扩容也可能搬运到新的地址。如果直接写成data realloc(data, new_size);一旦realloc失败它返回 NULL但原来的data还活着你现在把它直接覆盖掉了原指针丢失想释放都释放不了还造成内存泄漏。安全的写法是int *new_data (int*)realloc(data, new_size); if (new_data ! NULL) { data new_data; } else { // 保持 data 原样后续释放 }4.4 数组初始化的几种方式以及宏定义数组的坑C 语言数组初始化有很多人没注意到的细节。常见的包括int a[5] {1, 2, 3}; // 剩余元素自动补 0 int b[5] {0}; // 全部初始化为 0 int c[5] {[2] 10}; // C99 指定初始化器第 2 个元素是 10其余为 0 int d[] {1, 2, 3}; // 编译器自动推断长度为 3用{0}清零整个数组是很流行也非常可移植的写法但要记住它只保证第一个元素被显式初始化其他元素按“未初始化列表”规则自动补 0。面试问到“怎么把数组全部初始化为 0”标准答案就是这个。宏定义数组同样有坑。比如#define ARR_SIZE 10 int arr[ARR_SIZE] {0};这里没问题但一旦宏定义里混了类型转换或者逗号表达式很容易让数组声明变得不可读。我见过有人写成#define MAX_LEN (unsigned int)10 int arr[MAX_LEN]; // 某些编译器可能警告数组大小不是整数常量宏展开时机早于编译器类型检查很多错误信息会被拉得很远。现在 C 语言标准有constexpr风格的常量表达式支持但在 C 里还是推荐用enum或者#define定义纯整数常量避免把复杂类型塞进去。5. 排序、合并与文件读写把数组用起来5.1 冒泡和快排笔试最常用的两种排序实现排序是数组应用的经典场景。笔试手撕排序冒泡和快排出镜率最高。冒泡排序实现简单适合快速回答。优化过的版本长这样void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped 1; } } if (!swapped) { break; // 本趟没有发生交换已经有序 } } }加一个swapped标志序列本身有序时时间复杂度能降到 O(n)。这个细节在面试里很加分因为说明你考虑过最优情况。快速排序写起来稍微复杂一点但它是“分治思想”的经典体现。递归版本核心是分区函数int partition(int arr[], int low, int high) { int pivot arr[high]; int i low - 1; for (int j low; j high; j) { if (arr[j] pivot) { i; int temp arr[i]; arr[i] arr[j]; arr[j] temp; } } int temp arr[i 1]; arr[i 1] arr[high]; arr[high] temp; return i 1; } void quick_sort(int arr[], int low, int high) { if (low high) { int pi partition(arr, low, high); quick_sort(arr, low, pi - 1); quick_sort(arr, pi 1, high); } }快排的平均复杂度 O(n log n)但最坏情况下比如数组已经有序且每趟选的基准都是最大/最小退化到 O(n²)。工程上qsort库函数有各种优化自己写时可以考虑三数取中法选基准能有效避免最坏情况。5.2 双指针合并两个有序数组双指针是处理有序数组合并的利器。经典场景两个升序数组合并成一个升序数组。void merge_sorted(int a[], int na, int b[], int nb, int result[]) { int i 0, j 0, k 0; while (i na j nb) { if (a[i] b[j]) { result[k] a[i]; } else { result[k] b[j]; } } while (i na) { result[k] a[i]; } while (j nb) { result[k] b[j]; } }核心思想是两个数组各自维护一个指针谁小谁先放入结果数组然后对应指针前进。剩下的尾部直接拷贝。这个模板应用极其广泛归并排序、有序数组合并、求交集、找中位数都是它的变体。我在批改作业时发现很多人会忘记最后的两个while循环导致一个数组还没走完。这两个循环不是可有可无而是合并逻辑的必要收尾。5.3 数组和文件之间的读写闭环环境里总是会有“把数组数据保存到文件”“从文件读一批数据到数组”的诉求。如果数组里的元素是二进制数据可以一条fwrite/fread搞定int data[100]; size_t written fwrite(data, sizeof(int), 100, fp);这段代码之所以高效前提就是数组在内存中连续fwrite才能按块拷贝。如果换成一个指针数组每个指针指向不同地址就不能这样一次性写入必须逐个元素处理。读取时要先知道有多少个数据常见方案是文件头先存一个数量或者根据文件大小除以元素大小推断。从文件读数组的典型代码如下FILE *fp fopen(data.bin, rb); if (fp NULL) { /* 处理打开失败 */ } fseek(fp, 0, SEEK_END); long size ftell(fp); fseek(fp, 0, SEEK_SET); int count size / sizeof(int); int *arr (int*)malloc(count * sizeof(int)); fread(arr, sizeof(int), count, fp); fclose(fp);如果存的是文本格式的整数可以用fscanf循环读取但要注意处理换行和空白字符。文本格式可读性好方便调试二进制格式省空间、速度快适合大规模数据。工程上我通常数据交换用二进制日志和配置用文本。5.4 和 PLC、嵌入式等场景的延展思考数组不是只在普通应用里有用。工控领域用的西门子 1500 PLC、汇川 Easy522虽然主要用梯形图或结构化文本编程但一旦涉及 C 语言动态库、或者数组数据的批量处理底层的“数组是连续内存”这一概念完全一致。比如向 PLC 侧导出配方数据、批量采集传感器数据都要把一个连续的字节数组按照元素类型切分、转换。嵌入式实战里数组更是无处不在通信协议栈要处理字节数组缓冲区ADC 采样数据要存成数组做均值滤波按键扫描要用状态数组。C 语言里数组和指针的灵活配合在这些场景中能省非常多资源但同时也要求开发者的内存边界意识极强。6. 开发环境配置、调试技巧与高频坑位速查6.1 VSCode 配置 C 语言环境的最短路径网上很多 VSCode C 语言配置教程又臭又长我在这分享一条实测有效的最短路径。第一步安装编译器。Windows 建议用 MinGW-w64注意别用安装器装“老版本”去开源的 WinLibs 或 MSYS2 下载最新的。把bin目录加到系统 PATH 后在终端里执行gcc --version能输出版本号说明成功。第二步VSCode 里装两个插件C/CMicrosoft 官方那个和 Code Runner。前者提供语法高亮、智能提示和调试能力后者用来快速运行测试代码。第三步写代码后按 F5。如果是第一次运行选择“C (GDB/LLDB)”VSCode 会生成launch.json。再在.vscode目录下创建tasks.json把编译命令写进去{ version: 2.0.0, tasks: [ { label: build, type: shell, command: gcc, args: [-g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe], group: { kind: build, isDefault: true } } ] }关键点在于-g参数不加它调试器的断点、变量监视全部失效。很多人配置半天调试不了大多就是漏了这个。6.2 调试时怎么查看数组内容而不被刷屏当数组有几千上万个元素时调试器默认会把每个元素都显示出来刷得人眼睛疼。这时候可以用监视表达式按需查看。GDB 或 VSCode 调试里监视变量时输入arr[0]10写法含义是“从arr[0]开始连续查看 10 个元素”。arr换成你的数组名10换成你要看的个数。这样只看自己关心的切片不用在几千行里翻。想在设置断点后手动查看数组地址附近的内存也可以用同样的方式。这类技巧平时不常用但一旦遇到大规模数组排查效率差距非常明显。6.3 几个编译警告和常见报错的解读C 编译器报的警告很多人习惯性忽略。但警告往往是代码问题的前兆。我遇到最典型的是warning: assignment to int * from incompatible pointer type int (*)[5]看见这种“incompatible pointer type”的警告几乎可以确定是数组和指针的类型混用了。要么是试图把arr赋值给int*要么是没有正确写成行指针。还有一个很经典的报错error: unreferenced label warning: label error defined but not used这里值得一提的是有些编译器在-Wall下会对“定义了但没有使用”的 label 警告。通常原因是你写了跳转标签但没有配合goto使用或者条件编译#ifdef把使用它的代码切掉了。处理方式不是简单删掉而是想清到底要不要这个标签。如果只是暂时不用加#if 0注释掉最踏实。6.4 while 和 do-while在数组遍历里怎么选有热词特意提到“c语言while和do-while区别”可见这也是个高频困惑点。两者最大差别是执行时机的次数while是先判断后执行循环体可能一次都不跑do-while是先执行一次再判断至少跑一次。数组遍历时通常用for或while因为空数组的情况下循环体不应该执行。但有一种场景适合do-while比如从键盘输入一批数据直到输入 0 结束至少会有一个合法输入进入数组。这时用do-while更自然能把第一行输入代码统一到循环结构里。int arr[100]; int n 0; do { scanf(%d, arr[n]); } while (arr[n] ! 0);这个例子把数组读取和结束条件放在一起虽然简洁但要注意arr[n]在while条件里会改变数组下标边界要严格算清楚否则容易越界。6.5 C 语言数组高频笔试误区速查表拿计算机二级、校招笔试来说下面这些点几乎年年出现。整理成一张表考前过一遍效率很高常见误区正确理解数组名就是指针变量数组名是地址常量多数情况下退化为首元素指针但不能自增/自减sizeof(arr)在函数内部也能得数组大小函数参数中的数组形参退化为指针sizeof只能得到指针大小二维数组可以传给int**应传int (*)[N]行指针类型要匹配char str[5] hello没问题字符串含\0至少需要 6 字节strcpy会自动防止溢出它没有边界检查必须用strncpy或手动控制长度malloc分配后内存默认是 0malloc不初始化内存可能是任意值free后指针就是空指针free不改变指针值需手动置 NULLrealloc失败后还留着原指针失败返回 NULL原指针仍有效但别直接覆盖这张表里的每一条都是我或者身边的人在真实项目里踩过的坑。看着简单一不留神就中招。最后再分享一个小习惯我现在写任何和数组相关的函数第一行永远是检查“调用者传进来的指针是不是 NULL长度是否为 0”。不是矫情而是数组这种东西出错往往不是语法错是逻辑错、边界错。把防御式检查写进肌肉记忆能帮你少熬无数个大夜。
返回列表