C 语言结构体:从入门到进阶的全面解析
一、结构体类型的声明
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; // 对齐数为 1int 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字节,起始地址0int 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 位段的内存分配
按类型分配
位段通常按 int、unsigned 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 位段使用的注意事项
类型要求
位段类型必须是 int、unsigned int 或 signed int,其他类型(如 char、float)不可用。
不能取地址
不能使用 & 运算符获取位段成员地址,因为位段成员可能只占字节部分位,无独立内存地址。
跨平台兼容性
不同编译器和平台处理位段有差异,编写代码时要留意跨平台兼容性,充分测试。
避免位数溢出
给位段成员赋值时,不能超出其表示范围,否则会溢出,导致不可预期结果。例如:
#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 支持) - 结构体打印:自定义格式化输出函数
六、常见问题解答
-
结构体可以包含自身类型吗?
- 不能直接包含,但可以包含指针(自引用)
-
内存对齐会浪费空间吗?
- 是的,但这是空间与时间的权衡,现代编译器会优化
-
位段能跨字节边界吗?
- 取决于编译器,可能导致不可移植性
七、总结
结构体是 C 语言中最重要的复合数据类型之一,掌握内存对齐规则和位段技术能显著提升程序性能。在实际开发中,应根据场景选择合适的结构体设计方式,同时注意跨平台兼容性问题。
相关文章:
C 语言结构体:从入门到进阶的全面解析
一、结构体类型的声明 1.1 结构的声明 结构体是一种自定义的数据类型,允许将不同类型的数据组合成一个整体。声明语法如下: struct 结构体名 {数据类型 成员1;数据类型 成员2;// ... }; 示例: struct Student {char name[20];int age;fl…...
交换机与路由器连接方式
交换机和路由器连接的三种主要方式如下: 一、直连连接 这是最简单直接的连接方式。通过一根网线将交换机的一个端口与路由器的一个LAN端口相连。这种连接方式适用于小型网络,其中交换机负责局域网内部的数据交换,而路由器则负责将内部网络连接…...
自适应增强技术
1. 传统图像处理中的自适应增强(如CLAHE) 难度:⭐容易 实现方式:调用成熟的库(如OpenCV)函数即可完成。 示例代码(CLAHE增强): <PYTHON> import cv2# 输入灰度或彩…...
【前端基础】Day 1 HTML
总结: 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> …...
Docker run --add-host参数解析(在容器启动时向/etc/hosts文件中添加自定义的主机名与IP映射)(适用于临时调试或测试)
文章目录 Docker run --add-host 参数解析一、参数概述二、工作原理三、应用场景1. **开发与调试**2. **环境隔离**3. **跨网络访问** 四、使用示例示例 1:单个自定义映射示例 2:多个映射同时使用 五、注意事项六、总结 Docker run --add-host 参数解析 …...
电商网站如何解决高并发问题
电商网站如何解决高并发问题?当下电商行业蓬勃发展,电商网站面临的用户访问量和高并发问题日益严峻。在电商大促、节日促销等关键时期,如何确保网站稳定运行,提升用户体验,成为了电商企业亟需解决的问题。小编推荐大家…...
MySQL 入门“鸡”础
一、Win10 与Ubuntu安装 以下是一篇针对 Ubuntu 安装 MySQL 的过程中写的示例: --- # Ubuntu 安装 MySQL 详细指南 在本教程中,我们将向您展示如何在 Ubuntu 上安装 MySQL,并完成基本的安全配置。以下是具体步骤: # 1. 安装 …...
若依前后端分离框架修改3.8.9版本(重点在安全框架讲解与微信小程序登录集成)
若依模板改造(3.8.9) 1、基础改造 下载代码 从[RuoYi-Vue: 🎉 基于SpringBoot,Spring Security,JWT,Vue & Element 的前后端分离权限管理系统,同时提供了 Vue3 的版本](https://gitee.co…...
selenium爬取苏宁易购平台某产品的评论
目录 selenium的介绍 1、 selenium是什么? 2、selenium的工作原理 3、如何使用selenium? webdriver浏览器驱动设置 关键步骤 代码 运行结果 注意事项 selenium的介绍 1、 selenium是什么? 用于Web应用程序测试的工具。可以驱动浏览…...
kubernetes-完美下载
话不多说,直接开始从0搭建k8s集群 环境:centous7.9 2核 20G k8s-master 192.168.37.20 k8s-node1 192.168.37.21 k8s-node2 192.168.37.22 一:设置主机名 #设置主机名 hostnamectl set-hostname k8s-master hostnamectl set-h…...
PostgreSQL 常用函数
PostgreSQL 常用函数 在数据库管理系统中,函数是执行特定任务的基本构建块。PostgreSQL 是一个功能强大的开源关系数据库管理系统,提供了丰富的内置函数,这些函数极大地增强了数据库操作的能力。以下是一些在 PostgreSQL 中常用的函数&#…...
【初阶数据结构】树和二叉树
目录 前言树的概念与结构树的概念树的相关概念树的表示 二叉树的概念及结构二叉树的概念几种特殊的二叉树1.满二叉树2.完全二叉树 二叉树的性质二叉树的存储结构1、顺序存储2、链式存储 前言 前面我们学习了顺序表,单链表,栈和队列,它们在逻…...
【中等】59.螺旋矩阵Ⅱ
题目描述 给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1: 输入:n 3 输出:[[1,2,3],[8,9,4],[7,6,5]]示例 2: 输入:n…...
Spring Boot + Vue 接入腾讯云人脸识别API(SDK版本3.1.830)
一、需求分析 这次是基于一个Spring Boot Vue的在线考试系统进行二次开发,添加人脸识别功能以防止学生替考。其他有对应场景的也可按需接入API,方法大同小异。 主要有以下两个步骤: 人脸录入:将某个角色(如学生&…...
测试工程师玩转DeepSeek之Prompt
以下是测试工程师使用DeepSeek的必知必会提示词指南,分为核心场景和高效技巧两大维度: 一、基础操作提示模板 1. 测试用例生成 "作为[金融系统/物联网设备/云服务]测试专家,请为[具体功能模块]设计测试用例,要求࿱…...
虚中断理解
虚中断(Virtual Interrupt)是指在计算机系统中,特别是在虚拟化环境下,虚拟机或虚拟操作系统中使用的一种中断机制。它允许虚拟机监控程序(Hypervisor)或虚拟化管理程序在虚拟机之间进行中断处理和资源管理。…...
PC端-发票真伪查验系统-Node.js全国发票查询接口
在现代企业的财务管理中,发票真伪的验证至关重要。随着电子发票的普及,假发票问题日益严峻,如何高效、准确的对发票进行真伪查验,已经成为各类企业在日常运营中必须解决的关键问题。翔云发票查验接口做企业财务管理、税务合规的好…...
给Python加入自己的函数
在日常研究中,我们有时候会写一些Python没有的,但是很多个脚本都需要用的函数,反复的复制函数太过麻烦,我们可以进行一些简单的操作来变成一个可以直接import的函数 1. 首先我们新建一个.py文件,把我们的函数放进去&a…...
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工程,绘制UI如下所示。 mainwindow.h代码如下: #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QTcpServer> #include <QTcpSocket> #include &l…...
level2Day5
Makefile make是工程管理器 先写了1个f1.c里面写了一个函数 然后f2.c里面也写了一个函数 还有一个头节点 又写了一个makefile的函数 输入make编译,但是我没装make需要装一下。 sudo apt install make 然后make, Makefile变量的使用 通过赋值ÿ…...
青少年学习编程如何平衡使用DeepSeek与独立思考
前言 对于正在学习编程的青少年来说,DeepSeek生成代码的功能是一把双刃剑。如果合理使用,它可以成为青少年学习编程的有力助手;但如果过度依赖,可能会阻碍他们的思维发展和能力提升。关键在于引导青少年正确看待工具的作用&#…...
MySQL 8.0 Enterprise Backup (MEB) 备份与恢复实践指南
一、MEB 核心价值与特性 1.1 产品定位 MySQL Enterprise Backup (MEB) 是Oracle官方推出的企业级物理热备份工具,专为MySQL 8.0设计,支持InnoDB/XtraDB引擎的在线备份,同时兼容MyISAM表的锁定备份。 1.2 核心优势 零停机热备份࿱…...
UE5从入门到精通之多人游戏编程常用函数
文章目录 前言一、权限与身份判断函数1. 服务器/客户端判断2. 网络角色判断二、网络同步与复制函数1. 变量同步2. RPC调用三、连接与会话管理函数1. 玩家连接控制2. 网络模式判断四、实用工具函数前言 UE5给我们提供了非常强大的多人网路系统,让我们可以很方便的开发多人游戏…...
[Web 安全] 反序列化漏洞 - 学习笔记
关注这个专栏的其他相关笔记:[Web 安全] Web 安全攻防 - 学习手册-CSDN博客 0x01:反序列化漏洞 — 漏洞介绍 反序列化漏洞是一种常见的安全漏洞,主要出现在应用程序将 序列化数据 重新转换为对象(即反序列化)的过程中…...
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…...
Leetcode2717:半有序排列
题目描述: 给你一个下标从 0 开始、长度为 n 的整数排列 nums 。 如果排列的第一个数字等于 1 且最后一个数字等于 n ,则称其为 半有序排列 。你可以执行多次下述操作,直到将 nums 变成一个 半有序排列 : 选择 nums 中相邻的两…...
redis小记
redis小记 下载redis sudo apt-get install redis-server redis基本命令 ubuntu16下的redis没有protected-mode属性,就算sudo启动,也不能往/var/spool/cron/crontabs写计划任务,感觉很安全 #连接到redis redis-cli -h 127.0.0.1 -p 6379 …...
C/C++基础知识复习(47)
1) 接口继承与实现继承的区别 接口继承 接口继承意味着定义一个类,它只声明一组方法(通常是纯虚函数),但是不提供任何实现。继承这个接口的子类必须实现这些方法。接口继承的主要目的是规范化行为。 C 例子: 在 C 中…...
