深入理解C语言结构体

news2025/1/12 4:47:56

目录

引言

一. 结构体的基本概念

1.结构体的声明

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

3. 结构体成员访问操作符

4.结构体的特殊声明

1. 匿名结构体

2. 嵌套结构体

3.结构体自引用

4. typedef 声明

二、结构体内存对⻬

1.对⻬规则

2.为什么存在内存对⻬?

3.修改默认对齐数

三、结构体传参

1.按值传递和按指针传递对比

四、结构体实现位段  

1.位段的定义

 2.位段的内存分配

3.注意事项

总结


引言

在C语言中,结构体(struct)是一种强大的数据组织工具,它允许你将不同类型的数据组合成一个单一的实体。无论是在处理复杂数据、设计数据模型还是进行内存优化,结构体都能帮助你更好地管理和组织数据。在本文中,我们将深入探讨C语言中的结构体。

一. 结构体的基本概念

什么是结构体?
结构体是一种用户自定义的数据类型,它允许我们将逻辑上相关的数据组合在一起。每个数据项称为结构体的成员。结构体的成员可以是基本数据类型(如int、float、char等),也可以是其他复合数据类型(如数组、指针、甚至其他结构体)。

1.结构体的声明

在C语言中,结构体的声明用于定义新的数据类型,这种数据类型由多个不同的数据成员组成。声明结构体的基本语法如下:

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

示例:

#include <stdio.h>

// 声明一个结构体类型Student
struct Student {
   char name[20];//名字
   int age;//年龄
   char sex[5];//性别
   char id[20];//学号
};

在上面的代码中,Student是一个命名结构体,可以用这个类型名称创建多个结构体变量,而point是一个匿名结构体,没有显式的名称,以此无法无法使用这个结构体来创建其他的变量。

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

声明结构体类型后,你可以创建结构体变量并对其进行初始化。结构体变量可以是结构体类型的实例,你可以在声明时进行初始化,也可以在运行时赋值。

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

	//按照指定的顺序初始化
	struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .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; 
} 

运行结果: 

3. 结构体成员访问操作符

C语言提供了两种操作符来访问结构体的成员:
点操作符(.):用于通过结构体变量访问成员。
箭头操作符(->):用于通过结构体指针访问成员。

示例:

#include <stdio.h>
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
int main()
{
	struct Stu s = { "张三", 20, "男", "20230818001" };
	struct Stu* ptr = &s;
	printf("name: %s\n", ptr->name);
	printf("age : %d\n", ptr->age);
	printf("sex : %s\n", ptr->sex);
	printf("id : %s\n", ptr->id);
	return 0; 
} 

 运行结果:

4.结构体的特殊声明

1. 匿名结构体

当你定义一个匿名结构体时,你只能在定义它的同时创建一个变量。这个结构体没有名字,因此无法在其他地方使用这个结构体来创建新的变量。

struct {
    int x;
    int y;
} point;

这里point是一个结构体变量,而结构体本身没有名字。

2. 嵌套结构体

嵌套结构体就是在结构体内部定义另一个结构体。结构体可以嵌套其他结构体,包括匿名结构体。

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

struct Person {
    char name[50];
    struct Date birthday; // 嵌套结构体
    float height;
};

在这个例子中,Person 结构体包含了 Date 结构体作为其一个成员。

3.结构体自引用

结构体自引用是指结构体中的一个或多个成员是指向相同结构体类型的指针。

struct Node {
    int value;
    struct Node* next; // 自引用:指向相同结构体类型的指针
};

在这个例子中,Node 结构体包含一个名为 next 的指针,它指向另一个 Node 结构体实例。

4. typedef 声明

使用 typedef 关键字可以为结构体定义一个新的类型名,使结构体声明更加简洁。

typedef struct {
    char* name;
    int age;
} Person;
Person p1,p2;//创建两个结构体变量

在这个例子中,Person成为了struct { char* name; int age; }这个结构体类型的别名,可以用Person来创建多个结构体变量,如Person p1,p2;。

二、结构体内存对⻬

什么是内存对齐?

内存对齐是指将数据存储在内存中的特定地址上,使得数据的起始地址满足某种对齐要求。对齐的要求通常与数据类型的大小有关。例如,4字节的整数通常要求存储在4的倍数的地址上。

1.对⻬规则

⾸先得掌握结构体的对⻬规则:
1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值
- VS 中默认的值为 8
- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的
整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

示例:

#include <stdio.h>

struct S1
{
    char c1;//占1字节
    int i;//占4字节
    char c2;//占1字节
};

int main()
{
    printf("%d\n", sizeof(struct S1));//结果是12
    return 0;
}

内存分布: 

 

2.为什么存在内存对⻬?

1. 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到
让占⽤空间⼩的成员尽量集中在⼀起
#include <stdio.h>
struct S1
{
	char c1;//占1字节
	int i;//占4字节
	char c2;//占1字节
};
struct S2//s2中占用空间小的成员集中在了一起
{
	char c1;//占1字节
	char c2;//占1字节
	int i;//占4字节
};
int main()
{
	printf("Size of S1:%d\n", sizeof(struct S1));
	printf("Size of S2:%d\n", sizeof(struct S2));
}
S1 S2 类型的成员⼀模⼀样,但是 S1 S2 所占空间的⼤⼩有了⼀些区别:

3.修改默认对齐数

#pragma 这个预处理指令,可以改变编译器的默认对⻬数。

#include <stdio.h>

#pragma pack(1) // 设置对齐数为1字节
struct MyStruct {
	char a; // 占1字节
	int b; // 占4字节
	double c;// 占8字节
};
#pragma pack()// 恢复默认对齐方式

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

运行结果:

#pragma pack(1)的效果仅限于它和随后的#pragma pack()之间的代码。一旦执行到#pragma pack(),对齐数将恢复到编译器的默认设置,但这不会改变MyStruct的定义,因为MyStruct是在#pragma pack(1)的作用下定义的。

所以,MyStruct的大小计算如下:

char a; 占用1字节
int b; 由于对齐数为1,所以紧接着char a后面,占用4字节
double c; 由于对齐数为1,所以紧接着int b后面,占用8字节
因此,MyStruct的总大小是1 + 4 + 8 = 13字节。这里没有额外的填充字节,因为对齐数被设置为1,这意味着结构体中的成员是紧挨着存放的,没有额外的填充字节。

三、结构体传参

1.按值传递和按指针传递对比

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

上⾯的 print1 和 print2 函数哪个好些?

答案是:⾸选print2函数。

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

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

四、结构体实现位段  

1.位段的定义

位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以
选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字。

位段在结构体中的定义方式如下:

struct bit_field_struct {
    type member_name : width;
};

type 是位段的数据类型,通常是 unsigned int 或 int。
member_name 是位段的名称。
width 是位段的宽度,表示该位段所占的位数。

 2.位段的内存分配

#include<stdio.h>
struct Flags {
    unsigned int flag1 : 1; // 占用1位
    unsigned int flag2 : 1; // 占用1位
    unsigned int flag3 : 1; // 占用1位
    unsigned int flag4 : 1; // 占用1位
    unsigned int : 0;       // 用于对齐
    unsigned int value : 4; // 占用4位
};

int main() {
    struct Flags f;
    f.flag1 = 1; // 设置flag1
    f.flag2 = 0; // 清除flag2
    f.flag3 = 1; // 设置flag3
    f.value = 10; // 设置value为10(二进制1010)
    printf("Flag1: %d\n", f.flag1);
    printf("Flag2: %d\n", f.flag2);
    printf("Flag3: %d\n", f.flag3);
    printf("Value: %d\n", f.value);
    printf("Size of Flags:%d", sizeof(struct Flags));
    return 0;
}

运行结果:


在这个例子中,我们定义了一个名为Flags的结构体,它包含四个标志位和一个4位的值。每个标志位占用1位,而value占用4位。结构体总大小为8位。

3.注意事项

位段类型:位段的类型必须是int、unsigned int或signed int。
位段宽度:位段的宽度必须是一个非负整数常量表达式。
位段对齐:位段成员可能会跨越其类型的自然边界,这取决于具体的编译器实现。
未命名的位段:可以使用未命名的位段(如上面例子中的unsigned int : 0;)来强制下一个位段从下一个存储单元开始,这有助于对齐。
访问位段:可以使用结构体变量名和点操作符来访问位段成员,就像访问普通结构体成员一样。
位段的大小:结构体中位段的总大小可能比所有位段宽度之和要大,因为编译器可能为了对齐而添加填充位。
位段是一种节省内存的有效方式,特别是在嵌入式系统或需要大量布尔标志的情况下。然而,由于它们的实现细节和可移植性问题,使用位段时应谨慎。

总结

通过对C语言结构体的详细探讨,我们了解了结构体的声明、创建和初始化、成员访问、匿名结构体的使用、结构体自引用、内存对齐、结构体传参以及结构体实现位段。这些知识可以帮助你在C语言编程中更高效地组织和管理数据,编写出更清晰、更高效的代码。掌握这些概念对于任何C语言开发者都是必不可少的。如果你有任何问题或进一步的讨论,请在评论区留言,我们一起探讨!

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

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

相关文章

ffmpeg命令-Windows下常用最全

查询命令 参数 说明 -version 显示版本。 -formats 显示可用的格式&#xff08;包括设备&#xff09;。 -demuxers 显示可用的demuxers。 -muxers 显示可用的muxers。 -devices 显示可用的设备。 -codecs 显示libavcodec已知的所有编解码器。 -decoders 显示可用…

基于SpringBoot+Vue的小区物业管理系统(带1w+文档)

基于SpringBootVue的小区物业管理系统(带1w文档) 基于SpringBootVue的小区物业管理系统(带1w文档) 小区物业管理系统采用B/S(Browser/Server)架构和MVC模型进行设计开发。在B/S架构下&#xff0c;用户在浏览器端进行使用&#xff0c;主要工作通过服务器端进行实现&#xff0c;用…

电脑缺少dll文件怎么解决?10款dll修复工具大盘点,赶紧收藏起来!

电脑缺少dll文件怎么解决&#xff1f;DLL&#xff08;动态链接库&#xff09;是一种重要文件&#xff0c;包含了一系列指令&#xff0c;用于运行几乎所有 Win10、Win8和 Win7的程序。如果Windows 操作系统中缺少DLL文件&#xff0c;您可能会无法启动所需的程序或应用。在 Win10…

【AndroidStudio】修改app名称、版本号、图标

文章目录 1. 修改app名称(AndroidManifest.xml-app_name字段)2. 修改app版本号和版本名称3. 修改app图标4. 修改app启动过渡图片 1. 修改app名称(AndroidManifest.xml-app_name字段) 2. 修改app版本号和版本名称 通常是app目录下的build.gradle文件找到“versionCode”和“ver…

基于域名+基于ip+基于端口的虚拟主机+上线商务系统

一、回顾 1.jdk环境 tomcat服务器需要jdk环境 版本对应 ​ tomcat9>jdk1.8 配置系统变量JAVA_HOME sed -i $aexport JAVA_HOME/usr/local/jdk22/ /etc/profile sed -i $aexport PATH$JAVA_HOME/bin:$PATH /etc/profile ​ source /etc/profile ​ java -version java…

LeetCode | 441 | 排列硬币 | 二分查找

&#x1f64b;大家好&#xff01;我是毛毛张! &#x1f308;个人首页&#xff1a; 神马都会亿点点的毛毛张 今天分享的是LeetCode中一道标签为简单的算法题&#xff0c;本质是一道数学题 文章目录 1.题目描述2.题解2.1 公式解法2.2 暴力解法2.3 二分查找 LeetCode链接&#…

【 问题 】 AT32 F413CB 设置SRAM大小为64KB 导致Flash后64KB代码执行变慢 解决办法

背景 AT32的SRAM可以设置为16KB/32KB/64KB的不同大小&#xff0c;设置SRAM大小将导致Flash的部分空间的读写速度减缓&#xff0c;如下图&#xff1a; 这个问题看似不是很大&#xff0c;但是当运行一些很保证实时性&#xff0c;速度性的代码时&#xff0c;就会产生一些问题。 …

react-日期选择器封装

文件 import { useMemo, useState, useEffect } from "react" import dayjs, { Dayjs } from "dayjs" import "dayjs/locale/zh-cn" import "./App.css" dayjs.locale("zh-cn")function SimpleCalendar() {// 当前时间对象…

【数值计算方法】数值积分微分-python实现-p2

原文链接&#xff1a;https://www.cnblogs.com/aksoam/p/18279394 更多精彩&#xff0c;关注博客园主页&#xff0c;不断学习&#xff01;不断进步&#xff01; 我的主页 csdn很少看私信&#xff0c;有事请b站私信 博客园主页-发文字笔记-常用 有限元鹰的主页 内容&#xf…

基于ThinkPHP开发的校园跑腿社区小程序系统源码,包含前后端代码

基于ThinkPHP开发的校园跑腿社区小程序系统源码&#xff0c;包含前后端代码 最新独立版校园跑腿校园社区小程序系统源码 | 附教程 测试环境&#xff1a;NginxPHP7.2MySQL5.6 多校版本&#xff0c;多模块&#xff0c;适合跑腿&#xff0c;外卖&#xff0c;表白&#xff0c;二…

【Android】Kotlin 实现底部弹框日历组件

需求 如下图所示, 底部弹出日历组件 原生插件使用的有一个好处是可以根据你的系统语言切换插件内的语言, 刚好我们这个app面向的国外用户, 而产品对日历组件的日期显示有特别要求, 因此这无疑减少了我们切换语言的开发工作量 代码 1. custom_bottom_datepicker.xml <R…

Java小白入门到实战应用教程-权限修饰符

Java小白入门到实战应用教程-权限修饰符 前言 在前面的内容中我们其实已经接触到了权限修饰符&#xff1a;public 在java中权限修饰符除了public外&#xff0c;还有private、protected、默认权限。 权限修饰符可用来修饰类、成员变量、方法(函数)。 其中修饰类只能用publi…

springboot的表现层/控制层controller开发

第一步&#xff1a;新建文件和注入业务层对象 需要使用的注解&#xff1a; 第一个声明是restful风格开发 第二个是需要设置网页访问路径 RestController RequestMapping("/fuels")//http://localhost/fuels注入服务层对象&#xff1a; Autowiredprivate FuelServ…

晶振输出频率偏差过大的影响

晶体振荡器作为电子系统中的关键部件之一&#xff0c;其输出频率的准确性直接影响着整个系统的性能。无论是实时响应系统还是通信设备&#xff0c;都需要高度精确的频率源来维持其正常运作。本文将探讨晶振输出频率偏差过大会带来的影响&#xff0c;并简要介绍频率稳定性的概念…

【docker】下载docker 容器

1.下载地址&#xff1a;https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/rhel/9/x86_64/stable/ 2.配置仓库拉取容器镜像 docker镜像 通过软件仓库下载但不安装 挂载并放入一个东西里面&#xff08;存放rpm软件包&#xff0c;并不是软件仓库&#xff09; 采集rpm数据的…

VSCode安装和配置

一、VScode下载和安装 下载 Visual Studio Code - Mac、Linux、Windows 下载完成之后傻瓜式安装即可 二、 左侧活动栏介绍 三、安装中文插件 先下载&#xff0c;在重启即可 四、配置C语言运行环境 1、Code Runner 记得勾选图中的两个选项 2、安装C/C插件 3、安装编译器MinGW…

cpp学习记录05:类和对象02

继承 面向对象的语法思想都差不多继承的好处就是方便 语法&#xff1a; class 子类 : 继承方式 父类 继承方式 公共继承&#xff1a;继承的内容权限不变 保护继承&#xff1a;继承的内容权限变为protected 私有继承&#xff1a;继承的内容权限变为private 这三类继承都不…

1000W长连接,如何建立和维护?千万用户IM 架构设计

1000W长连接&#xff0c;如何建立和维护&#xff1f;千万用户IM 架构设计 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格&#xff0c;遇到很多很重要的架构类/设计类…

哈夫曼树及哈夫曼编码

目录 一. 前言 二. 哈夫曼树的构造 三. 哈夫曼编码 一. 前言 在学习哈夫曼树之前&#xff0c;我们先了解几个基本概念。 1.路径&#xff1a;从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。 2.结点的路径长度&#xff1a;两结点间路径上的分支数。 3.树的…

Visual Studio 2022 安装步骤

一、官方下载 下载地址&#xff1a;https://visualstudio.microsoft.com/zh-hans/vs/ 下载完成&#xff0c;得到VisualStudioSetup.exe文件 二、安装 Visual Studio 2022 由于我有 Professional 版本的密钥&#xff0c;因此安装Professional 版本。 Visual Studio 2022 Profes…