关于extern "C"深度剖析以及宏__cplusplus的作用

在自己第一次接触C++  dll的概念以及书写dll的时候,就会在网上看到这样的说法,在dll导出的时候,一定要加上extern "C",至于为什么大概知道是因为,C编译器和C++编译器对函数的处理不太一样,所以要这样做,没有深追究,但是后来别人问起的时候,感觉总是说的不是特别的透彻。

具体的问题可能如下:

1.如果我的代码只是在C++上使用,也就是只是在C++的编译器上应用,而不是放到C编译器下,还用extern "C"吗?

2.用了extern "C",以后的代码是在C++编译器上没有问题,如果放到C编译器上会报错吗?如何解决?

3.能给我举一个例子吗,不使用extern "C",在不同的编译器环境下,会报链接错误?

以及在C编译器下和C++编译器下的obj文件里的符号表里,变量和函数的命名是怎样进行的吗?

4.有extern "C",那么有extern"C++"吗?

为了回答上面的几个问题,先把原理讲一讲,我们才能更加容易的回答上面的几个问题。

 

首先要注意的是C++支持函数重载,就是函数的名字是可以相同的,只要函数的参数个数,类型,参数顺序有一个不同就可以了,

同时在C++里还引入了命名空间的概念,就是在不同的命名空间下,可以有两个完全相同的函数,而C语言是不支持函数重载的和命名空间的,另外,C++程序的构造方式仍然继承了C语言的传统,编译器是以.c,.cpp为单位,在编译阶段,每个cpp文件生成一个obj文件,编译器把每一个通过命令行制定的源代码文件看做一个独立的编译单元,生成目标文件.obj,然后链接器通过查找目标文件的符号表将他们链接到一起生成可执行程序。

编译和链接是两个阶段的事情,事实上,编译器和链接器是两个完全独立的工具。编译器可以通过语义分析知道那些同名符号之前的差别,而链接器却只能通过目标文件的符号表中保存的名字来识别对象。

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

编译器进行名字粉碎的目的是为了让链接器在工作的时候不陷入困惑,将所有的名字重新进行编码,生成全局唯一,不重复的新名字,让链接器能够准确识别每个名字所对应的对象。

对于C语言的编译器,因为没有命名空间,以及函数重载的概念,所有就很好处理,可以直接把函数的名字加个_之类的就行了,

因为本来名字就是全局唯一的。而C++的编译器可就不行了,如果它也这么简单的搞下,就区分不开命名空间里的相同函数,以及函数重载的函数了,那么它可能把命名空间,还是参数类型,个数这些信息也加上去,才能生成全局唯一的标识。

这样也就产生了一个问题,两种语言的编译器对待名字的处理方式是不一样的,这就给C和C++混合编程带来了麻烦。

就是c++编译器编译的库和c语言编译的库,在另外一个相对的环境下,不能使用了,因为在链接的时候,虽然知道函数名字,但是函数名字的规则不一样了,会出现链接错误,为了解决上面出现的问题,C++引入了链接规范(linkage specification)的概念,表示法为 extern "language string",C++编译器普遍支持的"language string"有"C"和"C++",分别对应C语言和C++语言。

链接规范的作用是告诉C++编译器,对于所有使用了链接规范进行修饰的声明或者定义,应该按照制定语言的方式来处理,比如名字,调用习惯等等。

链接规范的用法有两种:

a.单个声明的链接规范,比如: extern "C"  void foo();

b.一组声明的链接规范,比如:

extern "C"

void foo();

int bar();

那么我们现在来开始回答上面提出的三个问题,中间会有实际的例子来证明我的说法。

1.如果我的代码只是在C++上使用,也就是只是在C++的编译器上应用,而不是放到C编译器下,还用extern "C"吗?

答---我们之前的描述已经看到了,extern C主要针对的是 C/C++混合编程的情况,如果只有C++的编译器环境当然不用。

2.用了extern "C",以后的代码是在C++编译器上没有问题,如果放到C编译器上会报错吗?如何解决?

答---会报错的,因为之前上面我们说过,是C++后出现的,为了解决 C/C++混合编程的问题,才C++引入了链接规范(linkage specification)的概念,所以extern “C”在C编译器下会报错的。

怎么解决这个问题呢,那么就要区分编译器是C++的编译器还是C语言的编译器,这个C++已经想到了这个问题,他给我们提供了一个宏__cplusplus是某一个被定义的值,具体如下__cplusplus = 199711L

我们通常会这样使用

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

    extern int a_test;

    int max(int a, int b);

#ifdef __cplusplus
}
#endif // __cplusplus

3.能给我举一个例子吗,不使用extern "C",在不同的编译器环境下,会报链接错误?以及在C编译器下和C++编译器下的obj文件里

的符号表里,变量和函数的命名是怎样进行的吗?

这个例子很好举啊,但是现在我没有C编译器的环境,但是我们可以借助我们刚刚学到的知识进行模拟,这样更能够充分的理解

extern “C”,以及编译c/c++文件的时候,是以c/cpp文件为单位的,不以.h文件,下面看下代码,我先描述下我的代码文件

有四个文件,a.h,a.cpp b.h  b.cpp   我在a.cpp中定义了一个函数,max还有一个全局变量,然后在a.h中进行了声明,然后在b.cpp中进行了引用这个函数和全局变量,而b.h中什么也不干,因为b.cpp中不提供对外的函数功能。

//a.h
 #ifdef __cplusplus
extern "C" {
#endif 

	extern int a_test;

	int max(int a, int b);

#ifdef __cplusplus
}
#endif 
//a.cpp  这里一定要看清楚,这里没有包含自身的a.h
int a_test = 5;
int max(int a, int b)
{
	if (a > b)
		return a;
	else return b;
}
//b.h啥也没有
//b.cpp
#include"a.h"

int Temp = a_test;
int Temp1 = max(1,3);

上面的代码就是简单的示意,代码的规范性头文件重复包含问题,不做要求。

下面分析下代码 a.cpp中没有进行 #include "a.h"是我故意的,我测试程序使用的是vs2013,建立的c++工程,因为没有包含a.h,并且编译器是以cpp文件为编译单位的,所以编译器就会默认按C++编译,在obj符号表里生成c++的名字,具体名字规则下面再说,反正和C编译器的不一样。

b.cpp中为了引用a.cpp中定义的函数,就要包含a.h,包含了a.h上面的链接规范就起作用了,告诉编译器编译阶段生成.obj文件的时候就要按照C编译器的原则生成,所以这个在链接的时候就麻烦了,a.cpp中原函数和全局变量是按照C++的规则进行的,而b.cpp中在引用的是有是按照C规则找的,会产生链接错误。

错误	4	error LNK2001: 无法解析的外部符号 _max	E:\张怀超工作常用\测试代码\test\b.obj	test
错误	5	error LNK2001: 无法解析的外部符号 _a_test	E:\张怀超工作常用\测试代码\test\b.obj	test
错误	6	error LNK1120: 2 个无法解析的外部命令	E:\张怀超工作常用\测试代码\test\PDT_Release\test.exe	1	1	test

我们看到了在链接的时候,b.obj中按照C规则编译的,名字我们看到了就是前面加了个_找不到实现了。这个错误是这样的,和我们预想的是一样的。

同时通过这个例子我们也探究了,在C编译器下变量和函数在obj的符号表里的命名规则。下面我们顺便探讨下C++编译器下在obj,符号表的里的命名规则。

测试代码如下

//a.h 不变
#ifdef __cplusplus
extern "C" {
#endif 

	extern int a_test;

	int max(int a, int b);

#ifdef __cplusplus
}
#endif 
//a.cpp 改变,添加#include"a.h"
#include"a.h"
int a_test = 5;
int max(int a, int b)
{
	if (a > b)
		return a;
	else return b;
}
//b.h 不变
//b.cpp 改变 将#include"a.h"去掉,为了引用a.cpp中的变量和函数直接声明
extern int a_test;
int max(int a, int b);

int Temp = a_test;
int Temp1 = max(1,3);

这样写的目的是让a.cpp生成a.obj的时候是按照C编译器规则生成,而b.cpp生成b.obj的时候是按照C++规则,这样就会报链接错误,从而知道在C++规则下是怎样命名的,(这样更只观,大家也可以直接按文本打开obj查看),报的错误信息如下:

错误	1	error LNK2001: 无法解析的外部符号 "int __cdecl max(int,int)" (?max@@YAHHH@Z)	E:\张怀超工作常用\测试代码\test\b.obj	test
错误	2	error LNK2001: 无法解析的外部符号 "int a_test" (?a_test@@3HA)	E:\张怀超工作常用\测试代码\test\b.obj	test
错误	3	error LNK1120: 2 个无法解析的外部命令	E:\张怀超工作常用\测试代码\test\PDT_Release\test.exe	1	1	test

这样我们就能看到C++编译器下的命名规则了。

4.有extern "C",那么有extern"C++"吗?

答---有的,这个是合法的,但是没有看到使用过

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值