最近看别人代码突然看见一个操作让我感到很迷惑。代码如下:
代码语言:c复制#define offsetof(type, member) (size_t)&(((type*)0)->number)
如果稍微了解一下其他语言,比方说 Java 就会直到,将 0 强转为一个结构体类型指针,相当于一个空指针,空指针引用,这在 Java 中可是开发的心头大忌。
我开始也是怀疑这个是不是能够运行起来,就写了一段测试程序。
代码语言:c复制#include <stdio.h>
typedef struct test{
char ch;
int number;
}*Test;
int main(){
int a = (int)&(((Test)0)->number);
printf("%dn", a);
return 0;
}
上述代码只要熟悉 c 语言的基本都能看得懂,对代码进行编译时候编译也通过了。
编译器只是对编译过程做出了警告,并没有报 error ,所以语法层面是可以编译成功的。
那么既然可以运行那么有人直到结果是什么吗?
在得出结果前,我们先看一下这段代码是干什么的,首先(Test)0 是将 0 强制转化为一个指向 test 结构体的指针。
然后 ((Test)0)->number 指向了结构体的 number,其实就是相当于指向了 number 的首地址。因为空指针的初始地址是 0 那么这个指针的地址就是这个 number ,在结构体中的地址偏移。
既然直到这段是在求 number 在结构体中的地址偏移,那么他的代码输出结果是什么?
这就要提到结构体的占用内存的方式。
我们直到 char 占用 1 个字节,int 占用 4 个字节,那么这个结构体是不是占用 5 个字节?
内存对齐
算法的性能可以用空间复杂度和时间复杂度来评估,而 C 语言结构体很多设计也是空间复杂度和时间复杂度之间的取舍,结构体在使用过程中并不是一个字段地址挨着一个字段地址访问,而是为了访问效率进行内存对齐的操作
一般内存对齐都是 4 字节对齐,所以上述结构体大概的一个占用内存结构如下:
明白了上述内存对齐,那么number 的地址在结构体中的内存偏移我们就知道了,所以输出是 4。
但是如果我就是叛逆,我管你什么性能的,我看到空间浪费我就难受,对于穷孩子出身的我就算这饭吃了拉肚子我还是不扔怎么办?
设置内存对齐方式
内存对齐是编译器的默认的一种方式,如果想要禁止内存对齐或者自己设置内存对齐方式那么可以这样。
既然是编译器的一种设置方式,那么针对不同平台的语法也不一样,在 Linux 平台下使用 attribute((packed)) 和 attribute((aligned(4))) 来进行内存对齐,在结构体语言中就是
代码语言:c复制typedef struct __attribute__((packed)) test {
char ch;
int number;
} *Test;
修改后输出就是 1 了
当然也可以自己设置对齐方式,比方说如果设置为 8
代码语言:c复制typedef struct __attribute__((aligned(8))) test {
char ch;
int number;
} *Test;
这次输出什么呢? 很多人会认为是 8,但是实际大多可能是 4,这跟编译器有很大关系,编译器优化,默认对齐设置等等有关,所以自己设置对齐方式一般小于 4
Windows 平台也有优化选项
代码语言:c复制#pragma pack(push, 1)
typedef struct test {
char ch;
int number;
} *Test;
#pragma pack(pop)
如果不加 push 后边也不同 pop ,不过这样选择就是对后边所有结构体都进行了内存对齐设置了。
另外如果只针对某一个成员变量设置对齐方式,那么可以这样写。
代码语言:c复制typedef struct test {
char ch;
int number __declspec(align(1)); // 设置 number 成员的对齐为 1 字节
} *Test;
可以看到Windows下编译器对结构体控制粒度更细。