Bootstrap

高阶C语言|和结构体与位段的邂逅之旅

💬 欢迎讨论:在阅读过程中有任何疑问,欢迎在评论区留言,我们一起交流学习!
👍 点赞、收藏与分享:如果你觉得这篇文章对你有帮助,记得点赞、收藏,并分享给更多对C语言感兴趣的朋友!

结构体

结构体(struct)是C语言中用来表示一组不同类型数据的复合数据类型。通过结构体,我们能够将相关的数据元素组合成一个整体,方便统一管理。每个数据元素称为结构体的成员(member),这些成员可以是不同类型的变量。

结构体声明

结构体的声明使用 struct 关键字,语法格式如下:

struct tag {
    member-list;
} variable-list;

其中:

  • tag 是结构体的标签名(即结构体类型名)。
  • member-list 是结构体的成员列表,可以包括不同类型的数据。
  • variable-list 是结构体变量的声明。

示例:描述一个学生的结构体

typedef struct Stu {
    char name[20]; // 姓名
    int age;       // 年龄
    char sex[5];   // 性别
    char id[20];   // 学号
} Stu; // 注意分号不能省略

在这个示例中,我们定义了一个名为 Stu 的结构体类型,用来表示学生。typedef 用于给结构体类型起别名,这样我们在以后使用时就可以直接使用 Stu 来代表该类型,而不需要每次都写 struct Stu

特殊的声明——匿名结构体

匿名结构体是指在声明时省略结构体的标签(tag)。匿名结构体通常只会使用一次,因此它没有必要具名。其语法如下:

// 匿名结构体类型
struct {
    int a;
    char b;
    float c;
} x;

struct {
    int a;
    char b;
    float c;
} a[20], *p;

注意: 匿名结构体只能使用一次,无法重复定义。它适用于临时性需求,避免命名重复的问题。

结构体的自引用

结构体中可以包含指向自身类型的指针,这种结构体被称为自引用结构体。自引用结构体常常用于链表、树等数据结构。

正确的自引用

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

在这里,Node 结构体包含一个指向自己类型 Node 的指针 next,这使得它能够表示链表结构。

错误的自引用

struct Node {
    int data;
    struct Node next; // 错误:无限递归,导致内存占用爆炸
};

这里的错误在于,next 变量是一个 Node 类型,而不是一个指针。这样会导致结构体大小计算错误,从而使得 sizeof(struct Node) 变得无限大。

结构体变量的定义和初始化

结构体变量的定义和初始化其实非常简单。你可以在定义时直接进行初始化,也可以先定义再初始化。

定义结构体变量

struct Point {
    int x;
    int y;
} p1; // 声明结构体类型同时定义变量 p1

struct Point p2; // 仅定义结构体变量 p2

初始化结构体变量

你可以在定义结构体的同时进行初始化:

struct Point p3 = {10, 20}; // 在定义的同时给变量赋初值

也可以单独定义后进行初始化:

struct Stu { // 定义结构体类型
    char name[15]; // 姓名
    int age;       // 年龄
};
struct Stu s = {"zhangsan", 20}; // 初始化结构体变量 s

嵌套结构体初始化

结构体可以嵌套其他结构体,在初始化时需要按照嵌套顺序逐级初始化:

struct Node {
    int data;
    struct Point p;
    struct Node* next;
} n1 = {10, {4, 5}, NULL}; // 初始化嵌套结构体

struct Node n2 = {20, {5, 6}, NULL}; // 另一种结构体嵌套初始化

结构体成员的访问

结构体变量的成员是通过点操作符(.)进行访问的。点操作符是双目操作符,左边是结构体变量,右边是成员名。

示例:结构体成员访问

struct Stu {
    char name[20];
    int age;
};

int main() {
    struct Stu s;
    strcpy(s.name, "zhangsan"); // 使用 . 访问 name 成员
    s.age = 20; // 使用 . 访问 age 成员
    return 0;
}

结构体传参

在函数传递结构体时,通常通过指针传递,避免传递整个结构体的拷贝。通过传递指针,我们可以在函数中修改结构体的内容。

结构体地址传参

// 结构体地址传参
void print2(struct Stu* ps) {
    printf("%d\n", ps->age); // 通过指针访问成员
}

int main() {
    struct Stu s = {"zhangsan", 20};
    print2(&s); // 传地址
    return 0;
}

ps->age 等价于 s.age,使用箭头操作符(->)来访问结构体指针的成员。

结构体内存对齐

内存对齐是计算机系统中为了提高性能而对数据进行排列的一种方式。结构体的成员并不是按照顺序紧凑存储的,而是会根据对齐规则进行排列。

对齐规则

  1. 第一个成员在结构体变量的偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数= 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的值为8
    Linux gcc:没有默认对齐数,对齐数就是结构体成员的自身大小
  3. 结构体总大小必须是最大对齐数的整数倍。 如果结构体内部有成员的对齐数较大,整个结构体的大小也会是该对齐数的整数倍。
  4. 如果结构体中嵌套了结构体成员,要将嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

这里的最大对齐数是:包含嵌套结构体成员中的对齐数,的所有对齐数中的最大值!

内存对齐示例

struct S {
    char c1;
    int i;
    char c2;
};
  • char 占 1 字节,int 占 4 字节。
  • 编译器会将 int 类型成员 i 对齐到 4 字节边界,而 char 类型成员会填充到 4 字节对齐的位置。

为什么需要内存对齐?

内存对齐不仅有助于提高内存访问效率,还能避免在某些硬件平台上因不对齐的访问而引发硬件异常。在大多数现代处理器中,如果数据未对齐,处理器将需要额外的周期来完成访问,导致性能下降。
大部分参考资料:

  1. 平台原因(移植原因):

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

  1. 性能原因:

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

总体来说:

结构体的内存对齐是拿空间来换取时间的做法。

修改默认对齐数

使用 #pragma pack 指令可以调整结构体的内存对齐方式,从而优化内存布局和节省空间。

#pragma pack(8) // 设置对齐数为 8
struct S1 {
    char c1;
    int i;
    char c2;
};
#pragma pack() // 恢复默认对齐数

通过修改对齐数,可以减少内存填充,但也可能影响性能。使用时需要谨慎。

位段(Bit Fields)

什么是位段

位段是一种特殊的结构体成员,它允许我们在结构体中精确控制每个成员所占用的比特位数。位段的声明格式如下:

struct A {
    int _a:2;  // 占2位
    int _b:5;  // 占5位
    int _c:10; // 占10位
    int _d:30; // 占30位
};

注意: 位段成员必须是 intunsigned intcharsigned int 类型。

位段的内存分配

位段的内存分配有其独特的挑战,主要因为它依赖于硬件平台和编译器的实现。以下是一些位段大小计算的要点:

  • 每个成员的大小由它所占用的比特位数决定。
  • 如果成员的大小无法完全填充到所分配的内存单元(例如 int 类型占 4 字节,即 32 位),则剩余部分会被填充或丢弃。
示例:位段内存计算
struct A {
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
};
  • a 占 2 位,还剩 30 位。
  • b 占 5 位,还剩 25 位。
  • c 占 10 位,还剩 15 位。
  • d 占 30 位,分配失败,因此需要新开辟一个 32 位空间。

最终,整个结构体的大小是 8 字节(2 个 int 类型的大小)。

位段的跨平台问题

位段在跨平台时存在许多不确定性:

  1. 位段的符号问题int 位段是有符号还是无符号,在不同平台上可能不一致。
  2. 位段的排列顺序:位段成员在内存中的排列顺序(从左向右或从右向左)并没有统一标准。
  3. 内存浪费:位段成员的排列可能导致内存浪费,尤其是在成员大小无法完全填充对齐单元时。

建议: 尽量避免使用位段,尤其是在需要移植到不同平台时。


;