用函数实现模块化程序设计(适合考研、专升本)
函数
定义:本质上是一段可以被连续调用、功能相对独立的程序段
c语言是通过“函数”实现模块化的。根据分类标准不同函数分为以下几类。
用户角度:库函数、自定义函数
函数形式:有参函数、无参函数
作用域:外部函数、内部函数
-
函数的引入实现了两个目的:1.结构化、模块化编程 2.解决代码重复
-
函数为什么要先定义后使用:
通知编译系统函数的返回值类型、函数的名称、函数的参数个数与类型以及函数实现什么功能。
-
编译系统不检查函数名与形参名同名,即允许函数名与形参名同名。
-
函数是c语言主要组成部分,是模块化程序设计的思路。
-
一个c程序由一个或多个源程序文件组成,一个源程序文件对应一个程序模块。一个源程序文件(程序模块)由一个或多个函数及其他有关内容(指令、数据声明与定义等)组成,一个源程序文件是一个编译单位。
-
函数由函数原型与函数体构成,函数体由声明部分与语句部分构成。声明部分由函数内的变量定义以及对其他函数的声明构成
-
c程序的执行总是从main函数开始的,也是从main函数结束的。
-
函数返回值的类型应该与函数类型一致,若不一致以函数类型为准,即函数类型决定返回值类型。
-
函数间可以相互调用,但不能调用main函数(main只能被操作系统调用)
-
函数通常先定义,再声明,再使用。
-
对于不带返回值的函数,应该定义为void类型,此时函数体内的return语句不应该携带返回值。
-
函数的形参只能是默认的auto类型,即不能自己加auto或者其它类型
-
c语言规定全局变量只允许使用常量或常量表达式初始化
#include <stdio.h> int a=12,b=13+2; int c=a+b;//这里编译报错,因为a、b属于变量 int main(){}
-
全局变量不允许与符号常量同名
#include <stdio.h> float A=11.2;//全局变量虽然在符号常量前面,但是会先进行预编译进行宏替换,所以这里实际是 int 1.34=11.2,显然是一个语法错误 #define A 1.34 #define B 100 int B=100;//全局变量在符号常量后面,这里会产生编译错误,因为这里实际是 int 1.34=11.2,显然是一个语法错误
-
extern一般都是在其他文件中引入本文件的全局变量或函数在其他文件使用,或者扩展本文件全局变量的作用域时才会在本文件使用
-
默认定义的全局变量与函数是可以在同程序的文件之间共享,只是需要extern声明进来,所以这也是同一程序不同c文件不允许出现同名的外部变量与外部函数
-
课本要求函数实参的个数、类型必须与形参一致,否则会出现编译错误。但实际中实参的类型可以与形参不一致(地址除外)
函数定义
函数与变量、数组一样,都必须先定义后使用。函数的定义包括:指定函数数据类型(函数的返回值)、指定函数名(函数名反应其功能)、指定形参的类型与名字、完成函数功能。函数定义的目的是为了通知编译系统函数的返回值类型、函数的名称、函数的参数个数与类型以及函数实现什么功能。
格式:
1.定义无参函数: 数据类型 函数名(void){ //形参可以为空也可以写void,表示没有参数函数体;}2.定义有参函数: 数据类型 函数名(形参列表){函数体;} 3.定义空函数:void 函数名(){//没有函数体}
-
空函数的作用:给以后编写的函数占据一个位置
-
函数体包括声明部分与语句部分(执行部分)
-
函数定义时指定的形参不占用存储空间,只有在调用时才分配存储空间
-
函数定义省略函数的数据类型,此时默认为int类型,但是注意在声明时必须加上int,否则会编译错误
int main() {int sum(int,int);//但是声明时必须指定为intreturn 0; } sum(int sum,int y){//省略了函数数据类型return 0; }
-
c语言不允许变量名与函数名相同,但是允许形参名与函数名相同(不建议)
int sum(int sum,int y){return 0; }
-
形参与实参可以同名,因为他们的作用域不同,会分配不同的内存空间(不建议)
int add(int x);//add函数声明 int x=12; add(x);//实参x与形参x同名,但是不影响
-
函数定义的目的:通知编译系统函数的返回值类型,函数名、函数的参数个数及类型以及函数的功能。
-
c语言函数不允许嵌套定义(在一个函数内定义另一个函数),即所有函数都是平行的。(但是允许嵌套调用)
函数声明
格式:
main(){add(int x);//add函数声明sum(int);//sum函数声明(可以不写参数名) } void add(int x){} void sum(int y){}
作用:告诉计算机次函数在后面已经定义,将函数的作用域提升到声明处,在调用函数检查函数的正确性
-
函数声明就是函数原型(函数首部)加一个分号即可
-
函数声明中可以省略参数名而只写参数数据类型,因为函数调用不会检查参数的名字
-
函数调用时会检查函数类型,函数名,参数类型、个数,顺序必须与函数声明一致
-
可以将被调函数的声明放在文件开头(所有函数之前)进行声明(又称为外部声明),这样所有函数都可以不在本函数内声明直接调用该函数
#include <stdio.h> int add(int);//add函数声明 int main(){add(1);//调用时可直接调用 } int add(int x){return 0;};//add函数定义
-
如果被调函数在主调函数上面定义,那么可以不进行函数声明。
int sum(int x,int y){return 0; } int main() {int add=sum(2,2);//add函数已经在上面定义,故可不声明直接调用printf("%d\n",add);return 0; }
-
将一个底部的函数声明到另一个函数内,该函数的作用域只是增加了这一个函数的范围,哪怕中间还有其他函数,也无法调用该函数
在一个函数内调用另一个函数需要满足的条件:
-
如果是库函数,则需要在开头使用预处理命令对这些库函数引入
-
如果是自定义函数需要对该函数定义,然后在主调函数中对被调函数作函数声明
函数调用
格式:
函数名(实参列表);//如果是无参函数,则实惨列表为空,但是不能省略括号
-
实参列表如果是多个使用逗号隔开,无参函数调用实参列表为空,但是不能省略括号
-
函数调用有3种方式:函数调用语句、函数表达式、函数嵌套调用。
printf("hello!");//函数直接调用 SUM=sum();//将函数的返回值赋值给一个变量或出现在其他表达式中参加运算 sum(a,sum());//将返回值作为其他函数参数带入运算
-
实参可以是常量、变量、表达式。
函数调用的过程:
-
函数开始调用:为形参分配空间大小,然后将实参的值传递给形参
-
函数开始执行:为函数内定义的局部变量分配空间(函数内的static变量已经在编译阶段就开始初始化并分配空间),并对需要调用的其他函数进行声明等,再依次执行其他语句,直到第一个return结束
-
函数调用结束:释放本函数内局部变量的空间(函数内的static变量不会被释放),返回主调函数继续执行。
实参与形参之间的数据传递:
-
形参于实参之间的数据传递也称为虚实结合
-
实参可以是常量、变量、表达式,调用时实参的数据类型与个数必须与形参一一对应。如果实参的数据类型与形参不一致,则会进行自动类型转换,即把实参数据转换为形参类型再赋值。若实参于形参个数不一致则会出现编译错误。
int sum(int a);//函数已定义,在这里声明 SUM=sum(1.34);//将1.34转换为int类型,即1再赋值给形参a
-
按值传递:形参的改变不会影响实参的值。(数据只能单向由实参—>形参)
按地址传递:改变形参指向地址的值会改变实参指向地址的值,但改变形参本身的地址值不会改变实参本身的地址值。
int main(){void fn(int *,int *);int a=12,b=20,*p=&a;fn(p,&b);printf("%d\t%d\n",a,*p);/*输出:10,10(可见在函数中改变指向地址的值会改变外面实参地址的值,但是改变指针本身指向不会改变外面指针的指向)*/ } void fn(int *p,int *n){*p=10;p=n; }
函数的返回值:
-
return后面的括号可以加也可以不加。例如 return(z)等价于return z。return后面的值可以是一个表达式。例如 return x+y
-
函数为void类型时函数体内return不应该带返回值。
-
函数的类型与return不一致时,以函数类型为准,此时数据类型不同会进行自动类型转换。即函数类型决定返回值类型。
-
有数据类型的函数省略return时函数会返回一个不确定的值。
函数的嵌套调用
定义:在一个函数内调用另一个函数
函数的递归调用
定义:函数直接或间接调用自己本身(直接递归调用、间接递归调用)
递归调用的条件:
-
要解决的问题能能简化为一个新问题,这个新问题的解决方法与原解决问题的方法相同--寻找递归表达式
-
要有一个明确的结束递归条件结束递归调用--寻找递归出口
递归调用分为两个阶段:
-
递推阶段
-
回归阶段
数组作函数参数
1.数组元素作函数参数
定义:数组元素只能作函数实参,不能用作形参。数组元素作为实参传值给形参时是按值传递的方式,即数据从实参单向传递给形参。
void fn(int a[2]){}//a[2]是一个具有2个元素数组,而并非数组元素
普通变量与地址做函数参数的不同:
-
前者按值传递、后者按地址传递
-
前者分配与实参相应的存储空间、后者只分配第一保存地址的空间
-
前者不回改变实参的值,后者会改变实参的值。
2.数组名作函数参数
定义:数组名可以做形参也可以做实参,数组名作实参传递的是数组的首地址。
-
数组名在做函数形参时,不管是函数定义还是函数声明,都是作为指针变量处理,调用函数传入的实际上也是地址,而并非整个数组
int main(){int fn(int *a);//a实际是一个指针变量,可以看见声明int *a与下面的函数定义的int a[]等价int a[]={12,13,15};printf("%d\n",fn(a));//13int b=100;printf("%d\n",fn(&b));//100(这里也可以看出c系统对数组做形参实际按指针处理,否则这里不能传入变量b的地址) } int fn(int a[]){if(*a==100)return 100;return *(++a);//可以看见这里可以对数组名进行指针运算 }
-
数组名作函数参数,必须在主调函数与被调函数分别定义数组
int abc(int arr[]){//被调函数数组arr} int main(){int arr2[10]={};//主调函数数组arr2abc(arr2);//将arr2作为函数参数传递给abc函数的形参arr }
-
数组做函数参数时实参类型必须与形参类型一致,否则会出现编译错误(普通变量做函数参数两者不同时会进行自动转换)
即按值传递可以类型不同,会进行自动类型转换。按地址专递传递则必须类型相同。
-
形参数组如果是一维数组,那么可以省略长度。多维素组只能省略最低维(c编译系统不会检查第1维的大小)
int arr(int abs[]){}//形参是一维数组,可以参略长度 int arr(int abs[][2]){}//形参是二维数组,只可参略第一维长度 int arr(int abs[3][2]){}//与上面等价 int arr(int abs[4][2]){}//形参数组的第一维在与实参数组相同的情况下,可大于实参数组的第1维,但不能小。例如实参a[2][2]
-
sizeof(形参数组)返回的是实参数组的首地址,也就是首地址指针的sizeof
#include <stdio.h> int aver(int arr[]){return sizeof(arr);//实际上是arr数组a[0]的空间大小,是一个指针所以为8 } int main(){int arr[10];int length=aver(arr);printf("%d\n",length);//8printf("%d\n",sizeof(arr));//40(arr在本函数中任然属于一个变量,sizeof返回整个数组的空间大小printf("%d\n",sizeof(arr[0]));//4(arr第一个元素在本函数中也相当于一个变量,返回4)}
-
数组名作为函数实参,函数声明时,不允许值只写数据类型,正确形式为:数据类型 []
int A=12; int main(){int add(int[]);//不能是 int add(int) } int add(int a[]){}
局部变量与全局变量
任何变量都有作用域,根据作用域不同分为局部变量与全局变量。(数组、函数、指针等其实本质上都等同于变量,其也有作用域)
1.局部变量:函数内定义的变量
分类:在函数内定义、复合语句中定义、函数的形参
-
函数内定义的局部变量只在本函数内有效
-
复合语句的局部变量只在复合语句内有效(范围更小)
-
函数的形参只在本函数内有效
-
不同函数内的局部变量可以重名互不干扰。同一函数内不同作用域内的函数也可以重名,例如在复合语句定义的变量可以在包含该复合语句的函数中其他位置定义同名变量,在调用时候采取就近原则
-
c语言不允许函数名与变量名相同,但是允许在本函数中定义的局部变量与本函数重名
int add(){int add=20;return add; }
2.全局变量:函数外定义的变量
-
全局变量通常可以为本文件中其他所有函数共用,有效范围为定义位置到本源文件结束。(
-
全局变量的第1个字母通常大写
-
全局变量与局部变量重名以就近原则调用
-
默认定义的全局变量不加static的话可以被其他源程序文件调用(但是要使用extern要进行外部声明),所以相关联的多个.c文件的程序中,不同的c文件不允许有同名的全局变量,除非同名的全局变量属于静态外部变量
为什么不建议过多的使用全局变量?
-
全局变量在整个程序运行期间都占有存储单元
-
全局变量会使函数的通用性降低
-
全局变量过多,会降低程序的清晰性
变量的存储方式与生命期
1.存储方式
分类:静态存储、动态存储
-
全局变量全部采用的静态存储(包括静态局部变量),他们都是在程序开始执行时(编译)就分配固定的内存空间。
-
局部变量全部采用动态存储,只有在函数调用时分配空间,调用结束后就销毁
⚠️注意:全局变量与静态局部变量都在编译阶段就分配了内存地址,但是全局变量是在编译阶段就进行了初始化,而静态局部变量是在程序运行到该函数时才进行初始化,但是其内存空间是在编译阶段分配好了的。
2.存储类别:
每一个变量(包括数组、函数、指针变量)在定义时都要指定两个属性:数据类型、存储类别。
分类:auto、static、register、extern
⚠️注意:自动变量默认为auto,全局变量默认为extern,其可以在多个文件之间共享,但并非代表其作用域在所有文件,其作用域仍然只限于定义处到本文件结束,其他文件想使用该全局变量需要使用extern声明。换句话说,全局变量在其定义的文件内是全局可见的,但在其他文件中则不是默认可见的。为了在其他文件中使用同一个全局变量,需要使用 extern
关键字来声明该变量,这样编译器就知道在其他地方已经定义了该变量,并且可以在当前文件中访问它。
局部变量的存储类别:
-
static(静态局部变量):使用static关键字声明的局部变量(static变量与全局变量一样在编译开始时就分配了存储单元,直到程序结束。但是static变量的作用域也仅限于本函数内或复合语句内)
-
对于没有初始化的局部变量,静态局部变量(全局变量也是)系统自动赋初值0、0.0、'\0',而自动变量的值确实不可预知的。
-
register(寄存器变量):为提高执行效率,把局部变量的值放在CPU的寄存器中再进行频繁运算(因为寄存器的存取>内存的存取)
-
auto、static、register属于局部变量的存储类别,一般只能用于局部变量(static还可以放在全局变量前)
全局变量的存储类别:
-
全局变量的作用域是定义处到源文件结束,这就造成定义之前的函数无法访问该全局变量,可以使用extern做外部变量声明正常引用
#include <stdio.h> int main(){extern int A;printf("%d\n",A); } int A=12;
注意:将一个底部的全局变量外部声明到另一个函数内,该全局变量的作用域只是增加了这一个函数的范围,哪怕中间还有其他函数,也无法访问该全局变量
#include <stdio.h> //extern int A;必须把外部声明放在这里,才能把该全局变量作用域提高到全局,以下所有的函数都可以访问 int a();//函数声明 int main(){extern int A;//外部声明printf("%d\n",A);//12// printf("%d\n",a());//这里依然无法访问A,因为上面只是把A声明到本函数内,而a函数没有被声明 } int a(){return A; } int A=12;
extern 数据类型 变量名可以省略为 external 变量名
extern int A,B,C 等价于 extern A,B,C
-
还可以使用extern将全局扩展到其他.c文件
文件file1.c #include <stdio.h> int A=12; int main(){ } 文件file2.c extern A;//此时在file2中可以访问到A int main(){ } ⚠️注意:使用 extern 关键字在其他文件中声明全局变量时,不会为该变量分配新的内存空间,而是告诉编译器该变量已经在其他地方定义了。
-
还可以使用static将全局变量限制在本文件中,其他文件不允许访问(静态外部变量)
文件file1.c #include <stdio.h> static int A=12; int main(){ } 文件file2.c extern A;//因为A被限制在file1.c中,所以这里的声明无效 int main(){ }
声明全局变量的存储类型于声明局部变量的存储类型的区别:
-
对于局部变量使用auto、static、register声明的局部变量是为了指定变量的存储区域与生存期
-
对于全局变量使用extern与static是为了扩展或限制全局变量的作用域
内部函数与外部函数
内部函数:
定义:函数只能被本文件中的其他函数调用
格式:
static 数据类型 函数名(参数列表); 例如 static add(int a){}
外部函数:
定义:函数可以被其他文件中的函数调用
格式:
extern 数据类型 函数名(参数列表); 例如 extern add(int a);//声明函数已外部定义
-
定义函数时省略存储类别函数默认是外部函数
-
不加static定义的函数都属于外部函数,所以多个c文件的程序在连接时,不允许出现同名的函数,否则会产生重复定义,除非同名的函数是内部函数
-
若要调用其他文件的函数,与变量一样,需要使用extern将函数声明到本文件中
总结:不管是全局的变量、数组、还是函数,都可以使用extern和static对作用域进行扩展或限制,而对于局部的变量、数组、函数来说使用auto、static等只是改变其存储类型。
#include <stdio.h> extern int ARR[12]; // int a();//函数声明 int main(){extern int a();//与上面的函数声明等价printf("%d\n",a());//12printf("%d\n",ARR[0]);//1 } int a(){return 12; } int ARR[12]={1};