1、结构体中的对齐方式
#include <stdio.h>
struct person {
char name[20];
int age;
char gender;
float height;
};
int main() {
struct person stu; //声明了struct person类型的变量
printf("%ld\n", sizeof(stu)); //结果为32
return 0;
}
之所以 stu 变量占32个字节,和结构体的对齐方式有关。
结构体的对齐方式:默认情况下,首先要确定空间对齐大小,先判断各个字段中占用空间最大的类型占的字节数,以此作为开辟或申请空间的标准。如上例中,struct person 中的类型有char、int、char、float,当前 float 和 int 都占4个字节,所以以4个字节为对齐标准,即申请空间的时候要申请4字节的整数倍。所以:
- name:申请5倍4个字节(即20个字节)
- age:申请4字节
- gender:也需要申请4字节,但是只使用了第一个字节存储数据;
- height:gender开辟的空间中还有三个字节是空的,但是并不能存储height,所以还要再开辟4字节。
因此,一共就是 5 * 4 + 4 + 4 + 4 = 32字节。
存在一个宏,可以强行改变结构体对齐大小 #pragma pack
如下代码,依照之前的规则,以 4 字节为对齐标准,结构体中的大小就是 8 字节:
#include <stdio.h>
struct test {
char c;
int a;
};
int main() {
struct test t;
printf("%lu\n", sizeof(t)); //8
return 0;
}
但是如果加了宏 pragma pack,强行改变对齐大小,如下:
#include <stdio.h>
//强行规定对齐大小为1字节
#pragma pack(1)
struct test {
char c;
int a;
};
int main() {
struct test t;
printf("%lu\n", sizeof(t)); //5
return 0;
}
对齐标准为 1 字节,结构体的每个字段要分配的空间为 1 字节的整数倍,总共就是 1 + 4 = 5字节。
2、访问结构体中的字段
访问结构体中的字段有两种方式:
- 直接引用,即使用”."直接引用运算符,使用变量值访问字段的时候使用“.”。如:
stu.name,stu.age;
- 间接引用,使用“->” 间接应用运算符,通过地址访问字段的时候使用“->”。如:
struct person *p;
p->age;
3、计算结构体中各字段的偏移量
v1版:
#include <stdio.h>
//需求:求某个字段的地址偏移量,偏移了多少个字节
//temp类型是结构体类型,而a是其他类型,所以统一转成char *类型。先定义一个结构体类型的变量temp
#define offset(T, a) ({\
T temp;\
(char *)&temp.a - (char *)&temp;\
})
struct Data {
int a;
double b;
char c;
};
int main() {
printf("%ld\n", offset(struct Data, a)); //传类型
printf("%ld\n", offset(struct Data, b));
printf("%ld\n", offset(struct Data, c));
return 0;
}
v2版:利用空地址NULL做文章
#include <stdio.h>
//需求:求某个字段的地址偏移量,偏移了多少个字节
//先将空地址转为T*类型,获取结构体中的a字段,取a字段的地址,然后将地址转成long类型
#define offset(T, a) (long)(&(((T *)NULL)->a))
struct Data {
int a;
double b;
char c;
};
int main() {
printf("%ld\n", offset(struct Data, a)); //传类型
printf("%ld\n", offset(struct Data, b));
printf("%ld\n", offset(struct Data, c));
return 0;
}
运行结果为:
0
8
16
理解"#define offset(T, a) (long)(&(((T *)NULL)->a))":
- (T *)NULL:是一个指向T类型的指针,指针值为NULL(即0),其作用就是把从地址0开始的存储空间映射为一个T类型的对象;
- ((T *)NULL)->a:访问那类型中的成员a,相应地”&(((T *)NULL)->a)“ 就是返回这个成员的地址。由于对象的起始地址为0,所以成员的地址其实就是相对于对象首地址的成员的偏移地址,然后再通过类型转换为 long 类型。