目录
1.复习
2.结构体的特殊声明
01.匿名结构体
*定义
不完全声明,即结构体没有自己的名字(没有结构体标签)
*注意事项
1.匿名结构体只能使用一次
2.下列代码合法吗
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}*p;
int main()
{
p = &x;
return 0;
}
虽然编译没有问题 ,但生成解决方案会报警告
warning C4133: “=”: 从“*”到“*”的类型不兼容
编译器认为:x;和*p;是两个不同的结构体
02.自引用
前置知识
*数据结构:数据在内存中的存储结构
*常见的数据结构:数组,记录,链表
*一个典型的数据结构----链表:一种物理存储单元上非连续(因为用了指针)、非顺序(因为用了指针)的存储结构,用于线性方式存储数据(对比数组:元素的顺序集合)
下面这张图(摘自《计算机科学导论》)体现非顺序65-->66-->72-->96-->85-->74
*链表中的每一个元素称为结点,结点包含两个部分:数据和链(链是下一个节点的地址(指针),指向下一个节点的数据),链最后一个元素包含一个空指针
*链表名是头指针的名字,下面这张图(摘自《计算机科学导论》)
《计算机科学导论》对链表的解释
单向链表图例
代码实现链表的一个结点
struct Node
{
int data = 0;//数据
struct Node* next;//链(地址)(指针)
};
注:Node n.节点,结点
运行上方代码,x86环境下在内存中查看结构体
typedef重命名结构体
禁止使用typedef重命名匿名结构体
typedef struct Node
{
int data;
struct Node* next;
}Node;
简单实现链表
#include <stdio.h>
int main()
{
struct node
{
int val;
struct node* next;
};
struct node n1;
struct node n2;
struct node n3;
struct node n4;
struct node n5;
n1.val = 1;
n1.next = &n2;
n2.val = 2;
n2.next = &n3;
n3.val = 3;
n3.next = &n4;
n4.val = 4;
n4.next = &n5;
n5.val = 5;
n5.next = NULL;
return 0;
}
VS2022进入调试模式,在return 0; 处下断点,之后在监视窗口处查看链表的结构
3. 结构体内存对齐(考频高)
如果看过李忠老师的《x86汇编语言:从实模式到保护模式 第二版》将会比较好理解
以下节选自第39页~第40页
接着,你的任务是定义段地址并设置处理器的段寄存器,其中最重要的是段地址的选取。因为偏移地址总是要求从 0000H 开始,而 82260H 是第一个符合该条件的物理地址,它恰好对应着逻辑地址 8226H:0000H,符合偏移地址的条件,所以完全可以将段地址定为8226H。
但是,举个例子来说,如果你从物理内存地址 82255H 处加载程序,由于它根本无法表示成一个偏移地址为0000H的逻辑地址,所以不符合要求,段不能从这里开始划分。这里面的区别在于,82260H 可以被16(10H)整除,而82255H不能。通过这个例子可以看出,8086理器的逻辑分段,起始地址都是16的倍数,这称为是按 16 字节对齐的。
段的划分是自由的,它可以起始于任何 16 字节对齐的内存地址,也可以是任意长度,只要不超过 64KB。比如,段可以起始于物理地址 82260H,段的长度可以是3字节(此时,该段所对应的逻辑地址范围是 8226H:0000H~8226H:0002H,对应的物理地址范围是82260H~82262H、2KB(此时,该段所对应的逻辑地址范围 8226H:0000H~8226H:07FFH,对应的物理地址范围是82260H~82A5FH),甚至最多可以达到64KB(此时,该段所对应的逻辑地址范围是 8226H:0000H~8226H:FFFFH,对应的物理地址范围是 82260H~9225FH)。
同时,正是由于段的划分非常自由,使得 8086 的内存访问也非常随意。同一个物理地址,或者同一片内存区域,根据需要,可以随意指定一个段来访问它,前提是那个物理地址位于该段的64KB范围内。也就是说,同一个物理地址,实际上对应多个逻辑地址。比如说,对于一个物理地址 C0533H,它可以用逻辑地址C053H:0003H表示,也可以用逻辑地址C000H:0540H 来表示,还可以用逻辑地址 C050H:0030H 来表示,甚至用逻辑地址BFFFH:0540H 来表示,等等。
8086CPU的内存对齐方式
书中提到:8086CPU在存储数据段时采用16字节对齐的方式,如果定义的数据没有到16字节的整数倍,将以00填充满
例如:
db "Hello"
db "World!"
在内存中查看数据的排布:
设该字符串有x个字符(即x bytes)
其在内存中的存储占用的字节数S的公式:
① x ≦ 16,S=16
②x = 16k (k为正整数),S=16k
③ 16k<x ≦16(k+1) (k为正整数),S=16(k+1)
对于结构体来说,内存对齐有4条规则
1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量(即从第二个成员变量开始)要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值
对齐数要看偏移量
-->VS中默认的值为8 bytes
-->Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小(注意这里说的不是偏移量)为最大对齐数(结构体中每个成员变量(含第一个成员变量)都有一个对齐数,所有对齐数中最大的)的整数倍,这样可能会浪费一些空间
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
例题:非嵌套结构体
求下列代码的执行结果(在VS2022中测试)
#include <stdio.h>
struct s1
{
char a;
char b;
int c;
};
struct s2
{
char a;
int c;
char b;
};
int main()
{
printf("%zd,%zd", sizeof(struct s1), sizeof(struct s2));
return 0;
}
提示:结构体s1和结构体 s2唯一不同的地方在成员变量的排列顺序
s1:①char a ②char b ③int c
s2:①char a ②int c ③char b
答案速查
分析
在3.【C语言】内置数据类型文中讲过char占1个字节,int占4个字节
由内存对齐有4条规则可以画图(设CPU为结构体从偏移量为0处开辟空间):
对于结构体s1
1 < 8,a的对齐数为1
4 < 8,c的对齐数为4
从偏移量0到偏移量7,一共占8个字节,为最大对齐数4的整数倍(对齐数1 < 对齐数4)
因此sizeof(struct s1) == 8
对于结构体s2
c存储的位置必须从偏移量为4的最近位置开始
存储完a,c,b后,8-0+1=9 bytes,不是最大对齐数4的整数倍,因此要至偏移量为11的位置
因此打印结果为8,12
附:使用VS查看内存布局
鼠标悬停到结构体的名称上:
可看两种视图:点击 可以切换视图
练习
求下列代码的执行结果(在VS2022中测试)
#include <stdio.h>
struct s3
{
double d;
char c;
int i;
};
int main()
{
printf("%zd", sizeof(struct s3));
return 0;
}
答案速查
分析
例题:嵌套结构体
求下列代码的执行结果(在VS2022中测试)
#include <stdio.h>
struct s3
{
double d;
char c;
int i;
};
struct s4
{
char c1;
struct s3 s3;
double d;
};
int main()
{
struct s4 s4;
printf("%zd", sizeof(struct s4));
return 0;
}
答案速查
分析
结构体的整体大小就是所有最大对齐数的整数倍(s3的最大对齐数和s4的最大对齐数都是8)
4.内存对齐的原因
1. 平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2. 性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐.原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问.假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数.如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了.否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中
如32位机器一次性读取32位(4 bytes)的数据,考虑对齐,则读取的数据的次数会减少
struct s
{
char a;
int b;
};
现要读取b在内存中存储的数据,在32位下,求出分别在内存对齐和内存不对齐(连续存放)的情况下,CPU读取的次数
内存对齐:读一次(4字节对齐)
内存不对齐(连续存放):读两次
总体来说:结构体的内存对齐是拿空间来换取时间的做法(即妥协)
5.内存对齐节省空间的方法
让占用空间小的成员尽量集中在一起
本文例题:非嵌套结构体就是一个很好的说明,仅仅改变了成员变量的排列方式,浪费的空间就不同