CC语言通关
第06课 · 数组
教材对照
第6章 · 书页 139–166

第06课 · 数组

一维/二维数组、字符数组、字符串结束标志、字符串处理函数

9 个小节 75 段代码 建议 2h
教材依据第6章《利用数组处理批量数据》书页 p.139–166
本课覆盖:教材第6章正文 + 书末习题(p.165–166)。标「教材 p.X」= 教材段落总结;「例题」= 教材例题;「习题」= 书末题目;「本课补充」= 非教材原文。
在全书对照表中查看 →

教材《C程序设计》第五版(谭浩强)第6章,书页 139–166,共 28 页|建议 2h


课前一句话

📖 本课补充 · 非教材原文

前五课,你手里只有一种容器——变量:一个名字装一个数。这一课换一种容器——数组:一个名字装一串同类型的数,用编号取用。学完这一课,你能徒手写出:冒泡排序、矩阵转置、杨辉三角、单词统计、字符串的比较/复制/连接——期末卷上"批量数据"那一整片题,全在这一课。


一、先补地基:本章要解决的问题

📖 本课补充 · 非教材原文

1.1 一个真实的问题

一个班 30 个学生,每人一个成绩,求平均分。

理论上简单:加起来除以 30。真正的问题是——这 30 个成绩在程序里怎么表示?

用你现在的本事,只能这样:

c
float s1, s2, s3, ..., s30;

书上一针见血地指出两个毛病:

  1. 烦琐。要定义 30 个变量;如果有 1000 名学生怎么办?
  2. 没有反映出这些数据间的内在联系。这 30 个数明明是"同一个班、同一门课"的成绩,属性完全相同,却被拆成 30 个互不相干的孤儿。

1.2 换个思路:从"起名字"到"编号码"

想象学校走廊里的一排储物柜:

  • 整排柜子只有一个名字:比如"A 区柜";
  • 每个柜子有自己的编号:0 号、1 号、2 号……;
  • 柜子规格一样,只放同一种东西(都放书,不能一个放书、一个放自行车)。

于是 30 个成绩就变成:一个叫 s 的柜区,30 个柜子,编号 0~29。第 15 个柜子里的成绩就是 s[15]。

书上原话:

一批具有同名的同属性的数据就组成一个数组(array),s 就是数组名。这个右下角的数字称为下标(subscript)。

这个比喻整节课都用它:数组 = 一排柜子,下标 = 柜号,元素 = 柜子里的东西。

1.3 数组的三条"身份证"

后面所有规则都是这三条的推论:

序号教材原话大白话
(1)数组是一组有序数据的集合,下标代表数据在数组中的序号一排柜子,编号有顺序
(2)用一个数组名和下标来唯一地确定数组中的元素报"名字 + 柜号"精准取到那一格
(3)数组中的每一个元素都属于同一个数据类型柜子规格相同,只放同一种东西

第 (3) 条书上举例:学生的成绩和学生的性别不能放进同一个数组。

1.4 为什么键盘上写成方括号

计算机键盘只能输入有限的单个字符,打不出右下角的小字。所以 C 规定:用方括号里的数字表示下标。s₁₅ 就写成 s[15]。

把数组和循环结合,就能高效处理大批量数据——这就是本章标题的由来。


二、知识点讲解

6.1 怎样定义和引用一维数组

📖 教材 p.139–144 · 正文总结

一维数组的元素只需数组名 + 一个下标就能唯一确定。掌握它,二维、多维都是举一反三。

6.1.1 怎样定义一维数组

📖 教材 p.140 · 正文总结

一句话定义:定义数组,就是提前通知计算机——我要一排柜子,叫什么名、有几个、每个放什么类型。

拆名字:array(数组)= 一排、一列;subscript(下标)= sub(下面)+ script(写),写在右下角的小字。

生活比喻:去储物间领柜子,管理员要登记三件事——柜区名、柜子数量、每个柜子装什么。三样不齐,不给钥匙。

怎么用:

c
int a[10];

表示:定义了一个整型数组,数组名为 a,包含 10 个整型元素。

一般形式:

c
类型说明符 数组名[常量表达式];

书上三条说明:

  1. 数组名的命名规则和变量名相同,遵循标识符命名规则。
  2. 方括号中的常量表达式表示元素的个数(数组长度)。int a[10]; 有 10 个元素,下标从 0 开始,即 a[0]~a[9],不存在 a[10]。
  3. 常量表达式中可以包括常量和符号常量,int a[3+5]; 合法;不能包含变量,int a[n]; 不合法——C 语言不允许对数组大小作动态定义。

内存图(书图 6.1):int a[10]; 在内存中划出一片连续空间。用 Visual C++ 时一个 int 占 4 字节,共 40 字节:

c
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]

一句话总结:int a[10]; 相当于一次定义了 10 个整型变量,简捷方便。

易错 int a[10]; 的合法下标是 0~9,a[10] 是越界——本章第一个、最常考的坑。

【选学·小注】 C99 确有"变长数组"(VLA),但 MSVC 不支持,教材和期末考试都按"方括号里必须是常量表达式"判。考试按书本答。

6.1.2 怎样引用一维数组元素

📖 教材 p.140–141 · 正文总结

一句话定义:引用数组元素就是"报柜号取东西",形式是 数组名[下标]。

生活比喻:柜区名 + 柜号 = 唯一的一格。只说"A 区"没用,必须说"A 区 3 号"。

怎么用:a[0] 就是序号为 0 的元素。它和一个简单变量的地位、作用相似——可被赋值,也可参加运算。下标可以是整型常量或整型表达式:

c
a[0] = a[5] + a[7] - a[2*3];

定义 vs 引用(书上专门强调:形式相同,含义不同):

写法出现场合含义
int a[10];前面有类型名 int定义数组,指定包含 10 个元素
t = a[6];前面没有类型名引用序号为 6 的元素

易错 只能引用元素,不能一次整体调用整个数组全部元素的值。

易错 int a[10]; 最大下标是 9。书上给的常见错误:

c
for (i = 1; i <= 10; i++)
    a[i] = i;          // 错:a[10] 越界,a[0] 没被赋值

6.1.3 一维数组的初始化

📖 教材 p.142 · 正文总结

一句话定义:在定义数组的同时给元素赋初值,叫初始化,用花括号里的"初始化列表"实现。

拆名字:initial(最初的)+ ization(过程)= 初始化。

生活比喻:领柜子的同时,管理员直接往柜子里放好第一批东西;没提到的柜子,统一塞 0。

四条规则:

(1)对全部元素赋初值

c
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

花括号内称"初始化列表",数据间用逗号分隔。执行后 a[0]=0……a[9]=9。

(2)只给一部分元素赋值

c
int a[10] = {0, 1, 2, 3, 4};

只给前 5 个元素赋初值,系统自动给后 5 个元素赋初值为 0。

(3)全部元素值为 0

c
int a[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

或简写:

c
int a[10] = {0};      // 未赋值的部分元素自动设定为 0

(4)全部赋初值时可不指定长度

c
int a[5] = {1, 2, 3, 4, 5};    // 可写成 int a[] = {1,2,3,4,5};

系统数一数有 5 个数,就定长度为 5。但如果数组长度与初值个数不相同,方括号中的长度不能省略——想定义长度 10、只初始化前 5 个,必须写 int a[10] = {1,2,3,4,5};。

书上"说明"(考点):定义数值型数组时指定了长度并初始化,凡未被"初始化列表"指定初始化的元素,系统会自动初始化:

数组类型未初始化元素自动变成
数值型(int/float…)0
字符型(char)'\0'
指针型NULL(空指针)

易错 这条只管"定义时带了初始化列表"的数组。写成 int a[10];(只定义、不初始化),各元素是不可预料的垃圾值,不是 0。选择题常考。

6.1.4 一维数组程序举例

📖 教材 p.142–144 · 正文总结

本节三个例题的完整代码见第三节,这里先抽"算法骨头":

例题题目算法要点
例 6.1对 10 个元素赋 0~9,再逆序输出for(i=9;i>=0;i--) 倒着走
例 6.2数组求 Fibonacci 数列前 20 项f[i]=f[i-2]+f[i-1]
例 6.310 个地区面积按从小到大排序冒泡法(起泡法)

冒泡排序——本章第一号考点:

  • 排序(sort)分两种规律:升序(从小到大)、降序(从大到小)。
  • 基本思路:每次将相邻两个数比较,将小的调到前面。
  • 书上用 6 个数 9,8,5,4,2,0 演示:第 1 趟比较 5 次,最大的 9 一路"沉底"成为最后一个数,最小的 0 向上"浮起"一位;第 2 趟对余下前 5 个数比 4 次,次大的 8 沉底……
  • 规律(背下来):

有 n 个数,要进行 n-1 趟比较;第 1 趟比 n-1 次,第 j 趟比 n-j 次。

  • 为什么叫"冒泡":最小的数每经一趟就"上升"一位,最后升到第一个数,如同水底的气泡逐步冒出水面。

易错 两层循环的边界:外层 for(j=0;j<9;j++) 共 9 趟(n-1),内层 for(i=0;i<9-j;i++) 每趟 9-j 次(n-j)。少一层、多一层都会错。

说明:排序算法有多种,本例是起泡法,常用的还有选择法、希尔法。习题 2 要求用选择法。


6.2 怎样定义和引用二维数组

📖 教材 p.145–150 · 正文总结

为什么要二维:有 3 个小分队,每队 6 名队员,要保存队员的工资。一个下标只能表示"第几个队员",表示不了"第几队"——需要两个下标。

书上规定 pay[2][3] 表示"2 分队第 3 名队员的工资",值是 1725。

拆名字:matrix(矩阵)= 行列排成的方阵;row = 行;column = 列。

生活比喻:一维数组是一排柜子;二维数组是一整面墙的柜子——先报"第几排"(行),再报"第几列"。

6.2.1 怎样定义二维数组

📖 教材 p.146 · 正文总结

一句话定义:二维数组就是"每个元素又是一维数组"的数组。

怎么用:

c
float pay[3][6];

定义 float 型二维数组,第 1 维有 3 个元素,第 2 维有 6 个元素。每一维的长度各用一对方括号括起来。

一般形式:

c
类型说明符 数组名[常量表达式][常量表达式];

例如 float a[3][4], b[5][10]; ——a 是 3×4(3 行 4 列),b 是 5×10(5 行 10 列)。

易错 不能写成 float a[3,4];——两个下标必须各用一对方括号。

关键理解(书上原话):二维数组可被看作一种特殊的一维数组:它的元素又是一个一维数组。

float a[3][4]; 可把 a 看作一维数组,它有 3 个元素 a[0]、a[1]、a[2];每个元素又是一个含 4 个元素的一维数组,相当于:

c
float a[0][4], a[1][4], a[2][4];   // 此处 a[0]、a[1]、a[2] 被看作一维数组名

内存里怎么摆(重要):C 语言中二维数组元素按行存放——先顺序存第 1 行,接着存第 2 行。a[3][4] 从 2000 字节开始、每元素 4 字节,则 2000~2015 放第 0 行 4 个元素,2016~2031 放第 1 行,余类推。

注意:用矩阵形式(3 行 4 列)表示二维数组是逻辑上的概念;内存中各元素是连续存放的,不是二维的,是线性的。

多维数组:float a[2][3][4]; 有 2 页、3 行、4 列。排列顺序为:第 1 维下标变化最慢,最右边的下标变化最快。

易错 float a[3][4]; 是 3 行 4 列,不是 4 行 3 列。行数 = 第一个方括号,列数 = 第二个。

6.2.2 怎样引用二维数组的元素

📖 教材 p.147 · 正文总结

一句话定义:数组名[下标][下标],两个下标一起才能定位一格。

c
a[2][3]      // 序号为 2 的行中、序号为 3 的列的元素

下标应是整型表达式,如 a[2-0][2*2-0]。

易错 不要写成 a[2,3]——逗号表达式只算最后一个值,看起来能编过,意思全错。

【易错·越界】 定义 int a[3][4]; 后,"行下标"范围 0~2,"列下标"范围 0~3。a[3][4]=3; 越界——不存在 a[3][4] 元素。

书上专门提醒:严格区分定义时的 a[3][4] 与引用时的 a[3][4]。前者说明维数和各维大小;后者中的 3 和 4 是下标值(都从 0 起算)。

6.2.3 二维数组的初始化

📖 教材 p.148 · 正文总结

一句话定义:用花括号给初值,可以分行给,也可以按内存顺序一口气给。

生活比喻:往一面墙的柜子里放东西——最好一排一排地放(分行),也可以按"先第一排从左到右、再第二排"的顺序一股脑列出来。

四条规则:

(1)分行给二维数组赋初值

c
int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

第 1 个花括号给第 1 行,第 2 个给第 2 行……即按行赋初值,直观。

(2)所有数据写在一个花括号内,按内存排列顺序赋值

c
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

效果与(1)相同。但以第(1)种为好,一行对一行,界限清楚;第(2)种数据多时写成一大片,容易遗漏也不易检查。

(3)可以对部分元素赋初值

c
int a[3][4] = {{1},{5},{9}};        // 只给各行第 0 列
c
1  0  0  0
5  0  0  0
9  0  0  0

也可对各行的某一元素赋值:

c
int a[3][4] = {{1},{0,6},{0,0,11}};
c
1  0  0   0
0  6  0   0
0  0  11  0

也可只对某几行赋值:

c
int a[3][4] = {{1},{5,6}};          // 第 3 行不赋初值,全 0

也可对第 2 行不赋初值:

c
int a[3][4] = {{1},{},{9}};

(4)全部元素赋初值时,第 1 维长度可省,第 2 维长度不能省

c
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
// 等价于
int a[][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

系统按"数据总个数 ÷ 第 2 维长度"算第 1 维:12÷4=3 行。

定义时也可只对部分元素赋初值而省略第 1 维,但应分行赋初值:

c
int a[][4] = {{0,0,3},{},{0,10}};   // 编译系统据此知道有 3 行
c
0  0  3   0
0  0  0   0
0  10 0   0

一张表记住"能不能省":

写法第 1 维第 2 维说明
int a[3][4] = {...};可省不能省全部赋初值才能省第 1 维
int a[][4] = {...};省略必须写系统按"总个数 ÷ 列数"算行数
int a[][4] = {{...},{},{...}};省略必须写部分赋值也可省,但必须分行

易错 第 2 维长度永远不能省。写 int a[][] = {...}; 编译器不知道一行几个元素,直接报错。

6.2.4 二维数组程序举例

📖 教材 p.149–150 · 正文总结
例题题目算法要点
例 6.4二维数组行和列的元素互换,存到另一个数组b[j][i] = a[i][j];——行列下标对调
例 6.53×4 矩阵中值最大的元素及行号列号"打擂台"算法

打擂台算法(书上比喻):先找一人站在台上,第 2 人上去比武,胜者留台;第 3 人再与台上的人比……直到所有人比过,最后留在台上的就是冠军。

对应程序:先让 a[0][0] 作"擂主",值赋给 max;然后逐个元素与 max 比,比 max 大就取代它,同时记下行号 row、列号 column。全部比完,max 就是最大值。

考点 打擂台三件套:max(当前最大值)、row(行号)、column(列号),一次遍历同时更新。


6.3 字符数组

📖 教材 p.151–164 · 正文总结

铺垫(书上原话):字符型数据以字符的 ASCII 代码存储在存储单元中,一般占一个字节。由于 ASCII 代码也属于整数形式,C99 标准把字符类型归纳为整型类型中的一种。

C 语言中没有字符串类型,也没有字符串变量,字符串是存放在字符型数组中的。

这句话是本章的"总纲",后面所有字符串规则都从它来。

6.3.1 怎样定义字符数组

📖 教材 p.151 · 正文总结

一句话定义:用来存放字符数据的数组是字符数组,其中一个元素内存放一个字符。

怎么用:

c
char c[10];
c[0] = 'I';  c[1] = ' ';  c[2] = 'a';  c[3] = 'm';  c[4] = ' ';
c[5] = 'h';  c[6] = 'a';  c[7] = 'p';  c[8] = 'p';  c[9] = 'y';

赋值后数组状态(图 6.11):I ␣ a m ␣ h a p p y,正好 10 个元素。

由于字符型数据以整数形式(ASCII 代码)存放,也可以用整型数组存字符:int c[10]; c[0]='a';——合法,但浪费存储空间(int 占 4 字节,char 只占 1 字节)。

易错 用 int 数组存字符能编过,但别这么写:既浪费空间,又容易在输出时用错格式符。

6.3.2 字符数组的初始化

📖 教材 p.152 · 正文总结

一句话定义:用"初始化列表"把各个字符依次赋给数组中的各元素。

c
char c[10] = {'I',' ','a','m',' ','h','a','p','p','y'};

四条规则:

  1. 定义字符数组时不初始化,则各元素的值是不可预料的。
  2. 初值个数大于数组长度 → 语法错误。
  3. 初值个数小于数组长度 → 只赋给前面那些元素,其余元素自动定为空字符 '\0'。
  4. 初值个数与数组长度相同 → 定义时可省略长度:char c[] = {'h','a','p','p','y'};(长度自动定为 5)。

考点 字符数组初始化时,初值个数少于长度 → 其余自动 '\0'。这条是 6.3.4 理解 '\0' 的基础,也是选择题常客。

二维字符数组也可以定义并初始化(例 6.7 的菱形):

c
char diamond[5][5] = {
    {' ',' ','*',' ',' '},
    {' ','*',' ','*',' '},
    {'*',' ',' ',' ','*'},
    {' ','*',' ','*',' '},
    {' ',' ','*',' ',' '}
};

6.3.3 怎样引用字符数组中的元素

📖 教材 p.152 · 正文总结

一句话定义:c[i] 引用一个元素,得到的是一个字符——和引用数值数组元素没区别。

例 6.6(输出已知字符串)和例 6.7(输出菱形)都是"逐个元素处理",代码见第三节。

考点 例 6.7 的二维字符数组初始化写法容易考:每行 5 个字符,空格也要老老实实写出来,少写一个图形就歪。

6.3.4 字符串和字符串结束标志

📖 教材 p.153–155 · 正文总结

这一小节是整章的心脏。

问题:字符数组长度 100,实际有效字符只有 40 个,程序怎么知道字符串到哪儿结束?

答案:C 语言规定了一个"字符串结束标志",以字符 '\0' 作为结束标志。

在遇到字符 '\0' 时,表示字符串结束,把它前面的字符组成一个字符串。

拆名字:'\0' 读作 null character(空字符),null = 空、无。

生活比喻:一排柜子放了一串字,最后放一张"到此为止"的牌子。程序取字符串时不看柜子总数,只看牌子在哪。

三条要点:

  1. 系统会自动加 '\0':C 系统用字符数组存储字符串常量时会自动加一个 '\0'。例如 "C program" 有 9 个字符,在数组中占 10 个字节,最后那个 '\0' 是系统加的。
  2. 有了 '\0',数组长度就不那么重要了:程序靠检测 '\0' 的位置判定字符串是否结束,不是根据数组长度。但定义时仍应保证数组长度始终大于字符串实际长度;若先后存放多个不同长度的字符串,应大于最长的那个。
  3. '\0' 是什么:代表 ASCII 码为 0 的字符,它不是可显示字符,而是一个"空操作符",什么也不做。用它作结束标志不会产生附加操作、不增加有效字符,只起一个供辨别的标志作用。

printf 怎么知道停:执行 printf("How do you do? \n"); 时,系统在内存中自动在 '\n' 后面加 '\0'。printf 每输出一个字符检查一次,遇 '\0' 就停止输出。

用字符串常量初始化:

c
char c[] = {"I am happy"};
char c[] = "I am happy";        // 可省略花括号

字符串两端用双撇号,不是单撇号。

这里有个大坑(必考):

  • char c[] = "I am happy"; → 数组长度 11(字符串常量最后系统加 '\0')。
  • char c[] = {'I',' ','a','m',' ','h','a','p','p','y'}; → 数组长度 10。

两者不等价。等价写法是 char c[] = {'I',' ','a','m',' ','h','a','p','p','y','\0'};(长度 11)。

再看:char c[10] = {"China"}; → 前 5 个元素 C h i n a,第 6 个 '\0',后 4 个自动空字符。

说明(书上原话):字符数组并不要求最后一个字符为 '\0',甚至可以不包含 '\0'。像 char c[5] = {'C','h','i','n','a'}; 完全合法。是否需要加 '\0' 完全根据需要决定。但为处理方法一致、便于测定实际长度,常常人为加上 '\0'。

为什么必须加——经典反例:原数组 char c[] = {"C program."};,想用新串 "Hello" 代替,分别赋给前 5 个元素。不加 '\0',新旧串连成一片:

c
H e l l o g r a m .

输出就成了 Hellogram.。在 "Hello" 后加一个 '\0'(取代第 6 个字符),输出遇 '\0' 停止,只输出 Hello。

一句话:'\0' 是字符串的句号。没有句号,句子就会和下一句粘在一起。

考点 两个必背数字:char c[]="I am happy"; 长度 11(系统自动加 '\0');char c[]={'I',' ','a','m',' ','h','a','p','p','y'}; 长度 10。strlen 数到 '\0' 为止,不算 '\0';sizeof 把 '\0' 算进去。

6.3.5 字符数组的输入输出

📖 教材 p.156 · 正文总结

两种方法:

  1. 逐个字符输入输出:用格式符 %c 输入或输出一个字符(如例 6.6)。
  2. 将整个字符串一次输入或输出:用 %s 格式符,意思是字符串(string)的输入输出。
c
char c[] = {"China"};
printf("%s\n", c);      // 输出 China

输出时遇 '\0' 就停。

书上六条说明:

  1. 输出的字符中不包括结束符 '\0'。
  2. 用 %s 时 printf 的输出项是字符数组名,不是数组元素名。printf("%s", c[0]); 是错的——c[0] 是一个数组元素,不是数组名。
  3. 数组长度大于字符串实际长度,也只输出到遇 '\0' 结束。char c[10]="China"; 只输出 China,不是 10 个字符——这就是结束标志的好处。
  4. 一个数组中有多个 '\0',遇第一个就结束。
  5. scanf("%s", c); 可以输入一个字符串。输入项 c 是字符数组名,输入的字符串应短于数组长度(char c[6]; 输入 China,系统自动加 '\0')。若一次输入多个字符串,用空格分隔:
c
char str1[5], str2[5], str3[5];
scanf("%s%s%s", str1, str2, str3);    // 输入 How are you?

三个数组分别为 How\0\0、are\0\0、you?\0;未赋值的元素自动置 '\0'。

但如果只想读一整行:

c
char str[13];
scanf("%s", str);        // 输入 How are you? → 只得到 How

系统把空格作为字符串之间的分隔符,只把空格前的 How 送到 str,并在其后加 '\0'。

考点scanf("%s", ...) 遇空格就停,只读得到一个"词";想读整行(含空格)要用 gets。

  1. 【必考】 scanf 的输入项如果是字符数组名,不要再加地址符 &,因为数组名代表该数组第一个元素的地址(起始地址)。下面写法不正确:
c
scanf("%s", &str);       // 错:str 前面不应加 &

printf("%s", c) 到底怎么执行:按数组名 c 找到第一个元素的地址,然后逐个输出字符,直到遇 '\0' 为止。

一张表总结"能不能加 &":

输入对象写法说明
单个 intscanf("%d", &x);要加 &
单个 charscanf("%c", &ch);要加 &
字符数组(字符串)scanf("%s", str);不加 &(数组名本身是地址)

易错 把 scanf("%s", &str) 写熟了就改不过来——上机最容易犯的错。

6.3.6 使用字符串处理函数

📖 教材 p.157–161 · 正文总结

一句话定义:C 函数库提供了一批专门处理字符串的现成函数,要用就先写 #include <string.h>。

生活比喻:这 8 个函数就像柜房门口的 8 个搬运工——有的负责把字串搬进柜子、有的负责搬出来、有的负责量长度、有的负责比大小。

拆名字(全是"str + 功能缩写"):

函数全称拆开记
strcatSTRing CATenatestr + cat(连接)
strcpySTRing CoPYstr + cpy(复制)
strcmpSTRing CoMParestr + cmp(比较)
strlenSTRing LENgthstr + len(长度)
strlwrSTRing LoWeRcasestr + lwr(小写)
struprSTRing UPpeRcasestr + upr(大写)
putsput string输出字符串
getsget string读入字符串

总表(背这张就够):

函数一般形式作用返回值易错
putsputs(字符数组)输出一个字符串,自动换行(把 '\0' 转成 '\n')—只能一个字符串
getsgets(字符数组)读入一整行到数组数组起始地址只能一个字符串
strcatstrcat(s1, s2)把 s2 接到 s1 后面,结果在 s1s1 的地址s1 必须够大;s1 的 '\0' 被取消
strcpystrcpy(s1, s2)把 s2 复制到 s1s1 的地址s1 必须够大;不能用 s1=s2
strncpystrncpy(s1, s2, n)复制 s2 前 n 个字符s1n 不应多于 s1 原有字符数
strcmpstrcmp(s1, s2)按 ASCII 逐字符比较0 / 正 / 负不能写 s1>s2
strlenstrlen(s)测实际长度长度(不含 '\0')不是数组长度
strlwrstrlwr(s)转小写s非标准,VS 才有
struprstrupr(s)转大写s非标准,VS 才有

几个必须展开说的:

  • puts:输出时可包含转义字符,如 char str[]={"China\nBeijing"}; puts(str); 输出两行。它在输出时将 '\0' 转换成 '\n',即输出完自动换行。
  • gets:从终端输入一个字符串到数组,函数值是数组的起始地址。输入 Computer 时"送给数组的共有 9 个字符,而不是 8 个"(含 '\0')。gets 和 puts 都只能处理一个字符串,不能写 gets(str1, str2);。
  • strcat:char str1[30]={"People's Republic of "}; char str2[]={"China"}; printf("%s", strcat(str1,str2)); → People's Republic of China。s1 必须足够大;连接时s1 后面的 '\0' 被取消,只在新串最后保留一个。
  • strcpy:s1 必须是数组名(不能是常量),s2 可以是数组名或字符串常量:strcpy(str1,"China");。复制时把 s2 的字符连同其后的 '\0' 一起复制过去。 易错 字符数组名是地址常量,不能被赋值:
c
  str1 = "China";      // 错:企图用赋值语句把字符串常量赋给字符数组
  str1 = str2;         // 错:企图把一个字符数组直接赋给另一个

只能用 strcpy。用赋值语句只能把一个字符赋给一个字符变量或数组元素:a[0]='C';。

  • strcmp 比较规则:两个字符串自左至右逐个字符相比(按 ASCII 码值),直到出现不同字符或遇 '\0' 为止。全部相同则相等;否则以第 1 对不相同字符的比较结果为准。 例:"A"<"B"、"a">"A"、"computer">"compare"、"these">"that"、"CHINA">"CANADA"、"DOG"<"cat"。 简单规律:都由英文字母组成时,在英文字典中位置在后面的为"大";但小写字母比大写字母"大",所以 "DOG"<"cat"。 返回值:相等 → 0;s1>s2 → 正整数;s1<s2 → 负整数。 易错 不能用 if(str1>str2),因为 str1、str2 代表地址;只能用 if(strcmp(str1,str2)>0)。
  • strlen:char str[10]="China"; printf("%d",strlen(str)); 输出不是 10,也不是 6,而是 5。也可直接测常量:strlen("China")。
  • strlwr / strupr选学:分别把串中大写转小写、小写转大写。它们不是 C 标准库函数,只有部分编译器(含 VC/VS)提供,换编译器可能找不到。考试按书上答,自己写代码建议用 toupper/tolower(第 7 章讲)。

书上总结:库函数并非 C 语言本身的组成部分,而是编译系统为方便用户提供的公共函数。不同编译系统提供的函数数量和函数名、功能都不尽相同,使用时要小心,必要时查库函数手册。

6.3.7 字符数组应用举例

📖 教材 p.162–164 · 正文总结

例 6.8:输入一行字符,统计有多少个单词,单词之间用空格分隔。

关键:怎么确定"出现一个新单词了"?

  • 从第 1 个字符开始逐个检查,判断它是否是新单词的开头,是就使 num 加 1。
  • 判断依据:某字符为非空格,而它前面的字符是空格 → "新的单词开始了";当前非空格、前面也非空格 → 仍是原单词的继续。
  • 用 word 作标志:word=0 表示前一个字符是空格,word=1 表示前一个字符是非空格。
当前字符word 原值判断动作
空格任意—word=0,num 不加
非空格word==0(前一个是空格)新单词开始word=1,num++
非空格word==1(前一个非空格)还是原单词什么都不做

书上用 I am a boy. 检验,最后 num=4。

例 6.9:有 3 个字符串,找出其中"最大"者。

关键:设二维字符数组 str[3][20](3 行 20 列,每行一个字符串)。把它看成 3 个一维字符数组,用 gets 分别读入,两两比较得到最大者,放进一维数组 string。


6.4 书末习题(书页 165–166)

📖 教材 p.165–166 · 习题

本章共 15 道编程题,题目原文、思路和完整代码见第四节。先给你一张"考点地图":

题号题目大意考点
1筛选法求 100 之内的素数数组标记、按倍数划掉
2选择法对 10 个整数排序选择排序(与冒泡对比)
33×3 矩阵对角线元素之和a[i][i]
4有序数组插入一个数从后往前挪位
5数组逆序重新存放首尾交换 n/2 次
6杨辉三角形(10 行)a[i][j]=a[i-1][j-1]+a[i-1][j]
7魔方阵(1~n²)奇数阶构造法
8二维数组的鞍点行最大 + 列最小
9折半查找(15 个数降序)low/high/mid
103 行 80 字符分类统计字符 ASCII 区间判断
11输出图案循环控制空格与星号
12电文译码(A→Z, B→Y…)'A'+'Z'-c
13连接两个字符串(不用 strcat)找尾、逐个搬
14比较两个字符串(不用 strcmp)返回 ASCII 差值
15复制字符串(不用 strcpy)连 '\0' 一起复制

考点 第 6(杨辉三角)、7(魔方阵)、8(鞍点)、9(折半查找)是四道"经典算法题",期末和考研都爱从这里抽。


三、现场写代码

📖 教材例题 · 本课范围 p.139–164

下面把书上 9 个例题走一遍:题目 → 思路 → 完整代码 → 逐行解释 → 运行结果。代码全部实测可编译运行。

例 6.1 逆序输出 10 个数

📖 教材 p.141 · 例题 6.1

题目:对 10 个数组元素依次赋值为 0~9,要求按逆序输出。

思路:定义长度 10 的整型数组;值有规律,用循环赋值;输出时下标从大到小。

c
#include <stdio.h>
int main()
{
    int i, a[10];
    for (i = 0; i <= 9; i++)
        a[i] = i;                    // 对数组元素 a[0]~a[9] 赋值
    for (i = 9; i >= 0; i--)
        printf("%d ", a[i]);         // 输出 a[9]~a[0] 共 10 个数组元素
    printf("\n");
    return 0;
}

逐行解释:

  • int i, a[10]; —— 循环变量 i,加上能装 10 个 int 的数组 a。
  • for (i = 0; i <= 9; i++) —— i 从 0 到 9,正好是合法下标全集。
  • a[i] = i; —— 第 i 号柜子放 i。
  • for (i = 9; i >= 0; i--) —— 倒着走:从最后一个开始。
  • printf("%d ", a[i]); —— 每个元素后跟一个空格。

运行结果:9 8 7 6 5 4 3 2 1 0

书上"常见错误":for(i=1;i<=10;i++) a[i]=i; —— a[0] 没赋值,a[10] 越界。

例 6.2 用数组求 Fibonacci 数列前 20 项

📖 教材 p.142 · 例题 6.2

题目:用数组处理 Fibonacci 数列问题。

思路:第 5 章用简单变量只能"算一个扔一个",不能在内存中保存,想直接输出第 25 个数很困难。用数组后概念上反而简单:每个元素代表数列中的一个数,依次求出并存放即可。

c
#include <stdio.h>
int main()
{
    int i;
    int f[20] = {1, 1};                  // 对最前面两个元素 f[0] 和 f[1] 赋初值 1
    for (i = 2; i < 20; i++)
        f[i] = f[i - 2] + f[i - 1];      // 先后求出 f[2]~f[19] 的值
    for (i = 0; i < 20; i++)
    {
        if (i % 5 == 0) printf("\n");    // 控制每输出 5 个数后换行
        printf("%12d", f[i]);            // 输出一个数
    }
    printf("\n");
    return 0;
}

逐行解释:

  • int f[20] = {1, 1}; —— 20 个元素,只给前两个初值,其余 18 个自动为 0(后面会被覆盖)。
  • for (i = 2; i < 20; i++) —— 从下标 2 开始递推。
  • f[i] = f[i - 2] + f[i - 1]; —— 递推公式:前两项之和。
  • if (i % 5 == 0) printf("\n"); —— i=0、5、10、15 时先换行,每行 5 个。
  • printf("%12d", f[i]); —— 每个数占 12 格,右对齐,整齐。

运行结果:

c
           1           1           2           3           5
           8          13          21          34          55
          89         144         233         377         610
         987        1597        2584        4181        6765

考点 int f[20]={1,1}; 正是 6.1.3 第 2 条的应用:没写的元素自动为 0。

例 6.3 冒泡法排序(10 个地区的面积)

📖 教材 p.143 · 例题 6.3

题目:有 10 个地区的面积,要求按由小到大排序。

思路:这是排序(sort)。本例用起泡法:每次将相邻两个数比较,将小的调到前面。

c
#include <stdio.h>
int main()
{
    int a[10];
    int i, j, t;
    printf("input 10 numbers :\n");
    for (i = 0; i < 10; i++)
        scanf("%d", &a[i]);             // 输入 10 个数给 a[0]~a[9]
    printf("\n");
    for (j = 0; j < 9; j++)             // 进行 9 次循环,实现 9 趟比较
        for (i = 0; i < 9 - j; i++)     // 在每一趟中进行 9-j 次比较
            if (a[i] > a[i + 1])        // 相邻两个数比较
            {
                t = a[i];
                a[i] = a[i + 1];
                a[i + 1] = t;           // 交换 a[i] 与 a[i+1]
            }
    printf("the sorted numbers :\n");
    for (i = 0; i < 10; i++)
        printf("%d ", a[i]);
    printf("\n");
    return 0;
}

逐行解释:

  • scanf("%d", &a[i]); —— a[i] 是元素,要加 &(和字符串不同!)。
  • for (j = 0; j < 9; j++) —— 外层管"趟数":n=10 时共 n-1=9 趟。
  • for (i = 0; i < 9 - j; i++) —— 内层管"每趟比几次":第 j 趟比 9-j 次(最后 j 个数已排好)。
  • if (a[i] > a[i + 1]) —— 前面比后面大就换(升序)。
  • t = a[i]; a[i] = a[i+1]; a[i+1] = t; —— 三只杯子交换法,顺序不能乱,否则丢值。

运行结果(书上数据):

c
input 10 numbers :
34 67 90 43 124 87 65 99 132 26
the sorted numbers :
26 34 43 65 67 87 90 99 124 132

考点 n 个数 → n-1 趟;第 j 趟 → n-j 次比较。

例 6.4 矩阵转置

📖 教材 p.149 · 例题 6.4

题目:将一个二维数组行和列的元素互换,存到另一个二维数组中。如 a=[1 2 3; 4 5 6](2×3)→ b=[1 4; 2 5; 3 6](3×2)。

思路:a 为 2 行 3 列,b 为 3 行 2 列。只要把 a[i][j] 存到 b[j][i] 即可,用嵌套 for 完成。

c
#include <stdio.h>
int main()
{
    int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
    int b[3][2], i, j;
    printf("array a:\n");
    for (i = 0; i <= 1; i++)                 // 处理 a 数组中的一行中各元素
    {
        for (j = 0; j <= 2; j++)             // 处理 a 数组中某一列中各元素
        {
            printf("%5d", a[i][j]);          // 输出 a 数组的一个元素
            b[j][i] = a[i][j];               // 将 a 元素的值赋给 b 相应元素
        }
        printf("\n");
    }
    printf("array b:\n");
    for (i = 0; i <= 2; i++)
    {
        for (j = 0; j <= 1; j++)
            printf("%5d", b[i][j]);          // 输出 b 数组的一个元素
        printf("\n");
    }
    return 0;
}

逐行解释:

  • int a[2][3] = {{1,2,3},{4,5,6}}; —— 2 行 3 列,分行初始化。
  • int b[3][2]; —— 转置后是 3 行 2 列。
  • for (i = 0; i <= 1; i++) / for (j = 0; j <= 2; j++) —— 遍历 a 的每个元素。
  • b[j][i] = a[i][j]; —— 核心一行:行列下标对调,这就是"转置"。
  • 输出 b 时行列范围正好和 a 相反(3 行 2 列)。

运行结果:

c
array a:
    1    2    3
    4    5    6
array b:
    1    4
    2    5
    3    6

例 6.5 求 3×4 矩阵中的最大值

📖 教材 p.150 · 例题 6.5

题目:有一个 3×4 的矩阵,求出值最大的元素及其行号、列号。

思路:打擂台算法。先让 a[0][0] 作"擂主",值赋给 max;逐个元素比,大的取代 max,同时记下行号、列号。

c
#include <stdio.h>
int main()
{
    int i, j, row = 0, column = 0, max;
    int a[3][4] = {{1, 2, 3, 4}, {9, 8, 7, 6}, {-10, 10, -5, 2}};   // 定义数组并赋初值
    max = a[0][0];                                  // 先认为 a[0][0] 最大
    for (i = 0; i <= 2; i++)
        for (j = 0; j <= 3; j++)
            if (a[i][j] > max)                      // 如果某元素大于 max,就取代 max
            {
                max = a[i][j];
                row = i;                            // 记下此元素的行号
                column = j;                         // 记下此元素的列号
            }
    printf("max=%d\nrow=%d\ncolumn=%d\n", max, row, column);
    return 0;
}

逐行解释:

  • int a[3][4] = {{1,2,3,4},{9,8,7,6},{-10,10,-5,2}}; —— 3 行 4 列共 12 个数。
  • max = a[0][0]; —— 第一个上台的"擂主"。
  • 两层循环遍历每一个元素(3×4=12 个)。
  • if (a[i][j] > max) —— 挑战者更强才换人。
  • max=a[i][j]; row=i; column=j; —— 三件事一起更新,少记一个就输出错的行列。

运行结果:

c
max=10
row=2
column=1

最大值为 10,此元素为 a[2][1]。

书上变量名写作 colum(少一个 n),是印刷习惯,不影响运行;本课统一写 column。

考点 打擂台三个变量必须同时更新:值、行、列。

例 6.6 输出一个已知的字符串

📖 教材 p.152 · 例题 6.6

题目:输出一个已知的字符串。

思路:定义字符数组,用"初始化列表"赋初值,然后用循环逐个输出。

c
#include <stdio.h>
int main()
{
    char c[15] = {'I',' ','a','m',' ','a',' ','s','t','u','d','e','n','t','.'};
    int i;
    for (i = 0; i < 15; i++)
        printf("%c", c[i]);
    printf("\n");
    return 0;
}

逐行解释:

  • char c[15] = {...}; —— 15 个柜子,每个放一个字符(含 3 个空格)。
  • for (i = 0; i < 15; i++) —— 下标 0~14,一个不漏。
  • printf("%c", c[i]); —— %c 输出一个字符(不是 %s)。

运行结果:I am a student.

例 6.7 输出一个菱形图

📖 教材 p.153 · 例题 6.7

题目:输出一个菱形图。

思路:先画出平面菱形图案,每行 5 个字符,有的是空白、有的是 *。定义字符型二维数组,用"初始化列表"按图逐行初始化,再用嵌套 for 输出所有元素。

c
#include <stdio.h>
int main()
{
    char diamond[][5] = {
        {' ', ' ', '*', ' ', ' '},
        {' ', '*', ' ', '*', ' '},
        {'*', ' ', ' ', ' ', '*'},
        {' ', '*', ' ', '*', ' '},
        {' ', ' ', '*', ' ', ' '}
    };
    int i, j;
    for (i = 0; i < 5; i++)
    {
        for (j = 0; j < 5; j++)
            printf("%c", diamond[i][j]);
        printf("\n");
    }
    return 0;
}

逐行解释:

  • char diamond[][5] = {...}; —— 二维字符数组,第 1 维省略(系统数出 5 行),第 2 维必须是 5。
  • 第 1 行 {' ',' ','*',' ',' '} —— 第 3 个位置放星。
  • 第 2 行 {' ','*',' ','*',' '} —— 2、4 位置放星(中间是空格,所以是"空心"菱形)。
  • 第 3 行 {'*',' ',' ',' ','*'} —— 1、5 位置放星。
  • 输出时两层循环把所有元素(包括空格)原样打印。

运行结果:

c
  *
 * *
*   *
 * *
  *

易错 每行 5 个字符一个都不能少;空格漏写、多写,图形就歪。

例 6.8 统计一行字符中有多少个单词

📖 教材 p.162 · 例题 6.8

题目:输入一行字符,统计其中有多少个单词,单词之间用空格分隔开。

思路:见 6.3.7 的逻辑表。核心变量:num(单词数)、word(前一个字符是不是空格)。

c
#include <stdio.h>
int main()
{
    char string[81];
    int i, num = 0, word = 0;
    char c;
    gets(string);                                   // 输入一个字符串给字符数组 string
    for (i = 0; (c = string[i]) != '\0'; i++)       // 只要字符不是 '\0' 就继续循环
        if (c == ' ') word = 0;                     // 如果是空格字符,使 word 置 0
        else if (word == 0)                         // 如果不是空格且 word 原值为 0
        {
            word = 1;                               // 使 word 置 1
            num++;                                  // num 累加 1,表示增加一个单词
        }
    printf("There are %d words in this line.\n", num);
    return 0;
}

逐行解释:

  • char string[81]; —— 最多 80 个字符 + '\0'。
  • int i, num = 0, word = 0; —— 计数器和标志都从 0 开始。
  • gets(string); —— 读一整行(包括空格)。
  • for (i=0; (c=string[i])!='\0'; i++) —— 循环条件里先赋值再判断:把当前字符取给 c,再看是不是结束符。
  • if (c == ' ') word = 0; —— 遇空格,说明"当前不在单词里"。
  • else if (word == 0) —— 当前不是空格,且刚才那个字符是空格(或开头)。
  • { word = 1; num++; } —— 新单词开始,计数加 1,并标记"现在在单词里"。

运行结果:

c
I am a boy.
There are 4 words in this line.

书上特别提醒(考点):循环条件 (c = string[i]) != '\0' 里,赋值表达式两边的括号不可缺。写成 c = string[i] != '\0',因 != 优先级高于 =,会先算 string[i] != '\0',c 得到 1 或 0,而不是字符。

例 6.9 找出 3 个字符串中的"最大"者

📖 教材 p.164 · 例题 6.9

题目:有 3 个字符串,要求找出其中"最大"者。

思路:设二维字符数组 str[3][20](3 行 20 列,每行一个字符串)。把 str[0]、str[1]、str[2] 看成 3 个一维字符数组,用 gets 读入,两两比较,大的放进 string。

c
#include <stdio.h>
#include <string.h>
int main()
{
    char str[3][20];                               // 定义二维字符数组
    char string[20];                               // 定义一维字符数组,作临时字符数组
    int i;
    for (i = 0; i < 3; i++)
        gets(str[i]);                              // 读入 3 个字符串
    if (strcmp(str[0], str[1]) > 0)                // 若 str[0] 大于 str[1]
        strcpy(string, str[0]);                    // 把 str[0] 的字符串赋给 string
    else                                           // 若 str[0] 小于等于 str[1]
        strcpy(string, str[1]);                    // 把 str[1] 的字符串赋给 string
    if (strcmp(str[2], string) > 0)                // 若 str[2] 大于 string
        strcpy(string, str[2]);                    // 把 str[2] 的字符串赋给 string
    printf("\nthe largest string is:\n%s\n", string);
    return 0;
}

逐行解释:

  • char str[3][20]; —— 3 行,每行能放 20 个字符(19 个有效 + '\0')。
  • gets(str[i]); —— str[i] 本身就是一维数组名,正好当 gets 的参数。
  • strcmp(str[0], str[1]) > 0 —— 用 strcmp 比较,不能写 str[0] > str[1]。
  • strcpy(string, str[0]); —— 用 strcpy 复制,不能写 string = str[0]。
  • 第二个 if 把"前两个中的大者"和第三个再比——两两比较,3 个串只需 2 次。

运行结果:

c
Holland
China
America

the largest string is:
Holland

书上程序分析(3 条):

  1. 注释中的"大于"指字符串比较的"大于"。第 1 个 if 后 string 放了 str[0]、str[1] 中的"大者";第 2 个 if 再和 str[2] 比,string 就是三者最大者。
  2. str[0]、str[1]、str[2] 和 string 都是一维字符数组,可存放一个字符串。
  3. strcpy 复制时最后都带 '\0',因此 %s 输出遇第一个 '\0' 即结束,并不会输出 string 中全部字符。

四、书上习题精讲

📖 教材 p.165–166 · 习题

本章 15 道题全部逐题覆盖。完整代码均可编译运行。

第 1 题:用筛选法求 100 之内的素数

📖 教材 p.165 · 习题 1

题目:用筛选法求 100 之内的素数。

思路:准备 101 个"柜子",把 2~100 放进去;从 2 开始把它的所有倍数标记为 0;下一个非 0 的数就是素数,继续筛。

c
#include <stdio.h>
int main()
{
    int a[101], i, j;
    for (i = 2; i <= 100; i++)
        a[i] = i;                       // 先把 2~100 装进柜子
    for (i = 2; i * i <= 100; i++)
        if (a[i] != 0)                  // i 还在,说明 i 是素数
            for (j = i + i; j <= 100; j += i)
                a[j] = 0;               // i 的倍数全部划掉
    for (i = 2; i <= 100; i++)
        if (a[i] != 0) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:

  • a[i]=i; 让"编号即数值",方便判断。
  • 外层只需筛到 i*i <= 100(i ≤ 10),更大的合数必有更小的因子。
  • j=i+i 从两倍开始,避免把自己划掉。

第 2 题:用选择法对 10 个整数排序

📖 教材 p.165 · 习题 2

题目:用选择法对 10 个整数排序。

思路:每一趟从未排序部分选出最小的,和这一趟的第一个位置交换。与冒泡的区别:冒泡"相邻两两换",选择法"每趟只换一次"。

c
#include <stdio.h>
int main()
{
    int a[10], i, j, k, t;
    printf("input 10 numbers:\n");
    for (i = 0; i < 10; i++) scanf("%d", &a[i]);
    for (i = 0; i < 9; i++)
    {
        k = i;                          // k 记当前最小元素的下标
        for (j = i + 1; j < 10; j++)
            if (a[j] < a[k]) k = j;     // 找到更小的就更新 k
        if (k != i) { t = a[i]; a[i] = a[k]; a[k] = t; }   // 只在这时交换
    }
    for (i = 0; i < 10; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:

  • 外层 9 趟(最后一个数自然就位)。
  • k 是"擂主"下标,内层找更小的挑战者。
  • if (k != i) 可省,加上更高效。

第 3 题:求 3×3 矩阵对角线元素之和

📖 教材 p.165 · 习题 3

题目:求一个 3×3 的整型矩阵对角线元素之和。

思路:主对角线元素的特点是行下标 == 列下标,即 a[i][i]。

c
#include <stdio.h>
int main()
{
    int a[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int i, sum = 0;
    for (i = 0; i < 3; i++)
        sum = sum + a[i][i];            // 主对角线:行列下标相同
    printf("sum=%d\n", sum);
    return 0;
}

解析:

  • a[0][0]=1、a[1][1]=5、a[2][2]=9,和 = 15。
  • 若求两条对角线之和,副对角线是 a[i][2-i],注意中心 a[1][1] 被算两次,要减一次。

第 4 题:有序数组中插入一个数

📖 教材 p.165 · 习题 4

题目:有一个已排好序的数组,要求输入一个数后,按原来排序的规律将它插入数组中。

思路:从最后往前找位置:比 x 大的都往后挪一格,挪到不比 x 大为止,空出的位置放 x。

c
#include <stdio.h>
int main()
{
    int a[11] = {1, 4, 6, 9, 13, 16, 19, 28, 40, 100};
    int i, x;
    printf("input a number: ");
    scanf("%d", &x);
    for (i = 9; i >= 0 && a[i] > x; i--)
        a[i + 1] = a[i];                // 比 x 大的往后挪一格
    a[i + 1] = x;                       // 空出的位置放 x
    for (i = 0; i < 11; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:

  • 数组开 11 个位置,最后一个空着备用。
  • i >= 0 && a[i] > x —— 两个条件都不能少:i<0 时不能再比,否则越界。
  • 循环结束时 i+1 就是 x 该待的位置。

第 5 题:将数组中的值按逆序重新存放

📖 教材 p.165 · 习题 5

题目:如原来顺序 8,6,5,4,1,要求改为 1,4,5,6,8。

思路:首尾两两交换,交换 n/2 次,不要新建数组。

c
#include <stdio.h>
int main()
{
    int a[5] = {8, 6, 5, 4, 1}, i, t;
    for (i = 0; i < 5 / 2; i++)
    {
        t = a[i];
        a[i] = a[4 - i];
        a[4 - i] = t;                   // a[i] 与 a[4-i] 交换
    }
    for (i = 0; i < 5; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:

  • i < 5/2 → i<2,只换 (0,4) 和 (1,3) 两对,中间 a[2] 不动。
  • 若循环写 i<5,会换两次又换回来,等于没换。

第 6 题:输出杨辉三角形(10 行)

📖 教材 p.165 · 习题 6

题目:输出杨辉三角形,要求输出 10 行。

思路:每行第一个和最后一个都是 1;中间元素等于上一行同列 + 上一行前一列。

c
#include <stdio.h>
int main()
{
    int a[10][10], i, j;
    for (i = 0; i < 10; i++)
    {
        a[i][0] = 1;                    // 每行第一个是 1
        a[i][i] = 1;                    // 每行最后一个是 1
    }
    for (i = 2; i < 10; i++)
        for (j = 1; j < i; j++)
            a[i][j] = a[i - 1][j - 1] + a[i - 1][j];   // 等于上方两数之和
    for (i = 0; i < 10; i++)
    {
        for (j = 0; j <= i; j++) printf("%5d", a[i][j]);
        printf("\n");
    }
    return 0;
}

解析:

  • 先给两条边赋 1,再算中间。
  • 内层 j < i:第 i 行只算到第 i-1 列(第 i 列已是 1)。
  • 输出 j <= i,只输出三角形的左下半边。

第 7 题:输出"魔方阵"

📖 教材 p.165 · 习题 7

题目:每一行、每一列和对角线之和均相等的方阵。三阶为 8 1 6 / 3 5 7 / 4 9 2。要求输出 1~n² 的自然数构成的魔方阵。

思路(奇数阶构造法):

  1. 1 放在第一行中间;
  2. 下一个数放在上一行、下一列(右上方);
  3. 上面越界 → 绕到最后一行;右面越界 → 绕到第一列;
  4. 右上方已填(或刚放完 n 的倍数)→ 放在当前数的正下方。
c
#include <stdio.h>
int main()
{
    int a[15][15] = {0};
    int n, i, j, k, row, col;
    printf("input an odd number n: ");
    scanf("%d", &n);
    row = 0; col = n / 2;               // 1 放在第一行中间
    for (k = 1; k <= n * n; k++)
    {
        a[row][col] = k;
        if (k % n == 0) row = row + 1;  // 放满一行就往下走
        else
        {
            row = row - 1; if (row < 0) row = n - 1;   // 上移,越界绕到最后一行
            col = col + 1; if (col > n - 1) col = 0;   // 右移,越界绕到第一列
        }
    }
    for (i = 0; i < n; i++)
    {
        for (j = 0; j < n; j++) printf("%5d", a[i][j]);
        printf("\n");
    }
    return 0;
}

解析:

  • a[15][15] = {0} —— 全初始化为 0,后面用"是否为 0"判断格子空不空。
  • row=0; col=n/2; —— 1 的位置:第 0 行、中间列。
  • k % n == 0 等价于"刚放完 n 的倍数",下一格往下。
  • n=3 时输出正是书上的 8 1 6 / 3 5 7 / 4 9 2。
  • 输入必须是奇数(偶数阶另有算法,超出本章范围)。

第 8 题:找出二维数组中的鞍点

📖 教材 p.166 · 习题 8

题目:鞍点 = 该位置上的元素在该行上最大、在该列上最小。也可能没有鞍点。

思路:逐行处理——先找出这一行最大的元素及其列号,再检查它是不是该列最小的。

c
#include <stdio.h>
int main()
{
    int a[3][4] = {{4, 6, 8, 2}, {1, 11, 10, 3}, {2, 9, 12, 13}};
    int i, j, k, maxj, max, flag, found = 0;
    for (i = 0; i < 3; i++)
    {
        max = a[i][0]; maxj = 0;
        for (j = 1; j < 4; j++)
            if (a[i][j] > max) { max = a[i][j]; maxj = j; }   // 找本行最大值
        flag = 1;
        for (k = 0; k < 3; k++)
            if (a[k][maxj] < max) { flag = 0; break; }        // 检查是否本列最小
        if (flag)
        {
            printf("a[%d][%d]=%d\n", i, maxj, max);
            found = 1;
        }
    }
    if (!found) printf("no saddle point\n");
    return 0;
}

解析:

  • maxj 记住"本行最大值"的列号,这很关键。
  • 检查列最小用 k 循环,共 3 行。
  • flag 是"仍是鞍点"的标志,一票否决。
  • 可能没有鞍点,也可能有多个,用 found 统一记录。
  • 本例输出 a[0][2]=8:第 0 行最大的是 8,第 2 列(8、10、12)最小的也是 8,所以它是鞍点。

第 9 题:折半查找

📖 教材 p.166 · 习题 9

题目:15 个数按由大到小存放,输入一个数,用折半查找法找出它是第几个元素的值;不在则输出"无此数"。

思路:数组降序。每次取中间元素比较:比中间大 → 往左半找;比中间小 → 往右半找;相等 → 找到。

c
#include <stdio.h>
int main()
{
    int a[15] = {100, 96, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30};
    int x, low, high, mid;
    printf("input a number: ");
    scanf("%d", &x);
    low = 0; high = 14;
    while (low <= high)
    {
        mid = (low + high) / 2;
        if (a[mid] == x) break;
        else if (a[mid] > x) low = mid + 1;   // 降序:中间值偏大,往右找
        else high = mid - 1;                  // 中间值偏小,往左找
    }
    if (low <= high) printf("它是第 %d 个元素的值\n", mid + 1);
    else printf("无此数\n");
    return 0;
}

解析:

  • 降序数组方向要反过来想:a[mid] > x 说明目标在右边。
  • low <= high 是循环条件;结束时 low > high 表示没找到。
  • 输出 mid + 1 才是"第几个"(下标从 0 起)。
  • 若是升序数组,两个分支条件要互换。

第 10 题:统计 3 行 80 字符

📖 教材 p.166 · 习题 10

题目:3 行文字,每行 80 个字符,分别统计大写字母、小写字母、数字、空格、其他字符的个数。

思路:二维字符数组 text[3][80];gets 读 3 行;逐字符判断类别。

c
#include <stdio.h>
int main()
{
    char text[3][80];
    int i, j, upper = 0, lower = 0, digit = 0, space = 0, other = 0;
    for (i = 0; i < 3; i++) gets(text[i]);
    for (i = 0; i < 3; i++)
        for (j = 0; text[i][j] != '\0'; j++)
        {
            char c = text[i][j];
            if (c >= 'A' && c <= 'Z') upper++;
            else if (c >= 'a' && c <= 'z') lower++;
            else if (c >= '0' && c <= '9') digit++;
            else if (c == ' ') space++;
            else other++;
        }
    printf("upper=%d lower=%d digit=%d space=%d other=%d\n", upper, lower, digit, space, other);
    return 0;
}

解析:

  • 内层循环用 != '\0' 判断行尾,不用 80(用户可能只输入几个字)。
  • 判断顺序:先大写、再小写、再数字、再空格,剩下都算"其他"。
  • 字符比较本质是 ASCII 码比较。

第 11 题:输出指定图案

📖 教材 p.166 · 习题 11

题目:输出

c
* * * * *
 * * * * *
  * * * * *
   * * * * *
    * * * * *

思路:5 行,第 i 行先输出 i 个空格,再输出 5 组 * 。

c
#include <stdio.h>
int main()
{
    int i, j;
    for (i = 0; i < 5; i++)
    {
        for (j = 0; j < i; j++) printf(" ");      // 前导空格,逐行加 1
        for (j = 0; j < 5; j++) printf("* ");     // 5 个星号,每个后带一个空格
        printf("\n");
    }
    return 0;
}

解析:

  • 外层 i 既是行号又是空格数,这是这道题的巧处。
  • printf("* ") 输出"星 + 空格",5 次就是 * * * * * 。
  • 行末多一个空格不影响判题。

第 12 题:电文译码

📖 教材 p.166 · 习题 12

题目:电文按规律译成密码:A→Z,B→Y,C→X……即第 1 个字母变第 26 个,第 i 个字母变第 (26-i+1) 个,非字母字符不变。要求译回原文,并输出密码和原文。

思路:这是自逆变换(再译一次就解密)。公式:大写 c = 'A' + 'Z' - c,小写 c = 'a' + 'z' - c。

验证:'A'+'Z'-'A' = 'Z' ✔;'A'+'Z'-'B' = 155-66 = 89 = 'Y' ✔。

c
#include <stdio.h>
int main()
{
    char c[100];
    int i;
    gets(c);
    printf("密码:%s\n", c);
    for (i = 0; c[i] != '\0'; i++)
    {
        if (c[i] >= 'A' && c[i] <= 'Z') c[i] = 'A' + 'Z' - c[i];
        else if (c[i] >= 'a' && c[i] <= 'z') c[i] = 'a' + 'z' - c[i];
    }
    printf("原文:%s\n", c);
    return 0;
}

解析:

  • 先输出密码,再原地译码(直接改 c 数组),最后输出原文。
  • 非字母字符不进入任何分支,自动保持不变。
  • 大小写各用一套公式,不能混。

第 13 题:连接两个字符串(不用 strcat)

📖 教材 p.166 · 习题 13

题目:将两个字符串连接起来,不要用 strcat 函数。

思路:先找到 s1 的结尾('\0' 位置),再把 s2 一个字符一个字符搬过去,最后补 '\0'。

c
#include <stdio.h>
int main()
{
    char s1[100], s2[50];
    int i = 0, j = 0;
    gets(s1);
    gets(s2);
    while (s1[i] != '\0') i++;              // 找 s1 的结尾
    while (s2[j] != '\0')
    {
        s1[i] = s2[j];                      // 把 s2 的字符搬到 s1 尾部
        i++; j++;
    }
    s1[i] = '\0';                           // 别忘了补结束标志
    printf("%s\n", s1);
    return 0;
}

解析:

  • s1 必须足够大,能装下两个串。
  • 搬运完必须手动写 '\0',否则输出会拖出垃圾。
  • 这正是 strcat 内部做的事。

第 14 题:比较两个字符串(不用 strcmp)

📖 教材 p.166 · 习题 14

题目:比较 s1 和 s2:s1>s2 输出正数,相等输出 0,s1<s2 输出负数。不用 strcmp。输出的绝对值应是相应字符的 ASCII 码差值。

思路:逐字符比较,遇到不同或结束就停,输出两个字符的差。

c
#include <stdio.h>
int main()
{
    char s1[100], s2[100];
    int i = 0;
    gets(s1);
    gets(s2);
    while (s1[i] != '\0' && s1[i] == s2[i]) i++;   // 相同的字符一直往后走
    printf("%d\n", s1[i] - s2[i]);                 // 输出第一个不同字符的差
    return 0;
}

解析:

  • "And" vs "Aid":i=1 时 'n' vs 'i' → 110-105 = 5,与书上例子一致。
  • 完全相等时循环停在 '\0',0-0=0。
  • 若 s1 是 s2 的前缀("ab" vs "abc"),'\0'-'c' 为负,正确表示"小"。

第 15 题:复制字符串(不用 strcpy)

📖 教材 p.166 · 习题 15

题目:将 s2 中的全部字符复制到 s1 中,不用 strcpy。复制时 '\0' 也要复制过去,'\0' 后面的字符不复制。

思路:一个字符一个字符搬,搬完 '\0' 就停('\0' 本身已经搬过去了)。

c
#include <stdio.h>
int main()
{
    char s1[100], s2[100];
    int i;
    gets(s2);
    for (i = 0; ; i++)
    {
        s1[i] = s2[i];                  // 先复制
        if (s2[i] == '\0') break;       // 复制到 '\0' 就收工
    }
    printf("%s\n", s1);
    return 0;
}

解析:

  • for (i = 0; ; i++) 省略条件,靠里面的 break 退出。
  • 先复制再判断,保证 '\0' 也被复制过去。
  • s1 必须足够大。

🛠 随课练习(只给题不给答案)

📖 本课补充 · 非教材原文

🔮 猜输出

猜输出 1

c
#include <stdio.h>
int main()
{
    int a[5] = {1, 2, 3};
    int i;
    for (i = 0; i < 5; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

猜输出 2

c
#include <stdio.h>
#include <string.h>
int main()
{
    char s[] = "abc";
    printf("%d %d\n", (int)strlen(s), (int)sizeof(s));
    return 0;
}

猜输出 3

c
#include <stdio.h>
int main()
{
    int a[3][4] = {{1, 2}, {5}, {9, 10, 11}};
    printf("%d %d %d\n", a[0][2], a[1][3], a[2][0]);
    return 0;
}

猜输出 4

c
#include <stdio.h>
int main()
{
    char a[] = "China";
    char b[] = {'C', 'h', 'i', 'n', 'a'};
    printf("%d %d\n", (int)sizeof(a), (int)sizeof(b));
    return 0;
}

🛠 变式题

变式 1:把例 6.3 的冒泡排序改成降序(从大到小)。

变式 2:有一个 3×4 的矩阵,求每一行的最大值,并输出行号。

变式 3:不用 strlen,自己写一个 int my_strlen(char s[]),返回字符串长度。

变式 4:把一个 3×3 的矩阵原地转置(不许用第二个数组)。

🐛 改错题

改错 1

c
// ❌ 错误示例(故意)
#include <stdio.h>
int main()
{
    int a[10], i;
    for (i = 1; i <= 10; i++)
        a[i] = i;
    return 0;
}

改错 2

c
// ❌ 错误示例(故意)
#include <stdio.h>
int main()
{
    char s[10];
    scanf("%s", &s);
    printf("%s\n", s);
    return 0;
}

改错 3

c
// ❌ 错误示例(故意)
#include <stdio.h>
int main()
{
    char s1[10], s2[10] = "China";
    s1 = s2;
    printf("%s\n", s1);
    return 0;
}

📈 加练题(你随时喊,我随时追加到这里)

📖 本课补充 · 非教材原文

(暂无。想加练就说"再出几道 XX 题",我出的题会标上日期追加在这块。)


💬 问答区(你问过的问题 + 我的解答)

📖 本课补充 · 非教材原文

(暂无。你问的问题和我的解答会按 Q/A 整理进这里。)


该带走的 6 句话

📖 本课补充 · 非教材原文
  1. 数组 = 一个名字 + 一串从 0 开始的编号;int a[10] 的合法下标是 0~9,a[10] 已经越界。
  2. 定义时的方括号是"柜子总数",引用时的方括号是"第几号柜"——长得一样,含义完全不同。
  3. 初始化列表没写到的元素自动为 0(字符数组为 '\0');但只定义不初始化的数组里是垃圾值。
  4. 二维数组在内存里是按行排成一长条的;a[3][4] 是 3 行 4 列,第二维长度永远不能省。
  5. 字符串 = 字符数组 + '\0';%s、puts、strlen、strcpy、strcat、strcmp 全都"遇 '\0' 就停"。
  6. 冒泡排序:n 个数比 n-1 趟,第 j 趟比 n-j 次;相邻两两比较,大的往后沉。

📚 教材覆盖清单(第6章,利用数组处理批量数据)

这张表把本课和教材逐节对上:从网页可以回查书页,从书页也能找到网页位置。标「未展开」的,就是教材里有、本课没讲的部分,已明确列出。

教材节号标题书页网站对应状态
6.1怎样定义和引用一维数组p.139–1446.1 怎样定义和引用一维数组精讲
6.1.1怎样定义一维数组p.1406.1.1 怎样定义一维数组精讲
6.1.2怎样引用一维数组元素p.140–1416.1.2 怎样引用一维数组元素精讲
6.1.3一维数组的初始化p.1426.1.3 一维数组的初始化精讲
6.1.4一维数组程序举例p.142–1446.1.4 一维数组程序举例精讲
6.2怎样定义和引用二维数组p.145–1506.2 怎样定义和引用二维数组精讲
6.2.1怎样定义二维数组p.1466.2.1 怎样定义二维数组精讲
6.2.2怎样引用二维数组的元素p.1476.2.2 怎样引用二维数组的元素精讲
6.2.3二维数组的初始化p.1486.2.3 二维数组的初始化精讲
6.2.4二维数组程序举例p.149–1506.2.4 二维数组程序举例精讲
6.3字符数组p.151–1646.3 字符数组精讲
6.3.1怎样定义字符数组p.1516.3.1 怎样定义字符数组精讲
6.3.2字符数组的初始化p.1526.3.2 字符数组的初始化精讲
6.3.3怎样引用字符数组中的元素p.1526.3.3 怎样引用字符数组中的元素精讲
6.3.4字符串和字符串结束标志p.153–1556.3.4 字符串和字符串结束标志精讲
6.3.5字符数组的输入输出p.1566.3.5 字符数组的输入输出精讲
6.3.6使用字符串处理函数p.157–1616.3.6 使用字符串处理函数精讲
6.3.7字符数组应用举例p.162–1646.3.7 字符数组应用举例精讲
习题第6章书末习题p.165–166本课「书上习题精讲」逐题精讲(第1–15题)
插图本课范围内教材插图p.139–164用文字/ASCII 重画未逐张复刻
附录附录 A–Ep.357–370全书对照页未单独成课
  • 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
  • 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
  • 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。