【C语言】函数

目录

一、什么是函数

二、库函数

2.1、标准库、库函数、头文件的概念

2.2、库函数查阅工具

三、自定义函数

3.1、函数的语法形式

3.2、形参和实参

3.3、函数调用操作符

3.4、return语句

3.5、数组做函数的参数

3.6、函数的嵌套调用和链式访问

3.7、函数的声明和定义

四、多个文件

五、static和extern

5.1、作用域和生命周期

(1)作用域

(2)生命周期

5.2、extern

5.3、static

(1)static修饰局部变量

(2)static修饰全局变量和函数


一、什么是函数

函数是执行某个特定任务的一小段代码,又叫做子程序。一个大的计算机任务可以分成很多个函数。一个拥有特定功能的函数能够重复利用,减少代码量和代码冗杂,提高软件开发的效率。

函数分为库函数和自定义函数。

二、库函数

2.1、标准库、库函数、头文件的概念

C语言的国际标准ANSI C规定了一些常用函数的标准(包括功能、参数、返回值等),这些函数的标准就叫做标准库 。编译器厂商根据这些标准实现函数,这些函数就叫做库函数

程序员能直接使用库函数,而不必浪费时间精力再写有这些常用功能的函数;编译器厂商对于函数的实现不断优化更新,专业的人做专业的事,也比程序员自己重写的函数执行效率高、质量高。

这些库函数根据功能划分,声明在不同的头文件 中。库函数相关的头文件介绍:C 标准库头文件 - cppreference.com。不用一次性把库函数全部看懂,在今后需要用某个库函数时,再查看文档,经常用就熟悉了。

注意,<Window.h>不是C标准库的头文件,而是Windows操作系统的头文件。

2.2、库函数查阅工具

C\C++官方:C 标准库头文件 - cppreference.com

cplusplus.comC library - C++ Reference (cplusplus.com)

官方的网站是中文的,容易阅读,但是搜索功能是谷歌的,搜索不了。cplusplus.com是英文的,但能搜索,转换成旧版本就行。

以sqrt函数为例:

三、自定义函数

程序员应该聚焦在自定义函数上,发挥创造性编写有特殊功能、库函数中没有的函数。

3.1、函数的语法形式

cpp 复制代码
函数类型 函数名(参数){
   ......
}
  • 函数类型:表示函数的计算结果的类型。可以是void,表示什么也不返回。
  • 函数名:为了一看到名字就知道是什么功能,尽量起有意义的名字。
  • 参数:()里的是函数的参数。若没有参数,可以写void,也可以什么也不写;若有参数,需要写明参数的类型和名字。参数的数量可以是:0~很多个。
  • 函数体:{}里的就是函数体,是完成计算的过程。

函数根据传入的参数,执行函数体,最后给出计算结果。如果函数定义时没有给出函数的返回值类型,则隐含类型是int

3.2、形参和实参

实参是函数调用时,传给函数的实际参数 。形参是函数定义时写在()里的形式参数。形参和实参是一一对应的。

形参在函数调用时,为了存储实参传递过来的值,才申请内存空间的过程,叫做形参的实例化 。形参实际上是实参的一份临时的拷贝

为什么这么说呢?举个例子:

在函数调用前,形参是不存在的。

进入函数后,形参才被分配了内存空间,可以看到x与a、y与b的地址是不一样的,但是存储的内容相同,所以说形参是实参的一份拷贝。

函数返回计算结果后,可以看到形参的内存被回收了,所以说这份拷贝是临时的。

最后,形参和实参的命名是可以相同的。

3.3、函数调用操作符

在上面的例子中,我们看到了函数调用的方式,()就是函数调用操作符,Add、a、b都是它的操作数。同样对于printf("哈哈!")库函数,printf 和 "哈哈!" 是()的操作数。

3.4、return语句

  • return语句执行后,函数会立马返回。
  • return语句后面可以是一个值,也可以是一个表达式。如果是表达式,则先执行表达式,再执行return。
  • 对于返回值是void类型的函数,当需要提前返回时,则可以写return;。注意,break只能在循环和switch语句中使用。
  • 如果返回值和函数的返回类型不匹配时,会将返回值强制转换成函数的返回值类型。
  • 如果函数中有分支语句,则需要保证每种情况的都有return返回,否则编译器会报警告。

被调用函数的返回值会返回给调用的函数,mian函数的返回值是返回给系统指定的一个函数。

3.5、数组做函数的参数

  • 函数的实参是数组,形参也可以写成数组的形式(但实际上是数组的地址)。如果是一维数组,可以省略数组的大小;如果是二维数组,可以省略行的大小。
  • 数组传参,形参不会创建新的数组,而是跟实参操作同一个数组。

看下面的例子,单独写一个函数打印数组:

可以看到形参和实参的地址是相同的。因为形参没有创建新的数组,所以形参不需要标明数组的大小(申请内存空间才需要知道要多大)。

又因为实参传给形参的是数组的地址,sizeof函数是无法根据数组的地址 (a数据类型是int*)来计算数组的大小(arr的数据类型是int[10])的,所以数组的大小必须通过参数传入,而不能在定义的函数里计算。

验证一下,用sizeof计算形参a的大小,将会是指针类型的大小:

数组的大小:一个整型4字节,10个整型40字节。

数组指针的大小:本质是指针类型,在32位系统下为4字节,在64位系统下为8字节。

3.6、函数的嵌套调用和链式访问

函数的嵌套调用 就是函数之间互相调用。链式访问就是一个函数的返回值作为另一个函数的实参。

从最里层开始,打印43,返回字符数2;打印2,返回字符数1;打印1。

3.7、函数的声明和定义

函数应该先声明再使用。如果函数的定义在函数的调用之后,则必须在函数调用之前声明函数:

如果没有函数声明,虽然也能运行正确,但是编译器会给出警告,需要改正:

当然,如果把函数的定义写在函数调用之前,函数的定义本身是一种特殊的函数声明。注意,函数的声明只要是在调用的前面就行,如下的位置也可以:

四、多个文件

当代码过多时,会根据程序的功能拆分到多个文件中。函数声明、类型声明放到头文件中(.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"的内容如下:

cpp 复制代码
int add(int a, int b);

在调用函数前,声明函数 int add(int a, int b); 和使用 #include "add.h"的作用是一样的,最终编译器会把#include "add.h"替换成具体的声明内容。

相关推荐
Theodore_102235 分钟前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
网易独家音乐人Mike Zhou1 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
----云烟----3 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024063 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic3 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it3 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康3 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神4 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
搬砖的小码农_Sky4 小时前
C语言:数组
c语言·数据结构
宅小海4 小时前
scala String
大数据·开发语言·scala