文章目录
- 前言
- 一、结构体类型的声明
- 1、结构体回顾
- 1.1、结构体声明
- 1.2、结构体变量的创建和初始化
- 2、结构的特殊声明
- 3、结构体的自引用
- 二、结构体的内存对齐
- 1,什么叫偏移量
- 2、对齐规则
- 3、为什么存在内存对齐
- 4、修改默认对齐数
- 三、结构体传参
- 四、结构体实现位段
- 1、什么是位段
- 2、位段的内存分配
- 3、位段的跨平台问题
- 4、位段的使用注意事项
前言
在结构体初阶学习中小编讲述了一些关于自定义类型结构体相关的内容,以下内容是对于自定义类型结构体的深入了解,建议读者先去从结构体初阶学习进行学习。
提示:以下是本篇文章正文内容,下面案例可供参考
一、结构体类型的声明
1、结构体回顾
结构体初阶学习中,我们已经对结构体有了初步的了解,接下来让我们对前面内容进行温故一下
1.1、结构体声明
结构体的语法形式:
struct tag
{
成员变量;
};
例如描述一个学生
struct stu
{
char name[20]; //名字
int age; //年龄
float height; //身高
float weight; //体重
char id[20]; //学号
}; //分号不能丢
1.2、结构体变量的创建和初始化
结构体变量的创建
根据上面建立的结构体我们可以创建结构体变量
struct stu
{
char name[20]; //名字
int age; //年龄
float height; //身高
float weight; //体重
char id[20]; //学号
}s1,s2;
struct stu s4;
int main()
{
struct stu s3;
return 0;
}
在这里s1,s2,s4属于结构体的全局变量,s3属于结构体的局部变量。
结构体的初始化
在这里用结构体变量s3进行举例,结构体的初始化有三种情况:
- 当你不知道赋值给结构体什么样的值的时候,可以先给他赋予空值
int main()
{
struct stu s3 = {0};
return 0;
}
- 按照结构体成员变量的创建顺序一一对应进行赋值
int main()
{
struct stu s3 = { "张三", 21 ,175.3f,55.5f,"200020845" };
return 0;
}
3.如果不想按照结构体成员变量的创建顺序进行赋值,可以使用点+结构体成员的方式进行乱序赋值
int main()
{
struct stu s3 = { .age = 21,.id = "200020845" ,.height = 175.3f ,.height = 55.5f ,.name = "张三" };
return 0;
}
2、结构的特殊声明
在这里思考一下把结构体的名字去掉,还能对结构体进行创建么
在编程中(特别是在像C、C++或Go这样的语言中),当你定义了一个匿名结构体类型(即没有给它指定一个名字的结构体),那么这个结构体类型通常只能在它被定义的那个位置(或作用域内)被使用一次。这是因为匿名结构体没有名字,所以无法在其他地方通过名字来引用它。
在这里显示为声明符号,把结构体的名字去掉是不能在主函数中进行结构体变量创建的,结构体连名字都没有,主函数都不认识它,怎么能够对他进行变量的创建呢,这就跟大街上有人想要喊你,但是他不知道你的名字,只能喊一句喂,那总不能所有的人都以为是你喊他吧,然后回头对你进行目光注视。在这里结构体是一样的道理。但是是否也能够使用它呢?是可以的,正确的变量创建如下:
struct
{
int i;
}s = {2};
int main()
{
printf("%d\n",s.i);
return 0;
}
分析
在这里我们把这种创建没有名字的结构体称作匿名结构体
,匿名结构体的正确使用就是在创建变量的时候在创建结构体的后面紧接着创建结构体变量,这种匿名结构体的变量创建和初始化都需要在创建结构体之后紧跟着创建,而不能在其他地方,这是因为结构体没有名字,其他的地方都不认识这个结构体,无法通过名字对他进行创建和初始化。
综上:
匿名结构体类型只能在定义的时候用一次,下次你在想使用这个类型的时候,没有名字无法找到,也就无法使用。
3、结构体的自引用
在函数中,函数可以对自己进行函数使用称为递归,那么结构体是否能在结构体的内部对结构体进行引用呢?答案是可以的,结构体也是可以对自己进行自引用的。那么结构体怎么对自己进行自我引用呢?
如果我们按照函数的思维那么结构体的自我引用就是如下代码
//以下结构体自我引用的错误示范
struct stu
{
int i;
struct stu s;
};
那么这行代码是否是正确的呢?答案是错误的?首先我们对它进行求字节就会陷入一个无限的循环当中,在结构体里面创建一个结构体,结构体中带着一个结构体的变量i,那么结构体的大小就会无限的循环:4+4+4…直到内存溢出的情况。那么结构体到底如何自引用呢?其实函数换个思路方向也是正确的,函数的名字也就代表着函数的地址,那么我们结构体对自己进行解引用也可以带上自己的地址,这样我们可以通过结构体也可以找到地址对自己进行引用。
struct stu
{
int i;
struct stu *s;
};
在这里一个结构体自引用包括两个部分,一个是结构体本身存在的成员变量,另外一个则是结构体对自己进行解引用的下一个结构体的地址,又因为地址在内存中要么占4个字节,要么占8个字节,所以结构体的大小又是确定的。综上,结构体对自己进行解引用,需要的是自己本身的地址,而不是结构体变量本身。
出错点
在结构体自引用的过程中,还会出现下面这种情况:
typedef struct Node
{
int data;
Node* next;
}Node;
在这里我们重新用typedef对结构体变量进行重命名Node,然后我们在结构体内部自引用自己的时候直接使用重命名的结构体变量名,这样就会发生错误,,因为Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。因此在定义结构体变量的时候不要使用匿名结构体。
二、结构体的内存对齐
在学习结构体的内存对齐,我们通过一串代码引出:
求下列代码的输出结果
struct stu
{
char s1;
int i;
char s2;
};
int main()
{
printf("%d\n",sizeof(struct stu));
return 0;
}
在这里我们需要求解的是结构体的大小,在结构体中存在三个成员变量,分别占1个字节,4个字节,1个字节。总共6个字节,那么结构体变量是否是6个字节呢?下面看到我们的运行结果
在这里结构体求出的字节数是12,这是怎么回事呢?这就涉及到了结构体的对齐规则了
1,什么叫偏移量
在学习结构体的对齐规则前我们得先了解一个概念:偏移量
在C语言中,偏移量(Offset)通常指的是从一个基准位置(如数组的起始地址、结构体成员的起始地址等)开始,到另一个位置的距离。这个距离通常表示为字节数。在结构体中,每个成员相对于结构体起始地址都有一个固定的偏移量。
创建一个结构体变量
在这里创建了一个结构体变量,结构体地址存放在如图中,那么由结构体地址处向后的一个字节为第一个元素,因为第一个元素相对于首元素的位置为0个字节,结构体跨过一个字节内容到下一个字节内容那么就是偏移量为1,也就是第二个字节的内容相对于结构体地址需要跨过一个字节,也就是偏移量为1个字节。
2、对齐规则
结构体对齐规则主要分为以下几点:
- 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处:
对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值。
在vs中,编译器默认的对齐数是8;
linux中,gcc默认没有对齐数,对齐数就是成员变量本身的大小 - 结构体总大小为最大对齐数(结构体成员变量中每个结构体变量都有对齐数,所有对齐数中最大的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到⾃⼰的成员中最大对齐数的整数倍处,结构
体的整体大小就是所有最⼤对齐数数(含嵌套结构体中成员的对齐数)的整数倍。
实战
结构体的创建
struct stu
{
char s1;
int i;
char s2;
};
开辟空间和规则如下:
分析
根据所创建的结构体,我们在内存中开辟内存空间,结构体变量在内存中存储需要满足上面的条件:
看到第一个条件,结构体的第一个成员变量对齐到结构体变量起始位置偏移量为0的地址处,即结构体的第一个成员变量从偏移量为0的地方向后进行存储,即上面的绿色部分。
第二个条件:结构体成员变量对齐到对齐数的整数倍处,所谓对齐数就是编译器自己默认的对齐数和结构体的成员变量大小的较小值。这里第二个变量为整型变量,大小为4个字节,然后使用vs的话,vs编译器默认的对齐数是8,8和4比较,4较小,所以结构体第二个成员变量对齐到偏移量4的整数倍处。也就是如图中的橙色部分。第三个为字符型变量为1个字节,与8相比,1较小,所以偏移量为1的倍数就是下面的蓝色部分。
第三个条件则结构体变量的总大小为最大对齐数的整数倍,在这里s1的对齐数为1,i的对齐数为4,s2的对齐数为1,所以最大对齐数为4,根据上面分析前面一共已经占了9个字节,9不是4的倍数,12是4的倍数,所以结构体变量总大小为12个字节。
3、为什么存在内存对齐
平台的兼容性
平台兼容性的角度来看,不是所有的硬件平台都能访问任意地址上的任意数据。某些硬件平台只能在特定的地址边界上访问特定类型的数据。如果数据没有按照这些硬件平台的要求进行对齐,那么在访问这些数据时可能会出现硬件异常或错误。因此,为了保证数据在不同硬件平台上的正确访问,结构体内存对齐是必要的。
性能优化角度上
从性能优化的角度来看,对齐的内存访问通常比未对齐的内存访问更高效。这是因为处理器在访问对齐的内存时,可以一次性读取或写入所需的数据,而无需进行额外的内存访问或数据拆分。相反,如果数据未对齐,处理器可能需要执行多次内存访问才能完成数据的读取或写入,这会增加处理器的负担并降低程序的性能。因此,为了优化内存访问性能,结构体内存对齐也是非常重要的。
性能方面,平台方面举例
假设我们内存储存的话不是按照内存对齐的方式进行存储的话,下面创建一个结构体
struct s
{
char i;
int x;
char c;
};
在这里假设内存不存在对齐的话,我们数据存储在内存中因该是连续存储的。
在这里假设数据访问是四个字节进行访问的,那么首先拿出的就是i加上x的三个字节数据,然后在取出x的一个字节数据加上c,然后我们还需要取出x的数据,就得在进行一次字节访问,才能提取数据。假设内存对齐的话
在这里进行内存对齐可以保证数据,使用一次内存操作就可以将所有的数全部取出来了。这样就省去了内存再次操作的时间。
总体来说,内存对齐是拿内存换取时间的做法。
那在结构体中,又想要内存对齐,又想要节省空间的话,就尽量将占用空间较小的成员变量放在一起。
S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的大小有了⼀些区别。
4、修改默认对齐数
#pragma是预处理指令,Microsoft的Visual C++编译器提供了一个#pragma pack指令,允许开发者控制结构体成员的对齐方式。这个指令可以改变编译器默认的对齐行为,使得结构体成员可以按照指定的字节边界进行对齐,而不是按照编译器默认的对齐规则。
在这里#pragma pack(1)是把vs默认的对齐数8改成1,也就是说现在vs默认的对齐数是1,由结构体对接规则第二条,我们可以知道,现在所有元素的对齐数都为1了。此时结构体成员变量就只需要在偏移量为1的倍数的地方进行存储等价于内存不对齐的方式也就是6个字节。
三、结构体传参
struct S
{
int data[1000];
int num;
};
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()
{
print2(&s); //传地址
return 0;
}
上⾯的 print1 和 print2 函数哪个好些?
答案是:首选print2函数
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下
降。
这句话的意思也就是说向上面直接进行结构体的传参,结构体中又存在大型的数据如:int data[1000];这种需要开辟大量的内存空间,而在函数传参的时候直接传递结构体那么则需要开辟两份这么大型的空间,空间会造成浪费,然后使用数据进行赋值的时候又需要额外进行一一对应值进行传回来,这就浪费了大量的时间。而使用结构体地址进行传参,将结构体的地址传递过去,只需要占用四个字节,内存占用少,而且在实现函数内部对原结构体变量进行修改,可以直接通过地址进行修改,而不需要一个对着一个进行传值,减少了代码的运行时间。
结论:
结构体传参的时候,要传结构体的地址。
四、结构体实现位段
在学习完结构体之后,我们可以学习结构体位段,结构体位段是建立在结构体之上的
1、什么是位段
位段的声明和结构是类似的,有两个不同:
- 位段的位代表二进制位,所谓的位段就是在结构体成员变量之后加上一个冒号和一个数字,代表该数据要存多少个二进制位
- 位段的成员必须是 int、unsigned int 或signed int
struct s
{
int i : 3;
int x : 4;
};
这里就是结构体位段,结构体位段成员i表示存进内存为3个二进制位,而结构体位段成员x表示存进内存为4个二进制位。位段的空间是按照需要以四个字节(int)或者一个字节(char)的方式进行开辟空间的,所以这里sizeof(struct s)为四个字节,因为四个字节就可以把所有的位包括进去,具体怎么存放,还得向下进行学习。
2、位段的内存分配
- 位段的成员可以是int,unsigned int,signed int,或者char类型的数据
- 位段的空间是按照需要以四个字节(int)或者一个字节(char)的方式进行开辟空间的
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
例如上面案例
struct s
{
int i : 3;
int x : 4;
};
int main()
{
struct s s = { 0 };
s.i = 1;
s.x = 10;
return 0;
}
首先给这块内存开辟四个字节的空间也就是8个二进位。在这里我先画一个字节方便理解
根据结构体位段成员变量需要存进去多少二进制位,假设上面内存最左边是结构体位段地址,在vs中,结构体位段从一个字节的右边向左边进行存储
此时内存分配只需要一个字节的空间就可以将所有的元素存入。在这里我们也可以打开调试近距离观察一下:
首先代码会开辟四个字节的空间
然后存入结构体位段的成员,因为vs是小端存放,所以先存放1然后存放10的四位也就是1010,所以总的二进制位位01010001转为十六进制位为51
3、位段的跨平台问题
- int位段的有符号和无符号的不确定性
在位段中,int类型的成员被当作有符号数还是无符号数是不确定的。这取决于具体的编译器实现和平台规范。因此,在编写跨平台代码时,如果使用了int类型的位段,就需要特别注意这一点,以避免因符号位解释不同而导致的错误。
- 位段中最大位数的不确定性
位段中成员所能占用的最大位数也是不确定的。这同样取决于编译器的位数和平台规范。例如,在16位机器上,位段中的最大位数可能是16,而在32位机器上则可能是32。如果编写的代码在不同位数的机器上运行,就需要特别注意这一点,以避免因位数限制而导致的错误。
- 内存分配顺序的不确定性
位段中的成员在内存中的分配顺序也是未定义的。有的平台可能从左向右分配,而有的平台则可能从右向左分配。这种不确定性会导致在不同的平台上运行相同的代码时,位段成员在内存中的布局可能不同,从而引发错误。
- 位段剩余位的处理不确定性
当一个结构体包含两个位段成员,且第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用这些位是不确定的。这种不确定性同样会导致在不同的平台上运行相同的代码时,位段成员的值可能不同,从而引发错误。
4、位段的使用注意事项
- 无名位段:无名位段不能被访问,但是会占空间。在编写代码时需要特别注意这一点,以避免因误用无名位段而导致的错误。
//无名位段
struct
{
int i : 3;
int x : 4;
};
- 取地址操作:不能对位段进行取地址操作。这是因为位段并不占用独立的内存空间,而是与其他位段共享同一个整型空间。因此,对位段进行取地址操作是没有意义的,也是不被允许的。所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输入放在⼀个变量中,然后赋值给位段的成员
struct s
{
int i : 3;
int x : 4;
};
int main()
{
//错误使用
struct s s = { 0 };
//printf("%d",&s.i);
//正确的⽰范
int b = 0;
scanf("%d", &b);
s.i = b;
return 0;
}
- 赋值范围:对位段赋值时,不要超过位段所能表示的最大范围。否则,会导致位段的值溢出或发生其他不可预测的错误。
- 数组形式:位段不能出现数组形式。这是因为位段的大小是固定的,而数组的大小是可变的。因此,将位段定义为数组形式是没有意义的,也是不被允许的。