13. 结构体

一、什么是结构体

  “结构体” 是一种构造类型,它是由若干 “成员” 组成的,其中的每一个成员可以是基本数据类型也可以是一个构造类型。既然构造体是一种新的类型,就需要先对其进行构造,这里称这种操作为声明一个结构体。

二、结构体的声明

  我们需要使用 struct 关键字来声明结构体,其一般格式如下:

struct 结构体名
{
    数据类型 成员1;
    数据类型 成员2;
    ...
    数据类型 成员n;
};

  其中,struct 关键字表示声明结构体,其后的结构体名表示该结构体的类型名,大括号中的变量构成结构体的成员,也就是一般形式的成员列表,成员列表可以是任意一种 C 的数据类型,也可以是其它构造类型。右花括号后面的分号是声明所必须的,表示结构体声明的结束。

  我们可以把结构体的声明放在所有函数的外部,也可以放在一个函数定义的内部。如果把结构体声明置于一个函数的外部,该结构体只限于在这个函数中使用;如果把结构体声明位置所有函数的外部,那么该声明之后的所有函数都可以使用这个结构体。

在声明结构体时,要注意括号最后面有一个分号“;”

结构体名一般每个单词的首字母大写;

三、结构体变量的定义

  声明一个结构体表示的是创建一种新的类型名,需要用新的类型名在定义一个变量。不同结构体变量的成员是相互独立的,互补影响,一个结构体变量的更改,不会影响到另一个结构体。定义的方式有 3 种:

【1】、声明结构体类型,再定义变量

struct 结构体类型名 结构体变量;

【2】、在声明结构体时定义变量

struct 结构体名
{
    数据类型 成员1;
    数据类型 成员2;
    ...
    数据类型 成员n;
}变量名1, 变量名2, ..., 变量名n;

定义的变量不是只能有一个,可以定义多个变量;

【3】、直接定义结构体类型变量

  如果我们只需要确定个数的结构体变量,后面不需要再使用结构体数据类型定义变量,我们可以在定义时不给出结构体名,这种结构体称为 匿名结构体

struct
{
    数据类型 成员1;
    数据类型 成员2;
    ...
    数据类型 成员n;
}变量名1, 变量名2, ..., 变量名n;

四、结构体变量的初始化

  我们可以在结构体类的声明,变量声明,赋初值同时进行。在初始化时,初始化的值放在大括号中,多个值使用逗号 “,” 分隔,并且每一个数据要与结构体的成员列表一一对应。

struct 结构体名
{
    数据类型 成员1;
    数据类型 成员2;
    ...
    数据类型 成员n;
}变量名 = {值1, 值2, ..., 值n};

  结构体类型与其它基本类型一样,也可以在定义结构体变量时指定初始值。

struct 结构体名 变量名 = {值1, 值2, ..., 值n};
#include <stdio.h>

struct Person
{
    char name[20];
    char gender[10];
    int age;
}person={"Sakura","女",10};

int main(void)
{
    printf("name: %s\n",person.name);
    printf("gender: %s\n",person.gender);
    printf("age: %d\n",person.age);

    return 0;
}

  C99 和 C11 为结构体提供了 指定初始化器(designated unitializer),它使用 点运算符成员名 来表示特定的元素。它的语法格式如下:

struct 结构体名 变量名 = {
    .成员1 = 值1,
    .成员2 = 值2,
    ...,
    .成员n = 值n
};
#include <stdio.h>

struct Person
{
    char name[20];
    char gender[10];
    int age;
};

int main(void)
{
    struct Person person = {
        .name = "Sakura",
        .age = 10
    };

    printf("name: %s\n",person.name);
    printf("gender: %s\n",person.gender);
    printf("age: %d\n",person.age);

    return 0;
}

五、结构体变量的引用

  对结构体变量进行赋值、存取或运算,实际上就是对结构体成员的操作。引用结构体成员的一个格式为:

结构体变量名.成员名

不能直接将一个结构体变量作为一个整体进行输入和输出;

六、包含结构体的结构体

  结构体中的成员不仅可以是基本数据类型,也可以是结构体类型。

#include <stdio.h>

struct Date{
    int year;
    int month;
    int day;
};

struct Person{
    char name[20];
    int age;
    char gender[10];
    struct Date birthday;
}person = {"Sakura",10,"女",{1987,4,1}};

int main(void)
{
    printf("name: %s\n",person.name);
    printf("age: %d\n",person.age);
    printf("gerder: %s\n",person.gender);
    printf("birthday:%d-%d-%d\n",person.birthday.year,person.birthday.month,person.birthday.day);

    return 0;
}

七、结构体的自引用

  在结构体中包含一个类型为该结构体本身的成员。

struct Node
{
    int data;
    struct Node * next;
};

八、结构体内存对齐

  结构体的对齐规则:

  1. 第一个成员在结构体变量偏移量为 0 的地址。
  2. 其它成员变量要对齐到某个整数(对齐数)的整数倍的地址处。
    • 对齐数 = 编译器默认的一个对齐数 与 该成员大小 的 较小值
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
  4. 如果嵌套了结构体的情况。嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
#include <stdio.h>
#include <stddef.h>

// VS编译器默认对齐字节大小是8字节
// 其它编译器没有默认对齐数
// #pragma pack(1)     // 修改默认对齐数

struct S
{
    char c1;        // 字节大小1字节
    int i;          // 字节大小4字节
    char c2;        // 字节大小1字节
}s1;

int main(void)
{
    printf("%zu\n",sizeof(s1));

    // offsetof宏可以返回一个成员在结构体中的偏移量
    printf("%d\n", offsetof(struct S,c1));
    printf("%d\n", offsetof(struct S,i));
    printf("%d\n", offsetof(struct S,c2));

    return 0;
}

结构体内存对齐

#include <stdio.h>

struct S1
{
    double d;       // 字节大小8字节
    char c;         // 字节大小1字节
    int i;          // 字节大小4字节
};

struct S2
{
    char c;         // 字节大小1字节
    struct S1 s1;
    double d;       // 字节大小1字节
}s2;


int main(void)
{
    printf("%zu\n",sizeof(s2));
  
    return 0;
}

结构体嵌套结构体内存对齐

  为什么要存在结构体内存对齐呢?大部分的参考资料是如下解释的:

  ①、平台原因

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常;

  ②、性能原因

  数据结构(尤其是栈)应该尽可能在自然边界对齐。原因在于,为了访问未对其的内存,处理器需要做两次内存访问;而对齐的内存仅需要一次访问。

总体上来说,结构体的内存对齐是拿空间获取时间的做法;

我们在设计结构体时,让占用空间小的成员尽量集中在一起,这样既可以满足内存对齐,又可以节省空间;

posted @ 2023-03-09 17:44  星光樱梦  阅读(33)  评论(0编辑  收藏  举报