C语言代码的条件编译,动态库,静态库的制作

先来重温下C语言编译过程(以hello world为例)
C语言代码的条件编译,动态库,静态库的制作
1.预处理(Preproceessing)
预处理是读取c源程序,对其中的伪指令(以#开头的指令,也就是宏)和特殊符号进行“替代”处理;经过此处理,生成一个 没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,仍然是C文 件,但内容有所不同。 伪指令主要包括以下三个方面:
(1)宏定义指令,如#define Name TokenString,#undef以及编译器内建的一些宏,如_DATE_和_FILE_和_LINE_和_TIME_和_FUNCTION_等。
(2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。
(3) 头文件包含指令,如#include "FileName"或者#include <FileName>等。
预处理的过程主要处理包括以下过程:

将所有的#define删除,并且展开所有的宏定义 处理所有的条件预编译指令,比如#if ,#ifdef, #elif ,#else, #endif等处理。#include 预编译指令,将被包含的文件插入到该预编译指令的位置。 删除所有注释 “//”和”/* */”. 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。 保留所有的#pragma编译器指令,因为编译器需要使用它们
通常使用以下命令来进行预处理,参数-E表示只进行预处理:
C语言代码的条件编译,动态库,静态库的制作

gcc -E hello.c -o hello.i

C语言代码的条件编译,动态库,静态库的制作
C语言代码的条件编译,动态库,静态库的制作
也可以使用以下指令完成预处理过程,其中cpp是预处理器:
预处理后的结果hello.i还是C语言源代码,我们可以使用cat或vim命令查看他的代码。
宏定义:

#define            //定义一个预处理宏
#undef           // 取消宏的定义

#if                   //编译预处理中的条件命令,相当于C语法中的if语句
#ifdef              //判断某个宏是否被定义,若已定义,执行随后的语句
#ifndef            //与#ifdef相反,判断某个宏是否未被定义
#elif                //若#if, #ifdef, #ifndef或前面的#elif条件不满足,则执行#elif之后的语句,相当于C语法中的else-if
#else              //与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else
#endif             //#if, #ifdef, #ifndef这些条件命令的结束标志.
defined          //与#if, #elif配合使用,判断某个宏是否被定义

几种预处理命令及其使用方法,其中包括 #line 命令、#error 命令和 #pragma 命令

编译器会在警告消息、错误消息与调试信息中包含代码所在的行号与所在的源文件名,并提供给调试工具。你可以在源代码中利用 #line 命令改变编译器默认指定的文件名与行号信息。

#line line_number ["filename"]

#line 命令的下一行行号会指定为 line_number。如果该命令也包含可选字符串字面量"filename",那么该编译器会把该字符串名称作为当前源文件名。

line_number 必须是大于 0 的十进制常量。如下例所示:

#line 1200 "primary.c"

包含 #line 命令的那一行代码,也可以包含其他宏。如果包含其他宏,预处理器会先展开所有宏,然后再执行 #line 命令。但要确保在宏展开后,#line 命令是正确的。

程序可以利用标准预定义宏_LINE_ 和 _FILE_ 来访问当前的行号和文件名设置:

printf( "This message was printed by line %d in the file %s.\n",
        \__LINE\__, \__FILE\__ );

#line 命令通常用在将 C 源代码作为输出的程序上。通过将对应的输入文件行号放置在 #line 命令内,程序可以让 C 编译器的错误消息指向源文件中相应的行。

无论是否有实际错误,#error 命令都会让预处理器发出错误消息。它的语法如下:

#error [text]

如果上述命令存在可选项 text,则 text 就会被包含在预处理器的错误消息中。然后,编译器会终止处理源代码,并结束执行,仿佛遇到了严重错误。text可以是任意预处理器记号序列。如果 text 中有其他宏,它们都不会被展开。最好在这里使用字符串字面量,以避免标点符号字符(如单引号)的影响。

下面的例子测试标准宏 _STDC_ 是否已经被定义。如果没有,则生成一个错误消息:
纯文本复制

#ifndef \__STDC\__
  #error "This compiler does not conform to the ANSI C standard."
#endif

#pragma 命令是向编译器提供额外信息的标准方法,其格式如下:

#pragma [tokens]

如果 #pragma 之后的第一个标记(token)是 STDC,那么该命令就是一个标准 pragma。否则,该 #pragma 命令的作用取决于实现版本。为了保障代码的可移植性,应该尽量少使用 #pragma 命令。

如果预处理器支持所指定的标记,就会执行这些标记所代表的动作,或者把该信息传递给编译器。如果预处理器不支持所指定的标记,就忽略该 #pragma 命令。

例如,最新版本的 GNU C 编译器和微软 Visual C 编译器都支持 #pragma pack(n),它使得编译器让结构成员对齐到特定的字节边界。下面的例子使用 pack(1)指示每个结构成员必须对齐到字节边界:

#if defined( \__GNUC\__ ) || defined( _MSC_VER )
  #pragma pack(1)                             // 对齐字节,没有填充
#endif

单字节对齐方式可以确保结构成员之间不会有间隙。pack 的参数 n 通常是 2 的幂(但幂值较小)。例如,pack(2)把结构成员对齐到偶数地址,而 pack(4)把结构成员对齐到 4 为倍数的地址。pack()没有参数,它指示对齐方式设置为实现版本的默认值。

C99 新增下面三个标准的 pragma:
纯文本复制

#pragma STDC FP_CONTRACT on_off_switch
#pragma STDC FENV_ACCESS on_off_switch
#pragma STDC CX_LIMITED_RANGE on_off_switch

on_off_switch 的值必须是 ON、OFF 或 DEFAULT。

无法通过宏展开创建一个 #pragma 命令(或任何其他命令)。当碰到需要这么做的情况时,C99 新增了一个预处理运算符 _Pragma,它可以与宏配合使用。它的语法如下:

_Pragma (string_literal )

_Pragma 运算符的工作方式是这样的。首先,该 string_literal 操作数会被“解字符串化”(destringized),或被转换为预处理器记号序列,该过程如下:删除字符串前后的双引号;使用"替代 ";使用 \ 替代 \。然后,预处理器会翻译前述结果序列的记号,类似于 #pragma 命令。

下面这一行代码定义了一个名为 STR 的宏,借此可以使用 _Pragma 运算符重写 #pragma 命令:

#define STR(s)   #s             // 这个#是“字符串化”运算符

有了上述定义,下面两行代码是等同的:

#pragma tokens
_Pragma ( STR(tokens) )

下面的例子是在宏中使用 _Pragma 运算符:

#define ALIGNMENT(n) _Pragma( STR(pack(n)) )
ALIGNMENT(2)

宏替换会把调用宏 ALIGNMENT(2)的过程重写为如下形式:

_Pragma( "pack(2)" )

预处理器接着处理这行代码,实现方式与使用下面的命令一样:

#pragma pack(2)

2.编译(Compilation)
编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表 示或汇编代码。 优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境 也有很大的关系。优化一部分是对中间代码的优化,这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而 进行的。对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并 等)、复写传播,以及无用赋值的删除,等等。后一种类型的优化同机器的硬件结构密切相关,主要的是考虑是如何充分利用 机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水 线、RISC、CISC等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。

使用下面命令进行编译生成汇编文件。

  gcc -S hello.i > hello.s 

C语言代码的条件编译,动态库,静态库的制作

 arm-linux-gcc -S hello.i -o arm_hello.s

C语言代码的条件编译,动态库,静态库的制作
在这里,我们使用PC的编译器gcc就会编译生成x86的汇编,而使用ARM的编译器则生成ARM的汇编文件。同一份C代码不作 任何修改,使用不同的编译器编译就生成在不同机器上运行的程序,这就是C程序的可移植性。我们在PC上编写程序,在PC上用 ARM的交叉编译器,生成在ARM平台上的可执行程序的过程叫做交叉编译。
3. 汇编(Assembly)
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将终经过这 一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。 目标文件由段组成。通常一 个目标文件中至少有两个段:
代码段(文本段):该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写; 数据段:主要存放程序中要用到的各种常量、全局变量、静态的数据。一般数据段都是可读,可写,可执行的;

 gcc -c hello.s -o hello.o

4.链接(Linking)
汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另 一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题, 都需要经链接程序的处理方能得以解决。 链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符 号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体,也就是 可执行程序。 根据开发人员指定的库函数的链接方式的不同,链接处理可分为两种:
1.静态链接
2. 动态链接
详细介绍:https://blog.csdn.net/kang___xi/article/details/80210717
对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使终的可执行文件比较短小,并 且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一 定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

在windows和Linux下都存在着大量的库,库是什么呢?本质上来说,库时一种可执行代码的二进制形式,可以被操作系统载 入内存执行。我们通常将一些通用函数写成函数库,所以库是别人写好的,现有的,成熟的,可以复用的代码,你可以使用但要 必须得遵守许可协议。在我们现实开发过程中,不可能每一份代码都从头编写,当我们拥有库时,我们就可以直接将我们所需要 的文件链接到我们的程序中。可以为我们节省大量的时间,提高开发效率。Linux下库分为两种,静态库和动态库。这两种库相 同点是两种库都是由.o文件生成的,下边讨论一下它们的不同点:
5. 静态库
静态库文件名的命名方式是“libxxx.a”,库名前加”lib”,windows和linux下都是后缀用”.a”,“xxx”为静态库名, windows下的静态库名也叫libxxx.a; 链接时间: 静态库的代码是在编译过程中被载入程序中。 链接方式:静态库的链接是将整个函数库的所有数据都整合进了目标代码。这样做优点是在编译后的执行程序不在需要外 部的函数库支持,因为所使用的函数都已经被编进去了。缺点是,如果所使用的静态库发生更新改变,你的程序必须重新 编译。
Linux静态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.a。
第一步:将代码文件编译成目标文件。即将.c文件编译成.o文件。

gcc -c filename.c 

第二步:利用ar工具将目标文件.o打包成静态库文件.a

ar -rcs filename.a *.o

6.动态库
动态库的命名方式与静态库类似,前缀相同为“lib”,linux下后缀名为“.so(shared object)”即libxxx.so;而windows 下后缀名为“.dll(dynamic link library)”即libxxx.dll; 链接时间:动态库在编译的时候并没有被编译进目标代码,而是当你的程序执行到相关函数时才调用该函数库里的相应函 数。这样做缺点是因为函数库并没有整合进程序,所以程序的运行环境必须提供相应的库。优点是动态库的改变并不影响 你的程序,所以动态函数库升级比较方便。
它们两个还有很明显的不同点:当同一个程序分别使用静态库,动态库两种方式生成两个可执行文件时,静态链接所生成的文 件所占用的内存要远远大于动态链接所生成的文件。这是因为静态链接是在编译时将所有的函数都编译进了程序,而动态链接是 在程序运行时由操作系统帮忙把动态库调入到内存空间中使用。另外如果动态库和静态库同时存在时,链接器优先使用动态库。

gcc -shared -fPCI -o filename.so filename.c 

第一步编译器选项加-fPIC,生成目标文件。(fPIC是创建与地质无关的编译程序,是为了能够在多个应用程序间共享)
第二步链接器选项加-shared,生成动态库。(shared是指定生成动态链接库)

7.使用
gcc在编译的时候,找不到头文件。这时我们可以使用编译器的 -I(大写i)选项来指定头文件的路径。
gcc在链接的时候,找不到函数的定义。这时要告诉链接器在链接的时候要链接包含这些函数的库文件,通过-l (小写l) 选项来指 定。
链接器默认到系统动态库路径(/lib、/lib64、/usr/lib)下查找相应的库文件,如果找不到就出错。如果使用的动态库不在这些 路径下,我们就可以使用-L(大写l)选项来指定相应库的路径。如果动态库和静态库同时存在则优先使用动态库链接。而如果想使用静态库链接,一种方法是把动态库 移除,另外一种方法是在编译时加上链接选项 -static,当然这样程序中所有的库都使用静态链接库了。

静态编译的程序,因为所有代码段和数据段都被链接进可执行程序中,所以可以直接运行。
而动态编译的程序,动态库中的代码段和数据段并没有被链接进可执行程序中,只是记录了需要他们的一些信息。所以程序在运 行时需要操作系统帮忙加载这些动态库程序。这样直接执行就会出错。
Linux下在运行程序时,会默认到 /lib、/lib64、/usr/lib以及LD_LIBRARY_PATH环境变量指定的路径下查找所需的动态库下查 找所需的动态库文件,如果没有则抛错。而这时libmycrypto.so并不在系统库路径下,所以会出错。这时有两种解决方法:
(1) 将所需要的libmycrypto.so文件拷贝到/usr/lib路径下,当然这需要root权限;
(2)使用export命令在LD_LIBRARY_PATH环境变量中添加该动态库所在的路径,注意该命令只是临时生效,重启后失效。另 外指定的路径必须是绝对路径;
C语言代码的条件编译,动态库,静态库的制作

上一篇:CSC72002 Object Oriented Programming


下一篇:20145120 《Java程序设计》第6周学习总结