文章目录
函数是什么
数学中我们常见到函数的概念。但是你了解C语言中的函数吗? 百度百科中对函数的定义:
C程序是由一组变量或是函数的外部对象组成的。函数是一个自我包含的完成一定相关功能的执行代码段。我们可以把函数看成一个“黑盒子”,你只要将数据送进去就能得到结果,而函数内部究竟是如何工作的,外部程序是不知道的。外部程序所知道的仅限于输入给函数什么以及函数输出什么。函数提供了编制程序的手段,使之容易读、写、理解、排除错误、修改和维护。
打个比方来理解函数:
在一个饭店里面,顾客点了一盘菜后,服务员把该菜的名字给后勤,他负责洗菜这项功能,完毕后把菜交给配菜的师傅,待菜切好后,把菜交给厨师,厨师负责炒菜,完毕后,服务员把菜端给顾客。
在上面那个比方内,有四个不同的,独立的角色:服务员,后勤,配菜师傅,厨师。
他们每一个只是负责给他们指定的任务:
服务员相当于是main() 主函数
,他是整个过程的开始,主函数也被称作程序的接口,我们写的代码都是首先从这里执行下去的。
后勤,配菜师傅,厨师, 相当于三个不同的函数,他们执行各自的功能,互不影响,并且哪里出了问题,很快就可以确定问题的位置。
假如顾客吃到的菜有一只青虫,那我们基本可以确定出问题(bug)的是在后勤部分。
//比喻转载自博客
函数的分类
一、库函数
C语言为我们提供了上百个可调用的库函数,例如与字符串有关的strlen, strcat, strcpy
.或是我们刚接触C语言时候用到的printf, scanf
, 这些都是c语言为我们提供的。在我们使用某一库函数的时候,需要在程序中嵌入(#include<>
) 该函数所需要的头文件。
这也就是为啥我们在代码开头都需要写上#include <stdio.h>
,因为 printf, scanf,getchar,gets,putchar()
这些函数 (也称作标准I/O函数),都是在stdio
头文件中。
如何学习库函数
通过在网站查询学习使用
cplusplus界面:(已翻译)
简单的总结,C语言常用的库函数都有:
- IO函数
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
char * strcpy ( char * destination, const char * source );
void * memset ( void * ptr, int value, size_t num );
二、自定义函数
1.自定义函数的作用
- 方便管理代码,编写程序时思路清晰。
- 代码复用,同一段代码可以在不同位置多次执行。
2.函数的组成
ret_type fun_name(para1, *)
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
例如:
交换两个整数
#include <stdio.h>
void Swap1(int x, int y) {
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
void Swap2(int* px, int* py) {
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
运行结果:
注意:
- c语言一般根据函数的功能来取函数名,全部使用小写单词或者在每个单词首字母大写,多个单词之间用下划线分隔,如:add_to_arr(小写);Add_To_Arr(大写)。
- 参数的个数、类型由函数的功能决定,被调用时由调用者提供。如果函数不需要任何参数则写void,不要空着。如果不写,会有警告,默认函数类型为int。
- 返回值类型根据函数的结果决定,如果不需要返回值则写void。
3.函数的声明和定义
函数声明
返回值类型 函数名(类型 变量名,类型 变量名,…);
例如:
void Swap1(int* px, int* py);
- 实际上,在函数声明时,变量的名称并不重要,只有返回值类型,函数名和变量的类型是编译器必须要知道的,因此变量名称是可以忽略的。
函数定义
返回值类型 函数名(类型 变量名,类型 变量名,…)
{
函数体;
return (数据);
}
例如:
void Swap1(int* px, int* py) {
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
注意:
- 如果函数的定义出现在调用之前,声明可以省略,否则不能省略。
- 函数声明时,参数的变量名可以省略。
4.函数的参数
实际参数(实参)
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类
型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配
内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在
函数中有效。
上面Swap1和Swap2函数中的参数 x,y,px,py
都是形式参数。在main函数中传给Swap1的num1,
num2和传给Swap2函数的&num1,&num2
是实际参数。
- 这里可以看到Swap1函数在调用的时候,x,y拥有自己的空间,同时拥有了和实参一模一样的内容。所
以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。 - 临时变量在栈上开辟空间,如果不初始化,一般是随机值
void
叫做无类型,不能直接定义变量void *
可以定义变量,该变量可以接收任意类型,常用来接收指针类型。但是不能解引用,因为并没有无类型变量。
5.函数的调用
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
比如上面的Swap1
函数。
传址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操
作函数外部的变量。
比如上面的Swap2
函数。
注意
无论是传值或者是传址,都要定义临时变量。传址时定义指针变量。
6.函数的嵌套调用和链式访问
嵌套调用
例如:
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for (i = 0; i < 3; i++)
{
new_line();//在函数中调用函数
}
}
int main()
{
three_line();
return 0;
}
注意
- 函数调用结束返回时按照调用顺序的
反方向
返回。
链式访问
把一个函数的返回值作为另外一个函数的参数。
例如:
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"bit"));
printf("%d\n", ret);
return 0;
}
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));//这里有点难,理解就好
return 0;
}
输出结果:
8
4321
函数递归
1.什么是递归
程序调用自身的编程技巧称为递归( recursion)。 递归做为一种算法在程序设计语言中广泛应
用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复
杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可
描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在
于:把大事化小
简单理解
递归:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开它。若干次之后,你打开面前的门后,发现只有一间屋子,没有门了。然后,你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这你把钥匙打开了几扇门。
循环:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门(若前面两扇门都一样,那么这扇门和前两扇门也一样;如果第二扇门比第一扇门小,那么这扇门也比第二扇门小,你继续打开这扇门,一直这样继续下去直到打开所有的门。但是,入口处的人始终等不到你回去告诉他答案。
//例子为转载,博客
递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
!!!画图解释!!!
后面还有例题解释
2.例题
(1)求字符串的长度
#include <stdio.h>
int MyStrlen(const char* s)
{
if ('\0' == *s) {
return 0;
}
return 1 + MyStrlen(s + 1);//递归实现函数功能
//int len = 0;
//while (*s){
// s++;
// len++;
//}
//return len;//循环迭代实现函数功能
}
int main()
{
char str[] = "abcd!";
int len = MyStrlen(str);
printf("%d\n", len);
return 0;
}
画图解释为:
调用函数时,先判断条件是否满足,不满足后返回到函数开始处,并传入下一个数组元素的地址,开始函数迭代,并不断更新条件,直到满足if语句,返回0,满足迭代出口条件,函数栈帧逐级返回。
(2)计算n的阶乘
#include <stdio.h>
int factorial(int n)
{
//n!->n*(n-1)!->5!(5*4!)
if (1 == n){
return n;
}
return n*factorial(n - 1);//函数递归实现
//int a[100] = { 0 };
//int result = 1;
//while (n) {
// result *= n;
// n--;
//}
//return result;//循环迭代实现
}
int main()
{
int result = factorial(5);
printf("%d\n", result);
return 0;
}
画图解释为:
(3)接受一个整型值(无符号),按照顺序打印它的每一位。
#include <stdio.h>
#include <windows.h>
#pragma warning(disable:4996)
void ShowUint(u_int n)
{
if (n > 9){
ShowUint(n / 10);
}
//1. n<=9
//2. n>9,个位之前的数据按照要求打印完成了
printf("%d ", n % 10);
}
int main()
{
ShowUint(1234);
system("pause");
return 0;
}
画图解释:
注意
- 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
- 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
- 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
- 递归实现问题在处理较大数据时,由于无限次调用自己容易导致栈溢出(
系统分配给程序的栈空间是有限的,在死循环和死递归的情况下,一直在开辟栈空间,最终导致栈空间耗尽
)