C语言总结项目和入门——函数


  老样子,废话不说。

四、C语言入门——函数

  首先,我们为什么需要函数。
  在数学上,函数是一种映射关系,表示一个数集向另一个数集映射的对应法则。简单来说,就是给一个数,得到另一个数。Sin(x)是一个函数,因为给一个x,可以得到一个值y。编程中的函数和上面讲的有异曲同工。
  函数实现的是封装和模块化设计。
  这是什么意思?
在这里插入图片描述
  这是sin(x)的泰勒展开式,理论上来说,对于无穷多项上面等式是恒等的。我们反过来理解,即求这些幂级数的和,当我们要求比如x=1.5时上面幂级数的和的时候,我们需要算无穷多项。而且每次改变x的值,我们都要算这么多。这样的运算必然是繁琐而复杂的,所以我们使用了sin这个函数,来代替这些幂级数的和,用简短的东西代替复杂的东西,这就是封装的一种体现。当我们希望得到x=1.5时候上面的和,我们就用sin(1.5)来表示,这个函数会给我们一个值,我们叫返回值,这个值就是右端幂级数在x=1.5处的和。我们就把一个复杂的计算变成一个简单的函数调用。当然,我们很多时候或许根本不知道计算机是怎么算sin(1.5)的,这也是由于函数的封装性导致它内部的实现是隐藏的(封装嘛,都封起来了)。
  什么是模块化设计。
  大家拼乐高,零件的种类是不是很多,而最终千奇百态的作品,它们用到的零件可能是相同的,因为这些零件很多是通用的。对编程来说,函数就是零件,我们通常将函数封装成能实现一种特定的功能的形态,如实现大小写转换strlwr(a),strupr(a),实现数组的排序,实现文字打印(printf),我们自己的程序就是构建在这样的函数上的,自底向上的设计。
  知道函数是干什么的,我们来开始吧。

  • 函数的定义:
int A(参数列表)
{
	函数体;
	return返回语句;
}

  Int 表示这个函数返回值的类型,前面我们说了,函数就是给一个值,得到另一个值(当然并不是所有函数都是这样。)得到的这个值就是函数的返回值,这个值也是变量,是有类型的,这个类型由函数定义时说明。当然这里是举例,表示返回整型,返回double当然也是可以的。
  A - 函数名,这个函数叫什么,我们在调用函数的时候会用到。
  参数列表:这个函数需要的参数,就像sin需要x才能计算一样,当然有的函数需要两个以上的参数,所以这里是一个列表,有的函数不用参数就能自己跑,就不写。
  注:参数需要是完整的,包括参数的类型和名称,和我们声明一个变量时候要求的一样。
  如(int a,double b)这个参数列表,表明这个函数需要一个整型的参数,和一个双精度浮点型的参数,这里的a,b是函数里用的,不是说要传变量a和b进来才行,传进来的参数对于函数来说就是已知的了,和变量类似,对函数来说可以直接使用。
  return返回语句:return K;其中K是一个数据类型,这句话是这个函数运行的结果。函数总是从return语句退出,void类型可以没有return,则执行完函数最后一行语句后退出。
  对于一些特殊类型的函数,没有运行结果,我们用void的类型来定义,如void A();
是一个没有返回值,没有参数的函数A的声明。注意,如果函数不是void的,即是有返回值的,那么return语句是一定要有的。反之如果A是void,也可以有return语句,只不过这里return没有参数,只是起到退出函数的作用,如return;
举个例子:

int max(int a,int b)
{
	return a>b?a:b;
}

  这是一个找最大的函数,采用三目运算符的结果做返回值。这里a,b我们都是当已知的变量来使用的,并没有再定义什么的,就是因为它们是函数的参数,在函数调用的时候会有确定的值传进来。

  • 函数的调用

  很简单:函数名(参数);
  如上面的max(5,10);就实现了对max函数的调用,并传入了参数5和10,注意:传入的应该和函数定义时候的类型一致,函数要int类型,传参的时候就只能传int类型的数,否则会报错。
  不过max(5,10)这样并没有什么效果,因为函数的返回值如果没有存下来的话,就消失了,相当于白干,所以我们需要一个变量来接受函数的返回值,如c = max(5,10),其中c是变量。这样我们就把5,10中的大着赋给了c,c现在等于10。

  • 形参与实参

  函数里的接受的参数的值,和我们传过去的值有什么关系。
  如max(x1,x2),这里x1,x2是之前定义和使用过的变量,现在我们需要知道它们的大者,于是我们调用max函数,max接收到我们传给它的两个参数x1,x2,通过比较返回他们的大者,不过我们现在还没有接受这个值。
  我们知道在max函数中,我们是对a,b这两个参数进行操作的,当我们传入x1,x2的时候,max就按位置对应将值赋给了a,b,即a=x1,b=x2。注意,这里是赋值给,这说明a,b和x1,x2没有直接的关系,它们只是x1,x2的副本,同样的,在函数中对a,b的值的变化也不会反映到x1,x2上(实际上,此时x1,x2不存在(作用域不同))。这里我们把a,b叫做形式参数(形参),把传入的值x1,x2叫做实际参数(实参),实参和形参是没有直接挂钩的关系的,它们只是值一样,除此之外毫不相干。

  我们总结一下函数的要点:

函数的定义:
函数类型 函数名(参数)
{
	函数体;
	返回值;
}

  函数类型是指这个函数的返回值的类型,即这个函数的运算结果,如果函数没有返回值,用void类型定义,可以没有返回语句。
  函数名是这个函数的名称,我们调用函数时就是通过这个函数名调用的。
  参数列表:即形參列表,函数实现功能需要提供的参数。这里的形參需要是完整的,即变量类型和变量名都要有,和变量定义时需要的是一样的,在调用函数时将实參传给形參,函数是对形參的直接运算,与实參无关。
  返回值是这个函数的运算结果,通过return语句返回,return A,其中A为函数中的变量(在函数中定义的或者函数的形參)。对于不需要返回值的函数,即void类型的函数可以没有return语句。注意:return语句和break的作用有类似之处,函数一旦执行到return语句就直接跳出函数,同时返回return的值,而不管后面还有没有。
  函数的调用:函数名(参数);对于有返回值的函数,需要一个相对应的变量来接受,即函数的调用可以当右值(具体是函数的返回值当了右值)。

  好了,我们已经把函数的定义和调用都介绍了,接下来如何正确的在程序中使用呢?
  首先函数的定义要写在哪里?
  我们观察前面的提及的框架
int main()
{
  ……
  return 0;
}
  我们不难发现这是一个函数,我们叫main函数,int类型,返回0,main函数其实是有参数的,这里我们先不提及。C语言关于函数有个要求:函数不能嵌套定义,即函数里不能再定义函数,所以我们的函数定义当然不能写在main函数里,要写在main外面。
  按道理来说,只要写外面,写在main上面还是写下面都是可以的,但这里涉及一个程序执行的问题。
  我们之前说程序是从main函数里的第一条开始执行的,实际上编译器会从头开始看一下我们提供了什么,或者要求了什么,比如我们提供了头文件(说明书),编译器从上往下看的时候,就知道我们提供了这个说明书,他碰到函数的时候就可以来这个说明书里找。这些东西我称之为预备操作,真正有执行力还是要看main函数。这里问题来了,我们把函数的定义写在后面,然后在主函数(main函数)里调用,由于编译器是顺序看的,他必然是先看到main函数并进去执行,然后才能看到函数的定义,然而他看到main函数里的函数调用的时候就懵了,因为他前面没见过,找也找不到,其实在后面都有,但编译器此时就认为这个函数没有定义,他不认得,直接报错,根本就不看后面(这就是不好好读上下文的结果。)当然对于这个问题,我们把函数定义写在main函数之前就OK,但我就是想写后面,怎么办?更多时候我们希望把函数的定义写在后面,这样每次进来都能直接看到主函数。
  解决方法就是在main函数前面对这个函数进行声明,告诉编译器有这个函数,以及这个函数的一些信息供编译器查看。具体怎么个声明法?很简单,函数的定义第一行不是这样的吗:函数类型 函数名(参数),把这行复制,粘到main函数前面去,由于C语言语句的要求,要以分号结束,所以我们再加个分号,即
  函数类型 函数名(参数);
  这就是函数的声明,它包括了函数的名字,参数,返回值类型,供编译器查看。
  更简单的,声明的时候可参数列表可以不用写参数名称,只写参数类型,如(int,int,float)这个参数列表告诉编译器这个函数需要整形、整形、浮点型的参数,而对参数名叫什么,编译器不关心,是靠函数实现来规定的。
  现在我们终于可以来说说头文件(说明书)的事了,头文件就是一堆函数的声明,通常以.h文件的形式出现,头文件一般是一些常用的函数的声明集合,我们可以通过包含头文件的操作,来使用里面声明的函数,当然我们也可以编写自己的头文件,包含我们自己的函数,这个点我们以后再说。
  函数虽然不能嵌套定义,但可以嵌套调用,即在A函数中调用B函数,实际上,我们不就是在main函数中调用自己的函数的吗。需要注意的是,要保证编译器在检查翻译A函数的时候看见B函数,要认得——即要有B的声明或定义在前面。

  • 函数的递归调用

  小甲鱼有话说:普通人理解迭代,神理解递归。递归编起来很让人头疼,我自己也不怎么会,不过递归可以以简单的程序实现复杂的操作,当然,它的花销也是很大的,对内存的占用也很严重。
  递归调用,就是自己调用自己,知道达到某个想要的目的(找到某个东西,或者算出某个值),然后一层一层的返回,最后返回main函数中。

  如我们求n的阶乘
  -普通函数:迭代

int JieCheng(int n)
{
	int a;
	int result = 1;
	for(a = n;a>0;a--)
	{
		result *= a;
	}
	return result;
}

  我们知道n的阶乘就是从n乘到1,我们用for循环来实现。
  -递归函数:
  首先我们要知道阶乘的递归定义:
在这里插入图片描述

int JieCheng(int n)
{
	int f;
	if(f == 0 || f == 1)
	{
		return 1; //0和1的阶乘是1;
	}else{
		f = n*JieCheng(n-1);
	}
	return f;
}

  当f>1时,会将f-1再带入阶乘函数,一直到f = 1;然后依次返回相乘。

  这还是递归的简单的用法,更为具体的可以参考汉诺塔的递归实现。

  
  --下面我们对函数参数为数组时特殊讲述。
  数组元素作为普通变量当函数参数很正常,如fun(k[2]);表示k数组的第3个元素传给fun函数当实参,这个很普通,和平常传参没有本质区别。
  但数组名也可以做参数,如fun(k);把k传给fun,这是什么意思?
  我们首先来看一下参数为数组的函数的定义(以int类型为例):
int fun(int a[10])
{

}
  这样写很好理解,就是把a这个数组整体当参数给了fun,fun可以调用a中的任何元素。但实际上,定义中的a[10]规定这是10个元素的数组没有用,因为编译器不看,int fun(int a[10])和int fun(int a[ ])是一样的,因为大小不是编译器所关心的,因为这个特性,我们通常还需要传入数组的大小参数,防止访问越界
  上面的函数定义可以写成:
int fun(int a[ ],int n)//n为a的长度
{

}
  在函数中使用a的元素就很简单了,和普通数组一样,数组名a加下标访问,如a[2];
  注意不要超出n的范围。
  调用函数的时候,需要数组参数的地方我们传入数组名就OK,如上面定义的函数,调用fun(M,6);表示传入一个长度为6的整型数组M。
  实际上,传入数组名就是传入数组的首地址,函数定义的时候也是需要一个数组的首地址,但我们只知道首地址是不行的,还要知道这个数组在哪里结束,所以元素个数(数组长度)就必不可少了。(这里设计指针的知识,会在下章再提及的。)
  二维数组当参数:前面我们已经知道,二位数组定义的时候可以省略第一维的参数,但不能省略第二维,同样的函数定义时也是如此:int fun(int a[ ][ 4 ])表示这个函数需要一个二维数组,第二维是4

代码训练与详解:字符->数字

例1. 字符串转变数字的函数:
  当我们输入一串123456的字符时,我们希望得到123456这个数。
  首先我们知道这个函数需要一个字符串的参数,在C中我们使用字符串数组来实现,同时它返回一个数字,我们这里用double的类型。
  思路比较粗暴,对每一位的数字乘位权相加。

#include<stdio.h>
#include<math.h>

double Str2Num(char str[],int len);

int main()
{
	char a[] = "129999";
	int num = sizeof(a)/sizeof(a[0])-1;
	double res = Str2Num(a,num);
	printf("%.0lf",res);
	return 0;
}


double Str2Num(char str[],int len)
{
	int i; 
	double result=0;

	for(i = 0;i<len;i++)
	{
		result += (str[i]-0x30)*pow(10,len-i-1);
	}
	return result;
}

  传入数组的时候我们最后也附带上它的长度,这样方便操作。这个长度怎么求呢?由前面的知识,sizeof可以求出来一个变量占的空间大小,数组也是变量,sizeof(数组名)可以求出这个数组所占的总体大小,再除以数组一个元素的大小(任意一个都行,因为它们的类型是一样的),就可以得到元素个数了,但由于这是字符串,末尾会有一个字符的结束标志‘\0’,所以我们再减去一个元素。就OK了。
  对于头文件math.h里面包含了很多数学函数,像sin,cos什么的,当然还有我们使用的pow乘幂函数,这些都只有包含math.h的头文件才能用的,
  在函数中,我们按上面的操作就可以进行转换了,需要注意的是,由于str都是字符,是按ascii码存储的,如字符‘0’,在ascii中是以十进制48存储的,即对于char类型的0,其对应存储的整型十进制数字是48,对应的十六进制是30H,我们减去30H,即0x30,就能得到对应的数字了,再乘位权,再循环加,就大功告成了。
  大家会发现这个函数不能转换负数,对于小数也不能转换,在此基础上,我们来完善,所有的东西写出来都不是一开始就是完美的,先把基础的东西写好,再补充,就会趋近完美了。

例2、完善的字符转换函数
  将力求完美,编写这个函数。同时也希望能够说明现在的知识能够实现非常多的东西了,只有有想法,就去淦他!
  接上面的函数,我们先加入正负判断,因为这个比较简单

double Str2Num(char str[],int len)
{
	int i; 
	char isNegative;
	double result=0;

	if(str[0] == '-')
	{
		i = 1;
		isNegative = 1;
	}else if(str[0] == '+')
	{
		i = 1;
		isNegative = 0;
	}else{
		i = 0;
		isNegative = 0;
	}
	for(;i<len;i++)
	{
		result += (str[i]-0x30)*pow(10,len-i-1);
	}
	if(isNegative)
	{
		result = -result;
	}
	return result;
}

  这里我们用isNegative这个变量来存储是否是负值,这种情况很常见,用一个状态变量来存放一个状态,其实用bool类型是最合适的,但C语言不怎么见这种类型,用char也行,只有能区分就行了。
  判断什么的就好说了,因为符号只能出现在开头,取出来判断,if分支,给i和isNegative对不同的情况赋初值。
  最后是负的话取相反数就行了,符号判断OK。
  对于小数的转换,比较麻烦,我这里使用自己的思路,肯定不是最好的。
  首先找到小数点,小数点前面的位权都是10的正幂,小数点后面的位权都是0.1的正幂,或者说是10的负幂,这样分成两种情况,就OK了。

double Str2Num(char str[],int len)
{
	int i,j=len; 
	char isNegative;
	double result=0;

	for(i=0;i<len;i++)
	{
		if(str[i] == '.')
		{
			j = i;
			break;
		}
	}
	
	if(str[0] == '-')
	{
		i = 1;
		isNegative = 1;
	}else if(str[0] == '+')
	{
		i = 1;
		isNegative = 0;
	}else{
		i = 0;
		isNegative = 0;
	}
	for(;i<len;i++)
	{
		if(i<j)
		{
			result += (str[i]-0x30)*pow(10,j-i-1);	
		}
		if(i>j)
		{
			result += (str[i]-0x30)*pow(0.1,i-j);
		}
	
	}
	if(isNegative)
	{
		result = -result;
	}
	return result;
}

  注意,那个幂次要特别注意,多整几遍。
  现在这个程序看起来很像样了,但还是不好,我们希望它的健壮性更好一些,能判别输入是否合规,对于123a45这种字符串要拒绝。
  因为这个判断也要遍历,我们把它和找小数点的for循环放在一起:

for(i=0;i<len;i++)
	{
		if(str[i] == '.')
		{
			if(j != len)
			{
				printf("ERROR\a\n");
				return 0;
			}
			j = i;
		}
		if((str[i]<48 || str[i]>57 )&& str[i] != '.')
		{
			printf("NO\a\n");
			return 0;
		}
	}

  由于当存在小数点时,j的值会变为i的值,j不再等于原来的len,当发现有小数点时,而j又不等于len,说明前面就出现过小数点了,这个输入就有问题,如2.356.8,这个输入非法。
  if((str[i]<48 || str[i]>57 )&& str[i] != ‘.’)是对字符是否是数字的判断,在ascii中字符都处在48和57之间,由于我们中间会出现小数点,小数点不在这个范围内,但出现小数点是合法的(不合法的情况我们有前面的判断),所以要把小数点刨去。
  现在这个函数应该比较完善了。
  完整代码:(补充对开始符号输入判断的)

#include<stdio.h>
#include<math.h>

double Str2Num(char str[],int len);//声明

int main()
{
	char a[] = "1230.456789";
	int num = (sizeof(a)/sizeof(a[0])-1);
	double res = Str2Num(a,num);//调用和返回
	printf("%lf",res);
	return 0;
}


double Str2Num(char str[],int len)//定义
{
	int i,j=len; 
	char isNegative;
	double result=0;

	for(i=0;i<len;i++)
	{
		if(str[i] == '.')
		{
			if(j != len)
			{
				printf("ERROR\a\n");
				return 0;
			}
			j = i;
		}
		if ((str[i] < 48 || str[i]>57) && str[i] != '.' &&str[0] != '-' &&str[0] != '+')
		{
			printf("NO\a\n");
			return 0;
		}
	}
	
	if(str[0] == '-')
	{
		i = 1;
		isNegative = 1;
	}else if(str[0] == '+')
	{
		i = 1;
		isNegative = 0;
	}else{
		i = 0;
		isNegative = 0;
	}
	for(;i<len;i++)
	{
		if(i<j)
		{
			result += (str[i]-0x30)*pow(10,j-i-1);	
		}
		if(i>j)
		{
			result += (str[i]-0x30)*pow(0.1,i-j);
		}
	
	}
	if(isNegative)
	{
		result = -result;
	}
	return result;
}


作用域

  顺便来介绍一下作用域和生存周期的事,就是变量在哪个范围内有效的事。
  我们定义的变量是从定义时开始有效(存在),因为编译器现在才第一次见到这个变量,这很好理解,(变量的定义:int a)。
  但变量什么时候消失,就要看变量定义在哪里了。
  变量定义一般可以在3个地方

int a;

int main()
{
	int b;
	while(1)
	{
		int c;
	}
	return 0;
}

  由于main函数也是函数,所以可以得到下面的情况:
  a定义在函数外面,b定义在函数的开头,c定义在复合语句的开头(复合语句就是用一对大括号括起来的语句们)。
  其中,在函数内部定义的变量只在函数内有效,b的生存周期就是从定义开始到main函数结束,a又叫做全局变量,因为是全局有效的,即从定义开始到程序结束a都存在,c定义在复合语句中,则它的生存周期只存在于符合语句中。
  所有变量的生存周期一过,这个变量就消失了,死掉了,无法再访问。
  对于同名的变量来说,只要不在同一个生存周期内,就没事,否则会发生重定义的问题,如
{
  Int a;
  Int a;
}
很明显这两个a都是这个复合语句内的,会产生重定义的错误。
{
  Int a;//1
  {
     Int a; //2
  }
}
  这个就ok,因为两个a的生存周期不同,当使用a这个变量名的时候,指的是哪个变量呢?很好说,出了第二层括号2号a就歇菜了,所以在第二层括号外第一层括号内的a都是1号a,在第二层括号内,1,2号a同时存在,则2号a顶替1号a,成为这个括号内的a,简单来说就是短周期的顶替长周期的,所有变量都遵守这种规则。
  注意这个全局变量:由于普通函数的形参也只是在函数内存在,函数一退出就没了,函数能带出来的只有返回值,还必须赶紧接收,这使得我们想返回多个值成为问题。当然,我们可以用指针轻松解决这个问题,但更为简单的方法就是用全局变量带出来,因为全局变量全局存在,所以在函数中对全局变量的改变不会随函数的消失而消失。
  对于变量的存储类型,这里只说一个比较常见的:extern
  对于全局变量,也只能在本文件下生效,不能说我这个文件下写的全局变量,我想在另一个文件中用,或者是我在程序中间定义了一个全局变量,但我想在前面就用,对于这些情况,我们就要用extern前缀做外部变量声明,来扩大全局变量的生存周期:

extern int a;//1
……
int a;//2
  a是全局变量,正常来说a只在2以后才生效,但通过1,我们使a的生存周期从1开始,扩大了它的生存周期。
  对于扩展到其他文件中也是同样的,通过extern将变量扩展到别的文件中,如下面的例子

文件1#include<stdio.h>

int A;
int test(k);//2中的函数声明
int main()
{
	A = 5;
	printf("%d", test(4));
	return 0;
}

文件2extern  int A;

int test(int k)
{
	return k * A;
}

  可以正常运行,且A的值能传到文件2中,且2的函数也能正常使用
  下一章会就关于多文件编程补充总结一下(原本打算下一章上指针,但在写这篇文章的时候感觉函数的多文件编程还是很重要的,而老师上课讲的极少……)
  我们下章再见!


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值