一、什么是函数
函数是完成特定任务的独立程序代码单元,一般会有输入参数和返回值,还会提供过程的封装和细节的隐藏。一个实用的c程序一般由若干个函数组成。除了C语言提供的库函数之外,也可以根据你想要的功能,由用户自己编写想要的函数(比如main()函数是自己编写的)。
C语言中有库函数和自定义函数。
二、库函数
C语言本身不提供库函数,C语⾔的国际标准ANSIC规定了⼀些常⽤的函数的标准,被称为标准库,不同的编译器⼚商根据ANSI提供的C语⾔标准就给出了⼀系列函数的实现。这些函数就被称为库函数。(所以找到工作后每个公司提供的库可能会不一样哦)这些函数实现一些基础功能(如打印printf),每个程序员都可能用到,方便进行软件开发。其实在早期没有这么多的库函数(比如printf),但是有些功能呢大家都在使用,为了统一规范方便使用,所以就有了库函数。
关于库函数的使用可以查询下面的网站,博主使用的是第一个网站。
一些常用库函数:
IO函数:如printf,getchar
字符串操作函数:如strlen
内存操作函数:如memset
时间/日期函数:如time
数学函数:如sqrt
......
例:字符串处理头文件<string.h>中的函数:
1.strcpy-string copy(复制字符串给另一个)
由下面解释可以看出将来源的字符串复制到目的地,也就是说strcpy( ,)括号里面逗号前面是存放复制字符的变量,逗号后面放被复制字符的变量。返回值是目的地地址,也就是左边的地址。说的云里雾里,且看实操。
下面的代码的作用是将arr2的字符串拷贝下来放到arr1里面(把右边的字符串复制到左边),注意这里arr1的字符串长度要大于arr2,arr1你字符串长度不比arr2长,怎么放下人家的字符串(笑)。咳咳,想要放下人家的字符串就要比人家长!
注意:字符串的结束标志是\0,所以arr2里的\0也拷贝到arr1里面。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "you are my diamond !! ";
char arr2[] = "you are my priority";
strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
看到运行结果有没有这样一个疑惑:既然是复制替换过去,那打印的结果应该是you are my priority!! (这里有两个空格)
前面说过这个函数会把\0一并复制过去,存在arr1里面就是:you are my priority\0!
由于\0是字符串结束标志,printf遇到\0就会停止打印了。
2.memset-memory set
什么意思呢?
就是把ptr所指那块空间的前num个字节的内容设置为我们所指定的value这个值。
prt:要填充的内存块
value:要填充的值
num:要被填充的个数
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "I am a lucky dog!";
memset(arr1, ' ', 5);//前五个字符被替换成空格
printf("%s\n", arr1);
}
三、自定义函数
前面说了,库函数提供的是常用的、实现基础功能的函数。自定义函数能实现更多的功能,使代码具有创造性。自定义函数和库函数一样,具有函数名、返回值类型、函数参数,不同之处在于这些需要我们自己设计。自己设计?那带来的可操作性就大了去了。
下面看一个简单的自定义函数:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a, b, c;
printf("请输入数字a,b:\n");
scanf("%d%d", &a, &b);
c=Add(a, b);
printf("c=%d", c);
return 0;
}
这里面的Add是自定义函数,功能显而易见:实现两个数相加求和。
Add是函数名
x、y是函数参数
int是返回类型
注意由于这个函数实现求和,求出来和之后肯定需要给人家啊所以需要返回值也就是和。
而有的函数不需要所以返回类型是void,如:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
void swap(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
}
int main()
{
int a = 10, b = 20;
printf("a=%d b=%d\n", a, b);
swap1(a, b);
printf("a=%d b=%d\n", a, b);
return 0;
}
你是不是以为输出结果是a和b的值互换?nonono,两个输出结果都是a=10,b=20。
为什么呢?
a声明后有自己的地址,在内存中有自己独立的内存空间,存放着10。b声明后有自己的地址,在内存中有自己独立的内存空间,存放着20。swap函数接收a和b的值也需要声明变量x、y来存放,为了存放a、b的值,x、y在计算机内存中需要申请自己的内存空间。都是自己重新申请了,那肯定a和x的内存空间不是同一个,b和y的内存空间不是同一个。所以你最后打印a和b的值还是a=10 b=20.
怎么办呢?
既然他们的内存空间变了(也就是地址不一样),那我们通过使用解引用地址赋值不就行了?a和b地址是不会变的,把他们的地址放入指针变量x和y里面,通过解引用x和y进行赋值。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
void swap(int* x, int* y)//不需要返回值
{//指针变量x和y分别存放a和b的地址
int z = 0;
z = *x;
*x = *y;
*y = z;
}
int main()
{
int a = 10, b = 20;
printf("a=%d b=%d\n", a, b);
swap2(&a, &b);
printf("a=%d b=%d\n", a, b);
return 0;
}
四、函数的参数
函数参数分为实际参数(实参)、形式参数(形参)
实际参数:
函数调用时,函数名后面圆括号里面的是实参列表,实参可以是常量、变量、表达式、函数。
形式参数:
函数定义时,函数名后面圆括号里面的是形参列表,每个形参有类型和名称两部分组成,各形参之间用逗号隔开。
看完概念后感觉还是很抽象,其实是这样理解的:
在我们定义函数的时候,计算机不给形参分配存储单元,形参也没有具体的值(这句话很好理解,因为我们仅仅只是声明了参数)。只有当函数被调用时,形参被计算机暂时分配了存储单元,来存储调用函数时传过来的实参,一旦函数结束运行,计算机释放相应存储单元(过河拆桥,收回你的地盘,你没有利用价值了)。因此,形参只有在函数内部有效。 函数调用结束返回主函数后则不能再使用该形参变量。 可以理解为函数调用结束后形参被销毁了,不能用了。
形参和实参的功能是作数据传送。发生函数调用时, 主调函数把实参的值传送给被调函数的形参从而实现主调函数向被调函数的数据传送,形参值的改变不会改变实参的值(这就解释了上面a、b的值为啥无法交换)。这个过程只能单向。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x, int y)//定义函数
{
int z = x + y;
return z;
}
int main()
{
int a, b, c;
printf("请输入数字a,b:\n");
scanf("%d%d", &a, &b);
c=Add(a, b);//调用函数
//c=Add(a, Add(a,b));实参可以是函数,有种套娃的感觉
printf("c=%d", c);
return 0;
}
看这段代码,参数x和y都是形式参数,a和b都是实际参数。scanf读取用户输入的数据后,然后赋值给a和b,在调用函数Add后将数据传递给形参x、y。
函数的调用:
传值调用:
例如swap1函数,形参的改变不会影响实参。
传址调用:
例如swap2函数,把主函数内创建的变量的地址传递给被调函数的参数,实现被调函数的参数操作主函数的变量。
1.数组作为函数参数
实操一下吧:函数实现在有序数组中用二分查找到某一个数字,并打印其下标。
相信很多小伙伴是这样写代码的,但打印结果确是:找不到指定的数字
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int binary_search(int arr1[], int num)
{
int sz = sizeof(arr1) / sizeof(arr1[0]);
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = (left + right) / 2;
if (arr1[mid] < num)
{
left = mid + 1;
}
else if (arr1[mid] > num)
{
right = mid - 1;
}
else
return mid;
}
return 0;
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int num = 8;//num为想要找的数字
int ret=binary_search(arr1, num);
if (ret== 0)
{
printf("找不到指定的数字\n");
}
else
{
printf("找到了,下标是%d\n", ret);
}
return 0;
}
这是为什么呢?
我们在主函数中数组arr1传参到自定义函数中,但是这里有一个问题:
我们知道如果数组里面包含的元素如果有成千上万个,那么传参到自定义函数中岂不是还要申请大量内存单元存储?这势必造成资源大量浪费,谁愿意当大冤种?所以数组在传参的时候仅仅传过去的是数组第一个元素的地址。
那为什么传参的时候只能传首元素地址,后面却又能访问其他元素呢?
数组是一块连续的空间,它里面会存放很多的元素。记住了,是连续存放的,都有了首地址,计算机自然就知道其他元素了。
就比如上面自定义函数的代码中:arr1[mid],你知道首地址了,又是个int型,地址直接可以算出来:首地址+(mid-0)*4
不得不说真是巧妙的利用方法。
所以在自定义函数中,arr1[ ]你以为是一个数组,其实他是一个指针变量。
所以在求元素个数时,其实是两个整型变量所占内存空间的相除,也就是等于一。
怎么解决呢?
既然在自定义函数里面元素个数求不出来。那我们在主函数里面求出来,再传参到自定义函数里面不就行了吗?
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int binary_search(int arr1[], int num, int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = (left + right) / 2;
if (arr1[mid] < num)
{
left = mid + 1;
}
else if (arr1[mid] > num)
{
right = mid - 1;
}
else
return mid;
}
return 0;
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10};
int num = 8;//num为想要找的数字
int sz = sizeof(arr1) / sizeof(arr1[0]);
int ret=binary_search(arr1, num,sz);
if (ret== 0)
{
printf("找不到指定的数字\n");
}
else
{
printf("找到了,下标是%d\n", ret);
}
return 0;
}
五、函数返回值
函数的返回值通过return语句来实现。下面是return语句的注意事项:
return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执⾏表达式,再返回表达式 的结果。
return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。
return返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。
return语句执⾏后,函数就彻底返回,后边的代码不再执⾏。
如果函数中存在if等分⽀的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
六、函数的嵌套调用和链式访问
一、函数的嵌套调用
C语言不能嵌套定义函数,但是可以嵌套调用函数。
简单的例子:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fun2()
{
printf("现在我在fun2里面\n");
}
void fun1()
{
printf("现在我在fun1里面\n");
fun2();
}
int main()
{
printf("现在我在主函数里面\n");
fun1();
printf("现在我回到主函数里面了\n");
return 0;
}
二、函数的链式访问
其实就是原本实现一个功能可以由多行代码实现,而在这里需要精简代码,甚至一行代码就能实现功能。
例如:
printf("%d\n",strlen("abcd"));
再看一个:
怎么样?你以为的输出结果是什么?
第一个是15应该没有疑问,但为什么后面是3和2呢?
后面两个打印的是前一个printf函数的返回值,那么是什么呢?
看第一句话,意思是返回字符的个数。15是两个字符加上一个换行字符正好3个,3是一个字符,加上一个换行字符,正好是2.
七、函数的声明和定义
函数的声明和定义不是同一个概念。
一、函数的声明
函数的声明是指在程序编译的时候,对你这个函数的合法性进行调查,它告诉编译器你这个函数的名字、返回类型以及形参的类型、个数、顺序。函数的声明一般放在头文件里面 。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a, b;
printf("请输入数字a,b:\n");
scanf("%d%d", &a, &b);
int c=Add(a, b);
printf("c=%d", c);
return 0;
}
这里面 int Add(int x, int y) 就是函数的声明。声明,声明从这个词的意思来看就能理解。就好比你告诉人家你有一个东西,这个东西干嘛就先别管了,我就告诉你我有一个东西而已。
二、函数的定义
函数的定义就是交代函数的功能,是对函数的具体实现。
比如上面代码中的:
int Add(int x, int y)
{
int z = x + y;
return z;
}
注意声明要放在定义的前面。
这么看起来怎么有重复的,有点晕啊。没事我给你梳理一下。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x, int y);//这一行是函数的声明
int main()
{
int a, b;
printf("请输入数字a,b:\n");
scanf("%d%d", &a, &b);
int c=Add(a, b);
printf("c=%d", c);
return 0;
}
//下面的代码是函数的定义
int Add(int x, int y)
{
int z = x + y;
return z;
}
这种写法比较啰嗦,因为声明和定义可以放在一起。
但,真的吗??
存在即合理!!!
有这个东西肯定别的地方需要。记得我们前面说过函数的声明一般放在头文件里面这句话吧。
下面有一个概念:
模块化编程
功能拆分,提高编程效率。
这样我们就可以把函数的声明放在Add.h文件里面,把函数功能的实现放在Add.c文件里面。
需要强调的是上面的头文件(Add.h)是不规范的应该改为这样(不改也行,这是适用于大项目的):
#ifndef _ADD_H_
#define _ADD_H_
int Add(int x, int y);//函数的声明
#endif
至于为什么这样写,且听我徐徐道来:
我们在写代码的时候有#include<stdio.h>,当执行到这一步的时候,编译器会把stdio.h里面的内容全部copy过来 ,当我们模块化编程的时候,我要引用这样的头文件,你也要引用,大家都要引用,在一个工程里面引用了好多次,这样就产生了重复。
怎么避免呢?
需要在头文件里面加入这样的语句:
#ifndef _ADD_H_
#define _ADD_H_
...............
#endif
其中_ADD_H_是根据头文件的名字写的,然后加上下划线。
这几句话什么意思呢?
#ifndef _ADD_H_
ifndef-if not define
意思是:如果没有定义过_ADD_H_,执行下面的代码。
这样一来,当有人在此引用这个头文件的时候,由于之前定义过,所以头文件里面的代码将不再执行,防止重复。
八、函数的递归
在调用函数的过程中又出现直接或者间接调用该函数本身的情况称为递归。(可以理解为我调用我自己)
它可以把一个大型的、复杂的问题转化为与原问题相似的、规模较小的问题来解决。
递归只需要少量程序就可以描述解题过程中所需要的多次重复计算,大大的减少了程序的代码。
其实就是很普通的复杂问题简单化的思想。
函数的递归有两个限制条件:
1.递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续。
2.每次递归调⽤之后越来越接近这个限制条件。
先来一个简单的例子:
#include <stdio.h>
int main()
{
printf("haha\n");
main();
return 0;
}
这个程序会死循环打印haha然后出现错误。
这是为什么呢?
这是递归的常见错误:栈溢出。
浅讲一下:
每次调用函数的时候都会向内存申请空间,在C语言中内存一般划分为三个区域:栈区、堆区、静态区。
在写代码的时候要创建变量,局部变量创建申请的内存空间在栈区上面(还有函数的形参);
堆区里面放的是动态开辟的内存(比如malloc、calloc);
全局变量的创建放在静态区里面(包括static修饰的变量)。
函数的调用都需要从栈区里面申请内存空间。
我们调用main函数的时候就要从栈区里面分配一块内存空间,调用printf函数的时候也从栈区分配一块内存空间,接下来又遇到main函数又要从栈区分配一块内存空间。然后陷入死循环,不断从栈区里面申请空间,当空间耗尽的时候,就出现错误:stack overflow(栈溢出)
程序员的知乎:Stack Overflow - Where Developers Learn, Share, & Build Careers
再看一个例子:
使用函数递归实现打印一个数的个位十位等等
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void print(int a)
{
if (a > 9)//限制条件
{
print(a / 10);//调用,传递123//调用,传递12 //调用,传递1//打印1,这个1第一个打印出来
}
printf("%d ", a%10);
}
int main()
{
unsigned int num = 0;
scanf("%d", &num);//假设输入1234
print(num);//看清楚,这是自定义函数
return 0;
}
仔细感受揣摩一下,感觉妙极了,比for循环还要好使。使用监视器分析更直观。
一环套一环,这一环还没结束,就要到下一环,直到最后一环解决才能回到上一环,然后回到上上一环…emmm,梦中梦?盗梦空间?
再看一个例子:
计算字符串字符个数
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int the_strlen(char* str)
{
if (*str != '\0')
return 1 + the_strlen(str+1);//可以换成++str
else
return 0;
}
int main()
{
char arr[] = "abcd";
int len = the_strlen(arr);
printf("len=%d\n", len);
return 0;
递归刚开始学有点难度,想不出来正常,多做题才是王道。
那再来一个吧:
递归求n的阶乘
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int Fac(int m)
{
if (m > 1)
{
return m * Fac(m - 1);
}
else
return 1;
}
int main()
{
int n = 0;
printf("请输入需要阶乘的数字:");
scanf("%d", &n);
int ret = Fac(n);
printf("%d!的结果是%d\n", n, ret);
return 0;
}
最后一个例子:
求第n个斐波那契数
补一个斐波那契数列的概念:前两个数之和等于第三个数。
1,1,2,3,5,8,13,21,34,55,89....
分析结构可以发现
又是一个我调用我自己,下意识想到递归。
然后这样写:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Fib(int m)
{
if (m <= 2)
{
return 1;
}
else
{
return Fib(m - 1) + Fib(m - 2);
}
}
int main()
{
int n = 0;
int ret = 0;
scanf("%d", &n);//想找到的第n个数
ret = Fib(n);
printf("ret=%d\n", ret);
return 0;
}
这个程序如果算小于40的没什么,但你试一试算第50个数?要大几分钟吧!
这是怎么一回事呢?
算第50个数需要知道第49个数和第48个数,第49和第48又不是凭空出现的,所以他们还要计算,然后大致是下面这样,算的数呈指数级增长(而且算的还有重复的):
50
49、48
48、47;47、 46
47、 46;46、 45;46、 45;45、 44
.......
实验一下:我们看看算第40个斐波那契数的时候,第4个数被重复计算了多少次。
两千多万次!!这造成了多大的浪费!
可见有的问题不能用递归来解决,这种情况用迭代(也就是循环)更好。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Fib(int m)
{
int i = 0;
int a=1, b=1, c=0;
if (m <= 2)
{
return 1;
}
else
{
for (i = 3; i <= m; i++)
{
c = a + b;
a = b;
b = c;
}
return c;
}
}
int main()
{
int n = 0;
int ret = 0;
scanf("%d", &n);//想找到的第n个数
ret = Fib(n);
printf("ret=%d\n", ret);
return 0;
}
这样就解决重复计算的问题了。
关于递归问题的深入了解:
在C语⾔中每⼀次函数调⽤,都要需要为本次函数调⽤在栈区申请⼀块内存空间来保存函数调⽤期间的各种局部变量的值,这块空间被称为运⾏时堆栈,或者函数栈帧。函数不返回,函数对应的栈帧空间就⼀直占⽤。所以如果函数调⽤中存在递归调用的话,每⼀次递归函数调⽤都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。所以如果采⽤函数递归的⽅式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack over flow)的问题。
(关于递归,可以试着解决汉诺塔问题和青蛙跳台问题)