教材《C程序设计》第五版(谭浩强)第6章,书页 139–166,共 28 页|建议 2h
课前一句话
前五课,你手里只有一种容器——变量:一个名字装一个数。这一课换一种容器——数组:一个名字装一串同类型的数,用编号取用。学完这一课,你能徒手写出:冒泡排序、矩阵转置、杨辉三角、单词统计、字符串的比较/复制/连接——期末卷上"批量数据"那一整片题,全在这一课。
一、先补地基:本章要解决的问题
1.1 一个真实的问题
一个班 30 个学生,每人一个成绩,求平均分。
理论上简单:加起来除以 30。真正的问题是——这 30 个成绩在程序里怎么表示?
用你现在的本事,只能这样:
float s1, s2, s3, ..., s30;书上一针见血地指出两个毛病:
- 烦琐。要定义 30 个变量;如果有 1000 名学生怎么办?
- 没有反映出这些数据间的内在联系。这 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 怎样定义和引用一维数组
一维数组的元素只需数组名 + 一个下标就能唯一确定。掌握它,二维、多维都是举一反三。
6.1.1 怎样定义一维数组
一句话定义:定义数组,就是提前通知计算机——我要一排柜子,叫什么名、有几个、每个放什么类型。
拆名字:array(数组)= 一排、一列;subscript(下标)= sub(下面)+ script(写),写在右下角的小字。
生活比喻:去储物间领柜子,管理员要登记三件事——柜区名、柜子数量、每个柜子装什么。三样不齐,不给钥匙。
怎么用:
int a[10];表示:定义了一个整型数组,数组名为 a,包含 10 个整型元素。
一般形式:
类型说明符 数组名[常量表达式];书上三条说明:
- 数组名的命名规则和变量名相同,遵循标识符命名规则。
- 方括号中的常量表达式表示元素的个数(数组长度)。
int a[10];有 10 个元素,下标从 0 开始,即a[0]~a[9],不存在a[10]。 - 常量表达式中可以包括常量和符号常量,
int a[3+5];合法;不能包含变量,int a[n];不合法——C 语言不允许对数组大小作动态定义。
内存图(书图 6.1):int a[10]; 在内存中划出一片连续空间。用 Visual C++ 时一个 int 占 4 字节,共 40 字节:
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 怎样引用一维数组元素
一句话定义:引用数组元素就是"报柜号取东西",形式是 数组名[下标]。
生活比喻:柜区名 + 柜号 = 唯一的一格。只说"A 区"没用,必须说"A 区 3 号"。
怎么用:a[0] 就是序号为 0 的元素。它和一个简单变量的地位、作用相似——可被赋值,也可参加运算。下标可以是整型常量或整型表达式:
a[0] = a[5] + a[7] - a[2*3];定义 vs 引用(书上专门强调:形式相同,含义不同):
| 写法 | 出现场合 | 含义 |
|---|---|---|
int a[10]; | 前面有类型名 int | 定义数组,指定包含 10 个元素 |
t = a[6]; | 前面没有类型名 | 引用序号为 6 的元素 |
易错 只能引用元素,不能一次整体调用整个数组全部元素的值。
易错 int a[10]; 最大下标是 9。书上给的常见错误:
for (i = 1; i <= 10; i++)
a[i] = i; // 错:a[10] 越界,a[0] 没被赋值6.1.3 一维数组的初始化
一句话定义:在定义数组的同时给元素赋初值,叫初始化,用花括号里的"初始化列表"实现。
拆名字:initial(最初的)+ ization(过程)= 初始化。
生活比喻:领柜子的同时,管理员直接往柜子里放好第一批东西;没提到的柜子,统一塞 0。
四条规则:
(1)对全部元素赋初值
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};花括号内称"初始化列表",数据间用逗号分隔。执行后 a[0]=0……a[9]=9。
(2)只给一部分元素赋值
int a[10] = {0, 1, 2, 3, 4};只给前 5 个元素赋初值,系统自动给后 5 个元素赋初值为 0。
(3)全部元素值为 0
int a[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};或简写:
int a[10] = {0}; // 未赋值的部分元素自动设定为 0(4)全部赋初值时可不指定长度
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 一维数组程序举例
本节三个例题的完整代码见第三节,这里先抽"算法骨头":
| 例题 | 题目 | 算法要点 |
|---|---|---|
| 例 6.1 | 对 10 个元素赋 0~9,再逆序输出 | for(i=9;i>=0;i--) 倒着走 |
| 例 6.2 | 数组求 Fibonacci 数列前 20 项 | f[i]=f[i-2]+f[i-1] |
| 例 6.3 | 10 个地区面积按从小到大排序 | 冒泡法(起泡法) |
冒泡排序——本章第一号考点:
- 排序(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 怎样定义和引用二维数组
为什么要二维:有 3 个小分队,每队 6 名队员,要保存队员的工资。一个下标只能表示"第几个队员",表示不了"第几队"——需要两个下标。
书上规定 pay[2][3] 表示"2 分队第 3 名队员的工资",值是 1725。
拆名字:matrix(矩阵)= 行列排成的方阵;row = 行;column = 列。
生活比喻:一维数组是一排柜子;二维数组是一整面墙的柜子——先报"第几排"(行),再报"第几列"。
6.2.1 怎样定义二维数组
一句话定义:二维数组就是"每个元素又是一维数组"的数组。
怎么用:
float pay[3][6];定义 float 型二维数组,第 1 维有 3 个元素,第 2 维有 6 个元素。每一维的长度各用一对方括号括起来。
一般形式:
类型说明符 数组名[常量表达式][常量表达式];例如 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 个元素的一维数组,相当于:
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 怎样引用二维数组的元素
一句话定义:数组名[下标][下标],两个下标一起才能定位一格。
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 二维数组的初始化
一句话定义:用花括号给初值,可以分行给,也可以按内存顺序一口气给。
生活比喻:往一面墙的柜子里放东西——最好一排一排地放(分行),也可以按"先第一排从左到右、再第二排"的顺序一股脑列出来。
四条规则:
(1)分行给二维数组赋初值
int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};第 1 个花括号给第 1 行,第 2 个给第 2 行……即按行赋初值,直观。
(2)所有数据写在一个花括号内,按内存排列顺序赋值
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};效果与(1)相同。但以第(1)种为好,一行对一行,界限清楚;第(2)种数据多时写成一大片,容易遗漏也不易检查。
(3)可以对部分元素赋初值
int a[3][4] = {{1},{5},{9}}; // 只给各行第 0 列1 0 0 0
5 0 0 0
9 0 0 0也可对各行的某一元素赋值:
int a[3][4] = {{1},{0,6},{0,0,11}};1 0 0 0
0 6 0 0
0 0 11 0也可只对某几行赋值:
int a[3][4] = {{1},{5,6}}; // 第 3 行不赋初值,全 0也可对第 2 行不赋初值:
int a[3][4] = {{1},{},{9}};(4)全部元素赋初值时,第 1 维长度可省,第 2 维长度不能省
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 维,但应分行赋初值:
int a[][4] = {{0,0,3},{},{0,10}}; // 编译系统据此知道有 3 行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 二维数组程序举例
| 例题 | 题目 | 算法要点 |
|---|---|---|
| 例 6.4 | 二维数组行和列的元素互换,存到另一个数组 | b[j][i] = a[i][j];——行列下标对调 |
| 例 6.5 | 3×4 矩阵中值最大的元素及行号列号 | "打擂台"算法 |
打擂台算法(书上比喻):先找一人站在台上,第 2 人上去比武,胜者留台;第 3 人再与台上的人比……直到所有人比过,最后留在台上的就是冠军。
对应程序:先让 a[0][0] 作"擂主",值赋给 max;然后逐个元素与 max 比,比 max 大就取代它,同时记下行号 row、列号 column。全部比完,max 就是最大值。
考点 打擂台三件套:max(当前最大值)、row(行号)、column(列号),一次遍历同时更新。
6.3 字符数组
铺垫(书上原话):字符型数据以字符的 ASCII 代码存储在存储单元中,一般占一个字节。由于 ASCII 代码也属于整数形式,C99 标准把字符类型归纳为整型类型中的一种。
C 语言中没有字符串类型,也没有字符串变量,字符串是存放在字符型数组中的。
这句话是本章的"总纲",后面所有字符串规则都从它来。
6.3.1 怎样定义字符数组
一句话定义:用来存放字符数据的数组是字符数组,其中一个元素内存放一个字符。
怎么用:
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 字符数组的初始化
一句话定义:用"初始化列表"把各个字符依次赋给数组中的各元素。
char c[10] = {'I',' ','a','m',' ','h','a','p','p','y'};四条规则:
- 定义字符数组时不初始化,则各元素的值是不可预料的。
- 初值个数大于数组长度 → 语法错误。
- 初值个数小于数组长度 → 只赋给前面那些元素,其余元素自动定为空字符
'\0'。 - 初值个数与数组长度相同 → 定义时可省略长度:
char c[] = {'h','a','p','p','y'};(长度自动定为 5)。
考点 字符数组初始化时,初值个数少于长度 → 其余自动 '\0'。这条是 6.3.4 理解 '\0' 的基础,也是选择题常客。
二维字符数组也可以定义并初始化(例 6.7 的菱形):
char diamond[5][5] = {
{' ',' ','*',' ',' '},
{' ','*',' ','*',' '},
{'*',' ',' ',' ','*'},
{' ','*',' ','*',' '},
{' ',' ','*',' ',' '}
};6.3.3 怎样引用字符数组中的元素
一句话定义:c[i] 引用一个元素,得到的是一个字符——和引用数值数组元素没区别。
例 6.6(输出已知字符串)和例 6.7(输出菱形)都是"逐个元素处理",代码见第三节。
考点 例 6.7 的二维字符数组初始化写法容易考:每行 5 个字符,空格也要老老实实写出来,少写一个图形就歪。
6.3.4 字符串和字符串结束标志
这一小节是整章的心脏。
问题:字符数组长度 100,实际有效字符只有 40 个,程序怎么知道字符串到哪儿结束?
答案:C 语言规定了一个"字符串结束标志",以字符 '\0' 作为结束标志。
在遇到字符
'\0'时,表示字符串结束,把它前面的字符组成一个字符串。
拆名字:'\0' 读作 null character(空字符),null = 空、无。
生活比喻:一排柜子放了一串字,最后放一张"到此为止"的牌子。程序取字符串时不看柜子总数,只看牌子在哪。
三条要点:
- 系统会自动加
'\0':C 系统用字符数组存储字符串常量时会自动加一个'\0'。例如"C program"有 9 个字符,在数组中占 10 个字节,最后那个'\0'是系统加的。 - 有了
'\0',数组长度就不那么重要了:程序靠检测'\0'的位置判定字符串是否结束,不是根据数组长度。但定义时仍应保证数组长度始终大于字符串实际长度;若先后存放多个不同长度的字符串,应大于最长的那个。 '\0'是什么:代表 ASCII 码为 0 的字符,它不是可显示字符,而是一个"空操作符",什么也不做。用它作结束标志不会产生附加操作、不增加有效字符,只起一个供辨别的标志作用。
printf 怎么知道停:执行 printf("How do you do? \n"); 时,系统在内存中自动在 '\n' 后面加 '\0'。printf 每输出一个字符检查一次,遇 '\0' 就停止输出。
用字符串常量初始化:
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',新旧串连成一片:
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 字符数组的输入输出
两种方法:
- 逐个字符输入输出:用格式符
%c输入或输出一个字符(如例 6.6)。 - 将整个字符串一次输入或输出:用
%s格式符,意思是字符串(string)的输入输出。
char c[] = {"China"};
printf("%s\n", c); // 输出 China输出时遇 '\0' 就停。
书上六条说明:
- 输出的字符中不包括结束符
'\0'。 - 用
%s时 printf 的输出项是字符数组名,不是数组元素名。printf("%s", c[0]);是错的——c[0]是一个数组元素,不是数组名。 - 数组长度大于字符串实际长度,也只输出到遇
'\0'结束。char c[10]="China";只输出China,不是 10 个字符——这就是结束标志的好处。 - 一个数组中有多个
'\0',遇第一个就结束。 scanf("%s", c);可以输入一个字符串。输入项c是字符数组名,输入的字符串应短于数组长度(char c[6];输入China,系统自动加'\0')。若一次输入多个字符串,用空格分隔:
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'。
但如果只想读一整行:
char str[13];
scanf("%s", str); // 输入 How are you? → 只得到 How系统把空格作为字符串之间的分隔符,只把空格前的 How 送到 str,并在其后加 '\0'。
考点
scanf("%s", ...)遇空格就停,只读得到一个"词";想读整行(含空格)要用gets。
- 【必考】 scanf 的输入项如果是字符数组名,不要再加地址符
&,因为数组名代表该数组第一个元素的地址(起始地址)。下面写法不正确:
scanf("%s", &str); // 错:str 前面不应加 &printf("%s", c) 到底怎么执行:按数组名 c 找到第一个元素的地址,然后逐个输出字符,直到遇 '\0' 为止。
一张表总结"能不能加 &":
| 输入对象 | 写法 | 说明 |
|---|---|---|
| 单个 int | scanf("%d", &x); | 要加 & |
| 单个 char | scanf("%c", &ch); | 要加 & |
| 字符数组(字符串) | scanf("%s", str); | 不加 &(数组名本身是地址) |
易错 把 scanf("%s", &str) 写熟了就改不过来——上机最容易犯的错。
6.3.6 使用字符串处理函数
一句话定义:C 函数库提供了一批专门处理字符串的现成函数,要用就先写 #include <string.h>。
生活比喻:这 8 个函数就像柜房门口的 8 个搬运工——有的负责把字串搬进柜子、有的负责搬出来、有的负责量长度、有的负责比大小。
拆名字(全是"str + 功能缩写"):
| 函数 | 全称 | 拆开记 |
|---|---|---|
strcat | STRing CATenate | str + cat(连接) |
strcpy | STRing CoPY | str + cpy(复制) |
strcmp | STRing CoMPare | str + cmp(比较) |
strlen | STRing LENgth | str + len(长度) |
strlwr | STRing LoWeRcase | str + lwr(小写) |
strupr | STRing UPpeRcase | str + upr(大写) |
puts | put string | 输出字符串 |
gets | get string | 读入字符串 |
总表(背这张就够):
| 函数 | 一般形式 | 作用 | 返回值 | 易错 |
|---|---|---|---|---|
puts | puts(字符数组) | 输出一个字符串,自动换行(把 '\0' 转成 '\n') | — | 只能一个字符串 |
gets | gets(字符数组) | 读入一整行到数组 | 数组起始地址 | 只能一个字符串 |
strcat | strcat(s1, s2) | 把 s2 接到 s1 后面,结果在 s1 | s1 的地址 | s1 必须够大;s1 的 '\0' 被取消 |
strcpy | strcpy(s1, s2) | 把 s2 复制到 s1 | s1 的地址 | s1 必须够大;不能用 s1=s2 |
strncpy | strncpy(s1, s2, n) | 复制 s2 前 n 个字符 | s1 | n 不应多于 s1 原有字符数 |
strcmp | strcmp(s1, s2) | 按 ASCII 逐字符比较 | 0 / 正 / 负 | 不能写 s1>s2 |
strlen | strlen(s) | 测实际长度 | 长度(不含 '\0') | 不是数组长度 |
strlwr | strlwr(s) | 转小写 | s | 非标准,VS 才有 |
strupr | strupr(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'一起复制过去。 易错 字符数组名是地址常量,不能被赋值:
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 字符数组应用举例
例 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)
本章共 15 道编程题,题目原文、思路和完整代码见第四节。先给你一张"考点地图":
| 题号 | 题目大意 | 考点 |
|---|---|---|
| 1 | 筛选法求 100 之内的素数 | 数组标记、按倍数划掉 |
| 2 | 选择法对 10 个整数排序 | 选择排序(与冒泡对比) |
| 3 | 3×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 |
| 10 | 3 行 80 字符分类统计 | 字符 ASCII 区间判断 |
| 11 | 输出图案 | 循环控制空格与星号 |
| 12 | 电文译码(A→Z, B→Y…) | 'A'+'Z'-c |
| 13 | 连接两个字符串(不用 strcat) | 找尾、逐个搬 |
| 14 | 比较两个字符串(不用 strcmp) | 返回 ASCII 差值 |
| 15 | 复制字符串(不用 strcpy) | 连 '\0' 一起复制 |
考点 第 6(杨辉三角)、7(魔方阵)、8(鞍点)、9(折半查找)是四道"经典算法题",期末和考研都爱从这里抽。
三、现场写代码
下面把书上 9 个例题走一遍:题目 → 思路 → 完整代码 → 逐行解释 → 运行结果。代码全部实测可编译运行。
例 6.1 逆序输出 10 个数
题目:对 10 个数组元素依次赋值为 0~9,要求按逆序输出。
思路:定义长度 10 的整型数组;值有规律,用循环赋值;输出时下标从大到小。
#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 项
题目:用数组处理 Fibonacci 数列问题。
思路:第 5 章用简单变量只能"算一个扔一个",不能在内存中保存,想直接输出第 25 个数很困难。用数组后概念上反而简单:每个元素代表数列中的一个数,依次求出并存放即可。
#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 格,右对齐,整齐。
运行结果:
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 个地区的面积)
题目:有 10 个地区的面积,要求按由小到大排序。
思路:这是排序(sort)。本例用起泡法:每次将相邻两个数比较,将小的调到前面。
#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;—— 三只杯子交换法,顺序不能乱,否则丢值。
运行结果(书上数据):
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 矩阵转置
题目:将一个二维数组行和列的元素互换,存到另一个二维数组中。如 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 完成。
#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 列)。
运行结果:
array a:
1 2 3
4 5 6
array b:
1 4
2 5
3 6例 6.5 求 3×4 矩阵中的最大值
题目:有一个 3×4 的矩阵,求出值最大的元素及其行号、列号。
思路:打擂台算法。先让 a[0][0] 作"擂主",值赋给 max;逐个元素比,大的取代 max,同时记下行号、列号。
#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;—— 三件事一起更新,少记一个就输出错的行列。
运行结果:
max=10
row=2
column=1最大值为 10,此元素为 a[2][1]。
书上变量名写作
colum(少一个 n),是印刷习惯,不影响运行;本课统一写column。
考点 打擂台三个变量必须同时更新:值、行、列。
例 6.6 输出一个已知的字符串
题目:输出一个已知的字符串。
思路:定义字符数组,用"初始化列表"赋初值,然后用循环逐个输出。
#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 输出一个菱形图
题目:输出一个菱形图。
思路:先画出平面菱形图案,每行 5 个字符,有的是空白、有的是 *。定义字符型二维数组,用"初始化列表"按图逐行初始化,再用嵌套 for 输出所有元素。
#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 位置放星。 - 输出时两层循环把所有元素(包括空格)原样打印。
运行结果:
*
* *
* *
* *
*易错 每行 5 个字符一个都不能少;空格漏写、多写,图形就歪。
例 6.8 统计一行字符中有多少个单词
题目:输入一行字符,统计其中有多少个单词,单词之间用空格分隔开。
思路:见 6.3.7 的逻辑表。核心变量:num(单词数)、word(前一个字符是不是空格)。
#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,并标记"现在在单词里"。
运行结果:
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 个字符串中的"最大"者
题目:有 3 个字符串,要求找出其中"最大"者。
思路:设二维字符数组 str[3][20](3 行 20 列,每行一个字符串)。把 str[0]、str[1]、str[2] 看成 3 个一维字符数组,用 gets 读入,两两比较,大的放进 string。
#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 次。
运行结果:
Holland
China
America
the largest string is:
Holland书上程序分析(3 条):
- 注释中的"大于"指字符串比较的"大于"。第 1 个 if 后 string 放了 str[0]、str[1] 中的"大者";第 2 个 if 再和 str[2] 比,string 就是三者最大者。
- str[0]、str[1]、str[2] 和 string 都是一维字符数组,可存放一个字符串。
- strcpy 复制时最后都带
'\0',因此 %s 输出遇第一个'\0'即结束,并不会输出 string 中全部字符。
四、书上习题精讲
本章 15 道题全部逐题覆盖。完整代码均可编译运行。
第 1 题:用筛选法求 100 之内的素数
题目:用筛选法求 100 之内的素数。
思路:准备 101 个"柜子",把 2~100 放进去;从 2 开始把它的所有倍数标记为 0;下一个非 0 的数就是素数,继续筛。
#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 个整数排序
题目:用选择法对 10 个整数排序。
思路:每一趟从未排序部分选出最小的,和这一趟的第一个位置交换。与冒泡的区别:冒泡"相邻两两换",选择法"每趟只换一次"。
#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 矩阵对角线元素之和
题目:求一个 3×3 的整型矩阵对角线元素之和。
思路:主对角线元素的特点是行下标 == 列下标,即 a[i][i]。
#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 题:有序数组中插入一个数
题目:有一个已排好序的数组,要求输入一个数后,按原来排序的规律将它插入数组中。
思路:从最后往前找位置:比 x 大的都往后挪一格,挪到不比 x 大为止,空出的位置放 x。
#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 题:将数组中的值按逆序重新存放
题目:如原来顺序 8,6,5,4,1,要求改为 1,4,5,6,8。
思路:首尾两两交换,交换 n/2 次,不要新建数组。
#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 行)
题目:输出杨辉三角形,要求输出 10 行。
思路:每行第一个和最后一个都是 1;中间元素等于上一行同列 + 上一行前一列。
#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 题:输出"魔方阵"
题目:每一行、每一列和对角线之和均相等的方阵。三阶为 8 1 6 / 3 5 7 / 4 9 2。要求输出 1~n² 的自然数构成的魔方阵。
思路(奇数阶构造法):
- 1 放在第一行中间;
- 下一个数放在上一行、下一列(右上方);
- 上面越界 → 绕到最后一行;右面越界 → 绕到第一列;
- 右上方已填(或刚放完 n 的倍数)→ 放在当前数的正下方。
#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 题:找出二维数组中的鞍点
题目:鞍点 = 该位置上的元素在该行上最大、在该列上最小。也可能没有鞍点。
思路:逐行处理——先找出这一行最大的元素及其列号,再检查它是不是该列最小的。
#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 题:折半查找
题目:15 个数按由大到小存放,输入一个数,用折半查找法找出它是第几个元素的值;不在则输出"无此数"。
思路:数组降序。每次取中间元素比较:比中间大 → 往左半找;比中间小 → 往右半找;相等 → 找到。
#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 字符
题目:3 行文字,每行 80 个字符,分别统计大写字母、小写字母、数字、空格、其他字符的个数。
思路:二维字符数组 text[3][80];gets 读 3 行;逐字符判断类别。
#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 题:输出指定图案
题目:输出
* * * * *
* * * * *
* * * * *
* * * * *
* * * * *思路:5 行,第 i 行先输出 i 个空格,再输出 5 组 * 。
#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 题:电文译码
题目:电文按规律译成密码: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' ✔。
#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)
题目:将两个字符串连接起来,不要用 strcat 函数。
思路:先找到 s1 的结尾('\0' 位置),再把 s2 一个字符一个字符搬过去,最后补 '\0'。
#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)
题目:比较 s1 和 s2:s1>s2 输出正数,相等输出 0,s1<s2 输出负数。不用 strcmp。输出的绝对值应是相应字符的 ASCII 码差值。
思路:逐字符比较,遇到不同或结束就停,输出两个字符的差。
#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)
题目:将 s2 中的全部字符复制到 s1 中,不用 strcpy。复制时 '\0' 也要复制过去,'\0' 后面的字符不复制。
思路:一个字符一个字符搬,搬完 '\0' 就停('\0' 本身已经搬过去了)。
#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
#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
#include <stdio.h>
#include <string.h>
int main()
{
char s[] = "abc";
printf("%d %d\n", (int)strlen(s), (int)sizeof(s));
return 0;
}猜输出 3
#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
#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
// ❌ 错误示例(故意)
#include <stdio.h>
int main()
{
int a[10], i;
for (i = 1; i <= 10; i++)
a[i] = i;
return 0;
}改错 2
// ❌ 错误示例(故意)
#include <stdio.h>
int main()
{
char s[10];
scanf("%s", &s);
printf("%s\n", s);
return 0;
}改错 3
// ❌ 错误示例(故意)
#include <stdio.h>
int main()
{
char s1[10], s2[10] = "China";
s1 = s2;
printf("%s\n", s1);
return 0;
}📈 加练题(你随时喊,我随时追加到这里)
(暂无。想加练就说"再出几道 XX 题",我出的题会标上日期追加在这块。)
💬 问答区(你问过的问题 + 我的解答)
(暂无。你问的问题和我的解答会按 Q/A 整理进这里。)
该带走的 6 句话
- 数组 = 一个名字 + 一串从 0 开始的编号;
int a[10]的合法下标是 0~9,a[10]已经越界。 - 定义时的方括号是"柜子总数",引用时的方括号是"第几号柜"——长得一样,含义完全不同。
- 初始化列表没写到的元素自动为 0(字符数组为
'\0');但只定义不初始化的数组里是垃圾值。 - 二维数组在内存里是按行排成一长条的;
a[3][4]是 3 行 4 列,第二维长度永远不能省。 - 字符串 = 字符数组 +
'\0';%s、puts、strlen、strcpy、strcat、strcmp 全都"遇'\0'就停"。 - 冒泡排序:n 个数比 n-1 趟,第 j 趟比 n-j 次;相邻两两比较,大的往后沉。
📚 教材覆盖清单(第6章,利用数组处理批量数据)
这张表把本课和教材逐节对上:从网页可以回查书页,从书页也能找到网页位置。标「未展开」的,就是教材里有、本课没讲的部分,已明确列出。
| 教材节号 | 标题 | 书页 | 网站对应 | 状态 |
|---|---|---|---|---|
| 6.1 | 怎样定义和引用一维数组 | p.139–144 | 6.1 怎样定义和引用一维数组 | 精讲 |
| 6.1.1 | 怎样定义一维数组 | p.140 | 6.1.1 怎样定义一维数组 | 精讲 |
| 6.1.2 | 怎样引用一维数组元素 | p.140–141 | 6.1.2 怎样引用一维数组元素 | 精讲 |
| 6.1.3 | 一维数组的初始化 | p.142 | 6.1.3 一维数组的初始化 | 精讲 |
| 6.1.4 | 一维数组程序举例 | p.142–144 | 6.1.4 一维数组程序举例 | 精讲 |
| 6.2 | 怎样定义和引用二维数组 | p.145–150 | 6.2 怎样定义和引用二维数组 | 精讲 |
| 6.2.1 | 怎样定义二维数组 | p.146 | 6.2.1 怎样定义二维数组 | 精讲 |
| 6.2.2 | 怎样引用二维数组的元素 | p.147 | 6.2.2 怎样引用二维数组的元素 | 精讲 |
| 6.2.3 | 二维数组的初始化 | p.148 | 6.2.3 二维数组的初始化 | 精讲 |
| 6.2.4 | 二维数组程序举例 | p.149–150 | 6.2.4 二维数组程序举例 | 精讲 |
| 6.3 | 字符数组 | p.151–164 | 6.3 字符数组 | 精讲 |
| 6.3.1 | 怎样定义字符数组 | p.151 | 6.3.1 怎样定义字符数组 | 精讲 |
| 6.3.2 | 字符数组的初始化 | p.152 | 6.3.2 字符数组的初始化 | 精讲 |
| 6.3.3 | 怎样引用字符数组中的元素 | p.152 | 6.3.3 怎样引用字符数组中的元素 | 精讲 |
| 6.3.4 | 字符串和字符串结束标志 | p.153–155 | 6.3.4 字符串和字符串结束标志 | 精讲 |
| 6.3.5 | 字符数组的输入输出 | p.156 | 6.3.5 字符数组的输入输出 | 精讲 |
| 6.3.6 | 使用字符串处理函数 | p.157–161 | 6.3.6 使用字符串处理函数 | 精讲 |
| 6.3.7 | 字符数组应用举例 | p.162–164 | 6.3.7 字符数组应用举例 | 精讲 |
| 习题 | 第6章书末习题 | p.165–166 | 本课「书上习题精讲」 | 逐题精讲(第1–15题) |
| 插图 | 本课范围内教材插图 | p.139–164 | 用文字/ASCII 重画 | 未逐张复刻 |
| 附录 | 附录 A–E | p.357–370 | 全书对照页 | 未单独成课 |
- 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
- 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
- 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。