7. 函数
将中间高亮部分定义为自定义函数,main中调用该函数进行处理。这个isPrime还可用在别的程序中。
程序中出现几段几乎完全相似的代码,“代码复制”是程序不良的表现。因为将来做修改、维护的时候要维护很多处。
什么是函数?
函数是一块代码,接收零个/多个参数做一件事情,并返回零个/一个值。
可想像为数学中y = f(x)
函数定义
调用函数
需要给出:函数名(参数值);
()起到了函数调用的重要作用,即使没有参数也需要();
有参数的则需要给出正确的数量和顺序:
这些值会被按照顺序依次用来初始化函数中的参数。
函数知道每一次是哪里调用它,会返回到正确的地方。
从函数中返回
return 停止函数的的执行,并送回一个值。
return;
return 返回值;
一个函数可出现多个return语句。(单一出口更好)
int a,b,c;
a = 5;
b = 6;
c = max(10,12); //可以赋值给变量
c = max(a,b);
c = max(c,23);
c = max( max(c,a),5); //可以再传给函数
printf("%d\n", max(a,b));
max(12,13); //可以丢掉
没有返回值的函数用void,不能使用带值的return,可以没有return。调用的时候也不可以做返回值的赋值。
函数先后关系
C的编译器自上而下分析你的代码,函数得在其被调用前进行定义或声明。或是没定义声明时,编译器猜测,该函数的类型,如果后面发现后面的函数类型和它猜测的不符,就输出错误error。
可以先声明,后main,再定义函数。编译器会检查前后的函数类型、定义声明是否一致。
void sum(int begin,int end);//声明
int main()
{
//用到sum(a,b)函数
}
void sum(int begin,int end)//定义
{
int i,sum=0;
for(i=begin;i<=end;i++){
sum+=i;
}
printf("%d到%d的和是%d\n",begin,end,sum);
}
函数原型
—— 函数头,以分号;结尾,就构成了函数的原型;
—— 函数原型的目的时告诉编译器这个函数长什么样子:名称、参数(数量及类型)、返回类型。
—— 在函数里定义的参数类型与输入的变量的类型不一样,会发生自动类型转换。
—— 旧标准习惯将函数原型写在调用他的函数里面,现在一般卸载调用它的函数前
—— 原型里可以不写参数的名字,但是一般仍然写上,便于人阅读。
Void sum(int ,int)
参数传递
传递给函数的值可以是表达式的结果如(max(a,b),c)
包括字面量、变量、函数返回值(函数调用里有函数)、计算结果
当调用函数的值与参数类型不匹配:编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的。这是C语言传统上最大的漏洞。后续语言如c++/java在这方面会很严格。
void swap(int a,int b)//形参————>参数
int main()
{
int a=5;
b=6;
swap(a,b);//实参————>值
}
void swap(int a,int b)//形参————>参数
{
int t=a;
a=b;
b=t;
}
上面的代码期望交换ab的值,实际运行,调用swap处,它将main中的ab的值传给了swap的ab值,swap将swap中的ab进行了交换值,而main中的ab没有变化。
C语言在调用函数时,永远只能传递值给函数,函数中对变量做的处理和main中的变量无关。
每个函数又自己的变量空间,参数也位于这个独立空间中,和其他函数没有关系
过去对于函数参数表中的参数,叫做“形式参数”,调用函数时给的值,叫做“实际参数”。
现为了避免误会,认为是参数 和值的关系。 函数声明、定义处为参数,调用处为值。
本地变量
函数每次运行都会产生一个独立的变量空间,其中的变量是函数这一次运行所独有的,称为本地变量。
定义在函数内部的变量就是本地变量。写在函数参数表里的参数也是本地变量。
变量的生存期和作用域
生存期:这个变量出现和消亡的时间
作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
对于本地变量,这两个答案都是:大括号内(块)
本地变量的规则
—— 本地变量是定义在块内的:可以是定义在函数的块内,也可以定义在语句的块内,甚至可随便拉一对大括号来定义变量
—— 程序运行进入这个块之前,其中的变量不存在,离开这个块,其中变量消失。
—— 块外面定义的变量在里面仍然有效
—— 块里面定义了和外面同名的变量则掩盖了外面的这里面就是新定义的。从块里面出来之后又回到块外的值。(C语言可)
—— 不能在同一个块内定义同名的变量。
—— 本地变量不会被默认初始化。
—— 参数在进入函数时候被初始化了。
其他细节
在没有参数时,定义函数func,是void func(void) 还是 void func() ?
—— 在传统C中,void func() 表示函数的参数未知,不代表没参数。
—— 没有参数写成void func(void)
逗号运算符:调用函数时的逗号和逗号运算符的区分:调用函数时圆括号里的逗号时标点符号,不是运算符。f(a,b) f((a,b))前者传两个参数,后者传一个参数,且先进行逗号运算。
C语言不允许函数嵌套定义。
int i, j, sum(int a, int b) :定义int型变量i和j,声明sum函数要两个int参数输入,返回一个int型值。(不建议这样写)
return(i),()没啥意义。会使人误解return为函数。不建议这样写。
int main() 也是个函数,return返回值,不同系统使用的作用不一样,一般return 0 表示程序正常运行。
8. 数组
初试数组
计算用户输入的数字的平均数?
原来做法,不需要记录每一个数。
题目变为:如何写一个程序计算用户输入的数字的平均数,并输出所有大于平均数的数?
如何记录很多数?int num1,num2……?
#include<stdio.h>
int main()
{
int x;
double sum = 0;
int cnt = 0;
int number[100];//定义数组,数组可以放100个int
scanf("%d",&x);
while(x!=-1){
number[cnt]=x;//对数组中的元素赋值
//调试使用部分
{
int i;
printf("%d\t",cnt);
for ( i= 0;i<=cnt;i++){
printf("%d\t",number[i]);
}
printf("\n");
}
//
sum += x;
cnt++;
scanf("%d",&x);
}
if(cnt>0){
int i;
double average=sum/cnt;
for(i=0;i<cnt;i++){
if(number[i]>average){ //使用数组中的元素
printf("%d ",number[i]); //for循环遍历数组
}
}
}
return 0;
}
这个程序有安全隐患,它没有考虑使用的数组下标是否会超过100。
定义数组
<类型> 变量名称[元素数量];如int grades[100]; double weigh[20];
C99前元素数量必须是整数,C99开始,可以用变量定义数组大小。
数组是个容器,特点是:其中所有的元素有相同的数据类型;一旦创建,不能改变大小;数组中的元素在内存中是连续依次排列的。
int a[10] ——一个int的数组。
10个单元:a[0],a[1],...,a[9]
每个单元 就是一个int类型的变量;
可以出现在赋值左边或者右边:a[2] = a[1]+6; 在赋值左边叫做左值。
数组的单元
数组的每个单元就是数组类型的一个变量:
使用数组时,放在[]中的数字叫做下标或索引,下标从0开始计数:grades[0]、grades[99]
有效的下标范围
—— 编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写
—— 一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃
: segmentation fault
—— 但是也可能运气好,没造成严重的后果
—— 所以这是程序员的责任来保证程序只使用有效的下标值:[0,数组的大小-1]
对于前面程序的安全隐患,防止读入数字超过100个:
方法一:cnt=100之后停止读数;
方法二:利用c99数组大小可以是动态的的特性,定义number[cnt],让用户先输入cnt。
长度为0的数组:int a[0];可以存在,编译通过,没啥用,0都是越界的。
写一个程序,输入数量不确定的[0,9]范围内的整数,统计每一种数字出现的次数,输入-1表示结束。
#include<stdio.h>
// 输入数量不确定的[0,9]范围内的整数,统计每一种数字出现的次数,输入-1表示结束。
int main(void)
{
const int number = 10;//频繁出现的数,有相同意义可以一处定义,此处为数组的大小C99
int x;
int count [number]; // 定义数组
int i;
// 计数器数组初始化
for ( i=0; i<number; i++ ) {
count[i] = 0;
}
scanf ( "%d",&x);
while ( x!= -1 ) {
if ( x>=0 && x<=9 ) {
count [x]++; // 计数器数组参与运算
}
scanf( "%d",&x);
}
// 遍历数组做输出
for ( i=0; i<number; i++ ) {
printf ( "%d : %d\n", i, count [i]);
}
return 0;
}
数组运算
例子1:搜索:在一组给定数据中,怎样找出某个数据是否存在?
数组的集成初始化:
int a[]={2,4,6,7,1,3,5,9,11,13,23,14,32}
直接用大括号给出数组所有元素的初始值;不需要给出数组的大小,编译器替你数。
集成初始化,依次初始化数组的每一个单元,
若写成int a[13] = {2};则会初始化数组a,为13个单元,第一个单元为2,后面都是0。
如此可以将前面程序的计数器数组初始化部分去掉,在定义数组时:int count [number] = {0};
集成初始化时的定位(仅C99)
int a[10] = {[0] = 2,[2] = 3,6,};
如此,a[0]为2,a[2]为3,接下去的a[3]为6,其他为0。
用[n]在初始化数据中给出定位,没有定位的数据姐在前面的位置后面,其他位置的值补零。也可以不给出数组大小,让编译器算,特别适合初始数据稀疏的数组。
数组的大小
sizeof给出整个数组所占据的内容的大小,单位是字节
sizeof(a)/sizeof(a[0])
sizeof(a[0])给出数组中单个元素的大小,于是相除就得到了数组的单元个数
这样的代码,好处是:一旦修改数组中初始的数据,不需要修改遍历的代码
数组的赋值
int a[] = {0,1,2,3,4,5,6,7,8,9};int b[] = a;
数组本身不能被赋值
要把一个数组的所有元素交给另一个数组,只能采用遍历。
for(int i=0 ; i<length ; i++){
b[i] = a[i];
}
通常使用for循环,让循环变量i从0到<数组的长度>,这样循环体内最大的i正好是数组的最大的有效下标。
常见错误:循环结束条件是<=数组长度,或;
离开循环后,继续用的i的值来做数组元素的下标。
#include <stdio.h>
/**
找出key在数组a中的位置
@param key 要寻找的数字
@param a 要寻找的数组
@param length 数组a的长度
return如果找到,返回其在a中的位置; 如果找不到则返回-1
*/
int search(int key, int a[], int length);
int main(void)
{
int a[] = {2,4,6,7,1,3,5,9,11,13,23,14,32};
int x;
int loc;
printf("请输入一个数字:");
scanf( "%d",&x);
loc = search(x, a, sizeof(a) /sizeof(a[0]) );
if ( loc != -1 ){
printf ( "%d在第%d个位置上\n", x, loc);
}else {
printf("%d不存在\n",x);
}
return 0;
}
int search(int key, int a[], int length)
{
int ret = -1;
int i;
for(i = 0 ; i<length ; i++){
if(a[i] == key){
ret = i;
break;
}
}
return ret;
}
数组作为函数参数时,往往必须再用另一个参数来传入数组的大小。
因为:数组作为函数的参数时,不能在[]中给出数组大小;不能用sizeof来计算数组的元素个数(指针给出原因)。
例子2:之前找素数的例子,用数组来进行。
当我们想了解一个函数时,在编译器中输入man 函数名称(man sqrt)就能得到其相关信息。
我们不需要拿比x小的数字来测试x是不是素数,我们只需要拿比x小的素数就够了。(判断是否能被一直的且小于x的素数整除)
#include <stdio.h>
/**
求前100个素数
*/
int isPrime(int x,int knowsPrimes[],int numberofKnownPrimes);
int main()
{
const int number=10; //前10个
int prime[number];
// 数组初始化(集成初始化报错)
prime[0] = 2;
for ( int m=1; m<number; m++ ) {
prime[m] = 0;
}
int count = 1;
int i=3;
// 调试表头
{
int i;
printf("\t\t");
for(i=0;i<number;i++){
printf("%d\t",i);
}
printf("\n");
}
//
while(count < number){
if(isPrime(i, prime, count)){
prime[count++] = i; //写进去再移位
}
// 调试
{
printf("i = %d \tcnt=%d\t",i,count);
{
int i;
for( i=0; i<number; i++){
printf("%d\t",prime[i]);
}
}
printf("\n");
}
//
i++;
}
//最终结果,prime数组内为前number个素数
printf("*************结果******************\n");
for(i=0; i<number; i++){
printf("%d", prime[i]);
if( (i+1)%5 ) printf("\t");
else printf("\n");
}
return 0;
}
int isPrime(int x,int knowsPrimes[],int numberOfKnownPrimes)
{
int ret=1;
int i;
for(i=0; i<numberOfKnownPrimes; i++){
if(x % knowsPrimes[i] == 0){
ret = 0;
break;
}
}
return ret;
}
一边构造素数表,一边利用表来证明素数。
构造素数表
想构造n以内的素数表:
1. 令x为2;
2. 将2x、3x、4x直至ax<n的标记为非素数;
3. 令x为下一个没有被标记为非素数的数,重复2;直到所有的数已被尝试完毕。
——————————————
伪代码:
欲构造n以内(不含)的素数表
1 开辟prime[n],初始化其所有元素为1,prime[x]为1表示x是素数
2 令x=2
3 如果x是素数,则对于(i=2; x*i<n; i++) 令prime[i*x]=0
4 令x++,如果x<n,重复3,否则结束
#include <stdio.h>
//构造素数表
int main(){
const int maxNumber = 25;
int isPrime [maxNumber];
int i;
int x;
for ( i=0; i<maxNumber; i++ ) {
isPrime[i] = 1;
}
for ( x=2; x<maxNumber; x++ ) {
if ( isPrime [x] ) {
for ( i=2; i*x<maxNumber; i++ ) {
isPrime [i*x] = 0;
}
}
}
for ( i=2; i<maxNumber; i++ ) {
if ( isPrime[i] ) {
printf("%d\t", i);
}
}
printf( "\n");
return 0;
}
算法不一定和人的思考方式相同。
二维数组
int a[3][5]:通常理解为a是一个3行5列的矩阵。
内存中的放置:与线性代数理解相同。
二维数组的遍历:需要两重循环。
a[i][j]是一个int;表示第i行第j列上的单元。
a[i,j]表示意思=a[j],其中,为运算符
for( i=0; i<3; i++ ) {
for(j=0; j<5;j++ ) {
a[i][j] = i*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,或输出无人获胜。
#include <stdio.h>
/*
tic-tac-toe游戏(井字棋)
读入一个3×3的矩阵,矩阵中的数字为1表示该位置上有一个X,为0表示为O
·程序判断这个矩阵中是否有获胜的一方,输出表示获胜一方的字符X或O,或输出无人获胜。
*/
int main(){
const int size = 3;
int board[size][size];
int i,j;
int num0fX;//X是一方
int num0fO;//O是一方
int result=-1;//-1:平局,1:X方赢,0:O方赢
//读入矩阵
for(i=0; i<size; i++){
for(j=0; j<size; j++){
scanf("%d",&board[i][j]);
}
}
//检查行
for(i=0; i<size&&result==-1; i++){
num0fO=num0fX=0;
for(j=0; j<size; j++){
if(board[i][j]==1)num0fX++;
else num0fO++;
}
if(num0fO==size) result=0;//O方赢
else if(num0fX==size) result=1;//X方赢
}
// 检查列————遍历:先列后行
if(result == -1){
for(j=0;j<size&&result==-1;j++){
num0fO=num0fX=0;
for(i=0;i<=0;i++){
if(board[i][j]==1)num0fX++;
else num0fO++;
}
}
if(num0fO==size) result=0;//O方赢
else if(num0fX==size) result=1;//X方赢
}
//检查正对角线————board[1][1]、board[2][2]、board[3][3],一重循环即可
if(result ==-1){
num0fO=num0fX=0;
for(i=0; i<size&&result==-1; i++){
if(board[i][i]==1) num0fX++;
else num0fO++;
}
if(num0fO==size) result=0;//O方赢
else if(num0fX==size) result=1;//X方赢
}
//检查反对角线————board[0][2]、board[1][1]、board[2][0]————board[i][2-i],一重循环即可
if(result == -1){
num0fO=num0fX=0;
for(i=0; i<size&&result==-1; i++){
if(board[i][2-i]==1) num0fX++;
else num0fO++;
}
if(num0fO==size) result=0;//O方赢
else if(num0fX==size) result=1;//X方赢
}
if(result == 0) printf("O方赢了");
else if(result == 1) printf("X方赢了");
else if(result == -1) printf("平局");
return 0;
}