编译器编译过程
编译器编译的过程一般分为下面几步:
- 预处理
- 编译
- 汇编
- 链接
预处理
预处理过程主要处理代码中以 # 开头的预编译指令 比如 #include #if,主要处理规则如下:
- 将所有的 #define 删除,并且展开所有的宏定义
- 处理所有条件预编译指令,比如 #if、#ifdef、#elif、#else、#endif 等
- 处理 #include 预编译指令,将被包含的文件插入到该预编译指令的位置;注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件,所以头文件包含不能滥用,特别是交叉包含
- 删除所有的注释 "//" 和 "/**/"
- 添加行号和文件名标识,比如#2 "hello.c" 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能显示行号,LINE 这个宏 ,在这个阶段就已经生效
- 保留所有的 #pragma 编译器指令,因为编译器要使用它们,pragma 这个宏可以在编译阶段调试代码,确定程序分支
经过预编译后,会得到一个 .i 文件,这个文件不包含任何宏定义,所有的宏都已经被展开,并且包含的文件也被插入到 .i 文件中
gcc 中使用 -E 选项来只执行预编译操作
编译
编译过程就是把预处理完的文件进行一些列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件,这个过程往往是整个程序构建的核心部分
现代版的 gcc 把预编译和编译两个步骤合并成一个步骤,使用一个叫做cc1的程序来完成这两个步骤。对于C++来说是 cc1plus, Objective-C 是cc1obj,Java 是jc1,所以实际上 gcc 这个命令只是这些后台程序的包装器,它会根据不同的参数要求去调用预编译程序cc1、汇编器as、链接器ld
每个c 文件经过编译都会生成 .o文件
gcc 中使用 -S 选项将经过预处理的文件编译为汇编代码
汇编(汇编代码转换为机器码)
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器指令;汇编过程我们可以调用汇编器as 来完成
链接
链接是一个比较复杂的过程,包含下面几个步骤:
- 地址空间分配
- 符合决议
- 重定位
本质上是通过ld 命令将不同的.o 组合成为一个 .out 文件
整个链接的过程比较复杂,可以通过 gcc -v 获取链接的过程,从而分析
ld -static crt1.o crti.o crtbegin.o hello.o
--start-group -lgcc -lgcc_eh -lc --end-groud
crtend.o crtn.o
crt1.0 crti.o crtbegin.o cretend.o crtn.o 这几个库表示 glibc 的辅助运行库,这五个文件的作用是启动、初始化、构造、析构和结束,它们会自动链接到应用程序里
extern "C" 的作用
#ifndef _ALLOPERATORDEMO_
#define _ALLOPERATORDEMO_
#include <string>
#include <stdint.h>
#include <iostream>
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif
__cplusplus 是 C++ 编译环境都会定义的一个宏,所以 如果此头文件被C 编译环境编译,那么 extern "C" 不起作用,只有在 C++ 编译环境中使用才会起作用
extern "C" 这个关键字声明目的,就是实现 C++ 与C的混合编程,一旦被 extern "C" 修饰之后,它便以 C 的方式工作;可以实现在 C 中引用 C++ 库的函数,也可以 C++ 中引用 C 库的函数
函数重载导致函数符号差异
C++ 支持函数重载,而面向过程的语言 C 则不支持,所以函数被 C++ 编译后在符号库中的名字与 C 语言的有所不同
例如:假设某个函数的原型为:
void testdemofunction( int x, int y )
该函数被 C 编译器编译后在符号库中的名字为 _testdemofunction ,而 C++ 编译器则会产生像 _testdemofunction_int_int 之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为 mangled name)
_testdemofunction_int_int 这样的名字包含了函数名、函数参数数量及类型信息, C++ 就是靠这种机制来实现函数重载的;例如,在 C++ 中,函数 void testdemofunction( int x, int y ) 与 void testdemofunction( int x, float y ) 编译生成的符号是不相同的,后者为 _testdemofunction_int_float
-
C++ 头文件被C程序包含
如果不加上 extern "C" 声明,假设上面的头文件是 C++ 库的头文件,然后在B模块中被包含,如果B模块是C程序,因为函数名称符号不一致,导致链接错误
注意如果是 C++ 的头文件,可能声明了C++ 中使用到的 class 和其他关键字,所以一般不会这样做
同时由于是C的源文件,extern "C"的声明不支持的,所以加上了 #ifdef __cplusplus 的宏 -
C头文件被C++ 程序包含
如果不加上 extern "C" 声明,加上面的头文件是 C 库的头文件,然后在C++ 程序的B模块中被包含 也会因为函数名称符号不一致,导致链接错误
加上 extern "C" 的声明后,头文件如果是 C++ 库头文件,形式如下:
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int testdemofunction( int x, int y );
#endif
模块 A 编译生成 testdemofunction的目标代码时,没有对其名字进行特殊处理,采用了 C 语言的方式;连接器在为模块B的目标代码寻找 testdemofunction(2,3) 调用时,寻找的是未经修改的符号名 _testdemofunction,这样就解决了C 函数调用 C++ 库的头文件问题
C++ 包含 C头文件的处理
C++ 引用 C 函数的具体例子 在 C++ 中引用 C 语言中的函数和变量,在包含 C 语言头文件(假设为 cExample.h )时,需进行下列处理:
extern "C"
{
#include "cExample.h" //改变
}
C 库的编译是用 C 的方式生成的,其库中的函数标号一般也是类似前面所说的 _testdemofunction 的形式,没有任何参数信息
其实 extern "C" 是在告诉 C++ ,链接 C 库的时候,采用 C 的方式进行链接(即寻找类似 _testdemofunction 的没有参数信息的标号,而不是默认的 _testdemofunction_int_int 这样包含了参数信息的 C++ 标号了)
通过包含头文件前对函数进行声明,就能正确引用到头文件中声明的函数
注意:
C 语言中不支持 extern "C" 声明,不支持 extern "C" 关键字
C语言库的头文件中,进程包含下面的代码片段:
__BEGIN_DECLS
.....
.....
__END_DECLS
sys/cdefs.h 中大致定义如下:
#if defined(__cplusplus)
#define __BEGIN_DECLS extern "C" {
#define __END_DECLS }
#else
#define __BEGIN_DECLS
#define __END_DECLS
#endif
网友评论