C 语言结构体:从入门到进阶的全面解析

news2025/2/26 11:52:28

一、结构体类型的声明

1.1 结构的声明

结构体是一种自定义的数据类型,允许将不同类型的数据组合成一个整体。声明语法如下:

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

示例:

struct Student {
    char name[20];
    int age;
    float score;
};

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

  • 创建方式
struct Student stu1;  // 先声明类型,后定义变量
struct { int x; int y; } point;  // 匿名结构体

初始化方式

struct Student stu2 = {"张三", 18, 90.5};
struct Student stu3 = { .age = 20, .name = "李四",.score = 85.0};  // C99指定初始化器
   
   
    

1.3 结构的特殊声明

匿名结构体可以直接定义变量,但无法重复使用:

struct {
    int a;
    char b;
} anon_var;

1.4 结构的自引用

用于构建链表等复杂结构:

struct Node {
    int data;
    struct Node* next;  // 正确方式
};

错误示例

struct Node {
    int data;
    Node* next;  // 错误:未定义类型Node
};

二、结构体内存对齐

2.1 对齐规则

⾸先得掌握结构体的这四个对⻬的规则:
规则 1:结构体的第一个成员对齐到和结构体变量起始位置偏移量为 0 的地址处

可以把结构体想象成一个大箱子,这个箱子从地址 0 开始摆放。结构体的第一个成员就像是第一个要放进箱子的物品,它会直接放在箱子的最开始位置,也就是地址 0 处,不需要考虑其他对齐因素。

struct Example1 {
    char c;  // 第一个成员,直接放在起始地址 0 处
    int i;
};
规则 2:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值
 
  • 对齐数的计算:对于结构体中除第一个成员之外的其他成员,需要先计算它们的对齐数。对齐数是由编译器默认的对齐数和成员自身大小这两个值中较小的那个决定的。不同的编译器默认对齐数可能不同,例如在 Visual Studio(VS)中默认值为 8,而在 Linux 的 gcc 编译器中没有默认对齐数,此时对齐数就是成员自身的大小。
  • 成员放置位置:计算出对齐数后,该成员就要放在这个对齐数的整数倍的地址处。如果当前地址不是对齐数的整数倍,就需要在前面填充一些字节,直到达到对齐数的整数倍。

示例

#include <stdio.h>

struct Example2 {
    char c;  // 第一个成员,放在地址 0 处
    int i;   // 成员大小为 4 字节,VS 中默认对齐数为 8,对齐数取较小值 4
             // 由于 char 占 1 字节,当前地址 1 不是 4 的整数倍,需要填充 3 字节
             // 所以 i 从地址 4 开始存放
};

int main() {
    printf("Size of Example2: %zu\n", sizeof(struct Example2));
    return 0;
}

在这个例子中,char 类型的 c 放在地址 0 处,int 类型的 i 对齐数为 4,因为当前地址 1 不是 4 的整数倍,所以要在 c 后面填充 3 个字节,i 从地址 4 开始存放。

规则 3:结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍

结构体的所有成员都按照前面的规则放置好后,结构体整体占用的内存大小并不是成员实际占用字节数的简单相加,而是要保证结构体的总大小是所有成员对齐数中最大那个对齐数的整数倍。如果实际占用的内存大小不是最大对齐数的整数倍,就需要在结构体的末尾填充一些字节,使其达到最大对齐数的整数倍。

示例

#include <stdio.h>

struct Example3 {
    char c;  // 对齐数为 1,放在地址 0 处
    short s; // 对齐数为 2,由于 c 占 1 字节,当前地址 1 不是 2 的整数倍,填充 1 字节,s 从地址 2 开始存放
    int i;   // 对齐数为 4,s 占 2 字节,当前地址 4 是 4 的整数倍,i 从地址 4 开始存放
};

int main() {
    printf("Size of Example3: %zu\n", sizeof(struct Example3));
    return 0;
}

在这个结构体中,char 的对齐数是 1,short 的对齐数是 2,int 的对齐数是 4,最大对齐数是 4。成员实际占用的字节数是 1(c) + 1(填充)+ 2(s) + 4(i) = 8 字节,8 是 4 的整数倍,所以结构体的总大小就是 8 字节。

规则 4:如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍

当结构体中嵌套了另一个结构体时,嵌套的结构体就像一个 “小箱子”,这个 “小箱子” 要放在合适的位置。它的起始地址要对齐到它自己内部成员中最大对齐数的整数倍处。计算整个结构体的总大小的时候,要把嵌套结构体内部成员的对齐数也考虑进来,最终结构体的整体大小要保证是所有最大对齐数(包括嵌套结构体成员的对齐数)的整数倍。

示例

#include <stdio.h>

struct Inner {
    char c;  // 对齐数为 1
    int i;   // 对齐数为 4,最大对齐数是 4
};

struct Example4 {
    char c1;         // 对齐数为 1,放在地址 0 处
    struct Inner in; // 嵌套结构体,最大对齐数是 4,当前地址 1 不是 4 的整数倍,填充 3 字节,in 从地址 4 开始存放
    short s;         // 对齐数为 2,in 占 8 字节(1 + 3 填充 + 4),当前地址 12 是 2 的整数倍,s 从地址 12 开始存放
};

int main() {
    printf("Size of Example4: %zu\n", sizeof(struct Example4));
    return 0;
}

在这个例子中,嵌套结构体 Inner 的最大对齐数是 4,所以 Inner 要对齐到 4 的整数倍地址处。整个结构体 Example4 的所有成员最大对齐数也是 4,最终结构体的总大小要保证是 4 的整数倍。经过计算和填充,结构体 Example4 的总大小是 14 字节(1 + 3 填充 + 8 + 2),刚好是 4 的整数倍,所以最终大小就是 14 字节。

2.2 为什么存在内存对齐?

1. 硬件效率:按块读取内存

现代 CPU 读取内存是一块一块来的,就像从书架上拿书,一次拿一摞(比如 4 本或 8 本)。要是数据没对齐,就像书没摆好,CPU 得多次伸手去拿,才能凑齐想要的数据,效率低。而数据对齐后,CPU 一次就能拿到完整的数据,速度快多了。

2. 兼容性:不同平台要求不同

不同的硬件平台,就像不同的书架,对书的摆放要求不一样。有些书架要求书必须按顺序一本本对齐放,要是放乱了,就拿不出来或者拿错。所以程序在不同硬件上跑,数据对齐得符合人家的要求,不然就可能出错。

3. 性能优化:减少访问次数

内存访问就像去书架找书,次数多了很麻烦。合理对齐数据,能让 CPU 一次拿到更多有用的数据,就像一次能拿一摞需要的书,不用来回跑好几趟,程序自然就跑得快啦。

2.3 修改默认对齐数

使用#pragma pack()指令:

#pragma pack(2)  // 设置对齐数为2
struct Test {
    char a;  // 1字节,起始地址0
    int b;   // 4字节 → 按2对齐,起始地址2
};           // 总大小:6字节(2+4=6)
#pragma pack() // 恢复默认对齐

三、结构体传参

  • 值传递
void print_stu(struct Student s) { ... }  // 效率低,复制整个结构体
  • 指针(地址)传递

void print_stu(const struct Student* s) { ... }  // 推荐方式

注意:指针传递需确保指针有效,避免野指针问题


四、结构体实现位段

4.1 什么是位段

在 C 语言里,有些数据所需存储空间极小,像表示开关状态(开或关),1 位二进制数(0 或 1)就足够;表示 8 种不同等级,用 3 位二进制数(能表示 0 - 7)就行。但基本数据类型如 char 占 1 字节(8 位),int 一般占 4 字节(32 位),用它们存储这类数据会浪费内存。

位段可解决此问题,它允许在结构体中精准指定每个成员使用的二进制位数,从而高效利用内存。示例如下:

#include <stdio.h>

struct MyFlags {
    unsigned int is_open : 1;  // 1 位表示开关状态
    unsigned int level : 3;    // 3 位表示 8 种等级
    unsigned int mode : 2;     // 2 位表示 4 种模式
};

int main() {
    struct MyFlags flags;
    flags.is_open = 1;
    flags.level = 5;
    flags.mode = 2;
    printf("is_open: %u\n", flags.is_open);
    printf("level: %u\n", flags.level);
    printf("mode: %u\n", flags.mode);
    return 0;
}

4.2 位段的内存分配

按类型分配

位段通常按 intunsigned int 或 signed int 类型分配内存,常见系统中 int 占 4 字节(32 位)。

依次存放规则

位段成员在内存中依次存放。若前面位段成员占用位数与当前位段成员要占用的位数之和,未超过当前分配的内存块(通常 32 位),则当前位段成员接着分配;若超过,则从下一个内存块开始分配。

未命名位段用途

未命名位段可作 “占位符”,调整后续位段成员的起始位置,灵活控制内存使用。示例:

#include <stdio.h>

struct BitFieldExample {
    unsigned int part1 : 5; 
    unsigned int part2 : 3; 
    unsigned int : 2;       
    unsigned int part3 : 4; 
};

int main() {
    printf("Size of BitFieldExample: %zu bytes\n", sizeof(struct BitFieldExample));
    return 0;
}

该结构体中,各成员位段总和未超 32 位,所以通常占 4 字节。

4.3 位段的跨平台问题

存储顺序差异

不同编译器处理位段时,位段在内存中的存储顺序可能不同,有的从低位开始分配,有的从高位开始,这会使相同代码在不同编译器下,位段成员存储位置有差异。

长度限制不同

不同平台和编译器对位段成员长度限制有别,部分编译器不允许位段成员位数超特定值。

负数处理不同

有符号位段在不同编译器处理负数的方式可能不同,导致代码在不同平台运行结果有差异。

4.4 位段的应用

网络协议解析

网络协议头部包含众多标志位和状态信息,用很少位数就能表示,位段可方便解析处理这些头部信息。例如 IPv4 协议头部的 4 位版本号和 4 位首部长度:

#include <stdio.h>

struct IPv4Header {
    unsigned int version : 4;     
    unsigned int header_length : 4; 
};

int main() {
    unsigned char header_data = 0x45; 
    struct IPv4Header *ip_header = (struct IPv4Header *)&header_data;
    printf("Version: %u\n", ip_header->version);
    printf("Header Length: %u\n", ip_header->header_length);
    return 0;
}

设备寄存器控制

嵌入式系统开发中,常与硬件设备寄存器交互,寄存器很多位有特定含义,用于控制设备功能,位段可方便操作寄存器各位。

节省内存

在嵌入式系统或资源受限设备中,位段能精确控制数据占用位数,节省大量内存,提升程序运行效率。

4.5 位段使用的注意事项

类型要求

位段类型必须是 intunsigned int 或 signed int,其他类型(如 charfloat)不可用。

不能取地址

不能使用 & 运算符获取位段成员地址,因为位段成员可能只占字节部分位,无独立内存地址。

跨平台兼容性

不同编译器和平台处理位段有差异,编写代码时要留意跨平台兼容性,充分测试。

避免位数溢出

给位段成员赋值时,不能超出其表示范围,否则会溢出,导致不可预期结果。例如:

#include <stdio.h>

struct WrongUsage {
    unsigned int small_num : 2;
};

int main() {
    struct WrongUsage wu;
    wu.small_num = 5;  // 2 位位段只能表示 0 - 3,赋值 5 会溢出
    printf("small_num: %u\n", wu.small_num);
    return 0;
}


五、扩展知识

5.1 柔性数组成员

用于动态数组:

struct Array {
    int len;
    int data[];  // 柔性数组成员
};
// 动态分配:
struct Array* arr = malloc(sizeof(struct Array) + 10*sizeof(int));

5.2 结构体与枚举的结合

typedef enum { MALE, FEMALE } Gender;

struct Person {
    char name[20];
    Gender sex;
};

5.3 结构体常用操作

  • 结构体比较:逐个成员比较
  • 结构体复制:使用memcpy()或直接赋值(C99 支持)
  • 结构体打印:自定义格式化输出函数


六、常见问题解答

  1. 结构体可以包含自身类型吗?

    • 不能直接包含,但可以包含指针(自引用)
  2. 内存对齐会浪费空间吗?

    • 是的,但这是空间与时间的权衡,现代编译器会优化
  3. 位段能跨字节边界吗?

    • 取决于编译器,可能导致不可移植性

七、总结

结构体是 C 语言中最重要的复合数据类型之一,掌握内存对齐规则和位段技术能显著提升程序性能。在实际开发中,应根据场景选择合适的结构体设计方式,同时注意跨平台兼容性问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2306341.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【前端基础】Day 1 HTML

总结&#xff1a; 1. Web标准的构成 2. 基本标签 目录 1. Web标准的构成 2. 基本标签 2.1快捷键 2.2.1标题标签 2.2.2段落和换行标签 2.2.3文本格式化标签 2.2.4div和span标签 2.3.1 图像标签和路径 2.3.2路径 2.3.3超链接标签 2.4注释标签 2.5特殊字符 1. Web标准…

【前端基础】Day 2 HTML

目录 1.表格标签 2.列表标签 3.表单标签 4.综合案例 5.查阅文档 1.表格标签 <body><table align"center" border"1" cellpadding"0" cellspacing"0" width"500" height"100"><thead> …

若依前后端分离框架修改3.8.9版本(重点在安全框架讲解与微信小程序登录集成)

若依模板改造&#xff08;3.8.9&#xff09; 1、基础改造 下载代码 从[RuoYi-Vue: &#x1f389; 基于SpringBoot&#xff0c;Spring Security&#xff0c;JWT&#xff0c;Vue & Element 的前后端分离权限管理系统&#xff0c;同时提供了 Vue3 的版本](https://gitee.co…

selenium爬取苏宁易购平台某产品的评论

目录 selenium的介绍 1、 selenium是什么&#xff1f; 2、selenium的工作原理 3、如何使用selenium&#xff1f; webdriver浏览器驱动设置 关键步骤 代码 运行结果 注意事项 selenium的介绍 1、 selenium是什么&#xff1f; 用于Web应用程序测试的工具。可以驱动浏览…

kubernetes-完美下载

话不多说&#xff0c;直接开始从0搭建k8s集群 环境&#xff1a;centous7.9 2核 20G k8s-master 192.168.37.20 k8s-node1 192.168.37.21 k8s-node2 192.168.37.22 一&#xff1a;设置主机名 #设置主机名 hostnamectl set-hostname k8s-master hostnamectl set-h…

【初阶数据结构】树和二叉树

目录 前言树的概念与结构树的概念树的相关概念树的表示 二叉树的概念及结构二叉树的概念几种特殊的二叉树1.满二叉树2.完全二叉树 二叉树的性质二叉树的存储结构1、顺序存储2、链式存储 前言 前面我们学习了顺序表&#xff0c;单链表&#xff0c;栈和队列&#xff0c;它们在逻…

【中等】59.螺旋矩阵Ⅱ

题目描述 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;[[1,2,3],[8,9,4],[7,6,5]]示例 2&#xff1a; 输入&#xff1a;n…

Spring Boot + Vue 接入腾讯云人脸识别API(SDK版本3.1.830)

一、需求分析 这次是基于一个Spring Boot Vue的在线考试系统进行二次开发&#xff0c;添加人脸识别功能以防止学生替考。其他有对应场景的也可按需接入API&#xff0c;方法大同小异。 主要有以下两个步骤&#xff1a; 人脸录入&#xff1a;将某个角色&#xff08;如学生&…

JAVA中包装类和泛型 通配符

目录 1. 包装类 1.1 基本数据类型和对应的包装类 1.2 装箱和封箱 1.3 自动自动装箱和封箱 2. 什么是泛型 3. 引出泛型 3.1 语法 4. 泛型类的使⽤ 4.1 语法 4.2 ⽰例 4.3 类型推导(Type Inference) 5 泛型的上界 5.1 语法 6. 通配符 6.1 通配符解决什么问题 6.2…

Qt TCP服务端和客户端程序

1、服务端程序 利用QtCreator新建QMainWindow或QWidget工程&#xff0c;绘制UI如下所示。 mainwindow.h代码如下&#xff1a; #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QTcpServer> #include <QTcpSocket> #include &l…

level2Day5

Makefile make是工程管理器 先写了1个f1.c里面写了一个函数 然后f2.c里面也写了一个函数 还有一个头节点 又写了一个makefile的函数 输入make编译&#xff0c;但是我没装make需要装一下。 sudo apt install make 然后make&#xff0c; Makefile变量的使用 通过赋值&#xff…

minio作为K8S后端存储

docker部署minio mkdir -p /minio/datadocker run -d \-p 9000:9000 \-p 9001:9001 \--name minio \-v /minio/data:/data \-e "MINIO_ROOT_USERjbk" \-e "MINIO_ROOT_PASSWORDjbjbjb123" \quay.io/minio/minio server /data --console-address ":90…

redis小记

redis小记 下载redis sudo apt-get install redis-server redis基本命令 ubuntu16下的redis没有protected-mode属性&#xff0c;就算sudo启动&#xff0c;也不能往/var/spool/cron/crontabs写计划任务&#xff0c;感觉很安全 #连接到redis redis-cli -h 127.0.0.1 -p 6379 …

计算机视觉(opencv-python)入门之图像的读取,显示,与保存

在计算机视觉领域&#xff0c;Python的cv2库是一个不可或缺的工具&#xff0c;它提供了丰富的图像处理功能。作为OpenCV的Python接口&#xff0c;cv2使得图像处理的实现变得简单而高效。 示例图片 目录 opencv获取方式 图像基本知识 颜色空间 RGB HSV CV2常用图像处理方…

ActiveMQ之VirtualTopic

一句话总结&#xff1a; VirtualTopic是为了解决持久化模式下多消费端同时接收同一条消息的问题。 现实中多出现这样一个场景&#xff1a; 生产端产生了一笔订单&#xff0c;作为消息MessageOrder发了出去。 这笔订单既要入订单系统归档&#xff0c;又要入结算系统收款&#x…

UE5 Computer Shader学习笔记

首先这里是绑定.usf文件的路径&#xff0c;并声明是用声明着色器 上面就是对应的usf文件路径&#xff0c;在第一张图进行链接 Shader Frequency 的作用 Shader Frequency 是 Unreal Engine 中用于描述着色器类型和其执行阶段的分类。常见的 Shader Frequency 包括&#xff1a…

2.1部署logstash:9600

实验环境&#xff1a;关闭防火墙&#xff0c;完成java环境 yum -y install wget wget https://d6.injdk.cn/oraclejdk/8/jdk-8u341-linux-x64.rpm yum localinstall jdk-8u341-linux-x64.rpm -y java -version 1.安装logstash tar xf logstash-6.4.1.tar.gz -C /usr/local…

SQL笔记#集合运算

目录 一、表的加减法 1、什么是集合运算 2、表的加法——UNION 3、集合运算的注意事项 4、包含重复行的集合运算——ALL运算 5、选取表中公共部分——INTERSECT 6、记录的减法——EXCEPT 二、联结(以列为单位对表进行联结) 1、什么是联结(JOIN) 2、内联结——INSER…

多模态人物视频驱动技术回顾与业务应用

一种新的商品表现形态&#xff0c;内容几乎存在于手淘用户动线全流程&#xff0c;例如信息流种草内容、搜索消费决策内容、详情页种草内容等。通过低成本、高时效的AIGC内容生成能力&#xff0c;能够从供给端缓解内容生产成本高的问题&#xff0c;通过源源不断的低成本供给倒推…

多功能免费网络测速及问题诊断工具

​软件介绍 在日常网络使用中&#xff0c;网络问题常常难以即时察觉&#xff0c;很多时候&#xff0c;只有当视频卡顿、网页加载半天没反应&#xff0c;乃至无法连接部分服务时&#xff0c;我们才惊觉网络出状况了。 这里有一款免费工具&#xff0c;专为家庭、办公以及跨国网…