1. 函数是什么
2. 库函数
3. 自定义函数
4. 函数参数
5. 函数调用
6. 函数的嵌套调用和链式访问
7. 函数的声明和定义
8. 函数递归
一、什么是函数
在数学中有函数,在C语言中也有函数,我们直接先给出一个定义:
在基维百科中函数被定义为子程序:
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组
成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
例如:main函数、printf打印函数等等,都是C语言中的函数。
我们可以把函数比作一个工厂,把东西送进去加工,出来就会变成你想要的产品。
函数的基本构成:函数名、返回类型、函数参数和函数体
在C语言中,函数有两大类,库函数和自定义函数,下面一一介绍。
二、库函数
1.来源:C语言的基础库中提供了一系列库函数
2.库函数:
将函数比作加工厂,那么库函数就是已经搭建好的有专属名字的工厂。我们只需要把原材料(数据)送入该工厂,然后接收产品(结果)即可。我们举个例子:
- 计算字符串的长度:
char arr[]="love";//用数组来存放字符串
#include<stdio.h>
#include<string.h>
int main()
{
char arr[]="love";//定义的数组(原材料)
int ret=0;//定义一个变量记录字符串的长度(接收产品)
ret strlen(arr);//送入工厂
printf("%d\n",ret);//printf也是库函数
return 0;
}
我们很清楚的看到,只需要把数组名字arr放入strlen函数中,再用变量ret接收产品就可以。
- 数组名就是首元素的地址,通过首地址就可以找到整个字符串的内容,所以只需要把arr送入函数里面就可以。
注:
但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。所以为什么会有#include<stdio.h>(main函数、printf函数等函数需要包含的)、#include<string.h>(strlen函数需要包含)这些东西。下面我们通过库函数的分类大概确定每种库函数需要使用头文件的类别
3.库函数分类
一系列功能相近的函数
- IO函数
输入输出函数---<stdio.h>
- 字符串操作函数
strlen strcpy
- 字符操作函数
判断字母大小写、转换等等
- 内存操作函数
- 时间/日期函数
- 数学函数
<math.h>,如:Pow
- 其他库函数
4.库函数举例
pow函数:用来计算次方,如:2^3=8
函数原型
double pow (double base, double exponent);//函数原型
参数类型:该函数可以计算小数的幂,精度更高,所以也可以计算整形的变量。所以得出来的结果最高可以为小数,所以返回值是double类型。
函数参数:因为可以计算小数,所以参数的类型也可以是double类型。
第一个参数(double base)是底数,第二个参数(double exponent)是幂。
所以我们只需要把原材料(要计算的数)送到函数里面(工厂)就可以得出结果(产品)
用法:我们现在要计算2^3的结果,只需要送进函数里面就行。
#include<tsdio.h>
#include<math.h>//pow函数需要包含的头文件
int main()
{
int n=0;//创建一个变量用于接收计算的结果
//n=pow(2,3);这样也可以,不过编译器会有警告
n=(int)pow(2,3);
printf("%d\n",n);
return 0;
}
(int)为强制转换类型,转换为整形
总结:pow是一个可以计算次方的库函数,需要的头文件是#include<math,h>
strcpy函数:把A中的字符串复制到B中
函数原型:
char * strcpy ( char * destination, const char * source );//函数原型
返回值类型:返回目标的地址,所以需要用指针接收,又因为是字符串操作函数,所以char
函数参数:第一个参数(destination)是目的地指针(收获地址),就是被复制进去的
第二个参数(source)是源头指针(发货的地方),要复制的值
注意:复制的时候会自带一个\0(字符串结束的标志),但是原数组的\0后面的内容还存在,只是不打印。
用法:
#include<stdio.h>
int main()
{
char A[] = "#############";
char B[] = "love you";
printf("交换前A:%s\n", A);
printf("交换前B:%s\n", A);
strcpy(A, B);//过程是从B到A
printf("交换后B:%s\n",A);
return 0;
}
因为数组名就是首地址,所以不需要传地址。
总结:strcpy是字符串复制函数,第一个参数是被复制参数,第二个是复制参数
menset函数:初始化函数,用于内存设置
void * memset ( void * ptr, int value, size_t num );//函数原型
函数参数:第一个参数(ptr)是操作目标;第二个参数(value)是初始化的内容,第三个参数(num)是操作数目
用法:
#include<stdio.h>
int main()
{
char arr[] = "love you";//原数组
printf("%s\n", arr);
memset(arr,'6',3);操作之后
printf("%s\n",arr);
return 0;
}
三、自定义函数
前言:将函数比如工厂,库函数就是官方已经制作好的工厂,那么自定义函数则需要我们自己去构造工厂然后自己使用,因为工厂还没有制作好,所以名字我们可以自己起。
1.自定义函数的组成
函数名字+返回值类型+函数参数+函数体
对比库函数来看,自定义函数比库函数多了函数体部分,该部分就是工厂的功能,需要我们自己实现。
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
这里的语句项就是函数体
2.自定义函数举例
(1)写一个函数实现两个数找最大值(传值)
#include <stdio.h>
//get_max函数的设计
int get_max(int x, int y)
{
return (x > y) ? (x) : (y);
}
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
}
(2)写一个函数把两个整数的值交换
我们先看错误的写法
原因:把num1和num2的值传给形参x和y,所以x和y只是复制了一份数据。我们在复印件上面修改数据,是不会影响原件的。
我们看正确写法:
#include <stdio.h>
//正确的版本
void Swap2(int* px, int* py)//接收实参的时候,形参是指针变量
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap2(&num1, &num2);//注意这里是&num1,&num2
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
结果:
原因:把变量num1和num2的地址传递给形参,形参通过地址就可以找到源头,进而直接交换成功。这个称为传址调用
可以这么理解1:我们把户口的网址传递给别人,别人可以通过登录该网址找到我们的户口,而直接修改;然后是我们把户口复印件给别人,别人在复印件上面修改对我们的户口是没有影响的。
实质理解2:通过变量的地址可以找到该变量在内存中存放的位置,该位置就是变量数据所在地,直接在所在的修改数据从而达到修改的效果。
四、函数参数和函数调用
1.函数参数:实际参数(实参)和形似参数(形参)
实际参数:
真实传递给函数的参数,叫实参。可以是变量、表达式、常量和函数,但是必须有确定的值。
举例:
Max(520,num1)//520是常量,num1是变量
Max(3+5,&num1)//3+5是表达式,&num1是变量
Max(printf("%d",num1))//函数形式
形式参数:
用来接收实参。是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元)。只存在于函数中,出函数范围自动销毁。
举例:
2.实参和形参的内存
注:实参和形参所在的内存单元不一样,各自独立
我们用代码配图说明:
我们在编译器上面查看: 所以,形参是在函数体中创造的,只存在于函数中,出了函数自动销毁
3.函数调用
传值调用和传址调用
- 传值调用:函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参
- 传址调用:传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
传值调用:
#include <stdio.h>
//get_max函数的设计
int get_max(int x, int y)
{
return (x > y) ? (x) : (y);
}
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);//传值调用
printf("max = %d\n", max);
}
传址调用:
#include <stdio.h>
//正确的版本
void Swap2(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap2(&num1, &num2);//传址调用
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
五、函数链式访问和嵌套使用
1.嵌套调用
#include <stdio.h>
void new_line()
{
printf("hehe\n");//在new_line函数内部又调用了printf函数
//称为嵌套调用
}
void three_line()
{
int i = 0;
for(i=0; i<3; i++)
{
new_line();//在three_line函数内又嵌套调用new_line
}
}
int main()
{
three_line();
return 0;
}
函数可以嵌套调用,互相调用,但是不能嵌套定义
void test()
{
//int Add(int x,int y);//在函数内部嵌套定义
Add(x,y)//这样子就可以
}
int main()
{
test();
return 0;
}
2.链式访问
一个函数的返回值作为另一个函数的参数
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"bit"));//strcat的返回值作为strlen的参数
printf("%d\n", ret);
return 0;
}
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));//结果4321
//注:printf函数的返回值是打印在屏幕上字符的个数,打印几个,返回几
return 0;
}
六、函数的声明和定义
1.函数声明
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
- 声明!=定义
举例:正常写法
#include<stdio.h>
int Add(int x,int y)//该函数的位置在mian函数前面
{
return x + y;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d",&a,&b);
int sum = Add(a, b);
printf("%d\n",sum);
}
警告编译写法:
原因:因为编译器自上往下阅读,当调用Add函数的时候,前面没有遇到过Add函数,所以会有警告,当运行到后面才发现该函数。
正确做法:在调用该函数前面先声明
int Add(int x,int y);//声明函数写法
2.函数定义
函数的定义是指函数的具体实现,交待函数的功能实现。(函数体)
注:函数的定义是一种特殊的声明。
举例:
int Add(int x, int y)
{
return x + y;
}
这一整部分就是函数的定义
七、函数递归
前言:函数递归是函数部分较难点,用较少的代码表示庞大的过程。
1.我们看官方的定义;
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接
调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
总结:自己调用自己,把大事化小。
2.递归的两个条件
因为递归的思想是先把大块的事情不断分成小块的事情,当分到不能再分的时候(条件1),开始从最小块的事情往大块的事情执行,没执行一次,就会更加接近这个条件(条件2)
官方:
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续
- 每次递归调用之后越来越接近这个限制条件
3.举例
题目:接受一个整型值(无符号),按照顺序打印它的每一位。
例如:输入1234 打印结果1 2 3 4
我们先给一个知识点:一个整数n/10=去掉个位剩下的数,如:1234/10=123
一个整数n%10=个位,如:123%10=3
分析:我们发现,要求打印1 2 3 4,不能使用常规方法;如果是打印4 3 2 1的话,就可以写一个循环,对1234连续取模(%)4次就可以了,但是现在顺序反了不可以。
要打印1,需要1%10=1,打印2,12%10=2,打印3,123%10=3,打印4,1234%10=4
我们可以发现:输入1234,需要拆成1,12,123和1234从小的开始计算。
换个思想就是:打印1 2 3 4就可以拆成这样子:打印(1 2 3)和4,然后打印1 2 3又可以拆成打印(1 2)和3,打印1 2可以拆成打印1和2;1此时不能再拆,这就是限制条件1。很显然,这种思想就是递归的思想。
思路图:
代码实现:
#include <stdio.h>
void print(int n)
{
if(n>9)//条件1
{
print(n/10);//不断调用自身
}
printf("%d ", n%10);//条件2
}
int main()
{
int num = 1234;
print(num);
return 0;
}
解析:只要满足n>9的条件就不断调用,直到不满足便开始打印。
代码图解:
显而易见:递归把大事化小,用最短的代码表示最复杂的过程
函数递归是一个重难点,需要加大练习。后续会持续补充递归的内容;有时候递归太复杂,可以拆成非递归,也就是迭代。