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