关于extern "C"(详细剖析)

【目录】html

引言ios

extern “C”的前世此生c++

当心门后的未知世界安全

Q&A并发

c++调用c的方法函数

c调用c++的方法工具

 


 

在你工做过的系统里,不知可否看到相似下面的代码。测试

这好像没有什么问题,你应该还会想:“嗯⋯是啊,咱们的代码都是这样写的,历来没有所以碰到过什么麻烦啊~”。this

你说的没错,若是你的头文件历来没有被任何C++程序引用过的话。google

这与C++有什么关系呢? 看看__cplusplus(注意前面是两个下划线) 的名字你就应该知道它与C++有很大关系。__cplusplus是一个C++规范规定的预约义宏。你能够信任的是:全部的现代C++编译器都预先定义了它;而全部C语言编译器则不会。另外,按照规范__cplusplus的值应该等于1 9 9 7 1 1 L ,然而不是全部的编译器都照此实现,好比g++编译器就将它的值定义为1。

因此,若是上述代码被C语言程序引用的话,它的内容就等价于下列代码。

在这种状况下,既然extern "C" { }通过预处理以后根本就不存在,那么它和#include指令之间的关系问题天然也就是无中生有。

extern "C"的前世此生

在C++编译器里,有一位暗黑破坏神,专门从事一份称做“名字粉碎”(name mangling)的工做。当把一个C++的源文件投入编译的时候,它就开始工做,把每个它在源文件里看到的外部可见的名字粉碎的面目全非,而后存储到二进制目标文件的符号表里。

之因此在C++的世界里存在这样一个怪物,是由于C++容许对一个名字给予不一样的定义,只要在语义上没有二义性就好。好比,你可让两个函数是同名的,只要它们的参数列表不一样便可,这就是函数重载(function overloading);甚至,你可让两个函数的原型声明是彻底相同的,只要它们所处的名字空间(namespace)不同便可。事实上,当处于不一样的名字空间时,全部的名字都是能够重复的,不管是函数名,变量名,仍是类型名。

另外,C++程序的构造方式仍然继承了C语言的传统:编译器把每个经过命令行指定的源代码文件看作一个独立的编译单元,生成目标文件;而后,连接器经过查找这些目标文件的符号表将它们连接在一块儿生成可执行程序。

编译和连接是两个阶段的事情;事实上,编译器和连接器是两个彻底独立的工具。编译器能够经过语义分析知道那些同名的符号之间的差异;而连接器却只能经过目标文件符号表中保存的名字来识别对象。

因此,编译器进行名字粉碎的目的是为了让连接器在工做的时候不陷入困惑,将全部名字从新编码,生成全局惟一,不重复的新名字,让连接器可以准确识别每一个名字所对应的对象。

但 C语言倒是一门单一名字空间的语言,也不容许函数重载,也就是说,在一个编译和连接的范围以内,C语言不容许存在同名对象。好比,在一个编译单元内部,不容许存在同名的函数,不管这个函数是否用static修饰;在一个可执行程序对应的全部目标文件里,不容许存在同名对象,不管它表明一个全局变量,仍是一个函数。因此,C语言编译器不须要对任何名字进行复杂的处理(或者仅仅对名字进行简单一致的修饰(decoration),好比在名字前面统一的加上单下划线_)。

C++的缔造者Bjarne Stroustrup在最初就把——可以兼容C,可以复用大量已经存在的C库——列为C++语言的重要目标。但两种语言的编译器对待名字的处理方式是不一致的,这就给连接过程带来了麻烦。

例如,现有一个名为my_handle.h的头文件,内容以下:

而后使用C语言编译器编译my_handle.c,生成目标文件my_handle.o。因为C语言编译器不对名字进行粉碎,因此在my_handle.o的符号表里,这三个函数的名字和源代码文件中的声明是一致的。

随后,咱们想让一个C++程序调用这些函数,因此,它也包含了头文件my_handle.h。假设这个C++源代码文件的名字叫my_handle_client.cpp,其内容以下:

其中,粗体的部分就是那三个函数的名字被粉碎后的样子。

而后,为了让程序能够工做,你必须将my_handle.o和my_handle_client.o放在一块儿连接。因为在两个目标文件对于同一对象的命名不同,连接器将报告相关的“符号未定义”错误。

为了解决这一问题,C++引入了连接规范(linkage specification)的概念,表示法为extern"language string",C++编译器广泛支持的"language string"有"C"和"C++",分别对应C语言和C++语言。

连接规范的做用是告诉C++编译:对于全部使用了连接规范进行修饰的声明或定义,应该按照指定语言的方式来处理,好比名字,调用习惯(calling convention)等等。

连接规范的用法有两种:

1.单个声明的连接规范,好比:extern "C" void foo();

2. 一组声明的连接规范,好比:

 

extern "C"
{
  void foo();
  int bar();
}

对咱们以前的例子而言,若是咱们把头文件my_handle.h的内容改为:

而后使用C++编译器从新编译my_handle_client.cpp,所生成目标文件my_handle_client.o中的符号表就变为:

从中咱们能够看出,此时,用extern "C" 修饰了的声明,其生成的符号和C语言编译器生成的符号保持了一致。这样,当你再次把my_handle.o和my_handle_client.o放在一块儿连接的时候,就不会再有以前的“符号未定义”错误了。

但此时,若是你从新编译my_handle.c,C语言编译器将会报告“语法错误”,由于extern"C"是C++的语法,C语言编译器不认识它。此时,能够按照咱们以前已经讨论的,使用宏__cplusplus来识别C和C++编译器。修改后的my_handle.h的代码以下:

当心门后的未知世界

在咱们清楚了 extern "C" 的来历和用途以后,回到咱们原本的话题上,为何不能把#include 指令放置在 extern "C" { ... } 里面?

咱们先来看一个例子,现有a.h,b.h,c.h以及foo.cpp,其中foo.cpp包含c.h,c.h包含b.h,b.h包含a.h,以下:

现使用C++编译器的预处理选项来编译foo.cpp,获得下面的结果:

正如你看到的,当你把#include指令放置在extern "C" { }里的时候,则会形成extern "C" { } 的嵌套。这种嵌套是被C++规范容许的。当嵌套发生时,以最内层的嵌套为准。好比在下面代码中,函数foo会使用C++的连接规范,而函数bar则会使用C的连接规范。

若是可以保证一个C语言头文件直接或间接依赖的全部头文件也都是C语言的,那么按照C++语言规范,这种嵌套应该不会有什么问题。但具体到某些编译器的实现,好比MSVC2005,却可能因为 extern "C" { } 的嵌套过深而报告错误。不要所以而责备微软,由于就这个问题而言,这种嵌套是毫无心义的。你彻底能够经过把#include指令放置在extern "C" { }的外面来避免嵌套。拿以前的例子来讲,若是咱们把各个头文件的 #include 指令都移到extern "C" { } 以外,而后使用C++编译器的预处理选项来编译foo.cpp,就会获得下面的结果:

这样的结果确定不会引发编译问题的结果——即使是使用MSVC。

把 #include 指令放置在extern "C" { }里面的另一个重大风险是,你可能会无心中改变一个函数声明的连接规范。好比:有两个头文件a.h,b.h,其中b.h包含a.h,以下:

按照a.h做者的本意,函数foo是一个C++自由函数,其连接规范为"C++"。但在b.h中,因为#include "a.h"被放到了extern "C" { }的内部,函数foo的连接规范被不正确地更改了。

因为每一条 #include 指令后面都隐藏这一个未知的世界,除非你刻意去探索,不然你永远都不知道,当你把一条条#include指令放置于extern "C" { }里面的时候,到底会产生怎样的结果,会带来何种的风险。或许你会说,“我能够去查看这些被包含的头文件,我能够保证它们不会带来麻烦”。但,何须呢?毕竟,咱们彻底能够没必要为没必要要的事情买单,不是吗?

Q&A

Q: 难道任何# i n c l u d e指令都不能放在e x t e r n "C"里面吗?

A: 正像这个世界的大多数规则同样,总会存在特殊状况。

有时候,你可能利用头文件机制“巧妙”的解决一些问题。好比,#pragma pack的问题。这些头文件和常规的头文件做用是不同的,它们里面不会放置C的函数声明或者变量定义,连接规范不会对它们的内容产生影响。这种状况下,你能够没必要遵照这些规则。

更加通常的原则是,在你明白了这全部的原理以后,只要你明白本身在干什么,那就去作吧。


Q: 你只说了不该该放入e x t e r n "C"的,但什么能够放入呢?

A: 连接规范仅仅用于修饰函数和变量,以及函数类型。因此,严格的讲,你只应该把这三种对象放置于extern "C"的内部。

但,你把C语言的其它元素,好比非函数类型定义(结构体,枚举等)放入extern "C"内部,也不会带来任何影响。更不用说宏定义预处理指令了。

因此,若是你更加看重良好组织和管理的习惯,你应该只在必须使用extern "C"声明的地方使用它。即便你比较懒惰,绝大多数状况下,把一个头件自身的全部定义和声明都放置在extern"C"里面也不会有太大的问题。

Q: 若是一个带有函数/变量声明的C头文件里没有e x t e r n "C"声明怎么办?

A: 若是你能够判断,这个头文件永远不可能让C++代码来使用,那么就不要管它。

但现实是,大多数状况下,你没法准确的推测将来。你在如今就加上这个extern "C",这花不了你多少成本,但若是你如今没有加,等到未来这个头文件无心中被别人的C++程序包含的时候,别人极可能须要更高的成原本定位错误和修复问题。

Q: 若是个人C+ +程序想包含一个C头文件a . h,它的内容包含了C的函数/变量声明,但它们却没有使用e x t e r n "C"连接规范,该怎么办?

A: 在a.h里面加上它。

某些人可能会建议你,若是a.h没有extern "C",而b.cpp包含了a.h,能够在b.cpp里加上 :

extern "C"
{
  #include "a.h"
}

这是一个邪恶的方案,缘由在以前咱们已经阐述。但值得探讨的是,这种方案这背后却可能隐含着一个假设,即咱们不能修改a.h。不能修改的缘由可能来自两个方面:

1. 头文件代码属于其它团队或者第三方公司,你没有修改代码的权限;
2. 虽然你拥有修改代码的权限,但因为这个头文件属于遗留系统,冒然修改可能会带来不可预知的问题。

对 于第一种状况,不要试图本身进行workaround,由于这会给你带来没必要要的麻烦。正确的解决方案是,把它看成一个bug,发送缺陷报告给相应的团队 或第三方公司。若是是本身公司的团队或你已经付费的第三方公司,他们有义务为你进行这样的修改。若是他们不明白这件事情的重要性,告诉他们。若是这些头文 件属于一个免费开源软件,本身进行正确的修改,并发布patch给其开发团队。

在 第二种状况下,你须要抛弃掉这种没必要要的安全意识。由于,首先,对于大多数头文件而言,这种修改都不是一种复杂的,高风险的修改,一切都在可控的范围之 内;其次,若是某个头文件混乱而复杂,虽然对于遗留系统的哲学应该是:“在它尚未带来麻烦以前不要动它”,但如今麻烦已经来了,逃避不如正视,因此上策 是,将其视做一个能够整理到干净合理状态的良好机会。

Q: 咱们代码中关于e x t e r n "C"的写法以下,这正确吗?

 

A: 不肯定。

按照C++的规范定义,__cplusplus 的值应该被定义为199711L,这是一个非零的值;尽管某些编译器并无按照规范来实现,但仍然可以保证__cplusplus的值为非零——至少我到目前为止尚未看到哪款编译器将其实现为0。这种状况下,#if __cplusplus ... #endif彻底是冗余的。

但,C++编译器的厂商是如此之多,没有人能够保证某款编译器,或某款编译器的早期版本没有将__cplusplus的值定义为0。但即使如此,只要可以保证宏__cplusplus只在C++编译器中被预先定义 ,那么,仅仅使用#ifdef __cplusplus ⋯ #endif就足以确保意图的正确性;额外的使用#if __cplusplus ... #endif反而是错误的。

只有在这种状况下:即某个厂商的C语言和C++语言编译器都预先定义了__cplusplus ,但经过其值为0和非零来进行区分,使用#if __cplusplus ... #endif才是正确且必要的。

既然现实世界是如此复杂,你就须要明确本身的目标,而后根据目标定义相应的策略。好比:若是你的目标是让你的代码可以使用几款主流的、正确遵照了规范的编译器进行编译,那么你只须要简单的使用#ifdef __cplusplus ... #endif就足够了。

但若是你的产品是一个雄心勃勃的,试图兼容各类编译器的(包括未知的)跨平台产品, 咱们可能不得不使用下述方法来应对各类状况 ,其中__ALIEN_C_LINKAGE__是为了标识那些在C和C++编译中都定义了__cplusplus宏的编译器

这应该能够工做,但在每一个头文件中都写这么一大串,不只有碍观瞻,还会形成一旦策略进行修改,就会处处修改的情况。违反了DRY(Don't Repeat Yourself)原则,你总要为之付出额外的代价。 解决它的一个简单方案是,定义一个特定的头文件——好比clinkage.h,在其中增长这样的定义:

 【说明】以上内容转载自 http://code.google.com/p/effective-c/downloads/list 中的的effective C 文档


如下举例中c的函数声明和定义分别在cfun.h 和 cfun.c 中,函数打印字符串 “this is c fun call”,c++函数声明和定义分别在cppfun.h 和 cppfun.cpp中,函数打印字符串 "this is cpp fun call", 编译环境vc2010

c++ 调用 c 的方法(关键是要让c的函数按照c的方式编译,而不是c++的方式)

(1) cfun.h以下:

 

#ifndef _C_FUN_H_
#define _C_FUN_H_

    void cfun();

#endif

 

   cppfun.cpp 以下:

//#include "cfun.h"  不须要包含cfun.h
#include "cppfun.h"
#include <iostream>
using namespace std;
extern "C"     void cfun(); //声明为 extern void cfun(); 错误

void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
    return 0;
}

(2)cfun.h同上

       cppfun.cpp 以下:

extern "C"
{
    #include "cfun.h"//注意include语句必定要单独占一行;
}
#include "cppfun.h"
#include <iostream>
using namespace std;

void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
    return 0;
}

(3)cfun.h以下:

#ifndef _C_FUN_H_
#define _C_FUN_H_

#ifdef __cplusplus
extern "C"
{
#endif

    void cfun();

#ifdef __cplusplus
}
#endif

#endif

cppfun.cpp以下:

#include "cfun.h"
#include "cppfun.h"
#include <iostream>
using namespace std;

void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
    return 0;
}

 c调用c++(关键是C++ 提供一个符合 C 调用惯例的函数)

在vs2010上测试时,没有声明什么extern等,只在在cfun.c中包含cppfun.h,而后调用cppfun()也能够编译运行,在gcc下就编译出错,按照c++/c的标准这种作法应该是错误的。如下方法两种编译器均可以运行

cppfun.h以下:

#ifndef _CPP_FUN_H_
#define _CPP_FUN_H_

extern "C" void cppfun();


#endif

cfun.c以下:

//#include "cppfun.h" //不要包含头文件,不然编译出错
#include "cfun.h"
#include <stdio.h>

void cfun()
{
    printf("this is c fun call\n");
}

extern void cppfun();

int main()
{
#ifdef __cplusplus
    cfun();
#endif
    cppfun();
    return 0;
}

 【版权声明】转载请注明出处 http://www.cnblogs.com/TenosDoIt/p/3163621.html

相关文章
相关标签/搜索