提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
提示:这里添加了本文要记录的大概内容:
在编程的世界里,C语言以其高效、灵活和可移植性强的特点,一直占据着举足轻重的地位。作为一门通用编程语言,C语言不仅在底层系统开发、嵌入式开发、游戏开发等领域有着广泛的应用,同时也是学习其他编程语言(如C++、Java等)的重要基础。
在C语言的编程实践中,函数是一个至关重要的概念。函数是组织代码的基本单位,它能够将一段具有特定功能的代码封装起来,并通过特定的接口(参数和返回值)与外部进行交互。这种模块化的编程方式不仅提高了代码的可读性和可维护性,也便于代码的重用和共享。
今天,我们就来深入探讨C语言中的函数。本文将详细介绍函数的定义、调用、参数传递、函数的递归等相关概念,并通过实际案例展示如何在编程中合理使用函数。同时,我们还将分享一些在函数编程中常见的技巧和注意事项,帮助大家更好地掌握C语言函数编程的精髓。
在接下来的内容中,我们将逐步揭开C语言函数的神秘面纱,希望能够帮助读者更好地理解函数的本质和用法,为日后的编程实践打下坚实的基础。
提示:以下是本篇文章正文内容,下面案例可供参考
一、函数是什么
相信许多小伙伴在之前学习数学的时候以及学习过函数的相关概念,函数用通俗的语言来讲就是每个x对应了一个确定的y值,可以一对一或者多对一;但是不能一对多。用张宇老师的“铅垂画线法”来说就是在平面画一条垂直的直线,如果与图像只有一个交点,那说明就是函数!
- 那c语言里面的函数是什么呢?
来,我们baidu一下得到以下结果:
C语言的函数是完成特定功能的代码块,可以被程序中的其他部分调用。
- 为什么会有函数?
我们思考一下,如果你想要实现两个数相加,当你不知道函数的时候,是不是只能重复的写int c=a+b;但是如果有函数,我们只需要每次想要实现两个数相加的时候调用相关的函数并且把参数给它,然后就能实现加法功能,是不是变得非常方便呢!!
因此函数在C语言中扮演着核心角色,它们是程序的基本构建块,用于实现特定的功能或操作。函数可以执行各种任务,包括数学计算、文件操作、输入输出等。函数的设计使得程序更加模块化,提高了代码的可重用性和可维护性!
其实我们每次写的int main(){},main其实也是一个函数,它叫主函数,每个c程序都至少有一个主函数!
二、函数的分类
1.库函数
-
为什么会有库函数呢?
用一句简单的话来回答就是:世上本没有库函数,用的人多了,也便有了库函数!
在最早的c语言编写的时候是没有库函数这个概念的,但是例如大家都频繁的想要在控制台输出,或者对字符串进行某些操作等等…如果每个程序员都去编写不同的代码实现相同的功能,不管是后期阅读还是维护代码都是非常麻烦的。然后为了统一也利于代码维护,可读也就有了库函数! -
怎么学习c语言的库函数
打开该链接,可以在c library里面查看各种各样的库函数,头文件
https://cplusplus.com/reference/clibrary/
使用库函数,必须包含 #include 对应的头文件。
此外为大家提供一些库函数的查询学习网站:
MSDN
https://cplusplus.com/
https://en.cppreference.com/w/
https://zh.cppreference.com/w/%E9%A6%96%E9%A1%B5
2.自定义函数
只使用库函数是远远不够的,还需要自定义函数。自定义函数就是自己定义的函数!自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。这给程序员很大的发挥空间!
- 函数的定义
ret_type fun_name(para1, para2 )
{
statement;//语句项
//return 要与函数类型ret_type相呼应
return ret_type ;
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
函数如果不需要返回,函数的类型可以定义为void,无返回值。
- 函数使用举例
实例1:计算两个数的和
//实例1
//调用函数计算两个值的和
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int c=Add(a, b);
printf("%d", c);
return 0;
}
实例2:交换两个值
//实例2
//代码1
//通过函数交换两个数的值
void Swap(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
//a b实参
Swap(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
在控制台打出结果:
由此可见我们并没有交换成功,那是为什么呢?
我们打开监视窗口
由此可见,对于参数,x,y,a,b而言,在创建的时候,四个参数都向内存申请了内存地址,以上代码,只是首先将a,b的值传给了x,y;x,y将这两个数字存在自己的地址,进行了交换,但是最后打印的是a,b两个参数对应的地址里面存储的数字,并没有实现交换。
实际上这里的a,b是实参,x,y是创建的形参,当实参传递给形参的时候,形参是实参的一份临时拷贝。此时形参占用独立的内存,修改形参不会影响实参。(下面会详细讲解函数的参数内容)
接下来我们修改一下代码:
//代码2
void Swap(int *px, int *py)
{
int z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 10;
int b = 50;
//scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
//将a,b的地址传入
Swap(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
修改过后的代码,最后成功实现了两个值的交换功能。让我们再分析一下:
&a通过取地址符,我们可以得到a,b的内存地址;然后将内存地址传入Swap函数,之前讲过存放地址的变量就是指针变量,所以函数的参数设置的是两个指针px,py;*说明这两个变量是指针变量,这两个变量里面存的是a,b两个参数的地址。接下来在Swap函数里面,解引用操作符(也就是“*px”,“*py”)通过两个变量px,py存放的地址找到存放在a,b的参数,并且成功实现了交换。
其实也就是通过身份证号找到了本人!!!!!
三、函数的参数
1.实参
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
2.形参
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
形参实例化之后其实相当于实参的一份临时拷贝。
四、函数调用
1.传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
2.地址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操 作函数外部的变量。
3.练习
- 写一个函数可以判断一个数是不是素数。
//打印100-200的素数
//偶数一定不可能是素数,所以+=2,可以减少一半的数
#include<math.h>//sqrt()库函数引用,sqrt开平方
int main()
{
int i = 0;
for ( i = 101; i < 200; i+=2)
{
int flag = 1;//用来标记是不是,1代表是素数
int j = 2;
//用2——i-1的数去试除i,能否除尽,除尽了不是素数
//i-2改成sqrt可以提高代码的效率
//可以发现如果一个数不是素数,他一定有小于等于自己平方根的因子
for (j=2 ; j<=sqrt(i) ; j++)
{
if (i%j == 0)
{
flag = 0;//不是
break;
}
}
if (flag==1)
{
printf("%d ", i);
}
}
return 0;
}
//代码1
// 返回一个整数来表示x是否为素数(1表示是,0表示不是)
int Sushu(int x) {
if (x < 2) return 0; // 0和1不是素数
for (int i = 2; i * i <= x; i++) { // 使用i * i来避免浮点数比较
if (x % i == 0) {
return 0; // 如果x可以被i整除,则它不是素数
}
}
return 1; // 如果循环完成而没有返回0,则x是素数
}
int main() {
int k;
scanf("%d", &k);
if (Sushu(k)) {
printf("%d是素数\n", k);
}
else {
printf("%d不是素数\n", k);
}
return 0;
//代码2
void Sushu(int x)
{
int i = 0;
int flag = 1;
for ( i = 2; i < sqrt(x); i++)
{
if (x % i == 0)
{
flag = 0;
printf("%d不是素数", x);
break;
}
}
if (flag==1)
{
printf("%d是素数", x);
}
}
int main()
{
int k = 0;
scanf("%d", &k);
Sushu(k);
return 0;
}
以上代码1和代码2都能实现题目的相应功能,但是我们提倡代码1的写法,为什么呢?
这是因为在代码2里面可以看到那个函数不仅判断了是不是素数还实现了打印功能,因此Sushu函数的功能不够单一。假如以后有个人只需要判断素数的功能,那这个函数的功能就冗余了。函数功能单一(高内聚低耦合),在封装之后更好调用。因此我们在写函数的时候希望函数的功能是相对单一的,然后再到主函数调用。通过函数的返回值来判断。综上我们提倡代码1的写法
- 写一个函数判断一年是不是闰年。
闰年的标准:1.能被4整除,同时不能被100整除;
//2.能被400整除
int is_leap_year(int t)
{
if (((t%4==0)&&(t%100!=0))||(t%400==0))
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int year = 0;
scanf("%d", &year);
if (is_leap_year(year))
{
printf("%d是闰年 ",year);
}
else
{
printf("%d不是闰年 ", year);
}
return 0;
}
- 写一个函数,实现一个整形有序数组的二分查找。
//整形有序数组的二分查找
//数组传参实际上传递的是数组首元素的地址;而不是整个数组的内容
//这里的arr看起来是一个数组,本质上是一个指针变量;存的是传入的数组第一个元素首地址;所以在函数内部计算一个函数参数部分的数组的元素个数是不靠谱的;
binary_search(int arr[],int k,int sz)
{
int left = 0;
int right = sz - 1;
while (left<right)
{
int mid = left + (right - left) / 2;//防止溢出
if (k > arr[mid])
{
left = mid + 1;
}
else if (k < arr[mid])
{
right = mid - 1;
}
else
{
return mid;
}
}
return -1;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10,11 };
int k = 0;
scanf("%d",&k);
int sz = sizeof(arr) / sizeof(arr[0]);
//找到了返回下标
//没有找到返回-1,原因是返回1会有歧义,元素的下标是从0开始的
int ret=binary_search(arr, k,sz);
if (ret==-1)
{
printf("Not Found!");
}
else
{
printf("找到了,下标是:%d", ret);
}
return 0;
}
注意:不要在一个函数的内部计算参数传入的数组大小(int sz = sizeof(arr) / sizeof(arr[0]);不要将这条语句放入函数)!!!在调用binary_search函数传数组参数的时候,为了节约空间,其实是将数组第一个元素的地址传了过去,binary_search函数的int arr[]看起来是一个数组,实际上是一个指针,存入的是主函数里面数组首元素 的地址。由于数组的内容是连续存储的,在binary_search函数需要使用数组的时候可以通过该地址找到原来的所有数组内容。
- 写一个函数,每调用一次这个函数,就会将 num 的值增加1。
void Add(int* p)
{
(*p)++;//不加()会有运算符优先级的问题
}
int main()
{
int num = 0;
Add(&num);
printf("%d",num);
}
五、函数的嵌套调用和链式访问
1.嵌套调用
可以嵌套调用,但是不能嵌套定义!!
#include <stdio.h>
void new_line()
{
printf("哈哈哈\n");
}
void three_line()
{
int i = 0;
for (i = 0; i < 3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
2.链式访问
把一个函数的返回值作为另外一个函数的参数。
如果一个函数的返回类型是void,没有返回值,是无法实现链式访问的。
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
//结果是啥?
//注:printf函数的返回值是打印在屏幕上字符的个数
return 0;
}
打印结果是4321
六、函数的声明和定义
1.函数的声明
在写函数的时候,如果将函数放在main函数的后面,编译器在进行编译的时候可能会抛出警告。这是因为c语言编译的时候是从前往后一行一行的扫描的,在main函数里面扫描到某个函数的时候,因为之前并没有扫描过调用函数的相关信息。为了解除警告,可以在main()函数之前对调用的函数进行声明。
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。也就是可以假声明,具体函数是否存在取决于函数的定义。
- 函数的声明要在函数调用之前,也就是先声明后调用。
//声明
//也可以写成:int Add(int x,int y);
int Add(int,int);
int main()
{
int a = 10;
int b = 20;
int c=Add(a, b);
printf("%d", c);
}
int Add(int x, int y)
{
return x + y;
}
- 函数的声明一般放在头文件中。
把代码模块化对于以后在工作中是非常重要的,这里就要浅浅的介绍一下怎么将代码进行模块化。
对于不同功能(或者说人员负责的功能模块),我们可以有多个.c;.h的文件。例如某员工受到上级的指令要实现一个加法的功能,然后可以在头文件里面创建一个add.h的文件用来进行函数声明,在源文件里面创建一个add.c的文件,里面用于函数的定义。最后在进行项目整合的时候只需要在主文件里面进行相关的调用。
此时只需要引入头文件的库就可以使用库里面相关的函数。
#include"add.h"——>等价于拷贝了一份add.h里面的内容int Add(int, int);
2.函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现。
七、函数递归
1.递归的定义
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,
递归策略:只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
实际上递归就是自己调自己。
2.递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件
3.递归练习题
- 接受一个整型值(无符号),按照顺序打印它的每一位。 例如:输入:1234,输出1 2 3 4
//这段代码在我们定义的print函数里面调用函数本身,直到不满足条件n>9
void print(unsigned int n)
{
if (n>9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);
print(num);
return 0;
}
- 编写函数不允许创建临时变量,求字符串的长度。(实际上就是模拟strlen函数求字符串长度)
//代码1
//注意数组传过来的是首元素的地址,不是数组内的全部内容
//int my_strlen(char str[]) //参数部分写成数组的形式
int my_strlen(char *str)//参数部分写成指针形式
{
int count = 0;
while (*str!='\0')
{
count++;//但是这个count是我们在函数里面创建的临时变量
str++;//找下一个字符
}
return count;
}
int main()
{
char arr[] = "abcd";
int len = my_strlen(arr);
printf("%d\n",len);
}
//代码2(在代码1的基础上,不创建临时变量)
//递归求解
//也就是说++会改变传入值本身,而+1只会改变函数识别到的地址
int my_strlen(char* str)
{
if (*str!='\0')
//str++不可以,因为是后置,先使用再++每次都是同样的把最开始的str传进来再+1
{
return 1 + my_strlen(str+1);
}
else
{
return 0;
}
}
int main()
{
char arr[] = "abcd";
int len = my_strlen(arr);
printf("%d\n",len);
return 0;
}
4.递归与迭代
通过以上案例,我们发现有些代码通过递归实现也可以通过循环来实现。实际上循环就是迭代的一种。
(递归是倒着走,迭代是正着走)
- 求n的阶乘。(不考虑溢出)
//递归的形式
int fac(int x)
{
if (x<=0)
{
return 1;
}
else
{
return x * fac(x - 1);
}
}
//迭代的形式
//int fac(int x)
//{
// int i = 0;
// int r = 1;
// for ( i = 1; i <= x; i++)
// {
// r *= i;
// }
// return r;
//}
int main()
{
int n = 0;
scanf("%d",&n);
int ret = fac(n);
printf("%d的阶乘是%d\n",n, ret);
return 0;
}
在调试 fac函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)这样的信息。
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况 ,这样的现象我们成为栈溢出。
- 那如何解决上述的问题:
1). 将递归改写成非递归。
2). 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
- 求第n个斐波那契数。(不考虑溢出)
//递归
int fib(int x)
{
if (x<3)
{
return 1;
}
else
{
return fib(x - 1)+fib(x-2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("第%d个斐波那契数是%d\n", n,ret);
return 0;
}
在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
fib 函数在调用的过程中很多计算其实在一直重复。例如:算第40个的时候,我们要先算38和39,算38的时候算36和37,在计算39的时候算37和38…
因此在计算斐波那契数的时候可以考虑使用迭代,会大大提高代码效率。
//迭代
int fib(int n)
{
int a = 1;
int b = 1;
int c = 0;
if (n<3)
{
return 1;
}
else
{
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("第%d个斐波那契数是%d\n", n,ret);
return 0;
}
有的问题既可以用递归解决也可以用非递归解决;在不考虑效率的情况下我们可以用递归多写代码,锻炼我们的递归思维;但是有的时候递归效率太太太太太慢了,就可以考虑其他方法!!!
总结
提示:这里对文章进行总结:
以上就是今天要讲的内容,本文对c语言里面的函数进行了详细的介绍,通过对C语言中函数的深入探讨,我们不难发现其在编程中的核心地位。函数作为组织代码的基本单位,不仅能够提高代码的可读性和可维护性,还能实现代码的重用和共享。掌握C语言函数的定义、调用、参数传递、函数的递归等基本概念,对于编写高效、可靠的程序至关重要。希望本文的介绍能够帮助读者更好地理解和运用C语言函数,为未来的编程之路奠定坚实的基础。