函数的本质

在C语言当中,我们可以把函数当做一个"任务"或者"一个功能"

当我们把一块西瓜,放入榨汁机当中进行处理后,最后得到了一杯西瓜汁,整个过程其实就在执行一个特定的任务或者操作。

C语言中函数也是一样的,给一个"参数",最终经过一系列"步骤",得到一个"结果"。整个过程其实就是在完成一个任务或者特定的功能

1. 函数分类

C语言中的函数大致分为2类

  • 库函数
  • 自定义函数

 

2. 库函数

2.1 标准库和头文件

标准库的概念

C 标准库(C Standard Library)包含了一组头文件,这些头文件提供了许多函数和宏,用于处理输入输出、字符串操作、数学计算、内存管理等常见编程任务。C语言标准给出了一系列函数的实现,这些函数就被称为库函数。

我们前面内容中学到的printf、scanf都是库函数,库函数也是函数,不过这些函数已经是现成的,我们只要学会就能直接使用了。有了库函数,一些常见的功能就不需要程序员自己实现了,一定程度提升了效率;同时库函数的质量和执行效率上都更有保证。

各种编译器的标准库中提供了一系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行了声明。

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

有数学相关的,有字符串相关的,有日期相关的等,每一个头文件中都包含了,相关的函数和类型等信息,库函数的学习不用着急一次性全部学会,慢慢学习,各个击破就行。

2.2 库函数的使用方法

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

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

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

举例:sqrt    pow

函数原型:

double sqrt (double x);

  • sqrt 是函数名
  • x 是函数的参数,表示调用sqrt函数需要传递一个double类型的值
  • double 是返回值类型 - 表示函数计算的结果是double类型的值

double pow (double   base, double  exponet);

  • pow是函数名
  • base是底数,exponet是指数。表示调用pow函数需要传递一个double类型的值
  • double 是返回值类型 - 表示函数计算的结果是double类型的值

2.2.1 功能

sqrt

Compute square root (计算平方根)。

Returns the square root of x. (返回x的平方根)。

pow

Compute the exponential function(计算指数函数)

Returns the value of the exponential function(返回指数函数的值)

2.2.2 头文件包含

库函数是在标准库中对应的头文件中声明的,所以库函数的使用,务必包含对应的头文件,不包含是可能会出现一些问题的。sqrt 函数需要包含头文件 <math.h>

2.2.3 实践

sqrt

pow

 

2.2.4 库函数文档的一般格式

  1. 函数原型
  2. 函数功能介绍
  3. 参数和返回类型说明
  4. 代码举例
  5. 代码输出
  6. 相关知识链接

3. 自定义函数

自定义函数的必要性

库函数再多,都不能满足程序员的特殊需求。这时候我们就得自己定义函数。

3.1 函数的语法形式

其实自定义函数和库函数是一样的,形式如下:

函数语法格式:

ret_type fun_name(形式参数)
{
// 函数体
}

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

 

3.2 函数定义

1. 写一个函数求2个整数的和

2. 写一个函数求n的阶乘

  • 函数的名字最好见名思意,不要花里胡哨。
  • return 的是结果,同时也表示函数的结束。

 

定义函数的理解

 


区分


注意

  • int 和void的区别
  • void为无返回值类型,无需返回值
  • int需要返回值,但return只能返回一个返回值

 

3.3 函数调用

上述只是函数的定义,并不能被调用。我们以在main函数调用来进行举例。

示例1:

  • 函数调用需要使用:函数名(参数列表)的方式进行调用。
  • 函数调用过程当中参数的个数、数据类型、顺序均要匹配。
  • 函数调用过程当中返回值类型要匹配。

示例2:

 

补充

 

3.4 函数声明

上述函数调用修改如下方式,把函数的定义放在main函数之后。重新编译程序:

编译警告:

warning C4013: "fac"未定义;假设外部返回 int

原因:C语言在编译的时候是自顶向下进行编译的。在编译到fac的时候,编译器并没有看到fac函数的定义,所以就会出现警告。

解决方案:函数声明

解决办法就是在main函数之前进行声明,提前告知程序,函数是存在的。

 

建议:我们一般建议,把main函数写到最后,因为只要多一步操作,就有一次错误的风险

4. 实参和形参

4.1 实参

实参的定义

实参即实际参数,是在调用函数时,传递给函数的真实值。它可以是常量、变量、表达式或函数返回值。

在上述代码的 main 函数中,调用 fac(a) 时的变量 a 就是实参。

4.2 形参

形参的定义

形参即形式参数,是在定义函数时,函数名后面括号中的变量。它作为函数内部的局部变量,用于接收调用时传入的实参值。

在 int fac(int n)的函数定义中,变量 n 就是形参。

4.3 实参和形参的关系

参数传递机制:值传递

在目前的知识储备之下,我们可以理解为:

  • 形参和实参是2个不同的内存空间。
  • 函数调用时,实参把值传递给了形参,形参是实参的一份临时拷贝。

这个现象是可以通过调试来观察的。请看下面的代码和调试演示:


验证示例:尝试交换2个数

重要提示

因为形参只是实参的拷贝,所以在函数内部修改形参的值,无法影响到main函数中的实参。要解决这个问题,我们得引入指针!这个问题我们将在指针章节解决。

 

5. 数组做函数参数

在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。

比如:写一个函数将一个整型数组的内容,全部置为-1,再写一个函数打印数组的内容。

简单思考一下,基本的形式应该是这样的:

分析

1. set_arr需要哪些参数

2. print_arr()需要哪些参数

 

改进后的 main 函数调用如下:

介绍数组名字arr代表数组首个元素的地址

即arr=arr[0]=arr[ ]

数组作为参数传递给了 set_arr 和 print_arr 函数了,那这两个函数应该如何设计呢?

6. 嵌套调用和链式访问

6.1 嵌套调用

示例:求数字n的阶乘和,如:5!的和为 1! + 2! + 3! + 4! + 5!

1. 计算单个数字阶乘的函数

2. 计算阶乘和的函数

6.2 链式访问

所谓链式访问就是将一个函数的返回值作为另外一个函数的参数,像链条一样将函数串起来进行调用。这种方式可以使代码更紧凑。

示例:求n的阶乘的平方?

7. static 和 extern

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

static (静态的)

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

extern (外部的)

extern 用于声明一个在其他源文件中定义的全局变量或函数。

前置知识:作用域与生命周期

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

  • 局部变量的作用域:变量所在的局部范围(如函数内部)。
  • 全局变量的作用域:整个工程(项目)。

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

  • 局部变量的生命周期:进入作用域变量创建,生命周期开始,出作用域生命周期结束。
  • 全局变量的生命周期:与整个程序的生命周期相同。

7.1 static 的用法

7.1.1 static 修饰局部变量

对比下面两段代码的效果,可以理解static修饰局部变量的意义。

代码1


代码2


对比代码1和代码2的效果,理解static修饰局部变量的意义。

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

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

结论

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

static将局部变量i由栈区放到静态区。

使用建议

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

7.1.2 static 修饰全局变量

 

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

 

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

结论

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

使用建议

如果一个全局变量只想在它所在的源文件内部使用,不想被其他文件意外地访问或修改,就可以使用static修饰。

7.1.3 static 修饰函数

代码1

add.c


test.c


代码2

add.c


test.c


代码1是能够正常运行的,但是代码2就出现了链接错误。

 

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

使用建议

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

8. 多文件下的代码书写

在公司日常开发过程当中,我们经常会涉及多文件下的代码书写,项目代码不可能只放在一个.c文件中。

新建fac.c文件,把阶乘代码放进去。

按照上述方法,可以通过extern关键字进行声明。

那如果fac.c中再有一个函数呢?

test.c的繁琐形式如下

解决方案:使用头文件

为了解决多个 extern 声明带来的管理问题,我们可以使用头文件(.h 文件)来统一管理所有外部函数的声明。

定义 fac.h (头文件)

定义 fac.c (源文件)

test.c实现方式

这样就解决了多个extern带来的问题。

问题:使用尖括号引入头文件和使用双引号引入头文件的区别是什么?

主要区别在于编译器搜索头文件的路径和顺序不同

  1. 使用双引号时,编译器会按照以下顺序查找头文件:
    1. 当前目录:首先在包含此 #include 指令的源文件所在的目录中查找。
    2. 系统目录:如果在当前目录中没有找到该文件,编译器会接着到系统指定的标准包含目录中查找,这个过程和使用尖括号一样。
    3. 使用场景:通常用于包含项目自定义的头文件。因为这些头文件通常与源文件存放在同一个项目目录或其子目录中。
  2. 使用尖括号时,编译器只会在系统指定的标准包含目录中查找头文件。 它不会在源文件所在的当前目录中查找。

这些标准目录通常包括:

1. 编译器安装时自带的头文件目录(例如 /usr/include)。

2. Windows上安装的目录,比如我们可以找到stdio.h的所在的目录。(具体方法可以右键代码中的stdio.h,打开该文件,从而打开文件所在目录)

3. 使用场景:通常用于包含C标准库头文件外部库的头文件

以后书写建议

例如

📋 本节总结

  • 函数的概念:完成特定任务的代码块。
  • 函数的分类:库函数和自定义函数。
  • 自定义函数的定义、调用和声明三要素。
  • 参数传递机制:实参的值被拷贝给形参(值传递)。
  • 数组作为函数参数时,通常需要同时传递数组大小。
  • 函数的嵌套调用(函数内调函数)与链式访问(返回值作参数)。
  • static关键字改变变量的生命周期(存储位置)和符号的链接属性。
  • 使用头文件(.h)和源文件(.c)进行多文件模块化编程。

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐