一.结构体
1.结构的基础知识
结构是一些值得集合,这些值称为成员变量,结构得每个成员可以是不同类型的变量
2.结构体的声明
cs
struct tag//结构体名称
{
member-list;//成员变量
}variable-list;//全局变量
例:描述一个学生
cs
struct Stu
{
int age; //年龄
char name[10];//名字
}//s1,s2,s3为全局变量;
int main()
{
//s4,s5为局部变量
struct Stu s4;
struct Stu s5;
return 0;
}
3.结构体的初始化
cs
struct Stu
{
int age;
char name[10];
};
int main()
{
//pa为结构体的变量
struct Stu pa = { 10,"lisi" };//对结构体进行初始化
//pa.age,pa.name 为访问结构体的成员
printf("%d %s", pa.age, pa.name);
return 0;
}
结构体中含有指针怎么初始化
cs
struct S
{
char name[100];
int* ptr;
};
int main()
{
int a = 100;
//用空指针变量来对结构体进行初始化
struct S s = { "abcdef",NULL };
return 0;
}
4.结构体的重命名
typedef 把struct Stu 直接变成student,以后要写结构体时,直接写student即可,不用再写struct Stu
cs
typedef struct Stu
{
int age;
char name[10];
}student;//这里的student是结构体类型
int main()
{
//pa为结构体的变量
student pa = { 10,"lisi" };
//pa.age,pa.name 为访问结构体的成员
printf("%d %s", pa.age, pa.name);
return 0;
}
5.结构体的自引用
结构体内部包含一个指向自身类型的指针,这叫做结构体的自引用
cs
struct Node
{
int data;
//这是一个指向Node结构体的指针,用与指向下一节点
struct Node* next;
};
int main()
{
struct Node n1;
struct Node n2;
//这行代码将n1的next指针指向n2的地址
//从而n1与n2产生了联系
n1.next = &n2;
return 0;
}
6.结构体的嵌套
就是一个结构体中套了一个结构体
cs
struct S
{
int a;
char c;
};
struct B
{
float f;
struct S s;
};
int main()
{
//结构体变量的初始化
struct S s2 = { 100,'q' };
//按照自己的顺序来初始化
struct S s3 = { .c = 'r',.a = 2000 };
//嵌套结构体的初始化
struct B sb = { 3.14f,{10,'a'} };
//打印嵌套结构体
printf("%f %d %c", sb.f, sb.s.a, sb.s.c);
//sb.s.a的意思是用结构体B的变量sb进入到B的结构体中来访问结构体的成员
//.s就是进入到结构体S中,最后.c就是访问S中的结构体成员
return 0;
}
7.结构体的内存对齐(计算结构体的大小)
知识引入:
1.结构体的第一个成员永远放在偏移量为0的地址处
2.从第二个成员开始,以后的每个成员都要对齐到(某个对齐数)的最大整数倍
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
3.当成员全部存放进去时,结构体的总大小必须是所有成员对齐数
的最大对齐数的整数倍。
4. 如果嵌套结构体,嵌套的结构体要对齐到自己成员最大对齐数的整数倍数,
整个结构体的大小必须是最大对齐数整数倍(包含嵌套的结构体成员对齐数)
例:求下面代码结构体所占空间大小
看图理解👇
答案是12个字节
使用offsetof计算偏移量
代码如下👇
cs
//offsetof的头文件
#include<stddef.h>
struct S
{
char c;
int a;
};
int main()
{
//offsetof 可以用来算偏移量
printf("%d\n", offsetof(struct S, c));
//a是写在偏移量为4的地址处,因为是从0开始的,到4结束
printf("%d\n", offsetof(struct S, a));
return 0;
}
使用#pragma可以修改默认对齐数
cs
include <stdio.h>
#pragma pack(4)//设置默认对齐数为4
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
为什么存在内存对折
1.平台原因
不是所有硬件平台上都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出数据异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
8.结构体的函数传参
1.当结构体是普通变量(非指针类型)时,使用
.
操作符来访问结构体中的成员2.当结构体是通过指针来引用时,使用
->
操作符来访问结构体中的成员。
cs
struct S
{
int data[1000];
int num;
};
//s为结构体变量
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
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;
}
我们如何从结构体传参和指针传参中选择
函数传参的时候,参数需要压栈,会有时间和空间上的系统开销。
1.如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大
,所以会导致性能下降。
2.如果使用指针的时候,参数压栈的系统开销比较小,性能会提升很多
总结:
结构体传参的时候,要传结构体的地址
9.位段
1.位段的成员必须是unsigned int 或 signed int
2.位段的成员后边必须有一个 : 和 数字
cs
//结构体内部是位段
struct A
{
int _a : 2;//:后边的数字是bit(比特)
int _b : 5;
int _c : 10;
int _d : 30;
};
位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char 类型
2. 位段的空间上是按照需要以4个字节( char (属于整形家族)类型 int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
注意:这是在vs编译器下的位段分配方式!!👇
在VS编译器中,先开辟一个字节空间,如果不够用,则会继续开辟
分配到内存的比特位是从右向左进行使用的。位段中的成员在内存中从左向右分配。
分配的比特位不够用,直接浪费掉,直接开辟下一个字节空间
位段的跨平台问题
1.int位被当成有符号位,还是无符号位不确定
2.位段的最大数目不确定,在16位机器上是16,在32位机器上是32,在64位机器上是64,(如果位段是27,在16位机器上就不适用了)
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。
总结:
位段相对于结构体,可以节省很多空间,但是有跨平台问题出现