本文已收录至《C语言》专栏📖
欢迎多多浏览,点赞👍,收藏🖱?+关注📸
目录
🪧前言?
??正文
💻库函数
💻自定义函数
💻函数的声明和定义?
💻函数的参数?
💻函数的调用?
💻函数的嵌套调用和链式访问
📺函数的嵌套调用
📺函数的链式访问
💻函数递归与迭代
📺函数递归
📺函数迭代
💻递归小题目-青蛙跳台
🖥?总结
?
🪧前言?
我们知道,一个程序由很多条代码组成,但这些代码并不是都写在主函数main中,而是写在各个函数里面。可能有同学又要问,函数是什么?数学告诉我们,函数是求某个值的解法;那么在C语言中,一个函数则是一个解决方案,他是用来实现某个功能的,因为一个程序在启动时,不可能将里面的所有代码全部执行,而是分函数进行调用,这样可以使程序更加模块化,而我们生活中大多数程序都是高度模块化的,所有本章会向大家介绍函数的相关知识。
图片来源:视觉中国
??正文
在编译器中,函数有两类,一类是库函数,另一类是自定义函数。
💻库函数
什么是库函数呢?我们在写程序时,经常会用到各种关键字,这些关键字可以帮助我们减少实现某些功能的代码量,更有益于我们去编译程序。相信scanf和printf这两个关键字大家已经非常熟悉了,这两个关键字包含与stdio.h这个头文件,其实关键字也是一个函数,只不过编译器开发人员已经写好了,我们直接使用就可以。
printf关键字介绍:
?可能有小伙伴好奇,这是什么网站,这里我就向大家推荐一个学习C/C++库函数学习的地方Cplusplus(网站:cplusplus.com - The C++ Resources Network)
这个网站可以查找关于C/C++关键字以及库函数的完整相关信息,但是网页是英文版,不过很多浏览器已经支持翻译网页的功能了,实在不行也可以借助翻译工具进行学习。
printf关键字介绍(浏览器中文翻译):
以上的示例只是一小部分介绍,其实介绍的远远不止于这些,他比你想象中的更详细! |
其实编译器所包含的头文件非常多,功能也很丰富,大家可以在Cplusplus慢慢探索!? |
我们通下面过Cplusplus学习一个库函数:memset
该关键字在编译器中的函数声明:
void * memset ( void * ptr, int value, size_t num );
Ⅰ. 首先可以发现,这是一个指针类型的函数且没有指定返回值类型,返回值类型为void也就是空,但这并不是没有返回值而是可以返回任意类型的指针,该关键字返回的是ptr(也就是我们传入的第一个参数的地址)。
?
Ⅱ. 通过功能介绍我们可以知道,该关键字的作用是将指针ptr里面的前num个字节变为value。
?
Ⅲ. 那么我们就知道了该关键字需要三个什么参数了,首先是一个装有字符串变量的数组或指针,然后就是替换的字符,最后是指定替换从头开始多少个字符。
代码示例:
int main()
{
char str[10] = "ABCDEFG";
memset(str, '#', 3);//将str前三个字符替换为#
printf("str:%s\n",str);
}
memset示例代码运行结果
注? ? ? ? ? ?意
我们库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。使用时如果使用的是编译器自带的头文件则使用< >括起来,而且文件后缀名也要加上;如果是自己写的头文件,那么用" "?引用就可以,这两种引用方式是不同的,区别在于在< >是从编译器自带的头文件区域开始查找并引用,而" "则是从用户的解决方案头文件中开始查找并引用,所以在使用编译器自带的头文件时最好使用< >,而用户自己的头文件则用" ",而且两者不可混搭,编译器自带的头文件既可以使用< >也可以使用" ",但用户写的头文件只能使用" " !
#include < >//库函数头文件引用
#include " "//用户自定义头文件引用
?💻自定义函数
?前面我们介绍了关键字,解释了关键字其实也是一种函数,那么如果我们需要的某些功能库函数没有怎么办?那么就需要我们自己去定义和写出函数,函数中包含算法来解决我们所需要的问题。格式:
//示例
void fun(int sum)//无返回值类型函数fun
{//有一个整型参数sum
printf("sum=%d\n",sum);
}//输出接收的值sum
示例中我们定义了一个void类型的函数,说明该函数为无返回值类型,函数名为fun,有一个整型参数sum,函数作用是打印出接收的整型变量sum。
下面我们进行一个完整的示例!
示例一:实现两个数相加的函数
int add(int x,int y)
{
return x + y;//返回两数的加数
}
int main()
{
int a = 7;
int b = 8;
int c = add(a, b);//整型变量c接收返回值
printf("%d+%d=%d\n",a,b,c);//输出
}
示例二:仿写字符串比较函数,写一个数组比较函数?
int sumax(int x, int y)
{
if (x > y)
{
return 1;//如果x>y返回1
}
else if (x < y)
{
return -1;//如果x<y返回-1
}
else
{
return 0;//如果x==y返回0
}
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d",&a,&b);
switch (sumax(a, b))
{
case -1:
{
printf("a小于b!\n");
break;
}
case 0:
{
printf("a等于b!\n");
break;
}
case 1:
{
printf("a大于b!\n");
break;
}
}
}
💻函数的声明和定义?
函数声明:
1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3. 函数的声明一般要放在头文件中的。
?函数定义:
函数的定义是指函数的具体实现,交待函数的功能实现。 函数的实现中会有语句块以及算法。函数声明中没有语句块!
注? ? ? 意
函数的调用必须在函数已经声明的基础上或者函数写在主函数的前面,因为编译器在编译时是从第一行开始一直到主函数,如果被调函数写在了主函数后面,那么编译器编译到主函数时?后面的被调函数由于还没被读取则编译器不知道被调函数的存在,此时会报错。
//被调函数写在主函数的前面
void add {}//不需要声明
int main()
{
add();
}//直接调用
//被调函数写在主函数的后面
int add();//先声明函数
int main()
{
add();
}//声明后调用
void add {}
💻函数的参数?
接下来我们实现一个函数实现两值的交换,大家猜猜结果是否成功?
//实现两数的交换
void swap(int x,int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 1;
int b = 2;
swap(a, b);
printf("a=%d\nb=%d\n",a,b);
}
?实际参数(实参): 真实传给函数的参数,叫实参。 实参可以是:常量、变量、表达式、函数等。 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值(初始化并赋值),以便把这些值传送给形参。
形式参数(形参): 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在被调函数执行时中有效。
通俗的说:形参实例化之后其实相当于实参的一份临时拷贝?
?
?基于这样的思想,我们如果将a和b的地址传给指针形参,然后解引用进行修改就能对原来的值造成影响了,代码修正:
void swap(int* x, int* y)
{
int tmp = 0;
tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 1;
int b = 2;
swap(&a, &b);
printf("a=%d\nb=%d\n",a,b);
}
?通过调试我们发现,当我们将形参修改为指针,然后传入a和b的地址时,形参x和y的地址与a和b相同,那么形参的改变就会影响实参
所以如果我们的被调函数要对实参进行修改就必须使用指针在地址上进行操作,否则将无法修改。
💻函数的调用?
基于前面我们对参数的介绍,我们发现函数调用以及传递参数的方式有两种: ? 1. 传值调用:函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。 ? 2. 传址调用:传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操 作函数外部的变量。 |
💻函数的嵌套调用和链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。这样可以使程序运行的逻辑更加紧密清晰!
📺函数的嵌套调用
?被调函数之间是可以相互调用的,这样可以使程序功能多元化!
示例:嵌套函数返回形参加1
int test(int i)
{
return i + 1;
}
int add(int i)
{
return test(i)+1;
}
int main()
{
int i = 3;
printf("%d\n", add(i));
return 0;
}
?📺函数的链式访问
函数的链式访问就是把一个函数的返回值作为另外一个函数的参数。
示例:输出scanf和printf关键字的返回值
int main()
{
char arr[10] = {};
int a = 0;
int b = 0;
printf("scanfarr:%d\n",scanf("%s",arr));
printf("scanfab:%d\n",scanf("%d%d",&a,&b));
printf("printfarr:%d\n", printf("%s\n", arr));
printf("printf:%d\n", printf("Hello C!\n"));
return 0;
}
💻函数递归与迭代
📺函数递归
1.函数递归是程序调用自身的编程技巧。 递归做为一种算法在程序设计语言中广泛应用。
?
2.一个过程或函数在其定义或说明中有直接或间接调用自身的 一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略只需少量的代码就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
?
3.递归的主要思考方式在于:把大事化小!
?
4.函数递归的必要条件:一是存在限制条件,当满足这个限制条件的时候,递归便不再继续;二是每次递归调用之后越来越接近这个限制条件。
示例:利用函数递归打印出1-10之间的数字
void sum(int i,int k)
{
if (i<=k)
{
printf("%d ", i);
sum(i + 1,k);
}
}
int main()
{
sum(1,10);
return 0;
}
?程序递归过程:
?
先递归还是先执行其他语句其实现效果是不一样的,我我们调换一下printf和递归的位置再试试
void sum(int i,int k)
{
if (i<=k)
{
sum(i + 1,k);
printf("%d ", i);
}
}
📺函数迭代
我们讨论了递归之后,现在我们继续讨论另一个例子,用递归求斐波那契数,示例:
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
可以看出,这是一个特殊的递归,我们画图进行理解
我们观察,发现斐波那契数的计算在程序中产生了大量的重复计算,看似简单的代码实则非常占用性能。
而且在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃,因为栈溢出了。?
??在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出) 这样的信息。
??系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一 直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
如果我们在斐波那契程序中加一个计数的变量,可以发现程序计算了很多次!
?我们用全局变量_count计数3的斐波那契数被重复计算了多少次,可以发现,被计算了21次!已经超过我们当初传入的参数10了。
这种情况下,只能说明一个问题,这个功能不适合用递归去实现,而更好的方法就是非递归的迭代方法,也就是用循环去实现这个功能。
使用static(修饰为静态)对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不 仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保 存递归调用的中间状态,并且可为 各个调用层所访问。
我们将斐波那契数的计算修改为迭代算法:
int fib(int n)
{
int m;
int a;
int b;
m = a = 1;
while (n > 2)
{
a -= 1;
b = n;
a = m;
m = a + b;
}
return m;
}
>分? ? 析< ? 1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。 2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。 3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。 |
💻递归小题目-青蛙跳台
📝问题:一只青蛙一次可以跳上 1 级台阶,也可以跳上2 级。求该青蛙跳上一个n 级的台阶总共有多少种跳法?
//青蛙跳台
int sum(int s)
{
if (s == 1)
{
return 1;
}
else if (s == 2)
{
return 2;
}
return sum(s - 1) + sum(s - 2);
}
int main()
{
int s = 0;
scanf("%d", &s);
printf("跳法%d\n", sum(s));
}
?🖥?总结
本次我们对函数进行了相对详细的介绍,我们介绍了编译器有很多库函数可以帮助我们实现多元化的功能,同时也可以自定义函数来帮助我们实现某种特定功能,最后介绍了函数的递归可以减少代码量使代码非常整洁但是也有一定不足有时候也需要用迭代。
本次函数的知识分享就暂时先到这里啦,喜欢的读者可以多多点赞收藏关注。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
🌟其他文章本栏推荐🌟
C语言入门<操作符>_ARMCSKGT的博客-CSDN博客
?C语言入门<分支语句>_ARMCSKGT的博客-CSDN博客
?C语言入门<循环语句>_ARMCSKGT的博客-CSDN博客
🌹欢迎读者多多浏览多多支持!🌹
?
?
?
|