首页 > 编程语言 >名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀

时间:2023-04-25 16:01:08浏览次数:35  
标签:__ 函数 00000000 约定 C++ int extern


关于extern_C
通常,在C语言的头文件中经常可以看到类似下面这种形式的代码

#ifdef  __cplusplus  
extern "C" {  
#endif  


/**** some declaration or so *****/  


#ifdef  __cplusplus  
}  
#endif  /* end of __cplusplus */

那么,这种写法什么用呢?实际上,这是为了让CPP能够与C接口而采用的一种语法形式。之所以采用这种方式,是因为两种语言之间的一些差异所导致的。由于CPP支持多态性,也就是具有相同函数名的函数可以完成不同的功能,CPP通常是通过参数区分具体调用的是哪一个函数。在编译的时候,CPP编译器会将参数类型和函数名连接在一起,于是在程序编译成为目标文件以后,CPP编译器可以直接根据目标文件中的符号名将多个目标文件连接成一个目标文件或者可执行文件。但是在C语言中,由于完全没有多态性的概念,C编译器在编译时除了会在函数名前面添加一个下划线之外,什么也不会做(至少很多编译器都是这样干的)。由于这种的原因,当采用CPP与C混合编程的时候,就可能会出问题。假设在某一个头文件中定义了这样一个函数:

int foo(int a, int b);

而这个函数的实现位于一个.c文件中,同时,在.cpp文件中调用了这个函数。那么,当CPP编译器编译这个函数的时候,就有可能会把这个函数名改成_fooii,这里的ii表示函数的第一参数和第二参数都是整型。而C编译器却有可能将这个函数名编译成_foo。也就是说,在CPP编译器得到的目标文件中,foo()函数是由_fooii符号来引用的,而在C编译器生成的目标文件中,foo()函数是由_foo指代的。但连接器工作的时候,它可不管上层采用的是什么语言,它只认目标文件中的符号。于是,连接器将会发现在.cpp中调用了foo()函数,但是在其它的目标文件中却找不到_fooii这个符号,于是提示连接过程出错。extern “C” {}这种语法形式就是用来解决这个问题的。本文将以示例对这个问题进行说明。

首先假设有下面这样三个文件:

在这个头文件中只定义了一个函数,ThisIsTest()。这个函数被定义为一个外部函数,可以被包括到其它程序文件中。假设ThisIsTest()函数的实现位于test_extern_c.c文件中:

/* test_extern_c.c */  

#include "test_extern_c.h"  

int ThisIsTest(int a, int b)  
{  
    return (a + b);  
}

可以看到,ThisIsTest()函数的实现非常简单,就是将两个参数的相加结果返回而已。现在,假设要从CPP中调用ThisIsTest()函数:

/* main.cpp */  
#include "test_extern_c.h"  
#include <stdio.h>  
#include <stdlib.h>  

class FOO {  
public:  

int bar(int a, int b)  
{  
    printf("result=%i\n", ThisIsTest(a, b));  
}  
};  

int main(int argc, char **argv)  
{  
    int a = atoi(argv[1]);  
    int b = atoi(argv[2]);   

    FOO *foo = new FOO();  

    foo->bar(a, b);  

    return(0);  
}

在这个CPP源文件中,定义了一个简单的类FOO,在其成员函数bar()中调用了ThisIsTest()函数。下面看一下如果采用gcc编译test_extern_c.c,而采用g++编译main.cpp并与test_extern_c.o连接会发生什么情况

[cyc@cyc src]$ gcc -c test_extern_c.c  

[cyc@cyc src]$ g++ main.cpp test_extern_c.o  

[cyc@cyc src]$ ./a.out 4 5                  

result=9

可以看到,程序没有任何异常,完全按照预期的方式工作。那么,如果将test_extern_c.h中的extern “C” {}所在的那几行注释掉会怎样呢?注释后的test_extern_c.h文件内容如下:

/* test_extern_c.h */  
#ifndef __TEST_EXTERN_C_H__  
#define __TEST_EXTERN_C_H__  

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

/*  
 * this is a test function, which calculate  
 * the multiply of a and b.  
 */  

extern int ThisIsTest(int a, int b);  


//#ifdef   __cplusplus  
//  }  
//#endif   /* end of __cplusplus */  

#endif

除此之外,其它文件不做任何的改变,仍然采用同样的方式编译test_extern_c.c和main.cpp文件:

[cyc@cyc src]$ gcc -c test_extern_c.c  

[cyc@cyc src]$ g++ main.cpp test_extern_c.o  

/tmp/cca4EtJJ.o(.gnu.linkonce.t._ZN3FOO3barEii+0x10): In function `FOO::bar(int, int)':  

: undefined reference to `ThisIsTest(int, int)'  

collect2: ld returned 1 exit status

在编译main.cpp的时候就会出错,连接器ld提示找不到对函数ThisIsTest()的引用。

为了更清楚地说明问题的原因,我们采用下面的方式先把目标文件编译出来,然后看目标文件中到底都有些什么符号:

[cyc@cyc src]$ gcc -c test_extern_c.c      

[cyc@cyc src]$ objdump -t test_extern_c.o  

test_extern_c.o:     file format elf32-i386  

SYMBOL TABLE:  

00000000 l    df *ABS*  00000000 test_extern_c.c  

00000000 l    d  .text  00000000  

00000000 l    d  .data  00000000  

00000000 l    d  .bss   00000000  

00000000 l    d  .comment       00000000  

00000000 g     F .text  0000000b ThisIsTest  
[cyc@cyc src]$ g++ -c main.cpp            
[cyc@cyc src]$ objdump -t main.o          
main.o:     file format elf32-i386  
SYMBOL TABLE:  

00000000 l    df *ABS*  00000000 main.cpp  

00000000 l    d  .text  00000000  

00000000 l    d  .data  00000000  

00000000 l    d  .bss   00000000  

00000000 l    d  .rodata        00000000  

00000000 l    d  .gnu.linkonce.t._ZN3FOO3barEii 00000000  

00000000 l    d  .eh_frame      00000000  

00000000 l    d  .comment       00000000  

00000000 g     F .text  00000081 main  

00000000         *UND*  00000000 atoi  

00000000         *UND*  00000000 _Znwj  

00000000         *UND*  00000000 _ZdlPv  

00000000  w    F .gnu.linkonce.t._ZN3FOO3barEii 00000027 _ZN3FOO3barEii  

00000000         *UND*  00000000 _Z10ThisIsTestii  

00000000         *UND*  00000000 printf  

00000000         *UND*  00000000 __gxx_personality_v0

可以看到,采用gcc编译了test_extern_c.c之后,在其目标文件test_extern_c.o中的有一个ThisIsTest符号,这个符号就是源文件中定义的ThisIsTest()函数了。而在采用g++编译了main.cpp之后,在其目标文件main.o中有一个_Z10ThisIsTestii符号,这个就是经过g++编译器“粉碎”过后的函数名。其最后的两个字符i就表示第一参数和第二参数都是整型。而为什么要加一个前缀_Z10我并不清楚,但这里并不影响我们的讨论,因此不去管它。显然,这就是原因的所在,其原理在本文开头已作了说明。

[cyc@cyc src]$ gcc -c test_extern_c.c  

[cyc@cyc src]$ objdump -t test_extern_c.o  

test_extern_c.o:     file format elf32-i386  

SYMBOL TABLE:  

00000000 l    df *ABS*  00000000 test_extern_c.c  

00000000 l    d  .text  00000000  

00000000 l    d  .data  00000000  

00000000 l    d  .bss   00000000  

00000000 l    d  .comment       00000000  

00000000 g     F .text  0000000b ThisIsTest

那么,为什么采用了extern “C” {}形式就不会有这个问题呢,我们就来看一下当test_extern_c.h采用extern “C” {}的形式时编译出来的目标文件中又有哪些符号:

[cyc@cyc src]$ g++ -c main.cpp  

[cyc@cyc src]$ objdump -t main.o  

main.o:     file format elf32-i386  

SYMBOL TABLE:  

00000000 l    df *ABS*  00000000 main.cpp  

00000000 l    d  .text  00000000  

00000000 l    d  .data  00000000  

00000000 l    d  .bss   00000000  

00000000 l    d  .rodata        00000000  

00000000 l    d  .gnu.linkonce.t._ZN3FOO3barEii 00000000  

00000000 l    d  .eh_frame      00000000  

00000000 l    d  .comment       00000000  

00000000 g     F .text  00000081 main  

00000000         *UND*  00000000 atoi  

00000000         *UND*  00000000 _Znwj  

00000000         *UND*  00000000 _ZdlPv  

00000000  w    F .gnu.linkonce.t._ZN3FOO3barEii 00000027 _ZN3FOO3barEii  

00000000         *UND*  00000000 ThisIsTest  

00000000         *UND*  00000000 printf  

00000000         *UND*  00000000 __gxx_personality_v0

注意到这里和前面有什么不同没有,可以看到,在两个目标文件中,都有一个符号ThisIsTest,这个符号引用的就是ThisIsTest()函数了。显然,此时在两个目标文件中都存在同样的ThisIsTest符号,因此认为它们引用的实际上同一个函数,于是就将两个目标文件连接在一起,凡是出现程序代码段中有ThisIsTest符号的地方都用ThisIsTest()函数的实际地址代替。另外,还可以看到,仅仅被extern “C” {}包围起来的函数采用这样的目标符号形式,对于main.cpp中的FOO类的成员函数,在两种编译方式后的符号名都是经过“粉碎”了的。

因此,综合上面的分析,我们可以得出如下结论:采用extern “C” {} 这种形式的声明,可以使得CPP与C之间的接口具有互通性,不会由于语言内部的机制导致连接目标文件的时候出现错误。需要说明的是,上面只是根据我的试验结果而得出的结论。由于对于CPP用得不是很多,了解得也很少,因此对其内部处理机制并不是很清楚,如果需要深入了解这个问题的细节请参考相关资料。

备注:
1. 对于要在cpp中使用的在c文件中写好的函数func(),只需要在c文件的头文件中添加extern “C”声明就可以了。比如:extern “C” func() { …}

当然,可以使用

#ifdef __cplusplus

extern "C" {

#endif

和


#ifdef __cplusplus

}

#endif

将整个c文件的函数全都括起来

C++保留了一部分过程式语言的特点,因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。
extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。
比如说你用C 开发了一个DLL 库,为了能够让C ++语言也能够调用你的DLL输出(Export)的函数,你需要用extern “C”来强制编译器不要修改你的函数名。

揭秘extern “C”

从标准头文件说起

#ifndef __INCvxWorksh  /*防止该头文件被重复引用*/
#define __INCvxWorksh

#ifdef __cplusplus    //__cplusplus是cpp中自定义的一个宏
extern "C" {          //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
#endif

    /**** some declaration or so *****/  

#ifdef __cplusplus
}
#endif

#endif /* __INCvxWorksh */

extern “C”的含义

extern “C” 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。
被extern “C”限定的函数或变量是extern类型的;
1、extern关键字
extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在链接阶段中从模块A编译生成的目标代码中找到此函数。
与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

2、被extern “C”修饰的变量和函数是按照C语言方式编译和链接的
首先看看C++中对类似C的函数是怎样编译的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。
_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。 例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。
同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以”.”来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

3、举例说明
(1)未加extern “C”声明时的连接方式
假设在C++中,模块A的头文件如下:

// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif
//在模块B中引用该函数:
// 模块B实现文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);

实际上,在连接阶段,链接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

(2)加extern “C”声明后的编译和链接方式
加extern “C”声明后,模块A的头文件变为:

// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif

在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:

<1>A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;

<2>链接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

如果在模块A中函数声明了foo为extern “C”类型,而模块B中包含的是extern int foo(int x, int y),则模块B找不到模块A中的函数;反之亦然。

extern “C”这个声明的真实目的是为了实现C++与C及其它语言的混合编程。

应用场合

C++代码调用C语言代码、在C++的头文件中使用
在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

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

而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern “C”声明,在.c文件中包含了extern “C”时会出现编译语法错误

/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);     //注:写成extern "C" int add(int , int ); 也可以
#endif

/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
 return x + y;
}

// c++实现文件,调用add:cppFile.cpp
extern "C"
{
 #include "cExample.h"        //注:此处不妥,如果这样编译通不过,换成 extern "C" int add(int , int ); 可以通过
}

int main(int argc, char* argv[])
{
 add(2,3);
 return 0;
}

如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern “C”{}。

在C中引用C++语言中的函数和变量时,C++的头文件需添加extern “C”,但是在C语言中不能直接引用声明了extern “C”的该头文件,应该仅将C文件中将C++中定义的extern “C”函数声明为extern类型

//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif

//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
 return x + y;
}

/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
 add( 2, 3 );
 return 0;
}

 

 

===================================================================

所谓名字修饰约定,就是指变量名、函数名等经过编译后重新输出名称的规则。
比如源代码中函数名称为int Func(int a,int b),经过编译后名称可能为?Func@@YAHHH@Z、?Func@@YGHHH@Z、_Func@8,也有可能与源代码中名称相同为Func。
影响编译后输出的名称通常与名字修饰约定(extern "C"、extern "C++"等)和函数调用约定(__stdcall、__cdecl等)等相关。

首先,用C方式导出两个函数:

Dll1.c

_declspec(dllexport) int __cdecl Func_cdecl(int a,int b){    return 1;} 

_declspec(dllexport) int __stdcall Func_stdcall(int a,int b){    return 1;}

导出的两个函数名为:

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀_目标文件


再以C++方式导出:

Dll1.cpp

_declspec(dllexport) int __cdecl Func_cdecl(int a,int b){    return 1;} 

_declspec(dllexport) int __stdcall Func_stdcall(int a,int b){    return 1;}

导出的两个函数名为:

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀_编译器_02

 

 

然后,我们再以C++方式导出如下代码中的函数:

extern "C" _declspec(dllexport) int __stdcall Func_C_stdcall(int a,int b){    return 1;} 

extern "C++" _declspec(dllexport) int __stdcall Func_CPP_stdcall(int a,int b){   return 1;} 

extern "C" _declspec(dllexport) int __cdecl Func_C_cdecl(int a,int b){    return 1;} 

extern "C++" _declspec(dllexport) int __cdecl Func_CPP_cdecl(int a,int b){    return 1;}

导出结果如下:

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀_编译器_03


 

有了以上实验结果,我们再结合以下名字输出规则进行理解:

C方式编译(extern "C"):

__stdcall调用约定:输出名称在原名称前加一下划线,后面再加上一个“@”和其参数的总字节数(_原名称@参数总字节数),如名称int
 Func_C_stdcall(int a,int b)输出为_Func_C_stdcall@8;

__cdecl调用约定:与原名称相同,如名称int Func_C_cdecl(int a,int b)输出还是为Func_C_cdecl;

C++方式编译(extern "C++"):

__stdcall调用约定:
输出名称以“?”开始,后跟原名称;原名称后再跟“@@YG”,后面再跟返回值代号和参数表代号,代号表示如下:

  • X--void ,
  • D--char,
  • E--unsigned char,
  • F--short,
  • H--int,
  • I--unsigned int,
  • J--long,
  • K--unsigned long,
  • M--float,
  • N--double,
  • _N--bool,
  • ...

PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。如名称int Func_CPP_stdcall(int a,int b)编译后的输出名称为?Func_CPP_stdcall@@YGHHH@Z。__cdecl调用约定:与_stdcall调用约定基本一致,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。如名称int
 Func_CPP_cdecl(int a,int b)编译后输出名称为?Func_CPP_cdecl@@YAHHH@Z。

有个这个规则,再回头去看我们的实验结果,就很好理解了。
当然,编译C文件和编译CPP文件,不需加extern"C"和extern "C++",

因为编译C文件当然默认的是extern "C",而编译CPP文件则默认的是extern"C++"。

现在我们也能理解为什么导出DLL时通常需要加上extern"C"。试想,如果一个C++导出的dll,没有加extern "C",则导出的名称为extern "C++"约定下的名称。如果这个dll需要提供给用C编写的程序使用,那么这个程序是无法调用这个dll的,因为C写的程序遵循的是extern "C"约定,链接时链接器将按照extern "C"约定的名称去寻找外部名称,这当然找不到,因为dll中的输出名称为extern "C++"约定下的名称。

================================================================

就以为是约定俗成,其实也算是约定俗成,这样做的目的是为了防止符号名冲突,因为在一个程序中往往是包含汇编和C文件的,汇编用于启动部分,C文件用于应用程序,最终通过编译器实现编译,对于编译器来说,汇编和C是一视同仁的,那么就会有个问题,如果在汇编和C文件中使用了同一个名字,这是很可能出现的,毕竟汇编相当于机器码也算是稍微高级的语言,在定义子程序或函数时,也是可以用英文拼写的,而C文件中,更会习惯用英文拼写。

    所以为了防止类似的符号名冲突,UNIX下的C语言就规定,C语言的源代码文件中的所有全局变量和函数经过编译后,相应的符号名前面会自动的加上下划线“_”。这样做的好处,就是方便是程序开发人员,不用太小心翼翼的起名,避免了与汇编文件中的符号名的冲突。

==================================================================

有关DLL的问题现在资料很多,但是很多人写DLL时经常出现调用程序无法找到相关的导出函数的问题,这里主要的原因是DLL在声明时出的问题。
在这里主要有两个问题,一个是调用约定的问题,一个是函数名修饰的问题,而这两个问题又是相互影响的。
一:声明为:extern "C" int __declspec(dllexport)add(int x, int y);
这种声明是强制用C语言方式进行修饰,且用C的默认约定,即__cdecl方式。这种方式编译产生的DLL中有一个导出函数:add,不加任何修饰。
二:声明为:extern "C" int __declspec(dllexport) __stdcall add(int x, int y);
这种声明是强制用C语言方式进行修饰,且用stdcall约定,这种方式编译产生的DLL中有一个导出函数:_add@8,即前面有“_”,后面加了参数长。
三:声明为:int __declspec(dllexport) __stdcall add(int x, int y);
这种声明不强制用C语言方式进行修饰,但是用stdcall约定,这种方式编译产生的DLL中有一个导出函数:?add@@YGHHH@Z。这个名字很怪,后面的不好理解。
四:声明为:int __declspec(dllexport) __cdecl add(int x, int y);
这种声明是不强制用C语言修饰,且用cdecl约定,这种方式编译产生的DLL中有一个导出函数:?add@@YAHHH@Z,注意看,和第三种方有一点不同。

实验一:显式调用方式调用DLL中的add函数。

#include <stdio.h>
#include <windows.h>
typedef  int(_stdcall *lpAddFun)(int, int); //宏定义函数指针类型
int main(int argc, char *argv[])
{
HINSTANCE hDll; //DLL句柄 
lpAddFun addFun; //函数指针
hDll = LoadLibrary("1.dll");
if (hDll != NULL)
{
addFun = (lpAddFun)GetProcAddress(hDll, "add");
if (addFun != NULL)
{
int result = addFun(2, 3);
printf("%d", result);
}
else
printf("No Function");
}
else
printf("NO DLL");
FreeLibrary(hDll);
return 0;
}
方式一:调用成功。另外三种方式全部出错
实验二:隐式调用DLL中的add函数
#include <stdio.h>
#include <windows.h>
#pragma comment(lib,"1.lib") 
extern "C" int __declspec(dllimport) add(int x, int y);//声明方式随着DLL中的声明方式改变
int main(int argc, char *argv[])
{
int result = add(2, 3);
printf("%d", result);
return 0;
}

方式一:调用成功。另外发现一个奇怪现象:在调用程序中
声明函数时extern "C" int __declspec(dllimport) add(int x, int y);
写作:extern "C" int __declspec(dllecprot) add(int x, int y);同样成功,将__declspec(…)去掉也同样成功。换句话说,在调用DLL的程序中,导入是没有必要加的。
方式二:调用成功。同样出现上面导入标识可以不加的现象。
方式三:调用成功,同样也出现上面导入标识可以不加的现象。
方式四:调用成功,同样也出现上面导入标识可以不加的现象。
总结:对于DLL导出函数声明的四种写法,在动态调用时,
声明成这样:extern "C" int __declspec(dllimport) add(int x, int y);是最好的,其它声明方式调用都没有成功。但是众所周知,windows默认的调用约定是stdcall方式,如果想别的语言能用DLL的话,最好是将调用约定写成stdcall方式,但是这种方式又不能动态调用。
在隐式调用时,四种声明方式都是可以的,只要调用者的声明方式和DLL声明时的方式一致即可。另外,在调用程序中对于导入的声明是可以去掉的,大量书籍中关于导入、导出的问题都是利用宏来处理的,如:在头文件中写作:
#ifdef DLL_FILE
extern "C" int __declspec(dllexport) add(int x, int y);
#else
extern "C" int __declspec(dlleximport) add(int x, int y);
这样这个头文件既可以用在DLL工程中,又可以用在调用程序中,但是经过实验发现,这个根本就没有必要,在调用者程序中不管是写作__declspce(dllexport)还是写作__declspec(dllimport)或者不写都能成功调用。
关于DEF文件
在DLL工程中引用DEF文件,内容如下:
LIBRARY 1
EXPORTS
add @ 1
通过depends查看导出函数全是add,但是隐式方式调用时,还是要求调用者的声明方式和DLL中声明方式相同。
对于动态调用实验结果:
方式一:成功。方式二:不成功,但是将函数指针改为typedef int(_stdcall *lpAddFun)(int, int);成功,即调用者要声明约定方式与DLL中声明的调用约定方式相同,否则报错。
方式三:同方式二,同样要将函数指针改为typedef int(_stdcall *lpAddFun)(int, int);才成功完成调用。
方式四:成功。
总结:通过DEF文件来导出函数,调用者同样也要声明相同的调用约定,即_stdcall或是_cdecl必须要相同,其中_cdecl是C语言默认方式。

=================================================================

__imp,说明那不是真正的静态库,而是某个动态库的导入库
导入函数和函数自己不同名,所以加__imp
比如__imp_printf忘了有没有后缀了,就是 printf函数

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀_目标文件_04

所谓名字修饰约定,就是指变量名、函数名等经过编译后重新输出名称的规则。
比如源代码中函数名称为int Func(int a,int b),经过编译后名称可能为?Func@@YAHHH@Z、?Func@@YGHHH@Z、_Func@8,也有可能与源代码中名称相同为Func。
影响编译后输出的名称通常与名字修饰约定(extern "C"、extern "C++"等)和函数调用约定(__stdcall、__cdecl等)等相关。
口说千遍,不如实际演练一遍。那么,就让我们写代码来测试下。
注意,本文只讨论extern
 extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定,其他约定不在本文讨论范围内。

另外,编译的环境为WIN10  + VC2013

extern "C"+__cdecl

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀_编译器_05

gpleadrailcontrol.h

#ifndef _GP_LEAD_RAIL_CONTROL_H_
#define _GP_LEAD_RAIL_CONTROL_H_

#ifdef GPLEADRAILCONTROL_EXPORTS
# define CTRL_API __declspec(dllexport)
#else
# define CTRL_API __declspec(dllimport)
#endif

#ifdef  __cplusplus
# define MODBUS_BEGIN_DECLS  extern "C" {
# define MODBUS_END_DECLS    }
#else
# define MODBUS_BEGIN_DECLS
# define MODBUS_END_DECLS
#endif

MODBUS_BEGIN_DECLS

#define GP_LR_RET_SUCCESS 0		// 返回成功	
#define GP_LR_RET_ERROR   -1    // 返回错误
#define GP_LR_SET_SUCCESS 1		// 导轨控制设置成功
#define GP_LR_SET_FAILED  2     // 导轨控制设置失败


typedef struct _GP_LR_MOVE_DATA_
{
	char chDirection;			// 方向
	int  nSpeed;				// 速度等级
	int  nAcceleration;			// 加速度等级
	int  nPulseCount;			// 脉冲数
}GP_LR_MOVE_DATA;

int		CTRL_API GP_LR_Init(char* pComNameOrIpAddr, int nBaudRateOrPort);
void	CTRL_API GP_LR_Deinit();
int	    CTRL_API GP_LR_Open(int nDevIndex);
int     CTRL_API GP_LR_Close(int nDevIndex);
int     CTRL_API GP_LR_Home(int nDevIndex);
int	    CTRL_API GP_LR_Move(int nDevIndex, GP_LR_MOVE_DATA* pMoveData);
int		CTRL_API GP_LR_Stop(int nDevIndex);
int		CTRL_API GP_LR_SetSpeed(int nDevIndex,int nSpeed);
int		CTRL_API GP_LR_SetAcceleration(int nDevIndex, int nAcceleration);
int     CTRL_API GP_LR_GetInfo(int nDevIndex, GP_LR_MOVE_DATA* pstLrPositionInfo);

MODBUS_END_DECLS
#endif // _GP_LEAD_RAIL_CONTROL_H_

gpleadrailcontrol.cpp

// gpleadrailcontrol.cpp : Defines the exported functions for the DLL application.
#include "gpleadrailcontrol.h"
#include "LeadRailCtrl.h"
#include "singleton_instance.hpp"


extern "C"
{

	int CTRL_API GP_LR_Init(char* pComNameOrIpAddr, int nBaudRateOrPort)
	{
		int nRet = base::singleton_instance<LeadRailCtrl>::instance()->Init(pComNameOrIpAddr, nBaudRateOrPort);
		return nRet;
	}

	void CTRL_API GP_LR_Deinit()
	{
		base::singleton_instance<LeadRailCtrl>::instance()->DeInit();
	}

	int CTRL_API GP_LR_Open(int nDevIndex)
	{
		int nRet = base::singleton_instance<LeadRailCtrl>::instance()->OpenLR(nDevIndex);
		return nRet;
	}
	int CTRL_API GP_LR_Close(int nDevIndex)
	{
		int nRet = base::singleton_instance<LeadRailCtrl>::instance()->CloseLR(nDevIndex);
		return nRet;
	}
	int CTRL_API GP_LR_Home(int nDevIndex)
	{
		GP_LR_MOVE_DATA stLrMoveData = { 0 };
		int nRet = base::singleton_instance<LeadRailCtrl>::instance()->HomeLR(nDevIndex);
		return nRet;
	}
	int CTRL_API GP_LR_Move(int nDevIndex, GP_LR_MOVE_DATA* pMoveData)
	{
		int nRet = base::singleton_instance<LeadRailCtrl>::instance()->MoveLR(nDevIndex, pMoveData);
		return nRet;
	}

	int CTRL_API GP_LR_Stop(int nDevIndex)
	{
		int nRet = base::singleton_instance<LeadRailCtrl>::instance()->StopLR(nDevIndex);
		return nRet;
	}

	int	CTRL_API GP_LR_SetSpeed(int nDevIndex, int nSpeed)
	{
		int nRet = base::singleton_instance<LeadRailCtrl>::instance()->SetSpeed(nDevIndex, nSpeed);
		return nRet;
	}

	int	CTRL_API GP_LR_SetAcceleration(int nDevIndex, int nAcceleration)
	{
		int nRet = base::singleton_instance<LeadRailCtrl>::instance()->SetAcceleration(nDevIndex, nAcceleration);
		return nRet;
	}

	int CTRL_API GP_LR_GetInfo(int nDevIndex, GP_LR_MOVE_DATA* pstLrPositionInfo)
	{
		int nRet = base::singleton_instance<LeadRailCtrl>::instance()->GetCurrentInfo(nDevIndex, pstLrPositionInfo);
		return nRet;
	}


}

 

结果gpleadrailcontrol.dll的导入库gpleadrailcontrol.lib

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀_头文件_06

 

二、extern "C"+__stdcall

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀_编译器_07

如果屏蔽头文件中的//MODBUS_BEGIN_DECLS //MODBUS_END_DECLS编译会报错

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀_头文件_08

 

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀_编译器_09

 

__cdecl (/Gd):?xxxx@@YAHPADH@Z   同 代码中有extern"C++" +__cdecl (/Gd)

名字修饰约定: extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定、__imp_前缀_编译器_10

__stdcall (/Gz):?xxxx@@YGHPADH@Z 同 代码中有extern"C++" +__cdecl (/Gd)

 

__fastcall (/Gr):?xxxx@@YIHPADH@Z 同 代码中有extern"C++" +__cdecl (/Gd)

__vectorcall (/Gv):?xxxx@@YQHPADH@Z 同 代码中有extern"C++" +__cdecl (/Gd)

 

代码有extern"C" + __cdecl (/Gd):_xxxx

代码有extern"C" __stdcall (/Gz):_xxxx@8

 

代码有extern"C" __fastcall (/Gr):@xxxx@8

代码有extern"C" __vectorcall (/Gv):xxxx@@8

 

标签:__,函数,00000000,约定,C++,int,extern
From: https://blog.51cto.com/u_16081664/6224235

相关文章

  • java通过url得到文件对象(支持http和https)
    文字标题:java通过url得到文件对象(支持http和https)作者:锅巴1.场景:通过一个url地址来得到一个文件,此方式就是通过一个url将文件下载到本地的临时文件,直接上代码/***远程读取文件**@paramnetUrl*@return*/publicstaticFilegetNet......
  • Beats介绍
    1简介ElasticStack传统上由三个主要组件(Elasticsearch,Logstash和Kibana)组成现在早已脱离了这种组合,也可以与名为“Beats”的第四个元素结合使用--一个针对不同用例的日志运送者系列。现在网上有一种说法叫做ELKB,这里的B就是指的beats在集中式日志记录中,数据管......
  • Linux性能优化篇-了解CPU上下文切换
     我们了解到导致平均负载,有可能是以下几种方面:CPU密集型(造成cpu利用率升高,可以理解)I/O密集型(io和cpu互斥的,也造成cpu利用率增高-不可中断进程的)大量进程(???)根据平均负载的解释,单位时间内的处于可运行的进程和不可中断进程的进程数,Systemloadaveragesistheaveragenumberofproc......
  • 语义分割专栏(零)语义分割概述
    前言 在计算机视觉领域中,图像识别是一项非常重要的任务。而语义分割则是其中的一个子任务。与图像分类和目标检测不同,语义分割不仅需要识别出图像中的物体,还需要将每个像素分配给它所属的类别。本专栏适用于想要入门语义分割与想要对语义分割有一个全面系统的了解的读者。本教程......
  • 创信国产操作系统uos桌面卡死的解决办法
    如果是整个桌面卡死,不要按主机电源键强制关机,重启后有可能会有配置文件缺失的问题。----首先尝试:按Ctrl+Alt+F2,进入tty2输入用户名,回车输入密码,回车输入命令回车:killallkwin_x11按Ctrl+Alt+F1,回到桌面----如果还是没有缓解:按Ctrl+Alt+F2,进入tty2按Ctrl+......
  • H3C R4900 G5做RAID安装系统
    一、先进行BIOS设置HDM接口,通过HDM远程管理界面,按F10创建RAID 二、选择安装系统三、这样RAID1就创建好,如果还要创建RAID,再点“创建RAID” 四、再创建一个RAID5  五、创建完了RAID,然后返回到主页面,退出这个界面; 六、加载镜像文件,按F7选择从虚拟光驱引导; ......
  • git常用命令
    1.新建代码 在当前目录新建一个Git代码库gitinit 新建一个目录,将其初始化为Git代码库gitinit[project-name]下载一个项目和它的整个代码历史gitclone[url] ......
  • 汇编_将数据、代码和栈放入不同的段
    数据放在哪里之前的程序,只有一个代码段,需要运算的数据直接编码在代码里,例如:movax,1。如果我们想计算多个数求和,不能也傻乎乎的add多次,因为多个数可能是不一样,循环都不可以用。我们需要一段安全的空间用来存放数据。它更像是一个数组,占据一段连续的内存空间,通过[bx++]方式,可以对......
  • static成员变量 singleton
    99写入a5,再a5写入a0(返回地址)如果声明为返回类型为&,实际也一样......
  • 公用工程水电气热仪表能源消耗数据采集监测方案
     www.daq-iot.com19936624847--------------系统以降低能耗为目标,以能耗监视、统计分析、考核及管理为手段,融入先进的节能思想,为用户提供可视化的服务。系统向下对接企业的能源计量仪表和相关系统(MIS/EMS/DCS/ERP/OA等),向上通过共享数据接口功能提供上级政府部门要求上报的......