【C语言】【数据结构】自定义类型:结构体

news2024/12/22 19:33:42

引言

这是一篇对结构体的详细介绍,这篇文章对结构体声明、结构体的自引用、结构体的初始化、结构体的内存分布和对齐规则、库函数offsetof、以及进行内存对齐的原因、如何修改默认对齐数、结构体传参进行介绍和说明。

 158c3f50b199454985017a51dbef9841.png               ✨ 猪巴戒:个人主页✨

               所属专栏:《C语言进阶》

        🎈跟着猪巴戒,一起学习C语言🎈

目录

引言

结构体的声明

结构体的基础

结构的声明

匿名结构体类型

结构体的自引用

typedef作用于结构体的问题

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

多个元素的初始化要用大括号{ }

结构体的内存对齐

1.对齐规则

1.例子

2.例子

 3.例子

          

4.例子

offsetof

offsetof的使用

 ​编辑

 为什么要存在内存对齐

修改默认对齐数

 结构体传参


结构体的声明

  

结构体的基础

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

在一个变量中,要存放性别、年龄、成绩、地址多种类型的数据时,C语言允许用户自己建立由不同类型数据组成的组合型的数据结构,它称为结构体。

    

结构的声明

结构体是怎么声明的呢?

struct tag
{
    member_list;
}variable_list;  //分号不能丢

struct Student
{
	//学生的相关信息
	char name[20];
	int age;
}s1,s2;
  • tag,Student是结构体名
  • member_list是成员表列
  • struct是声明结构体类型是必须使用的关键字,不能省略
  • s1,s2变量就是学生变量。
  • { }后面要记得把“ ;”带上

struct tag就是一个结构体类型,我们可以根据自己的需要建立结构体类型,struct Teacher,struct Student等结构体类型,各自包含不同的成员。

如果将s1,s2放在main函数的外面,那么s1,s2就是全局变量。

struct Student
{
	//学生的相关信息
	char name[20];
	int age;
}s1,s2;

int main()
{
    return 0;
}

        

匿名结构体类型

结构体在声明的时候省略了结构体标签(tag),没有名字的结构体类型只能使用一次,被称为匿名结构体类型

由于没有名字,编译器会把下面的两个代码当成完全不同的两个类型。

所以,p = &x.

会因为类型不同报错。

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

struct
{
	char name[20];
	int age;
}a[20],*p;

        

结构体的自引用

结构体的自引用用到数据结构中的链表。

数据结构中有顺序表、链表的概念,

顺序表

数据在内存中是顺序排放的,可以逐个根据地址找到下一个数据。

链表

数据在内存中的存放是没有规律的但是存放数据,会分为两个部分,

一个部分叫数据域,存放有效数据,

另一个部分叫指针域,用来存放下一个数据的地址,可以通过地址直接找到下一个数据。

89dab2a99bb64a65acdd24ac4d63b4e8.png

我们通过链表就可以实现结构体的自引用。

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

        

typedef作用于结构体的问题

下面在结构体自引用使用的改成中,夹杂了typedef对匿名结构体类型重命名,看看下面的代码,有没有问题?

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

答案是不行的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。

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

          

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

struct Point是结构体类型,它相当于一个模型,是没有占据具体空间的,

当我们建立结构体变量p1,它相当于具体的房屋,在内存中储存数据。

struct Point
{
	int x;
	int y;
}p1 = { 2,3 };

        

多个元素的初始化要用大括号{ }

在结构体中,如果存在多个元素的变量,我们初始化时要使用大括号。

像数组一样,arr[] = { 0, 1, 2, 3, 4 };

  • 打印结构体,s1是struct Stu的变量,name是s1的成员变量,用s1.name表示s1结构体的name变量
  • s是struct Stu中的成员变量,用s1.s.n表示在结构体struct score的成员变量n。
struct score
{
	int n;
	char ch;
};
struct Stu
{
	char name[20];
	int age;
	struct score s;
};

int main()
{
	struct Stu s1 = { "zhangsan",20,{100,'q' } };
	printf("%s %d %d %c\n", s1.name, s1.age, s1.s.n, s1.s.ch);
	return 0;
}

        

结构体的内存对齐

如何计算结构体的大小?

结构体的内存分布是怎样的?

        

1.对齐规则

首先掌握结构体的对齐规则

1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处

2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。

对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。

- VS 中默认的值为 8

- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

3. 结构体总⼤⼩最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。

4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

          

只是文字的说明,免不了晦涩难懂,接下来用例子来给大家讲解

1.例子

#include <stdoi.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", sizeof(struct S1));
	return 0;
}

177ba4f9a12045639def737ae589d621.png

解析: 

右边表示的是偏移量,

1.第一个成员char c1要对齐到和结构体变量起始位置偏移量为0的地址处,占一个字节

2.其他成员要对齐到对齐数的整数倍的地址处

对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。

VS中的默认对齐数是8.

  int i的大小是4个字节,对齐数就是4。int i 的地址要对齐到为偏移量整数倍的地址,也就是4的整数倍,偏移量为4的地址。int i 是4个字节,那占据的地址偏移量为4~7

char c2 的大小是1个字节,对齐数是1。1可以为任意偏移量的整数倍。所以char c2的地址的偏移量就是8.

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

成员变量有char c1,int i ,char c2。它们的对齐数分别是1,4,1。因此最大对齐数为4。

结构体总大小为最大对齐数的整数倍,现在偏移量是0~8,一共是9个字节,要凑成4的整数倍,就是12个字节,在浪费3个字节就可以了,地址偏移量9~11一共是3个字节。

这个结构体的内存就储存在偏移量为0~11的空间。

d4167cce0c0f45e893871db222a54036.png

          

2.例子

#include<stdio.h>
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%d\n",sizeof(struct S2));
	return 0;
}

 1e4f04a380214b4bb0f2a9f78eed0999.png

          

解析:   

右边表示的是偏移量,

1.第一个成员char c1要对齐到和结构体变量起始位置偏移量为0的地址处,占一个字节

2.其他成员要对齐到对齐数的整数倍的地址处

对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。

VS中的默认对齐数是8.

char c1  的大小是1个字节,对齐数就是1。char c1的地址要对齐到为偏移量整数倍的地址,也就是1的整数倍,偏移量为1的地址。

int i 的大小是4个字节,对齐数是4。int i 的地址就要移到偏移量为4的倍数的地址。所以int i 的地址的偏移量就是4.int i 是4个字节,那占据的地址偏移量为4~7

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

成员变量有char c1,int i ,char c2。它们的对齐数分别是1,4,1。因此最大对齐数为4。

结构体总大小为最大对齐数的整数倍,现在偏移量是0~7,刚好是8个字节,是4的倍数。

这个结构体的内存就储存在偏移量为0~7的空间。

d669a6337351463187f729ae196ee7cb.png

        

 3.例子

#include<stdio.h>
struct S3
{
	double d;
	char c;
	int i;
};
int main()
{
	printf("%d\n",sizeof(struct S3));
	return 0;
}

4264bdd243934829baf65243a4fed71d.png

解析: 

1.第一个成员要对齐到结构体变量起始位置偏移量为0的地址处,double d占8个字节,所以占据的内存空间是偏移量为0~7的地址

2.其他成员要对齐到对齐数的整数倍的地址处

char c的大小是1个字节,任意偏移量都可以为1的整数倍,所以char c的地址是下一位,偏移量为8的地址。

int i 的大小是4个字节,要对齐到偏移量为4的倍数的地址,也就是偏移量为12,int i 占据的内存空间为偏移量为12~15的地址。

3.结构体的大小为最大对齐数的整数倍。

最大对齐数是double的对齐数,也就是8。现在的结构体占16个字节(偏移量为0~15),刚好是8的倍数。

695853325b444691812d75cc57fff25c.png

          

4.例子

这个例子包括了嵌套结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

#include<stdio.h>
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	printf("%d\n",sizeof(struct S4));
	return 0;
}

 4a85bf8db58440a5992a83d6a540841a.png

解析: 

1.第一个成员要对齐到结构体变量起始位置偏移量为0的地址处,char c1占1个字节,占据偏移量为0的空间。

2.嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

接下来是struct s3,要对齐自己成员的最大对齐数,double d的对齐数为8个字节,对齐到偏移量为8的地址,

3.其他成员要对齐到对齐数的整数倍的地址处,嵌套的结构体成员也是这样,double d占据8个字节,占据偏移量为8~15的地址。

char c对齐偏移量16,占据一个字节。

int i 的对齐数为4,对齐偏移量为20,占据4个字节,就是偏移量为20~23的空间。

struct S3整理完,继续到struct S4,轮到double d

double d的对齐数为8,对齐偏移量24,占据8个字节,占据空间偏移量为24~31。

4.结构体的大小为最大对齐数的整数倍。

当前空间一共是32个字节(0~31),结构体struct S4,struct S3中的成员的最大对齐数是8。因此结构体的大小要是最大对齐数的整数倍。32刚好是8的整数倍。

b3918788de7c482f9619ec761120d842.png

          

offsetof

返回成员的偏移量 ,头文件<stddef.h>

offsetof (type,member)

a0f30360e47c4f359c4a43f75531940b.png

offsetof的使用

type是类型,

#include <stdio.h>
#include <stddef.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));

	return 0;
}

 3e8aa4f2d6b64882ad78ae257b6f82c9.png

          

 为什么要存在内存对齐

1. 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。

 以32为机器为例,32位机器一次可以访问32位比特位的数据,

如果没有对齐规则,就像左边,机器要访问两次才可以得到 int i 的值,

有对齐规则,就像右边,想要访问 i ,只需要访问一次就足够了。

对齐规则的思想:把数据放在机器可以一次访问得到数据的空间内,使访问更具效率。 

f8a8927cd0f343869f356c4706bb6311.png   

         

修改默认对齐数

当结构体的对齐方式不适合时,我们也可以修改默认对齐数。

  • 在括号填写数字,对默认对齐数进行修改。
  • 如果()内没有数字,则时将默认对齐数恢复到默认值。
#pragma pack()

下面的struct S原本是占据12个字节的空间,对默认对齐数进行修改后,只占据6个字节的空间。 

#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S));
	return 0;
}
0ca731e68b5c4920b09113e7131a054a.png

           

 结构体传参

  • 传值调用,将数据通过参数传过去,然后函数print会创立独立的空间,对传过来的数据进行存储
  • 传址调用,将数据的地址传过去,函数通过指向数据的地址对数据进行使用,不需要再建立空间对数据进行存放。
#include<stdio.h>
struct S
{
	int data[1000];
	int num;
};
void print1(struct S ss)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ss.data[i]);
	}
	printf("%d\n", ss.num);
}
void print2(struct S* ps)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("%d\n", ps->num);
}
int main()
{
	struct S s = { {1,2,3},100 };
	print1(s);
	print2(&s);
	return 0;
}

b2d177f2dc1c40c6b3b1b06078388bec.png

        

上面的传值调用print1 和 传址调用print2 函数那哪个更好?

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

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

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

相关文章

Linux安装Nginx并部署Vue项目

今天部署了一个Vue项目到阿里云的云服务器上&#xff0c;现记录该过程。 1. 修改Vue项目配置 我们去项目中发送axios请求的文件里更改一下后端的接口路由&#xff1a; 2. 执行命令打包 npm run build ### 或者 yarn build 打包成功之后&#xff0c;我们会看到一个dist包&a…

中文语音标注工具FunASR(语音识别)

全称 A Fundamental End-to-End Speech Recognition Toolkit&#xff08;一个语音识别工具&#xff09; 可能大家用过whisper&#xff08;openAi&#xff09;&#xff0c;它【标注英语的确很完美】&#xff0c;【但中文会出现标注错误】或搞了个没说的词替换上去&#xff0c;所…

万户协同办公平台ezoffice wpsservlet接口任意文件上传漏洞

声明 本文仅用于技术交流&#xff0c;请勿用于非法用途 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任。 一、漏洞描述 万户ezOFFICE协同管理平台是一个综合信息基础应用平台&am…

Linux网络——高级IO

目录 一.五种IO模型 1.阻塞式IO 2.非阻塞式IO 3.信号驱动IO 4.多路转接IO&#xff1a; 5.异步IO 二.同步通信 vs 异步通信 三.设置非阻塞IO 1.阻塞 vs 非阻塞 2.非阻塞IO 3.实现函数SetNoBlock 四.I/O多路转接之select 1.初识select 2.select函数原型 3.socket就绪…

3.c++进阶语法函数和指针

1.函数 5&#xff1a;03 2.指针

【51单片机系列】74HC595扩展实验之使用74HC595芯片在LED点阵中显示数字

本实验实现的功能是使用74HC595芯片实在LED点阵中显示数字字符0。 要点亮多个LED灯&#xff0c;需要用到动态数码管的动态扫描原理。 首先如何点亮一行上面的多个灯或一列上面的多个灯&#xff0c;明显就是需要某行或某列有效&#xff0c;同时使多列或多行有效。比如在第一行有…

记录 | xftp远程连接两台windows

1、打开openssh 设置 -> 应用 -> 可选功能 -> 添加功能 -> OpenSSH 客户端&#xff0c;将 ssh 客户端安装将两台电脑的 ssh 开启&#xff0c;cmd 中输入 net start sshd2、配置 win10 账号密码 3、进行 xftp 连接

【Spring】@SpringBootApplication注解解析

前言&#xff1a; 当我们第一次创建一个springboot工程时&#xff0c;我们会对启动类&#xff08;xxxApplication&#xff09;有许多困惑&#xff0c;为什么只要运行启动类我们在项目中自定义的bean无需配置类配置&#xff0c;扫描就能自动注入到IOC容器中&#xff1f;为什么我…

java--LinkedList集合的底层原理

1.什么是链表&#xff1f;有啥特点&#xff1f; ①链表中的结点是独立的对象&#xff0c;在内存中是不连续的&#xff0c;每个结点包含数据值和下一个结点的地址。 ②链表的特点1&#xff1a;查询慢&#xff0c;无论查询那个数据都要从头开始找。 ③链表的特点2&#xff1a;链…

day3_qt

完善对话框&#xff0c;点击登录对话框&#xff0c;如果账号和密码匹配&#xff0c;则弹出信息对话框&#xff0c;给出提示”登录成功“&#xff0c;提供一个Ok按钮&#xff0c;用户点击Ok后&#xff0c;关闭登录界面&#xff0c;跳转到其他界面 如果账号和密码不匹配&#xf…

进程、容器与虚拟机的区别

进程、容器与虚拟机 参考&#xff1a;关于进程、容器与虚拟机的区别&#xff0c;你想知道的都在这&#xff01; 进程、容器与虚拟机的结构图 进程 介绍 进程是一个正在运行的程序&#xff0c;它是一个个可执行文件的实例。当一个可执行文件从硬盘加载到内存中的时候&#xf…

安装Anconda时出现Failed to extract packages的解决方法

目录 1. 问题所示2. 原理分析3. 解决方法 1. 问题所示 在win7安装Anconda的时候&#xff0c;出现Failed to extract packages 截图如下所示&#xff1a; 2. 原理分析 该版本过于新&#xff0c;无法兼容win7系统&#xff0c;要么更换系统 要么将anconda版本降低即可 3. 解决…

基于SSM超市订单管理系统(Java毕业设计)

大家好&#xff0c;我是DeBug&#xff0c;很高兴你能来阅读&#xff01;作为一名热爱编程的程序员&#xff0c;我希望通过这些教学笔记与大家分享我的编程经验和知识。在这里&#xff0c;我将会结合实际项目经验&#xff0c;分享编程技巧、最佳实践以及解决问题的方法。无论你是…

计算机二级Python基本操作题-序号46

Python 函数查询 1. 《卖火柴的小女孩》是丹麦童话故事作家安徒生的一篇童话故事&#xff0c;发表于1846年。主要讲了一个卖火柴的小女孩在富人阖家欢乐、举杯共庆的大年夜冻死在街头的故事。这里给出《卖火柴的小女孩》的一个网络版本文件&#xff0c;文件名为“小女孩.txt”…

IOday8作业

使用消息队列完成两个进程之间相互通信(多进程) #include<myhead.h>//定义结构体 struct buf {long mtype;char mtest[1024]; };#define SIZE (sizeof(struct buf)-sizeof(long))//进程 int main(int argc, const char *argv[]) {//创建keykey_t key1 ftok("/&quo…

关于北京医学sci论文翻译

在医学领域&#xff0c;翻译论文是一项非常重要的工作。医学论文的翻译需要准确、专业、严谨&#xff0c;同时也需要考虑到医学领域的特殊性和复杂性。那么&#xff0c;如何翻译医学论文呢&#xff1f;北京医学SCI论文翻译哪家好呢&#xff1f; 首先&#xff0c;需要具备专业的…

5.鸿蒙hap可以直接点击包安装吗?

5.鸿蒙hap可以直接点击包安装吗&#xff1f; hap与apk不同&#xff0c;获取的hap不能直接安装 安装方法1&#xff1a; DevEco studio打开项目源文件&#xff0c;打开手机USB调试&#xff0c;DevEco识别到手机后&#xff0c;点击播放按钮安装到手机 https://txwtech.blog.cs…

多线程案例-阻塞队列

阻塞队列是什么 阻塞队列是一种特殊的队列.也遵循"先进先出"的原则 阻塞队列能是一种线程安全的数据结构,并且具有以下特性: 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素. 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素…

小电流MOSFET 选型分析数据,可应用于电子烟,电动工具,智能穿戴等产品上

小电流双N&#xff0c;D-N通道MOSFET&#xff0c;电压60V-100V左右 电流300mA-500MA&#xff0c;采用封装形式多样。具有低导通电阻&#xff0c;可快速切换速度&#xff0c;易于设计的驱动电路也易于并联&#xff0c;ESD保护&#xff0c;低电压驱动使该器件非常适合便携式设备…

相控与斩控交交调压(THD的计算)

相控与斩控交交调压&#xff08;THD的计算&#xff09;