目录
一、什么是函数
函数是执行某个特定任务的一小段代码,又叫做子程序。一个大的计算机任务可以分成很多个函数。一个拥有特定功能的函数能够重复利用,减少代码量和代码冗杂,提高软件开发的效率。
函数分为库函数和自定义函数。
二、库函数
2.1、标准库、库函数、头文件的概念
C语言的国际标准ANSI C规定了一些常用函数的标准(包括功能、参数、返回值等),这些函数的标准就叫做标准库。编译器厂商根据这些标准实现函数,这些函数就叫做库函数。
程序员能直接使用库函数,而不必浪费时间精力再写有这些常用功能的函数;编译器厂商对于函数的实现不断优化更新,专业的人做专业的事,也比程序员自己重写的函数执行效率高、质量高。
这些库函数根据功能划分,声明在不同的头文件中。库函数相关的头文件介绍:C 标准库头文件 - cppreference.com。不用一次性把库函数全部看懂,在今后需要用某个库函数时,再查看文档,经常用就熟悉了。
注意,<Window.h>不是C标准库的头文件,而是Windows操作系统的头文件。
2.2、库函数查阅工具
C\C++官方:C 标准库头文件 - cppreference.com
cplusplus.com:C library - C++ Reference (cplusplus.com)
官方的网站是中文的,容易阅读,但是搜索功能是谷歌的,搜索不了。cplusplus.com是英文的,但能搜索,转换成旧版本就行。
以sqrt函数为例:
三、自定义函数
程序员应该聚焦在自定义函数上,发挥创造性编写有特殊功能、库函数中没有的函数。
3.1、函数的语法形式
函数类型 函数名(参数){ ...... }
- 函数类型:表示函数的计算结果的类型。可以是void,表示什么也不返回。
- 函数名:为了一看到名字就知道是什么功能,尽量起有意义的名字。
- 参数:()里的是函数的参数。若没有参数,可以写void,也可以什么也不写;若有参数,需要写明参数的类型和名字。参数的数量可以是:0~很多个。
- 函数体:{}里的就是函数体,是完成计算的过程。
函数根据传入的参数,执行函数体,最后给出计算结果。如果函数定义时没有给出函数的返回值类型,则隐含类型是int。
3.2、形参和实参
实参是函数调用时,传给函数的实际参数。形参是函数定义时写在()里的形式参数。形参和实参是一一对应的。
形参在函数调用时,为了存储实参传递过来的值,才申请内存空间的过程,叫做形参的实例化。形参实际上是实参的一份临时的拷贝。
为什么这么说呢?举个例子:
在函数调用前,形参是不存在的。
进入函数后,形参才被分配了内存空间,可以看到x与a、y与b的地址是不一样的,但是存储的内容相同,所以说形参是实参的一份拷贝。
函数返回计算结果后,可以看到形参的内存被回收了,所以说这份拷贝是临时的。
最后,形参和实参的命名是可以相同的。
3.3、return语句
- return语句执行后,函数会立马返回。
- return语句后面可以是一个值,也可以是一个表达式。如果是表达式,则先执行表达式,再执行return。
- 对于返回值是void类型的函数,当需要提前返回时,则可以写return;。注意,break只能在循环和switch语句中使用。
- 如果返回值和函数的返回类型不匹配时,会将返回值强制转换成函数的返回值类型。
- 如果函数中有分支语句,则需要保证每种情况的都有return返回,否则编译器会报警告。
被调用函数的返回值会返回给调用的函数,mian函数的返回值是返回给系统指定的一个函数。
3.4、数组做函数的参数
- 函数的实参是数组,形参也可以写成数组的形式(但实际上是数组的地址)。如果是一维数组,可以省略数组的大小;如果是二维数组,可以省略行的大小。
- 数组传参,形参不会创建新的数组,而是跟实参操作同一个数组。
看下面的例子,单独写一个函数打印数组:
可以看到形参和实参的地址是相同的。因为形参没有创建新的数组,所以形参不需要标明数组的大小(申请内存空间才需要知道要多大)。
又因为实参传给形参的是数组的地址,sizeof函数是无法根据数组的地址(a数据类型是int*)来计算数组的大小(arr的数据类型是int[10])的,所以数组的大小必须通过参数传入,而不能在定义的函数里计算。
验证一下,用sizeof计算形参a的大小,将会是指针类型的大小:
数组的大小:一个整型4字节,10个整型40字节。
数组指针的大小:本质是指针类型,在32位系统下为4字节,在64位系统下为8字节。
3.5、函数的嵌套调用和链式访问
函数的嵌套调用就是函数之间互相调用。链式访问就是一个函数的返回值作为另一个函数的实参。
从最里层开始,打印43,返回字符数2;打印2,返回字符数1;打印1。
3.6、函数的声明和定义
函数应该先声明再使用。如果函数的定义在函数的调用之后,则必须在函数调用之前声明函数:
如果没有函数声明,虽然也能运行正确,但是编译器会给出警告,需要改正:
当然,如果把函数的定义写在函数调用之前,函数的定义本身是一种特殊的函数声明。注意,函数的声明只要是在调用的前面就行,如下的位置也可以:
四、多个文件
当代码过多时,会根据程序的功能拆分到多个文件中。函数声明、类型声明放到头文件中(.h),函数的定义放到源文件中(.c)。如下例子:
这样写代码的好处:
- 方便多人协作。(若一个项目有很多个工程师分工合作,如果不拆分成很多个文件,那么等第一个人写完了,第二个人再去写,这样也就太慢了,不现实。)
- 模块化,让代码可以复用。
- 在一定程度上对源码进行隐藏。
接下来讲讲,为什么能对源代码进行隐藏。如果A公司开发了一套游戏引擎,卖给B公司10W一年,那么A肯定不愿意把源码卖给他,因为卖了源码B公司就不会再来买了。因此需要把源码隐藏起来,只给头文件,而不给源代码,像这样做:
上图是要卖的项目,右击Add项目 >> 属性 >> 常规 >> 配置类型 >> 改为静态库(.lib):
确定后再次生成解决方案,生成静态库文件:
将静态库文件打开,会是乱码,因为文件是二进制的(实现了隐藏):
现在A公司只需把Add.h和Add.lib两个文件卖给B公司,下面是B公司的项目:
有些编译环境默认不支持多文件编译的,比如VS Code,想要支持还得改配置文件。但是VS是支持多文件编译的,这是它的优势。
五、static和extern
5.1、作用域和生命周期
(1)作用域
代码中的名字并不是在每个地方都是可用的,这个名字可用的代码范围就叫做它的作用域。
- 局部变量的作用域是变量所在的局部范围。
- 全局变量的作用域是整个项目。
(2)生命周期
变量的创建(申请内存空间)到销毁(回收内存空间)之间的这段时间。
- 局部变量的生命周期是进入作用域创建,生命周期开始,出作用域生命周期结束。
- 全局变量的生命周期是整个程序的生命周期。
5.2、extern
extern是用来声明外部符号的。A文件定义的全局变量/函数,可以被B文件使用,但需要用extern声明。如下图代码验证:
5.3、static
(1)static修饰局部变量
static修饰局部变量,会将它的生命周期延长至跟程序的生命周期一样。如下图代码验证:
在5次循环中,每进入func函数,a都重新初始化为2并加1得3,返回函数后收回a的内存空间;b的内存空间在编译阶段就已经申请了,所以在执行代码的时候不会执行声明静态局部变量这一行(反汇编中没有翻译静态局部变量的汇编语句、调试逐行执行语句可以验证),它的内存空间到程序执行完毕才回收,因此每进入一次func函数就会累积加1。
在之前的文章(变量的分类部分:http://t.csdnimg.cn/cnzG1)中讲过,内存大概分为三个部分:栈区(存放局部变量、形参)、堆区(动态内存管理malloc、calloc、realloc、free)、静态区(全局变量、静态变量)。实际上static修饰局部变量的本质,是改变了局部变量的存储类型,从存储在栈区,改为了存储在静态区。
想局部变量出了作用域还存在,就用static修饰。
(2)static修饰全局变量和函数
static修饰全局变量/函数,会将其外部链接属性改为内部链接属性。尽管B文件中使用extern声明了外部A文件的全局变量/函数,也不能使用A文件中被static修饰的全局变量/函数了,因为它们只能在文件内部使用。如下图代码验证:
想全局变量和函数只在本源文件使用,可以用static修饰。
最后提醒一下,如果自定义头文件"add.h"的内容如下:
int add(int a, int b);
在调用函数前,声明函数 int add(int a, int b); 和使用 #include "add.h"的作用是一样的,最终编译器会把#include "add.h"替换成具体的声明内容。