前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【C语言】一篇速通结构体

【C语言】一篇速通结构体

作者头像
謓泽
发布2023-03-01 12:40:36
4210
发布2023-03-01 12:40:36
举报
文章被收录于专栏:【C】系列

🚩write in front🚩    🔎大家好,我是謓泽,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎 🏅2021|2022年度博客之星物联网与嵌入式开发TOP5|TOP4~2021|2022博客之星TOP100|TOP63~周榜159 ⌁ 总榜751~ 🆔本文由 謓泽 原创 CSDN首发🙉 如需转载还请通知⚠ 📝个人主页-謓泽的博客_CSDN博客 📃 📣系列专栏-【C】系列_謓泽的博客-CSDN博客🎓 ✉️我们并非登上我们所选择的舞台,演出并非我们所选择的剧本

目录

🚩write in front🚩   

结构体概念

结构体指针 

特殊的声明 

结构体自己引用 

结构体内存 

结构体对齐规则

为什么存在内存对齐 

如何修改默认对齐数 

offsetof() - 查询偏移量

位段 

位段的内存分配 


结构体概念

在这里用简短的话先说说结构体↓ 结构体实际上是一些值的集 合,结构的每个成员是不同的变量。所以在这里结构体实际上也是复杂对象类型称之为构造类型,我觉得可以把这个构造类型看成是一个项目的总共。而基本类型就是小的项目。 而在这里很多人可能会联想到数组,但是数组是一组相同类型的元素集合。而我们结构体可以是不同类型的元素的集合。在这里用玩具盒子来表示结构体名,用其它玩具表示每个不同の成员,如下图所示:↓ 

上述在这里书是一个复杂对象,我们要创建书的类型那么就是一个结构体类型。 那么我们来写一个"书"的结构体类型代码。如 ↓ 所示

代码语言:javascript
复制
#include<stdio.h>
typedef struct BOOK
{
	char book_title[20];
	char name[20];
	char PH[20];
	int Pricing;
	int id;
}BOOK;
int main(void)
{
	BOOK s = { "xxxxx", "张三", "aaaaa", 20, 123456 };
	printf("book_title:%s\n", s.book_title);
	printf("name:%s\n", s.name);
	printf("PH:%s\n", s.PH);
	printf("Pricing:%d\n", s.Pricing);
	printf("id:%d\n", s.id);
	return 0;
}

运行结果 ↓ book_title:xxxxx name:张三 PH:aaaaa Pricing:20 id:123456 

结构体指针 

代码语言:javascript
复制
struct stu* p;

如上述代码这是一个结构体指针变量说明结构体指针变量p指向(->)的是一个结构体类型变量地址也就是保存x的地址。

注意:(->)指向操作符是一种通过指针的方式去访问结构体内的成员一种便捷写法的反方式。

示例代码如下↓

代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//结构体类型制作
struct stu
{
	int id;
	char c;
}*p;
int main(void)
{
	struct stu age = { 18,'C' };//结构体变量以及初始化
	p = &age;	   //结构体指针
	printf("id:%d\n", p->id);
	printf("c:%c\n", p->c);
	return 0;
}

运行结果🖊

id:18

c:C


特殊的声明 

在声明结构体的时候,实际上可以不完全的进行声明。

代码语言:javascript
复制
struct 
{
	char book_title[20];
	char name[20];
	char PH[20];
	int Pricing;
	int id;
}BOOK;
代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct
{
	int id;
	char c;
}BOOK;
int main(void)
{
	printf("%d\n", BOOK.id = 18);
	printf("%c\n", BOOK.c = 'c');
}
运行结果:18 
         c

上述代码是个什么意思,实际上就是我们的结构体名是没有起名字的。但是整体来说我选中的还是依旧是一个结构体类型,那么这种叫做什么呢? → 匿名结构体类型。 注意🔥:匿名结构体创建完之后只能使用一次,具有局限性。 而用这个匿名结构体类型我们是直接创建了一个全局变量BOOK,和前面代码当中是一个意思只不过我们把结构体的标签也就是结构体当中的名给省略掉了。 那么下面再来说说匿名结构体指针,如下代码所示 ↓

代码语言:javascript
复制
struct 
{
	char book_title[20];
	char name[20];
	char PH[20];
	int Pricing;
	int id;
}*BOOK1;

*是匿名结构体的指针,用指针类型创建了BOOK1,那么此时我们可以用 BOOK1 = &BOOK吗?如 ↓ 代码所示:

代码语言:javascript
复制
#include<stdio.h>
struct
{
	char book_title[20];
	char name[20];
	char PH[20];
	int Pricing;
	int id;
}BOOK;
struct
{
	char book_title[20];
	char name[20];
	char PH[20];
	int Pricing;
	int id;
}*BOOK1;
int main(void)
{
	BOOK1 = &BOOK;
	return 0;
}

在这里编译器会出现一个警告,如果你强行这样去写也是运行的过去の。 

在这里看来,编译器虽然你们当中的结构体成员变量都是一模一样的,但编译器还是会认为这两个是不同的类型。那么 *BOOK1 就不能存放 BOOK 的地址了。


结构体自己引用 

结构体自己引用其实就是:结构体里面是可以包含结构体的。

代码语言:javascript
复制
struct Book
{
    int id;
    struct Book book;
}

在上述代码中创建了一个结构体,在里面当中也创建了一个结构体。  上述代码是错误的,因为它死递归了,没有限制大小,它的大小会一直增加下去,取决于你创建的结构体。

那么结构体该如何实现自引用。 如下代码所示↓

代码语言:javascript
复制
struct Book
{
    int id;
    struct Book* book;//指针变量大小固定可算
}

上述代码才是自引用使用正确的, 自引用不是包含同类型的结构体变量,而是包含同类型的结构体指针。看到这个如果学过数据类型的小伙伴们可以快速的理解。

数据域:存放的是我所要存储的数据。 指针域:是为了找到下一个的节点的地址。


结构体内存 

sizeof(表达式)→获取某个数据类型所占用空间的 字节数 或者 是表达式的长度。 那么我们来看看结构体内存的大小是多少,如下代码所示↓

代码语言:javascript
复制
#include<stdio.h>
struct B1
{
	int name;
	char c;
};
struct B2
{
	char c1;
    struct B1
	char c2;
};
int main(void)
{	
	struct B1 s1 = { 0 };
	printf("s1:%d\n", sizeof(s1));
	struct B2 s2 = { 0 };
	printf("s2:%d\n", sizeof(s2));

	return 0;
}

运行结果↓ s1:8 s2:12

这个时候张三就可能会有疑问了,说结构体B1不应该是5个字节吗,而B2应该是9个字节才对啊。 为什么会是这个样子阿~ 这个实际上就是结构体内存对齐的问题了,让我们来看看这到底是为什么。 从上面我们知道结构体存储跟我们实际想象💭的是不一样的,这里其实是在结构体的内存对齐当中是有结构体自己的规则的。

结构体对齐规则

我们以上述结构体类型B为示例。下面来说说结构体内存对齐的规则。

  1. 结构体的第一个成员放在结构体变量的内存中存储位置的0偏移出开始。
  2. 从第二个成员往后的所有成员,都放在一个对齐数(成员的大小和默认对齐数的较小值在这里int为4)的整数倍的地址处。
  3. 结构体的总大小是结构体的所有成员的对齐数种最大的那个对齐数的整数倍。
  • vs默认的对齐数为8,linux平台上无(没有对齐数的概念)。

如图表示↓

所以这里的结构体内存占了12个字节,如果上面这个多去理解下,还是能够很容易理解的||ヽ(* ̄▽ ̄*)ノミ|Ю 

当然如果有嵌套情况,像下面代码一样。 

代码语言:javascript
复制
#include<stdio.h>
struct B1
{
	double pai;
	char c;
};
struct B2
{
	char c1;
	struct B1;
	char c2;
};
int main(void)
{
	struct B2 s = { 0 };
	printf("%d", sizeof(s));
	return 0;
}

运行结果↓ 32  如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么存在内存对齐 

平台原因→移植原因:不是所有的硬件平台上都能访问任意地址上的任意数据的,某些硬件平台上只能在某些地址处某些特定的数据,不然会抛出硬件异常。 性能原因:数据结构尤其是栈,应该尽可能地在自然边界对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问。 总的来说→结构体的内存对齐是拿空间来换取时间的做法。

当然在设置结构体的时候我用成员较小的话存放在一起这样是可以节省内存空间的。 如下代码所示 ↓ 

代码语言:javascript
复制
#include<stdio.h>
struct B1
{
	int name;
	char c;
	char c1;
};
int main(void)
{
	struct B1 s1 = { 0 };
	printf("s1:%d\n", sizeof(s1));

	return 0;
}

如何修改默认对齐数 

在上面当中提到过vs的默认对其数是⑧,那么我们是否或者怎么样才能够修改这个默认对其数。如下代码所示 ↓ 

代码语言:javascript
复制
#include<stdio.h>
struct S8
{
	char c;//0
	//浪费1~3
	int name;//4~7
	char c1;//8
	//浪费9~11
	//总共字节大小为12
};
int main(void)
{
	printf("S8:%d\n", sizeof(struct S8));
	return 0;
}

运行结果↓ S8:12

那么上述代码当中的默认对齐数是8,注-vs默认对齐数都是8。那么我们如何把默认对齐数修改成大小为2❓那么如下所示↓

代码语言:javascript
复制
#pragma pack (2)

在这里我们加上这句代码的话就可以把我们默认对齐数从⑧修改成②了 如下代码所示↓

代码语言:javascript
复制
#include<stdio.h>
#pragma pack (2)
struct S8
{
	char c;
	int name;
	char c1;
	
};
#pragma pack ()//把修改的给取消掉
int main(void)
{
	printf("S8:%d\n", sizeof(struct S8));
	return 0;
}

运行结果↓ S8:8 那么在这里我们的程序运行结果就是为8了,那么这是为什么。如下图列表所示↓

那么当我们上面改成默认对齐数是1的话,那所有的都会是1的倍数了。 大小就不存在什么浪费不浪费了。 

offsetof() - 查询偏移量

该函数的同文件是→#include<stddef.h> offsetof()函数声明如下 ↓

代码语言:javascript
复制
offsetof (type,member)

  • 该宏以函数形式返回成员成员在数据结构或联合类型中的字节偏移值。
  • 返回的值是size_t类型的无符号整型值,包含指定成员与其结构体开头之间的字节数。 

如下代码所示 ↓ 

代码语言:javascript
复制
#include<stdio.h>
#include<stddef.h>
struct S8
{
	char c;//0
	//浪费1~3
	int name;//4~7
	char c1;//8
	//浪费9~11
	
};
int main(void)
{
	printf("偏移量→%d\n", offsetof(struct S8,c));
	printf("偏移量→%d\n", offsetof(struct S8, name));
	printf("偏移量→%d\n", offsetof(struct S8, c1));
	return 0;
}

运行结果如下 ↓ 偏移量→0 偏移量→4 偏移量→8

由于结构体的传参部分在上一篇结构体当中说过,这里就不再过多追述。


位段 

说完结构体那么再来说说什么是位端的概念,以及位段是什么。 位段和结构体的声明都是类似的,但是有两点不同↓

  1. 位段的成员必须是 整形类型(int)、无符号整形类型(unsigned int)、有符号整形类型(signed int)
  2. 位段的成员名后面是有一个冒号:和数字的,当然在位段当中也是可以使用字符类型(char)的,因为它实际上也是整形类型的一种。
  • 那么下面来说说位段是什么样子的,如下代码所示↓

那么下述代码当中这个结构体B就是一个位段类型。 

代码语言:javascript
复制
#include<stdio.h>
struct S
{
	int a : 2; //(1)
	int b : 5; //(2)
	int c : 10;//(3)
	int d : 30;//(4)
};
int main(void)
{
	printf("%d\n", sizeof(struct S));
	return 0;
}

运行结果↓ 8

  • a 占 两个比特位。
  • b 占 五个比特位。
  • c 占 十个比特位。
  • d 占 三十个比特位。

那么在上述一共有四十七位比特位。在这里我们要直到1个字节 = 8比特位。

位段的内存分配 

  1. 位段的成员可以是 int、unsigned int、signed int、char(本质上是属于整形的)类型。
  2. 位段的空间是需要按照(4个字节int)或者是(1个字节char)的方式来进行开辟空间。
  • 在位段当中是不涉及跨平台的,如果你是在跨平台当种的话是不介意使用位段的。
代码语言:javascript
复制
struct S
{
	int a : 2; //(1)
	int b : 5; //(2)
	int c : 10;//(3)
	int d : 30;//(4)
};

首先看到 int a 开辟4个字节,有32个比特位。那么 abc 加起来一共有17个比特位。 那么我们用 32 🗡 17 = 15,那么这里就还有15个比特位。 在这里15个比特位不够 int b 当中的30个比特位,那么就再开辟4个字节为32个比特位存放 int b 当中的30个比特位。 那么在这里我们到底是用了32个比特位当中,还是先用到15个比特位再用到32个当中的比特位 这个实际上在C语言当中是不确定的因素,因此为什么说位段是不跨平台的。因为在不同的平台下的实现方法是不一样的,gcc、vs、dev...这些平台的实现都是不同的。

总结→和结构体相比,位段是可以达到同样的效果,具有很好的节省空间,但是它是有跨平台的问题存在的。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-02-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 结构体概念
    • 结构体指针 
      • 特殊的声明 
        • 结构体自己引用 
      • 结构体内存 
        • 结构体对齐规则
        • 为什么存在内存对齐 
      • 如何修改默认对齐数 
        • offsetof() - 查询偏移量
      • 位段 
        • 位段的内存分配 
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档