函数的概述
- 函数:实现一定功能的,独立的代码模块。对于函数的使用,一定是先定义,后使用。
- 使用函数的优势:
① 我们可以通过函数提供功能给别人使用。当然我们也可以使用别人提供的函数,减少代码
量。
②借助函数可以减少重复性的代码。
③ 实现结构化(模块化) 程序设计思想。
关于 结构化设计思想 :将大型的任务功能划分为相互独立的小型的任务模块来设计。
- 函数是C语言程序的基本组成单元:
C语言程序是由一个(必然是 main 函数)或多个函数组成。
函数的分类
- 从函数实现的角度:
库函数:C语言标准库实现的并提供使用的函数,比如说: scanf() 、 printf() 、 fgets() 、
fputs() 、 strlen()
自定义函数:需要程序员自行实现,开发中大部分函数都是自定义函数。
- 从函数形式的角度:
无参函数:函数调用时,无需传递,可有可无返回值,举例:show_all();
有参函数:函数调用时,需要参数传递数据,经常需要配套返回值来使用,举例:
printf("%d\n",12);
- 从函数调用的角度:
主调函数:主动去调用其他函数的函数。(main函数只能作为主调函数)
被调函数:被其他函数调用的函数。
举例:
cs
// 主调函数
int main()
{
// 被调函数
printf("hello world!\n");
}
很多时候,尤其是对于自定义函数来说,一个函数有可能既是主调函数,又是被调函数。
函数的定义
定义
语法:
cs
[返回类型] 函数名([形参列表]) -- 函数头 | 函数首部
{
函数体语句; -- 函数体,整个{}包裹的内容包括返回值都属于函数体,{}不能省略
}
函数头:
-
返回类型:函数返回值的类型
-
函数名:函数的名称,遵循标识符命名(不能以数字开头,只能包含字母、数字、下划线,建议:小写+下划线命名,举例:show_all())
-
形参列表:用于接收主调函数传递的数据,如果有多个参数,使用","分隔,且每一个形参都需要指明类型。
小贴士:
形参列表(被调函数):主调函数给被调函数传递数据,主调函数 → 被调函数
返回类型(被调函数):被调函数给主调函数返回数据,被调函数 → 主调函数
说明:
- 函数的返回类型:就是返回值的类型,两个类型可以不同,但是必须能够进行转换。举例:
cs
double fun_a() // 函数的返回类型是double
{
return 12; // 函数的返回值是int
}
以上代码可以理解为:将 int 类型的 12 赋值给一个 double 类型的匿名变量( int → double ),此
时属于隐式转换。
cs
int fun_a() // 函数的返回类型是double
{
return 12.5; // 函数的返回值是int
}
- 在C语言中还可以定义无类型(void类型)的函数,这种函数不返回函数值(没有返回值),只 是完成某种功能,举例:
cs
void test()// 此时这个函数,没有返回值,也就是它的返回值是 return;
{
printf("hello\n");
}
// 下面写法等价于上面写法
void test()
{
return; // 一般,这个return是省略不写的。
}
- 在C语言中,函数的返回类型标识符是可以省略的,如果省略,默认返回int,举例:
cs
// 写法1:main的返回类型是int类型,默认的返回值是0,等价于 写法2
main()
{
...
}
// 写法2:main的返回类型是int类型,返回值是0
int main()
{
return 0;// 0:逻辑真,-1:逻辑假,支持非0表示
}
- 函数中返回语句的形式为 return(表达式) 或者 return 表达式 。
cs
// 写法1
int main()
{
return(0);
}
// 写法2 完全等价于写法1
int main()
{
return 0;
}
- 如果 参数列表 中有多个形式参数,则它们之间要用","分隔;即使它们的类型相同,在形式参数 中只能逐个进行说明,举例:
cs
// 正确示例
int avg(int x, int y, int z)
{
...
}
// 错误示例
int avg(int x, y, z)
{
...
}
- 如果 形参列表 中没有参数,我们可以不写,也可以用void标识,举例:
cs
// 写法1 推荐
int main()
{
...
}
// 写法2
int main(void)
{
...
}
形参和实参
函数定义时指定的参数,形参是用来接收数据的,函数定义时,系统不会为形参申请内存,只有当
函数调用时,系统才会为形参申请内存。主要用于存储实际参数,并且当函数返回时(执行
return),系统会自动回收为形参申请的内存资源。
实参(实际参数)
定义
- 函数调用时主调函数传递的数据参数(字面量、符号常量、表达式...,只要有确定的值),实参 就是传递的数据。
- 实参和形参必须类型相同,如果不同时,按赋值规定进行类型转换,比如隐式转换。
- 在C语言中,参数传递必须遵循 单向值传递 (通过实参给形参赋值),实参只是将自身的值传递 给了形参,而不是实参本身。形参的值的改变不会影响实参。
cs
int fun(int n) // n 是形参
{
printf("%d\n",n); // n = 10 n = 12
}
int main()
{
int a = 10; // a = 10
fun(a); // a 是实参,此时传递的数据是 10 实参是变量
fun(12); // 字面量12就是实参,此时传递的数据是 12 实参是常量
fun(a + 12); // 此时传递的数据是 22 实参是表达式
- 实参与形参在内存中占据不同的内存空间。
函数的返回值
- 若不需要返回值,函数可以没有return语句。
cs
// 如果返回类型是void,return关键字可以省略
void fun1
{
}
// 这种写法,return关键字也可以省略,但是此时默认返回是 return 0
int fun2()
{
}
// 这种写法,return关键字也可以省略,但是此时默认返回是 return 0
fun3() // 如果不写返回类型,默认返回int
{
}
- 一个函数中可以有多个return语句,但任一时刻只有一个return语句被执行。
cs
int eq(int num)
{
if (num % 2 == 0)
{
printf("%d是偶数\n",num);
return 0;// 第一次出现
}
printf("%d是奇数\n",num);
return 0; //第二次出现
}
- 返回值类型一般情况下要和函数中return语句返回的数据类型一致,如果不一致,以函数定义 时指定的返回值类型为标准。
函数的调用
调用的方式
①函数语句
cs
test(); //对于无返回值的函数,直接调用
int res=max(2,4); //对于有返回值的函数,一般需要在主调函数中接收被调函数的返回值
在一个函数中调用另一个函数具备以下条件: ①被调用的函数必须是己经定义的函数。
②若使用库函数,应在本文件开头用#include包含其对应的头文件。
③若使用自定义函数,自定义函数又在主调函数的后面,则应在主调函数中对被调函数进行声明。声明的作用是把函数名、函数参数的个数和类型等信息通知编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的 合法性。
函数的声明
函数调用时,往往要遵循先定义后使用,,但如果我们对函数的调用操作出现在函数定义之前,则需要对函数进行声明。
定义:
完整的函数分为三部分:
- 函数声明
cs
int max(int x,int y); //指明参数名称
int max(int,int); //省略参数名称
函数声明如果是在同一个文件,一定要定义在文件中所有函数定义的最前面。如果有对应的山文件,可以将函数的 声明抽取到h中。
- 函数定义
cs
int max(int x,int y,double z)
{
return x >y x : y > z (int) ? y : (int) z;
}
函数定义的时候,不能省略参数的数据类型,参数个数,参数名称,位置要和函数声明完全一致。
- 函数调用
cs
int main()
{
printf("%d\n",max(4,5,6));
}
函数声明的作用:
- 是把函数名、函数参数的个数、函数参数的类型和返回类型等信息通知给编译系统,以便于在遇到函数调用时,编译系统能正确识别函数,并检查函数调用的合法性(C语言规范)。
使用
错误演示:被调函数写在主调函数之后
cs
//主调函数
int main()
{
printf("%d\n",add(12,13)); //编译会报错
}
//被调函数
int add(int x,int y)
{
return x y;
}
注意:如果函数的调用比较简单,并且被调函数写在主调函数之前,此时是可以省略函数声明的。
正确演示:被调函数和主调函数无法区分前后,需要增加被调函数的函数声明
cs
//函数声明
//int add(int x, int y);
int add(int, int);
//主调函数
int main()
{
printf("%d\n",add(12,13)); //编译会报错
}
//被调函数
int add(int x,int y)
{
return x y;
}
注意:如果涉及函数的相互嵌套调用,或者复杂嵌套调用,此时是无法区分函数的前后的,这就需要函数声明。
声明的方式:
- 函数头加上分号
csint add(int a,int b);
- 函数头加上分号,可省略形参名称,但不能省略参数类型
csint add(int,int);
函数的嵌套调用
函数不允许嵌套定义,但允许嵌套调用
- 正确:函数嵌套调用:
cs
void a(){..}
void b()
{
a(0;
}
- 错误:函数嵌套定义
cs
void a()
{
void b()
{
}
}
嵌套调用:在被调函数内又主动去调用其他函数,这样的函数调用形式,称之为嵌套调用。
函数的递归调用
递归调用的含义:在一个函数中,直接或间接调用了函数自身,就称之为函数的递归调用。本质上还是函数的嵌套调用。
递归调用的本质
是一种循环结构,它不同于我们之前学的while、for、do.while这样的循环结构,这些循环结构是借助于循环变量;而递归调用时利用函数自身实现循环结构,如果不加以控制,很容易产生死循环。
递归调用的注意事项
①递归调用必须要有出口,一定要想办法终止递归(否则就会产生死循环)
②对终止条件的判断一定要放在函数递归之前(先判断,再执行)
③进行函数的递归调用。
④函数递归的同时一定要将函数调用向出口逼近。
数组做函数参数
定义
当用数组做函数的实参时,则形参应该也要用数组或者指针变量来接收(函数实参是数组,形参一
定是数组或者指着),注意的是,此次传递的并不代表传递了数组中所有的元素数据,而是传递了
第一个元素的内存地址(数组首地址),形参接收到这个地址后,则形参和实参就代表了同一个内
存空间,则形参的数据修改会改变实参。这种数据传递方式称之为地址传递。

如果用数组作为函数的形参,那么我们提供另一个形参表示数组的元素个数。原因是数组形参代
表的仅仅是实际数组的首地址。也就是说形参只获取到了实参数组的第一个元素的地址,并不确定
传递了多少个数据。所以提供另一个形参表示数组元素的容量,可以防止在被调函数对实际数组访
问时产生的下标越界。
但有一个例外,如果是用字符数组做形参,且实参数组中存放的是字符串数据(形参是字符数组,
实参是字符串常量)。则不用表示数组元素个数的形参,原因是字符串本身会添加自动结束标志
\n ,举例 :
cs
#include <stdio.h>
// 定义一个函数,传递一个字符串
void fun(char arr[])
{
char c;
int i = 0;
while((c = arr[i]) != '\0')
{
printf("%c",c);
i++;
}
printf("\n");
}
void main()
{
fun("hello");
}
变量的作用域
变量的作用域
概念:变量的作用范围,也就是说变量在什么范围有效。
变量的分类
根据变量的作用域不同,变量可以分为:
全局变量

举例:
cs
int num1; // 定义在所有函数之外的变量,称之为全局变量,num1能被fun1,fun2,main共同访问
void fun1(){}
int num2; // 定义在所有函数之外的变量,称之为全局变量,num2能被fun2,main共同访问
void fun2(){}
void main(){}
int num3; // 定义在所有函数之外的变量,称之为全局变量,num3不能被任何函数访问
局部变量:

使用全局变量的优缺点:
优点:
- 利用全局变量可以实现一个函数对外输出的多个结果数据。
- 利用全局变量可以减少函数形参的个数,从而降低内存消耗,以及因为形参传递带来的时间消
耗。
缺点: - 全局变量在程序的整个运行期间,始终占据内存空间,会引起资源消耗。
- 过多的全局变量会引起程序的混乱,操作程序结果错误。
- 降低程序的通用性,特别是当我们进行函数移植时,不仅仅要移植函数,还要考虑全局变量。
- 违反了 " 高内聚,低耦合 " 的程序设计原则
总结:我们发现弊大于利,建议尽量减少对全局变量的使用,函数之间要产生联系,仅通过实参
- 形参的方式产生联系。
作用域举例:

注意:
如果全局变量(外部变量)和局部变量同名,程序执行的时候,就近原则(区分作用域)
cs
int a = 10;// 全局变量
int main()
{
int a = 20;// 局部变量
printf("%d\n",a); // 20 就近原则
for (int a = 0; a < 5; a++) // 这里的a是块作用域,一旦出了for循环,就不能再访问
{
printf("a=%d ",a); // 0 1 2 3 4 就近原则
}
printf("%d\n",a); // 20
}
变量的生命周期
定义:
概念:变量在程序运行中的存在时间(内存申请到内存释放的时间)。
根据变量存在的时间不同,变量可分为静态存储方式和动态存储方式。
变量的存储类型
cs
变量的完整定义格式:[存储类型] 数据类型 变量列表;