第6章 数组与函数

6.1 数组基础

6.1.1 初试数组

求平均数的题目

int x;
double sum = 0;
int cnt = 0;
scanf("%d", &x);
while(x!=-1) {
    sum+=x;
    cnt++;
    scanf("%d", &x);
}
if(cnt>0) {
    printf("%f\n", sum/cnt);
}

但是,如果题目变了呢?要求你写一个程序计算用户输入的数字的平均数,并输出所有大于平均数的数,那怎么办?

如何记录很多数呢?

  • int num1, num2, num3...

数组

  • int number[100];
  • scanf("%d", &number[i]);
int sum=0, x, count=0, number[100];//定义数组
double average;
	
scanf("%d", &x);
while(x!=-1) {
	number[count] = x;//对数组中的元素赋值
    {
        int i;
        printf("%d\t",count);
        for(i=0;i<=count;i++) {
            printf("%d\t", number[i]);
        }
        printf("\n");
    }
	sum+=x;
	count++;
	scanf("%d", &x);
}

if(cnt>0) {
    int i;
    average = 1.0*sum/count;
    for(i=0;i<count;i++) {//遍历数组
        if(number[i]>average) {
            printf("%d", number[i]);//使用数组中的元素
        }
    }

}
return 0;
  • 但个程序存在安全隐患,是什么?
    • 看一下它的数组数量的定义,是固定的,所以当我们输入的数大于这个元素数量,那么就会出现问题。

6.1.2 定义数组

定义数组

  • <类型> 变量名称[元素数量];
    • int grades[100];
    • double weight[20];
  • 元素数量必须是整数
  • C99之前:元素数量必须是编译时刻确定的字面量

数组

  • 数组是一种容器(放东西的东西),特点是:
    • 其中所有的元素具有相同的数据类型;
    • 一旦创建,不能改变大小
    • (数组中的元素在内存中是连续依次排列的)
  • 语言所能提供容器的大小,可以提现出语言能力的大小

int a[10]

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

数组的单元

  • 数组的每一个单元救赎数组类型的一个变量
  • 使用数组时放在[]中的数字叫做下标或索引,下标从0开始计数:
    • grades[0]
    • grades[99]
    • average[5]
  • 不只是C语言有数组,但是从0开始起,是从C语言起的

有效的下标范围

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

所以上面的程序是有问题的,那么我们有两个解决方案:

  • 一,局限性的一百个元素数量
  • 二,在计算的过程中来计数(C99新功能)

计算平均数

  • 如果先让用户输入有多少数字要计算,可以用C99的新功能来实现
int x;
double sum = 0;
int cnt;
printf("请输入数字的数量:");
scanf("%d", &cnt);
if(cnt>0) {
    int number[cnt];
    scanf("%d", &x);
    while(x!=-1) {
        number[cnt]=x;
        sum+=x;
        cnt++;
        scanf("%d", &x);
    }
}
  • 这个数组创造了,就不能再改变了,这是数组的根本特点。

长度为0的数组?

  • int a[0];
  • 可以存在,但是无用,也就是没有意义
int sum=0, x, count=0, number[100];
double average;
scanf("%d", &x);
while(x!=-1) {
	number[count] = x;
	sum+=x;
	count++;
	scanf("%d", &x);
}
average = 1.0*sum/count;
while(count!=0) {
	if(number[count-1]>average)
		printf("%d ", number[count-1]);
	count--;
}
return 0;

6.1.3 用数组做散列计算

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

三种算法

一,最优算法

const int number = 10;//数组的大小
int num[number];
int x;
int i;
for(i=0;i<number;i++) {//初始化,这是个安全的做法
    num[i]=0;
}
scanf("%d", &x);
while(x!=-1) {
    if(x>=0&&x<=9) {
        num[x]++;//参与运算
    }
	scanf("%d", &x);
}	
for(i=0;i<number;i++) {
	printf("有%d个%d\n", num[i], i);
}
return 0;
  • 要懂的掌握简单方法和技巧

二,我的版本

#include <stdio.h>

int main() {
	int num[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
	int number[100];
	int x, count = 0;
	
	scanf("%d", &x);
	while(x!=-1) {
		number[count] = x;
		count++; 
		scanf("%d", &x);
	}
	
	int i, j;
	for(i=0;i<count;i++) {
		for(j=0;j<10;j++) {
			if(number[i]==j) {
				num[j]++;
			}
		}
	}
	for(i=0;i<10;i++) {
		printf("有%d个%d\n", num[i], i);
	}
	
	return 0;
}

三,分支思路

int number[100];
int x,count=0;
int one=0, two=0, three=0, four=0, five=0, six=0, seven=0, eight=0, nine=0, ten=0, zero=0;
	
scanf("%d", &x);
while(x!=-1) {
	number[count] = x;
	count++;
	scanf("%d", &x);
}
	
printf("%d", count);
count--;
	while(count!=0) {
		switch(number[count]) {
			case 1:
				one++;
				break;
			case 2:
				two++;
				break;
			case 3:
				three++;
				break;
			case 4:
				four++;
				break;
			case 5:
				five++;
				break;
			case 6:
				six++;
				break;
			case 7:
				seven++;
				break;
			case 8:
				eight++;
				break;
			case 9:
				nine++;
				break;
			case 10:
				ten++;
				break;
			default:
				zero++;
				break;
		}
		count--;
	}
	
	printf("1:%d\n 2:%d\n 3:%d\n", one, two, three);
	return 0;

6.2 函数基础

6.2.1 初见函数

素数求和

int isPrime(int i) {
	int ret=1;
	int k;
	for(k=2;k<i-1;k++) {
		if(i%k==0) {
			ret=0;
		}
	}
	return ret;
}

int main() {
	int m, n;
	int i;
	int sum=0;
	scanf("%d %d", &m, &n);
	
	if(m==1) m=2;
	for(i=m;i<=n;i++) {
		if(isPrime(i)) {
			sum+=i;
		}
	}
	printf("%d", sum);
	return 0;
}

"代码复制“是代码质量不好的表现

void sum(int a, int b) {
	int sum;
	int i;
	for(i=a;i<=b;i++) {
		sum+=i;
	}
	printf("从%d到%d的和是%d\n", a, b, sum); 
}

int main() {
	int m, n;
	scanf("%d %d", &m, &n);
    sum(m, n);
	return 0;
}
  • 要把代码相同,或者重复的部分取出来封装成函数

6.2.2 函数的定义和使用

什么是函数?

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

函数定义

64

调用函数

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

函数返回

  • 函数知道每一次是哪里调用它,会返回到正确的地方

6.2.3 从函数中返回

从函数中返回值

  • 我们都知道C语言有个原则“单一出口”

    • int max(int a, int b) {
          int ret;
          if(a>b) {
              ret = a;
          } else {
              ret = b;
          }
          return ret;
      }
      
  • int max(int a, int b) {
        if(a>b) {
            return a;
        } else {
            return b;
        }
    }//这个程序不好,就是因为有多个出口,这个可以,但这个不太好
    
  • return停止函数的执行,并送回一个值

    • return;
    • return 表达式;
65
  • 丢弃那种比较常见,有时候我们没有返回值,就是这种了

没有返回值的函数

  • void函数名(参数表)
  • 不能使用带值的return
    • 可以没有return
  • 调用的时候不能做返回值赋值
    • 而如果函数是有返回值的,那就必须带return返回值

6.3 函数深入

6.3.1 函数原型

函数先后关系

66
  • 如果你在它调用之前,而没有声明过sum,那它会一脸懵逼的

如果不知道

  • 也就是把要调用的函数放到下面了
int main() {
    sum(1, 10);
    sum(20, 30);
    sum(35, 45);
    return 0;
}

void sum(int begin, int end) {
    int t;
    int sum = 0;
    for(i=begin;i<=end;i++) {
        sum+=i;
    }
    printf("%d到%d的和是%d\n", begin, end, sum);
}
  • 会出现错误(在LLVM编译器下的结果,可能也是因为它是一个严格的编译器)

  • 而我们不想把函数放在前面,因为它会太繁杂,而挡住了主函数,那怎么办呢?这个时候我们就用到了函数的原型声明:

    void sum(int begin, int end);//把函数头移到前面
    
    int main() {
        sum(1, 10);
        sum(20, 30);
        sum(35, 45);
        return 0;
    }
    //这下面这一块叫函数的定义
    void sum(int begin, int end) {
        int t;
        int sum = 0;
        for(i=begin;i<=end;i++) {
            sum+=i;
        }
        printf("%d到%d的和是%d\n", begin, end, sum);
    }
    
    • 我们也要确保它的声明和定义的函数头是相同的,否则就是白费

函数原型

  • 函数头,以分号“;“结尾,就构成了函数的原型
67
  • 函数原型的目的是告诉编译器这个函数长什么样

    • 名称
    • 参数(数量及类型)
    • 返回类型
  • 旧标准习惯把函数原型写在调用它的函数里面。旧版的会像下面一样:

    
    int main() {
        int i;
        void sum(int begin, int end);//把函数头移到前面
    
        sum(1, 10);
        sum(20, 30);
        sum(35, 45);
        return 0;
    }
    //这下面这一块叫函数的定义
    void sum(int begin, int end) {
        int t;
        int sum = 0;
        for(i=begin;i<=end;i++) {
            sum+=i;
        }
        printf("%d到%d的和是%d\n", begin, end, sum);
    }
    
    • 旧版的会像上面这个一样,它的所有初始化或者声明(无论是变量还是函数这些)都是在前面的
  • 现在一般写在调用它的函数前面

  • 再深入

    void sum(int , int );//这样也不会有影响,因为只是提醒它有这个函数存在
    //void sum(int a, int b);也可以,但为了对人类有意义,最好还是和下面的函数头相匹配(相同的)
    int main() {
        sum(1, 10);
        sum(20, 30);
        sum(35, 45);
        return 0;
    }
    
    void sum(int begin, int end) {
        int t;
        int sum = 0;
        for(i=begin;i<=end;i++) {
            sum+=i;
        }
        printf("%d到%d的和是%d\n", begin, end, sum);
    }
    

6.3.2 参数传递

调用函数

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

类型不匹配?

  • 调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞

  • 编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的

    • void cheer(int i) {
          printf("cheer %d\n", i);
      }
      
      int main() {
          cheer(2.0);//输出2
          double f = 2.4;
          cheer(f);//输出2
              
          return 0;
      }
      
  • 后续的语言,C++/Java在这方面很严格

传过去的是什么?

void swap(int a, int b);

int main() {
    int a=5, b=5;
    swap(a, b);
    printf("a=%d, b=%d\n", a, b);
    
    return 0;
}

void swap(int a, int b) {//这里的a和b和上面的a和b没有任何关系
    int swap;
    int t = a;
    a = b;
    b = t;
}
  • 这样完全不能交换a和b的值
  • 所以C语言在调用函数时,永远只能传值给函数

传值

69
  • 这个传参与实参来源于Fortran,而有些语言的确能在调用函数的时候传入变量。
  • 我们认为它们是参数和值的关系

6.3.3 本地变量

本地变量

  • 函数的每次运行,就产生了一个独立的变量空间,在这个空间中的变量,是函数这次运行所独有的,称作本地变量
  • 定义在函数内部的变量就是本地变量
  • 参数也是本地变量,在参数表里面的这些参数与本地变量一样具有相同的作用域

变量的生存期和作用域

  • 生存期:什么时候这个变量开始出现,到什么时候它消亡了

  • 作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)

  • 对于本地变量,这两个问题的答案是统一的:大括号内—快

  • 自己的结论:而当两个函数拥有着相同变量名且相同类型的变量时,它们其实都互不相关

70

本地变量的规则

  • 本地变量是定义在块内的

    • 它可以是定义在函数的块内

    • 也可以是定义在语句的块内(If语句)

    • 定义在大括号内也一样,例如下面这一句。

      //例一
      {
          int i = 888;
      }
      
      printf("%d", i);//它会出现error说你没有声明定义这个变量;
         
      //例二
      int i = 999;
      {
          int i = 888;
      }
      
      printf("%d", i);//它会输出999,而不是888.为什么呢?因为它的生存周期与作用域只在大括号里面
      
  • 也就是程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了

  • 块外面定义的变量在里面仍然有效,例如

    int i = 999;
    {
       printf("%d", i);//它会输出999
    }
    
  • 块里面定义了和外面同名的变量则掩盖了外面的

    int a = 5;
    int b = 6;
    
    {
        int a = 0;
        printf("a=%d\n", a);//输出0
    }
    printf("a=%d, b=%d\n", a, b);//输出a=5,b=6。因为在这里的话,里面括号的那个a就消失了,不见了
    
  • 不能在块内定义同名的变量

  • 本地变量不会被默认初始化

  • 但参数在进入函数的时候就会被初始化了(就是在调用函数的时候输入的实参)

6.3.4 杂事

没有参数时

  • void f(void);

  • 还是void f();

    • 在传统C中,它表示f函数的参数表未知,并不表示没有参数

      void swap();
      
      int main() {
          int a=5, b=5;
          swap(a, b);//因为函数是需要两个int的传递
          printf("a=%d, b=%d\n", a, b);
          
          return 0;
      }
      
      void swap(int a, int b) {能编译
          int swap;
          int t = a;
          printf("a=%f, b=%f", a, b); 
          a = b;
          b = t;
      }
      

      例二

      void swap();//2.所以不要写出这样的原型来
      //void swap(void);要这样写
      
      int main() {
          int a=5, b=5;
          swap(a, b);
          printf("a=%d, b=%d\n", a, b);
          
          return 0;
      }
      
      void swap(double a, double b) {//1.得到的数字很奇怪
          int swap;
          int t = a;
          printf("a=%f, b=%f", a, b); 
          a = b;
          b = t;
      }
      

逗号运算符?

  • 调用函数时的逗号和逗号运算符怎么区分?

    • f(a, b)这个是函数的标点符号
    • f((a, b))这个是逗号运算,是一个结果
  • 调用函数时的圆括号里的逗号是标点符号,而不是运算符

函数里的函数?

  • C语言不允许函数嵌套定义
  • 但可以放另一个函数的声明

这是什么?

  • int i, j, sum(int a, int b);
    • C语言支持这样的写法,但不推荐
  • return (i);
    • 这个括号可以加也可以不加都行
    • 而加上了会让人类误以为i是函数

关于main

  • int main()也是一个函数
  • 要不要写成int main(void)?
  • return的0有人看吗?
    • Windows:if error level 1...
    • Unix Bash:echo $?
    • Csh: echo $status
    • return 0是有意义的
  • main函数不是程序第一条运行的代码,在mian函数前还有其它东西,这些东西是为你的运行做准备的,当做好准备后,会调用main函数

6.4 数组进阶

6.4.1 二维数组

二维数组

  • int a[3][5];
    //int a[行][列]
    
  • 通常理解为a是一个3行5列的矩阵

    71

二维数组的遍历

for(i=0;i<3;i++) {
    for(j=0;j=5;j++) {
        a[i][j]  = i*j;
    }
}

//a[i][j]是一个int
  • 表示第i行第j列上的单元
    • a[i, j]是什么?
      • 这个不是一个正确的表达二元数组的方式,在这里,它是个逗号运算,其结果是j。所以它表达a[j]

二维数组的初始化

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

tic-tac-toe游戏(井字棋)

  • 读入一个3*3的矩阵,矩阵中的数字为1表示该位置上有一个X,为0表示为O
  • 程序判断这个矩阵中是否有获胜的一方,输出表示获胜一方的字符X或O,或输出无人获胜
//1.读入矩阵 
    const int size = 3;
	int board[size][size];
	int i, j;
    int numOfX;
    int numOfO;
    int result = -1;

    for(i=0;i<size;i++) {
        for(j=0;j<size;j++) {
            scanf("%d", &board[i][j]);
        }
    }

//2.判断是否赢了(检查行)
    for(i=0;i<size&&result==-1;i++) {
        numOfO=numOfX=0;
        for(j=0;j<size;j++) {
    	    if(board[i][j]==1) {
                numOfX++;
            } else {
                numOfO++;
                printf("执行了%d, numOfO=%d\n", i, numOfO);
            }
	    }
		
		if(numOfO==size) {
            result = 0;
            printf("执行了?\n");
        } else if(numOfX==size) {
            result = 1;
        }
    }


//3.检查列
if(result == -1) {
    for(j=0;j<size&&result==-1;j++) {
        numOfX=numOfO=0;
        for(i=0;i<size;i++) {
    	    if(board[i][j]==1) {
                numOfX++;
            } else {
                numOfO++;
            }
	    }
	    
	    if(numOfO==size) {
            result = 0;
        } else if(numOfX==size) {
            result = 1;
        }
    }
    
    
}


    printf("%d", result);
    

6.5 主函数

6.5.1 参数

C/C++语言中的 main 函数,经常带有参数 argc,argv,如下:

int main(int argc, char** argv)
//或者
int main(int argc, char* argv[])

在上面代码中,argc 表示命令行输入参数的个数(以空白符分隔),argv 中
存储了所有的命令行参数。假如你的程序是 hello.exe,如果在命令行运行该程序
(如图 1.14。首先应该在命令行下用 cd 命令进入到 hello.exe 文件所在目录),
运行命令为:

hello.exe Shiqi Yu
  • 那么,argc 的值是 3,argv[0]是"hello.exe",argv[1]是"Shiqi",argv[2]是"Yu"。
71

下面的程序演示 argc 和 argv 的使用:

#include <stdio.h>
int main(int argc, char ** argv) {
    int i;
    for (i=0; i < argc; i++)
        printf("Argument %d is %s.\n", i, argv[i]);
    return 0;
}

假如上述代码编译为 hello.exe,那么运行

hello.exe a b c d e

将得到

Argument 0 is hello.exe.
Argument 1 is a.
Argument 2 is b.
Argument 3 is c.
Argument 4 is d.
Argument 5 is e.

运行

hello.exe lena.jpg

将得到

Argument 0 is hello.exe.
Argument 1 is lena.jpg.
赞赏