struct

在 c 语言中,结构体(struct)是一种复合数据类型,用来将一系列相同或不同类型的变量聚集在同一个内存区间内并赋予同一个名字,使得通过一个指针就能访问集合中的所有成员。结构体中可以包含许多简单或符合数据类型,因此从内存分配上看,有点类似数组,而从变量组织上看,又类似于面向对象编程语言中的类。

定义 struct

定义一个 struct 非常简单,使用struct关键字即可。

struct student {
    char *name;
    int age;
};

struct 的初始化

通常有四种常用的 struct 初始化方式。

1.定义并初始化

struct {
    char *ext;
    char *filetype;
} extensions[] = {
    {"gif", "image/gif"},
    {"jpg", "image/jpg"}
};

2.按照成员声明的顺序初始化

struct Person {
    char *name;
    int age;
};

struct Person zhangsan = {"张三", 24};

本质上,这种方式跟第一种是一样的。

3.指定初始化,成员顺序可以不定,Linux Kernel 中多采用这种方式。

struct Student {
    char c;
    int score;
    const char *name;
};

struct Student zhangsan = {
    .name = "张三",
    .c = 'B',
    .score = 92,
};

4.指定初始化,成员顺序可以不定。

struct Student {
    char c;
    int score;
    const char *name;
};

struct Student zhangsan = {
    c: 'C',
    score: 93,
    name: "张三",
};

structure member alignment, padding and data packing.

首先我们来看看以下程序的结果

#include <stdio.h>

typedef struct structa_tag {
    char c;
    short int s;
} structa_t;

typedef struct structb_tag {
    short int s;
    char c;
    int i;
} structb_t;

typedef struct structc_tag {
    char c;
    double d;
    int s;
} structc_t;

typedef struct structd_tag {
    double d;
    char c;
    int s;
} structd_t;


int main(int argc, char *argv[])
{
    printf("sizeof(structa_t) = %d\n", sizeof(structa_t));
    printf("sizeof(structb_t) = %d\n", sizeof(structb_t));
    printf("sizeof(structc_t) = %d\n", sizeof(structc_t));
    printf("sizeof(structd_t) = %d\n", sizeof(structd_t));
    return 0;
}

在执行程序之前,不妨分析下运行的结果。

data alignment

许多实际意义的计算机对基本类型数据在内存中存放的位置有限制,他们会要求这些地址的起始地址的值是某个数 k 的倍数,这就是所谓的内存对齐,而这个 k 则被称为改数据类型的对齐模数(alignment modulus)。这种强制的要求一来是简化了处理器与内存之间传输系统的涉及,二来可以提升读取数据的速度。

比如有这么一种处理器,它每次读写内存的时候都从某个 8 倍数地址开始,那么读或写一个 double 类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的 8 字节内存块上。

对齐的原则

  • 数据类型自身对齐: char 类型自身对齐为 1 字节,short 类型为 2 字节,int/float 为 4 字节,double 为 8 字节
  • 结构体或类的自身对齐:其成员中自身对齐值最大的那个值
  • 指定对齐:#pragma pack(value)指定对齐的 value
  • 数据成员,结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的 pack 值}

基于以上说明,我们来分析下上例程序的运行结果:

对于structa_tag, char占1个字节,short int占 2 个字节,假如short int紧跟着char之后来分配内存,那么它将会以一个奇数边界开始。所以,计算机会在char之后填充 1 字节来保证short int起始地址从 2 的倍数开始。所以stracta_t的总大小为:sizeof(char) + 1(padding) + sizeof(short),也就是 1 + 1 + 2 = 4 bytes.

对于structb_tagsizeof(structb_t) = sizeof(short) + sizeof(char) + 1 (padding) + sizeof(int),即 2 + 1 + 1 + 4 = 8 bytes.

对于structc_tag,由于double为 8 字节对齐,所以在char后需要填充 7 字节,通常我们以为这样就够了,sizeof(char) + 7 (padding) + sizeof(double) + sizeof(int),1 + 7 + 8 + 4 = 20 bytes。然而实际上并不是这样的结果,因为虽然前述内容看上去满足了基本数据类型的自身对齐,但是对于结构体这种复合数据类型,作为一个整体也需要满足对齐,根据 对齐原则,结构体或类的自身对齐,是以成员中自身对齐最大的那个值,也就是structc_t自身对齐的值为 8,所以在int后还有4个字节的填充,sizeof(char) + 7 (padding) + sizeof(double) + sizeof(int) + 4 (padding), 1 + 7 + 8 + 4 + 4 = 24 bytes。

对于structd_tag,对于这个的分析就简单多了,这里就不再赘述,结果显而易见,sizeof(double) + sizeof(char) + 1 (padding) + sizeof(int) + 2 (padding),8 + 1 + 1 + 4 + 2 = 16 bytes。

packing

#pragma pack(n)手动指定对齐数值。但是并不是指定后一定就能生效,因为根据对齐原则,有效对齐值=min{自身对齐值,当前指定的 pack 值}

在 linux 中,使用 gcc 编译器时候可以使用 gcc 特有的语法来强制指定对齐数值,__attribute__((aligned (n)))

#include <stdio.h>

typedef struct A A;

struct A {
    char c[3];
} __attribute__((aligned (4)));

int main(int argc, char *argv[])
{
    printf("sizeof(A) = %d\n", sizeof(A));
    return 0;
}

// will output 4

这种写法在 linux kernel 中非常常见,例如著名的rbtree中有如下声明:

struct rb_node {
	unsigned long  __rb_parent_color;
	struct rb_node *rb_right;
	struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
    /* The alignment might seem pointless, but allegedly CRIS needs it */

关于__attribute__的其它用法,可以参考以下文章:

位域(bit field)

有些数据在存储时不需要占用一个完整的字节,只需要占用一个或几个二进制位。例如记录开关状态的变量,用01表示,也就是一个二进制位就能满足。正式基于这种考虑,c 语言又提供了一种叫做位域的数据结构。 c 标准允许unsigned int/signed int/int类型的位域声明,c99 中加入了_Bool类型的位域。但像 GCC 这样的编译器自行加入了一些扩展,比如支持shortchar等整形类型的位域字段。

以下是 c 语言中 bit field 的一般形式

#include <stdio.h>

int main(int argc, char *argv[])
{
    struct A {
        unsigned short s1 : 1,
                       s2 : 2,
                       s3 : 3,
                       ......,
                       sn : k;
    };
    return 0;
}

要特别注意的是,bit field 是基于二进制位的一种结构,而不是基于字节(byte).

bit field 的出现,让我们可以通过变量名代表某些 bit,并通过变量名直接获得和设置一些内存中 bit 的值,而不是通过晦涩难以理解的位操作来进行。例如:

#include <stdio.h>

int main(int argc, char *argv[])
{
    struct foo_t {
        unsigned int a : 3,
                     b : 2,
                     c : 4;
    };

    struct foo_t f;
    f.a = 3;
    f.b = 1;
    f.c = 12;
    return 0;
}

另外,使用 bit field 可以在展现和存储相同信息时,自定义更加紧凑的内存布局,节约内存的使用量。这使得 bit field 在嵌入式领域,在驱动程序领域得到广泛的应用。比如可以仅用两个字节就可以将 tcpheader 从 dataoffset 到 fin 的信息全部表示和存储起来:

#include <stdio.h>

int main(int argc, char *argv[])
{
    struct tcphdr {
        __u16 doff : 4,
              res1 : 4,
              cwr : 1,
              ece : 1,
              urg : 1,
              ack : 1,
              psh : 1,
              rst : 1,
              syn : 1,
              fin : 1;
    };
    return 0;
}

注意:在 c 语言中,尝试获得一个 bit field 的地址是非法操作。

#include <stdio.h>

int main(int argc, char *argv[])
{
    struct flag_t {
        int a : 1;
    };

    struct flag_t f;
    printf("%p\n", &f.a);
    return 0;
}

//compile output
liubang@venux:~/workspace/c/learn/02$ gcc demo5.c -o demo5
demo5.c: In function main:
demo5.c:10:20: error: cannot take address of bit-field a
     printf("%p\n", &f.a);
                    ^

匿名位域

顾名思义,匿名位域就是位域成员没有名称,只给出了数据类型和位宽,如下所示:

#include <stdio.h>

int main(int argc, char *argv[])
{
    struct bs {
        int m : 12;
        int   : 20;
        int n : 4;
    };
    return 0;
}

匿名位域一般用来填充或者调整成员的位置。因为没有名称,所以匿名位域不能使用。