1.结构体类型的声明
1.1 结构体
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构的声明
struct tag
{
member-list;
}variable-list;
// 结构体类型的声明 -- 假设描述一个学生structStu{charname[20];// 姓名intage;// 年龄charsex[5];// 性别charid[20];// 学号};// 分号不能丢结构体变量的创建和初始化
- 注意:
结构体的声明之后,使用前必须手动赋值,否则是随机未知的值。
// 结构体变量的创建structStu{charname[20];intage;charsex[5];charid[20];}s3,s4;// 方式三// 方式二:全局变量structStus2;// 结构体变量的初始化intmain(){// 方式一:局部变量// 初始化方式一:按照结构体成员的顺序初始化structStus1={"张三",20,"男","20230818001"};printf("name: %s\n",s1.name);// 张三printf("age: %d\n",s1.age);// 20printf("sex: %s\n",s1.age);// 男printf("id: %s\n",s1.id);// 20230818001// 初始化方式二:按照指定的顺序初始化structStus5={.age=18,.name="lisi",.id="20230818002",.sex="女"};printf("name: %s\n",s1.name);// lisiprintf("age: %d\n",s1.age);// 18printf("sex: %s\n",s1.age);// 女printf("id: %s\n",s1.id);// 20230818002return0;}2.结构的特殊声明
- 在声明结构的时候,可以不完全的声明。
- 匿名结构体类型 - 只能创建一次结构体,后续不能再创建结构体
struct{inta;charb;floatc;}x;struct{inta;charb;floatc;}*p=&x;警告:编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
匿名的结构体类型,如果没有对结构体重命名的话,基本上只能使用一次。
可以使用typedef进行类型重定义操作
typedefstruct{inta;charb;floatc;}S;intmain(){S s1={4,'a',4.0};return0;}3.结构的自引用
在结构体中包含一个类型为该结构体本身的成员是否可以?
比如,定义一个链表的节点:
代码块 struct Node { int data; struct Node next; // 访问下一个节点 };其实这是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。
正确的自引用示范:
代码块 struct Node { int data; struct Node* next; // 访问下一个节点指针 };结构体自引用使用过程中,夹杂了typedef对匿名结构体类型重命名,也容易引入问题比如下面代码。
typedef struct { int data; Node* next; }Node;因为Node是对前面匿名结构体的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。
解决办法:
定义结构体不要使用匿名结构体了。
typedef struct Node { int data; struct Node* next; }Node; int main() { Node n1; printf("%zu\n", sizeof(struct Node)); // 16 return 0; }4.结构体内存对齐(Structure Memory Alignment)
本节深入讨论一个问题:计算结构体的大小。这是特别热门的考点:结构体内存对齐。
补充知识点
offset()C语言标准宏 – offset of member 成员偏移量
函数原型
#define offsetof(type, member)((size_t)&((type*)0)->member)- 参数
type:结构体类型member:结构体里的成员 - 返回值:
size_t类型,该成员相对于结构体首地址的字节偏移量。 - 本质是宏,不是函数。
- 包含头文件
<stddef.h>
宏和函数的核心区别
- 本质区别:
宏Macro是预处理阶段的文本替身,没有类型检查,直接简单替换字符串。
函数Function是编译阶段编译指令,有函数调用栈、参数压栈、返回值,有类型检查。- 详细对比:
2.1 执行阶段不同:
宏:预处理阶段(编译前直接文本替换)
函数:编译+运行阶段,运行时调用
2.2 参数处理不同
宏:无类型检查,原样替换,容易出现运算优先级Bug。
如#define ADD(a, b) a+b
ADD(2, 3)*4 —> 2+3*4 = 14
函数:有类型检查,参数先计算值在传入
2.3 开销不同
宏:无调用开销,直接展开代码,速度快、但代码冗余(多处使用会复制多份)
函数:有调用开销(压栈、跳转、返回);代码复用,体积更小。
2.4 作用域与安全
宏:无作用域,全局生效,不做参数计算,会重复执行。
函数:参数值计算一次,安全,有作用域。
2.5 调试
宏:不能断点调试,展开后才是调试
函数:可正常断点调试
4.1 对齐规则
首先得掌握结构体的对齐规则:
- 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
- 从第2个成员变量开始,都要对齐到某个对齐数的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小 的较小值
VS中默认的值为8
Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
- 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
#include<stddef.h>#include<stdio.h>structS1{// 成员变量大小 VS默认对齐数 对齐数charc1;// 1 8 1inti;// 4 8 4charc2;// 1 8 1}intmain(){// 查看结构体成员变量对齐的起始偏移量printf("%zu\n",offset(structS1,c1))// 0printf("%zu\n",offset(structS1,i))// 4printf("%zu\n",offset(structS1,c2))// 8printf("%zu\n",sizeof(structS1));// 12}分析:
该结构体变量的对齐数为4,所以第二个变量在偏移量为4的地址处,所以该变量的大小为 1+3(浪费)+4+1=9 -> 最大对齐数为4,结构体总大小为4*3=12
#include<stddef.h>#include<stdio.h>structS2{// 成员变量大小 VS默认对齐数 对齐数charc1;// 1 8 1charc2;// 1 8 1inti;// 4 8 4};intmain(){printf("%zu\n",offsetof(structS2,c1));// 0printf("%zu\n",offsetof(structS2,c2));// 1printf("%zu\n",offsetof(structS2,i));// 4printf("%zu\n",sizeof(structS2));// 8return0;}分析:
c1的偏移量是0,c2的偏移量是1,i的对齐数是4,所以偏移量是4,结构体变量大小是1+1+2(浪费)+4=8 -> 最大对齐数是4,结构体总大小为4*2=8
#include<stddef.h>#include<stdio.h>structS3{// 成员变量大小 VS默认对齐数 对齐数doubled;// 8 8 8charc;// 1 8 1inti;// 4 8 4};intmain(){printf("%zu\n",offsetof(structS3,d));// 0printf("%zu\n",offsetof(structS3,c));// 8printf("%zu\n",offsetof(structS3,i));// 12printf("%zu\n",sizeof(structS3));// 16return0;}分析:
第一个成员变量d的偏移量是0,c的偏移量是8,i的偏移量是12,结构体变量大小是8+1+7(浪费)+4=12,最大对齐数是8,所以结构体变量总大小是8*2=16
#include<stddef.h>#include<stdio.h>structS3{// 成员变量大小 VS默认对齐数 对齐数doubled;// 8 8 8charc;// 1 8 1inti;// 4 8 4};structS4{// 成员变量大小 VS默认对齐数 对齐数charc1;// 1 8 1structS3s3;// 16 8 8doubled;// 4 8 4};intmain(){printf("%zu\n",sizeof(structS3,c1));// 0printf("%zu\n",sizeof(structS3,s3));// 8printf("%zu\n",sizeof(structS3,d));// 24printf("%zu\n",sizeof(structS3));// 32return0;}分析:
d的偏移量是0,c的偏移量是1+7,i的偏移量是8+16=24(3*8),结构体变量大小是1+7(浪费)+16+8=32,最大对齐数是8,结构体总大小是32
4.2 为什么存在内存对齐?
大部分参考资料都是这样说的:
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的整数倍。如果能保证将所有的double类型的数据的地址都对齐成8的整数倍,那么就可以用一个内存操作来读或者写值了。否则,可能需要执行两次内存访问。因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。 - 在设计结构体的时候,既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中在一起。(成员顺序可以调整的情况)
4.3 修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
- 一般修改的对齐数都是2的次方数,1、2、4、8、。。。
// 设置默认对齐数为1#pragmapack(1)structS{charc1;// 1 1 1inti;// 4 1 1charc2;// 1 1 1};// 取消设置的对齐数,还原为默认。#pragmapack()intmain(){printf("%zu\n",sizeof(structS));// 6return0;}5.结构体传参
- 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
- 结论:
- 结构体传参的时候,要传结构体的地址。
- 推荐
print2()
structS{intdata[1000];intnum;};// 值传递 - 不推荐,还要创建一次变量,可能结构体过大voidprint1(structSt){for(inti=0;i<5;i++){printf("%d ",t.data[i]);}printf("\n");printf("%d\n",t.num);}// 址传递 - 推荐,不会额外开辟空间voidprint2(conststructS*ps){for(inti=0;i<5;i++){printf("%d ",ps->data[i]);}printf("\n");printf("%d\n",ps->num);}intmain(){structSs={{1,2,3,4,5},100};print1(s);print2(&s);return0;}6.结构体实现位段
6.1 什么是位段
- 位段的声明和结构体是类似的,有两个不同
- 位段的成员必须是
int、unsigned int或signed int,在C99中位段成员的类型也可以选择其他整形家族类型,比如char。 - 位段的成员名后边有一个冒号和数字。
语法:
- 位段的成员必须是
strcut 位段名 { 类型 成员名 : 占用位数; // 核心:冒号+数字指定位数 };注意:占用位数不能超过类型本身的位数(如
int最多32位)
structB{int_a;// 32bitint_b;// 32bitint_c;// 32bitint_d;// 32bit};structA{int_a:2;// 1字节int_b:5;// 1字节int_c:10;// 2字节int_d:30;// 4字节};// 2+5+10+30=47bitintmain(){printf("%zu\n",sizeof(structB));// 16printf("%zu\n",sizeof(structA));// 8return0;}6.2 位段的内存分配
- 位段的成员可以是
int、unsigned int、signed int或者char等所有整型类型。 - 位段的空间上是按照需要以4字节(int)或者1个字节(char)的方式来开辟。
- 位段涉及很多不稳定因素,位段是不跨平台的,注意可移植的程序应该避免使用位段。
vs环境下:
每个字节分配的内存空间是从右向左使用的(低位向高位)
剩余的空间不够下一个成员使用的时候,直接浪费。
6.3 位段的跨平台问题
int位段被当成有符号数还是无符号数是不确定的。- 位段中最大的数目不能确定(早期16位机器int最大16,32位机器最大32,写成27,在16为机器会出问题)。
- 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
- 总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间。
但是有跨平台的问题存在。
structS{chara:3;charb:4;charc:5;chard:4;};/* 解析:内存中每个字节中假设从右向左使用 b a c d 0 1100 010 |000 0 0011 |0000 0100 内存中是 6 2 0 3 0 4 小端存储为:62 03 04占用3个字节 */intmain(){structSs={0};s.a=10;// 0000 1010s.b=12;// 0000 1100s.c=3;// 0000 0011s.d=4;// 0000 0100return0;}6.4 位段的应用
网络协议:IP数据报的格式,很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。
6.5 位段使用的注意事项:
- 位段的几个成员共用同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。
- 所以不能对位段的成员使用
&操作符,这样就不能scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。
structC{int_a:2;int_b:5;int_c:10;int_d:30;};intmain(){structCsa={0};//scanf("%d", &sa._a); // err// 正确的示范intb=0;scanf("%d",&b);sa._b=b;return0;}7.练习
练习1:变种水仙花数
变种水仙花数 - Lily Number:把任意的数字,从中间拆分成两个数字,
比如1461可以拆分成(1和461),(14和61),(146和1),如果所有拆分后的乘积之和等于自身,
则是一个Lily Number。
例如:
655 = 6 * 55 + 65 * 5
1461 = 1461 + 1461 + 146*1
求出5位数中的所有 Lily Number。
输入描述:
无
输出描述:
一行,5位数中的所有 Lily Number,每两个数之间间隔一个空格。
#include<math.h>#include<stdio.h>intmain(){// 1. 遍历五位数的整数for(inti=10000;i<=99999;i++){intret=0;// 保存加起来的和intexp=1;// 10的指数inta=0;// 拆分五位数的前半部分intb=0;// 拆分五位数的后半部分while(exp<5){a=i/pow(10,exp);b=i%pow(10.exp);ret+=a*b;exp++;}// 判断是否是变种水仙花数,将其打印出来if(ret==i){printf("%d ",i);}}return0;}// 方式二intmain(){for(inti=10000;i<=99999;i++){intsum=0;for(intj=10;j<=10000;j*=10){sum=sum+(i/j)*(i%j);}if(sum==i){printf("%d ",i);}}}2.练习2:序列中去除指定数字
描述
有一个整数序列(可能有重复的整数),现删除指定的某一个整数,输出删除指定数字之后的序列,序列中未被删除数字的前后位置没有发生改变。
若序列中有多个指定的数,需要一起删除。
数据范围:序列长度和序列中的值都满足
1≤n≤50
输入描述:
第一行输入一个整数(0≤N≤50)。
第二行输入N个整数,输入用空格分隔的N个整数。
第三行输入想要进行删除的一个整数。
输出描述:
输出为一行,删除指定数字之后的序列。
intmain(){// 1.输入整数intN;scanf("%d",&N);// 2.输入N个整数intarr[N];// 可变数组,VS不支持,可以改成int arr[50] = { 0 };for(inti=0;i<N;i++){scanf("%d",&arr[i]);}// 3.输入需要删除的数字intdel;scanf("%d",&del);intj=0;for(inti=0;i<N;i++){if(arr[i]!=del){arr[j]=arr[i];j++;}}return0;}intmain(){// 1.输入整数intN;scanf("%d",&N);// 2.输入N个整数intarr[N];// 可变数组,VS不支持,可以改成int arr[50] = { 0 };for(inti=0;i<N;i++){scanf("%d",&arr[i]);}// 3.输入需要删除的数字intdel;scanf("%d",&del);for(inti=0;i<N;i++){if(arr[i]==del){// 将后面的序列全部向前移一位for(intk=i;k<N-1;k++){arr[k]=arr[k+1];}N--;i--;}}return0;}莫因表格遮前路,代码长明赴山海