C++ 的 C 方式编译和 C 链接约定

晨曦之光 发布于 2012/05/23 11:02
阅读 981
收藏 0

原文: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:

编译示例说明

工程 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:

  • 链接约定 (Linkage):链接约定是调用约定与修饰名在链接时的具体表现,如 C linkage 和 C++ linkage

    参考 MSDN:

操作修饰名的一个主要时机是链接阶段,链接器在各二进制模块间查找引用(被调用)的函数修饰名,组装起它们间的函数调用,无论是 (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 条件编译


原文链接:http://blog.csdn.net/breakerzy/article/details/6871416
加载中
返回顶部
顶部