数组

数据可以存放在变量里,每一个变量有一个名字,有一个类型,还有它的生存空间。如果我们需要保存一些相同类型、相似含义、相同生存空间的数据,我们可以用数组来保存这些数据,而不是用很多个独立的变量。数组是长度固定的数据结构,用来存放指定的类型的数据。一个数组里可以有很多个数据,所有的数据的类型都是相同的。

初试数组

  • 如何写一个程序计算用户输入的数字的平均数?
    • 不需要记录,也就不需要数组
  • 如何写一个程序计算用户输入的数字的平均数,并输出所有大于平均数的数?

    • 需要记录一下,这个时候需要记录一下数

      数组

  • int number [100];

  • scanf("%d", &number[i]);

    1. int main()
    2. {
    3. int current;
    4. double sum = 0.0;
    5. int count = 0;
    6. int number[100];
    7. scanf("%d", &current);
    8. while (current != -1)
    9. {
    10. number[count] = current;
    11. sum += current;
    12. count++;
    13. scanf("%d", &current);
    14. }
    15. if (count > 0)
    16. {
    17. printf("%f\n", sum / count);
    18. for (int i = 0; i < count; i++)
    19. {
    20. if (number[i] > sum / count)
    21. {
    22. printf("%d\n", number[i]);
    23. }
    24. }
    25. }
    26. return 0;
    27. }

    注意:这段程序存在安全隐患,我们预先定义好了这个数组长度为100,而我们输入的时候,并没有限制要输入多少个,所以输入的数字数量可能超过100

    定义数组

    • <类型> 变量名称[元素数量];
      • int grades[100]
      • double weight[20]
    • 元素数量必须是整数
    • C99之前:元素数量必须是编译时刻确定的字面量
    • 数组是一个容器,这是现代程序语言设计都应该有的一种特性,特点是:
      • 其中所有的元素具有相同的数据类型
      • 一旦创建,不能改变大小
      • 数组中的元素在内存中是连续依次排列的

初始化数组

int a[4] = { 1, 2 };
int a[]  = { 1, 2, 0, 0 };
int a[]  = { 1, 2, 0, 0, };
int a[4] = { 1, 2, 0, 0, 5 };

int a[10]

  • 一个int类型的数组
  • 10个单元:a[0], a[1], ......, a[9]
    • image.png
  • 每个单元就是一个int类型的变量
  • 可以出现在赋值的左边或右边
    • a[2] = a[1] + 6;
  • 在赋值左边的叫做左值

    数组的单元

    • 数组的每个单元就是数组类型的一个变量
    • 使用数组时放在[]中的数字叫做下标或索引,下标从0开始计数:
      • grades[0]
      • grades[99]
      • average[5]

有效的下标范围

  • 编译器和运行环境都不会检查数组下标是否越界,无论对数组单元做读还是写
  • 一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃
    • segmentation fault
  • 但是可能运气好,没造成严重的后果
  • 所以这是程序员的责任来保证程序只使用有效的下标值:[0, 数组的大小-1]

长度为0的数组?

  • int a[0];
  • 可以存在,但没什么用

    数组的例子:统计个数

    写一个程序,输入数量不确定的[0, 9]范围内的整数,统计每一种数字出现的次数,输入-1表示结束。

int main()
{
    int current;
    int record[10];
    for (int i = 0; i < 10; i++)
    {
        record[i] = 0;
    }

    printf("请输入数字:");
    scanf("%d", &current);
    while (current != -1 && current >= 0 && current <= 9)
    {
        record[current]++;
        printf("请输入数字:");
        scanf("%d", &current);
    }
    for (int i = 0; i < 10; i++)
    {
        printf("record[%d]=%d ", i, record[i]);
    }
    return 0;
}

函数的定义和使用

初见函数

int fnGetLength(int num)
{
    int length = 0;
    while (num != 0)
    {
        num /= 10;
        length++;
    }
    return length;
}

求和

求出11020303545的三个和

可以发现这三个程序大同小异,都是求和,我们可以将它们封装起来,提高代码质量,提升开发效率

int fnGetSum(int num1, int num2)
{
    int sum = 0;
    for (int i = num1; i <= num2; i++)
    {
        sum += i;
    }
    return sum;
}

什么是函数

  • 函数是一块代码,接受零个或多个参数,做一件事情,并返回零个或一个值
  • 可以先现象成数学中的函数:
    • y = f(x)

函数定义

image.png

调用函数

  • 函数名(参数值);
  • ()起到了表示函数调用的重要作用
    • 即使没有参数也需要()
  • 如果有参数,则需要给出正确的数量和顺序
  • 这些值会被按照顺序依次用来初始化函数中的参数

从函数中返回

从函数中返回值

  • return停止函数的执行,并返回一个值
  • return;
  • return 表达式;
  • 可以赋值给变量
  • 可以再传递给函数

没有返回值的函数

  • void函数名(参数表)
  • 不能使用带值的return
    • 可以没有return
  • 调用的时候不能做返回值的赋值

函数的参数和变量

函数原型

函数先后关系

  • 像这样把sum()写在上面,是因为:
  • C的编译器自上而下顺序分析你的代码

如果用户自定义函数很多,全部放在main()函数前面,会降低用户的可读性,因此我们要说明函数原型

函数原型

  • 函数头,以分号;结尾,就构成了函数的原型
  • 函数原型的目的就是告诉编译器这个函数长什么样
    • 名称
    • 参数(数量及类型)
    • 返回类型
  • 旧标准习惯把函数原型写在调用它的函数里面
  • 现在一般写在调用它的函数前面
  • 原型里可以不写参数的名字,但是一般仍然写上
int fnGetLength(int num);
int fnGetSum(int num1, int num2);

函数传递

调用函数

  • 如果函数有参数,调用函数时必须传递给它数量、类型正确的值
  • 可以传递给函数的值时表达式的结果,这包括:
    • 字面量
    • 变量
    • 函数的返回值
    • 计算的结果

类型不匹配

  • 调用函数时给的值与参数的类型不匹配时C语言传统上最大的漏洞
  • 编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的
  • 后续的语言,C++/Java在这方面很严格

传过去的是什么?

void fnSwapValue(int num1, int num2)
{
    int temp = num1;
    num1 = num2;
    num2 = temp;
}

这样的代码能交换ab的值吗?
C语言在调用函数的时候,永远只能传值给函数

传值

  • 每个函数有自己的变量空间,参数也位于这个独立的空间中,和其他函数没有关系
  • 过去,对于函数参数表中的参数,叫做“形式参数”,调用函数时给的值,叫做“实际参数”
  • 由于容易让初学者误会实际参数就是实际在函数中进行计算的参数,误会调用函数的时候把变量而不是把值传进去了,所以我们不建议用这种古老的方式来称呼它们。
  • FORTRAN语言有把变量传进去的方法,而C语言只能传值
  • 我们认为,它们时参数和值的关系

本地变量

  • 函数的每一次运行,就产生了一个独立的变量空间,在这个空间中的变量,时函数的这次运行所独有的,称作本地变量
  • 定义在函数内部的变量就是本地变量
  • 参数也是本地变量

变量的生存期和作用域

  • 生存期:什么时候这个变量开始出现了,到什么时候它消亡了
  • 作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
  • 对于本地变量,这两个问题的答案是统一的:大括号内——块

本地变量的规则

  • 本地变量是定义在块内的
    • 它可以是定义在函数的块内
    • 也可以定义在语句的块内
    • 甚至可以随便拉一对大括号来定义变量
  • 程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了
  • 块外面定义的变量在里面仍然有效
  • 块里面定义了和外面同名的变量则掩盖了外面的

    函数轶事

    没有参数时

    • 编写函数原型时一定要严谨,要明确说明有没有参数,否则可能会出错
      • void swap(void);

逗号运算符

  • 调用函数时的逗号和逗号运算符怎么区分?
  • 调用函数时的圆括号里的逗号时标点符号,不是运算符
    • f(a, b)
    • f((a, b))

函数里的函数?

C语言不允许函数嵌套定义

what?

  • int i, j, sum(int a, b);
    • 定义了int型变量ij,也声明了函数sum,需要两个int型参数ab
  • return (i);
    • 没有任何意义,是一个表达式,并没有什么意义,但会让人误以为是一个函数

关于main()

  • int main()也是一个函数
  • 要不要写成int main(void)
  • return0有人看吗?
    • Windows: if errorlevel 1...(批处理,可根据不同返回结果,做相应的操作)
    • Unix Bash: echo $?
    • Csh: echo $status

一些题

  1. 对于不返回值而且只有一个int类型的参数的函数,以下哪些函数原型是正确的?
    1. void f(int x);
    2. void f();
    3. void f(x);
    4. void f(int);
  2. 以下程序的输出是什么? ```c void swap(int a, int b); int main() { int a = 5; int b = 6; swap(a,b); printf(“%d-%d\n”, a, b); return 0; } void swap(int a, int b) { int t = a; a = b; b = t; }

// swap()函数并没有改变变量的值 // 所以出书的输出结果是:5-6

<a name="qsHm1"></a>
# 二维数组
> `int a[3][5];`
> 通常理解为`a`是一个`3`行`5`列的矩阵

![image.png](https://cdn.nlark.com/yuque/0/2022/png/1557552/1641384070843-ac296537-de2a-408e-8e2a-504fd3f94ee8.png#clientId=u4f0d4fe9-4b73-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=116&id=u1afe46f4&margin=%5Bobject%20Object%5D&name=image.png&originHeight=232&originWidth=902&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12538&status=done&style=none&taskId=u3cda2d86-e6ce-4727-85e2-9953bd96eee&title=&width=451)
<a name="CF89u"></a>
### 二维数组的遍历
```c
int main()
{
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j++)
        {
            printf("%d ", a[i][j]);
        }
    }
    return 0;
}
  • a[i][j]是一个int
  • 表示第i行第j列上的单元
  • a[i, j]是什么?
    • a[j],逗号运算符的值是右边的值,也就是j

二维数组的初始化

int a[][5] = 
{
    {0, 1, 2, 3, 4},
    {2, 3, 4, 5, 6},
};
  • 列数是必须给出的,行数可以由编译器来数
  • 每行一个{},逗号分隔
  • 最后的逗号可以存在,有古老的传统;
  • 如果省略表示补零
  • 也可以用定位(* C99 ONLY
  • 参考一维数组初始化

    井字棋

    // 比较X和O数量,来确定谁赢了
    int main()
    {
      int countOfX = 0;
      int countOfO = 0;
      char chess[3][3];
    
      for (int i = 0; i < 3; i++)
      {
          for (int j = 0; j < 3; j++)
          {
              if (chess[i][j] == "x")
              {
                  countOfX++;
              }
              if (chess[i][j] == "o")
              {
                  countOfO++;
              }
          }
      }
      if (countOfX == countOfO)
      {
          printf("平局!");
      }
      else if (countOfX < countOf)
      {
          printf("X获胜!");
      }
      else
      {
          printf ("O获胜!");
      }
    }