C语言函数大揭秘:开启编程魔法的钥匙

我们从小就在学习数学,数学又离不开函数,而编程的本质是通过编写代码来实现计算机的控制,以解决实际问题或实现特定功能。编程是通过组织和调用函数来实现特定功能的系统性活动,函数是编程中可复用的代码单元,二者相辅相成,共同构建软件系统。

一、函数的概念

C语言也引入函数(function)的概念,有些翻译为:子程序,子程序这种翻译更加准确⼀些。 C语言中的函数就是⼀个完成某项特定的任务的一小段代码。这段代码是有特殊的写法和调用方法的。 C语言的程序其实是由无数个小的函数组合而成的,也可以说:一个大的计算任务可以分解成若干个较小的函数(对应较小的任务)完成。同时⼀个函数如果能完成某项特定任务的话,这个函数也是可以复永的,提升了开发软件的效率。

二、库函数

1、标准库与头文件

C语言标准中规定了C语言的各种语法规则,C语言并不提供库函数;C语言国际标准ANSI C规定了一些常用的函数的标准,被称为标准库,那不同的编译器厂商根据ANSI提供的C语言标准就给出了⼀系列函数的实现。这些函数就被称为库函数。

我们前面内容中学到的 printfscanf 都是库函数,库函数也是函数,不过这些函数已经是现成 的,我们只要学会就能直接使用了。这样也大大提高了程序员的编程效率。

库函数相关头文件:https://zh.cppreference.com/w/c/header

2、库函数的使用方法

库函数的学习和查看⼯具很多,比如:

C/C++官方的链接:https://zh.cppreference.com/w/c/header

cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/

3、举例

接下来我们以sqrt这个函数为例子来介绍

double sqrt (double x);
//sqrt 是函数名 
//x 是函数的参数,表⽰调⽤sqrt函数需要传递⼀个double类型的值 
//double 是返回值类型 - 表⽰函数计算的结果是double类型的值 

其功能就是计算平方根,库函数是在标准库中对应的头文件中声明的,所以库函数的使用,务必包含对应的头文件,不包含是可能会出现⼀些问题的。这个函数的头文件就是#include<math.h>

#include <stdio.h>
#include <math.h>
int main()
{
 double d = 16.0;
 double r = sqrt(d);
 printf("%lf\n", r);
 return 0;
}

上述代码的运行结果就是4.000000,一般浮点型计算结果默认到小数点后六位。

三、自定义函数

自定义函数就是由我们自身去进行定义的函数

ret_type fun_name(形式参数)
{

}
//ret_type 是函数返回类型
//fun_name 是函数名
//括号中放的是形式参数
//{ }括起来的是函数体

ret_type 是用来表示函数计算结果的类型,有时候返回类型可以是 void ,表示什么都不返回,也可以返回int表示函数返回一个整型,这个根据自己的需求来定义。

fun_name 是为了方便使用函数;就像人的名字一样,有了名字方便称呼,函数有了名字方便调
用,所以函数名尽量要根据函数的功能起的有意义。当然自己记得住的是最好的,这些都是很有张力的可以根据自身需求来定义

函数的参数我们也叫形式参数,他可以没有,但是如果有的话那就需要交代清楚类型、名字以及参数个数。

{ }括起来的部分被称为函数体,函数体就是完成计算的过程,函数的具体内容就在函数体内实现。

接下来我们通过一个例子来更好的了解函数:
 

#include<stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int c = a + b;
	printf("%d\n", c);
	return 0;
}

这是一段普通的代码,代码的目的是实现加法,那我们思考一下我们是否可以定义一个加法函数来实现这段代码:

int add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
#include<stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int c = add(a, b);
	printf("%d\n", c);
	return 0;
}

我们将代码跑起来果然可以实现加法,这就是函数的简单应用,这时候有些人肯定要发问了这不是比原来还要麻烦了吗多了一大堆东西,这是因为这个程序本身就很简单,假设我们学习到了后期写一个大型程序我们全写在一个main函数中会不方便我们调试。

四、形参与实参

还是以上述的加法函数举例:

1、实参

我们把调用add函数时,传递给函数的参数a和b,称为实际参数,简称实参。实际参数就是真实传递给函数的参数。

2、形参

在上面代码中,定义函数的时候,在函数名add后的括号中写的x和y,称为形式参数,简称形参。 为什么叫形式参数呢?实际上,如果只是定义了add函数,而不去调用的话,add函数的参数x和y只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在 函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化。

大家记住一句话即可:形参是实参的⼀份临时拷贝

五、return语句

• return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执行表达式,再返回表达式的结果。

• return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。

• return语句执行后,函数就彻底返回,后边的代码不再执行。

• return返回的值和函数返回类型不⼀致,系统会自动将返回的值隐式转换为函数的返回类型。

• 如果函数中存在if等分支的语句,则要保证每种情况下都有return返回,否则会出现编译错误。

• 函数的返回类型如果不写,编译器会默认函数的返回类型是int。

• 函数写了返回类型,但是函数中没有使用return返回值,那么函数的返回值是未知的。

六、数组做函数参数

我们现在要写⼀个函数将⼀个整型数组的内容,全部置为-1,再写⼀个函数打印数组的内容。

我们该如何实现呢?

我们先写出我们的主体框架:
 

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[1]);
	set_arr(arr, sz);
	print(arr, sz);
	return 0;
}

set_arr我们想要实现将数组的内容全部置为-1;

print我们想要实现的是打印这个数组;

当然我们要记住:数组传参的时候传的是数组名

接下来我们便可以去完善函数的其他部分:
 

void set_arr(int arr[],int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		arr[i] = -1;
	}
}
void print(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

这时候写完了我们可以总结一下:

• 函数的形式参数要和函数的实参个数匹配

• 函数的实参是数组,形参也是可以写成数组形式的

• 形参如果是以一维数组,数组大小可以省略不写

• 形参如果是二维数组,行可以省略,但是列不能省略

• 数组传参,形参是不会创建新的数组的

• 形参操作的数组和实参的数组是同⼀个数组

最后很重要的一点:数组传参的时候传的是数组名!!!

七、嵌套调用和链式访问

1、嵌套调用

嵌套调用就是函数之间的互相调用,每个函数就像⼀个乐高零件,正是因为多个乐高的零件互相无缝的配合才能搭建出精美的乐高玩具,也正是因为函数之间有效的互相调用,最后写出来了相对大型的程序。

接下来我们还是以实际例子来说明:

我们写一个程序来计算某年某月有多少天:

#include<stdio.h>
int main()
{
	int year = 0;
	int month = 0;
	scanf_s("%d %d", &year, &month);
	is_year_of_month_of_day(year, month);
	int day = is_year_of_month_of_day(year, month);
	printf("%d\n", day);
	return 0;
}

这就是函数的主题框架,其中is_year_of_month_of_day就是我们要写的函数,但是我们要考虑一点就是闰年的2月和平年的2月天数不一样所以我们调用函数的时候还要再写一个函数判断他是闰年还是平年。

int is_run(int year)
{
	if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
		return 1;//闰年
	else
		return 0;//平年
}
int is_year_of_month_of_day(int year, int month)
{
	int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	//数组的下标是从0开始的
	int day = days[month];
	if (is_run(year) == 1 && month == 2)
	{
		day += 1;
	}
	return day;
}

通过上述的简单函数我们就实现了这个简单程序,当然嵌套调用的魅力肯定不止于此,带读者自己去发掘。

2、链式访问

所谓链式访问就是将一个函数的返回值作为另外一个函数的参数,像链条⼀样将函数串起来就是函数的链式访问。

#include <stdio.h>
int main()
{
 int len = strlen("abcdef");//1.strlen求⼀个字符串的⻓度 
 printf("%d\n", len);//2.打印⻓度  
 return 0;
}

#include <stdio.h>
int main()
{
 printf("%d\n", strlen("abcdef"));//链式访问  
 return 0;
}

上述代码其本质是一样的。

接下来我们再看一个有趣的例子:
 

#include <stdio.h>
int main()
{
 printf("%d", printf("%d", printf("%d", 43)));
 return 0;
}

你觉得这段代码的输出结果是什么呢?

答案:4321

我相信肯定出乎了很多人的预料,这是为啥呢?还是我们没能好好理解printf的本质,printf函数返回的是打印在屏幕上的字符的个数。这么一看就显得合理了。

第三个printf打印43,在屏幕上打印2个字符,再返回2

第⼆个printf打印2,在屏幕上打印1个字符,再返回1

第⼀个printf打印1,所以屏幕上最终打印:4321

八、static和extern

static和extern都是C语言中的关键字。

static是静态的的意思,可以用来:

• 修饰局部变量 • 修饰全局变量 • 修饰函数

extern是用来声明外部符号的。

1、作用域与生命周期

在此为大家拓展俩个概念:作用域和生命周期

作用域是程序设计概念,通常来说,⼀段程序代码中所用到的名字并不总是有效的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

1. 局部变量的作用域是变量所在的局部范围。

2. 全局变量的作用域是整个工程(项目)。

生命周期指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的⼀个时间段。

1. 局部变量的生命周期是:进入作用域变量创建,生命周期开始,出作用域生命周期结束。

2. 全局变量的生命周期是:整个程序的生命周期。

2、static
1、static修饰局部变量

上述俩段代码唯一的区别就是第二段代码有个static,代码1的结果是打印1 1 1 1 1,而代码2打印的结果是1 2 3 4 5;

代码1的test函数中的局部变量i是每次进入test函数先创建变量(生命周期开始)并赋值为0,然后再++,再打印,出函数的时候变量生命周期将要结束(释放内存)。

代码2中,我们从输出结果来看,i的值有累加的效果,其实test函数中的i创建好后,出函数的时候是不会销毁的,重新进入函数也就不会重新创建变量,直接上次累积的数值继续计算。

结论:static修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型,本来一个局部变量是存储在内存的栈区的,但是被 static 修饰后存储到了静态区。存储在静态区的变量和全局变量是一样的,生命周期就和程序的生命周期一样了,只有程序结束,变量才销毁,内存才回收。但是作用域不变的。

使用建议:未来⼀个变量出了函数后,我们还想保留值,等下次进入函数继续使用,就可以使用static修饰。

2、static修饰全局变量

extern是用来声明外部符号的,如果⼀个全局的符号在A文件中定义的,在B文件中想使用,就可以使用extern进行声明,然后使用。

代码1正常,代码2在编译的时候会出现链接性错误。

结论:⼀个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。本质原因是全局变量默认是具有外部链接属性的,在外部的文件中想使用,只要适当的声明就可以使用;但是全局变量被static修饰之后,外部链接属性就变成了内部链接属性,只能在自己所在的源文件内部使用了,其他源文件,即使声明了,也是无法正常使用的。

使用建议:如果一个全局变量,只想在所在的源文件内部使用,不想被其他文件发现,就可以使用 static修饰。

3、static修饰函数

代码1是能够正常运行的,但是代码2就出现了链接错误。其实static修饰函数和static修饰全局变量是一模一样的,一个函数在整个工程都可以使用,被static修饰后,只能在本文件内部使用,其他文件无法正常的链接使用了。本质是因为函数默认是具有外部链接属性,具有外部链接属性,使得函数在整个⼯程中只要适当的声明就可以被使用。但是被static修饰后变成了内部链接属性,使得函数只能在自己所在源文件内部使用。

使用建议:⼀个函数只想在所在的源文件内部使用,不想被其他源文件使用,就可以使用static修 饰。

总结以及下期预告:

在本期内容中我们学习了许多函数相关的知识,明白了代码块拆分写的重要性,下一期我们先学习函数的递归同时我也会向大家展示VS2022中的一些快捷键帮助我们更好的写代码以及调试我们的代码。我还会为大家带来一期内容全是算法题,我们从实际的代码出发去感受这些知识,没有实践的知识是不牢固的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值