目录
1.结构体类型的声明
2.结构体变量的创建和初始化
2.1结构体成员的直接访问
2.2结构体成员的间接访问
2.3结构体变量的创建和初始化
3.结构的自引用
4.结构体内存对齐
4.1对齐规则
4.2为什么存在内存对齐?
4.3修改默认对齐数
5.结构体传参
6.结构体实现位段
6.1什么是位段
6.2位段的内存分配
6.3位段的缺陷
1.结构体类型的声明
struct tag
{
member-list;
}variable-list;
例如描述一个学生:
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
};
2.结构体变量的创建和初始化
2.1结构体成员的直接访问
结构体成员的直接访问是通过点操作符(.)来访问的
使用格式为
结构体变量名.结构体成员名
2.2结构体成员的间接访问
如果我们得到的是一个结构体的地址,而不是结构体本身,这时我们就要使用另外一个操作符来进行操作了。格式如下:
结构体指针->成员名
2.3结构体变量的创建和初始化
结构体变量类型为:struct 类型名
这里我们也可以通过typedef来重命名一下我们的结构体名,这样子我们就可以不写struct了。
现在我们通过代码来实践一下上述内容
#include <stdio.h>
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "裤裤",18,"男","123456789" };
printf("name:%s\n", s.name);
printf("age:%d\n", s.age);
printf("sex:%s\n", s.sex);
printf("id:%s\n", s.id);
//重命名
typedef struct Stu Stu;
//指定顺序初始化
Stu s2 = {.name="短裤",.age=13,.id="1234486789",.sex="男",};
//使用->操作符打印
Stu *ps2 = &s2;
printf("name:%s\n", ps2->name);
printf("age:%d\n", ps2->age);
printf("sex:%s\n", ps2->sex);
printf("id:%s\n", ps2->id);
return 0;
}
当然,我们也可以在定义结构体变量时直接typedef,使用实例如下:
typedef struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
}Stu;
3.结构的自引用
在结构体中再创建一个自己可以吗?
struct Node
{
int data;
struct Node next;
};
恐怕是不可以的,只怕是子子孙孙无穷匮也,sizeof(Node)也取不到头了。这个结构体也变成无穷大的了。
那么正确的自引用方式是什么样的呢?
struct Node
{
int data;
struct Node*next;
};
//要先创建才能重命名
//没创建就使用重命名,内部写Node*会报错
typedef struct Node
{
int data;
struct Node*next;
}Node;
4.结构体内存对齐
我们已经掌握了结构体的基本使用,现在我们可以深入研讨一个问题:如何计算结构体的大小。
这个问题便是:结构体内存对齐。
4.1对齐规则
我们首先要掌握结构体的对齐规则:
1.结构体的第一个成员对齐到结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。
--VS2022中的默认对齐数是8
--Liunx中gcc没有默认对齐数,对齐数就是成员自身的大小
3.结构体总大小为所有成员对齐数中的最大对齐数的整数倍
4.如果嵌套了结构体,嵌套的结构体成员对齐到自己的成员的最大对齐数处,结构体的整体大小就是所有成员的最大对齐数(含嵌套结构体成员)的整数倍
现在我们来看一段代码。
int main()
{
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
}
再来看一段代码
int main()
{
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
}
最后看一段代码
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
}
4.2为什么存在内存对齐?
有两个原因
1.平台原因(移植原因):
不是所有的硬件平台都可以访问任意地址上的任意数据的;某些硬件平台只能在特定地址处取出特定类型的数据,否则会出现硬件异常的问题。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要两次内存访问;而对齐的内存访问一次即可。
假设一个处理器一次在内存中取出八个字节,地址必须是8的整数倍,如果我们能保证所有的double类型的地址都能对齐到8的倍数,那么我们就可以通过一次内存操作来完成读写操作了。否则我们可能需要两次取内存操作才能完成读写操作。
总结:结构体的内存对齐是拿空间来换时间的做法。
那么,我们在设计结构体的时候,既要满足对齐又要节省空间,就可以把占用空间小的成员尽量集中在一起。
4.3修改默认对齐数
#pragma这个预处理命令可以改变编译器的默认对齐数。格式如下
#pragma pack(数字)//设置默认对齐数为括号内的数字
#pragma pack()//括号内没有数字,表示还原默认对齐数
5.结构体传参
//结构体传参
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函数
原因:
函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销 。
如果传递一个结构体对象的时候采用传值调用,在压结构体入栈的时候会导致系统开销 过大,而导致性能下降。
因此,我们采取传址调用。
6.结构体实现位段
6.1什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须为int类型,有符号无符号都可,但在C99标准中,位段也可选择其他类型。
2.位段的成员名后面有一个冒号和一个数字。数字表示其占用的比特位
下面来实现一个位段类型
struct A
{
int _a : 2;//占用2个比特位
int _b : 5;//占用5个比特位
int _c : 10;//占用10个比特位
int _d : 30;//占用30个比特位
};
6.2位段的内存分配
位段的几个成员可能在同一个字节中,这些有些成员的起始位置就并不是某个字节的起始位置,但是内存是给每一个字节分配一个地址,那么字节内部的比特位是没有地址的。所以不能对位段的成员使用取地址操作符,也就代表我们不能用scanf来给位段赋值,我们赋值的唯一方法是先输入放在一个变量中,然后赋值给位段的成员。
Struct A b;
int a = 2 = 8;
b._a = a;
那么A占的内存大小是多少呢?
- a,b,c,d一共47比特位,但是我们却占用了八个字节64比特位,因此,位段节省空间的能力也是有限的。
那么位段的分配到底是怎么样的呢?
当一个结构体包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时, 是舍弃剩余的位还是利用呢?
我们以下列程序举例:
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;
return 0;
}
我们先假设一下我们位段的分配方式
- 假设位段在一个字节内部是从高地址到低地址分配。
- 假设当一个结构体包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时, 是舍弃。
现在我们来画一下图!
现在我们只需要在系统中验证一下是否按照我们预料的存储即可。
- 通过验证,我们发现我们的猜想是正确的。
位段虽然能帮助我们节约内存,但是也有许多缺陷,尤其在跨平台问题上有很大的缺陷。
6.3位段的缺陷
1.int型位段成员被当作有符号数还是无符号数不确定
2.位段的最大位数不确定:在16位机器上int型最大为16位,而在32位系统上则为32位。
3.位段的成员在内存中是从高地址向低地址分配还是从低地址向高地址分配未定义
4.当一个结构体包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的空间时,剩余的空间是利用还是舍弃不可知。