c语言struct_Introduction to CSAPP(十七):复杂数据组织与C语言的 struct与union

如果说数组是同一种类型的数据的连续排列的数据组织形式,那么对于不同类型的数据来说,他们被有机组织起来的方式有两种,分别是structunion

Struct

C语言的 struct 创建一种数据类型,将可能不同的数据类型的对象聚合到一个对象中。结构体中各个组成部分由名字来引用。类似于数组的实现,结构体的所有组成部分都存放在存储器中的一段连续的区域内,指向结构体的指针就是结构体第一个字节的地址。编译器维护每个结构类型的信息,指示每个字段(field)的字节偏移。以这些偏移作为存储器引用中指令的位移,从而产生对结构体元素的引用。

一个例子:

 struct rec{
     int i;
     int j;
     int a[2];
     int *p;
 };

那么在内存中的排列是:

430d01f59a76dc7e6c4cc8029205b641.png

对于struct中的成员,我们可以使用结构地址 + 偏移量的方式来进行访问。例如指向结构体 rec 的指针变量 r 存放在寄存器 %rdi 中,下面的代码将元素 r->i 复制到 r->j

movl    (%rdi), %eax        # 将 r->i 放到寄存器中
movl    %eax, 4(%rdi)       # 将寄存器的值复制到 r->j 中

可以看到,为了访问字段 j,代码将 r 的地址加上偏移量 4。

Union

联合体提供了一种方式,能够规避C语言的类型系统,允许以多种类型来引用同一个对象。联合体的语法和结构体一样,但语义差别很大——联合体用不同的字段来引用相同的内存

考虑下面的声明:

 struct S{
     char c;
     int i[2];
     double v;
 }
 ​
 union U{
     char c;
     int i[2];
     double v;
 }

在一台x86-64 Linux 机器上编译时,字段的偏移量、数据类型的完整大小如下:

ced482d6513e647fa1bf2e733cb35293.png

可以发现:

  • 对于类型结构体 U 的指针 p,p->cp->i[0]p->v 都是引用数据结构的起始位置
  • 一个联合体的总的大小总是等于它的最大字段的大小。

union的作用在于节省内存空间。举一个例子:

现在我们要设计一个特殊的二叉树的数据结构,它有一个特点:每个叶子节点有两个 double 类型的值,但是指针不指向任何地方。每个内部节点有左右子节点的指针,但是没有数据。如果声明如下:

 // 每个节点都需要 32 个字节
 struct node_s {
     struct node_s * left;
     struct node_s * right;
     double data[2];
 };

如果用上述结构体,那么每个节点都需要 32 个字节。每个类型的节点都要浪费一半的内存。如果我们这样声明:

 union node_u {
     struct {
         union node_u *left;
         union node_u *right;
     } internal;
     double data[2];
 };

那么每个节点所需要的内存大小就仅为internaldouble[2]二者所占最大的大小了,也就是16字节:

  • 当它是中间节点:我们使用n->internal.leftn->internal.right 来访问子节点
  • 当它是叶节点时:我们使用n->data[0]n->data[1]来访问

然而这样表示,我们是没办法确定一个给定的节点到底是叶子节点,还是内部节点。有一个方法,是引入一个枚举类型,定义这个联合中可能的不同选择,再用一个结构体将两者包含起来:

typedef enum {LEAF, INTERNAL} nodetype;

struct node_t{
    nodetype type;
    union {
        struct
        {
            union node_t *left;
            union node_t *right;
        } internal;
        double data[2];
    } info;
};

这个结构共需24个字节:

  • type是4个字节(int值)
  • union info是16个字节
  • 他们二者之间需要做填充,因此是 16 + 8 = 24个字节

但是从这个例子来说,使用union是得不偿失的,因为它并没有节省多少空间,但是它在编码带来了不少麻烦。

对于那些有较多字段的数据结构,这种空间上的节省可能才是足够吸引人的。

数据对齐

对齐主要是为了提高内存的访问效率。

计算机系统对于基本的数据类型的合法地址做出了一定的限制,要求地址必须是某个值的倍数(例如4、8、16)。这种对齐限制简化了处理器系统和存储器系统之间的接口硬件设计。

CPU在读取数据时,会从偶地址开始读取一定位的内存数据,如果一个数据存放的地址不是从偶地址开始的,那么为了读取这个完整的数据,CPU需要读取两次。

计算机通过将部分内存“闲置”的方法,来提高内存的访问效率,这种做法就叫做数据对齐。

计算机字节对齐的规则是:

8005be0f6f6d3be268c7d1b6f5b0babf.png

一个例子是:

 struct S{
     char c;
     int i[2];
     double v;
 } *p;

de5ea05738f66962528083b4ee2d850e.png

它的总大小是24字节。显然不等于 sizeOf(c) + sizeOf(i) + sizeOf(v) = 1 + 4*2 + 8 = 17,如果是这样的话,它的内存排布则是十分紧凑的:

4bc1a40a751a57c05aedd39587000fb9.png

实际上,编译器使用了对齐的手段。它在x86-64位上内存中的排布方式是:

e0154b3e64108c141319329b1a42a81f.png
int值必须在以4的倍数的地址处被存储 double值必须在以8的倍数的地址处被存储

那么对于

struct S{
    double v;
	  int i[2];
    char c;  
} *p;

则为,它在结尾会以8字节,即以struct内的double的对齐方式进行对齐:

8be7f57c5ce976fbd389265e44fda0e8.png

原因在于,它基于一个假设,即,如果此结构体构建一个数组,且我们可以保证地址是8的倍数的话,这个结构体数组的每个元素都是合理对齐的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值