如何建设一个自己 的网站首页,网站域名后缀,设计网站公司收费,广告设计与制作专业就业方向前言
在你工作过的系统里#xff0c;不知能否看到类似下面的代码。 这好像没有什么问题#xff0c;你应该还会想#xff1a;“嗯⋯是啊#xff0c;我们的代码都是这样写的#xff0c;从来没有因此碰到过什么麻烦啊#xff5e;”。
你说的没错#xff0c;如果你的头文件…前言
在你工作过的系统里不知能否看到类似下面的代码。 这好像没有什么问题你应该还会想“嗯⋯是啊我们的代码都是这样写的从来没有因此碰到过什么麻烦啊”。
你说的没错如果你的头文件从来没有被任何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)的概念表示法为externlanguage stringC编译器普遍支持的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.cC语言编译器将会报告“语法错误”因为externC是C的语法C语言编译器不认识它。此时可以按照我们之前已经讨论的使用宏__cplusplus来识别C和C编译器。修改后的my_handle.h的代码如下 小心门后的未知世界
在我们清楚了 extern C 的来历和用途之后回到我们本来的话题上为什么不能把#include 指令放置在 extern C { ... } 里面
我们先来看一个例子现有a.hb.hc.h以及foo.cpp其中foo.cpp包含c.hc.h包含b.hb.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.hb.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: 难道任何#include指令都不能放在extern C里面吗
A: 正像这个世界的大多数规则一样总会存在特殊情况。
有时候你可能利用头文件机制“巧妙”的解决一些问题。比如#pragma pack的问题。这些头文件和常规的头文件作用是不一样的它们里面不会放置C的函数声明或者变量定义链接规范不会对它们的内容产生影响。这种情况下你可以不必遵守这些规则。
更加一般的原则是在你明白了这所有的原理之后只要你明白自己在干什么那就去做吧。
Q: 你只说了不应该放入e x t e r n C的但什么可以放入呢
A: 链接规范仅仅用于修饰函数和变量以及函数类型。所以严格的讲你只应该把这三种对象放置于extern C的内部。
但你把C语言的其它元素比如非函数类型定义结构体枚举等放入extern C内部也不会带来任何影响。更不用说宏定义预处理指令了。
所以如果你更加看重良好组织和管理的习惯你应该只在必须使用extern C声明的地方使用它。即使你比较懒惰绝大多数情况下把一个头件自身的所有定义和声明都放置在externC里面也不会有太大的问题。 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(Dont Repeat Yourself)原则你总要为之付出额外的代价。解决它的一个简单方案是定义一个特定的头文件——比如clinkage.h在其中增加这样的定义 以下举例中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()
{coutthis is cpp fun callendl;
}int main()
{cfun();return 0;
}
2cfun.h同上 cppfun.cpp 如下
extern C
{#include cfun.h//注意include语句一定要单独占一行;
}
#include cppfun.h
#include iostream
using namespace std;void cppfun()
{coutthis is cpp fun callendl;
}int main()
{cfun();return 0;
}
3cfun.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()
{coutthis is cpp fun callendl;
}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.hvoid cfun()
{printf(this is c fun call\n);
}extern void cppfun();int main()
{
#ifdef __cpluspluscfun();
#endifcppfun();return 0;
}