当前位置: 首页 > news >正文

[C]基础17.自定义类型:结构体

  • 博客主页:向不悔
  • 本篇专栏:[C]
  • 您的支持,是我的创作动力。

文章目录

  • 0、总结
  • 1、结构体类型的声明
    • 1.1 结构的声明
    • 1.2 结构体变量的创建和初始化
    • 1.3 结构体的特殊声明
    • 1.4 结构的自引用
    • 1.5 typedef与结构自引用
  • 2、结构体内存对齐
    • 2.1 对齐规则
    • 2.2 为什么存在内存对齐?
    • 2.3 修改默认对齐数
  • 3、结构体传参
  • 4、结构体实现位段
    • 4.1 什么是位段
    • 4.2 位段的内存分配
    • 4.3 位段的跨平台问题
    • 4.4 位段的应用场景
    • 4.5 位段使用的注意事项


0、总结

在这里插入图片描述

1、结构体类型的声明

结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

1.1 结构的声明

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

例如描述一个学生

struct Stu {char name[20];  // 名字int age;        // 年龄char sex[5];    // 性别char id[20];    // 学号
};  // 分号不能丢

1.2 结构体变量的创建和初始化

#include <stdio.h>
struct Stu
{char name[20];    //名字int age;          //年龄char sex[5];      //性别char id[20];      //学号
};int main()
{//按照结构体成员的顺序初始化struct Stu s = { "张三", 20, "男", "20250529001" };printf("name :%s\n", s.name);printf("age  :%d\n", s.age);printf("sex  :%s\n", s.sex);printf("id   :%s\n", s.id);printf("-----------------\n");//按照指定的顺序初始化struct Stu s2 = { .age = 18,.name = "lisi",.id = "20250530002",.sex = "女" };printf("name :%s\n", s2.name);printf("age  :%d\n", s2.age);printf("sex  :%s\n", s2.sex);printf("id   :%s\n", s2.id);return 0;
}
运行:
name :张三
age  :20
sex  :男
id   :20250529001
-----------------
name :lisi
age  :18
sex  :女
id   :20250530002

1.3 结构体的特殊声明

在声明结构的时候,可以不完全的声明(匿名结构体)。

// 匿名结构体类型
struct {int a;char b;float c;
} x;struct {int a;char b;float c;
} a[20], *p;

注意:编译器会把上面的两个声明当成完全不同的两个类型,所以下面的代码是非法的:

p = &x;  // 非法操作

思考:为什么非法?

  • 类型不同:即使两个匿名结构体成员完全相同(int a; char b; float c;),编译器仍将它们视为完全不同的类型。
  • 匿名特性:没有结构体标签(tag),无法声明相同类型。
  • 指针不兼容:p是指向类型B的指针,&x是类型A的地址,类型不匹配。

如果要解决,可以这样:

  • 1、添加标签

    struct Standard {  // 添加标签int a; char b; float c;
    };
    struct Standard x;
    struct Standard a[20], *p;
    p = &x; // 合法
    
  • 2、使用 typedef 统一类型

    typedef struct {  // 创建类型别名int a; char b; float c;
    } MyType;
    MyType x;
    MyType a[20], *p;
    p = &x; // 合法
    

警告:匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。

1.4 结构的自引用

结构自引用指在结构体内部包含指向自身类型结构体的指针成员,这是实现链式数据结构(如链表、树等)的基础。

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

struct Node {int data;struct Node next;  // 错误,会导致无限大小
};

这种写法是错误的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷大。

Node = int + Node= int + (int + Node)= int + (int + (int + Node))... // 无限循环

正确的自引用方式

struct Node {int data;struct Node* next;  // 使用指针
};

优势

  • 固定大小:指针大小固定(4/8字节),不引起递归问题

  • 内存高效:仅存储地址,不复制整个结构

1.5 typedef与结构自引用

错误用法分析:

typedef struct {     // 匿名结构体int data;Node* next;      // 错误:Node尚未定义
} Node;

问题原因:

  • 编译顺序:编译器先处理结构体定义

  • 未定义类型:在结构体内部使用Node*时,Node尚未被定义

解决方案:定义结构体不要使用匿名结构体。

  • 完整标签声明

    typedef struct Node {  // 明确结构体标签int data;struct Node* next; // 使用标签声明指针
    } Node;                // typedef别名
    
  • 分步声明

    struct Node;           // 前向声明typedef struct Node {int data;struct Node* next; // 使用前向声明
    } Node;
    

总结:

  • 结构自引用必须使用指针而非完整结构体

  • typedef自引用需配合结构体标签或前向声明

  • 匿名结构体不适合需要自引用的场景

  • 正确使用自引用可实现高效链式数据结构

2、结构体内存对齐

2.1 对齐规则

首成员规则:结构体的第一个成员对齐到偏移量为0的地址处

成员对齐规则:其他成员变量要对齐到"对齐数"的整数倍地址

  • 对齐数 = min(编译器默认对齐数, 成员自身大小)
  • VS编译器默认对齐数=8,Linux gcc无默认对齐数(对齐数=成员大小)

结构体大小规则:结构体总大小为最大对齐数(所有成员中对齐数的最大值)的整数倍

嵌套结构体规则

  • 嵌套的结构体成员对齐到其内部成员的最大对齐数的整数倍处
  • 整体结构体大小是所有最大对齐数(含嵌套结构体成员)的整数倍

练习1:struct S1

struct S1 {char c1;  // 大小1,对齐数min(8,1)=1int i;    // 大小4,对齐数min(8,4)=4 → 需对齐到4的倍数char c2;  // 大小1,对齐数1
};
// 总大小:12字节(最大对齐数4的倍数)
0    1   2   3   4   5   6   7   8   9   10  11
[c1] 空  空  空  [i] [i] [i] [i] [c2] 空  空  空

练习2:struct S2

struct S2 {char c1;  // 大小1,对齐数1char c2;  // 大小1,对齐数1int i;    // 大小4,对齐数4
};
// 总大小:8字节(最大对齐数4的倍数)
0   1   2   3   4   5   6   7
[c1][c2] 空  空  [i] [i] [i] [i]

练习3:struct S3

struct S3 {double d;  // 大小8,对齐数min(8,8)=8char c;    // 大小1,对齐数1int i;     // 大小4,对齐数4 → 需对齐到4的倍数
};
// 总大小:16字节(最大对齐数8的倍数)
0-7                         8   9   10  11   12-15
[d][d][d][d][d][d][d][d]   [c]  空  空   空   [i][i][i][i]

练习4:嵌套结构体

struct S4 {char c1;       // 大小1,对齐数1struct S3 s3;  // 大小16,对齐数8(S3中最大对齐数)double d;      // 大小8,对齐数8
};
// 总大小:32字节(最大对齐数8的倍数)
0    1-7          8-23           24-31
[c1] 空(对齐)    [s3:16字节]     [d][d][d][d][d][d][d][d]

练习5:混合字符串类型

struct MixedString {char *pname;    // 指针8字节,对齐数8char name[12];  // 字符数组,对齐数1int age;        // 4字节,对齐数4
};
// 总大小:32字节
0-7  : [pname]    // 指针(8)
8-19 : [name]    // char[12] (12字节)
20-23: [age]    // int(4)
24-31: 填充      // 总大小32(最大对齐数8的倍数)

练习6:数组+嵌套结构体

struct Inner {char c;     // 1字节,对齐数1double d;   // 8字节,对齐数8 → 最大对齐数8
};
// 总大小:16字节struct Outer {int arr[2];         // 元素大小4,对齐数4struct Inner in;    // 嵌套结构体,对齐数8short s;            // 2字节,对齐数2
};
// 总大小:32字节
0-7  : [arr]      // int[2] (4×2=8字节)
8-15 : [in.c]    // Inner.c
16-23: [in.d]   // Inner.d
24-25: [s]      // short(2)
26-31: 填充      // 总大小32(最大对齐数8的倍数)

2.2 为什么存在内存对齐?

两大核心原因:

平台兼容性

  • 不是所有硬件都能访问任意地址上的任意数据
  • 某些硬件平台只能在特定地址访问特定类型数据
  • 未对齐访问可能导致硬件异常

性能优化

  • 对齐的内存访问只需一次操作

  • 未对齐访问可能需要两次操作

  • 示例:读取8字节double类型

    对齐地址(8的倍数):一次读取完成
    未对齐地址:可能跨越两个内存块,需要两次读取+数据拼接
    

空间优化技巧

  • 让占用空间小的成员尽量集中在一起

  • 对比S1和S2:成员相同但大小不同,S2更优!

    struct S1 { char c1; int i; char c2; }; // 12字节
    struct S2 { char c1; char c2; int i; };  // 8字节
    

2.3 修改默认对齐数

使用 #pragma pack 指令

#include <stdio.h>#pragma pack(1) // 设置默认对齐数为1
struct S {char c1;  // 大小1,对齐数min(1,1)=1int i;    // 大小4,对齐数min(1,4)=1char c2;  // 大小1,对齐数1
};
#pragma pack() // 恢复默认对齐数int main() {printf("%d\n", sizeof(struct S)); // 输出:6return 0;
}

使用场景

  • 空间敏感场景:网络传输、嵌入式系统

  • 兼容特定硬件:需要1字节对齐的设备

  • 数据打包:文件存储格式定义

注意:修改对齐数可能导致性能下降,仅在必要时使用

3、结构体传参

#include <stdio.h>struct S {int data[1000]; // 4000字节数组int num;        // 4字节
};void print1(struct S s) {      // 传结构体值printf("%d\n", s.num);
}void print2(struct S* ps) {    // 传结构体地址printf("%d\n", ps->num);
}int main() {struct S s = { {1,2,3,4}, 1000 };print1(s);   // 传结构体print2(&s);  // 传地址return 0;
}
运行:
1000
1000

上面的print1print2函数哪个好些?

答案是:首选print2函数

原因:

  • 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
  • 如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。

总结:结构体传参的时候,要传结构体的地址。

4、结构体实现位段

4.1 什么是位段

位段(Bit-field)是C语言中一种特殊的数据结构,它允许我们将多个变量紧凑地存储在同一个字节或字中,从而有效地节省内存空间。

位段的声明方式与结构体类似,但有两点关键区别:

  • 位段成员必须是整型(如intunsigned intsigned int),在C99标准中也可以使用其他类型

  • 每个成员名后需要跟一个冒号和数字,表示该成员占用的位数

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

4.2 位段的内存分配

位段在内存中的分配方式有以下特点:

  • 空间按需分配,通常以4字节(int)或1字节(char)为单位开辟
  • 位段成员在内存中的排列顺序(从左到右或从右到左)取决于编译器和平台
  • 当一个位段无法容纳在剩余空间时,可能舍弃剩余位或利用下一单元,这由编译器决定
struct S {char a:3;char b:4;char c:5;char d:4;
};

在VS2013环境下,上述结构体可能的内存布局如下(假设从右向左分配):

  • 第一个字节:a(3位) + b(4位) + 1位剩余
  • 第二个字节:c(5位) + 3位剩余
  • 第三个字节:d(4位)

4.3 位段的跨平台问题

位段虽然节省空间,但存在严重的跨平台问题:

  • int位段可能被视为有符号数或无符号数,标准未定义

  • 位段最大位数不确定(16位机器最大16,32位机器最大32)

  • 成员在内存中的分配方向(从左到右或从右到左)未标准化

  • 当空间不足时,是舍弃剩余位还是利用下一单元,标准未定义

总结: 跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

4.4 位段的应用场景

位段特别适合用于需要精确控制数据位数的场景,如:

  • 网络协议:IP数据报头部包含许多只需要几位表示的字段

  • 硬件寄存器:与硬件寄存器位对应

  • 嵌入式系统:内存受限的环境下节省空间

4.5 位段使用的注意事项

  • 不能取地址:位段成员可能不位于字节起始位置(几个成员共有同一个字节),因此没有地址

  • 输入处理:不能直接用scanf读取位段成员,需要通过临时变量中转

  • 可移植性:注重可移植性的代码应避免使用位段

// 错误示例
scanf("%d", &sa._b);  // 错误,不能取位段地址// 正确做法
int b = 0;
scanf("%d", &b);
sa._b = b;

总结:

位段是一种强大的内存优化工具,能够将多个小范围值紧凑地存储在内存中,特别适合内存受限的环境或需要精确控制数据位数的场景。

然而,其跨平台问题使得它在需要可移植性的应用中受到限制。在使用位段时,开发者需要在节省空间和保证可移植性之间做出权衡。

相关文章:

  • MMdetection推理保存图片和预测标签脚本
  • API网关和API管理的区别
  • C++模板类深度解析与气象领域应用指南
  • 【计算机网络】第2章:应用层—应用层协议原理
  • 机器学习-线性回归基础
  • Emacs 折腾日记(二十六)——buffer与窗口管理
  • 接口自动化测试(六)
  • Windows10家庭版添加本地安全策略(需要联网)
  • 【JavaWeb】基本概念、web服务器、Tomcat、HTTP协议
  • 彻底卸载安装的虚拟机VMware Workstation软件
  • 「动态规划::状压DP」网格图递推 / AcWing 292|327(C++)
  • 什么是生成式人工智能?
  • 软考-系统架构设计师-第十六章 层次式架构设计理论与实践
  • PostgreSQL的聚集函数
  • PostgreSQL 修改表结构卡住不动
  • 使用grpc建立跨语言通讯
  • day31 5月29日
  • 【测试】设计测试⽤例方法
  • 尚硅谷redis7 74-85 redis集群分片之集群是什么
  • Java ThreadLocal 应用指南:从用户会话到数据库连接的线程安全实践
  • 展厅设计公司成都/长春seo公司哪家好
  • 深圳贸易网站开发/百度数据分析工具
  • 旅游景区网站建设规划/seo公司官网
  • 旅游网站wordpress/设计网站官网
  • 网站建设diy/百度竞价在哪里开户
  • wordpress文章位置/苏州seo免费咨询