目录
前言:
一、结构体类型的声明
1.1、什么是结构体?
1.2、结构的声明:
1.23、结构体变量的创建和初始化
1.3.1 结构体变量的创建
1.3.2 结构体变量的初始化
1.4、结构的特殊声明
二、结构体的自引用
2.1、概念
2.2、通过链表来理解自引用:
三、结构体内存对齐
3.1、对齐规则
四、结构体实现位段
4.1 什么是位段
4.2 位段的内存分配
4.3 位段的跨平台问题
4.4 位段使用的注意事项
4.4.1 位数限制:
4.4.2 取地址操作限制:
4.4.3 整型升级:
4.4.4 赋值范围:
4.4.5 数组限制:
4.4.6 跨平台问题:
结束语:
前言:
哈喽!好久未见,甚是想念!不知道大家国庆玩的怎么样?反正小编去了北京玩了两天就病倒了 ,只能在宾馆度过余下的假期。不过呢也算因祸得福,提前从北京回来,回家里休息了两天,但是仅仅回家两天都能吃胖一些,简直是不可思议!!本来去北京玩几天都瘦了,结果回家吃了两天体重就增加了,很难想象……
言归正传!废话不多说,小编今天将会更新结构体模块,也希望本篇文章能够给大家带来一些学习结构体的帮助!!!
一、结构体类型的声明
1.1、什么是结构体?
结构体是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。
1.2、结构的声明:
struct name
{member-list;
}variable-list;
- name指的是该结构的名称
- member-list是指成员列表
- variable-list是变量列表
我们来描述一个人的信息
struct Stu {char name[20];int age;char id[12];float height;
};//这里的分号是编译器默认的,不可缺少的
结构名称就是Stu,也就是你要描述的对象;
成员列表是指描述对象的一些信息,比如说一个学生的姓名,年龄,学号,体重等信息
这样我们就可以认为struct定义了一个学生类型,括号里面的都是它的成员,可以有一个成员,也可以有多个成员,并且这些成员的类型是可以不相同的。
1.3、结构体变量的创建和初始化
1.3.1 结构体变量的创建
variable-list是变量列表,这里这个没有表现出来,解释一下
struct Stu是以一个结构体类型,类型是用来创建变量的,因此当我们有了结构体类型,就可以进行下面这个操作
//结构体类型 struct Stu {char name[20];int age;char id[12];float height; }; int main() {//创建3个结构体变量struct Stu s1;struct Stu s2;struct Stu s3;return 0; }
我们不仅可以将变量创建在main函数中,也可以放在结构体括号后面,比如说:
struct Stu {char name[20];int age;char id[12];float height; }s4,s5,s6;
这里的s4,s5,s6与前面的s1,s2,s3是一样的,唯一的区别就是s4,s5,s6是全局变量,s1,s2,s3是局部变量
1.3.2 结构体变量的初始化
struct Stu {char name[20];int age;char id[12];float height;
}s4,s5,s6;
int main()
{//创建3个结构体变量struct Stu s1;struct Stu s2;struct Stu s3;return 0;
}
接下来对s1进行初始化
第一种初始化(按照上面struct中成员顺序初始化):
struct Stu s1 = {"lisi",20,"20246061",57.2};
第二种初始化(使用.操作符):
struct Stu s1 = { .age = 20,.height = 78.2,.name = "lisi",.id = "2024123"};
第一种初始化需要根据成员的顺序进行一一对应的初始化,第二种初始化不需要按照顺序进行,只需要通过.操作符寻找到struct中的成员即可。
我们该如何打印呢?
printf("%d", s1.age);
这样便可以打印s1中的成员,我们也可以同时打印几个成员
printf("%s %d", s1.name,s1.age);
补充:.操作符的用法:结构体变量名.成员名
1.4、结构的特殊声明
结构体的特殊声明又称匿名结构体,顾名思义,匿名就是将结构体的名称隐藏起来,也就是结构体的不完全声明。
正常的结构体:
struct Stu {char name[20];int age;char id[12];float height;
}s;
匿名结构体:
struct
{char name[20];int age;char id[12];float height;
}s;
匿名结构体怎么使用呢?其实和正常的结构体是一样的
但是注意:匿名结构体类型只能使用一次
int main()
{s.age = 19;printf("%d", s.age);return 0;
}
也就是说在你创建匿名结构体类型后,创建一个变量使用一次之后,该匿名结构体类型便不能够再被使用。
举个栗子:
对于有名字的结构体来说:
struct Student {int age;char id[20];};struct Student s1, s2;
- 这里定义了 Student 结构体类型,可以方便地创建多个该类型的变量s1和s2。
- 但是对于匿名结构体,以下代码是错误的:
struct {int age;char name[20];} s1;struct {int age;char name[20];} s2;
- 虽然s1和s2的结构相同,但由于匿名结构体没有名称,编译器会认为它们是不同的、独立的匿名结构体类型,所以不能这样重复定义类似结构的变量。在实际应用中,如果需要创建多个相同结构的变量,应该使用有名字的结构体类型。
二、结构体的自引用
2.1、概念
在 C 语言中,结构体的自引用是指结构体内部包含一个指向自身类型的指针成员。这种结构允许构建动态数据结构,如链表、树等。(这里了解即可,后面会详细介绍这些知识)
2.2、通过链表来理解自引用:
火车大概都清楚把!
可以把结构体的自引用想象成一列火车。每个车厢就像是一个结构体。车厢里的人(对应结构体中的数据成员),还有一个连接装置(对应指向自身结构体类型的指针)。这个连接装置可以用来连接下一个车厢,这样就可以形成一长串的车厢,也就是链表。例如,我们有一个结构体代表车厢:
struct train{int people; // 车厢里的人,给每个人一个编号struct Carriage *next; // 连接下一个车厢的装置};
这就像火车车厢依次链接一样,通过next指针,可以将多个train结构体连接起来,从车头开始,沿着next指针就能遍历整列火车,这也就是链表。
三、结构体内存对齐
关于结构体的基本使用,就介绍到这里,接下来将会介绍一个相对来说比较重要的知识点,结构体的内存对齐。你可能会问学习这个有什么用呢?
还记得结构体是什么吗?结构体是一种类型,它和整型,字符型都是一样的,我们知道这些类型都是有大小的,整型的大小是4个字节,字符型的大小是1个字节,那么结构体类型的大小是多少呢?由于结构体的特殊性,因此,结构体类型的大小是需要计算的,那么该如何计算,就需要用到我们接下来要介绍的结构体内存对齐了。
3.1、对齐规则
(1)结构体的第一个成员的地址与结构体的起始地址相同,即偏移量为 0。
(2)其它成员变量要对齐到每个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值
- vs中默认的值是8
- Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
(3)结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大 的)的整数倍。
(4)如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
第二条理解:
从第二个成员开始,每个成员的存储地址必须是该成员类型大小(如果类型大小 小于等于 4 字节)或者是 4 字节(如果类型大小 大于 4 字节)的整数倍。例如,对于一个int类型(通常为 4 字节)的成员,它的存储地址必须是 4 的倍数;对于一个char类型(1 字节)的成员,它的存储地址只要是 1 的倍数即可。
第三条理解:
结构体的总大小必须是其内部最大成员类型大小(如果最大成员类型大小小于等于 4 字节)或者是 4 字节(如果最大成员类型大小大于 4 字节)的整数倍。这可能会导致在结构体的末尾添加填充字节。
文字解释可能有些难以理解,接下来结合例题来理解内存对齐
先来看两组结构体对比:
struct s1
{char c1;char c2;int n;
};
struct s2
{char c1;int n;char c2;
};
int main()
{printf("%zd\n", sizeof(struct s1));printf("%zd\n", sizeof(struct s2));return 0;
}
s1与s2的区别只有成员列表的顺序不同,接下来来打印s1与s2的大小。
哎!这是为何呢?明明s1与s2中的成员是一模一样的,为什么大小不同呢?
是不是很神奇?这里便涉及到了结构体的内存对齐
结构体s1的大小
c1是第一个成员,c1的大小是1,直接填写到起始地址(0)处即可,c2的大小是1,与8比较后对齐数是1,1是1的倍数,因此c2填写到地址为1的地方,n的大小是4,与8比较后对齐数是4,而2,3不是4的倍数,因此跳过从4开始填写,向后占4个空间,这时总大小便是从0~7,占8个位置,根据第三条结构体总大小需要是成员中最大值的倍数,s1中成员最大值是4,8是4的倍数,因此,最终结构体s1的大小即为8
补充:这里有待完善,描述的不是很清晰,还请大家先将就看一下,我会尽快将这一块整理清楚,介绍给大家,还请见谅!!!
四、结构体实现位段
4.1 什么是位段
位段的声明和结构是类似的,但有两个不同:
- 位段的成员必须是int,unsigned int 或 signed int ,在c99中位段成员的类型也可以选择其它类型。
- 位段的成员名后边又一个冒号和一个数字。
比如说:
struct A
{int a : 2;int b : 5;int c : 10;int d : 30;
};
这里的冒号和数字是什么意思呢?
注意:位段这里的位指的是2进制位
这里的位段A所占内存的大小是多少?
如果A是结构体的话,该占多少内存呢?
struct A
{int a;int b;int c;int d;
};
a占4个字节,b占4个字节,c占4个字节,d占4个字节,按照前面介绍的对齐方式,刚好对齐下来,从0到15,总共占16个字节。
那么位段A占多少字节呢?我们来算一下
printf("%d\n", sizeof(struct A));
运行结果是8,也就是说位段A所占内存是8个字节。似乎比结构体所占的空间要小,那么这个8是怎么算出来的呢?
按理来说每个整型都占4个字节,四个整型怎么能够算出来8个字节的呢?
因此,只能说冒号后面的数字所指的便是bite位了,只有这样的话才有可能算出来8个字节位。
位段中:a的意思是只占2个bite位,:b只占5个bite位,:c只占10个bite位,:d只占30个bite
这里又有一个问题,如果按照bite来算的话,位段A总共只占了47个bite位,一个字节是8个bite,那么应该只需要6个字节就可以了呀!为什么最后需要8个bite呢?
这里就涉及到了位段的内存分配。
4.2 位段的内存分配
位段是如何给它里面的成员分配空间的呢?
- 位段的成员可以是int,unsigned int ,signed int 或者是 char类型
- 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
- 位段涉及很多不确定因素,位段时不跨平台的,注意可移植的程序应该避免使用位段。
主要看第二个,位段开辟空间是根据所需来开辟,并且一次可以开辟4个字节或者1个字节
我们来用一个例子展示一下如何开辟的空间。
struct A
{char a : 3;char b : 4;char c : 5;char d : 4;
};
int main()
{struct A s = { 0 };s.a = 10;s.b = 12;s.c = 3;s.d = 4;return 0;
}
首先,我们先看一下位段中的成员,abcd都是char类型的,因此我们一次开辟一个字节的空间,一个字节8个bite位。
先开辟一个字节的空间,此时呢我们有8个bite,拿出3个用来存放a,这时候又会出现一个问题,是从左边开始存放还是右边存放呢?这里C语言并没有明文规定,因此我们先假设从右边开始存放
在第一个字节中,我们存放了a和b, 由于开辟的第一个字节空间不够存放c 因此我们在开辟一个字节的空间。第一个字节中还剩下的空间只能浪费。为了存放d,我们需要在开辟一个字节
如果按照这样分析的话,该位段占3个字节,我们来运行一下程序
结果是3,也就是说前面的分析是没有问题的。
vs上位段的开辟方式:
- 如果是char类型,我们就一个字节一个字节的开辟
- 一个字节内部,从右向左使用,如果剩余的空间不够下一个成员使用的话,浪费掉,然后开辟新的空间存放。
但是注意:在vs上是这样开辟不代表所有的都是这样开辟的,因此位段是不能够跨平台的。
现在我们来算一下第一个例子中位段的大小是8个字节。
struct A
{int a : 2;int b : 5;int c : 10;int d : 30;
};
位段成员类型是整型,因此一下开辟4个字节的空间,也就是32个bite位,从右侧开始存放
所以最终该位段A占8个字节的空间。
4.3 位段的跨平台问题
之所以说位段是不跨平台的有以下几点:
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定,(16位机器最大16,32位机器最大32,写成17,在16位上会出问题。
- 位段中的成员在内存中从左向右分配还是从右向左分配,标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳与第一个位段剩余的位时,是舍弃剩余的位还是利用,是不确定的。
总结以下:
和结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题。
4.4 位段使用的注意事项
4.4.1 位数限制:
位段占的二进制位数不能超过该基本类型所能表示的最大位数。例如在常见的 32 位系统中,如果使用int类型定义位段,那么最多只能是 32 位;如果超出这个范围,编译器可能会报错或者产生不可预期的结果。
4.4.2 取地址操作限制:
不能对位段进行取地址操作。因为位段的成员在内存中的位置可能不是按照字节对齐的,取地址操作可能会导致不可预期的结果,所以编译器不允许这样做。如果需要给位段成员赋值,可以先将值存入一个普通变量中,再将该变量的值赋给位段成员。
4.4.3 整型升级:
若位段出现在表达式中,则会自动进行整型升级,自动转换为int型或者unsigned int型。这可能会影响表达式的计算结果,特别是在与其他类型的数据进行混合运算时,需要注意数据类型的转换和结果的准确性。
4.4.4 赋值范围:
对位段赋值时,最好不要超过位段所能表示的最大范围。例如,一个定义为 2 位的位段,其取值范围是 0~3,如果赋给它一个大于 3 的值,可能会导致数据丢失或其他不可预期的结果。
4.4.5 数组限制:
位段不能出现数组的形式。即不能定义位段数组,因为位段的存储方式和普通数组的存储方式不兼容,这样的定义是不合法的。
4.4.6 跨平台问题:
不同的编译器和平台可能对位段的实现细节存在差异,如位段的存储顺序(从左到右或从右到左)、位段是否支持跨字节边界等。如果代码需要在不同的平台上运行,要特别注意位段的可移植性问题,尽量避免依赖特定平台的实现细节。
结束语:
本篇文章介绍了结构体相关知识,不够可能是小编还没有学到通透,有些地方介绍的比较模糊,还希望各位看官大人见谅!后续小编也希望能够写出越来越好的文章,也希望能够帮助大家整理C语言的相关知识点。最后!期待我们都能变得越来越好!成为自己想做的人!
还有一个重磅消息!小编即将更新Java系列,希望各位能够捧捧场,来看一看,敬请期待!!1