目录

  • 引言
  • extern “C”的前世今生
  • 小心门后的未知世界
  • c++调用c的方法
  • c调用c++的方法

引言

在你工作过的系统里,不知能否看到类似下面的代码。

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

你说的没错,如果你的头文件从来没有被任何C++程序引用过的话。

这与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” { }里面的时候,到底会产生怎样的结果,会带来何种的风险。或许你会说,“我可以去查看这些被包含的头文件,我可以保证它们不会带来麻烦”。但,何必呢?毕竟,我们完全可以不必为不必要的事情买单,不是吗?

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"
{#endifvoid 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++(关键是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 __cpluspluscfun();
#endifcppfun();return 0;
}

关于extern “C“(详细剖析)相关推荐

  1. 从特征分解到协方差矩阵:详细剖析和实现PCA算法

    从特征分解到协方差矩阵:详细剖析和实现PCA算法 本文先简要明了地介绍了特征向量和其与矩阵的关系,然后再以其为基础解释协方差矩阵和主成分分析法的基本概念,最后我们结合协方差矩阵和主成分分析法实现数据降 ...

  2. sql replace替换多个字符_牛客网数据库SQL实战详细剖析(4150)

    文章来源:大数据肌肉猿 作者:无精疯 这是一个系列文章,总共61题,分6期,有答案以及解题思路,并附上解题的一个思考过程.具体题目可参考牛客网的SQL实战模块:https://www.nowcoder ...

  3. sql not exists用法_牛客网数据库SQL实战详细剖析(5160)(更新完结)

    文章来源:大数据肌肉猿 作者:无精疯 这是一个系列文章,总共61题,分6期,有答案以及解题思路,并附上解题的一个思考过程. 具体题目可参考牛客网的SQL实战模块: https://www.nowcod ...

  4. 手把手教你读懂源码,View事件的注册和接收详细剖析

    关于Android的Touch事件传递机制,只是知道事件传入Activity后的流程,但是这些事件是如何传递给Activity的一直模糊不清.现在再来好好回顾一遍,顺道整理一点儿东西出来,同时分享给大 ...

  5. 【Qt炫酷动画】1.easing官方demo详细剖析

    文章目录 1.demo效果 2.demo项目构建 3.代码详细剖析 代码目录结构 代码实现 4.实现过程分析 1.demo效果 2.demo项目构建 打开Qt Creator,按照图上操作 使用Min ...

  6. 二进制文件的读写详细剖析

    一).一般问题 二进制文件与我们通常使用的文本文件储存方式有根本的不同.这样的不同很难用言语表达,自己亲自看一看,理解起来会容易得多.因此,我推荐学习二进制文件读写的朋友安装一款十六进制编辑器.这样的 ...

  7. 深度阅读:详细剖析 extern “C“

    [导读]:本文详细解析extern "C"的底层原理与实际应用. 以下是正文 在你工作过的系统里,不知能否看到类似下面的代码. 这好像没有什么问题,你应该还会想:"嗯⋯是 ...

  8. 详细剖析 extern C

    [导读]:本文详细解析extern "C"的底层原理与实际应用. 以下是正文 在你工作过的系统里,不知能否看到类似下面的代码. 这好像没有什么问题,你应该还会想:"嗯⋯是 ...

  9. c语言赋值x为字母,C语言算术、赋值、关系、逻辑运算详细剖析---

    标识符和关键字 ¨标识符:用来标识程序中的变量.符号常量.函数.数组.类型.文件等对象的名字.标识符只能由字母.数字和下划线组成,且第一个字符必需为字母或下划线.C语言中大小写字母是两个不同的字符. ...

最新文章

  1. 集成学习Bagging和Boosting算法总结
  2. React服务端渲染实现(基于Dva)
  3. Python中的format()函数
  4. python处理rgb_如何在Python中读取给定像素的RGB值?
  5. 19、mysql中定时器的创建和使用
  6. hessian学习笔记
  7. python向CSV文件写内容
  8. SpringCloud工作笔记036---oauth2微服务Establishing SSL connection without server's identity verification
  9. C# async await
  10. 2021牛客暑期多校训练营7,签到题FHI
  11. 在一个请求分页系统中,分别采用 FIFO、LRU和 OPT页面置换算法时,假如一个作业的页面走向为 4、3、2、1、4、3、5、4、3、2、1、5,当分配给该作业的物理块数M分别为 3、4时,
  12. Linux-apache的编译安装1
  13. Atitti 数据库事务处理 attilax总结
  14. java socket是什么_Java网络编程-JavaSocket编程是什么呢?
  15. cesium——鼠标拾取坐标并转换为经纬高
  16. java jsp ssm210KTV点歌系统毕设成品源码项目介绍
  17. DirectX11参考资料之美
  18. React官方状态管理库—— Recoil
  19. Kubernetes集群功能演示:deployment的管理和kubectl的使用
  20. 安卓无线蓝牙耳机哪款好?实惠好用的蓝牙耳机品牌

热门文章

  1. 一个 DAG 工作流引擎的设计实现源代码实例
  2. IDEA 一次性解决运行报错Error:java: 无效的源发行版: 11问题
  3. 思修复习知识点背诵-《思想道德基础与法律修养》
  4. 怎样写出杀手级的工程师简历
  5. 福迪宝微波水饺捐赠爱心物资,助力广州疫情防控
  6. 第三方支付(二):揭秘第三方支付包含哪些业务 | 牌照角色篇
  7. 【译】JavaScript 开发者年度调查报告
  8. 【社媒营销】Facebook速推帖子如何运作?值得吗?
  9. 国外问卷调查平台 新手入门要怎么做?
  10. (48.1)【WAF绕过-权限控制】webshell、小马、权限脚本、权限工具