一、什么是函数
函数是一个个代码的组合,相当于是个代码块,可以完成程序员自主编写的功能
二、函数的种类
函数主要分为两类:自定义函数和库函数
1.库函数
库函数的存在是因为某一些功能会被程序员重复使用,如计算字符串的长度,计算变量的大小等,为了程序的高效和C语言的可移植性,所以C语言提供了这样一个标准库,只需要引用对应的头文件,就可以使用了
库函数的使用:需要包含这个库函数的头文件
#include <stdio.h> //这个是printf函数的头文件
2.自定义函数
但是库函数没有办法全部描述生活中的问题,或者说实现我们想要达到的效果,这时候就需要程序员自己自定义函数,来完成指定的功能
三、函数的形式(包括一些使用)
eg.double sqrt(double x);
返回类型 函数名 参数
函数是由上面三个部分组成的,下面我将会给大家一一介绍
1.返回类型
1.1返回类型是和返回值一一对应的
int get_max(int x, int y)
{
if (x > y)
return x;
else
return y; //返回的x和y一定都是int类型的
}
1.2 如果我不想有返回值该怎么办
用void,这个函数就不会有返回值,相当于是函数把下面要执行的代码给包裹起来了
void menu(void)
{
printf("hahaha");
}
int main()
{
menu(); //单纯打印出hahaha,不要给我返回什么值
return 0;
}
1.3如果我没有写返回类型会发生什么
如果没有设置返回类型,编译器会默认设置为int类型
2.函数名
2.1 函数/变量的取名要有意义
- 不要用a,b,c之类的,没有什么特殊含义的来取名
- 不要用拼音或者中英混杂,会显得很low
- 命名不要太过
- is_leap_year 可以-----字符+下划线
- IsLeapYear 可以-----首字母大写
- Is_Leap_Year 不可以----命名得太过了
3.函数参数
函数的参数必须要求是一个确定的值,可以是变量、表达式、函数等
3.1 实参VS形参
含义
-
实参是在调用这个函数的时候传过去的值(真实传给函数的参数),可以是:常量、变量、表达式、函数等,要求这是个确定的值,以便这个数可以传送给形参
实参最好不要是全局变量:难以控制而且会降低代码的质量 -
形参是定义函数的时候,用来接收实参的。形式上,就是函数名后括号里的变量,在调用的时候才会分配内容,出了这个函数就会被销毁—只在函数内部有效
函数内部实现逻辑(以3.2的代码为例)
main函数里面调用了Add函数,调用时传进去的a和b就是实参,而上面Add函数里面所定义的x和y就是对应接收实参a和b的形参。
a,b会在main函数这个栈帧区间最后所压上的edi、eai、ebx上面创建空间来存放传过去的值。
而x和y是不会多余创造空间的,它们直接是去访问存放存放值的那个地址,也就是说形参只是实参的临时拷贝,形参的改变不会影响实参,这也是下面要介绍的传参调用和传址调用的主要区分点
调用函数里面的参数是可以名字一样的,因为调用函数彼此之间相互独立,是不会去访问对方的
3.2 传值调用
#include <stdio.h>
//计算3+5的值
void Add(int x, int y)
{
printf("%d", x + y);
}
int main()
{
int a = 3;
int b = 5;
Add(a, b);
return 0;
}
3.3 传址调用
把地址传过去,这样函数里面的实参,就可以改变我实参传过去的值
#include <stdio.h>
//实现交换
void Add(int* x, int* y)
{
*x = 5;
*y = 3;
printf("%d %d\n", *x,*y);
}
int main()
{
int a = 3;
int b = 5;
printf("%d %d\n", a, b);
Add(&a, &b);
return 0;
}
3.4 如何选择是传参还是传址
如果这个函数可能会改变我穿过去的形参的值,那我就要采用传址调用,使得形参的改变可以影响我的实参
如3.3中,我穿过去的a是3,b是5,但是函数要实现的是把我的a改成5,b改成3,所以就要采用传址调用;而3.2中,只是单纯把a和b相加,本身是没有改变a和b的值的,所以可以采用传值调用
4.返回多个值
4.1通过数组返回
#include <stdio.h>
void test(int arr[])
{
arr[0] = 3;
arr[1] = 4;
}
int main()
{
int arr[2] = { 0 };
printf("%d %d\n", arr[0], arr[1]);
test(arr);
printf("%d %d\n", arr[0], arr[1]);
return 0;
}
4.2 通过全局变量返回
int x = 0;
int y = 0;
void test()
{
x = 3;
y = 4;
}
int main()
{
test();
printf("%d %d\n", x, y);
return 0;
}
四、函数的使用
函数的使用需要声明和定义
1.声明
void Add(int* x, int* y)
告诉程序有这个函数,这个函数的返回类型是什么,参数是什么,函数名是什么,但是它是如何实现的,是否真的有这个函数,都算是未知数。
就相当于排队时,给人占一个位,主观上这里等会会来一个人,但是实际上这个人最终会不会在队排完前来,依旧是一个未知数
注意声明放的位置:
- 声明要放在调用函数的代码前面,因为程序是一条一条执行下来的,如果声明在使用前,程序即使后面知道你声明了,也会报错。
- 一般要放在头文件里面
int Add(int x,int y);
int Add(int ,int ); //只是告诉程序有没有这个函数,传值也计算不了
#define M 100
2.定义
说明这个函数是如何实现的,这个函数里面的代码是什么样的,一般放在源文件里面
void Add(int* x, int* y)
{
*x = 5;
*y = 3;
printf("%d %d\n", *x,*y);
}
3.声明和定义的关系
定义是一种特殊的声明,事实上,如果我已经定义了是没有什么必要再去声明的
4.extern
如果主函数和调用的函数在不同的源文件,我们可以通过extern进行声明
extern void Add(int* x, int* y)
5.额外补充
我们在写项目的时候,绝不是把声明和定义就这样,全部放在主函数的那个源文件上。
我们会自己写头文件和对应的源文件,主函数那边只要把自定义的头文件包含就可以了
#include "add.h" //自定义的头文件用双引号
函数可以嵌套调用(函数里面调用函数),但是不能嵌套定义
可以把函数的返回值作为另一个函数的参数,这个操作叫“链式访问”
5.递归和迭代
5.1 递归
#include <stdio.h>
int is_my_strlen(char* ch)
{
if (*ch != '\0')
{
return 1 + is_my_strlen(ch + 1);
}
else
{
return 0;
}
}
int main()
{
char ch[] = "bit";
int ret = is_my_strlen(ch);
printf("%d", ret);
return 0;
}
5.1.1 定义
递归就是在这个函数里面调用这个函数(自己调用自己)
为了方便理解,可以不要把一个递归函数中调用自己的函数看作是在调用自己,而就当它是在调另一个函数。只不过,这个函数和自己长得一样而已。
思考方式:
(1)把一个复杂问题拆成几个相似的小问题,即大事化小,小事化了
(2)理解什么时候递,什么时候归
5.1.2 递归的条件
- 存在限制条件,满足这个限制条件后,递归就不再继续
- 每次递归调用后越来越接近这个限制条件
5.2 迭代
迭代就是“不是递归的情况”,效率高,但是代码可读性不大
#include <stdio.h>
void is_my_strlen(char* ch)
{
int count = 0;
while (*ch != '\0')
{
count++;
ch ++;
}
printf("%d", count); //指针的进位
}
int main()
{
char ch[] = "bit";
is_my_strlen(ch);
return 0;
}
5.3 递归和迭代的使用场景
递归形式清晰,但是会有栈溢出和运行效率低的问题,如果在写某个代码时,这个问题不是很明显,就用递归,否则就用迭代,或者迭代里面的变量用static修饰,转为静态变量