学习笔记之——C语言 函数
采用函数的原因:
随着程序规模的变大,产生了以下问题:
——main函数变得相当冗杂
——程序复杂度不断提高
——代码前后关联度提高,修改代码往往牵一发而动全身
——变量使用过多,命名都成了问题
——为了在程序中多次实现某个功能,不得不重复多次写相同的代码
小甲鱼将函数一部分作为自学内容放在课后作业s1e23里,要求自学。阅览后,认为讲解不清,网上到处查询。看到CSDN里一篇详解,认为可用,抄录下来以备查询。(//blog.csdn.net/qq_43469639/article/details/123765064)
1、 函数是什么
在维基百科中,对于函数的定义是子程序。子程序是一个大型程序中的某部分代码,由一个或多个语句块组成,他负责完成某项特点的任务,而且相较于其他代码,具备相对的独立性。
C语言中函数分为库函数和自定义函数两大类。
2、 库函数
为什么会有库函数
2.1我们知道在我们学习C语言编程的时候,总是在一个代码编写完成后迫不及待的想要知道结果,想要把这个结果打印到我们的屏幕上看看,这个时候我们会频繁的使用一个功能,将信息按照一定的搁置打印到屏幕上。
2.2在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)
2.3在编程时,我们也会计算,也总数会计算n的k次方这样的运算
像上面我们描述的基本功能,他们不是业务性的代码,我们在开发的过程中每个程序员都有可能用到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一些列类似的库函数,方便程序员进行软件开发。
简单总结,C语言常用的库函数都有:
IO函数 printf scanf getchar putchar
字符串操作函数 strcmp strlen
字符操作函数 toupper
内存操作函数 memcpy memcmp menset
时间/日期函数 time
数学函数 pow sqrt
其他库函数
注:不同与作者,我的观点是:库函数精炼了大量人员需要重复使用的比较复杂的函数,并组装成库,方便调用。其他语言也有类似情况,比如我认为python就是大量的各种应用场景比较小的库的组合,使用时,根据规则调用即可直接使用。
3、 自定义函数(前方高能,函数的精髓所在)
如果库函数能干所有的事情,那还要程序员干什么,所以有更加重要的自定义函数
自定义函数和库函数一样,有函数名,返回值类型,函数参数,但是不一样的是这些都是我们自己设计的,这给程序员一个很大的发挥空间。
int add(int x,int y)//int 返回值类型 add为函数名 括号里为函数的参数
{
intz=0;
return z=x+y;
}
注:自定义函数是程序员为了满足自己程序里的需要,避免重复输入,或者简化程序逻辑负责性,或者满足库函数以外的功能,自行编制的函数。
4、 函数参数
函数的参数分为实际参数(实参)和形式参数(形参)
实参:在调用函数时传递给函数的参数
真实传递给函数的参数,叫做实参,实参可以是:常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用的时候,它们必须有确定的值,以便把这些值传给形参。
形参:在函数里,用于接收实参的参数
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(为形式参数分配内存单元),所以叫形式参数,形式参数当函数调用完成之后就自动销毁了,因此形式参数只在函数中有效。
函数例子:
#include<stdio.h>
int add(int x,int y) //这里为形式参数,用于接收实际参数的值
{
int z=0;
return z=x+y;
}
int main()
{
int a=0;
int b=0;
scanf(“%d %d”,&a,&b);
add(a,b); //这里是实际传过去的参数,要与对应函数的参数类型和数量对应。这里是为函数传递实际参数
return 0;
}
5、 函数的调用
传值调用
函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的 一种调用函数的方式
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接处理外部的变量
int add(int x,int y)
{
int z=0;
return z=x+y;
}
int add2(int* x,int* y)
{
int z=0;
return =*x+*y;
}
int main()
{
int a=0;
int b=9;
scanf(“%d %d”,&a,&b);
printf(“%d\n”,add(a,b)); //传值调用
printf(“%d\n”,add2(&a,&b)); //传址调用
return 0;
}
6、 函数的嵌套调用和链式访问
嵌套调用
函数和函数之间可以有机的组合的
例:
int main()
{
int a=0;
int b=0;
scanf(“%d %d”,&a,&b);
add(a,b);
add2(&a,&b); //本句与上一句组成嵌套函数(没搞懂)
return 0;
}
链式访问
把一个函数的返回值作为另一个函数的参数
int add(int x,int y)
{
int z=0;
return z=x+y;
}
int add2(int * x,int * y)
{
int z=0;
return z=*x+*y;
}
include<stdio.h>
int main()
{
int a=0;
int b=0;
scanf(“%d %d”,&a,&b);
printf(“%d\n”,add(add2(&a,&b),add2(&a,&b)); //链式访问
return 0;
}
7、 函数的声明和定义
函数声明
1) 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么,但是具体是不是存在,无关紧要
2) 函数的声明一般出现在函数使用之前,要满足先声明后使用
3) 函数的声明一般要放在头文件中
注:以博客作者的井字棋为例:
#include “game.h” //引用头文件
//棋盘
void.showborad(char board[row][col],int row,int col);//声明
//函数的定义
//函数的定义是指函数的具体实现,交代函数的功能实现
//初始化棋盘
void showborad(char board[row][col],int row ,int col)
{
int i=0;
int j=0;
for(i=0;i<row;i++)
{
for(j=0;j<col;j++)
{
board[i][j]=’ ’;
printf(“%c”,board[i][j]);
if(j<col-1)
{
printf(“|”);
}
}
printf(“\n”);
for(j=0;j<col;j++)
{
printf(“-“);
if(j<col-1)
{
printf(“|”);
}
}
printf(“\n”);
}
}
8、 函数的递归
什么是递归
程序调用自身的编程技巧称为递归,递归作为一种算法在程序设计语言中广泛应用,一个过程或函数在其定义活说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题蹭蹭转化为一个与原题目相似的规模较小的问题来求解,递归策略只需少量的程序就可以描述出解题过程所需要的多次重复计算,大大的减少了程序的代码量,递归的主要思考方式在于:把大事化小
递归的两个必要条件
1) 存在限制条件,当满足这个限制条件的时候,递归便不再继续
2) 每次递归调用之后越来越接近这个限制条件
9、 当自定义的函数名与库函数重名的时候:
根据//zhidao.baidu.com/question/369199943840037324.html中说明:当自定义函数与库函数同名时,一般的调用是自定义函数优先,但是标准库函数并不失去意义。只是调用方式要有所改变:用双冒号::开头是调用库函数,直接写函数名是调用自定义的函数。
举例说明:
#include<stdio.h>
void printf()
{
puts(“12345”);
}
int main()
{
::printf(“abc\n”);
printf();
return 0;
}
运行结果如下:
abc
12345
10、
以下摘抄自谭浩强主编的《C语言程序设计》(第3版),算是我对照课本的学习笔记吧:
1、 函数是什么:
每一种计算机语言里都有子程序、函数之类,用于表达一小部分具体的功能,可以被其他部位的 函数调用使用。函数这个概念来源于英文的function,译成中文可以是函数、概念、功能等。函数的存在使大规模的问题处理可以分解为一个个小的问题来解决,把整个问题分成模块化组装来解决。这是函数存在的主要意义。另外利用调用函数的方法可以减少代码的数量,在主程序里不同部位出现的重复的功能组成单独的、可以调用的模块,在使用时不必重复敲入,同时还让代码直观。
C语言使用函数化编程,main函数成为主函数,程序在编译过程中,从前往后编译,实际运行的时候通过运行程序的命令直接调用main主程序,在主程序里通过直接或者间接的调用其他子函数。
2、 函数在未调用时候,不占用内存,不分配位置,在实参传递给形参后,程序分配内存。在调用结束后(子函数运行完毕),释放形参单元。所以函数是相互独立的,定义需要分别定义,不能嵌套定义。这个应该属于局部变量的范畴了,后面学习局部变量应该容易理解。
3、 由于编译过程是从上到下。那么函数的编写在被引用点前的话,在引用的函数里不需要声明出来。在引用点后面编写的函数,在引用的函数里要声明。
函数的声明方法:形式与函数名一致,后面要求加分号。
4、 当没有返回值的时候(return为0),函数要指定为viod类型。有返回值的时候,函数类型为返回值类型。这样的定义,有助于在调用层函数里把这个函数可以作为一个变量使用。一个函数只能带回一个返回值。
5、 定义函数: 类型名 函数名(形参列表){ }
6、 声明函数:类型名 函数名(形参列表){ };
7、 在函数的最后位置对函数返回值进行限制。
限制语句为 return (返回变量名),或者简写为return 变量名;
8、 函数在被调用的时候,可以当作一个本函数里的变量看待,调用方法和使用一个变量一样。谭浩强教程中给出3种调用方式:单独一个语句printf_star; 函数表达式c=max(x,y); 作为函数的参数printf(“%d”,max(a,b);
9、 函数在被调用的时候,实参的列表顺序和数据类型和形参一致。
这一条里面有两个问题没有搞懂:
1) 声明函数的时候,形参可以不写参数名。比如void print(int,float,char);是合法的,那么在函数里怎么判断该调用那个参数了呢?(解决:在需要调用函数的程序里,声明函数只是给函数预留出足够用的内存,所以可以不用写参数名,但是在编程过程中,形参位置一定要写参数名。)
2) 存在调用函数可以少写参数的情况。比如主函数main在规定里是带有两个参数的,平时可以不写。根据要求必须一一对应,那么如何处理调用过程中参数缺省情况呢?
10、 函数可以调用其他函数,称为嵌套调用。
函数可以直接或者间接的调用函数本身,称为递归调用。递归调用的时候要注意设置结束条件。
11、 调用函数的过程,其实是一个把实参传递给形参,函数再返回一个值的过程。调用的过程中,单个的数值(包括单个变量和数组元素)被传递给函数(值传递方式)。在编译过程中,对形参分配一个对应数据类型的地址,调用过程中,实参被存储在形参准备好的地址里使用(地址传递方式)。调用过程中,数组名被传递给函数。由于数组的本质是指针,故传递过来的只是实参数组的指针地址,所以在函数里,形参部位需要用方括号“[ ]”来体现传递过来的是一个数组(注重数据类型,不注重数组长度可以不指定数组大小,谭浩强在202页上半部说明,并指明“形参数组名实际上是一个指针变量”)。
12、 使用数组传递参数的时候,第一维大小可以省略,第二维及以上不可省略。
13、 全局变量:在主函数开始前,程序最开始定义的变量可以作用与所有的函数里,直到最终。称为全局变量。
在一个函数或者复合语句中定义的变量,在这个函数或复合语句结束后就失去效果的变量,称为局部变量。
全局变量与局部变量的作用域举例:国家有统一的法律法规,各省可以根据需要指定地方的法律和法规。在甲省,国家统一的法律法规和甲省的法律法规都是有效的。而在乙省,则国家统一的法律法规和乙省的法律法规有效,显然,甲省的法律法规在乙省无效。
全局变量可以在别的函数里改变这个函数的值,效果上相当于通过函数调用得到了一个以上的值。
14、 建议不在必要时使用全局变量:
1) 全局变量在程序的全部执行过程中都占有存储单元,而不是仅在需要时才开辟单元。
2) 全局变量降低了函数的通用性。因为函数在执行过程中依赖其所在的程序文件中定义的外部变量。当把一个函数移植到另一个文件中时,还需要将有关外部变量和值一起移过去,而且一旦与移植到文件中出现同名变量,会发生冲突,降低程序的可靠性。
使用全局变量过多,会降低程序的清晰性,往往难以清楚的判断出每个瞬间各个外部变量的值,容易出错。
15、 变量的静态存储和动态存储方式:
谭浩强《C语言程序设计》(第3版)中提到变量的生存期,也就是变量值的存在时间。我理解和局部变量、全局变量一致,仅仅是从具体空间、时间的角度来解释变量。
静态存储方式是指在程序运行期间由系统在镜头存储区分配存储空间的方式,在程序运行期间不释放。而动态存储方式则是在函数调用期间根据需要在动态存储区分配存储空间的方式。全局变量采用静态存储方式。在函数中定义的变量,在函数调用开始时分配动态存储空间,函数结束时释放空间。
程序里可以指定变量的存储方式,来强制存放位置和是否静态存储。
1)auto ——声明自动变量 auto int b,c=3; //定义b,c为自动变量
在函数的形参位置、函数内部、复合语句里定义的变量都属于这一类。在函数调用的时候分配存储空间,在调用结束时释放空间,auto可以省略(就是指平时使用的函数内变量)。
属于动态存储方式
2)static——声明静态变量 static int f=1;
在函数中,如果希望在函数结束后变量不消失,可以在下一次调用这个函数的时候使用里面的数值,那么指定为static。(注意到:教程里提到的是这个变量可以在下一次这个函数被调用的时候使用,语意中不能被别的函数调用。是否属实,须待核实。)static声明的变量,只限于被本文件引用,而不能被其他文件引用。对于局部变量来说,它使变量有动态存储方式改为静态存储方式,而对于全局变量来说,它使变量局部化(其他文件不能使用)。
可以避免被别的文件调用。
属于静态存储方式
3) register——声明寄存器变量 register int f;
普通变量存储在内存中,每次调用时,从内存送到运算器使用,每次存储数据时,从运算器送到内存保存。对于使用极度频繁的变量,相对于存取变量的时间就是可观的,为了提高效率,C语言允许将局部变量的值房子CPU的寄存器里,减少来回取存的时间。现在由于计算机性能的提高,速度越来越快,编译系统能够识别高频使用的变量,可以自动将变量放在寄存器里,而不需要设计者指定。故现有已经不需要指定register声明了。仅仅用于看别人程序时知道就可以了。
4) extern——声明外部变量作用范围 extern b;
大型的程序都是由多个人编写的,都是多个文件组成的。普通全局变量只是在所在文件的声明点到文件结束有效,别的文件不能使用这个变量,需要用一个个的函数进行传递。
可以在变量声明部位加入 “extern b;”语句将别的文件中的外部变量合法的引入这个文件中使用。在定义时候,要在前面加关键字:“extern int fun (int a,int b);”
各类变量作用域与存在性情况
变量存储类别 |
函数内 |
函数外 |
|||
作用域 |
存在性 |
作用域 |
存在性 |
||
本文件内 |
多个文件间 |
||||
自动变量auto、寄存器变量register |
√ |
√ |
× |
× |
√ |
静态局部变量(普通全局变量) |
√ |
√ |
× |
× |
√ |
静态外部变量static |
√ |
√ |
√ |
× |
√ |
外部变量extern |
√ |
√ |
√ |
√ |
√ |