第5章 数组
运行环境以Dev-C++、Visual Studio 2022、MacOS的命令行和Xcode为主
0.章节引言
-
1.截至目前所学的数据类型均为简单数据类型,只能存放一个值,如
int a = 1; int b = 2;。随着用户程序功能的扩展,有时需要一个变量能存储多个值,如将上述a、b、c的值存储在一个变量中,这种变量的类型属于构造类型 -
2.C语言中的数组是一种构造类型,由基本数据类型按照一定规则组成的新类型
-
3.数组中每个值的类型必须与数组的类型相同,使用数组名和下标来唯一确定数组中的元素
1.一维数组的定义和使用
-
1.1 一维数组的定义
-
一般格式:
类型说明符 数组名[常量表达式] -
注意事项
-
1.数组名的命名规则与变量名相同,遵循标识符命名规则,即只能由字母、数字和下划线组成且数字不能开头
-
2."常量表达式"表示数组元素的个数,定义数组时必须指定该值,然后系统才能根据数组的类型和长度向内存申请空间
-
3.引用元素时下标从 0 开始,所以长度为 N 的元素下标范围为 0 ~ N - 1
-
C语言不对下标做越界检查,强行访问下标为 N 或其后的元素时,有的编译器不会报错,但这是个逻辑错误,可能会造成程序或系统崩溃等严重的后果
-
以下代码因越界访问而造成“死循环”
#include <stdio.h> int main() { int i = 0; int arr[10] = {5, 6, 7, 8, 9, 0, 11, 23, 25, 55}; for (i = 0; i <= 12; i++) { arr[i] = i + 1; printf("test\n"); } return 0; } -
-
4.在
C89和C90标准中,"常量表达式"包括基本常量和符号常量(含宏常量,如#define N 7 int a[N]是允许的),不能包含变量,这一点与switch结构中case的使用相似
// 以下代码若在C99之前的标准下编译会报错 // 在C99及其之后的标准中,"常量表达式"允许包含变量,下述代码编译通过 int n = 0; scanf("%d", &n); int a[n]; int n = 0; scanf("%d", &n); int a[n] = {0}; // 报错,不能对可变数组初始化![]()
// 可通过赋值的方式对可变数组进行初始化 int i = 0, n = 5; int a[n]; for (i = 0; i < 5; i++) { a[i] = i; printf("%d ", a[i]); }- 5.定义数组时,C语言分配足够的内存来存放所有元素,且元素在内存中连续存放,数组名本身表示内存的首地址。数组的下标相当于某个元素相对于数组首地址的偏移量
-
-
关于数组的地址
- 1.数组名表示数组在内存中的首地址
#include <stdio.h> int main(){ int a[5]; printf("数组a在内存中的首地址为%x\n", a); return 0; }![]()
-
2.数组名的位移与数组元素的地址
-
数组名表示数组在内存空间中存储的首地址,首地址处存储的恰好是下标为0的数组元素
-
数组名向下偏移时增量的物理意义为:
增量值 * sizeof(数组类型)。代码中a+1的值与a+0的值相差4而不是1,因为整型数组a中每个元素均为整型,在内存中占用4B
-
#include <stdio.h> int main(){ int a[5]; printf("数组a在内存中的首地址为%x\n", a); printf("基于数组名的偏移量取地址: %x %x %x\n", a+0, a+1, a+2); printf("基于数组元素取地址: %x %x %x\n", &a[0], &a[1], &a[2]); return 0; }![]()
![]()
-
-
1.2 一维数组元素的引用
-
一般格式:
数组名[下标]; []为下标引用操作符,数组名和下标为[]运算符的操作数,优先级为1 -
注意事项
-
1.C语言中只能逐个引用数组元素,不能一次引用整个数组
-
2."下标"可以是整型常量或整型表达式,不要越界
-
3.对数组部分元素或所有元素批量读写是常规操作,常配合循环语句一起使用
-
-
案例分析
- 1.输入10个学生的成绩,先计算它们的总分,再输出它们的平均分
#include <stdio.h> int main() { // 1.定义变量 int i = 0; float scores[10] = {0}; float sum = 0.0f, avg = 0.0f; printf("请输入10个学生的成绩:\n"); // 2.数据输入 for (i = 0; i < 10; i++) { scanf("%f", &scores[i]); } // 3.数据处理 for (i = 0; i < 10; i++) { sum += scores[i]; } avg = sum / 10; // 4.数据输出 printf("平均分=%.2f\n", avg); return 0; } // 简化版代码,在数据输入的同时完成累加 #include <stdio.h> int main() { int i = 0; float scores[10] = {0}; float sum = 0.0f, avg = 0.0f; printf("请输入10个学生的成绩:\n"); for (i = 0; i < 10; i++) { scanf("%f", &scores[i]); sum += scores[i]; } printf("平均分=%.2f\n", sum/10); return 0; }
-
-
1.3 一维数组的初始化
- 1.为全部元素赋值,元素值的类型必须与数组类型匹配,
{}中值的个数不能超过数组长度
int a[5] = {5, 4, 3, 2, 1};- 2.为部分元素赋值,其余未赋值元素值默认为0
int a[10] = {5, 4, 3, 2, 1}; // a[5] ~ a[9]值均为0 int a[10] = {1*10}; // a[0] = 10, a[1] ~ a[9]值均为0- 3.若
{}中指定了所有元素值,则数组长度可省略,编译器根据赋值的个数自动定义数组的长度。当并未指定所有元素值时长度不能省略
int a[] = {5, 4, 3, 2, 1}; // 数组长度5可省略 scanf("%f", scores); // 该语句只能实现对第1个(下标为0)元素的输入,且新值一直在覆盖旧值。scores等价于&scores[0]- 4.求数组元素个数
int arr[10] = {1, 2, 3, 4, 5}; printf("数组a占用内存空间%dB\n", (int)sizeof(arr)); printf("数组a中共有%d个元素\n", (int)sizeof(arr)/(int)sizeof(arr[0]));![]()
-
5.案例分析
- 1.输出斐波那契数列的前10个数
// 分析:定义长度为10的整型数组a存储数列的前10个数。斐波那契数列从第3项(数组中下标为2的元素)开始,每个数等于前两个数之和 // 通项公式:a[i] = a[i - 1] + a[i - 2](i >= 2) #include <stdio.h> int main() { int i = 0, a[10] = {1, 1}; for (i = 2; i < 10; i++) { a[i] = a[i - 1] + a[i - 2]; } for (i = 0; i < 10; i++) { printf("%4d", a[i]); } return 0; }
- 1.为全部元素赋值,元素值的类型必须与数组类型匹配,
2.二维数组的定义和使用
-
2.1 二维数组的定义及引用
-
一般格式
-
定义:
类型说明符 数组名[常量表达式1][常量表达式2]; -
引用:
数组名[行标][列标];
-
-
注意事项
-
1.常量表达式的规则和一维数组相同,可使用宏常量
-
2.C语言采用行优先的方式来存储二维数组(顺序存储第1行、第2行...第M行的所有元素),逻辑上是一张M行N列的表格,物理上在内存中仍然是顺序存储
-
3.二维数组的输入和输出通常结合双重循环使用
- 第1行元素:
c[0][0] c[0][1] c[0][2] c[0][3] c[0][4] - 第2行元素:
c[1][0] c[1][1] c[1][2] c[1][3] c[1][4] - 第3行元素:
c[2][0] c[2][1] c[2][2] c[2][3] c[2][4]
- 第1行元素:
// 从宏观上看,总共有3行,行标遍历了0、1、2 ---> 外循环 // 从微观上看,每一行的列标均遍历了0、1、2、3、4 ---> 内循环 // 每固定到一行,列标遍历了0 ~ 4所有的数,总共遍历3行,执行逻辑恰好符合双重循环 for (i = 0; i < ROWS; i++) { for (j = 0; j < COLS; j++) { scanf("%d", &c[i][j]); } } for (i = 0; i < ROWS; i++) { for (j = 0; j < COLS; j++) { printf("%d ", c[i][j]); } }- 4.一维数组是普通单个字面量的集合,而二维数组相当于多个一维数组的组合,逻辑上的每一行可以看做一个一维数组,
c[0]、c[1]、c[2]可看做一维数组的数组名。由于数组名也是地址,所以c[0]、c[1]、c[2]为每行元素开始存储时的首地址,也就是c[0][0]、c[1][0]、c[2][0]的首地址,具体可通过以下代码验证
#include <stdio.h> #define M 3 #define N 5 int main(){ int c[M][N] = {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {11, 12, 13, 14, 15}}; printf("每一行元素的首地址分别为: %x %x %x\n", c[0], c[1], c[2]); printf("每一行第一个元素的首地址分别为: %x %x %x", &c[0][0], &c[1][0], &c[2][0]); return 0; }![]()
-
5.实际应用中可采用按列优先访问方式,具体代码如下:
- 第1列元素:
c[0][0] c[1][0] c[2][0] - 第2列元素:
c[0][1] c[1][1] c[2][1] - 第3列元素:
c[0][2] c[1][2] c[2][2] - 第4列元素:
c[0][3] c[1][3] c[2][3] - 第5列元素:
c[0][4] c[1][4] c[2][4]
- 第1列元素:
// 从宏观上看,总共有5列,列标遍历了0、1、2、3、4 ---> 外循环 // 从微观上看,每一列的行标均遍历了0、1、2 ---> 内循环 // 每固定到一列,行标遍历了0 ~ 2所有的数,总共遍历5列,执行逻辑恰好符合双重循环 for (i = 0; i < COLS; i++) { for (j = 0; j < ROWS; j++) { scanf("%d", &c[j][i]); // 注意行标为j,列标为i } } for (i = 0; i < COLS; i++) { for (j = 0; j < ROWS; j++) { printf("%d ", c[j][i]); } } -
-
-
2.2 二维数组的初始化
- 1.分行全部赋值,直观易懂,容易看出各元素的值,推荐使用该赋值方式
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};- 2.分行部分赋值,未赋值的元素值为0
int a[3][4] = {{1, 2}, {9, 10, 11}}; // 下标为2的行不赋值 int a[3][4] = {{1, 2}, {}, {9, 10, 11}} // 下标为1的行不赋值- 3.按一维数组的形式(部分)赋值
int a[3][4] = {1, 2, 3, 4, 5} // 缺乏直观性,可读性差,不推荐使用该赋值方式- 4.若为全部元素赋值,定义数组时可省略第一维的长度,但第二维长度不能省略,系统根据数据的总数分配内存
int a[][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}- 5.若采用分行部分赋值形式初始化数组,也可省略第1维长度
int a[][4] = {{1, 2}, {}, {9, 10, 11}}; // 系统可确定为3行4列-
6.案例分析
- 1.输入一个 3 * 4 矩阵,找出矩阵中负数的个数并输出
// 分析:1.3行4列的二维数组也可称作3行4列的矩阵 // 2.题目的本质在于通过双重for循环对数组赋值后遍历,找到负数并计数 #include <stdio.h> int main() { int matrix[3][4] = {0}; int i = 0, j = 0, count = 0; printf("输入一个3×4矩阵:\n"); for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { scanf("%d", &matrix[i][j]); // 如果当前元素是负数,则计数器加1 if (matrix[i][j] < 0) { count++; } } } // 输出矩阵中负数的个数 printf("矩阵中负数的个数是: %d\n", count); return 0; }
3.字符数组的定义和使用
-
3.1 字符数组的定义及元素引用
- 一般格式:定义方式与一维数组相似,只是数组和元素类型为
char
char s[6]; // 字符数组s,长度为6B,元素为s[0] ~ s[5]-
注意事项
- 1.C语言中不存在字符串类型,用字符数组来存储字符串
char s1[10] = {'f', 'a', 's', 'h', 'i', 'o', 'n'} // 定义字符数组s1存放字符串"fashion"- 2.C语言规定字符
'\0'作为字符串的结束标志符,存储字符串常量时自动在其末尾添加'\0'。'\0'表示对字符'0'进行转义,转义之后表示特殊的含义,ASCII码值为0。该标志用于判断字符串是否结束,而不是用字符串数组长度
for (i = 0; s[i] != '\0'; i++) { putchar(s[i]); }-
3.实际应用中更关注字符串的长度,而不是字符数组的长度。由于字符串末尾有默认的
'\0',所以在为字符数组赋值时,要预留出1个字符的位置放置'\0' -
4.字符串字面量初始化与字符列表初始化的区别
// 代码1 #include <stdio.h> int main(){ char arr1[] = "abcd"; char arr2[] = {'a', 'b', 'c', 'd'}; // 末尾不含'\0' printf("%s\n", arr1); printf("%s\n", arr2); return 0; } char arr3[5] = {'a', 'b', 'c', 'd', '\0'}; // 等价于 char arr3[] = "abcd";![]()
![]()
// 代码2 #include <stdio.h> int main() { char s1[] = "helloworld"; char s2[] = {'h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd'}; puts(s1); puts(s2); return 0; } // 以上代码在Visual Studio 2022中的运行结果如下图,有乱码(下图1) // Visual Studio 2022中在尝试打印s2时会有异常提示,因为省略字符数组长度以字符形式赋值时,不包含'\0'(下图2) // 调试阶段查看s1和s2的值,发现s1实际上是11个字符,最后有'\0';而s2只有赋值的10个字符,不含'\0'(下图3)![]()
![]()
![]()
-
5.区分
0 '0' '\0'-
数字
0,int型字面量,值为0 -
字符
'0',字符常量,ASCII码为48 -
字符
'\0',空字符,ASCII码为0
![]()
-
- 一般格式:定义方式与一维数组相似,只是数组和元素类型为
-
3.2 字符数组的初始化
- 1.字符列表初始化,对部分(或全部)字符赋值,其余元素为零值(即ASCII码为0的字符)。若
{}内字符个数超过数组长度会报语法错误
char s1[10] = {'f', 'a', 's', 'h', 'i', 'o', 'n'}; // s1[7] ~ s1[9]的结果均为'\0',这是不完全初始化带来的默认初始化- 2.字符串字面量初始化,字符个数应少于数组长度,书写简洁
char s2[10] = {"fashion"}; // s1[7]~s1[9]的结果均为'\0'- 3.省略一维长度,字符串长度由赋值号右边字符串常量的字符个数决定
char s3[] = {"perfect"}; char s4[] = "perfect"; // 数组长度为8 - 1.字符列表初始化,对部分(或全部)字符赋值,其余元素为零值(即ASCII码为0的字符)。若
-
3.3 字符数组的输入/输出
- 1.逐个处理字符数组元素
char s[10] = {0}; for (i = 0; i < 10; i++) { scanf("%c", &s[i]); } for (i = 0; i < 10; i++) { printf("%c", s[i]); }-
2.使用格式符
%s整体输入与输出字符数组- 使用scanf()函数整体输入
// 1.数组s最多接收9个字符 // 2.当输入的字符串中存在空格、制表符或回车符,因为scanf()的存在,上述字符会作为字符串的输入结束标志 char s[10] = {0}; scanf("%s", s); // 若输入"thank you",则只有"thank"会被接收 scanf("%s", &s); // 错误写法,s本身已经是地址- 使用printf()函数整体输出
// 若数组以字符形式对全部元素赋值,且元素中不包含'\0',采用"%s"输出时,由于检测不到结束标志,可能会额外输出乱码字符。即3.1注意事项部分第4点提及的内容 char str[] = "perfect"; printf("%s", str); // 系统检测到第一个'\0'即停止输出,打印"perfect" char str[] = "hello!\0ab"; printf("%s", str); // 系统检测到第一个'\0'即停止输出,打印"hello!" printf("ab0d"); // 打印"ab0d" printf("abc\0ef"); // 打印"abc" printf("abc\\0ef"); // 打印"abc\0ef" -
3.使用gets()函数或puts()函数整体输入与输出字符数组
- 使用gets()函数整体输入
// gets(字符数组名); 从键盘上输入一个字符串赋给该数组 char s[20] = {0}; gets(s); // gets()函数以回车符作为数据输入结束的标志,因此空格可以作为字符串的一部分输入,这一点与scanf()函数完全不同- 使用puts()函数整体输出
// puts(字符数组名); puts(字符串常量); 将字符数组或字符串输出 char str[20] = {"One Two \0 Three"}; printf("%s", str); puts(str); puts("thank you! \0 abc "); // puts()函数和printf()函数都能输出字符串中第一个'\0'(不含)之前的所有字符,不同的是puts()函数输出字符串后会自动换行
-
3.4 字符串处理函数
-
1.求串长函数strlen(s)
-
作用:计算字符串
s的长度,即首次出现'\0'之前的字符个数,s可以为字符串常量或字符数组 -
案例分析
char s2[20] = {"One\0 Two \0 Three"}; // strlen(s2)为3 char s3[] = "thank&you!\0 abc"; // strlen(s3)为10 strlen("abc\t1234\123\n"); // 结果为10,'\t'和'\n'均为转义字符 strlen("C:\test\628\test.c"); // 结果为14,'\62'是一个转义字符,'8'是单独的字符,八进制中不包含8- 代码实现:编写代码计算字符串
str的长度,从第一个字符开始遍历,逐次累加基数变量,直到当前字符为'\0'
#include <stdio.h> int main() { char str[] = "hello\0abc"; int length = 0; while (str[length] != '\0') { length++; } printf("%d", length); return 0; } -
-
2.串赋值函数strcpy(s1, s2)
-
作用:将字符串
s2的内容复制到字符串s1中,复制方向为第2个参数赋值给第1个参数,s1必须是容量足够大的字符数组,s2可以是字符串常量或字符数组 -
案例分析
char s1[20] = "thank you!", s2[] = "hello!"; strcpy(s1, s2); // 此时s1为"hello!" // 字符串之间的赋值不能通过普通变量赋值语句a = b;实现,因为字符数组名是地址常量- 代码实现:编写代码实现字符串的复制,遍历源字符数组,依次将读取的字符复制到目标数组中,直到源数组读取到
'\0',注意目标数组的结束符\0
#include <stdio.h> int main() { char src[20] = "thank you!", dest[20] = {0}; int i = 0; while (src[i] != '\0') { dest[i] = src[i]; i++; } // dest[i] = '\0'; return 0; } -
-
3.串连接函数strcat(s1, s2)
-
作用:将字符串
s2的内容连接到字符串s1的后面构成一个新的字符串并存入字符串s1中。连接方向为第2个参数到第1个参数,s1必须是容量足够大的字符数组,s2可以是字符串常量或字符数组 -
案例分析
// 连接时,系统自动删除 s1 后的 '\0',然后将 s2 的内容连接到 s1 后方,并在新字符串末尾添加 '\0' char s1[20] = "thank ", s2[] = "you!"; strcat(s1, s2); // 此时s1为"thank you!"- 代码实现
#include <stdio.h> int main() { char src[20] = "very much!", dest[20] = "thank you "; int dest_index = 0, src_index = 0; // 找到目标字符串的结束位置 while (dest[dest_index] != '\0') { dest_index++; } // 将源字符串复制到目标字符串的末尾 while (src[src_index] != '\0') { dest[dest_index] = src[src_index]; dest_index++; src_index++; } // 添加字符串结束符 dest[dest_index] = '\0'; return 0; } -
-
4.串比较函数strcmp(s1, s2)
-
作用:比较字符串s1和s2内容的大小,s1、s2可以是字符串常量或字符数组
-
比较标准
-
字符串比较并不是比较它们的长度,而是对两个字符串从左至右依次比较对应的字符(比较标准为ASCII码值),直到遇到不同字符为止
-
只有全部字符相同时才认为两字符串相等,否则以遇到的第一个不同字符的相对大小来确定大小关系
-
若strcmp(s1, s2)的返回结果小于0,则s1 < s2;若返回结果大于0,则s1 > s2;否则s1 = s2
-
-
案例分析
"abc"大于"ABC",因为'a' > 'A' "abCdef"小于"abcd",因为'C' < 'c' "COM"小于"COMPUTER",因为'\0'小于'P'- 代码实现:写代码实现两个字符串的比较功能
// 情况1:"thank you" VS "thaNk you" // 情况2:"hello" VS "Welcome" // 情况3:"chinese" VS "chineseGOOD" // 情况4:"english" VS "english" #include <stdio.h> int main() { char s1[20] = "thank you!", s2[20] = "thaNk you"; int i = 0; for (i = 0; s1[i] != '\0' && s2[i] != '\0'; i++) { if (s1[i] != s2[i]) { printf("%d\n", s1[i] - s2[i]); return 0; } } printf("%d\n", s1[i] - s2[i]); return 0; } #include <stdio.h> int main() { char s1[20] = "thank you!", s2[20] = "thaNk you"; int i = 0; int sub = 0; for (i = 0; s1[i] != '\0' && s2[i] != '\0'; i++) { if (s1[i] != s2[i]) { break; } } sub = s1[i] - s2[i]; if (sub > 0) { printf("s1 > s2\n"); } else if(sub < 0) { printf("s1 < s2\n"); } else { printf("s1 == s2\n"); } return 0; } -
-
4.数组的应用举例
- 1.找出10个学生中成绩低于平均成绩的人数并输出
// 分析:1. 定义一个长度为10的浮点型数组scores,存储10个学生成绩。for循环 scanf()输入
// 2. 计算出平均成绩。为简化程序代码量,可以在成绩输入的同时将其累加到变量avg中
// 3. 计算平均成绩,统计低于平均成绩的人数。遍历 for循环中嵌入if 计数变量 printf()打印输出
#include <stdio.h>
#define NUM 10
int main(int argc, const char * argv[]) {
// insert code here...
int i = 0, count = 0;
float scores[NUM] = {0.0f};
float avg = 0.0f;
printf("请输入%d位学生成绩:", NUM);
for (i = 0; i < NUM; i++) {
scanf("%f", scores + i);
avg += scores[i];
}
avg /= NUM;
printf("\n平均成绩:%.1f\n", avg);
for (i = 0; i < NUM; i++) {
if (scores[i] < avg) {
count++;
}
}
printf("低于平均成绩的人数为: %d\n", count);
return 0;
}
-
2.输入 5 个学生的 3 门课程成绩,求每个学生的平均成绩和每门课程的平均成绩
-
1.明确本题与上一题的差别,维度升为"二维"。因为成绩是由<学生,科目>共同决定的,因此定义二维数组
scores[5][3]存储各学生各科成绩,需要用到双层for循环、scanf语句 -
2.各学生的平均成绩 vs 各门课的平均成绩,刚好是
scores[5][3]的两个分量维度——科目和学生。科目有多门,学生也有多个,可分别用一维数组avg_stu[5]和avg_course[3]存储- 2.1 求解平均成绩,应当先求总成绩,包括每位学生的总成绩和每门科目的总成绩,参考下图:需求转化为计算各行数据的平均值 + 各列数据的平均值
![]()
-
2.2 此问题本质上依然是二维数组的遍历,因此双重for循环必不可少,只是双重循环内要做的是按行循环加和与按列循环加和。以按列循环为例,累加的对象是
s[0][0]、 s[1][0]、 s[2][0]、 s[3][0]、 s[4][0]……以此类推。在行列标中,列号是不变的,所以它充当了外循环,行号依次递增,所以它充当了进入外循环后的内循环 -
2.3 遍历avg_stu[5]和avg_course[3],输出各元素值,使用for循环、printf
#include <stdio.h> #define NUM 10 int main(int argc, const char * argv[]) { // insert code here... float scores[5][3] = {0}; float avg_stu[5] = {0}, avg_course[3] = {0}; int i = 0, j = 0; printf("输入5个学生的3门课程成绩:\n"); for (i = 0; i < 5; i++) { for (j = 0; j < 3; j++) { scanf("%f", &scores[i][j]); avg_stu[i] += scores[i][j]; } avg_stu[i] /= 3; } for (i = 0; i < 3; i++) { // 由于数组avg_course已经初始化,此处无需再加语句 avg_course[i] = 0; for (j = 0; j < 5; j++) { avg_course[i] += scores[j][i]; // 注意此处的行列标顺序为j i而非i j } avg_course[i] /= 5; } printf("每个学生的平均成绩为:\n"); for (i = 0; i < 5; i++) { printf("%-8.2f", avg_stu[i]); } printf("\n每门课程的平均成绩为:\n"); for (i = 0; i < 3; i++) { printf("%-8.2f", avg_course[i]); } return 0; } // 本题的另一种非数组解法在第4章第4小节的循环嵌套案例2已提到 -
-
3.已有一个排好序的序列,输入一个数插入到该序列中,使之仍然保持有序。例如将15插入到有序序列 {3, 7, 10, 12, 18, 20} 中
-
将有序序列保存在一个数组中,逆序遍历序列,凡是大于待插入数据
x的数依次后移动。"移动"是重复动作且次数不确定,此处选择while循环 -
情形1:待插入数据比所有数据都大
-
以 25 为例,逆序遍历时先判断
a[5](值为20) < x(值为25),所以a[5]前方的数据都不需要再判断,循环直接结束,将25写在a[5]后方的存储位置,即a[6] -
整个过程没有数组元素的移动,直接将 25 覆盖掉了
a[6]位置上原始的0(定义时数组长度为20)。故插入一个大于所有数组元素的数时不存在异常情况
-
-
情形2:待插入数据比所有数据都小
-
以 1 为例,它小于所有数,
a[5] ~ a[0]的数均需依次后移一个位置(a[5]的值移动到a[6],a[4]的值移动到a[5],依次类推) -
当
while循环判断a[0] > x(即 3 > 1)时依然成立,继续执行循环体会使得下标i减 1,就会判断a[-1] > x,产生逻辑错误。故插入一个小于所有数组元素的数时存在下标越界的异常情况 -
在
while循环中加一个i >= 0的条件表达式防止下标越界
-
#include <stdio.h> int main() { int a[20] = {3, 7, 10, 12, 18, 20}; int x = 1, n = 6; int i = n - 1; while(i >= 0 && a[i] > x) { a[i + 1] = a[i]; i--; } a[i + 1] = x; n ++; for(i = 0; i < n; i ++) { printf("%-4d", a[i]); } return 0; }- 逻辑错误分析:它不一定会导致程序运行错误或崩溃,但可能会产生严重的后果
-
数组下标为 -1 的地址是越界访问,该地址是有意义的,即所申请的数组存储空间的首地址向前偏移一个单位所对应的地址
-
该地址未随数组空间一起初始化,其中的数据值不确定。若是正在被系统或其他APP使用中的地址空间,则可以被访问,其数据的意义取决于系统或APP所写入的数据,访问后可能会引起系统或APP异常
-
没有被使用的地址被称为"野地址",其数据随机且无意义,因此数组中的 -1 下标应当避免,如以下代码
-
// 以下代码在MacOS的Xcode中运行为死循环,直接原因是下标越界,根本原因是变量 i 的地址恰巧与 arr[-5] 的地址一致 #include <stdio.h> int main(int argc, const char * argv[]) { int i = 0; int arr[6] = {6, 5, 4, 3, 2, 1}; // printf("i: %p\n", &i); for (i = 5; i >= -5; i--) { arr[i] = 0; printf("循环内\n"); // printf("&arr[%d]: %p\n", i, &arr[i]); } printf("循环外\n"); return 0; }![image]()
-
-
4.编写一个程序,将字符串转置并输出。例如"abcde"——>"edcba"
// 方法1:使用两个字符数组 s 和 t,倒序遍历 s,依次将从 s 中读取到的字符顺序写入 t 中。此方法中 s 的下标从后往前,t的下标从前往后,理清下标的变化关系
#include <stdio.h>
#include <string.h>
#define MAX 100
int main() {
char s[MAX] = {0};
char t[MAX] = {0};
int i = 0, j = 0;
int n = 0;
printf("请输入字符串:");
gets(s);
n = strlen(s);
i = n - 1;
while(i >= 0) {
// t[j++] = s[i--];
t[j] = s[i];
i--;
j++;
}
// 由于数组 t 最开始已初始化为0, 此处无需再加"t[j] = '\0';",若未初始化则需要加上。建议无论是数组还是普通变量在定义的同时直接初始化
printf("转置后的字符串为:");
puts(t);
return 0;
}
// 方法2:仅使用一个字符数组s,就地转置,即 s 中第 1 个字符和最后一个字符交换,第 2 个字符和倒数第 2 个字符交换,以此类推。定义 i 作为下标从前往后移动,定义 j 作为下标从后往前移动
// 以长度为奇数位的字符串"score"为例,'s'与'e'交换,'c'与'r'交换,'o'没必要再与自身交换,即 i 后移到'o'、j 前移到'o'时循环退出,此时 i == j
// 以长度为偶数位的字符"orange"为例,'o'与'e'交换,'r'与'g'交换,'a'与'n'交换,此时已完成逆序操作。当 i 后移到 'n',j 前移到 'a' 时循环退出,此时 i > j
// 综上,循环继续的条件为以上两种情况取并集(i >= j)后的补集,即 i < j
#include <stdio.h>
#include <string.h>
#define MAX 100
int main() {
char s[MAX] = {0};
char temp = 0;
int i = 0, j = 0;
int n = 0;
printf("请输入字符串:");
gets(s);
n = strlen(s);
j = n - 1;
while(i < j) {
temp = s[i];
s[i] = s[j];
s[j] = temp;
i++;
j--;
}
printf("转置后的字符串为:");
puts(s);
return 0;
}















浙公网安备 33010602011771号