2024 - 10 - 13 - 笔记 - 26
作者(Author): 郑龙浩 / 仟濹(CSDN账号名)
自定义类型 - 结构体
平时用的数组是一组相同类型的数据,如果想表示一组不同类型的数据,那么就可以结构体了。
① 结构体的声明(重要)
自己起的名字,为了方便记忆和复盘,并不是规范的名字
-
直接使用法(只能使用一次)
【匿名结构体类型】
不写结【结构体标签(结构体名)】,只写【结构类型的变量】,所以只能使用一次
struct { char name[20]; char sex[10]; }BB;//BB 为机构体变量名
-
直接声明法 (结构体可以使用多次,或者叫类型可以使用多次)
写机构体名,所以后续想继续使用此机构体类型,仍可以定义此类型的结构类型的变量
struct AA //AA 为机构体名 { char name[20]; char sex[10]; }; struct AA BB, CC; //BB 和 CC就是机构体变量名
-
直接定义法
声明结构体的同时也声明了【该结构类型的 变量 】。
struct AA //AA 为机构体名 / 结构体标签 { char name[20]; char sex[10]; }ABC; struct AA BB, CC; //BB 和 CC就是机构体变量名,ABC 也是【该结构类型的 变量 】
-
类型定义法
typedef struct AA //AA 为结构体名 { char name[20]; char sex[10]; }ABC;//重新命名,可以想成 ABC 代替了 struct AA,方便后期使用。 //此时这里的ABC就不是结构体变量了,而是将结构体类型重新写了一个名字,方便使用,否则每次定义机构体变量都要写struct AA,这样再次定义结构体变量就可以直接使用ABC作为类型了。 struct AA BB, CC; ABC BB, CC;//效果与struct AA BB, CC;相同
-
结构体的自引用
在定义结构体类型的时候嵌套一个该结构的结构变量是可行的吗???
不可行!!!
第 ① 种:
错误写法:
//这中写法是错误的,试着读一下,这是一个嵌套又嵌套又嵌套,无限套娃的一个结构体,编译器是不会执行的 struct Node { int data; struct Node next; };
正确写法:
//既然无法在某结构类型中,嵌套该结构类型的变量,那么我可以在定义该类型的时候,创建一个该结构类型的指针 struct Node { int data; struct Node* next; };
第 ② 种
错误代码:
//在重命名的时候也是不可行的 typedef struct { int data; Node* next; }Node;
正确代码
typedef struct Node { int data; struct Node* next; }Node;
②结构体的初始化,结构体成员的赋值
下面举例的这几种方法都是可以的(随便写的几个例子):
-
struct AA { char name[20]; int age; }ABC = { "aaa", 10 };
-
struct AA { char name[20]; int age; struct BB { int days; char ch; } }ABC = { "aaa", 10, { 100, 'a' } };
-
struct AA { char name[20]; int age; }; struct AA ABC = { "aaa", 10 };
-
typedef struct { char name[20]; int age; }AA; AA ABC = { "aaa", 10 };
③ 结构体的内存对齐 - 结构体大小的计算(非常重要!!!)
1) 例题
想一下,结构体的大小是怎么计算的呢???
#include <stdio.h>
//练习1
struct S1
{
char c1;
int i;
char c2;
};
//练习2
struct S2
{
char c1;
char c2;
int i;
};
//练习3
struct S3
{
double d;
char c;
int i;
};
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S1));//12
printf("%d\n", sizeof(struct S2));//8
printf("%d\n", sizeof(struct S3));//16
printf("%d\n", sizeof(struct S4));//32
return 0;
}
打印结果:
12
8
16
32
关于结构体如何计算大小,其实还是稍微复杂一些的。
2) 结构体【内存对齐】规则
-
第一个成员在与结构体变量偏移量为0的地址处。
-
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值,即**【默认对齐数】与【该成员对其数】进行比较,哪个小,就是真正的对齐数。(VS编译器中默认的值为8,其他好多编译器没有所谓的【默认对齐数】,也就不用将【成员对齐数】与【默认对齐数】**进行比较了)
比如 int a 【成员对其数】是4,VS编译器【默认对齐数】是8,取两者较小值,为4,所以对齐数就是 4 .
-
每个结构体成员会按照其数据类型的大小和对齐要求来分配内存。
-
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。即使结构体的最后一个成员之后没有足够的空间来满足最大对齐要求,编译器也会添加填充字节(padding)来达到这一要求。
-
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
3) 为什么存在【内存对齐】
- 硬件访问效率:
- 现代计算机的内存访问通常是以块或字节对齐的方式进行的。
- 如果数据没有正确对齐,处理器可能需要额外的时钟周期来访问数据,这会导致性能下降。
- 简化硬件设计:
- 内存对齐使得硬件设计更加简单和高效。
- 处理器不需要处理复杂的地址计算和数据拆分/合并操作。
- 提高缓存命中率:
- 缓存通常是以块为单位进行操作的,这些块的大小通常是2的幂次方(如64字节)。
- 如果数据按缓存块大小对齐,则更容易完全装入缓存块中,从而提高缓存命中率。
- 数据一致性和安全性:
- 某些数据类型(如浮点数)在内存中需要特定的对齐方式才能保证数据的正确性和一致性。
- 不正确的对齐可能导致数据被错误地解释或损坏。
- 便于多处理器系统同步:
- 在多处理器系统中,内存对齐有助于简化数据同步和一致性问题。
- 对齐的数据更容易进行原子操作,从而避免数据竞争和不一致性。
注:【内存对齐】是一种 拿 【空间】 来换取 【时间】的一种做法
4) 解释结构体如何计算大小 、 如何进行【内存对齐】的
#include <stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S1) );
printf("%d\n", sizeof(struct S2) );
}
打印结果:
12
8
成员类型都是一样的,只是换了成员的顺序,为什么结构体大小就不同了呢?
下面我作图进行分析:
5) 结构体传参 - 最好用第二种方法,传结构体地址
struct AA
{
int data[1000];
int num;
};
struct AA BB = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct AA BB)
{
int i = 0;
for( i = 0; i < 3 ; i ++ )
{
printf("%d", BB.date[i] );
}
printf("%d\n", BB.num);
}
//结构体地址传参
void print2(struct AA* p)
{
int i = 0;
for( i = 0; i < 3 ; i ++ )
{
printf("%d", p -> date[i] );
}
printf("%d\n", p -> num);
}
int main()
{
print1(BB); //传结构体
print2(&AA); //传地址
return 0;
}
第 1 种是传【值】进行调用 -> 占内存空间较大
第 2 种是传【址】进行调用 -> 占内存空间较小,最好用这一种
而第 2 种有可能会误改了结构体中的数据,如果不想被更改,可以在形式参数位置加上const
Eg:
void print2(const struct AA* p) { int i = 0; for( i = 0; i < 3 ; i ++ ) { printf("%d", p -> date[i] ); } printf("%d\n", p -> num); }