1. 结构体

1.1. 结构体的声明、定义和初始化

typedef struct Student {
    char name[1024];
    char gender[10];
    unsigned int age;
}student;

int main() {
    student stu = { "Jack", "Male", 13 };  // right
    stu = { "Mike", "Male", 14 };  // wrong 大括号只能初始化结构体变量,不能赋值

    system("pause");
    return 0;
}

C语言允许没有名字的结构体,可以把这种结构体看作“一次性”的。

struct {
    char name[1024];
    char gender[10];
    unsigned int age;
}stu1;

1.2. 结构体的自引用

如果你的结构体里想要包含一个类型为该结构体本身的成员变量,该怎么做呢?

typedef struct Student{
    char name[1024];
    char gender[10];
    unsigned int age;
    struct Student stu;  // 不能这样包含自身,设想这样的结构体怎么求sizeof?
}student;

而应该用指向这个结构体的指针。

typedef struct Student{
    char name[1024];
    char gender[10];
    unsigned int age;
    struct Student* stu;
}student;

1.3. 结构体内存对齐 ⭐

首先要清楚一点,C语言中,结构体的总大小绝不是简单的把每个成员变量要占的空间直接加起来就行了,而要符合一些特定的规则分配内存空间,其中最重要的规则就是“内存对齐”。

结构体的对齐规则:

  • 第一个成员变量在整个结构体偏移量为0的位置
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。vs默认为8 Linux默认为4
  • 结构体的总大小为所有成员变量的最大对齐数的整数倍
  • 如果嵌套了结构体,嵌套的结构体对齐到自己的所有成员变量的最大对齐数的整数倍处
  • 如果成员变量是数组,计算结构体空间分布时相当于把数组拆开,每个元素都是一个成员变量

下面详细分析一个例子:

typedef struct {
    char ch1;
    int integer;
    char ch2;
}myStruct;

int main() {
    printf("%d\n", sizeof(myStruct));  // 12

    system("pause");
    return 0;
}

首先ch1是第一个成员变量,所以要对齐到整个结构体偏移量为0的位置,占1个字节。之后是integer,根据规则2,integer的对齐数为4,所以要对齐到结构体偏移量为4的整数倍的位置,占4个字节。在之后是ch2,对齐数为1,所以紧跟在integer后面。最后根据规则3,这个结构体成员变量的最大对齐数是4,所以整个结构体所占空间应该是4的整数倍,在例子里就是12字节,所以最终的内存布局就如上图所示。

下面还有一些关于结构体内存对齐的测试,大家可以练练手。

struct S1 {
    char c1;
    char c2;
    int i;
};

struct S2 {
    double d;
    char c;
    int i;
};

struct S3 {
    char a;
    short b;
    int c;
    double d;
    int e;
    short f;
    char g;
};

struct S4 {
    char c1;
    struct S3 s3;
    double d;
};

struct S5 {
    char a;
    char arr[10];
    char b;    
};

int main() {
    printf("%d\n", sizeof(struct S1));  // 8
    printf("%d\n", sizeof(struct S2));  // 16
    printf("%d\n", sizeof(struct S3));  // 24
    printf("%d\n", sizeof(struct S4));  // 40
    printf("%d\n", sizeof(struct S5));  // 12

    system("pause");
    return 0;
}

修改默认对齐数

之前说VS的默认对齐数为8,但这个数值是可以修改的:

#pragma pack(1)  // 从这条预处理指令开始,默认对齐数修改为1
struct S1 {
    char c1;
    char c2;
    int i;
};
#pragma pack()  // 恢复原来的默认对齐数

int main() {
    printf("%d\n", sizeof(struct S1));  // 6

    system("pause");
    return 0;
}

1.4. 结构体传参

如果需要通过参数传递结构体,最好通过结构体指针来传递。因为指针可以修改原结构体,而且传参开销更小。

2. 位段

2.1. 什么是位段

位段也叫位域。位段可以看成结构体的另一种形式,以二进制位为单位来存储结构体中的每个成员变量。

先来讨论一下,为什么要有位段。假如我要一个Student结构体来表示每个学生的抽象,里面成员变量有gender,age等。但我们知道性别只能有男或女,年龄我撑死给你算最大值为255,那么我们定义这个结构体的时候要是用int等类型来存储这些成员变量的话,就会有很多二进制位被浪费了,gender只要1位二进制就够了,age也只需要8位二进制。那怎么办呢,位段就是用来处理这种情况的。

假如你希望更合理的利用结构体中的空间,你可以定义位段解决这个问题。下面是一个位段的示例:

struct Student{
    unsigned int gender : 1;
    unsigned int age : 8;
}s3;

这样,将结构体中的每个成员变量后面加一个冒号,之后再加上你希望这个成员变量在内存中用几位二进制来存储,这种定义结构体的方法就叫做位段。

2.2. 位段的规则与内存中的存储

其次,如果位段的所有成员变量都是同样的类型(不区分有符号和无符号)比如都是int,那么位段中的所有成员就在int类型本来需要的4字节的空间(称为位段单元)中紧挨着存储,如果所有位段元素的大小加起来不到一个位段单元,那么整个位段就占4字节,如果超过了一个位段单元,总大小就以位段单元为单位往上加。

定义一个位段,成员a、b、c各占4位,并赋值为1 ,因为三个成员加起来只占12位,不到int的32位(一个位段单元),所以求sizeof得到4

s在内存中的存储,可以看出同样是小端字节序存储,并且a存储在4字节的最低字节的低4位上,b存储在高四位上,c存储在第二个字节的低四位上,三个成员紧挨着存储

如果成员加起来超过了一个位段单元,那么整个位段就会扩展一个int的大小,所以求sizeof得8

并且,某一个成员如果在第一个位段单元中存不下了,它不会跨位段单元(类型为int位段单元就是4字节)存储,在这个例子中就是,d之前只剩8位了,但d需要9位,它不会在第一个4字节中存储一部分,剩下的部分存在下一个4字节中,而是会直接跳到下一个四字节开始存储

但是如果位段中的成员变量具有不同的类型,就需要进行类似于结构体的内存对齐。

成员a只占1位,但是因为下一个成员是int,所以根据结构体的内存对齐的规则,b会存储在偏移量为4字节的位置,而c又是char,所以得存储在偏移量为8的位置,所以最终的sizeof得12

在位段的成员中还允许未命名且长度为0的成员,这表示在这里跳过当前的位段存储单元。

可以看出成员c跳过了第一个位段单元(4字节)

2.3. 位段练习题

了解了位段是什么以及怎么在内存中存储,现在做一道练习题来加深一下印象吧。

struct {
    unsigned int a : 5;
    unsigned int b : 3;
}s;

int main() {
    char str[100] = "0134324324afsadfsdlfjlsdjfl";
    memcpy(&s, str, sizeof(s));
    printf("%d\n", s.a);  // 16
    printf("%d\n", s.b);  // 1
    printf("\n");

    system("pause");
    return 0;
}

根据前面的知识,我们知道位段s在内存中占4字节,而a和b都存储在最低字节上。所以这个memcpy我们只需要关注str中的第一个字符'0',因为他也是str数组中的最低字节的内容。'0'的ASCII码是48,二进制是0011 0000,然后a占了右边5位即1 0000,所以是16,b占了左边3位即001,所以是1。

2.4. 位段总结

不管是位段还是结构体,光靠脑子想不自己实验各种情况是很难完全掌握的,希望如果有人看到了这,还是多加练习把。

3. 枚举

3.1. 枚举定义

枚举,顾名思义就是一种所有可能取值能被枚举出来的数据结构,并且枚举可以把每个取值都用一个更加好记的名字来表示。

typedef enum {
    Red,
    Green,
    Blue,
}color;

如果没有显式的指明每个名字对应的数值是多少,则第一个名字默认值为0,下面的依次递增。如果指明了第一个名字的值,则后面的跟着第一个值依次递增。如果指明了某个不是第一个名字的元素的值,则之前的从0递增,之后的从给定的值递增。

C语言对于枚举变量的规则设的十分宽松。例如,可以给枚举变量赋值不在定义中的数值,也可以赋负值,枚举的定义中也允许给不同的名字赋同样的值。

4. 联合(共用体)

4.1. 联合是什么

联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间。

4.2. 联合在内存中的存储

联合的大小至少是最大成员的大小。

当最大成员大小不是联合中的成员的最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

union Un1 {
    char c[5];  // 最大成员
    int i;  // 成员中的最大对齐数
};

union Un2 {
    short c[7];  // 最大成员
    int i;  // 成员中的最大对齐数
};

int main() {
    printf("%d\n", sizeof(union Un1));  // 8
    printf("%d\n", sizeof(union Un2));  // 16
    system("pause");
    return 0;
}

4.3. 联合的应用

可以用来方便的分割IP地址

union IP {
    uint32_t data;
    struct Dec {
        uint8_t a;
        uint8_t b;
        uint8_t c;
        uint8_t d;
    }dec;
}ip;

int main() {
    ip.data = 0xC0A80001;
    printf("%d.%d.%d.%d\n", ip.dec.d, ip.dec.c, ip.dec.b, ip.dec.a);
    system("pause");
    return 0;
}
最后修改:2019 年 11 月 10 日
如果觉得我的文章对你有用,请随意赞赏