引言
在编程中,结构体是一种自定义的数据类型,它允许开发人员将不同类型的数据组合在一起,并为其定义相关属性和行为。结构体提供了一种灵活的方式来表示复杂的数据结构,使得程序设计更加模块化和可读性更高。
结构体类型的声明
结构的声明
声明格式如下:
struct 结构体类型名 { 成员名-list; }直接声明变量-list;
结构体变量的声明和使用
下面是一个程序。首先创建了一个结构体类型Stu,里面包括了成员变量name、age、sex和 id。在主函数中创建了结构体变量s,并打印。
结构体变量创建格式:
① 按成员顺序初始化:结构体类型名 + 自定义变量名(+ 初始化内容);
② 按指定顺序初始化:结构体变量名 + 自定义变量名 (+ .成员名);
变量的使用:结构体变量名 . 成员名
#include <stdio.h> struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }; int main() { //按照结构体成员的顺序初始化 struct Stu s = { "张三", 20, "男", "20230818001" }; printf("name: %s\n", s.name); printf("age : %d\n", s.age); printf("sex : %s\n", s.sex); printf("id : %s\n", s.id); //按照指定的顺序初始化 struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "⼥" }; printf("name: %s\n", s2.name); printf("age : %d\n", s2.age); printf("sex : %s\n", s2.sex); printf("id : %s\n", s2.id); return 0; }
结构的不完全声明
在声明结构体类型的时候可以不完全声明,直接在结构体类型后声明变量,这样创建的变量就是一次性变量,之后只能一次性使用。
声明如下:
struct { int a; char b; float c; }x; struct { int a; char b; float c; }a[20], * p;
结构的⾃引⽤
结构的自引用典型例子就是链表中对节点的定义,用于连续节点连接,具体有关链表的知识可以点击这段蓝字阅读博主另一篇博客。
自引用结构声明格式如下:
struct Node { int data; struct Node next; };
自定义结构体
声明格式如下:
格式:typedef + struct 结构体类型名
{
成员变量;
}自定义类型名;
示例声明如下:
typedef struct Node { int data; struct Node* next; }Node;
结构体内存对⻬(热门考点)
引子
我们经常会用sizeof运算符计算各个变量的字节大小,例如:
#include<stdio.h> int main() { printf("%d\n", sizeof(int)); printf("%d\n", sizeof(short)); printf("%d\n", sizeof(long long)); return 0; }
得到结果:
如文所示,我们可以用sizeof来计算各个类型的大小,那么结构体变量计算会得到什么结果呢?
对⻬规则
结构体变量在内存中会遵循结构体对齐规则,对齐规则如下:
1.结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数=编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。
VS 中默认的值为 8
Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
3.结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
结构体内存对齐练习
1.非嵌套结构体
#include<stdio.h> int main() { struct S1 { char c1; int i; char c2; }; printf("%d\n", sizeof(struct S1)); return 0; }
按照内存对齐规则,从编译器行数从上到下进行内存存储。逐个对比各个成员和VS的默认对齐数8对比,取二者最小对齐数作为对齐数。
根据对齐数从0开始偏移计算每个变量开始存储的内存地址,成员变量要对⻬到对⻬数的整数倍的地址处,如图所示。
最后计算结构体总共大小是需要按照结构体中成员变量的最大对齐数进行对齐,最终结构体大小是最大对齐数的整数倍,产生的浪费空间也要计入总大小。
内存存储图示如下:
2.嵌套结构体
#include<stdio.h> struct S3 { double d; char c; int i; }; int main() { struct S4 { char c1; struct S3 s3; double d; }; printf("%d\n", sizeof(struct S4)); return 0; }
计算嵌套结构体的字节大小时对待被嵌套的结构体时,就相当于把嵌套结构体当做结构体的一个成员进行内存对齐。最终计算字节总大小的时候用所有成员中最大对齐数(包括被嵌套结构体中的成员)进行整数倍的计算。
下图即为上述代码的演示图例:
为什么存在内存对⻬?
1. 硬件访问要求: 计算机硬件对于访问内存通常有一定的要求,例如某些硬件可能只能从特定地址开始读取数据,或者只能按照特定的字节长度进行读取。通过内存对齐,可以保证结构体中的字段在内存中按照一定的规则排列,满足硬件访问的要求。
2. 性能优化: 在数据结构中,尤其是在涉及栈这种数据结构时,我们应该尽可能使数据在自然边界上对齐。这样做的原因在于,处理器访问未对齐的内存时需要进行两次内存访问,而对齐的内存访问只需要一次访问。举例来说,如果一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能够保证所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。因此,通过合理地对数据进行内存对齐,我们可以提高程序的执行效率和性能表现。
3. 内存空间利用: 内存对齐可以使数据结构更加紧凑,减少内存空间的浪费。如果结构体中的字段按照对齐规则排列,编译器可以合理地利用内存空间,避免由于未对齐而导致的内存浪费。
4. 平台移植性: 不同的计算机架构可能对内存对齐有不同的要求。合理地处理结构体的内存对齐可以增强程序在不同平台上的移植性,使程序更容易地在不同平台上移植和运行
针对于性能优化,我们可以了解到结构体对齐是为了优化性能,用空间换时间,那么有没有什么办法让我们尽量的减少浪费的空间呢?
我们可以利用结构体对齐的规则,将小的结构体尽量的凑在一起,这样他们会在空间上连续存储,因为对齐数小的和大的之间会存在大对齐数所造成的空间浪费,,所以将小的放一块这样就可以将其中的浪费空间给合理利用起来。
具体如下代码示例及图示:
struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; };
修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对⻬数。
例如,我们要将编译器的默认对齐数修改为1,那么勇以下代码实现:
#pragma pack(1)
如果需要取消修改的默认对齐数,使用以下代码即可实现:
#pragma pack()
位段结构体
当我们需要在C或C++中表示一些具有特定位长度的数据时,位段(bit fields)结构体就成为了一种非常有用的工具。位段结构体允许我们将数据按位组织,并且可以更加高效地使用内存空间。
什么是位段结构体?
位段结构体是C和C++中的一种特殊结构,它允许我们定义结构体的成员为特定位长度的字段,从而更为灵活地管理数据。通过位段结构体,我们可以精确地控制每个字段的位数,从而在内存中节约空间。
如何定义位段结构体?
在C和C++中,我们可以使用结构体来定义位段。
位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以
选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字
下面是一个简单的例子:
struct BitFieldStruct { unsigned int flag1 : 1; unsigned int flag2 : 2; unsigned int flag3 : 3; };
位段的内存分配
分配规则:
1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
通过上文已经得知位段结构体如何创建,下面请通过示例代码和图示来了解位段结构体再内存中的分配原理。
代码如下:
struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; struct S s = { 0 }; s.a = 10; s.b = 12; s.c = 3; s.d = 4;
代码中定义了一个结构体类型S,在main函数中创建S型变量s并初始化为0。重点在于,在已经规定的位段情况下,后面的a,b,c,d赋值后在内存中是如何存储的呢?
图示操作如下:
最后的d由于在第二个字节段中无法存储,所以会直接存到下一个字节中,大小位4比特。
使用位段的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位
置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先⼊放在⼀个变量中,然后赋值给位段的成员。
struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; }; int main() { struct A sa = { 0 }; //这是错误的 scanf("%d", &sa._b); //正确的⽰范 int b = 0; scanf("%d", &b); sa._b = b; return 0; }
位段结构体的优势
- 节省内存空间:位段结构体可以将多个字段压缩到一个字节中(或者更少),这样可以减少内存使用量。在一些嵌入式系统或需要高效利用内存的场景中,位段结构体可以发挥重要作用。
- 更好的可移植性:位段结构体可以帮助开发者更好地处理不同机器上的字节顺序问题和对齐方式。因为位段结构体的字段是按照位来处理的,所以不受机器的字节顺序和对齐方式的影响。
- 方便地操作位数据:位段结构体可以方便地处理二进制数据,例如一些硬件寄存器中的位标志。使用位段结构体可以使得代码更加简洁和易读,降低出错的风险。
- 更好的兼容性:位段结构体的语法与普通结构体非常相似,因此可以很容易地与其他代码进行交互和集成。此外,C++11标准中还引入了新的标准化位字段类型,称为 std::bitset,可以更加方便地处理位数据。
I'm Kevin, and we'll see you in the next blog