C语言函数详解

一.函数的定义

各位老铁们在学习数学中常常听到函数一词,那么我们编程中的函数是否与数学中的函数相同呢,若不相同,又有什么特殊的含义呢?
下面引用维基百科对编程中函数的定义

  • 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
    subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组
    成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
  • 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软
    件库。

看到这里,相信老铁们大概能揣摩到,函数原来就是一些代码块,而这些代码块具有特定的功能和相对的独立性。与数学中的函数表达形式不同,但是思想却有异曲同工之妙:

  • 在表达形式上,数学上采用数字、字母、运算符的结合,表达自变量与因变量之间的因果关系,而在编程中,则采用各类语句的组成,表达输入与输出之间的逻辑关系。
  • 在思想上,数学与编程同样要给函数输入参数,经过对应法则(输入与输出的逻辑关系),最后带回返回值。

二.函数的分类

数学中的函数分为基本初等函数、多项式函数、复变函数和常用函数四类,那么编程中的函数又分为哪几类呢?

编程中的函数分法非常简单,分为库函数自定义函数,顾名思义:前者是C语言定义的函数,而后者是需要自己创建的函数。

2.1库函数

2.1.1库函数的定义与来源

库函数其实就是C语言本身定义出来的函数,这里需要强调一点:C语言仅仅只是交代函数的功能和名称,而函数的具体实现则是由各个创建编译器的程序员完成,因此在不同的编译器中,有些函数的实现结果会有所不同。
那么,为什么会出现库函数呢?

  • 在各位老铁人生第一次敲代码时,打印"Hello world!"是不是要使用到printf函数
  • 在后续敲代码的过程中,要频繁的使用pow函数进行平方,sqrt函数进行开方
  • 并且在计算字符串长度时,我们常常会用到strlen函数

而对于这些会频繁使用到的功能,先辈们想创建一些标准函数,存入编译器中,等到需要时,再拿出来就好了,这极大提高编写代码的效率。

2.1.2如何记住并使用库函数

首先我们了解一下库函数到底有多少呢?

大致分类一下:

  • IO函数(标准和系统)
  • 数学函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 字符串操作函数
  • 其他库函数

那么对于这么多的库函数,以及对应的头文件如何才能把它们记住呢?

其实根本不用刻意去记,编程是一门非常需要动手实践的学科,我们只需要在平时练习代码的过程中,不断加深印象即可。

这里庄哥附上查阅C语言头文件的链接: C plus plus

那么如何才能使用库函数呢?
其实很简单,只需要引入对应函数的头文件即可,举个例子:

#include<stdio.h>//引入头文件
int main()
{
	printf("Hello world!");//使用库函数
}

除此之外,在使用库函数时,我们需要注意函数的参数的类型返回的类型,举个例子:
链接: strlen
点进链接我们可以看到下面这一栏
在这里插入图片描述
由图可以看到,strlen函数的返回类型为“size_t”,参数类型为“const char* str”,它的功能为获取字符串的长度,我们在调用库函数时需要符合上面介绍的类型,否则程序将会出错。

#include<string.h>
#include<stdio.h>//记得引头文件
int main()
{
	char arr[7]="abcdef";//注意arr的创建类型
	size_t num=0;//注意size_t的创建类型
	num=strlen(arr);//引头文件后,即可使用相应的库函数
	printf("%zd",num);//注意size_t类型的打印类型为%zd
}

温馨提示:网上很多查阅库函数介绍的网站都是英文,因此各位老铁需要尽快提高自身的英语水平,虽不说雅思托福,但至少不能影响自己阅读文献的速度哦!


2.2自定义函数

2.2.1自定义函数的产生原因

这时候有老铁肯定会想,如果库函数这么方便的话,那所有代码都用库函数,引个头文件不就行了吗?

答案当然是否定的,代码解决的问题源于生活,而随着社会不断进步,生活中的问题层出不穷、各种各样,就算穷尽所有程序员的一生,也不可能解决所有问题。因此我们引入自定义函数,供程序员自由发挥,针对不同场景和环境条件创建不同功能的函数,极大提高了程序员的上限。

2.2.2创建自定义函数的格式

那么自定义函数与库函数的区别又是什么呢,在创建的时候又有什么要注意的呢,组成部分又有哪些呢?

  • 自定义函数与库函数同样有函数名、参数类型和返回类型。但这些在自定义函数中,可供程序员自己设计。因此,我们在创建时,需要留意参数的类型和返回值的类型。
  • 自定义函数组成:函数名、返回值、形式参数、函数体
  • 自定义函数的创建其实就是对此函数的定义

下面庄哥通过一道例题来介绍如何创建自定义函数
例如:创建一个函数求两个数的和

#include<stdio.h>
//这里需要注意庄哥将Add函数的创建放在了main函数前面,后面会介绍为什么
int Add(int x,int y)//这里注意函数的创建格式:返回类型  函数名(参数类型 参数名,参数类型 参数名,...)
//下面是函数体的创建,与写main函数的规则一致
{
	return x+y;
}

int main()
{
	int a=10,b=20,c;
	c=Add(a,b);//这里注意传入的参数
	printf("两个数的和为%d",c);
}

各位老铁可以先自己动手模仿敲一敲哦,先弄清楚自定义函数是由哪些部分组成的,以及它的创建格式(后续内容庄哥会继续介绍,这里是先让老铁们熟悉一下哈!)

三.关于函数的传参与调用问题

3.1实际参数与形式参数

下面庄哥来介绍函数传参问题

  • 使用函数的时候常常需要传入一些参数,例如:strlen函数需要我们传入字符串参数,自定义函数Add需要传入a和b的值,而这些真实传给函数的参数,叫做实际参数,简称实参
  • 在定义函数的时候,在函数名后面括号中的变量叫做形式参数,简称形参
  • 简而言之:传给函数的参数叫“实参”,函数中接收实参的参数叫“形参”

老铁们这时肯定就有疑惑了,可以传任何参数给函数吗,传过去后,形参又是如何接收的呢?

  • 实参的类型:常量、变量、函数、表达式等
    温馨提示:不论实参是什么类型,它们都必须带有确切的值,以便形参能够接收
  • 形参的接收类型根据实参的类型而定,并且形参只有函数被调用的时候才实例化(分配内存单元),但当函数被调用完时,就立即销毁。
    温馨提示:函数在被调用时,会维护一个函数栈帧(内存上的一块区域),函数调用结束,栈帧销毁。(后面庄哥会专门出一篇关于数据存储的博客,由于内容较多,限于篇幅,老铁们先了解一下)

3.2传值调用与传址调用

介绍完形参与实参,下面我们就再来看看函数调用的两种方式

3.2.1传值调用

传值调用就是实参的值单向传给函数的形参
注意:传值调用不是传实参的地址,所以形参与实参分别占用不同的内存块,这时对形参的修改并不会影响到实参,可以理解为此时的形参是实参的一份临时拷贝

3.2.2传址调用

传址调用就是实参将自身的地址传给形参
注意:此时形参与实参是在同一块内存空间,所以对形参的修改将会影响到实参,但并不是传地址就是传址调用哦,一定传的是实参自身的地址

例如下面这个例子:

#include<stdio.h>
int fun(int* pa,int* pb)
{
	return 0;
}

int main()
{
	int a,b;
	int* p1=&a;
	int* p2=&b;
	fun(p1,p2);
	//这里虽然p1,p2传的是地址,但是对形参pa、pb进行修改,并不会影响到实参p1、p2,影响的是a、b,因此属于传值调用,而不是传址调用
}

3.2.3总结二者的区别

我们通过下面这段代码总结二者的区别:

#include<stdio.h>
//swap1为传值调用,swap2为传址调用
int swap1(int x, int y)
{
	int tmp;
	tmp = x;
	x = y;
	y = tmp;
}
int swap2(int* p1, int* p2)
{
	int tmp;
	tmp = *p1;
	*p1 = *p2;
	*p1 = tmp;
}
int main()
{
	int a=10, b=20;
	swap1(a, b);
	printf("a的值为:%d b的值为:%d\n", a, b);
	swap2(&a, &b);
	printf("a的值为:%d b的值为:%d\n", a, b);
}

各位老铁可以将代码复制到编译器上哦,调试后查看形参与实参的地址,即可发现传值调用与传址调用的本质区别

四.函数的声明和定义

看到这里,相信老铁们已经对函数的整体面貌有了大致的认识。可是,创建好函数后如何向编译器介绍我们的自定义函数呢?

4.1函数的声明

其实很简单,通过函数声明的方式告诉编译器函数叫什么、参数是什么、返回类型是什么

这里需要注意3点:

  • 函数的声明一般放在头文件中
  • 函数的声明要放在函数使用之前,即先声明后使用
  • 函数声明决定不了函数是否真实存在

那么什么才能决定函数是否存在呢?

4.2函数的定义

函数的定义指交代函数的具体实现,即函数功能的创建,只有定义才能够让函数有机会在内存中开辟空间

以这道题为例:创建一个函数求两个数的和

#include<stdio.h>
int Add(int a,int b);//注意看这里函数声明放在函数调用前,就可以将创建函数的部分放在调用函数之后了
int main()
{
	int a=10,b=20,c=0;
	c=Add(a,b);
	printf("%d",c);
}

int Add(int a,int b)
{
	return a+b;
}

这时候细心的老铁就看到了:函数定义和函数的声明为什么都有int Add(int a,int b)这一句话呢,这难道不是给自己添麻烦吗,将函数声明与定义结合起来,这不是更方便吗?

其实不然,等老铁们以后能够自行研发项目后就能明白:如果将函数定义部分与函数调用部分放在一起,等项目验收的时候,你编写的代码将一览无余的暴露在收验人面前,如果这个人居心不轨,将你的代码公布出去,那么你就没有利用价值了。所以,我们一般将函数定义与函数调用部分分开编写,验收时只交出函数调用的部分,这样就可以起到保密作用。

而函数调用部分与函数定义之间的桥梁就是函数声明

所以老铁们可以从现在开始养成将函数调用与函数定义分开两个文件编写的习惯,但要记得在调用前进行声明。(此文中的程序都很短,庄哥为了方便就合在一起写了)

五.函数的嵌套调用与链式访问

5.1嵌套调用

指函数与函数之间可以根据自己的实际需求进行组合,也就是互相调用
注意:函数可以嵌套调用,但不能嵌套定义

例如下面这段代码:

#include <stdio.h>
void new_line()
{
   printf("hehe\n");
}
void three_line()
{
   int i = 0;
   for(i=0; i<3; i++)
   {
        new_line();//在此函数中调用了new_line函数
   }
}
int main()
{
   three_line();
   return 0;
}

5.2链式访问

把一个函数的返回值作为另外一个函数的参数

我们以下面这段代码为例:

#include <stdio.h>
int main()
{
	//printf的返回值,是在屏幕上打印字符的个数
    printf("%d", printf("%d", printf("%d", 43)));
    return 0;
}

请问各位老铁,这段代码输出的结果是什么呢?
答案就是:4321

各位老铁做对了吗,函数的嵌套调用与链式访问知识点较少,老铁们了解并能运用即可,下面我们直接进入今天的重头戏——函数递归

六.函数递归

6.1递归的定义以及限制条件

  • 定义:函数调用自身的编程技巧叫做递归
  • 精髓:先递推,再回归
  • 核心思想:大事化小,一步步解决问题
  • 好处:递归作为一种算法,能极快解决一类特殊的问题,在编程中广泛运用

两个必要条件:

  • 存在限制条件,当满足这个条件后,递归停止
  • 随着递归进行,越来越接近这个限制条件

下面通过一道例题给各位老铁感受一下递归的力量!
例:不允许创建临时变量,求字符串的长度

#include<stdio.h>
size_t str(char* pa)//注意函数定义的格式哦
{
	if (*pa != '\0')
	{
		return str(pa + 1)+1;//通过不断调用函数计算字符串个数
	}
	else
	{
		return 0;
	}
}
int main()
{
	char arr[10] = "abcd";
	size_t len;
	len = str(arr);
	printf("%zd", len);
}

看流程图帮助老铁们更好理解

这里请各位老铁再琢磨一下先递推,再回归这句话

从‘a’到‘\0’逐层往下递推。到‘\0’时,进入else语句,返回0,并且停止递推,开始回归。每层加1,最后得到此字符串有4个字节。

下面庄哥列几道题供有兴趣的老铁琢磨,后续庄哥会在gitee仓库中更新源码

  1. 接受一个整型值(无符号),按照顺序打印它的每一位。例如:输入1234 输出:1 2 3 4
  2. 青蛙跳台阶问题
  3. 汉诺塔问题

6.2迭代

那么是否所有的问题都可以用递归来解决呢?

答案当然是否定的,由于每递归调用一次函数,就会开辟一次栈空间。但是,系统分配的栈空间又是有限的,所以递归次数过多,则会导致栈空间耗尽,这种情况称之为栈溢出(stack overflow)

同样通过例题供各位老铁们理解:
用递归的方法求第n个斐波那契数

#include<stdio.h>
int fib(int n)
{
	if (n <= 2)
	{
		return 1;
	}
	else
	{
		return fib(n - 1) + fib(n - 2);
	}
}

int main()
{
	int n,sum;
	scanf("%d", &n);
	sum=fib(n);
	printf("%d", sum);
}

各位老铁可以将上面的代码复制到你们的编译器中。当你们输入n为5的时候,很快便得出了答案。可是当你们输入n为50时,电脑就无法很快得出答案了,为什么呢?
请看下面这张流程图

当要算第50个斐波那契数的时候,我们需要知道第49个和第48个斐波那契数,但是想要知道第49个和第48个斐波那契数的时候,又得需要知道第48个、第47个和第46个斐波那契数,这其中进行了大量的重复计算,不仅运算效率低,而且开辟了大量栈空间。所以,可见得递归并非是这道题的最佳解决方案。

那么这道题究竟该如何解决呢?

  • 方法一:使用static关键字保存递归调用的中间状态,使其可为每一个调用层所访问。这样就避免了大量的重复计算和开辟大量的栈空间。
  • 方法二:使用非递归的方法

这里我重点介绍第二种方法:迭代

迭代是指一种不断用旧值递推新值的过程

根据迭代的方法,我们对上述代码进行改进

#include<stdio.h>
int main()
{
	int next=1, last=1, sum=1,n;//这里next表示下一个数字,last表示上一个数字,sum表示二者之和
	scanf("%d", &n);
	while (n > 2)
	{
		sum = next + last;
		last = next;
		next = sum;
		n--;
	}
	printf("%d", sum);
}

运行思路:

  • 当n<=2时,不进入循环,sum=1
  • 当n>2时,算出第一次sum后,将第二个数next的值赋给第一个数last,再将sum的值赋给第二个数next,再加上n–。就这样循循渐进,最后算出第n个斐波那契数。

下面请看流程图帮助理解

那么至于什么时候用递归、什么时候用迭代,这就需要各位老铁们自己多加练习、积累经验,达到一看到题就知道最优方法境界。


总结

下面附上庄哥此博客的思路:
在这里插入图片描述
本文到此就结束了,欢迎各位老铁在评论区指出不足的地方,庄哥一定加以改进!!!

  • 14
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值