第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.在C89C90标准中,"常量表达式"包括基本常量和符号常量(含宏常量,如#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;
      }
      

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]
      // 从宏观上看,总共有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]
      // 从宏观上看,总共有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
    
  • 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;
}
posted @ 2025-03-06 09:40  pycoder_666  阅读(59)  评论(0)    收藏  举报