详解VC++动态链接库中的结构成员规则与调用约定

12 篇文章 0 订阅
3 篇文章 0 订阅

下附详解和完整源代码 (附完整 VC++2017 DLL 动态链接库源代码 和 VB2017 测试程序源代码。)

在国外很多博客里溜达,也没有找到结构体中连续多布尔值处理方法,在动态链接库中的运用。经过多次反复测试 VC++ 代码,终于找到了多布尔值结构体的解决办法。VB6 或 VB2017 引用 VC++DLL 库,传输结构体变量,使用连续的多布尔值。
VB6:

Type LogicParam
     Dim eTBool as Boolean
     Dim CMBool as Boolean
     Dim NBool as Boolean
     Dim EBool as Boolean
End Type
'传递多布尔参数 LP 到 DLL 中初始化变量
Public Declare Sub ParamInitializn Lib "Test.dll" (ByVal X As Integer, ByVal LP As LogicParam)
Dim lp as LogicParam
sub test()
    lp.eTBool = True
    lp.CMBool = False
    lp.NBool = True
    lp.EBool = True
    call ParamInitializn(2022,lp)
end sub

VB.NET:

Structure LogicParam
      Dim eTwoBool As Boolean
      Dim CentBool As Boolean
      Dim NortBool As Boolean
      Dim EastBool As Boolean
End Structure
'传递多布尔参数 LP 到 DLL 中初始化变量
Public Declare Sub ParamInitializn Lib "Test.dll" (ByVal X As Integer, ByVal LP As LogicParam)
'...
'...
'...

一、VC++动态链接库中的结构成员规则

项目属性中,设置结构体成员对齐,如下图:
在这里插入图片描述

  • 1、对于VC++动态链接库编程,如果使用结构体,就必须知道结构体对齐规则。
    为了提高内存访问速度和效率,需要各种类型数据变量按照一定的规则在内存上排列,
    Windows 32 位系统中,默认为 4 字节对齐,Windows 64 位系统中,默认为 8 字节对齐。
    Linux 32 位,默认 4 字节对齐,Linux 64 位,默认 8 字节对齐。
    VC6.0 结构成员对齐默认为 4 字节(/Zp4),VC2017结构成员对齐默认为 8 字节(/Zp8)。

  • 2、数据对齐规则
    不论是结构体(struct)或者联合(union)在内存分布时都会有字节对齐,首先数据类型分浮点型,整型,字符类型。
    自身对齐值:数据类型自己的对齐值,例如char类型的自身对齐值是1,short类型是2;
    指定对齐值:编译器或程序员指定的对齐值,32位的指定对齐值默认是4;
    有效对齐值:自身对齐值和指定对齐值中较小的那个。
    对齐有两个规则:
    Ⅰ、不但结构体的成员有有效对齐值,结构体自己也有对齐值,这主要是考虑结构体的数组,对于结构体或者类,要将其补齐为其有效对齐值的整数倍。结构体的有效对齐值是其最大数据成员的自身对齐值;
    Ⅱ、存放成员的起始地址必须是该成员有效对齐值的整数倍。

  • 3、各变量的占用字节
    32位系统或程序,即计算机数据总线宽度为 32 个,一次可以处理 32 位bit(即 4 个字节,1 字节为 8 bit),
    64位系统或程序,即计算机数据总线宽度为 64 个,一次可以处理 64 位bit(即 8 个字节,1 字节为 8 bit),

对于 16、32、64 位的系统、程序、编译:

数据
说明符
数据类型默认16 /32/64
位系统占
用字节数
字节16 /32/64
位系统
变量位数
范围说明
bool布尔型无符号188True 或 False
(0 ~ 1)
逻辑值
byte字节型无符号1880 ~ 255
(0 ~ 28-1 )
char单字符型有符号188-128 ~ 127
(-27 ~ 27 -1)
针对数字符号时
char *多字符型
指针变量
有符号2 / 4 / 8816 / 32 / 64-215 ~ 215 -1
-231 ~ 231 -1
-263 ~ 263 -1
16位系统
32位系统
64位系统
short短整型有符号2 / 2 / 2816 / 16 / 16-215 ~ +215 -1
int整数型有符号2 / 4 / 4816 / 32 / 32-215 ~ 215 -1
-231 ~ +231 -1
16位系统
32/64位系统
long长整型有符号4 / 4 / 8832 / 32 / 64-231 ~ 231 -1
-263 ~ 263 -1
16/32位系统
64位系统
float单精度浮点型有符号4 / 4 / 4832-231 ~ 231 -1
long long长长整型有符号8 / 8 / 8864-263 ~ 263 -1
double双精度浮点型有符号8 / 8 / 8864-263 ~ 263 -1
数据说明符数据类型自定义16 /32/64
位系统占
用字节数
字节16 /32/64
位系统
变量位数
范围说明
unsigned char单字符型无符号1880 ~ 255
(0 ~ 28-1)
针对数字符号时
unsigned short短整型无符号2 / 2 / 28160 ~ + 65535
(0 ~ 216 -1)
unsigned int整数型无符号2 / 4 / 4816 / 32 / 320 ~ 216 -1
0 ~ 232 -1
16位系统
32/64位系统
unsigned long长整型无符号4 / 4 / 8832 / 32 / 640 ~ 232 -1
0 ~ 264 -1
16/32位系统
64位系统
unsigned float单精度无符号4 / 4 / 48320 ~ 232 -1
unsigned long long长长整型无符号8 / 8 / 88640 ~ 264 -1
unsigned double双精度无符号8 / 8 / 88640 ~ 264 -1
数据定义数据类型字节说明
unsigned无符号
signed有符号所有数字变量默认为有符号
char *多字符8表示连续字符串 (char * Str = “这是连续字符串”; )

C语言函数 sizeof 的作用是求对象在计算机内存中所占用的字节数。形式为:sizeof(object),object可以是变量、表达式或者数据类型名。

如果程序中有#pragma pack(n) 预编译指令,则所有成员对齐以n字节为准(即偏移量为n的整数倍),不在考虑当前类型以及最大结构体类型。

  • 4、数据对齐实例

Ⅰ、计算结构体 A占用字节:

struct A
{
	char a='0';//占用 1 个字节
	int b=0;   //占用 4 个字节
	short c=0; //占用 2 个字节
};
main() {
	printf("A = %d",sizeof(A));//12
}

结构体 4 字节对齐,显示 A = 12。结构体 8 字节对齐,显示 A = 12。
本来结构体A,总共应该是7个字节,使用函数 sizeof(A) 算出来却是占用12个字节,因为根据结构体规则,按最长的字节对齐,3 个变量 X 4 个字节对于 12 字节,按最长的 4 字节对齐,a 后面添加了3个空字节,c 后面添加了 2 个空字节。

设置结构体 1 字节对齐,1+4+2 = 7 ,结构体 A 占用 7 个字节。

设置结构体 2 字节对齐,2X4 = 8 ,结构体 A 占用 8 个字节,* 号是补齐空位,
实际数据内存排列如下:

12
a*
bb
bb
cc

设置结构体 4 字节对齐,4X3 = 12 ,结构体 A 占用 12 个字节,
实际数据内存排列如下:

1234
a***
bbbb
cc**

设置结构体 8 字节对齐,因为结构体中 b 占用字节最大,按最大的 4 字节排列占用内存,c 只补 2 字节,结构体 A 占用 12 个字节,
实际数据内存排列如下:

12345678
a***bbbb
cc**

Ⅱ、使用预编译指令,设置对齐为 8 字节:
预编译指令#pragma pack(n)手动设置 n 取 1 2 4 8 16

#pragma pack(8)
struct A
{
	int a;    //占用 4 个字节
	double b; //占用 8 个字节
	float c;  //占用 4 个字节
};
 
struct
{
	char e[10]; //占用 1 X 10 个字节
	int f;      //占用 4 个字节
	short h;    //占用 2 个字节
	struct A i; //占用 4 + 8 + 4 = 16个字节
}B;
#pragma pack() //恢复默认对齐

结构体 B 按计算值占用 32 个字节,实际占用 48 个字节。
实际数据内存排列如下:

12345678
eeeeeeee
ee******
ffffhh*
aaaa****
bbbbbbbb
cccc****
#pragma pack(8)
struct A
{
	short a;//占用 2 个字节
	int b;  //占用 4 个字节
};
 
struct
{
	char e[2];//占用 2 个字节
	int f;    //占用 4 个字节
	short g;  //占用 2 个字节
	struct A j; //占用 6 个字节
	double i;  //占用 8 个字节
}B;
 
main() {
	printf("B%d",sizeof( B));//32
}

计算结构体 B 为 22字节,实际对齐占用 32 字节。
实际数据内存排列如下:

12345678
ee**ffff
gg**aa**
bbbb****
iiiiiiii

计算结构体 B 的大小:

#pragma pack(8)
struct A
{
	short a;//占用 2 个字节
	int b;  //占用 4 个字节
	short c;//占用 2 个字节
};
 
struct
{
	char e[2];//占用 2 个字节
	int f;    //占用 4 个字节
	short g;  //占用 2 个字节  
	struct A j; //占用 8 个字节
	short k;    //占用 2 个字节
	double i;   //占用 8 个字节
 
}B;
 
main() {
 
	printf("B%d",sizeof( B));//40
}

计算结构体 B 占用 26 个字节,实际对齐占用 40 字节。

12345678
ee**ffff
gg**aa**
bbbbcc**
kk******
iiiiiiii

Ⅲ、Dll库函数中,使用结构体变量,优化设置结构体对齐,才能更好的正确获得参数值,少占用内存,获得最快运行速度(重点)

  • A、C++ 结构体设计,应该按规则排列,使其占用内存少,避免变量溢出;
  • B、C++ 结构体设计,尽量使用同一类变量在结构体中,使其填充空位少,占用内存少;
  • C、C++ 结构体设计中,bool 逻辑变量的使用很特殊,其只占用 1 字节,连续几个 bool 变量跟在其它变量中,容易导致 bool 变量溢出,应该使 bool 变量不连续,参杂在其它变量中(这样占用内存大),或单独使用逻辑结构体,并使其结构体对齐为 1 字节,这样占用内存少。

实例使用VB6编译的程序,调用VC2017编译的DLL动态链接库:

VB6里设置结构体和调用函数:

Type A
   Dim i as Integer
   Dim s as String
   Dim d as Double
   Dim b as Boolean
   Dim bo as Boolean
End Type

Public Declare Sub ParamInitializn Lib "Test.dll" (ByVal X As Integer, ByVal Test As A)'参数初始化

Private Sub Command1_Click()
   Dim BType as A
   BType.i = 9
   BType.s = "测试"
   BType.d = 168.898989
   BType.b = True
   BType.bo = False
   call ParamInitializn(2022,BType)
End Sub
  • VC2017编辑 DLL动态链接库,为了VB6能调用DLL库文件,调用约定 __stdcall (/Gz),编译为 32 位动态链接库

Test.h //测试头文件

struct A//结构体
{
	int i;
	char* s;
	double d;
	bool b;
    bool bo;
};
void ParamInitializn(int X, struct A PP);

Test.cpp //测试源文件

void ParamInitializn(int X, struct A PP)
{
  char buf[500];//定义字符串数组
  const char *showFormat = " X = %d\n i = %d\n s = %s\n d = %.6f\n b = %d\n bo = %d";//定义显示格式
  sprintf_s(buf, showFormat , X, PP.i , PP.s, PP.d, PP.b, PP.bo);//将变量按格式赋予 buf
  MessageBoxA(0, buf, "测试DLL调用参数", 0);//对话框显示
}

Dll库模块定义文件 Test.def 显式导出 ParamInitializn

LIBRARY
EXPORTS
    ; 此处可以是显式导出
	ParamInitializn @1

提出问题:
1、VC2017 编辑 DLL动态链接库,默认结构成员对齐为 8 ,编译的库文件,VB6 能正常调用么? 是否能弹出对话框!
2、VB6 为 32 位程序,没有定义结构成员规则,默认结构成员对齐为 4 ,如何使 VC2017 编辑的 32 位 DLL动态链接库能被VB6正确调用?

解决问题:
1、可以在 VC2017 项目里面把结构成员对齐设定为 4
2、VC2017 项目里,结构成员对齐依然是默认 8 ,在头文件里,在结构体 A 前后,单独定义结构体为 4 ,并结束单独定义。

#pragma pack(4)//定义结构成员对齐为 4 字节
struct A//结构体
{
	int i;
	char* s;
	double d;
	bool b;
    bool bo;
};
#pragma pack()//恢复默认定义结构成员对齐

一个疑惑的问题,无法处理的多布尔 bool 结构体 DLL 库,一直没有找到正确方法:
当设定一个多布尔 bool 结构体在上面的DLL里,VB 为多布尔 Boolean 结构体赋值,DLL无法正确获取多布尔 bool 结构体的值,
如果将上面 DLL 动态链接库修改为多布尔bool结构体,如下:

//Test.h //测试头文件
struct LogicParam
{
	bool eBool;
	bool CMBool;
	bool NBool;
	bool EBool;
};

void ParamInitializn(int X, struct LogicParam PP);
 
//Test.cpp //测试源文件
void ParamInitializn(int X, struct LogicParam PP)
{
  char buf[500];//定义字符串数组
  const char *showFormat = " X = %d\n eBool = %d\n CBool = %d\n NBool = %d\n EBool = %d\";//定义显示格式
  sprintf_s(buf, showFormat , X, PP.eBool , PP.CMBool, PP.NBool, PP.EBool);//将变量按格式赋予 buf
  MessageBoxA(0, buf, "测试DLL调用参数", 0);//对话框显示
}

//Test.def 同上

VB6里设置结构体和调用函数:

Type LogicParam
   Dim eBool as Boolean
   Dim CMBool as Boolean
   Dim NBool as Boolean
   Dim EBool as Boolean
End Type

Public Declare Sub ParamInitializn Lib "Test.dll" (ByVal X As Integer, ByVal LP As LogicParam)'参数初始化

Private Sub Command1_Click()
   Dim BType as LogicParam
   BType.eBool = True
   BType.CMBool = False
   BType.NBool = True
   BType.EBool = False
   call ParamInitializn(2022,BType)
End Sub

DLL 动态链接库无法正确获得 LogicParam 结构体值,无论怎么设置结构成员对齐值,还是使用 *LogicParam 指针参数 。

当使用如下结构体,使逻辑变量参杂在其它变量中,使结构体按其它变量字节对齐,DLL 动态链接库可以正确获取 LogicParam 结构体值。

struct LogicParam
{
	int A;
	bool eBool;
	int B;
	bool CMBool;
	int C;
	bool NBool;
	int D;
	bool EBool;
};

我也知道可以采用纯粹的布尔数组来处理这个问题,但谁能解答如何处理这种纯粹的多布尔 bool 结构体?

(今天更新:2022-08-15)
在国外很多博客里溜达,也没有找到我所需要的答案。
经过多次反复测试 VC++ 代码,终于找到了多布尔结构体的解决办法,那就是 VC++ 中使用大写的 BOOL 声明布尔变量,小写 bool 只占 1 字节,使用结构体时,4 个 bool 永远只 4 字节排列内存, BOOL 是 int 整型,与传输进来的变量内存一致,就不再出错。操作只要改变 VC++ 结构体声明,VB6代码无需改变,具体如下:

//Test.h //测试头文件
struct LogicParam
{
	BOOL eBool;
	BOOL CMBool;
	BOOL NBool;
	BOOL EBool;
};

//Test.cpp //测试源文件
void ParamInitializn(int X, struct LogicParam PP)
{
bool eTwoBool = LP.e2ParamBool;
bool CentBool = LP.CentralBool;
bool EastBool = LP.EasterlyBool;
bool NortBool = LP.NorthwardBool;
...
...
...
}

代码完美解决纯布尔结构体变量的引用。

完整源代码示例下载:
https://download.csdn.net/download/zyyujq/86400945

二、VC++调用约定

项目属性中,设置调用约定,如下图(VC2017,VC6类同):
在这里插入图片描述

  • 1、调用协议常用场合
    Ⅰ、__stdcall:Windows API默认的函数调用协议。(标准调用)
    Ⅱ、__cdecl:C/C++ 默认的函数调用协议。(C/C++语言调用)
    Ⅲ、__fastcall:适用于对性能要求较高的场合。(快速调用)
    注:
    为了VB、C#等其它语言编译的程序可以直接调用Dll库,编译Dll库时,应设置为标准调用协议。

  • 2、函数参数入栈方式
    Ⅰ、__stdcall:函数参数由右向左入栈。
    Ⅱ、__cdecl:函数参数由右向左入栈。
    Ⅲ、__fastcall:从左开始不大于4字节的参数放入CPU的ECX和EDX寄存器,其余参数从右向左入栈。
    注意:
    __fastcall在寄存器中放入不大于4字节的参数,故性能较高,适用于需要高性能的场合。

  • 3、栈内数据清除方式
    Ⅰ、__stdcall:函数调用结束后由被调用函数清除栈内数据。
    Ⅱ、__cdecl:函数调用结束后由函数调用者清除栈内数据。
    Ⅲ、__fastcall:函数调用结束后由被调用函数清除栈内数据。
    注意:
    A、不同编译器设定的栈结构不尽相同,跨开发平台时由函数调用者清除栈内数据不可行。
    B、某些函数的参数是可变的,如printf函数,这样的函数只能由函数调用者清除栈内数据。
    C、由调用者清除栈内数据时,每次调用都包含清除栈内数据的代码,故可执行文件较大。

  • 4、C语言编译器函数名称修饰规则
    Ⅰ、__stdcall:编译后,函数名被修饰为“_functionname@number”。
    Ⅱ、__cdecl:编译后,函数名被修饰为“_functionname”。
    Ⅲ、__fastcall:编译后,函数名给修饰为“@functionname@nmuber”。
    注:
    A、“functionname”为函数名,“number”为参数字节数。
    B、函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。

  • 5、C++语言编译器函数名称修饰规则
    Ⅰ、__stdcall:编译后,函数名被修饰为“?functionname@@YG******@Z”。
    Ⅱ、__cdecl:编译后,函数名被修饰为“?functionname@@YA******@Z”。
    Ⅲ、__fastcall:编译后,函数名被修饰为“?functionname@@YI******@Z”。
    注:
    A、“******”为函数返回值类型和参数类型表。
    B、函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。
    C、C语言和C++语言间如果不进行特殊处理,也无法实现函数的互相调用。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

中游鱼

获取完整源代码,提高工作效率

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值