C语言之函数,以及static和extern关键字


系列文章目录

前言

一、函数的概念

二、库函数

三、自定义函数

四、static和extern关键字

总结


前言

        本文将仔细介绍C语言函数的基础知识,主要包括库函数和自定义函数两类,介绍库函数的使用以及查阅,介绍自定义函数的定义语法、实参与形参、嵌套调用以及链式访问等,介绍static和extern关键字的作用和使用建议,希望对大家有所帮助


一、函数的概念

        C语言中的函数与数学中的函数概念相似,如同一个一次函数:y = kx + c ,传入一个x值就能返回一个y的值

        C语言中的函数就是⼀个完成某项特定的任务的一小段代码,它可以有返回值,也可以没有返回值,C语言的程序就是由无数个小的函数组合而成的,也可以说:一个大的计算任务可以分解成若干个较小的函数(对应较小的任务)完成。同时一个函数如果能完成某项特定任务的话,这个函数也是可异复用的,提升了开发软件的效率。

C语言中的函数一般可以分为两类:

  • 库函数
  • 自定义函数

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

       简单说就是ANSIC规定了常用函数的名字以及功能,然后像VS,GCC等编译器厂商自己将这些函数功能写出来,然后内置在编译器里,提供给开发者使用

        自定义函数:即为我们在写代码时根据需求自己编写的函数。那么自定义函数就非常重要了,它能使代码有更多的创造性,也是我们使用最多的一类函数,自定义函数的数量远远大于库函数的数量,

另外,熟练的程序员自己也可以写出一些与库函数功能相同的函数,但是一般不这么做,因为有现成的函数直接使用更好的提高开发效率

下面开始介绍库函数


二、库函数

1.使用方法

库函数的使用必须包含头文件,包含头文件的格式为:#include <头文件名>

像我们初学C语言时使用的最多的函数就是,printf函数和scanf函数,这两个函数就属于库函数,而使用库函数必须包含头文件,因此我们一般在主函数前需要写上 #include <stdio.h> 来包含头文件

我们在C/C++官网C 标准库头文件 - cppreference.com就能找到<stdio.h>

我们找到并点击stdio.h文件就能浏览该文件中包含的所有函数

除了stdio.h,C语言拥有众多的库函数,我们以math.h为例

2.举例

我们以math.h中的abs为例:

通过查阅我们知道abs是计算整数的绝对值,需要的参数为一个整形,返回值就是该数的绝对值


3.小结

库函数的内容非常多,但我们不需要所有的函数用法都记住,我们可以在用的时候去查,常用的库函数用的多了自然就记住了,平时看官方文档,知道有这个功能的函数,到真正需要时就能通过文档查阅的到


三、自定义函数

1.语法形式

ret_type   fun_name(形式参数)

{

        函数体;

}

  • ret_name是函数计算结果的返回类型,如int则返回一个整形数据,void则什么都不返回
  • fun_name是函数名,一般取名要与函数的功能相关
  • 形式参数简称形参,为函数接收的参数及类型,不需要参数可以不写
  • 大括号里即是函数体,即函数计算的过程

函数的调用就比较简单:直接写上函数名和需要的参数,如果函数有返回值,就可以用对应返回类型的变量进行接收,如果函数的返回类型为void,就不需要使用变量接收

fun_name(实际参数);

注意:函数调用传入的实参与函数定义中形参按照前后顺序一一对应


2.实参与形参

我们先举例一段代码:加法函数

首先我们定义了一个add函数,也就是加法函数

  • 它的返回类型为int,也就是返回一个整形数据
  • add为它的函数名
  • 它有两个参数,int x,int y,也就是函数的形参
  • 函数体就是计算两个形参的和

因为函数的返回类型为int,因此我们定义了一个整形变量c去接收函数的返回值

而调用函数中的a和b即为实际参数,简称实参

实参:就是真实传递给函数的参数

形参:add 函数的参数 x 和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在 函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化。

通过调试我们可以观察到形参和实参的地址和数值:

还未进入到函数之前,x,y变量并未在内存中申请空间

继续调试:

进入函数后,x,y变量创建,对应的值与实参a,b相同,但是对应的地址并不相同

所以我们可以理解为形参是实参的一份临时拷贝

拓展:像这样的传值函数,在函数内对形参的修改不会影响实参,因为变量储存的地址不一样。而假如是传址函数,也就是将实参的地址传给函数,而函数的形参为指针类型,这时候对形参的修改同样也会改变实参,因为它们的地址相同,这里涉及到指针的知识,仅作为一个扩展

关于形参和实参最后需要注意的是:形参和实参的参数名可以相同,因为这两处的变量作用域不相同,一般不会相互影响


3.return语句

我们先观察下面两段代码:

注:第一段代码中的add函数是上一段的简化版,也就是直接返回表达式,当然表达式会先计算

        第二段代码中使用了多组输入,也就是while搭配scanf函数使用

通过上面两段代码我们可以发现:

  • 第一段代码中的函数为int类型,返回了一个整形表达式
  • 第二段代码中的函数为void类型,无返回值,但使用了return语句,在满足传入的数能被3整除时执行打印语句,但只有第一条打印语句被执行,因为return语句直接终止了函数调用,并且什么都没有返回,在return语句后的代码就不会被执行

总结return语句的注意事项:

  • return后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式的结果。
  • return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。
  • return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型,一般我们都会返回与函数类型一致的结果
  • return语句执行后,函数就彻底返回,后边的代码不再执行。
  • 如果函数中存在if等分支的语句,则要保证每种情况下都有return返回,否则会出现编译错误,这种主要在除void的函数中需要注意

        


4.数组做函数的参数

我们在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作操作,这时候就有一些细节需要注意

如:分别对传入的一维数组和二维数组进行初始化,并在主函数中进行打印

一维数组:

二维数组:

通过之前对数组的学习,我们知道数组名为数组的首地址,因此这里其实是一个的传址函数(前面拓展有介绍),而在函数里面创建数组形参其实等同于一个指针类型的数组,形参的数组接收了实参数组传来的地址,本质上两个数组是一个数组,所以这里对形参数组的初始化也同样初始化了实参数组

以下我们总结关于数组作为函数参数需要注意的地方:

  • 函数的形式参数要和函数的实参个数匹配,即传参要传完整
  • 函数的实参是数组名,形参是可以写成数组形式的
  • 形参如果是⼀维数组,数组大小可以省略不写
  • 形参如果是⼆维数组,行可以省略,但是列不能省略
  • 数组传参,形参是不会创建新的数组的,本质上形参操作的数组和实参的数组是同⼀个数组


5.函数的嵌套调用

函数的嵌套即函数之间的互相调用

举例:我们写两个函数,一个用来判断素数,一个用来打印

#include <stdio.h>

//判断素数函数
int is_prime(int a)
{
    int i = 0;

    if (a == 1)
     {
           return 0;
     }

    for (i = 2; i < a; i++)
    {
        if (a % i == 0)
        {
            return 0;
        }
    }
    return 1;
}

//打印函数
void print(int a)
{
    if (is_prime(a))
    {
        printf("%d为素数\n", a);
    }
    else
    {
        printf("%d不是素数\n", a);
    }
}

int main()
{
    int a = 0;

    //多组输入
    while (scanf("%d", &a) != EOF)
    {
        print(a);
    }

    return 0;
}

执行结果:

这里我们在print函数里调用了is_prime函数,这就是函数的嵌套调用

当然不止这些:在函数内调用库函数也属于函数的嵌套调用

  • 我们在main函数中调用了scanf函数
  • 我们在print函数中除了调用is_prime函数里还调用了printf函数

这些都是函数的嵌套调用

注意:函数之间可以嵌套调用,但不能嵌套定义

即:以上面代码为例,我们不能在print函数内部去定义is_prime函数,在main函数中也不行,只能在函数外部去定义函数


6.链式访问

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

如:

库函数strlen的返回值作为了printf函数的参数,这就是函数的链式访问

接下来举一个特别的例子

#include <stdio.h>

int main()
{
    printf("%d", printf("%d", printf("%d", 43)));

    return 0;

这样3个printf函数的链式访问会打印什么在上面?

想要弄清楚这个,我们就必须了解printf函数的返回值,可以通过官网查询:

大致意思是prinf函数的返回值为打印在屏幕上的字符个数,输出错误则为负值

由此分析上面的代码:

  • 由链式逐层进入到最里层printf("%d",43)这个函数,它将会打印43两个数字在屏幕上,因此它将返回一个数字2
  • 第一个printf函数返回的数字2将作为外边第二个printf函数的参数,因此第二个printf函数将会在屏幕上打印一个2,由于只打印一个字符,因此第二个printf函数返回了一个数字1
  • 第二个printf函数返回的数字1作为了第三个printf函数的参数,因此最外边也就是第三个printf函数将打印一个1在屏幕上,而它的返回值没有被接收,链式访问由此停止
  • 最后屏幕上依次会打印4321

结果:


7.函数的声明与定义

与定义变量一样我们一般先定义变量再去使用,因为编译器是按顺序从上往下执行代码,在没有定义变量就去使用时,编译器就会报错。函数也是一样,所以我们前面展示的函数定义都是写在主函数main的前面

我们把函数定义在main函数上面与下面对比:

上面:代码正常执行

下面:虽然VS2022比较聪明,能够正常执行代码,但是依旧报了警告—

—显示add未定义

这是因为:在C语言中,函数必须先声明再使用

如果我们想把函数写在main函数下面,我们需要再main函数上面进行声明

函数声明的语法:

ret_type   fun_name(形式参数);

与定义函数一样,函数的声明需要写上该函数的类型,函数名,函数参数(没有可以省略),需要注意的是末尾的一个分号

如:这次没有任何警告

注意:在函数的声明里,形参的变量名可以省略:

另外,我们在main函数前面定义函数后不用再声明函数的原因为:

函数的定义也是一种特殊的声明


8.多文件的函数声明与定义

前面我们所讲的函数声明与定义是单个文件中,而一般在企业中我们写代码时候,代码可能比较多,不会将所有的代码都放在一个文件中;我们往往会根据程序的功能,将代码拆分放在多个文件中。 ⼀般情况下,函数的声明、类型的声明放在头文件(.h)中,函数的实现是放在源文件(.c)文件中。

以上面加法函数为例:我们把函数声明放在.h头文件中,函数的定义放在.c源文件中

我们将add函数声明放在头文件中(注:在头文件下添加创建.h文件即可)

我们将add函数的定义放在新创建的.c文件中

最后我们需要在主程序中使用该函数,那么使用函数就应该先声明再使用,我们像包含标准库头文件一样包含自定义函数的头文件,只不过<>符号需改为" "符号

如:

注:多文件的函数声明与定义能帮助我们在代码比较多的情况下更高效的进行开发,管理代码,也能够很好的进行团队之间的合作,因此非常重要


四、static和extern关键字

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

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

extern 是用来声明外部符号的,比如在没有头文件的情况下,在另一个.c文件中写的变量或者函数就可以使用extern关键字进行引用

1.static修饰局部变量

如图:对比

我们很容易就能发现,在两次调用test函数后,被static修饰的局部变量 i 的值进行了累加,而并没有在第一次调用完后被消除

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

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


2.static修饰全局变量

如图:对比

未修饰:

修饰后:

我们发现,没有被static修饰的全局变量num可以在被extern引用后正常使用,而被static修饰后的全局变量num却无法被extern引用

结论:

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

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


3.static修饰函数

如图:对比

未修饰:

修饰后:

同修饰全局变量一样,函数没有被正常引用

结论:

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

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


总结

        以上就是C语言函数的基本知识了,希望对大家有所帮助,有问题可以评论区提出,如果有哪里写的不好提出来我一定改正,感谢大家的支持

  • 43
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值