概述

结构体、联合体、枚举被称为C语言的构造数据类型,使用它们之前需要先把相应的类型构造出来,然后才可以用它定义变量。位域可以看成是一种特殊的结构体。

结构体定义

结构体可以把其他的数据类型打包成一个新的数据类型。这样,如果程序中需要处理一些关联性比较强的数据,那么则可以使用结构体来定义。比如统计班上学生的信息,每个人都有姓名,学号,分数等信息,如果将这些信息分别用不同的变量存储,那么程序将非常杂乱,不易读懂。而如果将学生的信息定义成一个新的类型,则可以用一个该类型的变量来存储学生的全部信息,非常方便,以下是结构体的一般定义方法:

struct 结构体名称
{
    成员1;
    成员1;
    ...
};

比如,可以用以下结构体来表示学生信息:

struct student
{
    char name[10];
    int id;
    int score;
};

那么,则定义了一个新的数据类型,它的类型是struct student,接下来可以用它来定义变量并存储信息:

struct student stu = {"Louis", 22, 85};

结构体内存图

Alt text

访问结构体成员

定义好结构体变量之后,就可以用.运算符访问结构体对象的成员了,如下:

stu.id = 10;
stu.socre = 90;
strcpy(stu.name, "Louis");

同样,如果要知道结构体的地址和成员的地址,可以用取地址符&表示,如下:

printf("%p\n", &stu1);
printf("%p\n", stu1.name);
printf("%p\n", &stu1.id);
printf("%p\n", &stu1.score);

结构体指针

用结构体类型同样可以定义指针变量,如下:

struct student stu = {"Louis", 10, 85};
struct student *p = &stu;    //定义结构体指针变量并赋值

通过该指针也可以访问到结构体的成员,只要解一下地址就可以了:

(*p).name ==> "Louis"  //通过解地址方式访问成员
(*p).id ==> 10
(*p).score ==> 85

但是,如果某个指针是结构体类型的指针,通过该指针来访问结构体的成员时,可以不用写那么麻烦,直像像下面那样写就可以了:

p->name ==> "Louis"  //通过结构体指针访问成员
p->id ==> 10
p->score ==> 85

结构体内存对齐

要理解内存对齐,首先明白一个概念,在现代计算机体系中,CPU从内存中拿数据,CPU与内存通过总线连接,进行数据读写。在x86体系下,总线位宽是32位,这也是所有指针类型都占用4个字节的原因。既然总线位宽是32位,那就意味着CPU一次可以从内存拿到连续4个字节的数据。这样,将数据按照相隔4字节进行存放,存取的效率是最高的,相邻距离在4字节以内,存取效率反而会降低了。64位系统与之同理,只不过64位系统一次可读写的值是8字节。

为了提高存取的效率,编译器在存放结构体的时候,会默认对结构体的成员进行内存对齐,也就是在结构体的各个成员后面填充空字节,使填充后的结构体大小满足对齐要求,从面更方便CPU读写,对齐的规则如下:

  1. 只按char、short、int、float、double、指针等“原子类型”对齐,结构体嵌套时,需要将子结构体展开成原子类型。
  2. char对齐值为1,short对齐值为2,int、float对齐值为4字节,指针和double的对齐值和体系结构有关,32位系统下,指针和double的对齐值为4字节,64位系统下则为8字节。注意在32位系统下,double类型的大小为8字节,但对齐值为4字节。
  3. 32位系统下的系统对齐值为4字节,64位系统下的系统对齐值为8字节。
  4. 成员变量相对于结构体起始位置的偏移,必须是该成员对齐值与系统对齐值这两者较小值的整数倍
  5. 整个结构体的大小,必须是结构体对齐值的整数倍。结构体对齐值为系统对齐值与结构体成员中的最大对齐值这两者的较小值

关于体系结构的验证,在64位Linux系统下,可以通过gcc的-m32选项编译出32位的可执行程序来验证,以下是一份用于检验的代码,重点关注几个结构体在32位和64位系统下的对齐规则:

#include <stdio.h>

struct A {
    char a;
    double b; 
};

struct B {
    double a;
    char b;
};

struct C {
    short a;
};

struct D {
    short a;
    char b;
    short c;
};

struct E {
    short a;
    int b;
    short c;
};

struct F {
    short a;
    double b;
    short c;
};


int main()
{
    printf("sizeof(char)   = %d\n", (int)sizeof(char));     // 1
    printf("sizeof(int)    = %d\n", (int)sizeof(int));      // 4
    printf("sizeof(double) = %d\n", (int)sizeof(double));   // 8

    int a = 1;
    printf("sizeof(a)      = %d\n", (int)sizeof(a++)); // 4
    printf("a = %d\n", a); // a = 1

    printf("sizeof struct A = %d\n", (int)sizeof(struct A));
    printf("sizeof struct B = %d\n", (int)sizeof(struct B));
    printf("sizeof struct C = %d\n", (int)sizeof(struct C));
    printf("sizeof struct D = %d\n", (int)sizeof(struct D));
    printf("sizeof struct E = %d\n", (int)sizeof(struct E));
    printf("sizeof struct F = %d\n", (int)sizeof(struct F));
    return 0;
}


修改默认的对齐方式

可以通过预编译指针 #pragma pack(value) 来告诉编译器使用指定的对齐值进行对齐。
如下:

#pragma pack(1) // 指定按1字节对齐
struct stu
{
    char str[10];
    int a;
};
#pragma pack() //取消指定对齐,恢复默认对齐

sizeof(struct stu)为14字节。

注意点

只有在设计二进制协议或映射寄存器等特殊场景需要关注字节对齐问题,否则在工作上字节对齐只是一个原则问题,并不需要一定按照最高存储效率的方式来定义结构体,只需要知道尽量将小的成员变量往前写,避免过度填充就可以了。

结构体-其他

结构体定义与初始化

  1. 只可以在定义时对结构体进行一次性赋值,定义完成后,再次引用结构体时,只能引用结构体的成员,而不能引用整个结构体,如下:

    struct student stu;
    stu = {"Louis", 10, 85};  //不可以这样赋值
  2. 定义结构体变量时,可以只对结构体的某些变量赋值,采用如下方式:

    struct student
    {
        char name[10];
        int id;
        int score;
    };
    struct student stu = {
        .id = 10, 
        .score = 85
    };
  3. 可以在定义结构体类型的同时定义变量,如下:

    struct student
    {
        char name[10];
        int id;
        int score;
    } stu1, stu2;
  4. 可以定义匿名的结构体变量,如下:

    struct
    {
        char name[10];
        int id;
        int score;
    } stu1, stu2;
  5. 结构体的也存在部分赋值,规则与数组的部分赋值一致,如下:

    struct A
    {
        int a;
        int b;
        int c;
    };
    struct A stru1 = {0};    //定义结构体变量并清零
    struct A stru2 = {1, 2}; //定义变量并设置前两个成员的值,后面的值则为0;
  6. 可以通过结构体给另一个结构体赋值,如下:

    struct A a = {1, 2, 3};
    struct A b = a; // 结构体直接赋值

结构体数组

通过结构体定义的数组与普通数据无本质区别。

结构体包含结构体

结构体的成员变量可以是另一个结构体,但不可以是自身的类型,类似下面的写法是错误的:

struct stu
{
    struct stu b; //错误,成员不可以是自身类型
};

结构体作为函数参数

结构体作为参数参数时,参数由实参传给形参时,也是按照传值的方式进行传递的,实参的值会完整地复制一份给形参。

结构体类型转换

结构体本身不可以进行类型转换,如果想实现类型转换,只能借助指针:

struct A a;
struct B b  = (struct B)a; // 错误,结构体对象不可以使用类型转换
struct B *p = (struct B*)&a; // 正确,pb作为结构体B的指针,实际指向的是结构体a的内存空间

结构体与指针

1、结构体包含自身类型的指针 ==> 链表结构,数据结构部分讲解
2、结构体包含函数指针 ==> 表驱动法,,《代码大全(第2版)》第18章。
3、结构体包含指向堆内存的指针 ==> 深拷贝与浅

位域(位段)

字节对齐有时会浪费一部内存,但是有的时候,节约一点点的内存也会有奇效,比如网络传输。而且,在存储某些信息的时候,我们可能并不需要占用一个完整的字节,而只需要占一个或几个二进制位就可以了,比如存储一个八进制数只需要3个二进制位。为了节省存储空间,C语言提供了位段这种数据结构。所谓位域,就是把存储空间中的二进制位划分成几个不同的区域,每个区域有一个名称,允许在程序中按这个名称对内存进行操作。C语言中,定义位段的方式如下:

struct 位域结构名
{
    类型说明符 位域名1 : 位域长度;
    类型说明符 位域名2 : 位域长度;
    ...
};

细分的定义方式和结构体一样,不再赘述。

比如可以按如下方式定一个位段结构:

struct bitField
{
    unsigned char l:4;
    unsigned char h:4;
}

使用位域要注意以下几点:

  1. 位域的类型只可以是整数类型(char, int及其变体)。
  2. 位域的定义必须从右往左的顺序,从数据的最低位开始定义。
  3. 一个位域必须存储在同一个字节中,不能跨越两个字节,如果一个字节所剩空间不够放另一个域时,应该从下一单元起存放位域,如下所示:

    struct A
    {
        int a : 4;
        int : 0;   //空域
        int b : 5; //从第二个字节开始存放
        int c : 3;
    };
  4. 一个位域不能大于一个字节,也就是说一个位域不能超过8位。
  5. 一个位域可以无位域名,这时它只作填充或调整位置。无名的位域不能使用。

    struct A
    {
        int a : 4;
        int : 2; //这两位不能使用
        int b : 2;
        int c : 5;
        int d : 3;
    };
  6. 不可以对位域进行取地址操作。

联合体

联合体的关键字是union,它与struct有一样的语法规则,不同的是,联合体的各个成员在内存中是共享一块内存的,它占用的内存大小是整个联合体中最大的那个成员的大小。以下是union的使用示例与内存图:

union A
{
    int a;
    int b;
    double c;
};

内存图展示:
Alt text

通过联合体判断系统的大小端

/**
 * @brief 获取系统大小端
 * 
 * @return 小端返回0,大端返回1
 */
int endian(void)
{
    union
    {
        int i;
        char c;
    } test;
    test.i = 1;
    return (test.c == 1);
}

枚举

所有的基本数据类型都有自己的范围,比如unsigned char类型的范围是0-255,那么用unsigned char定义的变量就可以取0~255以内的任何值。而如果希望某种数据类型,用它只能表示某几个值,那么就可以用枚举类型来定义。

枚举是一种特殊的构造类型,它本身和int是等价的,但它的值是有限个int常量的合集,这些常量称为枚举符。定义枚举类型的方式和结构体类似,如下:

enum 类型名称 { 枚举符列表 };

比如:

enum WEEKDAY { MON, TUE, WED, THU, FRI, SAT, SUN };

定义了枚举类型之后,就可以用这个枚举类型去定义变量了,用枚举定义的变量,其值只能取枚举列表里面的值,如果不在该列表中,则会报错或者警告,如下:

enum WEEKDAY a = MON; // 定义枚举变量a,它的值是枚举符MON
enum WEEKDAY b = 7;   // 错误,枚举变量只能取枚举符列表里的值


由于枚举符在C语言中是int类型的常量,所以它们可以用在int常量可以出现的任何地方。如果在定义枚举类型时没有用“=”指定枚举符的值,则所有的枚举符从常量值0开始。如果有用“=”为枚举符指定一个值,则从其后的枚举符从这个值开始依次递增。

enum test
{
    m1,     //枚举符从0开始,m1等于0
    m2,     //依次递增,m2等于1
    m3 = 5, //用=指定了枚举符的值,则m3等于5
    m4,     //从m3的值依次递增,m4等于6
};

枚举符的值可以相同,但是这样做容易引起歧义,所以在设计枚举类型的时候要注意避免。

枚举作为一种数据类型,它同样可以用于构造数组,指针,或是作为结构体成员变量,性质与基本数据类型一样,无需要特殊讨论。

  • 无标签