结构体类型


变量名的要求(额外要说的点)

 

变量名只能是字母,数字,下划线这三种类型组成,且不能由数字开头。

内存中数据的存放(额外要说的点)

 

对于大小端存储模式只适用于单个数据(超过单个字节的数据)里的各个字节的排列顺序,其会使该数据的各个字节都安排在对应的地址上 (如在vs中最高位字节安排在最高地址处,最低位字节安排在最低地址处,vs为小端存储模式),它不影响多个数据中的排列。之前就很细致的讲过了在这篇文章中写文章-CSDN创作中心

现在再补充一点,其大小端不影响单个字节内部的比特位排序,其单个字节内部比特位排序是固定的,如1为00000000 00000000 00000000  00000001,则放到栈中因为为小端存储的关系低字节放到低地址处,而其并不改变单个字节内部的比特位排序,所以一个字节内部的比特位并没改变,则存放之后为 00000001  00000000 00000000  00000000  地址由低到高,所以十六进制表现为01 00  00  00 ,跟vs调试中的一样。

所以得出结论,数据在内存存放时:无论如何对于一个字节内部的两个十六进制数字第一个都是高进制位比特,第二个才是低进制位比特(如01中0为高进制比特位,1为低进制比特位),不要搞反了。我们上面那段说的就是产生这种现象的原因。

所以对于一个字节内部的比特位:高比特进制位永远在低比特比特位左边。(仅限一个字节内部)顾名思义一个字节内部的比特位顺序永远不会改变,一个字节内部左边的永远是高比特位。

了解了以后方便以后我们在调试看内存条时能看懂为什么这么放置。 并且我们在看一个变量在内存条的存放放置时:我们能直接依据其内存存放放置将变量转化为十进制数字。(像别人不清楚怎么存放的话,他们就算不出来这个变量到底为多少,极有可能算错)

结构体的基本使用

结构体的声明及基本使用 

 在之前我们就已经学习过结构体了。在操作符的详解中讲过。

https://blog.csdn.net/Easonmax/article/details/134298830?spm=1001.2014.3001.5501

现在简单的看一下就行 

 

结构体里面可以包含很多数据类型,如数组,结构体(除自己本身结构体),结构体指针等。 

在声明完后,可以直接在后面创建变量,可以是普普通通的一个结构体变量,也可以是一个结构体指针变量,还可以是一个结构体数组。

还可以在声明完后就直接将其声明完后的结构体用typedef替换为其他名字,如

对于其相关的两个操作符,一个如果前面是结构体名字就用 . 直接访问操作符)

一个如果前面是结构体地址(指针)就用->间接访问操作符 

它们两后面为内部数据名就都能访问到其对应的结构体里面的数据。

之前我已经在操作符详解那篇文章中讲到这两个的具体操作,这里不详细阐述。

 结构体的特殊声明(特殊使用)

对于结构体可以匿名,但是我们只能使用匿名的它创建变量一次,意味着用该匿名结构体类型创建变量时只能在声明的同时在后面创建变量,除此之外它不能再创建变量。

虽然对于匿名之后的结构体创建变量我们只能声明的同时创建,但是我们对结构体匿名的次数并不是有限的,能一直匿名。(匿名结构体的次数不会对系统有什么影响,而是匿名结构体创建变量的地方对系统会有影响)

 

这里还要说一点,对于普通结构体,如果我们这里声明了了两个完全相同的结构体,编译器会认为这是两个类型完全不同的类型,所以导致出现下面这种状况。 

这里可以执行 

 

 这里因为是两个完全不同的类型,所以不能存入。

对于两个完全相同的匿名的struct,同样它们的类型完全不同。 

所以只要是有两个完全相同的结构体,我们就知道它们的类型是完全不同的 ,从而就能避免很多操作所带来的问题。

对于匿名struct的这个创建变量只能声明时创建变量的这个局限,我们就可以用typedef这个关键词解决这个问题。我们将匿名的struct命名为其他名字,此时用其他名字去创建变量,就可以被编译器所允许。

 

该图中用typedef作用于匿名struct后,其就能在其他地方创建变量了。 

对于struct的特殊声明我们只需要了解知道有这个东西就行,到时候别人代码出现了我们能看懂就行,对于我们自己写时几乎用不到这个特殊声明(不排除有些人拿来炫技用到这个) 

不是只有结构体能进行匿名操作,对于之后要讲的位段和联合体都能进行匿名操作,(他们的代码语法很相似)匿名之后的作用肯定也是一样,都是只能在声明的同时创建。

结构体的自引用 (特殊使用)

 对于结构体的自引用,不能出现结构体里面包含自己结构体,否则会因为无限循环,从而无限大。

真正的自引用应该是

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

这个自引用并没有循环,没有无限大,这个才为真正的自引用。里面包含着指向自己的指针。

在结构体⾃引⽤使⽤的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引⼊问题,看看
下⾯的代码,可⾏吗?
typedef struct
{
int data;
Node* next;
}Node;

因为替换的名字node是在struct声明完后才有的,而struct里面有node,而在struct还没声明好前node是不为结构体,所以struct由于存在一个不知道为什么类型的node从而声明错误,自然也就替换不了,编译错误。

所以定义自引用时的结构体不要使⽤匿名结构体了(本身匿名结构体就几乎用不到,只需要了解就行,如果你乱炫技,在这自引用时明明可以用普通声明的结构体,在这非要炫技用到匿名结构体,就会导致错误)

 正常做法就是不要对匿名结构体重命名,而是对普通结构体重命名。如下

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

 结构体内存对齐

对齐规则

⾸先得掌握结构体的对⻬规则:
对于结构体内部内存
1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到对⻬数的整数倍的偏移量处。(偏移量是其位置与结构体起始位置的地址差值)
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
- VS 中默认的值为 8
- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的
整数倍。(是总大小,不是)
4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
对于嵌套内的结构体的内部内存分布其内存分布规则就也符合前面讲的规则,只不过对于其内部变量分布如偏移量现在是相对于其嵌套的结构体来计算的,并不会对于本身大结构体去计算。

 5.对于一个数组来说,其数组的对齐数=编译器默认的一个对齐数与其成员的类型大小,如int arr[40]其对齐数就为 4. 

 offsetof函数

对于该函数适用于求结构体内部各个数据中相对于结构体起始位置的偏移量。 

返回值为size_t类型,返回其对应的偏移量,其offsetof(结构体类型名,结构体内部成员名)这是其所需内部参数

下面有段代码就是对上述的应用 (对于讲的上述知识点都用到了)

#define _CRT_SECURE_NO_WARNINGS
#include<stddef.h>
#include<stdio.h>
struct sat 
{
	int a;
	int b;
	char c;
 
};
int main() {

	printf("%d ", sizeof(struct sat));
		printf("%d ", offsetof(struct sat, a));
 }

 

为什么存在内存对齐

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

 

所以我们应该要让占⽤空间⼩的成员尽量集中在⼀起
1 //例如:
2 struct S1
3 {
4 char c1;
5 int i;
6 char c2;
7 };
8
9 struct S2
10 {
11 char c1;
12 char c2;
13 int i;
14 };,
第二个结构体字节为8,第一个字节大小为12.所以让占用空间小的(字节小的)集中在一块更能节省空间(字节空间更小)。

 修改默认对齐数

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

#define _CRT_SECURE_NO_WARNINGS
#pragma  pack(1)//将默认对齐数修改为1
#include<stddef.h>
#include<stdio.h>
struct sat 
{
	int a;
	int b;
	char c;
 
};
#pragma  pack()//将默认对齐数还原为初始
struct st
{
	int a;
	int b;
	char c;
 
};
int main() {

	printf("%d ", sizeof(struct sat));
	printf("%d ", sizeof(struct st));
		
 }

 这里用了#pragma pack(1)将默认对齐数修改为1

而#pragma pack()是将默认对齐数还原为初始状态

 

struct sat是在默认对齐数为1时创建的,而struct st 是默认对齐数为8时创建的。所以其字节大小不同 。

对于linux中gcc其不存在默认对齐数时我们用pragma作用它,无需现在考虑它的作用,到时候如果真不存在默认对齐数我们再去探讨其到底怎样作用。现在我们只需考虑当存在默认对齐数时pragma其作用为上所述。(绝大部分情况编译器都存在默认对齐数,极少情况不存在)

 结构体传参

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函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。且空间过大可能会导致栈溢出从而出现问题。
而传递一个结构体地址时,其接受参数时创建的结构体地址变量内存只有八个字节或者16个字节(按照环境决定),并不会比传整个结构体时的空间大,所以更好。
所以结构体传参的时候,我们都是传结构体的地址。

目前我们所学的数据名为地址类型的只有函数名,数组名,字符串(整个如“asddasds”),其他都是代表其整个数据。像结构体也是代表整个数据,其结构体变量名代表着整个结构体,而不是其结构体地址 。

结构体实现位段 

这是结构体的一个很细致的知识点(知道就行,其实很少用到)

 什么是位段

位段的声明和结构是类似的,都是用到struct关键词,其用法也几乎一样,结构体所用的间接操作符和直接操作符 位段都可以用到,且用法相同。
对于间接操作符和直接操作符,在之后的联合体中也可以使用,用法一样。

其有两个不同:
1. 由于位段有很多不确定因素,位段的每个成员必须类型都相同,且类型只能在 char,int之间选(这里的int是指unsigned int ,sigined int  int这三种,char同理)
2. 位段的成员名后边有⼀个冒号和⼀个数字。
下面给一个例子
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
}
 

其struct A(位段)内存大小为多少,答案为8个字节。那么为什么呢?接下来就要讲到其位段的内存分配

 位段的内存分配

 其跟结构体内存分配不一样,结构体分配符合对齐规则。以空间换取时间所以空间浪费会较多。 

而对于位段来说,其特点就是很能节省空间(不代表不会浪费空间,但相较于结构体浪费的肯定少)

现在说下其内存分配的细节:

我们的这个数字2或者5其实指的是其创建的变量所占的比特位大小,如a空间大小为两个比特位。

之所以设计位段是因为假设我们让a存入一个2,那么如果是int类型的a,就只会给最后两个比特位变化,其他30个比特位不变,白白浪费掉了30个比特位,而我们这时候设计一个为2个比特位的a,刚好不会浪费,相比于普通的int类型大大的节省了空间。 

这里还有个限制,我们的数字不能超过其数据类型的比特位大小,如前面是int,则数字不能超过32,否则系统会错误。

struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
}
 

当我们类型为int类型时,我们是以int类型的内存大小为单位来开辟的。

当我们类型为char类型时,我们是以char类型的字节大小为单位来开辟的。

上述就是c语言对于位段的明确规定。但是c语言还是有一些并没有明确规定,如把其数据以怎么的形式存入到其开辟的空间上去等等,c语言没有明确规定的东西就都是编译器所决定的,而不同的编译器有不同的规定,所以位段涉及很多不确定因素。位段是不跨平台的,注重可移植的程序应该避免使⽤位段(否则在vs能实现该功能换了个编译器就实现不了,此时要实现相同功能必须换代码)

 现在我们就说下在vs中其数据是以怎样的形式存入到其开辟的空间上去。

vs具体是将其数据依次从开辟的空间从右往左放(下一个开辟的空间是往右边创建,因为左边是低地址右边为高地址,注意这是数据内部,跟数组一样内部数据依次从低往高创建,多个数据才是高地址到低地址创建)

当开辟的空间所剩余的空间⽆法容纳下一个数据时,直接舍弃再创造另一个开辟的空间去容纳。(vs是直接舍弃,有的编译器还要利用)

对于是位段类型,但是出现没有:和数字的情况,如上图,就看做将:和8(类型所含比特大小)隐藏起来。从而写成 unsigned char  ucpim1;,其实是  unsigned char  ucpim1:8;。

只要出现了:和数字就直接认为是位段类型,没有出现就是结构体。

从而我们解析下,

首先将a的内存放入第一个开辟的空间(一个字节内存),放入其空间的最右边(从右往左放入数据),然后b的内存从右往左放入第一个开辟的空间,由于只剩一个比特位了,c肯定不够,所以直接舍弃,。而后开辟第二个空间,其c依然从最右边放,而后剩余3个,不够放直接舍弃,从而创建第三个开辟的空间,依然从右边往左边放。这时就ok了,其位段内存为三个字节大小

可能在赋值时会出现这种情况10的二进制形式为01010,但它最多为3个比特位,所以只能切割留下最后三个010,从而值变为2.

图上的开辟的空间是以char类型字节的大小为单位去开辟的,以int类型字节的大小为单位去开辟同理也是从右往左,直接舍弃

 位段的跨平台问题

 内存分配中就提过跨平台问题,但那只是其中一部分,还有其他问题。

1. int 位段被当成有符号数还是⽆符号数是不确定的。(绝大部分认为有符号,但存在少数)
2. 位段中最⼤位的数⽬不能确定。(16位机器中int大小为两个字节,32位机器中int大小为4个字节,所以开辟的空间大小会发生变化,从而出现问题)
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当⼀个开辟空间包含两个位段成员,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段成员剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
总结:
跟结构体相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

 位段的应用

下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。

 

 位段使⽤的注意事项

位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。并且位段成员内存大小是以比特为单位,而地址是以字节为单位,内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。所以其位段成员根本不存在地址。
所以不能对位段的成员使⽤&操作符,这样根本得不出地址,所以就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。
整个位段(整体)存在地址,而其成员不存在地址。
int main()
{
 struct A sa = {0};
 scanf("%d", &sa._b);//这是错误的
 
 //正确的⽰范
 int b = 0;
 scanf("%d", &b);
 sa._b = b;
 return 0;
}

 总结

那么在这里,我们的结构体类型就讲清楚了(里面还有位段这种类型),之后将会给大家介绍联合体类型和枚举类型!

谢谢大家!!!

  • 30
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值