C语言中存在着大量的库函数,所谓库函数就是C语言已经给我们写好的函数,当然我们也可以自定义函数。这些函数和我们数学中的函数类似,都是为了实现某一功能而被设计出来。本章我们将了解一下这些函数,由于库函数较多,库函数的实现将再以后单独开篇博客来讲。
文章目录
一、C语言中的库函数
在C语言中,有一些功能是我们在日常中频繁会使用到的,因此为了提高开发效率,C语言将这些功能封装成了库函数。比如我们日常用的scanf和printf操作,都是库函数。使用库函数必须引用库函数所在的头文件。
更多的库函数可以通过下面的网站获取:
我们经常使用到的库函数有:
- IO函数
输入和输出函数
- 字符串操作函数
比如求字符串长度的strlen函数
- 字符操作函数
比如判断字符的大小写,将小写字母转化为大写字母
- 内存操作函数
内存复制、查找等操作
- 时间/日期函数
获取时间等
- 数学函数
开平方sqrt等函数
- 其他库函数
1.库函数如何使用
以我们常用的strcpy库函数为例:
该库函数属于string.h这个头文件,因此在使用前要先引用这个头文件
从上面的库函数网站可以找到strcpy的具体用法:
如果看不懂可以翻译一下:
因此在使用这个函数的时候,我们需要向函数参数传入destination和source两个字符串,同时该函数的返回值是char* ,它会将拷贝后的destination起始地址返回给我们。
如以下的代码:
#include <stdio.h>
#include <string.h>
int main()
{
char str1[] = "Sample string";
char str2[40];
char str3[40];
char* str4;
strcpy(str2, str1);
str4=strcpy(str2, str1);
strcpy(str3, "copy successful");
printf("str1: %s\nstr2: %s\nstr3: %s\nstr4: %s\n", str1, str2, str3,str4);
return 0;
}
可以看出str2,str3都得到了拷贝,另外由于它的返回值是char* ,因此我们用一个char* 类型的变量str4接收这个返回值,依然可以打印拷贝以后的字符串str2
二、自定义函数
虽然C语言中的库函数很多,但我们在学习和开发中所需要的功能库函数并不能全部满足,因此就需要我们自定义函数,来封装我们需要实现的功能。
自定义函数的定义语法为:
ret_type fun_name(para1, * )
{
statement;//语句项
return 返回值;//该返回值与返回类型必须相同,如果是void型函数,则不需要返回值,因此可以不写。
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
在使用函数的过程中,我们用函数名(函数参数)的形式来调用自定义函数,由于一般函数在调用完以后产生一个返回值(比如一个两个数相加的加法函数,实现两个数相加以后返回两个数的和),因此我们可以用一个变量来接收这个返回值。
下面是一个可以找出两个整数最大值的函数:
#include <stdio.h>
int get_max(int x, int y) {
return (x > y) ? (x) : (y);//三目操作符,在第一章初识C语言中讲过,如果x>y则返回x,反之则返回y
}
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
printf("max = %d\n", get_max(num1, num2));
return 0;
}
在程序运行过程中,给get_max这个函数传入num1,num2这两个参数,函数调用完后会返回num1和num2中的最大值,因此可以用max来接收这个返回值。当然也可以不用接收,因为在函数运行完以后,get_max(num1,num2)就相当于这个返回值,该返回值可以当做printf的参数直接进行打印操作。
1.函数的参数
实际参数(实参):
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。在进行函数调用时,它们都必须有确定的值,这样才能把这些值传送给形参。
以上面的找出最大值的函数举例,传给函数的num1,num2就是实际参数。
形式参数(形参):
形式参数是指函数名后括号中的变量,在函数调用中会在栈区开辟一块存放传入的实际参数,这块内存中放的参数就是形式参数。形参只是实参的一份临时拷贝。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
以上面的找出最大值的函数举例,函数中的x,y就是形式参数。
由于形参只是实参的一份临时拷贝,因此对形参的修改并不会使实参发生任何改变,具体的内容在下面的函数的调用部分会详细说明。
2.函数的调用
2.1.函数的声明
在调用函数之前,我们必须对函数进行声明,告诉编译器我们有这样一个函数。函数声明的语法与函数的定义去掉{}后的部分相似,只是多了一个分号
ret_type fun_name(para1, * );
ret_type 返回类型
fun_name 函数名
para1 函数参数
如果函数在主函数之前定义的话,定义本身就是一种声明,因此我们不需要进行声明,如果函数的定义是在调用该函数的后面,则需要进行声明。还是以刚才的函数为例:
#include <stdio.h>
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
printf("max = %d\n", get_max(num1, num2));
return 0;
}
int get_max(int x, int y) {
return (x > y) ? (x) : (y);
}
如果不加声明,编译器会找不到这个函数。
加了声明以后就可以正常调用了。
2.2函数的传值调用
相当于将实参拷贝到形参中,对形参的修改不会影响实参。
如果我们想写一个函数来实现对两个数的交换:
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("num1 = %d \nnum2 = %d\n", num1, num2);
return 0;
}
由于形参并不影响实参,函数在调用过程中只是对形参x,y进行了交换,并没有影响到num1,num2;并且函数在调用结束以后,x,y就已经被销毁了。
从监视中就可以看出,交换的仅仅是x,y且x,y的地址和num1,num2不同,他们相当于一块独立的空间,自身的改变并不会影响num1,num2。
2.3函数的传址调用
传址调用是直接把函数实参变量的内存地址传递给函数参数的一种调用函数的方式。
通过这个实参的地址可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操
作函数外部的变量。
我们对上面的交换程序运用传址调用:
#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("num1 = %d \nnum2 = %d\n", num1, num2);
return 0;
}
通过这种方式可以使得变量进行真正的交换。
通过监视可以看出,px和py就是一个指针,其中放的就是num1,num2的地址,*是解引用操作符,它可以通过地址找到地址中存放的变量值,比如px就是num1的指针,它的值是num1的地址,对px解引用就可以找到num1地址中存放的变量1,然后我们就可以对变量1进行操作了。
2.4传值和传址的使用场景
函数内部的形参只需要借用函数外部实参的值的时候用传值调用,比如求两个数的较大值。当函数内部需要对函数外部变量进行操作时用传址调用,比如交换两个数。
2.5函数的嵌套调用和链式访问
2.5.1嵌套调用
函数和函数之间是可以互相调用的。
#include <stdio.h>
void fun2()
{
printf("hello world\n");
}
void fun1()
{
int i = 0;
for (i = 0; i < 3; i++)
{
fun2();
}
}
int main()
{
fun1();
return 0;
}
我们通过main函数调用fun1,通过fun1调用三次fun2,这就是函数的嵌套调用。
2.5.2链式访问
把一个函数的返回值作为另外一个函数的参数。
在上面的程序中:
#include <stdio.h>
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", get_max(num1, num2));
return 0;
}
我们将get_max的返回值作为printf函数的参数,这种方式就是链式访问。
下面有一个有趣的程序:
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
这个程序实际上就是用printf的返回值作为printf的参数,因此想要弄明白这个程序,我们得先知道printf的返回值,通过查找cplusplus.com可以知道printf返回值
所以printf返回值是写入的字符总数,也就是字符的个数。
最内层的printf打印43,第二层的printf打印的是最内层printf的返回值,也就是“43”这个内容的元素个数2,最外层打印的是printf("%d",2)的返回值,返回值是其元素个数1。所以会打印出4321这四个数。
三、函数的递归与迭代
1.递归是什么
递归就是程序自己调用自己的过程,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
1.1递归的条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续,否则递归会不断进行,直到程序崩溃。
- 每次递归调用之后越来越接近这个限制条件。
2.函数的递归
函数的递归就是函数自己调用自己的过程.
如果我们输入一串数字,,我们如何才能在屏幕上从高位向低位输出,比如输入123,输出1 2 3。
我们想要从高位向低位输出,就必须要依次获取到最高位到最低位,因此我们可以这一串数字每次除以10,每次进行判断是否小于10,小于10则不再进行除以10的操作,这样我们就可以得到最高位了,为了获取其他位,我们可以在每次除以10之前进行取模10的操作,得到剩下的位。
#include <stdio.h>
void print(int n) {
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int num = 123;
print(num);
return 0;
}
画图分析一下这个程序:
2.1函数递归中的问题
我们如果用递归来计算第50个斐波那契数:
#include <stdio.h>
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
int num = fib(50);
printf("%d", num);
return 0;
}
运行程序,会发现程序要很长时间才能算出来,因为递归造成了很多重复性的计算。
不难看出,47在这里计算了3次,随着递归越来越小,更小的数会被重复计算更多次。
我们可以用以下的程序来验证:
#include <stdio.h>
int count = 0;
int fib(int n)
{
if (n == 2)
{
count++;
}
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
int num = fib(30);
printf("%d\n", num);
printf("%d\n", count);
return 0;
}
当计算到fib(2)时,count就自增1,可以看到fib(2)一共被计算到了514229次,这种方式效率非常低。
而且,这种方式还容易造成栈溢出:
内存一般分为栈区、堆区、静态区三部分,栈区主要用于存放局部变量、函数的形式参数等等,属于临时分配的空间。
当调用函数时,内存就会在栈区开辟空间,而系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),或者递归数量很大,这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这种情况一般称为栈溢出(Stack overflow)
比如我们计算第1000000个斐波那契额数
为了该防止出现上面这种计算时间过长和栈溢出的情况,我们可以不用递归来写菲波那切数。
我们知道菲波那切数中一个数是前两个数之和,递归是从后往前算,我们可以从前往后算:
#include <stdio.h>
int fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int num = fib(1000000);
printf("%d\n", num);
return 0;
}
这样就不会出现上述的问题了。
3.函数的迭代
迭代就是重复反馈过程的活动,每一次迭代的结果会作为下一次迭代的初始值。
比如我们上面的从前向后算的菲波那切数。
4.函数递归和迭代的区别与优势
- 有些问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
- 有些问题的迭代实现往往比递归实现效率更高,但是代码的可读性稍微差些。
- 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
四、函数的分文件编写
在需要多人处理的大型项目中,如果将函数进行分文件操作,每个人只需完成自己负责的函数,最后将每个人写好的文件进行引用即可,并不需要所有人都写在同一个文件里面,这样将大大加快任务完成的效率。
1.我们一般将函数的声明和函数的定义分开写,声明放在头文件(.h)中,定义放在源文件(.c)中,防止出现编译错误
2.使用函数时需要先引头文件,语法是 #include “头文件名.h”
比如我们要分别实现加减乘除四个函数,我们就可以各自写各自的头文件和源文件,最后在main函数所在的文件中引用即可。
比如下面的加法头文件和源文件:
在test1中引用各自的头文件:
运行结果:
总结
这篇博客带大家了解了C语言中的库函数和自定义函数,我们可以通过这些实现各种各样的功能。
更多函数(比如strlen,strcmp,二分查找函数等)的实现我会在未来开篇博客单独说明。
当然,这篇博客如果有任何错误,或者各位大佬有任何建议,可以在评论区指出和提出,我会对博客进行修改和完善。