在日常开发过程中,我们常常用如下这种形式的结构体来传递数据。

1
2
3
4
5
6
typedef struct  _PATH_INFO {
HANDLE hPPid; // 父进程 PID
HANDLE hPid; // 子进程 PID
ULONG PathLength; // 子进程路径长度
TCHAR Path[1]; // 用于存储子进程路径
} PATH_INFO, *PPATH_INFO;

其中前三个成员用来描述一个进程的父进程PID和自身进程的PID以及路径长度信息,而最后一个成员来描述该路径的实际内容,由于路径长度是不定的,我们为了节省内存,加了一个 PathLength 的成员来描述路径长度,不会将实际储存路径的成员设置成固定的 512 大小或者 1024 大小,这样会非常浪费内存,在使用过程中,我们会想如下这种方式来给结构体分配内存和填充数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "stdafx.h"

// 一个用于传递数据的结构体
typedef struct _PATH_INFO {
HANDLE hPPid; // 父进程 PID
HANDLE hPid; // 子进程 PID
ULONG PathLength; // 子进程路径长度
TCHAR Path[1]; // 用于存储子进程路径
} PATH_INFO, *PPATH_INFO;

int _tmain(int argc, _TCHAR* argv[])
{
PPATH_INFO pPathInfo = NULL;
TCHAR szPath[] = _T("C:\\Windows\\notepad.exe");
ULONG ulLength = sizeof(PATH_INFO) + sizeof(szPath);

pPathInfo = (PPATH_INFO)malloc(ulLength);

// 两个 HANDLE + 一个 ULONG + 实际路径占用的空间
cout << "应该分配内存:" << sizeof(HANDLE) * 2 + sizeof(ULONG) + sizeof(szPath) << endl;
cout << "实际分配内存:" << ulLength << endl;

pPathInfo->hPPid = (HANDLE)8273;
pPathInfo->hPid = (HANDLE)188;
pPathInfo->PathLength = sizeof(szPath);
memcpy(pPathInfo->Path, szPath, sizeof(szPath));

cout << "PPID = " << pPathInfo->hPPid << endl;
cout << "PID = " << pPathInfo->hPid << endl;
cout << "Length = " << pPathInfo->PathLength << endl;
wcout << "Path = " << pPathInfo->Path << endl;

free(pPathInfo);
getchar();
return 0;
}

以上代码打印的结果如下:

1
2
3
4
5
6
应该分配内存:58
实际分配内存:62
PPID = 00002051
PID = 000000BC
Length = 46
Path = C:\Windows\notepad.exe

可以看出,我们手动计算了两个 HANDLE + 一个 ULONG + 实际路径占用长度后得出的数值是 58,但实际分配内存的时候却分配了 62 个字节的内存。后面我们拷贝数据到分配的内存中以后,有 4 个字节的空间没有使用而被浪费。那为什么 sizeof(PATH_INFO) 会比我们自己计算的长度多出来 4 个字节呢?这涉及到对结构体内存对齐的知识的了解了,请看本站曾经写过的一篇关于结构体内存对齐的非常详细的文章(图文,非常易懂):http://www.mycode.net.cn/language/cpp/1489.html 在看完上面介绍的结构体内存对齐的文章后,我想你应该已经知道了为什么会比我们预计的多出 4 个字节。接下来就是如何解决这样的问题了,以实现不浪费一丝空间。那么本文的主角 FIELD_OFFSET 宏闪亮登场了。如果我上来就讲这个宏是干什么用的,大家可能也就一看,顶多自己敲敲代码测试一下,很难理解它到底有什么作用。而在我们上面碰到问题的背景下再来简述一下这个宏的作用就非常容易理解且难再忘记它。 FIELD_OFFSET 计算一个结构体成员在结构体内部的字节偏移位置,需要给其传递两个参数,一个是结构体的类型名称,一个是你要计算偏移量的成员名称。怎么理解呢?看如下图描述: 2016-09-25_103740 当我们传递结构体名 PATH_INFO 和结构体成员 Path 给该宏以后,它会帮我们计算出这个成员在结构体中的起始位置,换个角度说就是计算出了这个成员前的所有成员的大小!以这个大小再加上我们最后一个成员的长度即可得出实际我们需要的空间,而不会因为内存对齐的问题导致计算错误。使用这个宏修正我们的代码后效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "stdafx.h"

// 一个用于传递数据的结构体
typedef struct _PATH_INFO {
HANDLE hPPid; // 父进程 PID
HANDLE hPid; // 子进程 PID
ULONG PathLength; // 子进程路径长度
TCHAR Path[1]; // 用于存储子进程路径
} PATH_INFO, *PPATH_INFO;

int _tmain(int argc, _TCHAR* argv[])
{
PPATH_INFO pPathInfo = NULL;
TCHAR szPath[] = _T("C:\\Windows\\notepad.exe");
ULONG ulLength = FIELD_OFFSET(PATH_INFO, Path) + sizeof(szPath);

pPathInfo = (PPATH_INFO)malloc(ulLength);

// 两个 HANDLE + 一个 ULONG + 实际路径占用的空间
cout << "应该分配内存:" << sizeof(HANDLE) * 2 + sizeof(ULONG) + sizeof(szPath) << endl;
cout << "实际分配内存:" << ulLength << endl;

pPathInfo->hPPid = (HANDLE)8273;
pPathInfo->hPid = (HANDLE)188;
pPathInfo->PathLength = sizeof(szPath);
memcpy(pPathInfo->Path, szPath, sizeof(szPath));

cout << "PPID = " << pPathInfo->hPPid << endl;
cout << "PID = " << pPathInfo->hPid << endl;
cout << "Length = " << pPathInfo->PathLength << endl;
wcout << "Path = " << pPathInfo->Path << endl;

free(pPathInfo);
getchar();
return 0;
}

最终打印的结果如下:

1
2
3
4
5
6
应该分配内存:58
实际分配内存:58
PPID = 00002051
PID = 000000BC
Length = 46
Path = C:\Windows\notepad.exe

可以看到和我们预计的长度一模一样,不会有浪费空间的情况出现了。接下来我们再来看 FIELD_OFFSET 宏的实现,你会发现原来这么简单啊。

1
#define FIELD_OFFSET(type, field)    ((LONG)(LONG_PTR)&(((type *)0)->field))

内部实现其实就是将 0 这个地址强制转换为结构体类型,然后使用强转后的结构体类型去访问其 Path 成员,访问到这个成员以后取该成员的地址,由于整个结构体的起始地址是从 0 转换而来的,所以取这个成员的地址时也是从 0 计算的。这样就得出了该成员在整个结构体中的位置。 最后总结下,FIELD_OFFSET 宏是为了计算一个结构体成员的精确偏移位置,我们可以利用此宏介绍很多的空间浪费的情况。再实际编写代码过程中,会使代码业务逻辑严谨不易出错。