导图社区 C语言思维导图
本图讲述了C语言编译过程及函数库的封装,包括C语言编译过程、C语言动态库和静态库、动态库和静态库的对比,感兴趣的收藏下图了解吧!
编辑于2021-08-04 22:26:00C语言编译过程及函数库的封装
C语言编译过程
编译器
类别
C++编译器是一种系统软件,用来将源代码转换成机器码。现在比较著名的编译器有Microsoft的CL、Borland的BCC、Intel的ICC,以及开源的、跨平台的GCC。虽然C++标准是唯一的,但各个编译器的实现未必一致。尽管如此,各种编译器的组成以及编译过程还是基本一致的。
编译器的组成结构
预处理器
根据源代码中的预处理指令,对程序文本进行处理
程序前两行行首的字符“#”是一个预处理标志。程序在编译之前,先运行预处理器。预处理器遍历整个源代码,找到由“#”开始的行,并执行其后的预处理指令。include是一条预处理指令,其含义是将后面的文件复制到当前源代码文件中。include指令后面的文件,在C和C++中习惯上称为头文件。文件名周围的尖括号“<>”表明这个文件是一个工程或标准头文件。预处理器首先从预定义的目录中开始查找,预定义的目录可通过环境变量和预处理器的命令行选项指定。如果文件名被双引号("")包围,则从当前目录开始查找。
词法分析器
词法分析器的作用是从左至右逐个字符地对源程序进行扫描,用其中由字符组成的单词产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。(输出符号表)
。例如对于“aVar = bVar + cVar;”语句,词法分析的目标就是要找出aVar,bVar,cVar,以及“=”和“+”这些符号,并结合符号表对其进行管理,
语法分析器
以上述中间程序作为输入,分析其中的单词符号串是否符合语法规则。语法分析器首先判断各种单词符号所属的类型,如表达式、赋值、循环等。然后判断这些符号是否符合语法规则,再按照语法规则将每条语句构造成语法树或其他结构。(输出语法树)
语义分析器
根据语义规则对语法树中的语法单元进行静态语义检查,如类型检查和转换等,其目的在于保证语法正确的结构在语义上也是合法的。
,右侧的子树表现出来的语法结构是两个操作数相加,语义分析器的任务就是判断这两个操作数能否相加,需不需要进行类型转换等。
如果需要进行类型转换等操作,就在上述语法树中插入一些操作,构造新的语法树,然后再进行语义分析。在这个过程中,如果有不符合语法、语义的代码,编译器就会报错。
代码优化器
生成较短的目标代码。
充分利用计算机中的寄存器,减少目标代码访问存储单元的次数。
充分利用计算机指令系统的特点,以提高目标代码的质量。
机器码生成器
目标代码生成器把语法分析后或优化后的中间代码变换成目标代码
目标代码有以下三种形式
可以立即执行的机器语言代码
待装配的机器语言模块,由链接程序将其和其他模块链接起来,转换成能执行的机器语言代码。
汇编语言代码,必须经过汇编程序汇编后,才能成为可执行的机器语言代码。
源代码的组成
预处理指令
名称空间
标准C++有一个std名称空间,所有C++标准库中的标识符都在这个名称空间中。例如标准IO对象cin和cout就定义在这个名称空间中:
指明名称空间的方法有两种
一种是在标识符前面加上名称空间名和域运算符“::”,例如std::cin(std是标准C++名称空间);
另外一种是在使用标识符之前,使用using namespace加上名称空间名,这样以后用该名称空间中的标识符时就不用再指明了。
注释
注释要说明设计的原因,而不是描述代码:阅读代码的人更加关心的是程序为什么要这么设计,而不是代码的功能。
函数:一般函数
返回类型(最左面的int)、函数名(add)、参数列表(int x, int y)和函数体。
函数名和参数列表唯一标识了程序中的一个函数。
语句
变量
主函数
调用函数
cout对象
宏对象
程序开发生成过程
源代码书写
完成,不代表一个程序开发结束,还需要对源代码进行编译和链接。
编译
编译的目的是将源代码转换成计算机可以辨认的二进制指令和数据,并保存到一个二进制文件中,即目标文件
目标文件通常以.obj作为扩展名。(.o) 编译成的目标文件仍然不是可执行程序
链接
链接器
将目标文件和各种必需的库链接成可执行程序。
生成可执行文件
示例
gcc
常见的三个参数
-I(大写的i)
示例1
main.c
_strlen.c
header.h
print_string.c
header.h
stdio.h
stdio.h
#include <stdio.h> #include "header.h" /* main - Entry point. * Return: 0 - success. */ int main(void) { char string1[] = "I love you"; char string2 [] = "mama and yuanqiuxia "; // 调用打印函数 print_string(string1); printf("\n"); print_string(string2); printf("\n"); // 调用长度函数 printf("Size of string 1: %d\n",_strlen(string1)); printf("Size of string 2: %d\n",_strlen(string2)); return (0); }
#include "header.h" /** * _strlen - returns the length of a string. * @s: input string. * Return: length of a string. */ int _strlen(char *s) { int count = 0; while (*(s + count) != '\0') count++; return (count); }
#include <stdio.h> #include "header.h" /** * print_string - print a string. * @s: input string. * Return: no return. */ void print_string(char *s) { int i, j; char temp; for(i=0; *(s + i)!='\0'; i++) { printf("%c\t", *(s+i)); } }
#ifndef _HEADER_H_ #define _HEADER_H_ /* header.h*/ int _strlen(char *s); void print_string(char *s); #endif
编译
gcc main.c _strlen.c print_string.c
生成了一个a.out文件可执行
执行
./a.out
我们把头文件统一放到inc文件夹中
rm -rf a.out
mkdir inc
mv header.h inc
继续编译
gcc -I ./inc/ main.c strlen.c print_string.c
总结
上面把add.h移动到inc目录下后, gcc就找不到add.h了, 所以报错。 此时,要利用-I来显式指定头文件的所在地, -I就是用来干这个的:告诉gcc去哪里找头文件。
-I后面紧跟着用户设定的编译器头文件查找路径
如: -I/my_include_path/
-L(大写的l)
我们上面已经说了, -I是用来告诉gcc去哪里找头文件的, 那么-L实际上也很类似, 它是用来告诉gcc去哪里找库文件。 通常来讲, gcc默认会在程序当前目录、/lib、/usr/lib和/usr/local/lib下找对应的库。
-L后面紧跟着用户设定的编译器库文件查找路径
如: -L/my_lib_search_path/
-l(小写的l)
Linux中的静态库和动态库, -l的作用就是用来指定具体的静态库、动态库是哪个。
-l用来指明编译器要链接哪些库
如: gcc test.c -o test -lmylibname
C语言动态库和静态库
背景
通常,有一些代码行需要在源代码的不同部分执行。 为了避免出现大而杂乱的重复行的程序代码,使用了称为代码重构(code refactoring)的软件开发技术。 它包括更改源代码的结构,而不影响其功能。 应用重构技术,在应执行的程序代码部分调用的函数中写入重复的行。 为了在源代码中调用这些函数,需要创建和使用一个库。
库的定义
库是收集多个目标文件的文件。 它在源代码编译后被合并, 作为编译过程的链接阶段中的单个实体
库的优点
更加模块化,这意味着代码可以被分成模块,更具可读性和可管理性。
重新编译更快,因为链接在库中排序的源代码和目标文件比链接此代码和分离的目标文件要快。
更容易更新
分类
动态库
动态库 是在构建时引用的对象文件的集合,以提供可执行信息,它们最终将如何使用,但它们直到运行时才会使用。换句话说,这些对象被动态链接到使用它们的可执行文件中。
静态库
它们是一组目标文件,在源代码由静态链接器编译之前连接到源代码,使其完全自给自足。
区别
Unix 系统允许创建两种类型的库:静态库和共享库。 在静态库中,目标文件在程序可以运行之前安装到编译文件中。
在共享库中,当程序启动时,库被称为动态加载器的系统加载到内存中。
静态库封装方法
静态库存储已经预编译的文件(二进制代码)。 该目标文件在链接阶段被合并到源程序的编译文件中。 然后,生成一个可执行文件,其中包含源文件的二进制代码以及驻留在静态库中的目标文件。 这个工作过程如图 1 所示。在这个例子中,main.c 文件中有一个入口点,它调用存储在静态库中的函数 sort_char。 源代码被转换为目标文件 (main.o)。 然后,链接器将 sort_char 的目标文件添加到编译文件中,最后生成一个包含这两个代码的可执行文件 (main.out)。
优点
没有兼容性问题
链接目标文件在库中排序的程序比链接目标文件在磁盘上分开的程序要快。
缺点
另一方面,静态库有一些缺点。 当可执行文件与存储大量目标文件的静态库链接时,它的大小会增加。 此外,如果在各种源程序中调用某个静态库的某些函数,则它们被内置在每个可执行文件中,从而增加了使用的内存空间。 在这种情况下,最好创建和使用共享库。
示例
假设示例1中目录下只有上面描述的函数和头文件:
header.h print_string.c _strlen.c
我把main.c函数移入了inc文件夹中
为了创建上述函数的静态库,需要使用 GCC 编译器生成它们的目标文件:
gcc -Wall -pedantic -Werror -Wextra -c *.c
结果为什么报错:
这里表示所有的warning均看作错误 华为思想,咱不能被警告!!!
修改之后(是我声明了没有用的变量)
我们发现生成了两个.o文件
分析命令参数
-Wall
编译后显示所有警告
-w(小w)
-w的意思是关闭编译时的警告,也就是编译后不显示任何warning,因为有时在编译之后编译器会显示一些例如数据转换之类的警告,这些警告是我们平时可以忽略的。
-W(大w)
-W选项类似-Wall,会显示警告,但是只显示编译器认为会出现错误的警告。
-c
“-c”标志允许生成驻留在当前目录 (*.c) 中的 c 文件的编译程序,而无需链接它们。
第二步
为了创建静态库,它使用了一个名为“ar”的命令,意思是“archiver”。 使用这个程序,可以修改和列出静态库中目标文件的名称:
ar -rc lib_string.a print_string.o _strlen.o
得到了lib_string.a静态库
分析命令参数
-rc
执行此命令时,会创建一个名为“lib_string.a”的静态库,其中包含目标文件“rev_string.o”和“_strlen.o”的副本。 ‘r’ 标志允许用库中的新目标文件替换旧目标文件。 如果库不存在,‘c’标志允许创建库。
第三步
使用“-t”标志列出目标文件的名称
第四步
对库进行索引
然后有必要对库进行索引。 编译器(特别是链接器)使用索引来加速库内的符号查找。 “ranlib”是用于索引静态库的命令:
ranlib lib_string.a
静态库的使用方法
要使用该库,一旦创建,需要在编译源代码时添加其名称:
gcc main.c -L. -l_string -o main
此命令行生成可执行文件。 “-l”标志允许通过其名称读取库,提供链接器“_string”。 在此步骤中,省略了前缀“lib”和“.a”后缀。 “-L”标志向链接器指示必须在给定目录中找到库(“.”代表当前目录)。 如果未使用此标志,编译器将搜索 usr/bin/lib 中的库,即标准库所在的位置。
总结
注意: 1. Linux下进行链接时, 默认是链接动态库。 2. 如果需要使用静态库,需要使用 编译选项-static。 例: gcc -static test.c -o test 制作静态库: 1. gcc –c mylib.c –o mylib.o(只编译不连接) 2. ar cqs libmylib.a mylib.o 3. 将制作好的libmylib.a 复制到/usr/lib目录下 注意: 在链接时,若要使用静态库,则需要增加编译选项: -lname:GCC在链接时,默认只会链接C函数库,而对于其他的函数库,则需要使用 -l 选项来显示地指明需要的链接库。 例:gcc test.c –lmylib -o test
红帽企业版6默认不提供libc.a静态函数库,需要安装:
rpm -ivh glibc-static-2.12………….i686.rpm
动态库
程序所要用到的库函数代码在链接时不会全部被复制到每个程序中,只是复制一次,每个程序共同分享函数库。
制作动态库:
gcc –c mylib.c –o mylib.o
gcc -shared -fPIC mylib.o -o libmylib.so
将制作好的libmylib.so 复制到/usr/lib
-fpic 使输出的对象模块是按照可重定位地址方式生成的。
-shared 指明产生动态链接库。
使用动态库
使用动态函数库同样需要使用 -l 编译选项。
例:gcc test.c –lmylib -o test
例:gcc test.c –lmylib -o test
查看
使用 readelf -d test 可以查看程序使用的函数库。
动态库和静态库的对比
1、静态链接库的优点:
代码装载速度快,执行速度略比动态链接库快
只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱等问题。
2、动态链接库的优点:
更加节省内存并减少页面交换
DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性
不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
3、两者各自的不足之处:
使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费;
使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;
速度比静态链接慢 。当某个模块更新后,如果新模块与旧的模块不兼容,则程序就无法正常运行。