目录
前言
一、结构体类型的声明
1.结构的声明
2.结构体变量的创建和初始化
3.结构的特殊声明
4.结构的自引用
二、结构体内存对齐
1.对齐规则
2.为什么存在内存对齐?
三、结构体传参
四、结构体实现位段
1.什么是位段
2.位段的内存分配
3.位段的跨平台问题
4.位段的应用
5.位段使用的注意事项
总结
前言
我们已经将C语言中的内置类型学完了,下面我们来学习一下C语言的自定义类型,结构体。
一、结构体类型的声明
1.结构的声明
结构是一些值的集合,这些值称为成员变量。
结构的每个成员可以是不同类型的变量,如: 标量、数组、指针,甚至是其他结构体。
struct tag
{
member-list;
}variable-list;
- tag : 称呼 ,表示这个结构体的名字
- member-list : 成员列表 ,是写成员变量的地方
- variable-list : 变量列表 ,是写属于这个结构的全局变量的地方,可写可不写
eg:
struct Stu
{
char name[20];
int age;
char sex;
}s1,s2,s3;
这里的s1,s2,s3的作用s4相同
Struct Stu s4;
这里创建了一个Stu类型的全局变量s4
2.结构体变量的创建和初始化
#include <stdio.h>
struct Stu
{
char name[20];
int age;
char sex;
};
int main() {
//方式一:根据结构体成员顺序书写
struct Stu s1 = { "张山",32,"男" };
//方式二:根据指定顺序书写
struct Stu s2 = { .name = "李斯" ,.sex = "男" , .age = 23 };
//输出方式
printf("name: %d", s1.age);
return 0;
}
3.结构的特殊声明
匿名的结构体类型,即省略tag(称呼)
struct
{
char name[20];
int age;
char sex;
}s1;
这种声明只能用一次
4.结构的自引用
这里先介绍两种数据结构,顺序表和链表
顺序表可以用数组来实现,没啥好说的
在链表中,这五个方块都是一个节点,分别存储着数据和下个节点的位置,这样才能在纷乱的位置中依次找出数据
那我们可以这这样做吗?
struct INT
{
int data;
struct INT n;
};
可是这样做是错的,因为sizeof(struct INT)是多少呢?
我们无法计算它的大小,它会无穷的大
正确的自引用方式:
struct INT
{
int data;
struct INT* n;
};
一个结构体指针变量,这个变量指向下个节点的地址,并且指针变量可以求出大小 ,没有上述的问题
还有,当结构体自引用时,夹杂了typedef对结构体类型重命名的问题
typedef struct INT
{
int data;
INT* n;
}INT;
这里会出现错误,因为结构体内提前使用INT,无法分辨INT是啥类型
解决办法: 定义结构体不要使用匿名结构体
typedef struct INT
{
int data;
struct INT* n;
}INT;
二、结构体内存对齐
1.对齐规则
- 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
- 对齐数=编译器默认的⼀个对齐数与该成员变量大小的较小值
- VS中默认的值为8
- Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
- 结构体总大小为最大对齐数 (结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的) 的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构 体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
举个例子:
int main() {
struct S1
{
char c1; //1 8 -> 1
int i; //4 8 -> 4
char c2; //1 8 -> 1
};
printf("%zd\n", sizeof(struct S1));
}
规则1, 结构体第一个变量c1在结构体变量起始位置偏移量为0处
规则2,根据对齐数,c2的对齐数是4,所以跳过前面,到4处; c3的对齐数是1,到8处
规则3,c1,c2,c3的对齐数分别是1,4,1 最大对齐数是4,所以该结构体总大小为12
2.为什么存在内存对齐?
大部分的参考资料都是这样说的:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
当我们要满足对齐,又要节省空间,办法是:
让占用空间小的成员尽量集中在一起
当然,结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
#pragma pack(1) //设置默认对⻬数为1
#pragma pack() //取消设置的对⻬数,还原为默认
三、结构体传参
传参分为两种
struct Stu
{
char name[1000];
int num;
};
struct Stu s = { "大明", 1000};
//结构体传参
void print1(struct Stu s)
{
printf("%s\n", s.name);
}
//结构体地址传参
void print2(struct Stu* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
当然print2 比 print1 好
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下 降。
结论:
结构体传参的时候,要传结构体的地址。
四、结构体实现位段
1.什么是位段
位段和结构体类似,但有两个不同的地方
- 位段的成员类型有要求,只能是int、unsigned int 、signed int ,只是在C99中位段的成员类型有其他类型(如char....)
- 位段的成员名后面有一个冒号(:)和一个数字(这个数字表示比特位的数量)
例子:
struct A
{
int a:2;
int b:5;
int c:10;
};
这个结构体的大小是4个字节
为什么呢?
2.位段的内存分配
位段的空间是按照成员类型来分配内存的,我们再举个例子
struct A
{
char a:2;
char b:4;
char c:6;
char d:7;
};
如: 第一个成员类型是char类型,那么会先给1个字节
这里还要说明一点,位段的储存顺序是不确定的,可能是从左往右,也可能是从右往左,我们假设是从右往左
在这个字节中,我们将a存入,a占两个比特位,接着存入b,b占4个比特位
但当我们要存入c时,剩下的空间不够了(利用还是舍弃不确定,这里舍弃),于是再根据c的类型,生成一个字节
d同理
综上所示,这个结构体的大小就是3个字节
位段涉及很多不确定的因素,是无法跨平台的,注重可移植的程序应该避免使用位段
3.位段的跨平台问题
- int位段被当成有符号还是无符号数是不确定的
- 位段中最大的数目不能确定。(16位机器最大16,32位机器最大32。当写成27时,在16位机器就会出现问题)
- 位段中成员在内存中从左向右分配,还是从右向左分配,标准尚未定义
- 当一个结构体包含两个位段,第二个位段成员比较大时,无法容纳于第一个位段剩余的位时,是舍弃剩余的位段还是利用,这是不确定的
总结:
跟结构相比,位段可以达到相同的效果,并且可以很好的节约空间,但有跨平台的问题存在
4.位段的应用
下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。
5.位段使用的注意事项
因为同一个字节中可能会有多个位段成员,而这些成员的起始位置并不是某个字节的起始位置,所以这些位置处是没有地址的
内存中每个字节分配一个地址,但字节内的bit位是没有地址的
因此我们不能对位段的成员使用&操作符
总结
结构体在C语言中并不难,学完后找一些题做做就行啦