资讯详情 C语言一维数组详解:从内存原理到冒泡排序与字符串处理
📅 2026/10/11 6:06:10
1. 一维数组到底在解决什么问题先聊个很实际的场景。你写了一个班级成绩统计程序要存50个学生的C语言期末成绩。最原始的做法是声明50个变量score1、score2、score3……光是声明变量就能写到手抽筋后面的遍历、求和、排序更是噩梦。一维数组就是用来终结这种噩梦的。C语言里的数组本质上就是在内存中申请一段连续的空间用来存放一组相同类型的数据。这段话里有三个关键词连续、相同类型、一组。连续意味着每个元素紧挨着排列相同类型意味着每个元素占用同样大小的内存一组意味着我们通过一个名字加下标就能访问所有元素。数组能做什么它能让“批量数据”变得可管理、可遍历、可计算。无论是字符串处理、排序算法、查找统计还是图形图像里的像素矩阵、音频采样数据底层几乎都是一维数组在支撑。对于刚学C语言的人一维数组是第一个真正意义上的“数据结构”它帮你建立起“内存视角”看问题的习惯——这对后面学指针、学链表、学动态内存管理至关重要。这篇教程适合谁看正在上C语言课的本专科学生、准备计算机二级或PTA刷题的备考党、以及工作中需要补C语言基础的自学者。我会把数组的内存本质、核心操作、函数传参、常见坑位全拆开讲配合能直接上机跑的代码和练习思路尽量让你少走我当年踩过的弯路。2. 声明、初始化与内存地址的底层逻辑2.1 数组声明的三条规则声明一维数组的语法长这样类型 数组名[元素个数]; int scores[50]; double heights[100]; char name[32];这里面有几个隐藏规则很多人背过却不一定理解。第一数组名必须符合标识符命名规则不能是关键字第二方括号里的元素个数必须是整数常量不能是变量比如int n 5; int arr[n];在标准C89里是不允许的虽然在C99之后VLA变长数组成了可选特性很多编译器接受了但考试和移植性要求下不建议这么写第三类型决定了每个元素占多少字节直接决定了数组总长度。计算数组占用内存大小的方法很简单总字节数 元素个数 × 每个元素字节数。比如int scores[50]在我的64位Ubuntu上int是4字节那scores就占据200字节的连续内存。你可以用sizeof(scores)直接验证结果就是200而不是50。我推荐用sizeof而不是硬编码常量尤其是遍历的时候。假设你要改成double scores[50]如果遍历循环里写死i 50那没问题但如果你在函数间传数组就得谨慎了——这个坑后面专门讲。2.2 初始化方式与缺省规则数组的初始化有三种常见姿势// 姿势一完全初始化 int a[5] {1, 2, 3, 4, 5}; // 姿势二部分初始化其余自动补0 int b[5] {1, 2, 3}; // b[3] 0, b[4] 0 // 姿势三不指定长度编译器根据初始化列表推导 int c[] {10, 20, 30}; // 等价于 int c[3]第三种最容易被忽略int c[]不写长度编译器会自动数大括号里的元素个数。这个特性在写静态映射表、星期字符串数组时特别好用比如char *week[] {Mon, Tue, Wed};以后加一个Thu长度自动变不用回头改数字。特别提醒一个经典陷阱局部数组如果只声明不初始化里面的值是随机的垃圾值而不是0。很多人刚上手时以为int arr[10];就是全0结果输出一屏的怪异数字然后怀疑编译器坏了。静态数组static int arr[10];和全局数组会被编译器自动清零局部数组不会这是C和Java等语言的重要区别。如果你想把整个数组初始化为同一个非零值不能直接写int arr[5] {6, 6, 6, 6, 6};之外的捷径——不存在int arr[5] 6这种写法也不存在int arr[5] {6};这样只会把第一个元素设为6其余补0。实在要快速填充用循环或者memset仅适合字节模式的0或-1。2.3 下标从0开始这个设计并不随意数组的下标从0开始从10开始学编程的人几乎都会困惑为什么不是1把数组的地址图画出来你就明白了。数组名其实是一个指针常量指向首元素的地址。假设int a[5]的首地址是0x1000那么a[0]的地址是0x1000a[1]的地址是0x1004a[i]的地址是0x1000 i * 4。也就是说下标本质上是偏移量。如果下标从0开始编译器计算某个元素的地址只需要做一次基地址 偏移量 × 元素大小的乘法如果下标从1开始就得额外做一次减法基地址 (i-1) × 元素大小多一次运算。在C语言诞生年代这种性能毫厘必争。这个历史原因决定了今天所有从C衍生出来的语言C、Java、JavaScript、Python的list都沿用了0起始。实操时还有一个细节a[i]和i[a]在C语法里是等价的因为编译器会把a[i]翻译成*(ai)而加法交换律使*(ia)也成立。这个知识在面试题里偶尔出现看懂即可日常千万别写i[a]阅读性太差了。3. 核心操作全拆解遍历、统计、逆序、排序3.1 遍历与求和的基本功数组最基础的操作是遍历也就是用循环把每个元素过一遍。经典求和代码如下#include stdio.h int main(void) { int scores[] {68, 79, 85, 92, 76}; int n sizeof(scores) / sizeof(scores[0]); int sum 0; for (int i 0; i n; i) { sum scores[i]; } printf(总和: %d, 平均分: %.2f\n, sum, (double)sum / n); return 0; }注意sizeof(scores) / sizeof(scores[0])这句。它用“数组总字节数除以单个元素字节数”来获取元素个数比手动写死n 5更安全以后增删元素代码不用改。这个技巧在遍历时几乎是标配但只在数组本身的“地盘”里有效——一旦数组作为参数传进函数sizeof就失效了原因下一章细说。求最值算是最常见的练习题找最大数。思路是先假设第一个元素是最大值然后依次和后面的比较遇到更大的就更新。这个“打擂台”的思路贯穿所有统计类算法int max scores[0]; for (int i 1; i n; i) { if (scores[i] max) { max scores[i]; } }3.2 字符串逆序输出原地逆序与整体逆序关于“字符串逆序输出c”这个热搜网上相关练习非常多。其实字符串在C语言中就是一个特殊的字符数组以\0结尾。比如char s[] hello;数组实际占6个字节h e l l o \0。逆序输出的核心思路有两个。第一种原样输出逆序。这个最简单从最后一个有效字符往前遍历即可char s[] hello; int len strlen(s); // len 5不含\0 for (int i len - 1; i 0; i--) { putchar(s[i]); } putchar(\n);第二种原地逆序reverse in place也就是把字符串本身翻转。思路是双指针一头一尾交换然后依次向中间靠拢。这里有个容易踩的坑——\0不能跟着交换所以要先算出长度只处理有效字符void reverse_string(char s[]) { int len strlen(s); for (int i 0, j len - 1; i j; i, j--) { char temp s[i]; s[i] s[j]; s[j] temp; } }很多人第一次写原地逆序会写成从0到len/2但忘了同时操作尾部下标结果只换了一半。用i配合j--这是最稳妥的双指针写法逻辑清晰且不容易越界。还有一个练习叫“字符串逆序输出c语言pta”那是PTA平台上的经典题做法类似。核心提醒输入带空格的字符串不要用scanf(%s)它会停在空格处要用gets不安全不推荐或fgets或者scanf(%[^\n])。我建议直接用fgets(s, sizeof(s), stdin)它会把换行符也读进来逆序前要手动把末尾的\n替换成\0或从长度里减去1。3.3 冒泡排序从思路到代码一次到位冒泡排序是热搜词“冒泡排序c语言”的核心内容。它的原理很形象每一趟都比较相邻的两个元素如果顺序不对就交换最大的元素像气泡一样“冒”到数组末尾。重复n-1趟后整个数组有序。void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { // 外层控制趟数 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; } } } }外层循环i代表已排好序的元素个数内层j的结束条件是n - 1 - i因为每跑一趟末尾就多一个已经归位的最大值不需要再去碰它。如果忘记减掉i虽然程序不会错但无谓的多比较效率低一些。冒泡还可以优化如果某一趟没有任何交换说明数组已经有序直接用标志位跳出循环int swapped 0; // 内层循环里面每次交换都设 swapped 1 // 一趟结束后 if (!swapped) break;这样处理接近有序的数组时间复杂度能从O(n²)降到接近O(n)。面试的时候写出来算加分项上机练习也可以这么练手。这里插一句排序算法对比C语言书里经常会先教选择排序、冒泡排序再教插入排序。其实都建议动手实现一遍别只背代码。数组一维结构是理解算法复杂度的最佳载体。4. 数组与函数、指针的纠缠关系4.1 数组作函数参数传进去的不是数组本身很多新手第一次在函数里写void foo(int arr[10])然后自信地sizeof(arr)想获取数组长度结果输出的却是8或者4。原因特别重要C语言在函数形参里写int arr[10]本质上是int *arr的语法糖编译器不会真的复制整个数组而是传递首元素的地址。可以用代码验证void test(int arr[]) { printf(在函数里 sizeof(arr) %zu\n, sizeof(arr)); } int main(void) { int data[10]; printf(在主函数里 sizeof(data) %zu\n, sizeof(data)); // 40 test(data); // 可能输出 8 return 0; }因为函数形参退化成指针所以函数内部拿不到数组长度必须额外传递一个长度参数。这是C语言数组和现代语言Java、Python的显著区别也是初学时的核心障碍之一。所以规范的写法是double average(int arr[], int n);在调用侧传average(scores, n)n最好通过主函数里的sizeof计算好。4.2 指针访问数组的三种写法数组名是指向首元素的常量指针所以指针和数组几乎可以混用。访问第i个元素下面三种形式都合法int a[5] {10, 20, 30, 40, 50}; int *p a; // 等价于 int *p a[0]; // 写法一 printf(%d\n, a[i]); // 写法二 printf(%d\n, *(a i)); // 写法三 printf(%d\n, *(p i));但a本身不能自增自减a是编译错误因为数组名是常量地址。指针p则可以做p、p--。这给大家一个灵活遍历的方式for (int *p a; p a 5; p) { printf(%d , *p); }在实际工程中这种指针遍历常用于字符串操作比如统计字符串中小写字母个数int count_lower(const char *s) { int cnt 0; while (*s) { if (*s a *s z) cnt; s; } return cnt; }注意字符串以\0结尾所以while (*s)循环就能自然结束。4.3 一维数组与字符数组的特殊之处字符数组和字符串之间的关系让很多人晕。先记住三个概念的区别字符数组、字符串字面量、字符串变量。char str1[] hello; // 字符数组可修改长度是6含\0 char *str2 hello; // 字符串字面量存于只读区不能修改 char str3[6] {h,e,l,l,o,\0}; // 手动初始化char *str2 hello;这种写法如果试图str2[0] H;在部分编译器上是运行期崩溃或未定义行为因为字符串字面量通常在只读存储区。这是比数组越界更隐蔽的坑。用双引号赋初值时C会自动在结尾补\0。手动用{}初始化则必须自己加\0否则printf(%s)会一路输出到内存里碰到的第一个0为止产生乱码。我记得自己第一次写字符数组就漏了\0结果输出字符串后面多了一串垃圾字符后来用调试器才明白是结尾标志缺失。5. 一维数组实战案例学生成绩统计系统5.1 需求拆解与函数拆分学一维数组最忌讳只背概念不写完整程序。下面给一个能直接跑的成绩统计程序覆盖前面所有知识点。需求输入n个学生的C语言成绩n不超过100计算平均分、最高分、最低分并按降序输出成绩。拆解下来至少需要四个函数input_scores输入成绩print_scores打印数组compute_stats统计平均分、最高分、最低分用指针出参sort_desc降序排序简单改一下冒泡让较大的往前冒5.2 完整代码与运行结果#include stdio.h #define MAX_N 100 void input_scores(int arr[], int n) { for (int i 0; i n; i) { printf(请输入第%d个学生的成绩: , i 1); scanf(%d, arr[i]); } } void print_scores(const int arr[], int n) { for (int i 0; i n; i) { printf(%d , arr[i]); } printf(\n); } void compute_stats(const int arr[], int n, double *avg, int *max, int *min) { int sum 0; *max arr[0]; *min arr[0]; for (int i 0; i n; i) { sum arr[i]; if (arr[i] *max) *max arr[i]; if (arr[i] *min) *min arr[i]; } *avg (double)sum / n; } void sort_desc(int arr[], int n) { for (int i 0; i n - 1; i) { 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; } } } } int main(void) { int scores[MAX_N]; int n; printf(请输入学生人数: ); scanf(%d, n); if (n 0 || n MAX_N) { printf(人数超出范围\n); return 1; } input_scores(scores, n); printf(\n原始成绩: ); print_scores(scores, n); double avg; int max, min; compute_stats(scores, n, avg, max, min); printf(平均分: %.2f, 最高分: %d, 最低分: %d\n, avg, max, min); sort_desc(scores, n); printf(降序排列: ); print_scores(scores, n); return 0; }运行效果大致这样请输入学生人数: 5 请输入第1个学生的成绩: 80 请输入第2个学生的成绩: 92 请输入第3个学生的成绩: 75 请输入第4个学生的成绩: 66 请输入第5个学生的成绩: 88 原始成绩: 80 92 75 66 88 平均分: 80.20, 最高分: 92, 最低分: 66 降序排列: 92 88 80 75 66这个程序把前面所有核心点串起来了数组声明、遍历、函数传参、指针出参、冒泡排序变体。建议你自己动手敲一遍不要复制敲的过程才会遇到那些“忘了加”、“sizeof坑”、“变量名遮蔽”之类的真实问题。5.3 这个案例还能怎么扩展如果你想把练习做深一点可以加这些功能统计及格率和优秀率、支持删除一个成绩后自动前移、按学号查找成绩、读入数据前先判断输入合法性。每一个扩展都在逼你更熟练地操作数组下标和边界条件。翁恺老师的C语言练习题里有很多类似案例比如“逆序输出”、“求最大值及其下标”、“求一批整数中出现最多的数字”。这些题表面上简单但特别训练数组下标处理能力和边界敏感度强烈建议逐个写完并提交到PTA或者OJ系统里看测试点是否全过。6. 常见问题速查与排错技巧很多初学者调试数组程序会觉得“程序没报错但结果就是错的”——这种隐蔽问题往往比编译错误更让人抓狂。以下是我见过的最高频的几种整理成速查表。症状原因排查与解决输出一堆垃圾值局部数组未初始化显式初始化或用循环赋值字符串输出带乱码字符数组末尾没\0手动补\0或用双引号初始化函数里sizeof(arr)结果异常数组形参退化为指针额外传长度参数不能用sizeof修改字符串时崩溃指向字符串字面量的指针被修改改用char str[]数组越界但没报错C不做边界检查靠开发者自觉结合地址调试检查循环边界冒泡排序只排了一半内层循环边界忘减i把条件改成j n - 1 - i逆序输出顺序不对起始下标算错把\0也输出用strlen先取长度从len-1开始输入带空格的字符串被截断用了scanf(%s)改用fgets或scanf(%[^\n])下面挑几个典型的展开讲讲。6.1 数组越界C语言不给你兜底C语言不像Java的ArrayIndexOutOfBoundsException它不会在越界时主动报错而是让程序继续运行访问内存中数组旁边的数据。这意味着越界读取可能返回垃圾值越界写入可能覆盖其他变量的值甚至导致段错误Segmentation fault。排查越界最好的方法是在循环里打印下标或者用调试器gdb设置断点观察。写循环时问自己一句“最后一次循环时下标是否还落在[0, n-1]区间内”很多边界bug都能靠这一问解决。6.2 数组长度计算动态传参的坑再看一次sizeof陷阱。在main里sizeof(scores)是总字节数在函数里就变成了指针大小。解决办法是定义宏#define ARR_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))但注意这个宏同样不能在函数形参里使用因为形参已经是退化的指针。你不妨学一个习惯写数组工具函数时除非有明确的NUL终止标记否则永远把长度一起传进来。6.3 调试工具肉眼找bug不如打印信息初学者遇到程序结果不对第一反应是“重新看一遍代码”但十几行还好几十行以上就容易视觉疲劳。我强烈建议用printf插桩法在关键节点打印变量的值和数组的内容。比如冒泡排序每趟排序后打印一遍数组你很快就能定位是哪一趟开始不对的。用gdb的话可以break到第某行用print arr[0]10查看连续10个元素的值方便观察。这在处理越界问题时尤其好用。7. 学习路径与练习建议一维数组刷题可以先按“基础遍历——统计模拟——排序检索——字符串处理——综合应用”这条线走。基础遍历至少要练正序输出、逆序输出、求最值、求平均值、统计满足条件的元素个数。统计模拟类的经典题包括统计各分数段人数、投票统计、桶计数排序。排序检索类的入门题就是冒泡、选择、二分查找用数组存储有序数据时。字符串处理则主要围绕字符数组的遍历和修改。推荐练习题来源有以下几类全部免费可访问翁恺老师《程序设计入门——C语言》慕课的课后编程题题目偏基础适合热身PTA平台上的“C语言程序设计”题库测试点特别严密会逼你考虑各种边界条件教材《C程序设计第五版》每章后面的上机题虽然有点老但经典题型齐全自己改题拿一个成绩数组要求改成“去掉最高分和最低分再求平均分”这个改动看似不大却把下标处理和统计逻辑都练到了我个人的体会是数组这道坎跨过之后后面指针、结构体都会通畅很多。很多同学学到指针觉得难其实是因为数组和地址之间的关系没想透。你只要在纸上画出那个连续内存格子图每个格子标上地址和下标亲自走一遍a[i]的寻址过程很多困惑自然就解开了。最后再分享一个小技巧写任何数组处理代码前先在注释里写出“输入是什么、输出是什么、边界是什么”比如“n在1到100之间”、“数组升序后输出”。看上去多花了几秒钟但能帮你少写好几十行debug代码。数组越界、下标从1开始、漏掉\0这类问题大多是在“边界是什么”这一步就能被提前掐死的。