下附详解和完整源代码 (附完整 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 | 布尔型 | 无符号 | 1 | 8 | 8 | True 或 False (0 ~ 1) | 逻辑值 |
byte | 字节型 | 无符号 | 1 | 8 | 8 | 0 ~ 255 (0 ~ 28-1 ) | |
char | 单字符型 | 有符号 | 1 | 8 | 8 | -128 ~ 127 (-27 ~ 27 -1) | 针对数字符号时 |
char * | 多字符型 指针变量 | 有符号 | 2 / 4 / 8 | 8 | 16 / 32 / 64 | -215 ~ 215 -1 -231 ~ 231 -1 -263 ~ 263 -1 | 16位系统 32位系统 64位系统 |
short | 短整型 | 有符号 | 2 / 2 / 2 | 8 | 16 / 16 / 16 | -215 ~ +215 -1 | |
int | 整数型 | 有符号 | 2 / 4 / 4 | 8 | 16 / 32 / 32 | -215 ~ 215 -1 -231 ~ +231 -1 | 16位系统 32/64位系统 |
long | 长整型 | 有符号 | 4 / 4 / 8 | 8 | 32 / 32 / 64 | -231 ~ 231 -1 -263 ~ 263 -1 | 16/32位系统 64位系统 |
float | 单精度浮点型 | 有符号 | 4 / 4 / 4 | 8 | 32 | -231 ~ 231 -1 | |
long long | 长长整型 | 有符号 | 8 / 8 / 8 | 8 | 64 | -263 ~ 263 -1 | |
double | 双精度浮点型 | 有符号 | 8 / 8 / 8 | 8 | 64 | -263 ~ 263 -1 |
数据说明符 | 数据类型 | 自定义 | 16 /32/64 位系统占 用字节数 | 字节 | 16 /32/64 位系统 变量位数 | 范围 | 说明 |
---|---|---|---|---|---|---|---|
unsigned char | 单字符型 | 无符号 | 1 | 8 | 8 | 0 ~ 255 (0 ~ 28-1) | 针对数字符号时 |
unsigned short | 短整型 | 无符号 | 2 / 2 / 2 | 8 | 16 | 0 ~ + 65535 (0 ~ 216 -1) | |
unsigned int | 整数型 | 无符号 | 2 / 4 / 4 | 8 | 16 / 32 / 32 | 0 ~ 216 -1 0 ~ 232 -1 | 16位系统 32/64位系统 |
unsigned long | 长整型 | 无符号 | 4 / 4 / 8 | 8 | 32 / 32 / 64 | 0 ~ 232 -1 0 ~ 264 -1 | 16/32位系统 64位系统 |
unsigned float | 单精度 | 无符号 | 4 / 4 / 4 | 8 | 32 | 0 ~ 232 -1 | |
unsigned long long | 长长整型 | 无符号 | 8 / 8 / 8 | 8 | 64 | 0 ~ 264 -1 | |
unsigned double | 双精度 | 无符号 | 8 / 8 / 8 | 8 | 64 | 0 ~ 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 个字节,* 号是补齐空位,
实际数据内存排列如下:
1 | 2 |
---|---|
a | * |
b | b |
b | b |
c | c |
设置结构体 4 字节对齐,4X3 = 12 ,结构体 A 占用 12 个字节,
实际数据内存排列如下:
1 | 2 | 3 | 4 |
---|---|---|---|
a | * | * | * |
b | b | b | b |
c | c | * | * |
设置结构体 8 字节对齐,因为结构体中 b 占用字节最大,按最大的 4 字节排列占用内存,c 只补 2 字节,结构体 A 占用 12 个字节,
实际数据内存排列如下:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|
a | * | * | * | b | b | b | b |
c | c | * | * |
Ⅱ、使用预编译指令,设置对齐为 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 个字节。
实际数据内存排列如下:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|
e | e | e | e | e | e | e | e |
e | e | * | * | * | * | * | * |
f | f | f | f | h | h | * | |
a | a | a | a | * | * | * | * |
b | b | b | b | b | b | b | b |
c | c | c | c | * | * | * | * |
#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 字节。
实际数据内存排列如下:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|
e | e | * | * | f | f | f | f |
g | g | * | * | a | a | * | * |
b | b | b | b | * | * | * | * |
i | i | i | i | i | i | i | i |
计算结构体 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 字节。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|
e | e | * | * | f | f | f | f |
g | g | * | * | a | a | * | * |
b | b | b | b | c | c | * | * |
k | k | * | * | * | * | * | * |
i | i | i | i | i | i | i | i |
Ⅲ、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++语言间如果不进行特殊处理,也无法实现函数的互相调用。