概述

结构体、联合体、枚举被称为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字节以内,存取效率反而会降低了。

为了提高存取的效率,编译器在存放结构体的时候,会默认对结构体的成员进行内存对齐,对齐的规则如下:

  1. 只按char, short, int, float, double,指针等“原子类型对齐”,结构体嵌套时,需要将子结构体展开成原子类型。
  2. char对齐值为1,short对齐值为2,int,float,double,指针的对齐值为4。
  3. 每个成员变量存放的起始位置相对于结构体起始位置的偏移量,必须是该成员对齐值与系统对齐值这两者较小值的整数倍。
  4. 整个结构体的大小,必须是结构体对齐值的整数倍。结构体对齐值为系统对齐值与结构体成员中的最大对齐值这两者的较小值。

修改默认的对齐方式

可以通过预编译指针 #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);
}

  • 无标签