C语言中extern和头文件以及静态动态库概念梳理

最近因为Arduino使用的较多,开始学起来了C语言,看了还多天,无非就是基本的数据类型,运算符,控制语句,简单得很,后来发现这仅仅是语法层面,C语言是除了汇编最为底层的语言了,要了解这门语言,就要从了解支撑起C的底层原理开始了解,编译原理,操作系统,计算机硬件。而这次遇到的C语言的库文件,就好比Python中的标准库与第三方库,怎么共享,中间又是个什么过程,一团黑。因为涉及内容太多太多,我还是偏向于找一些好的资料然后分主题整理下来,自己再串起来,解惑也。

1. 先说说声明与定义

stackoverflow上有个很全面的回答,What is the difference between a definition and a declaration?

下面是我摘抄的一些简单概念:

定义(definition):表示创建变量或分配存储单元
声明(declaration):说明变量的性质,但并不分配存储单元
extern int i; //是声明,不是定义,没有分配内存
int i; //是定义
如果在声明的时候给变量赋值,那么就和去掉extern直接定义变量赋值是等价的

extern int a = 10;
int a = 10;//上述两条语句等价
谨记:声明可以多次,定义只能一次

2. extern与static

2.1 extern

当需要在外部文件导入函数或者变量时,我们可能很正常的找到了extern这个关键字,依然有篇经典的回答,extern的使用详解(多文件编程)——C语言

对变量而言,如果你想在本源文件中使用另一个源文件的变量,就需要在使用前用extern声明该变量,或者在头文件中用extern声明该变量;

不加extern也可以…源于某些不可描述的原因(可从上面那链接中找到原因)

对函数而言,如果你想在本源文件中使用另一个源文件的函数,就需要在使用前用声明该变量,声明函数加不加extern都没关系,所以在头文件中函数可以不用加extern。

2.2 static

对于static,我暂时用的不多,查看C primer plus书籍,我们看到static的三个用法

  • 块作用域的静态变量
  • 外部链接的静态变量
  • 内部链接的静态变量

相关问答:What does “static” mean in C?

上面的讲解都附带了一些例子,我们来看一些更清晰的例子: static vs extern

3. 头文件

对于我自己创建头文件这件事,我的第一反应是项目多文件的时候导入外部函数啥的用的,后来我发现了extern这个关键字,我就开始疑问,我都可以直接导入外部变量与函数,我为啥还要用头文件这个东西? 后来发现自己还是too young too simple!

其实头文件是种约定,对计算机而言没什么作用,它只是在预编译时在#include的地方展开一下,没别的意义了,其实头文件主要是给别人看的。

所以,按这么说,我在test.h文件中写着extern int max(int a,int b)(函数的extern可以省略),然后主文件中incluede "test.h",等价于我直接extern int max(int a,int b)放到主文件中,这就达到了和我所产生的疑问一样的目的,然后头文件作用不仅仅这样。

在以下场景中会使用头文件:

  • 通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功 能,而不必关心接口怎么实现的。
  • 多文件编译。将稍大的项目分成几个文件实现,通过头文件将其他文件的函数声明引入到当前文件。
  • 头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

既然头文件是一种规定,那么写头文件时也有相当多的规则,如何组织好 C 的头文件很有必要。

4. 编译系统

对与一个简单的hello程序

1
2
3
4
5
6
7
8
#include <stdio.h>

int main()
{
printf("hello, world\n");
return 0;
}

当我们调用gcc编译时,过程是这样的:

compilation system
关于对每个部分的具体相关命令,可以看下这篇文章前半部分

5. 链接

接下来我们着重讲讲链接以及相关的内容:

链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编’译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(load-er)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器(linker)的程序自动执行的。
链接器的两个主要任务是符号解析(symbol resolution)与重定位(relocation)

5.1 可重定位目标文件

在上面编译过程中的链接之前,我们经过预处理,编译,汇编,得到了.o文件,
也就是可重定位目标文件,包含了二进制代码与数据(是一堆乱码)。

5.2 函数库

这里说个函数库的概念,顾名思义:里边存放了一堆供程序员使用的函数。其实不但有函数名、函数对应的实现代码,还有链接过程中所需的重定位信息。函数库分为静态库(linux .a 文件 ,windows 为.lib文件)和动态库(.so,windows为.dll文件)文件。
当然,Linux 中也有标准的 C 函数库,里面有我们平常熟知的printfscanf等标准c函数,都统一在libc.a与libc.so中,会存放在某个文件夹,我们可以通过gcc --print-file-name=libc.a 查找
而用户,也可以根据自生需求,建立自己的用户函数库,这也是为什么我们上面说多个文件共享等等事情。

这里需要注意,由于函数库来自于 .o 文件,也就是说,是一堆二进制文件构成,你看不到里面的库函数代码。所以库函数该怎么用呢?这就体现了头文件中的重要性。所以 .h 文件与 .c 最好是要分开写。

6. 静态库

静态链接器(static linker)读取一组可重定位目标文件,将所有相关的目标模块打包成为一个单独的文件,称为静态库(static library),也可称之为静态函数库,最后再和主函数的.o文件链接起来创建一个可执行目标。

为什么会有静态库? 在静态库之前,我们可以选择这么做?

把所有printf标准函数放到libc.o中,然后gcc main.c /usr/lib/libc.o ,我们就可以用标准函数了。优点是我们将编译器的实现与标准函数实现分开了,但是缺点就是造成浪费,libc.o中我们有上百个函数,但我们平常的一个程序,我们可能仅仅用到printf等几个常用函数,全部导入进来,这样就造成空间浪费,还有就是对某个函数改变了,等重新编译整个源文件,开发维护复杂。

再者,我们可以为每个标准函数创建独立的.o文件,然后用:
gcc main.c /usr/lib/printf.o /usr/lib/scanf.o
这也是可行的,但是一看就知道太麻烦耗时

之后,静态库概念被提出来,以解决这些不同方法的缺点。相关的函数可以被编译为独立的目标模块,然后封装成一个单独的静态库文件。然后,应用程序可以通过在命令行上指定单独的文件名字来使用这些在库中定义的函数。比如,使用c标准库和数学库中函数的程序可以用形式如下的命令行來编译和链接:
gcc main.c /usr/lib/libm.a /usr/lib/libc.a
在链接时,链接器只复制被程序引用的目标模块,这就减少了磁盘跟内存大小。

在Linux系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。

简单举个例子来,有两个向量加法与乘法的.c文件,分别为addvec.c,multvec.c,还有个主文件main.c

  • 我们先生成.o文件:
    gcc -c addvec.c multvec.c
  • 创建静态库:
    ar rcs libvector.a addvec.o multvec.o
  • 编译主文件:
    gcc -c main.c
  • 最后链接:
    gcc -static -o a.out main.o ./libvector.a

7. 动态库

然而,静态库仍然有一些明显的缺点。静态库和所有的软件一样,需要定期维护和更新。如果应用程序员想要使用一个库的最新版本,他们必须以某种方式了解到该库的更新情况,然后显式地将他们的程序与更新了的库重新链接。
另一个问题是几乎每个C程序都使用标准I/O函数,比如printf和scanf。在运行时,这些函数的代码会被复制到每个运行进程的文本段中。在一个运行上百个进程的典型系统上,这将是对稀缺的内存系统资源的极大浪费。
共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起來。这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的。共享库也称为共享目标(shared object),在Linux系统中通常用.so后缀来表示。微软的操作系统大鼠地使用了共享库,它们称为DLL(动态链接库)
所有引用某库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行文件中

还是展示上面向量那个例子:

  • 构造共享库:
    gcc -shared -fpic -o libvector.so addvec.c multvec.c
  • 链接
    gcc -o a.out main.c ./libvector.so

这样就创建了一个可执行目标文件a.out,而此文件的形式使得它在运行时可以和libvector.so链接。基本的思路是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。认识到这一点是很重要的:此时,没有任何libvector.so的代码和数据节貞的被复制到可执行文件a.out中。反之,链接器复制了一些重定位和符号表信息,它们使得运行时可以解析对libvector.so中代码利数据的引用

所以静态库与动态库的区别:

静态库:
1.链接时将程序放进进可执行程序
2.会产生多分副本
3.不依赖程序运行
动态库:
1.程序运行时,加载时才去动态库找函数
2.多进程共享
3.依赖程序运行

还有个题外话,gcc链接标准库时默认是动态的还是静态的?

8. 总结

花了两天,算是把相关的概念过程整理清楚了,有些资料第一次读没有读懂,回过头来再细读,豁然开朗!花了蛮多精力,不知道以后有没有机会用C做代码量多一点的项目,哈哈~