数组这玩意儿,凡是写过两行 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 -fsanitize=address -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/C++(Microsoft 官方那个)和 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分配后内存默认是 0 | malloc不初始化内存,可能是任意值 |
free后指针就是空指针 | free不改变指针值,需手动置 NULL |
realloc失败后还留着原指针 | 失败返回 NULL,原指针仍有效,但别直接覆盖 |
这张表里的每一条,都是我或者身边的人在真实项目里踩过的坑。看着简单,一不留神就中招。
最后再分享一个小习惯:我现在写任何和数组相关的函数,第一行永远是检查“调用者传进来的指针是不是 NULL,长度是否为 0”。不是矫情,而是数组这种东西出错往往不是语法错,是逻辑错、边界错。把防御式检查写进肌肉记忆,能帮你少熬无数个大夜。