读书笔记-C语言-函数
1、c语言的函数有以下特点:
(1)才源程序由函数组成,一个主函数main()+若干其他函数
C程序中的函数类似文章中的段落,但与段落不同的是,程序的执行是由main函数这一段起始,在main函数这一段中结束,而不是由第一个函数起始,在最后一个函数中结束。main外的其他函数,是由main调用执行的。
(2)函数之间是调用的关系,调用某函数的函数称主调函数,被调用的函数称为被调函数。main函数(我们)是主体,有权调用其他函数,其他函数之间也可以相互调用以协调工作,例如餐厅也调用了打车。但其他函数不能调用main函数。
(3)除main函数外,其他函数都不能独立运行;其他函数只有在被调用时才运行,不调用不运行。
(4)同一函数,可被反复多次调用。
(5)函数的返回,谁调用的返回谁
(6)函数分为两种,系统提供的库函数和自定义的函数
系统提供的库函数如printf()、getchar()、sqrt()等,这些函数是由系统提供的,随时听命由我们派遣。我们不必关心其内部细节,要调用他们,只要包含相应的头文件(xxx.h)。
(7)函数的参数和返回值。有的函数有1~多个参数,有的没有参数。有的函数有1个返回值,有的没有返回值。
函数的返回值有些类似数学中函数的函数值,如sin(30)得到0.5,0.5就是函数sin(30)的返回值。60度的正弦值为0.866,0.866是sin(60)的返回值。这说明参数不同,函数的返回值也可能不同。而有些函数有没有返回值,如上例的"打车"函数,它只用于实现一些功能,并不需要返回什么东西。注意函数如果有返回值,返回值最多只能有1个。
2、函数的定义和调用
自己的函数必须先定义,然后才能使用。
定义自己函数的一般形式如下所示:
函数返回值类型 函数名(参数类型1 参数名1,参数类型2 参数名2,...)
{
语句1;
语句2;
....
}
注意:各个函数的定义是互相平行和独立的,以上内容必须与main函数或其他自定义函数并列。函数的定义不能嵌套,在一个函数体内部不允许在定义另一个函数。
在定义函数时,对于函数的头部应该注意以下问题:
(1)返回值类型,应写在函数名之前,规定函数所返回的数据的类型,如int、float、double、char等。如果不写返回值类型,表示规定返回值类型为int,不是没有返回值。要规定函数没有返回值,这一部分应写为关键字void,而不是省略。
(2)函数名是符合标识符命名规则的名称,但最好"见名知意"。
(3)函数名后的()必不可少,即使函数没有参数。
(4)在()内规定函数的参数,多个参数时参数之间以逗号分隔。参数的定义与变量的定义类似,也是类型+参数名的形式(实际上参数确实可以被看做变量),但与变量定义不同的是,每个参数都必须在参数名前写有类型,且不写分号(;)下面的定义是错误的int maxnum(int a,b){}在b之前也必须写出b的类型,不能省略。
对于没有参数的函数,在函数的()中也可写上void强调。举例:
void print(void){
}
两个void含义是不同的,第一个void规定函数没有返回值,第二个()中的void强调了函数没有参数。无参数函数的()内写不写void都是可以的,但()万万不可省略。
函数的调用:
函数的调用方法:调用函数有两种方式:(1)将函数调用作为独立的语句:函数名(参数,参数,...)+分号(;),(2)在表达式中调用函数:表达式....函数名(参数,参数,...)。
对于有返回值的函数以上两种调用方式均可,只是若采用第(1)中方式函数所返回的值就没什么用了,返回值返回后会被直接丢掉(但函数中的语句还是被正常执行)。而对与无返回值的函数只能采用上面第(1)中调用方式,因为函数无返回值,表达式无法求值。如z=2*print()+1;
在表达式中调用函数:例如:z=sqrt(4)*3;表示计算4的平方根,在*3后将6存入变量z中,其中计算4的平方根是通过调用有返回值的系统库函数sqrt完成的。对于有返回值的自定义函数也可以用类似的调用方式。这种调用方式就是讲函数作为表达式的一部分,将来用函数的返回值替换这一部分参与表达式的计算。
注意:形参一定是变量,实参既可以是常量,也可以是变量或表达式或函数。如int x=25,y=73;maxnum(x*3,y);
在调用函数时,所给出的实参必须和形参在数量上、顺序上、类型上严格一致,或类型上可以进行转换。
参数传递时,实参和形参是按照位置的先后顺序一一对应的,对应的实参和形参的类型应一致。如不一致,则以形参类型(函数定义头部)为准,自动将实参转换为改类型。
函数调用的过程:
(1)每个函数都有自己独立的内存空间,函数中的变量包括形参都位于各自的内存空间中,互不干涉。被调函数的内存空间只能在函数被调用后运行时才存在,不调用不运行其空间不存在。
(2)在调用函数时,主调函数暂停运行,程序转去执行被调函数。在转去执行被调函数之前的准备工作为,将形参当做变量,在被调函数自己的内存空间中开辟这些形参变量的空间,然后将实参的值单项的传递给对应的形参变量,即用实参值给形参变量赋值。
(3)运行被调函数,逐条执行被调函数中的语句,这与在main函数中的执行方式一样。被调函数运行结束后,被调函数的空间包括其中所有的变量,也包括形参变量全部消失,它们的空间即可被系统回收不在存在(但对有返回值的函数,返回值不会消失)。
(4)返回到主调函数中刚才暂停的位置继续运行主调函数的后续程序。
(5)变量都要位于他所属函数的空间中,而不能位于其他函数的空间中。形参是被调函数中的变量,应位于被调函数的空间中。由于不同的函数有各自不同的空间,因此在不同的函数中可以使用同名变量,若实参是主调函数中的变量,形参和实参也可以同名(形参是被调函数中的”变量“,二者分属不同的空间)。
函数的返回值:
函数被调用后,可以没有返回值,也可以有1个返回值。
无论有无返回值,函数均可被调用,其调用过程也一致。函数结束后也都能返回到主调函数的调用处继续运行主调函数的后续程序。只不过对有返回值的函数,在表达式中调用时,一般还要用返回值替换其中函数的调用部分,然后在计算表达式,例如z=sqrt(4)*3;sqrt(4)的返回值为2,用2替换sqrt(4)部分得z=2*3;在执行此语句变量z被赋值为6.
sqrt是系统库函数,它的返回值是系统自动算出的。而对于自定义函数,要返回值,就要由我们自己通过return语句让函数返回一个值。return是关键字,return语句的形式为:return 表达式;或return (表达式);
表达式的值将被算出,并将表达式的值作为函数的返回值返回。其执行过程是:计算表达式的值,然后开辟一个临时空间,将表达式的值存入此临时空间,再将此临时空间中的值作为函数返回值返回给主调函数。
关于return语句,还应注意以下几点:
(1)同一函数内允许出现多个return语句,但在函数每次被调用时只能有其中一个return语句被执行,函数只能返回一个值。
(2)一旦执行return,函数立即结束,如果本函数内在return后还有其他语句则这些语句也不会被 执行了。也就是说return兼有返回值和强行跳出函数的双重作用,它会使程序即刻返回到主调函数的调用处继续运行主调函数后面的程序。
(3)return语句还有一个用法,是:return;这种用法没有表达式,不能返回值,仅起到强行跳出函数的这个作用。这种用法只能用于没有返回值的函数。
(4)没有返回值的函数既可以写return;语句,也可以不写return;语句。后者在函数语句全部运行完毕之后,函数会自动结束,自动返回到主调函数。因此函数有、无返回值和有无return;语句并没有什么必然关系。
实际上,即使有返回值的函数,也可以没有return语句,这样在函数语句全部运行完毕之后,函数将返回一个随机值。但不建议这样做,对有返回值的函数应该写有return 表达式;的语句让它返回一个值。如希望不写return,不如将函数定义为无返回值的(void)。
(5)如果函数有返回值,则实际返回值的类型应该和函数定义头部的函数名前的类型一致,如不一致,则以函数定义头部的函数名钱的类型为准,自动将return语句后的表达式值的类型转换为定义头部的类型然后在返回。
main函数实际也是一个自定义函数,我们通常对main函数的写法是:
main()
{
}
函数名main前的返回值类型省略,表示main函数式有返回值的且返回值是一个int型的数据。所以也可以将main函数写为:
int main()
{
}
以上二者是等效的。
main的返回值用于在程序结束后返回给操作系统(如Windows)表示程序运行情况。我们可以在main函数中用return 0;或return 1;等向操作系统返回一个值(程序若正常结束一般返回0)。一般我们不必关心甚至也可以不向操作系统返回值,或者干脆规定main函数没有返回值,将main函数写为下面的形式也是正确;
void main()
{
}
显然一旦在main函数中执行了return语句,将跳出main函数,那么整个程序也就运行结束了。
函数的声明:
程序中函数之间是调用与被调用的关系,因此函数彼此出现的先后顺序无关紧要。。。。
解决这个问题有两种方法:一是像例子中先写函数fun的定义,然后在写main函数,在main函数中调用;还有一种方法是使用函数的声明。
函数的声明写法非常简单,就是函数定义的头部“照抄”,后面再加分号(;)就可以了。比如:void fun (int p);
它类似与一条语句,但不会产生任何操作,只是“告诉”编译系统,存在这样一个函数(可能在后面定义),让编译系统提前认识这个函数。
函数的声明,也称函数的原型声明,也称函数的原型。原型就是样式,它表示了函数的返回值类型、函数名、形参个数、顺序及每个形参的类型。声明函数就是告诉编译系统这种函数的样式,因为编译系统需要这些信息来认识这个函数。
注意在函数的原型中,有一样东西是可有可无的,就是形参的名字。也就是说,在函数的声明中,形参的名字可以省略,但不能省略形参类型,除了形参名字之外,其他任何内容均不可省略。如:void fun (int);
举例:#includevoid fun(int p);
main()
{
int a=10;
fun(a);
printf("main:%d\n",a);
}
void fun(int p)
{
int a=2;
a=a*p;
printf("fun(1):%d\n",a);
if(a>0)return;
printf("fun(2):%d\n",a);
}
函数声明的位置:
函数的声明既可以出现在函数外,也可以出现在其他的函数体内。两者的区别如下所示:
在函数外声明:使编译系统从声明之处开始到本源程序文件的末尾的所有函数中,都认识该函数。
在函数内声明:使编译系统仅在本函数内、从声明之处开始认识该函数,但在本函数之外有不认识该函数。
程序中函数的声明不是一定要有的。只有先调用函数,后出现函数定义的时候,才需要在调用之前声明函数。如果先出现函数的定义,后调用函数,就可以不必声明,当然这种情况下声明函数也不会出错。无论如何,我们的目的是在调用函数之前让编译系统认识这个函数,定义可以让他认识,声明也可以让他认识。对同一函数还可以声明多次,但定义只能出现一次。
调用系统库函数,也需要提前声明函数。但系统库函数的函数声明已被事先写到头文件(.h)中了,我们通常用#include命令在程序中包含对应的头文件,就是把对应函数的声明包含到我们的程序中。这就是为什么在调用库函数之前,一定要包括对应头文件的原因。例如:要调用库函数sqrt(),就需包含头文件math.h,因为math.h中有sqrt函数的声明。
函数的嵌套调用和递归调用:
函数的嵌套调用:定义函数不允许嵌套,但调用函数可以嵌套,即在被调函数中又调用其他函数。
函数的递归调用:在一个函数中可以调用其他函数,那么如果在一个函数中调用自己这个函数本身又会如何呢?在程序设计中,函数自己调用自己不仅是合法的,而且是一个非常重要的程序设计技巧称为递归。例如:
int fun(int x)
{
.....
fun(y);
....
}
变量的作用域及存储类别:
变量作用域:作用域是指变量在程序中能够起作用的地域范围。
变量既可以在函数内定义,也可以在函数外定义。在函数内定义的变量为局部变量(也称内部变量),在函数外定义的变量为全局变量(也称外部变量)。
局部变量:
形参在函数被调用时,也作为函数内的局部变量。
局部变量的特点:
(1)只在本函数内有效,在其他函数中都不能直接使用。
(2)局部变量若在定义时未赋初值,初值为随机数。
(3)不同函数中可使用同名变量,它们互不干扰。形参和实参也可以同名。
(4)函数执行结束后,变量空间被回收。
局部变量是在函数内定义的变量,还可以让它更局部一些,在复合语句(一对{}括起的语句)内还可以定义局部变量。这种局部变量的作用范围更小,仅在所在的复合语句范围内有效,而且其生存期也在复合语句范围内,一旦复合语句执行结束,其空间就被回收。
全局变量:在函数外定义的变量为全局变量(也称外部变量)。全局变量的重要特点:
(1)这种变量的作用范围是“全局的”
,从它定义处开始到本源程序文件末尾的所有函数都共享此变量。
(2)如定义时未赋初值,初值自动为0,而不是随机数。
如果函数内有局部变量与全局变量同名,则在该局部变量的作用范围内将使用该局部变量,同名全局变量被屏蔽不起作用。
全局变量的优点是在多个函数中都能同时起作用,在一个函数中对某变量值的改变可被带到其他函数中,所以通过全局变量可在不同函数间传递数据。
扩大全局变量的作用域:全局变量的作用域有一个限制,就是只能从变量的定义处之后才能被使用。
举例:#includeint sum;
void fun1()
{
sum+=20;
}
int a;
void fun2()
{
a=20;sum+=a;
}
main()
{
sum=0;
fun1();
a=8;
fun2();
printf("sum=%d,a=%d",sum,a);
}
上例中fun1函数式不能使用全局变量a的,因为a在fun1之后才定义。如果一定要在fun1函数中使用全局变量a,能不能办到呢?可以,这需要用关键字extern来扩大其作用范围。extern是用来声明全局变量的,在用extern声明全局变量之后,就可以使用该全局变量了(尽管他可能在以后才被定义)。声明的写法与变量定义的写法基本相同,只是在之前加extern,例如:extern int a; 举例:如下
注意变量的声明与变量的定义不同,声明不会开辟变量的存储空间,而定义就要开辟存储空间了。因此全局变量的声明可出现多次,而定义只能是一次。
#includeextern int a;
int sum;
void fun1()
{
sum+=20;
/*在fun1函数中可以使用全局变量a*/
}
int a;
void fun2()
...
只有先使用全局变量,后定义的才有必要声明。如果全局变量是先出现的定义,而后才使用的,可不必声明,当然如果声明了也不会出错。
多文件编程:
当一个c程序由多个源文件组成时,注意多个源文件组成的是一个程序,而不是多个程序。因此只能在一个源文件中有main函数,且只能有一个main函数。
file1.c如下
int a;/*全局变量*/
extern void fun();
main()
{
a=10;
printf("(1)a=%d\n",a);
fun();
printf("(2)a=%d\n",a);
}
file2.c如下:
extern int a;
void fun()
{
a=20;
printf("fun中a=%d\n",a);
}
注意:(1)在一个文件中定义了全局变量,有时希望在其他文件中也能使用该全局变量。这时要在使用该变量的其他文件中声明该变量。比如:file1.c中定义了全局变量a,在file2.c中应该先用extern声明全局变量a,才能在file2.c中使用file1.c中的a。
(2)调用其他文件中定义的函数也要声明。比如:在main函数中调用了其他文件的fun函数,因此在file1.c的第二行事先声明了fun函数 extern void fun();这时的extern用在函数声明前,它表示该函数是在其他文件中定义的。extern也可以省略,编译系统会自动 先查找本文件中有无此函数的定义,如未找到再到其他文件中查找。
(3)在函数定义的头部还可以加extern以强调该函数允许被其他文件调用,如上例file2.c中对函数fun的定义还可以写为:extern void fun()
{
a=20;
printf("fun中a=%d\n",a);
}
通常函数定以前的extern可以省略,其效果是相同的。
总结extern的用法和作用:
(1)全局变量的声明或函数的声明前加extern,表示该全局变量或函数是其他文件中定义的,或是本文件稍后定义的,这里extern用于扩大作用域范围。注意声明并不开辟变量空间。
(2)函数定义前加extern或不写extern,都表示该函数允许被其他文件调用。
如果不让其他文件调用本文件定义的全局变量或函数,该如何做呢?在全局变量的定义或函数定义前加static,可限制其只能在本文件中使用,不允许在其他文件中使用,即使在其他文件中使用extern声明也不行例如:static int a;
static int MyFun(int a, int b)
{
}
允许被其他文件调用的函数称为外部函数,不允许被其他文件调用的函数称为内部函数。
注意:若允许被其他文件使用,函数在定义时不写static也可以,或加extern强调也可以。但对于全局变量,定义时只能不写static,不能加extern强调。如果加了就是声明变量而不是定义变量了,会导致变量未定义的错误。
总结static的用法和作用:
(1)全局变量定义时或函数定义时加static,表示限制其只允许在本文件内被使用,不允许在其他文件中被使用。
(2)局部变量定义前加static,表示静态变量。
变量的存储类别:
存储类别表示变量在计算机中的存储位置,有3中存储位置分别是:内存动态存储区、内存静态存储区、cpu寄存器。
在定义变量时,如何指定变量的存储类别呢(这里仅指局部变量,全局变量除外)?
(1)在定义变量时,变量名前用关键字auto,则变量将位于内存动态存储区,这种变量也称自动型变量。
(2)在定义变量时,变量名前用关键字static,则变量将位于内存静态存储区,这种变量也称静态性变量。
(3)在定义变量时,变量名前用关键字register,则变量将位于CPU寄存器,这种变量也称寄存器型变量。
auto可以省略,也就是说,如在定义变量时没有写出以上3中关键字的任何一种,则该变量为auto型,与写出auto是等效的。
举例:
auto int a;/*或写为int a;auto a;*/
static int b;/*或写为static b;*/
register int c;/*或写为register c;*/
以上3个变量分别位于内存动态存储区、内存静态存储区、CPU寄存器中,其中,auto可以省略,在写出auto、static、register且变量为int型是,int也可以省略。
注意:位于CPU寄存器中的变量,是没有在内存中的,当然也没有地址,因此不能用&取地址。
不同存储类别变量的特点:
注意:register型变量的速度要远远快于其他存储类别的变量
重新初始化:是指如果函数中一个变量在定义时赋了初值,则每次调用这个函数都要重新为变量赋初值。
只初始化一次:是指在本函数被多次调用时,只有第一次被调用才给变量赋初值,该函数以后再被调用不会再给变量重新赋初值。
局部变量和全局变量的存储类别:
(1)全局变量:只能是static型的,即只能位于内存静态存储区。
(2)函数中,或{}中的局部变量:可以使auto、static或register型的。
(3)函数的形参:可以是auto或register型的,不能为static型的。
注意:在定义全局变量时,有无static都是静态的,如有static则是另外的含义,它不允许被其他源文件使用。
预编译处理:
c源程序的运行要经过编译、链接两个阶段。这里讲的预编译在编译之前,也就是预编译处理-编译-链接。编译系统一般都是把预编译、编译两个阶段一起完成,因此在上机操作时一般我们感觉不到预编译的存在。直接单击编译按钮就可以了,编译系统会自动先预编译,在编译。
预编译处理,也称编译预处理。顾名思义,就是在编译之前所做的工作。
预编译处理有三类:包含文件(#include)、宏定义(#define、#undef)和条件编译(#if,#elif,#else,#ifedf,#ifndef,#endif)。预编译处理不是执行语句,只能称其为"命令"。
(1)预编译命令单独占一行,以#号开头,后无分号(;)。
(2)先预处理,在编译。
(3)预编译命令本身不编译。
宏定义:
我们学习过的符号常量就是一种宏定义,例如:#define PI 3.14。它的含义是将PI定义为文本3.14的代替符号,源程序中所有PI都将首先被替换为3.14,然后在编译,这样所编译的内容中就不在有PI而只有3.14,编译的是3.14而不是PI。
#define命令的用法是:#define 宏名 替换文本
它是一个命令,而不是语句,称为宏定义。宏定义的含义是用一个宏名去代替一个替换文本。宏名是个标识符,符合标识符命名规则的任意名称都可以。
在编译预处理时,将对源程序中的所有宏名都用替换文本去代换,称为宏代换或宏展开,然后在对代换后的内容进行编译。
#define命令必须写在函数外,其作用域为从命令起到源程序结束。
宏定义的替换文本可以是任意文本,而不仅限于数字。预处理时不做语法检查,只有在宏展开以后编译时,在对宏展开以后的内容做语法检查。
注意宏展开时没有任何计算的过程。
在源程序中引号之内的宏名是不会被替换的。例如:#define N 3+5 则语句printf("N");仍在屏幕上输出N本身,引号内的N不会被替换为3+5.
在宏定义命令的行尾是不加分号(;)的。若加分号(;),对宏定义命令本身系统并不报错,只不过在宏展开时,分号也会被视为替换文本的一部分,将跟随一起替换。只要保证替换后的内容无语法错就可以了。
(1)无参宏定义
(2)带参宏定义
文件包含:
文件包含命令是#include,它也是预编译处理的一种。例如:#include <stdio.h> #inluce "Math.h"
文件包含是指将另一文件的内容包含到当前文件的#include命令的地方,取代#include命令行。如同将另一个文件打开、全选、复制,在到#include命令的地方粘贴一般。
所包含文件的文件名可用一对<>括起,也可用""括起。其区别是:<>表示所包含的文件位于系统文件夹中;""表示位于用户文件夹中(一般与本C源程序同一文件夹),当使用""时,若在用户文件夹中没有找到要包含的文件,计算机会自动再去系统文件夹中查找。