extern "c" __declspec(dllexport) __declspec(dllimport)

DLL导出详解

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

extern "C"  __declspec(dllexport)  __declspec(dllimport)都是用于函数或者变量,甚至类的声明的(可以把extern "C"放在class的前面,但是编译器会忽略掉,最后产生的还是C++修饰符,而不是C修饰符)这样的用法有个好处就是下面的代码可以在混有类的函数和 变量上使用下面的宏,虽然对类不起作用:

#ifdef __cplusplus
extern   " C "
{
// 函数声明
// 变量声明,变量一般前面都有extern
// 类声明,这个不起作用,编译器直接忽略掉class前面的extern “C”
#ifdef __cplusplus
}

#endif

C 和C++ 对应不同的调用约定,产生的修饰符也各不相同,如下:

调用约定extern "C" 或 .c 文件.cpp、.cxx 或 /TP

C 命名约定 (__cdecl )

_test

?test@@ZAXXZ

Fastcall 命名约定 (__fastcall )

@test@0

?test@@YIXXZ

标准调用命名约定 (__stdcall )

_test@0

?test@@YGXXZ


__declspec(dllexport)  __declspec(dllimport)一般也是使用宏的形式:

#ifdef ONEDLL_EXPORTS
#define  ONEDLL_API __declspec(dllexport)
#else
#define  ONEDLL_API __declspec(dllimport)
#endif

这样在DLL代码本身就是__declspec(dllexport) ,在使用DLL的程序中就变成了__declspec(dllimport),这两标志分别用来指明当前的函数将被导出,和当前函数是被导入的。
 

上面的两个宏结合一下就是下面这样的了:

//  下列 ifdef 块是创建使从 DLL 导出更简单的
//  宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 ONEDLL_EXPORTS
//  符号编译的。在使用此 DLL 的
//  任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
//  ONEDLL_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
//  符号视为是被导出的。
#ifdef ONEDLL_EXPORTS
#define  ONEDLL_API __declspec(dllexport)
#else
#define  ONEDLL_API __declspec(dllimport)
#endif

//  此类是从 OneDll.dll 导出的
#ifdef __cplusplus
extern   " C "
{
#endif
class  ONEDLL_API COneDll  {
public :
    COneDll( void );
     ~ COneDll( void );
    
     //  TODO: 在此添加您的方法。
     int  m_a;
     int  m_b;
     int   * m_p;
     int  m_n;

     void  AddValue();

}
;

extern  ONEDLL_API  int  nOneDll;

ONEDLL_API  int  fnOneDll( void );

#ifdef __cplusplus
}

#endif


如果调用模块和被调用模块都是C++(而且是同一种编成环境,如VC,甚至需要同一版本的VC),那么就不需要extern “C”了,因为这个标志的作用就是用在函数和变量声明前,无论是调用模块,还是被调用模块,都将生成C修饰符,调用模块将需要C修饰符的函数,而被调用模 块将产生C修饰符的函数,所以这个标志在两者都是C++的时候使用并不受影响,不使用这个标志,也不受影响。
但是如果C模块要调用C++ 模块,那么C++模块就需要使用extern “C”,当然C不用,由于是在头文件的声明中使用,所以使用下面的宏能够使得这个头文件也在C中顺利使用:

#ifdef __cplusplus
extern   " C "
{
// 函数声明
// 变量声明,变量一般前面都有extern
// 类声明,这个不起作用,编译器直接忽略掉class前面的extern “C”
#ifdef __cplusplus
}

#endif


如果C++模块要调用C模块,那么C++模块还是需要extern “C”,当然C不用,由于是在头文件的声明中使用,所以使用上面的宏同样能够使得这个头文件也在C中顺利使用。

总结一下就是加上extern “C”在什么情况下都没错,但是要注意函数重载的问题。



def文件是一种比较麻烦的方法,下面是MSDN中的部分内容:

 

模块定义 (.def) 文件是包含一个或多个描述 DLL 各种属性的 Module 语句的文本文件。如果不使用 __declspec(dllexport)  关键字导出 DLL 的函数,则 DLL 需要 .def 文件。

.def 文件必须至少包含下列模块定义语句:
1.文件中的第一个语句必须是 LIBRARY 语句。此语句将 .def 文件标识为属于 DLL。LIBRARY 语句的后面是 DLL 的名称。链接器将此名称放到 DLL 的导入库中。
2.EXPORTS 语句列出名称,可能的话还会列出 DLL 导出函数的序号值。通过在函数名的后面加上 @ 符和一个数字,给函数分配序号值。当指定序号值时,序号值的范围必须是从 1 到 N,其中 N 是 DLL 导出函数的个数。

例如,包含实现二进制搜索树的代码的 DLL 看上去可能像下面这样:

LIBRARY   BTREE
EXPORTS
   Insert   @ 1
   Delete   @ 2
   Member   @ 3
   Min   @ 4

 

提示:

如果希望优化 DLL 文件的大小,请对每个导出函数使用 NONAME  属性。使用 NONAME  属性时,序号存储在 DLL 的导出表中而非函数名中。如果导出许多函数,这样做可以节省相当多的空间。


其实__declspec(dllexport) 的作用就是让编译器按照某种预定的方式(前面大致解释了这种方式的规则)来输出导出函数及变量的符号,而def文件则是自己为每一个函数和变量指定导出符号,所以def是一个非自动化,手工很强的方式,不是特殊情况的话,实在没有必要浪费这些时间。
还有一个问题,就是使用def会把调用方式和__declspec(dllexport) 的作用全部覆盖掉,所以还需要自己处理调用方式不同产生的错误。
一般使用def文件的情况是你需要使用运行时加载,并且需要使用GetProcAddress函数获得函数地址,这个函数需要直接指明函数产生的导出符号,而可以自己指定导出符号的方式就是使用def。

<think>嗯,用户现在问的是`__declspec(dllexport)`和`__declspec(dllimport)`的区别。之前他们问过关于创建DLL的代码,所以现在这个问题应该是进一步的深入。 首先,我需要确认这两个关键字的用途。记得在Windows的DLL开发中,导出和导入声明是用来控制符号的可见性。当编译DLL本身时,需要用`dllexport`来标记要导出的函数或类,这样其他程序才能使用它们。而当其他程序使用这个DLL时,则需要用`dllimport`来导入这些符号,这样编译器知道这些函数来自外部DLL。 用户可能已经了解了头文件中的条件编译,比如通过预处理器宏来切换这两个关键字。比如在DLL项目中定义MATHLIB_EXPORTS,这样头文件中的MATHLIB_API就会变成导出,否则是导入。这个时候需要解释清楚这两个关键字在不同编译情境下的作用。 另外,用户可能想知道为什么需要区分导出和导入。可能需要解释符号链接的过程,导出是在生成库的时候把符号放到导出表中,导入则是在编译客户端代码时生成正确的调用指令,比如使用导入地址表(IAT)而不是直接调用。 还要注意extern "C"的作用,之前的问题里也提到了,这里可能需要简要提到,但重点还是导出和导入的区别。不过用户的问题里没有提到extern "C",所以可能不需要深入。 可能存在的误区是,用户可能认为这两个关键字的功能有重叠,或者不理解为什么需要条件编译。需要明确说明在编译DLL和使用DLL时,同一个头文件如何通过宏定义来切换这两个属性,确保符号正确导出和导入。 此外,用户可能关心实际应用中的例子,比如导出一个函数时,编译器如何处理,而导入时又是如何生成不同的代码。可能需要提到调用约定、名称修饰等问题,特别是对于C++的函数,如果没有extern "C",名称修饰会导致导入失败,但这个问题可能属于另一个话题。 总结下来,回答的结构应该先分别解释两个关键字的作用,然后对比它们的用途,再说明为什么需要同时存在,最后可能给出一个头文件的条件编译示例,帮助用户理解实际应用中的写法。</think>在Windows平台动态链接库(DLL)开发中,`__declspec(dllexport)`和`__declspec(dllimport)`是控制符号可见性的关键修饰符,它们的区别可以用以下数学表达式描述: **定义域与作用:** $$ \begin{cases} \_\_declspec(dllexport): & \text{当} \quad \text{编译DLL工程} \quad \Rightarrow \quad \text{将符号写入导出表} \\ \_\_declspec(dllimport): & \text{当} \quad \text{使用DLL工程} \quad \Rightarrow \quad \text{从导入表读取符号} \end{cases} $$ **技术对比矩阵:** $$ \begin{array}{|c|c|c|} \hline \text{特性} & \text{\_\_declspec(dllexport)} & \text{\_\_declspec(dllimport)} \\ \hline \text{使用场景} & \text{DLL开发者侧} & \text{DLL使用者侧} \\ \hline \text{编译阶段} & \text{DLL生成时} & \text{客户端程序编译时} \\ \hline \text{符号流向} & \text{导出符号→DLL的.lib文件} & \text{导入符号←DLL的.lib文件} \\ \hline \text{代码生成} & \text{产生导出地址表(EAT)} & \text{生成间接调用指令} \\ \hline \text{优化影响} & \text{无特别优化} & \text{允许编译器优化间接调用} \\ \hline \end{array} $$ **典型应用示例:** ```cpp // 条件编译实现自动切换 #ifdef MATHLIB_EXPORTS #define MATHLIB_API __declspec(dllexport) // 编译DLL时导出 #else #define MATHLIB_API __declspec(dllimport) // 使用DLL时导入 #endif // 导出的函数声明 extern "C" MATHLIB_API int MatrixMultiply(double* input, int rows, int cols); ``` **深层原理说明:** 1. **导出过程**:当使用`dllexport`时,编译器会在生成的DLL中创建导出段(.edata),将被修饰的符号地址写入导出目录,对应的.lib文件会包含这些符号的桩代码 2. **导入过程**:使用`dllimport`时,编译器会生成间接调用指令: $$ \text{call dword ptr [\_\_imp\_FunctionName]} $$ 而不是直接调用函数地址,这种间接寻址通过导入地址表(IAT)实现动态绑定 3. **性能优化**:`dllimport`修饰的函数调用比显式使用`LoadLibrary`+`GetProcAddress`的方式效率更高,因为编译器可以预先处理调用约定和参数传递优化 **应用场景验证:** 假设开发一个高性能数学计算库: - 当编译`MathLib.dll`时,`MATHLIB_API`展开为导出声明,确保计算函数(如FFT变换、矩阵运算)被正确导出 - 当其他工程(如C#数据分析程序)引用该DLL时,同样的头文件会将`MATHLIB_API`解析为导入声明,使得CLR能正确绑定本地方法 这种设计模式保证了接口的统一性,同时严格区分了库的构建和使用场景,是Windows平台C/C++模块化开发的基石。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值