结构体是 c
语言用来描述基本数据类型的一种组合类型
,
是
c
语言中非常常用的一种数据类型,因为世间万物很多都不仅仅体现一种数据类型,而有很多种数据类型组合而来,比如我们描述一个人,他有名字(char *),
有年龄
(int),
有身高体重
(float)
,很多描述物体的数据都是要有很多数据组合而来的,所以便衍生了组合数据类型。至于如何定义怎么使用作者在这里就不细细说明,同学们自行学习。
结构体内存对其问题
原因
首先是因为平台原因或移植原因:
“
不是所有的硬件平台都能访问任意地址上的
任意数据
”
;某些硬件平台只能在某些特定地址处取某些特定的数据,否则就会抛出硬件异常”
。也就是说在计算机在内存读取数据时,只能在规定的地址处读数据,而不是内存中任意地址都是可以读取的
第二个是效率原因
:
正是由于只能在特定的地址处读取数据,所以在访问一些数据时,对于访问未对齐的内存,处理器需要进行两次访问;而对于对齐的内存,只需要访问一次就可以。 其实这是一种以空间换时间
的做法,但这种做法是值得的。
正是因为存在内存对其,所以一般在公司中的结构体代码一般都是严格字节对其的,对于没法对其的,程序员常用定义相应大小的留空数据顶替。
对其的规则
规则 1.
首先整个结构体的大小必须是最大字节基础成员的整数倍,例如:
struct A
{
int a;
double b;
char c;
};
那么上面结构体的字节大小一定是
double
字节的整数倍,也就是
8
的整数倍。
规则 2
.
结构体内每一个成员的地址都相对于结构体本身的地址是自身字节的整数倍,什么意思呢,就是成员的地址减去结构体的地址的值要是自己字节大小的整数倍。有人说,那第一个成员呢,第一个成员的地址就是结构体的地址,相对是 0,0
是任何数的倍数。所以上面结构体每个成员的地址就是这样排列的,假如结构体地址为 0x0
,那么 a
的起始地址为
0
,
a
的字节大小是
4
,向后偏移
4
,但是
b
是
8
字节的,所以还要向后偏移 4
到
0x8
这里才能是
b
的起始地址,
c
的大小是
1
,那么直接
0x10
就是
c 的地址,那么结构体的末地址就是 0x11
了吗,不是,依据规则
1
,那么结构体的末位置应该是 0x18
,这样算出来
A
结构体的大小应该是
24
规则 3
.
如果结构体内有成员还是结构体
,
那么成员结构体的起始地址是成员结构体内最大字节成员变量的整数倍,且父结构体的大小为所有子成员中最大字节基本成员的整数倍。例如:
struct A
{
int a;
double b;
char c;
};
struct B
{
int a;
struct A b;
char c;
};
int main()
{
struct B m;
printf("Asize = %ld\n",sizeof(struct A));
printf("Bsize = %ld\n",sizeof(struct B));
printf("B.a bar = 0x%p\n",&m.a);
printf("A.a bar = 0x%p\n",&m.b.a);
printf("A.b bar = 0x%p\n",&m.b.b);
printf("A.c bar = 0x%p\n",&m.b.c);
printf("B.c bar = 0x%p\n",&m.c);
return 0;
}
如上代码,
B
中嵌套
A,
那么我们运行看看结果
可以看到,A
的大小确实是
24
,
B
的大小是
40,
因为
B
的大小要为最大字节基础成员的倍数,也就是子成员结构体 b
中的
b
,也就是
double
。然后子结构体的起始地址 A.a
确实相当于父结构体的地址是最大字节成员的整数倍
8.
自定义对其方式
第一种是
gcc
支持但是不推荐的自定义对其指令
#pragma pack(n) n=1/2/4/8/2^n
使用方法就是在程序开头指定对其方式:
#pragma pack(2)
例如指定以
2
字节对其
,
虽然可以,但是不推荐这样去使用哦。
第二种是 gcc
推荐的对齐指令
__attribute__((packed))
,
__attribute__((aligned(n)))
。
__attribute__((packed))
是取消内存对其,
__attribute__((aligned(n)))
是指定按
n
字节对其
使用方法是直接放在需要自己指定对其方式的结构体后面,例如:
struct C
{
char a;
char b;
char c;
char e;
char d;
int w;
}__attribute__((packed));
这样就取消了结构体
C
的字节对其,求得
C
的
size
是
9.
struct C
{
char a;
char b;
char c;
char e;
char d;
int w;
}__attribute__((aligned(8)));
这样就指定结构体
C
按
8
字节对其,求得
C
得
size
为
16.
结构体位域问题
有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0
和
1
两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。
位段成员必须声明为 int
、
unsigned int
或
signed int
类型(
short char
long
)。
例如:
struct C
{
int a:8;
int b:2;
char c:1;
int d:5;
}
这样 a,b,c,d
就是
4
个位域变量,对位域变量是没法取地址的,位域值的首地址不需要考虑什么对其问题,
因为他们仅仅是代表这个结构体这几个位的信息,他们的值也就是这几个位的值,这样做的目的可以充分的利用系统内存。
需要注意的点是,使用不同类型的位域对位域值本身来说没有任何影响,但是对于结构体的大小却会带来影响,例如使用 int
型的位域,那么结构体的大小至少也会按 4
字节的倍数去扩展
,
所以在使用位域不大的情况下尽量选择
char
型。
无名位域和空域
什么是无名位域,例如:
struct C
{
char a:8;
char b:2;
char :3;
char d:3;
};
其中我们看到 char :3
就是无名位域,无名位域不能使用,唯一的作用就是用来填充或调整位置
空域是指值为 0
的无名位域,例如:
struct C
{
char a:8;
int b:2;
int :0;
char d:3;
};
它的作用是将不满空域倍数对应字节大小的位填充,下一个成员将从新的位域单元开始存储,例如上面的结构体,a
占
8
位,
b
占
2
位,后面遇到
int
型空域,那么就会补全剩下的不满足 32
位的字节,也就是将剩下的
22
位补全,
d
从下一个整形单元开始存储.
没理解再看一个例子:
struct C
{
char a:3;
char :0;
char c:4;
char d:4;
};
a
占
3
个位,遇到空域,那么
c
从下一个
char
单元开始,空域就要补充
5
个位。
结构体中的零长数组
零长数组又名柔性数组和可变数组,定义时,数组元素为 0
的数组称为零长数组,零长数组其实就可以相当于一个指针,单独的零长数组是没有什么用处的,它一般很活跃在结构体中,例如:
struct D
{
char name[16]; //名字
int num; //arg 的个数
struct C arg[0];
};
当一个结构体里面存在暂时不确定个数的成员变量时,可以用零长数组代替,注
意一定要写在结构体的最后一个成员位置,零长数组不占内存,所以用
sizeof
求上面
的结构体大小是
20
,使用零长数组是可以延展的,
所以程序中如果确定了零长数组的个数就可以使用 malloc
分配 结构体字节大小
+(
零长数组成员个数
*
零长数组成员大小)
来为达到程序目的,例如,上面的结构体如果表示一个人在一局游戏中的人头统计的话,name
表示玩家名称,
num
表示击杀玩家个数,
arg
表示被击杀的玩家信息。因为每局游戏都会遇到不同的玩家,击杀的数目也不一样,所以对于这种编译前不确定的东西我们用零长数组来解决,当结果出来的时候我们就可以知道 num
的值,任何就可以分配出合理的内存来存放我们需要的信息,这就是零长数组的妙用。