👦个人主页:Weraphael
✍🏻作者简介:目前正在回炉重造C语言(2023暑假)
✈️专栏:【C语言航路】
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、结构体的声明
- 1.1 结构的概念
- 1.2 结构的定义
- 1.3 结构成员的类型
- 1.4 结构体变量的初始化
- 1.5 结构的特殊声明
- 1.6 结构的自引用
- 二、结构体成员的访问
- 2.1 变量 . 成员
- 2.2 指针变量 -> 成员
- 2.3 解引用访问
- 三、结构体嵌套
- 四、结构体传参
- 4.1 传值调用
- 4.2 传址调用
- 4.3 传值调用和传址调用的区别
- 五、结构体内存对齐(重点)
- 5.1 计算结构体大小
- 5.2 嵌套结构体大小计算
- 5.3 验证偏移量为0的地址处
- 5.4 修改默认对齐数
- 六、实战 - 通讯录
- 七、位段
- 7.1 什么是位段
- 7.2 位段的大小
- 7.3 位段的内存分配
- 7.4 位段跨平台问题。
- 7.5 总结
一、结构体的声明
1.1 结构的概念
- 结构是一些=值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。
- 类比数组。数组也是一些值的集合,但类型是相同的。
1.2 结构的定义
结构体是用来描述复杂对象的
// tag - 是标签名,是可以根据需求改变的
struct tag //这一整串是类型
{
//代码块里是结构体成员
//可以是不同类型
member_list // 成员列表
}list;
//list - 全局结构体变量
//注意:后面有分号
还可以用typedef
来定义
//描述一位学生
typedef struct tag
{
member_list // 成员列表
}tag; //tag- 将结构体类型struct tag重新命名为tag
//分号不可缺
通过上面的模板来举个例子
假设要描述一名学生(名字、年龄、分数)
struct student //类型
{
//结构体成员
char name[20];
int age;
double score;
}stu,stu2;
// stu、stu2 - 也是结构体变量(全局变量)
int main()
{
// 创建结构体变量(局部变量)
// 类型 + 变量
struct student stu1;
return 0;
}
1.3 结构成员的类型
结构的成员可以是标量、数组、指针,甚至是其他结构体。
1.4 结构体变量的初始化
struct student //类型
{
char name[20];
char sex[10];
int age;
double score;
};
int main()
{
//结构体变量初始化
struct student stu1 = { "小明","男",18,72.5 };
return 0;
}
1.5 结构的特殊声明
在声明结构的时候,也可以不完全的声明。
【匿名结构体类型(缺少标签)】
// 匿名结构体类型(不愿起名)
struct
{
int a;
char b;
double d;
}s;
注意:
- 若要使用匿名结构体类型,在创建类型马上在后面创建变量
- 如果两个匿名结构体中的成员变量完全一样也会被编译器当成两个完全不同的类型
1.6 结构的自引用
在结构中包含一个类型为该结构本身的成员是否可以?
【举个例子】
假设内存中有1,2,3。它们不是连续存储的,而是随机分布的。那应该如何存储它们呢?我们可以使用链表(是一种数据结构),一个节点中存储一个数,并且当前的节点能够找到下一个节点。所以我们可以把节点定义成一个结构体
【错误代码】
struct Node
{
int data;
struct Node next;
};
上面的代码是错误的,假如说要计算结构体的大小,
data
是4个字节,next
包含date
和下一个节点,下个next
又包含date
和下一个节点…,这样下来会发现它的大小其实是very very大的。
【正确代码】
我们可以放一个指向下一个节点的指针,也就是结构体指针
struct Node
{
int data; //4个字节
struct Node* next; //4/8个字节
};
那么接下来我把代码改成这样是否也是正确的?
typedef struct Node
{
int data;
Node* next;
}Node;
这其实是错误的!因为代码编译是从上到下的,当来到
Node* next
时,struct Node
还未被typedef
重命名
【正确代码】
typedef struct Node
{
int data;
struct Node* next;
}Node;
二、结构体成员的访问
往期博客链接:点击传送
2.1 变量 . 成员
#include <stdio.h>
struct student //类型
{
char name[20];
int age;
char sex[10];
double score;
};
int main()
{
//结构体变量初始化
struct student stu1 = { "小明",18,"男",95.5 };
//结构体访问
printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n", stu1.name, stu1.age, stu1.sex, stu1.score);
return 0;
}
【程序结果】
2.2 指针变量 -> 成员
#include <stdio.h>
struct student //类型
{
char name[20];
int age;
char sex[10];
double score;
};
int main()
{
//结构体变量初始化
struct student stu1 = { "小明",18,"男",95.5 };
struct student* pa = &stu1;
//结构体访问
printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n", pa->name,pa->age,pa->sex,pa->score);
return 0;
}
【程序结果】
2.3 解引用访问
#include <stdio.h>
struct student //类型
{
char name[20];
int age;
char sex[10];
double score;
};
int main()
{
//结构体变量初始化
struct student stu1 = { "小明",18,"男",95.5 };
struct student* pa = &stu1;
//结构体访问
//*pa就是stu1
printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n",(*pa).name,(*pa).age,(*pa).sex,(*pa).score);
return 0;
}
【程序结果】
三、结构体嵌套
#include <stdio.h>
struct S
{
int a;
char c;
};
struct P
{
double d;
struct S s;
double f;
};
int main()
{
//100和's'是结构体s的
struct P p = { 5.5,{100,'s'},33.3 };
//只打印嵌套的结构体
printf("%d %c\n", p.s.a, p.s.c);
return 0;
}
【程序结果】
只要一步一步去访问,嵌套结构体同样也能访问。当然也可以用指针来访问,这里就不为大家演示了。
四、结构体传参
4.1 传值调用
注意:如果使用传值调用,形参的改变不影响实参。
#include <stdio.h>
struct student
{
char name[20];
int age;
char sex[6];
double score;
};
//不需要返回参数用void
void Print(struct student stu) //传结构体变量同样也能用结构体变量来接收
{
printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n", stu.name, stu.age, stu.sex, stu.score);
}
int main()
{
struct student stu1 = { "小明",18,"男",95.5 };
//封装一个Print函数负责打印
Print(stu1);
return 0;
}
【程序结果】
4.2 传址调用
#include <stdio.h>
struct student
{
char name[20];
int age;
char sex[6];
double score;
};
//不需要返回参数用void
void Print(struct student* stu) //传地址需要用指针来接收
{
printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n", (*stu).name, (*stu).age, (*stu).sex, (*stu).score);
// 或者还可以使用->操作符来访问
//printf("名字:%s\n年龄:%d\n性别:%s\n分数:%.1lf\n", stu->name, stu->age, stu->sex, stu->score);
}
int main()
{
struct student stu1 = { "小明",18,"男",95.5 };
//封装一个Print函数负责打印
Print(&stu1);
return 0;
}
【程序结果】
4.3 传值调用和传址调用的区别
那么大家认为传值调用好还是传址调用好呢?
答案是:传址调用
- 因为传址调用时,形参也有自己独立的空间,把实参传递(拷贝)给形参就要消耗时间,这时传参的压力就比较大。如果是传址调用,因为一个地址的大小无非就是4/8个字节,形参用指针变量接收,压力也相对较小些。
- 官方说法:函数传参的时候,参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
因此,建议大家在结构体传参时,都使用传址调用
五、结构体内存对齐(重点)
5.1 计算结构体大小
在了解结构体内存对齐之前,我们先想想 如何计算结构体的大小
#include <stdio.h>
struct Stu1
{
char c;
int a;
double x;
};
struct Stu2
{
char c;
double x;
int a;
};
int main()
{
printf("Stu1:%d\n", sizeof(struct Stu1));
printf("Stu2:%d\n", sizeof(struct Stu2));
return 0;
}
【程序结果】
为什么
Stu1
和Stu2
的成员变量一样,就仅仅交换了位置,结构体大小却不一样?这就要涉及到 结构体内存对齐
🎈结构体内存对齐的规则
- 第一个成员在与结构体变量偏移量为
0
的地址处- 其他成员变量要对齐到某个数字 (对齐数)的整数倍的地址处。
注:对齐数是 编译器默认对齐数与该成员类型大小的较小值。
VS中默认对齐数为8
Linux环境下无对齐数,对齐数就是结构体成员的自身大小- 结构体总大小为所有结构体成员最大对齐数的整数倍。
现在解释开头的代码
以下解析默认是在visual studio
环境下
【Stu1】
【Stu2】
5.2 嵌套结构体大小计算
#include <stdio.h>
struct Stu1
{
char c;
int a;
double x;
};
struct Stu2
{
char c;
struct Stu1 s1;
int a;
};
int main()
{
printf("Stu2:%d\n", sizeof(struct Stu2));
return 0;
}
如果结构体中嵌套了结构体成员,要将嵌套的结构体成员对齐到自己成员中最大对齐数的整数倍处,最后的最大对齐数还要包括嵌套成员的对齐数。
5.3 验证偏移量为0的地址处
C语言有一个宏叫offsetof
,它的功能是计算一个结构体成员相较起始位置的偏移量(头文件:#include <stddef.h>
)
5.4 修改默认对齐数
在计算结果体大小中,我们提到了默认对齐数,在VS中默认对齐数为8,那能否修改默认对齐数呢?
可以用#pragma
这个预处理指令
#pragma pack(需要修改的对齐数)
【例如】
那么接下来问题来了,怎么恢复呢?
// 恢复默认对齐数
#pragma pack()
六、实战 - 通讯录
点击跳转
七、位段
7.1 什么是位段
位段的声明和结构其实是类似的,但有两个不同:
- 位段的成员必须是
int
、unsigned int
或signed int
,但也可以是char
类型,因为char
是属于整型家族的- 位段的成员名后边有一个冒号和一个数字
- 位段里的成员一般都是同类型的
- 位段的 位 其实表示二进制位
【举个例子】
#include <stdio.h>
struct segment
{
int a : 2; //表示a只占内存的2个二进制位
int b : 5;//表示b只占内存的5个二进制位
int c : 10;//表示c只占内存的10个二进制位
int d : 30;//表示d只占内存的30个二进制位
};
struct segment
就是一个位段类型。
7.2 位段的大小
结构体的大小计算和位段的计算是一样的吗?答案其实是不一样的
位段的计算过程其实是这样的
int
是4个字节,有32个比特位,a
占内存2个二进制位,还剩下30个比特位b
占内存5个二进制位,还剩下25个比特位c
占内存10个二进制位,还剩下15个比特位d
占内存30个比特位,上一步还剩下15个比特位,假设把这15个比特位舍弃,继续向内存申请32个比特位给d
用,所以还剩余2个比特位
从上过程中,类型在创建的时候向内存一共申请了32+32
个比特位,也就8
字节。也能看出位段其实比结构体更加节省空间,有多少用多少。
7.3 位段的内存分配
- 位段的成员可以是
int
、unsigned int
、signed int
或者是char
类型- 位段的空间上是按照需要以4和字节(
int
)或者1个字节(char
)的方式来开辟的- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
【举个例子】
我们一起看看下面这一串代码在内存中是如何分配的
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
}
首先先定制一些规则:
- 假设位段中的成员在内存中是从低位向高位分配
- 当第二个位段成员占内存二进制位比较大时,无法容纳前一个剩余位段时,直接把前一个剩余的舍弃
- 在visual studio测试
分配过程如下:结构体初始化为000000000
a
被赋值成10
,转化为二进制:00001010
,而a
只占内存3
个二进制位,也就是010
(最高位的1
被截断),分配:00000010- b被赋值成
12
,转化为二进制:00001100
,b占内存4
个二进制位,恰好能容纳上一个剩余位段。分配01100010c
被赋值成3
,转化为二进制:00000011
,c
只占内存5
个二进制位,也就是:00011
,然后这次的位段成员无法容纳上一个剩余位段,我们就舍弃,继续向内存申请:00000000
,分配:00000011d
被赋值成4
,转化为二进制:00000100
,d
只占内存4
个二进制位,然而上一个剩余位段还是无法容纳这次的成员位段,继续像内存申请:00000000
,分配:00000100- 所以现在内存里为:
0110010
00000011
00000100
转化为十六进制也就是62 03 04
,我们F10调试,在内存中看看
这结果恰好就是我们分析出来的结果!
7.4 位段跨平台问题。
int
位段被当成有符号数还是无符号数是不确定的- 位段中最大位的数目不能确定。16位机器最大16,32位机器最大32,如果
int
位段成员占内存27个二进制位,那么在16位机器会出现问题- 位段中的成员咋子内存中从左向右分配,还是从右向左分配标准还未确定。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余位时,是舍弃剩余的位还是利用,这也是未确定的。
7.5 总结
跟结构体相比,位段可以达到同样的效果,但是位段可以很好的节省空间,但是有跨平台的问题存在