原文:C++ 的 C 编译和链接方式 (VC)
作者:Breaker <breaker.zy_AT_gmail>
C++ 与 C 的编译方式
所有的 C 程序都是 C++ 程序,而所有的 C++ 编译器都是 C 编译器(几乎所有),兼容 C99 标准 wiki: C99。一些编译器以 C/C++ Compiler 命名,如 VC: cl (C/C++ Optimizing Compiler)、ICC (Intel C/C++ Compilers)
开发环境:VC 2005
以 VC 为例,它根据源文件的扩展名来选择 C 或 C++ 的编译方式,并且有一组编译选项显式指定以 C 还是 C++ 的方式编译,规则如下:
1. .c 源文件默认以 C 方式编译,这时拒绝 C++ 的语法,如 class
2. .cpp/.cxx 源文件默认以 C++ 方式编译
3. 使用 /Tc, /Tp, /TC, /TP 编译选项显式指定源文件的 C 或 C++ 编译方式:
- /Tc file.ext: 指定 file.ext 以 C 方式编译
- /Tp file.ext: 指定 file.ext 以 C++ 方式编译
- /TC: 指定传给 cl 的所有源文件都以 C 方式编译,是全局选项
- /TP: 指定传给 cl 的所有源文件都以 C++ 方式编译
规则的覆盖顺序为:Tc/Tp => TC/TP => 根据扩展名 .c/cpp/.cxx 默认判断
参考 MSDN:
- How Do I Compile and Link C Code, Not C++
- VC Compiler Options: /Tc, /Tp, /TC, /TP (Specify Source File Type)
编译示例说明
工程 TestProj 含以下源文件:
testproj.c: 程序入口和主要逻辑
tool.h: 工具例程 void tool() 的声明
tool.c: 工具例程 void tool() 的实现
ctool.h: 工具例程 void ctool() 的声明
ctool.c: 工具例程 void ctool() 的实现
include 预处理、编译单元和对象文件对应如下:
[testproj.c include=> tool.h, ctool.h] => testproj.obj [tool.c include=> tool.h] => tool.obj [ctool.c include=> ctool.h] => ctool.obj 编译单元:translation unit,上面用 [] 表示,一个 .c/.cpp 源文件预处理之后的结果。编译器针对编译单元进行编译,一个编译单元生成一个 obj 对象文件分别编译:separate compilation,类似 TestProj 的这种源文件组织方式,是一种实际中常用的减小编译单元依赖、加快重编译过程的源文件组织方式
编译示例 1
编译命令如下:
cl /TP testproj.c tool.c /Tc ctool.c解释:- /TP: 覆盖默认的扩展名自动判断方式,将 testproj.c、tool.sub 作为 C++ 源文件编译
- /Tc ctool.c: 覆盖 /TP 的作用,将 ctool.c 作为 C 源文件编译
意义:
Q1: 明显是 C 语言程序 testproj.c,为何要以 C++ 方式编译?
A1: 利用 C++ 的严格类型规则检查。C++ 比 C 拥有更严格的类型规则,以 C++ 的方式编译,如果程序中有类型安全 (type safety) 的 BUG,编译会报错或告警。案例:用 DDK 开发 Windows 驱动时,均是 C 程序,但通常以 .cpp 命名源文件,或者使用 /TP 编译选项
Q2: 在全部用 C++ 方式编译的情况下,为何 ctool.c 又用 /Tc 指明以 C 方式编译?
A2: 这种情况应用在:需要用 C 的约定进行链接 (C Linkage),例如 TestProj 生成的可执行文件是需要导出 C 链接约定的 ctool() 函数的动态链接库 (DLL)
Q3: 为何一定非要以 C 链接约定,而不以 C++ 链接约定导出函数呢?
A3: 这很大程度上属于设计、人为规定和版本兼容问题,两种情况:(1). 以 C 链接约定导出函数是在很早的时候由 DLL 用户和 DLL 提供者共同商榷而定的(或是 DLL 提供者单方面规定的),由于长期的使用和影响,二进制接口不能随便更改 (2). DLL 用户程序用 C 语言开发,如果 DLL 导出 C++ 链接约定的函数,不便使用
由于 Q2、Q3 原因,假定 TestProj 是 DLL 工程,编译命令修改如下:
cl /LD /D_EXPORT_CTOOL /TP testproj.c tool.c /Tc ctool.c/LD: 传递给链接器 /DLL 链接选项,以生成 DLL,并以 .dll 为扩展名,即具有 /link /DLL /OUT:testproj.dll 的作用而 ctool.h 大致如下:
#ifdef _EXPORT_CTOOL #define CTOOL_DECL __declspec(dllexport) #else #define CTOOL_DECL __declspec(dllimport) #endif CTOOL_DECL void ctool(); 问题:如果 testproj.c 中调用了 ctool(),上面的编译命令无法成功,在链接阶段报错原因:Q2 中说明在动态链接中需要保持一致的链接约定,调用者和被调者(实现方)需要用一样的 C Linkage 或 C++ Linkage 才行。其实静态链接也同样需要遵守这个规则,main.obj 可以调用 tool.obj 中的 tool() 函数,是因为它们都使用 C++ Linkage,链接时 main.obj 和 tool.obj 对 tool() 的修饰名都是 ?tool@@YAXXZ。而 ctool.obj 是以 C 方式编译的,对于 ctool() 函数,main.obj 寻找的是 C++ 修饰名的 ?ctool@@YAXXZ,但 ctool.obj 中只有 C 修饰名的 _tool,两者自然无法链接
调用和链接约定
三个概念:
-
调用约定 (Calling Convention):用来规定函数调用时,参数入栈顺序、谁负责退栈、修饰名等规则的约定,如 __cdecl、__stdcall、__thiscall,详细说明见 MSDN:Calling Conventions
-
修饰名 (Decorated Name):源程序中函数的原始名经过编译后,在二进制模块内部转换为另一个名字,称为修饰名,二进制模块间的此函数的引用、查找等,都以修饰名进行
为什么二进制模块内部需要修饰名?(修饰名的作用)
1. 不同调用约定的函数的修饰名是不一样的,修饰名的作用之一就是标识不同的调用约定
2. 同一调用约定情况下,C 与 C++ 方式编译所得修饰名不一样。C++ 为了支持函数名重载,将函数的参数类型、返回类型等信息编码成字符,和原函数名拼成修饰名。修饰名的另一作用是支持函数重载参考 MSDN:
- Name Decoration: 修饰名变换规则
- Decorated Names: 查看和解析修饰名的方法,如使用 undname、dumpbin 工具,/FA 编译选项
- UnDecorateSymbolName(): DbgHelp.dll 中的函数,解析 C++ 名字符号的修饰名
- Code Project: A tool to view a LIB: 查看和解析 .lib 中导出符号的修饰名,生成导出符号对应的 .h 头文件
-
链接约定 (Linkage):链接约定是调用约定与修饰名在链接时的具体表现,如 C linkage 和 C++ linkage
参考 MSDN:
- Types of Linkage
- Using extern to Specify Linkage: 用 extern 指定链接约定,VC 支持 extern "C" 和 extern "C++"
操作修饰名的一个主要时机是链接阶段,链接器在各二进制模块间查找引用(被调用)的函数修饰名,组装起它们间的函数调用,无论是 (1). 静态链接,二进制模块 obj、lib 等,还是 (2). 动态链接,二进制模块 exe、dll 等
多数情况下程序员不需要直接操作修饰名,只要注意:修饰名和链接约定的不一致导致链接失败,务必保持一致就行了,具体的修饰名转换、查找操作交给链接器,不用操心
但如果在汇编源程序中引用 C/C++ 源程序中的函数,就需要显式指定修饰名,而非原始名,因为修饰名才是存在于二进制模块中的“函数真名”,而汇编不是编译,不吃调用约定那套东西(换句话说,你得手工实现调用约定)
编译示例 2
在示例 1 的基础上修改 ctool.h:
#ifdef __cplusplus #define EXTERN_C extern "C" #define EXTERN_C_BEGIN extern "C" { #define EXTERN_C_END } #else // __cplusplus defined #define EXTERN_C extern #define EXTERN_C_BEGIN #define EXTERN_C_END #endif // __cplusplus NOT defined EXTERN_C_BEGIN CTOOL_DECL void ctool(); EXTERN_C_END仍采用示例 1 的编译命令:
cl /LD /D_EXPORT_CTOOL /TP testproj.c tool.c /Tc ctool.c解释:- extern "C": 虽然以 C++ 方式编译 [testproj.c include=> ctool.h],但对 ctool() 函数使用 C 链接约定,链接时 main.obj 和 ctool.obj 就会有相同的 C 修饰名 _ctool
- __cplusplus 的条件编译:因为以 C 方式编译 [ctool.c include=> ctool.h],而 C 语法中没有 extern "C",只有 extern,所以用 C++ 语言标准预定义宏 __cplusplus 和条件编译跳过 extern "C"
编译示例 3
在示例 2 编译命令的基础上去掉 /Tc ctool.c:
cl /LD /D_EXPORT_CTOOL /TP testproj.c tool.c ctool.c等价于:
cl /LD /D_EXPORT_CTOOL testproj.cpp tool.cpp ctool.cpp虽然成功,但这与示例 2 是有区别的:(1). 示例 2 中以 C 方式编译 [ctool.c include=> ctool.h],而这里以 C++ 方式编译它,所以执行了 C++ 的严格类型检查,只是将 ctool() 变为 C 的链接约定
(2). 如果 ctool.c 中有其它函数,又没用 extern "C",则它们会用 C++ 的修饰名
这种情况下,ctool.h 中的 __cplusplus 条件编译可以去掉,但为了 (ctool.h, testproj.dll) 发布后,也能被 C 程序使用,通常保留 __cplusplus 条件编译