上一篇 中,我们了解了函数的概念,以及函数实现与程序编译过程。本章节,我们分析内部函数和外部函数,以及变量的生命周期。
章节预览:
4. 外部函数与内部函数
4.1 外部函数
4.2 内部函数
5. 变量的生命周期与作用域
目录预览
章节内容:
4. 外部函数与内部函数
默认情况下,我们定义或声明的函数属于“外部”函数,又称为“全局”函数。全局函数即可以被本文件中的其他函数调用,也可以被其他文件中的函数调用。首先,我们分析函数如何在其他文件中调用到。
接下来,编写一个多文件工程(预编译加载函数实现):
math.c代码:
int Add(int a, int b)
{
return a + b;
}
main.c代码:
#include #include “math.c”
int main()
{
int res = Add(2, 3);
printf(“res:%d.”, res);
return 0;
}
在这个工程中,math.c属于源文件,main.c属于主文件(包含main函数)。 我们在math.c文件内实现了Add全局函数,在main.c内的main内调用Add函数。这个程序中,我们相当于把math.c当做头文件使用,为什么是这样呢?接下来,我们分析一下编译过程:
一. 预编译:
在控制台中输入gcc -E main.c -o main.i,预处理器把main.c中包含的文件经过预处理后写入main.i,打开main.i文件可以看到stdio.h和math.c文件预处理后的内容,而Add函数定义也在这一步保存在了main.i文件中。
二. 编译为汇编代码:
在控制台中输入gcc -S main.i -o main.s,编译器把main.i中的内容转换为汇编代码后写入main.s,打开main.s文件可以看到Add和main函数定义的汇编实现代码,以及main函数调用Add函数的过程。
三. 生成为目标文件:
在控制台中输入gcc -c main.s -o main.o,汇编器把mian.s中的内容编译为机器码后写入main.o,这里面就包含了供程序执行使用的Add和main函数的实现部分(可以通过hexdump -C main指令查看具体内容)。
四. 链接:
在控制台中输入gcc -o main main.o,链接器把main.o中的Add和main函数实现部分打包生成一个可执行程序main(可以通过hexdump -C main指令查看具体内容)。
实际编译:
实际编译时我们不需要了解其中的细节,控制台中输入gcc -o main main.c。
另一种形式,编写一个多文件工程(链接加载函数实现):
math.c代码:
int Add(int a, int b)
{
return a + b;
}
math.h代码:
extern int Add(int a, int b);
main.c代码:
#include #include “math.h”
int main()
{
int res = Add(2, 3);
printf(“res:%d.”, res);
return 0;
}
在这个工程中,math.c属于源文件,math.h属于头文件,main.c属于主文件(包含main函数)。我们在math.c文件内实现了Add全局函数,在math.h文件内声明Add全局函数,在main.c内的main内调用Add函数。这个程序中,我们相当于把math.c当做外部库使用(链接时加载),而math.h内的Add函数声明作为生成目标文件(main.o)时的Add函数原型配对。
实际编译:
在上个示例中,我们了解工程编译过程,这里我们简单介绍一下编译流程。
控制台中输入gcc -o main main.c math.c,这里相当于把main.c和math.c当做两个模块分别执行预编译、编译为汇编代码、生成为目标文件,每一步得到两个文件,比如分别执行gcc -E main.c -o main.i、gcc -E math.c -o math.i后,得到main.i和math.i。执行到链接时,把main.o和math.o中的Add、main函数实现部分打包生成一个可执行程序main。
4.1 外部函数
上述示例中,我们了解了自定义函数如何编译到可执行程序中,以及在别的文件内的调用方式。实际编程时,如果被调用的函数定义不在本文件中,可以通过在函数声明左侧增加extern关键字,表示这个函数来自外部(别的文件或外部库)。比如,我们为程序执行预编译操作后,查看main.i文件可以了解到Add函数声明,参考图4-1。
图4-1 预处理文件
C语言中,函数声明分为两种形式:一种是形式声明(函数原型,上述所说的函数声明),另一种是实体声明(函数定义)。
当程序编译时,一个函数可以存在多个形式声明,只能存在一个实体声明。比如上述示例中,可以在main.c中形式声明Add函数,但不能实体声明Add函数。
形式声明的作用也可以分为两种情况:一种是为了编程人员方便查看函数原型,另一种是在生成目标文件时编译器匹配函数原型。
4.2 内部函数
在定义函数时,可以通过指定函数存储类别表示函数的作用域 (6)。函数存储类别分为“static”和上述所说的“extern”,其中static表示内部函数,extern表示外部函数。
定义函数时,如果省略extern关键字,则默认为是外部函数。但定义内部函数时,不能省略static关键字。
内部函数定义形式:
static 返回类型 函数名称(参数列表)
{
函数体
}
内部函数定义举例:
static void func(int a)
{
printf(“a的值为:%d”, a);
}
内部函数又称为静态函数,它只能被本文件内的其他函数所调用。使用内部函数可以确保其他文件中即使有相同命名的内部函数,也互不干扰。
(6):编程世界中,作用域一般表示某个变量、某个常量、某个函数的可使用范围区域。比如Add全局函数可以在math.c文件内使用,也可以在main.c文件内使用,它的作用域属于整个工程内。
5. 变量的生命周期与作用域
在之前编写的示例中,我们都是在函数中定义变量。比如在main函数中定义int a、自实现函数中定义char c等,这些变量随函数入栈 (7) 后分配栈内的内存空间(一般称作压入栈),然后随函数出栈时反顺序出栈(后入先出),最后栈地址跳转到函数入栈时的地址。从这里我们可以了解到,函数中定义的变量生命周期随函数入栈分配变量内存空间生效,随函数出栈时失效,这类变量统称为局部变量。当然,局部变量还有另外一种情况,在变量定义时指定存储类别改变变量的生命周期。
总体来看,在一个函数内部定义的变量只在本函数内有效,也可以认为只能在本函数中使用这个变量,而这个变量的有效范围也是由“作用域”表示。
(7):栈内存属于扩充形式增长内存。比如当前栈内存分配100k,如果在一个函数中栈内存用到500k,则栈内存自动扩充到500k(大致数据,满足栈内存需求),当这个函数退出后栈内存还是会保持在500k,一直到进程退出后归还栈内空间。
首先,我们了解局部变量的定义方式:
1.在函数内的开头定义变量:
2.在函数内的复合语句内定义变量:
3.在函数内的执行语句后定义变量:
4.函数内的形参变量:
如果我们需要变量在多个函数内部都允许使用时,可以定义全局变量。全局变量定义在函数外部,又称为全程变量,可以在本文件或别的文件内的不同函数内部使用。
通常情况下,全局变量的有效区域在定义变量处至文件末尾,生命周期从程序开始执行到程序执行结束。
接下来,我们了解全局变量的定义方式:
全局变量定义在预处理、宏定义下方至函数之间比较常见,如int a,可以在本文件中的所有函数内部使用。示例中,全局变量a和c随main函数执行生效至main函数退出失效。
目录预览