0、结构体基础知识
1、结构体是一些值的集合,这些值被称为成员,它们的类型是可以不同的。(与数组相似,但数组元素的类型都是相同的)。用来描述由基本数据类型组成的复杂类型。
2、结构体也有全局的和局部的。
3、struct+标签是一种结构体类型,可以用typedef重命名,以省略struct的书写。
1、结构体不完全声明
匿名结构体类型(省略标签)。(只想使用一次)
写结构体类型时直接创建变量,之后只能使用变量,无法找不到匿名结构体类型。
值得注意的是:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
尽管两个匿名结构体的成员完全相同,将其 p=&x 的操作仍然不行(类型不兼容)。编译器认为它们并不是一种类型,结论是匿名结构体仅能使用一次。
2、结构体的自引用
在链表中,一个节点既要存储一个数据,又要能够找到下一个节点。
因此我们可以将每个节点的类型设置为结构体类型。
当我们在这个结构体类型中包含下一个节点的结构体时,会导致结构体的大小无限增大,为避免这一错误,并且顺利找到下一节点,我们可以创建一个结构体指针,指向下一个节点。
struct Node
{
int data;
struct Node next;
};
struct Node
{
int data;
struct Node* next;
}
注意:typedef重命名必须在一个类型已经完整定义后才能使用,否则将无法识别。
typedef struct Node
{
int data;
struct Node* next;
}Node;
{}中的struct不可省略。
3、结构体变量的定义和初始化
结构体变量的定义有局部和全局。
初始化时类似数组,使用{},结构体内有其他结构体时,可以嵌套使用{}。
还可以乱序/不规则初始化,只需要 用 .找到相应成员,然后赋值即可。
struct Point
{
int x;
int y;
}p1 = {10, 20};
struct Point p2 = {0,0};
struct S
{
int num;
char ch;
struct Point p;
float d;
};
int main()
{
struct Point p3 = {1,2};
struct S s = { 100, 'w', {2,5}, 3.14f};
struct S s2 = {.d=1.2f, .p.x=3,.p.y=5, .ch = 'q', .num=200};
printf("%d %c %d %d %f\n", s.num, s.ch, s.p.x, s.p.y, s.d);
printf("%d %c %d %d %f\n", s2.num, s2.ch, s2.p.x, s2.p.y, s2.d);
}
4、结构体重难点内存对齐
牺牲空间以换取时间、效率。
由于结构体存在内存对齐,我们在计算结构体大小时,不能简单的将其成员的类型所占大小简单的加和,而要考虑内存存储,以及CPU计算的知识。
结构体内存对齐规则:
1、结构体的第一个成员对齐到结构体在内存中存放位置的0偏移处。
2、对于之后的成员,每个成员都要对齐到一个对齐数的整数倍处。
这个对齐数是 成员自身大小和默认对齐数的较小值。
在VS环境下,默认对齐数是8,在Linux和gcc环境下无默认对齐数(为成员自身大小)
3、结构体的总大小为所有成员最大对齐数的整数倍
4、如果结构体中嵌套了结构体,要将嵌套的结构体对齐到它自己的成员中最大对齐数的整数倍处
第一个成员c1放在0偏移处(灰色部分)。
i大小为4,默认对齐数为8,较小值为4,因此i对齐到4的倍数,从4开始存储(黄色部分)。
而蓝色部分123的空间会被浪费掉。(蓝色)
c2大小为1,因此对齐到1的倍数,即接着存放,放到8的位置(红色部分)。
此时结构体占用了9个字节。
所有成员的最大对齐数为4,结构体总大小为其整数倍,最后得到12byte
9-11的空间会被浪费掉(蓝色)
为观察成员在内存中的存储,我们可以用offsetof宏计算成员相对起始位置的偏移量。
简单记忆:先取小,再对齐存放,再取大,求总大小。
结构体中包含一个结构体
内部的那个结构体,按照其成员中最大的对齐数 的整数倍来对齐。
最终结构体的总大小是所有对齐数中最大的那个的整数倍。
5、内存对齐的原因
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访
问。
总结:除移植性外,内存对齐是为了读取时尽量一次读取完整,或每次读取都是有效的数据,当CPU进行无数次访问时,就会有效地提高性能、效率,空间换取时间。
当然,当成员顺序没有更多要求时,为了不浪费过多空间,我们在对结构体成员顺序的涉及上,可以将占用内存较小的成员集中放在一起,相当于将这些小成员替换到那些浪费的空间中。
6、修改默认对齐数
前面我们知道,VS中默认对齐数是8。但是某些情况下,经过我们的计算,可以将其修改。
利用#pragma预处理指令。
#pragma()为取消默认对齐数,里面放多少,就把默认对齐数修改为多少。
修改为1后,每个成员都是连续存放(即没有对齐)。当然,这种操作后可能会产生移植性的问题,以及性能降低的问题,为了避免,需要我们程序员进行相应的计算(按需更改)。
7、结构体传参
结构体成员一般较多,结构体占用的内存空间也较大。
传结构体时,用结构体接收。 形参接收后用.访问结构体的成员。
传结构体的地址时,用相应结构体的指针来接收。用->访问结构体的成员。
函数传参时,参数需要压栈,就会有时间和空间上的系统开销。当传递的结构体对象较大时,系统开销就较大,进而使得性能下降。
因此,结构体传参时尽量传递地址。