结构体类型的声明:
结构体通过 struct
关键字声明。例如:
struct Person {
char name[50];
int age;
float height;
};
结构体变量的创建和初始化:
struct Person person1 = {"Alice", 30, 5.5}; // 直接初始化
struct Person person2;
person2.age = 25; // 逐个成员赋值
结构成员访问操作符:
使用点(.
)操作符来访问结构体的成员。如果通过指针访问结构体成员,使用箭头(->
)操作符。例如:
printf("%s", person1.name); // 直接访问
struct Person *ptr = &person2;
printf("%d", ptr->age); // 指针访问
在声明结构的时候,可以不完全的声明:
在C语言中,结构体可以有一个“不完全声明”或“前向声明”。这种声明方式在定义结构体时不完全指定其所有成员,通常用于处理交叉引用或循环引用的情况,以及在定义涉及相互引用的结构体时。
特点和用途
-
交叉引用:当两个或多个结构体彼此引用对方的成员时,不可能在一个声明中完全定义它们。不完全声明允许在结构体定义之前声明其存在,从而使结构体可以互相引用。
-
循环引用:在数据结构如链表、树、图等中,元素需要指向相同类型的其他元素。在这种情况下,不完全的结构体声明是必要的。
-
头文件和模块化:在模块化编程和使用头文件时,不完全声明允许将结构体的定义从其使用中分离。
示例
不完全声明的一个简单例子可能如下所示:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
在上⾯代码的基础上,下⾯的代码合法吗?
p = &x;
p
是指向一个由匿名结构体构成的数组的指针,而 x
是一个单独的匿名结构体变量。由于 x
和 a[20]
使用的是不同的匿名结构体类型(即使它们的字段相同),它们在C语言中被视为不同的类型。
C语言的类型系统是基于严格的类型兼容性的。即使两个匿名结构体具有完全相同的字段和布局,如果它们是分别声明的,它们仍然被视为不同的类型。因此,不能将一个结构体类型的地址赋给另一个不同结构体类型的指针,即使这两个结构体在实际布局上是相同的。
所以,p = &x;
这行代码在C语言中是不合法的,因为 p
是指向一个匿名结构体数组的指针,而 x
是一个不同的匿名结构体实例。如果要使这行代码合法,p
和 x
必须是相同的结构体类型。例如,可以这样声明:
struct {
int a;
char b;
float c;
} x, *p;
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
结构的自引用:
在C语言中,结构体的自引用是指结构体中包含指向相同类型的结构体的指针。这是一种常见的做法,特别是在创建链表、树、图等数据结构时。自引用结构体不能直接包含其自身的实例,因为这会导致无限大小的结构体,但它们可以包含指向相同类型的结构体的指针。
示例
以下是一个自引用结构体的例子,用于表示链表的节点:
struct Node {
int data;
struct Node *next; // 自引用:指向下一个节点的指针
};
在这个例子中,每个 Node
结构体包含两个成员:一个 int
类型的数据和一个指向下一个 Node
的指针。这样的结构体布局允许创建一个节点序列,每个节点都指向列表中的下一个节点。
自引用结构体的用途
- 链表:链表中的每个节点包含数据和一个指向列表中下一个节点的指针。
- 树:在树结构中,每个节点可能包含指向其子节点的指针。
- 图:图的节点(或顶点)可以包含指向其他节点的指针,表示边或链接。
注意事项
- 在声明自引用结构体时,必须使用结构体的前向声明(如果结构体是匿名的,则无法实现自引用)。
- 自引用通常涉及动态内存分配,因为结构体的完整大小在编译时未知。
这种自引用机制使得结构体在表示复杂数据结构时非常灵活和强大。
结构体内存对齐:
结构体的内存对齐是指在结构体中,各成员可能会有内存填充(padding)来确保特定的对齐方式。这是由于硬件和性能优化的原因。编译器通常根据成员类型的自然对齐边界进行对齐。
首先得掌握结构体的对齐规则:
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的
整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构
体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
为什么存在内存对齐?
大部分的参考资料都是这样说的:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定
类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的doubl类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起
修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
}
结构体传参:
结构体可以作为整体传递给函数,或者通过指针传递。传递整个结构体会复制整个结构体数据,而传递指针则更为高效。例如:
void displayPerson(struct Person p) { ... } // 通过值传递
void modifyPerson(struct Person *p) { ... } // 通过指针传递
原因: 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
结论: 结构体传参的时候,要传结构体的地址。
结构体实现位段(Bit-fields):
在C语言中,位段(Bit-fields)是一种用于结构体中的特殊语法,允许程序员更精确地控制数据结构中各成员的位级表示。这在需要精确大小控制或与特定硬件接口对接时特别有用,例如在硬件编程或网络协议中。
位段的声明和使用
-
声明:位段在结构体内部声明,通过在成员后面指定位数来定义。例如,声明一个可以使用3位存储的无符号整数:
struct { unsigned int field : 3; };
-
大小和类型:位段成员的类型通常是整数类型(如
int
、unsigned int
、signed int
)。大小是通过冒号后面的数字指定的,表示该成员占用多少位。 -
内存布局:位段可以帮助减少数据结构的大小,因为它允许多个成员共享同一个字节,只要它们加起来的位数不超过该字节的大小。
-
访问:位段成员的访问方式与普通结构体成员相同。但是,位段的读取和写入可能比普通成员更慢,因为编译器可能需要生成额外的代码来处理位的掩码和位移操作。
示例
以下是位段在结构体中的一个示例:
struct BitField {
unsigned int is_enabled : 1; // 只占用1位
unsigned int is_visible : 1; // 只占用1位
unsigned int mode : 3; // 占用3位
unsigned int reserved : 3; // 占用3位,用作填充或未来使用
};
在这个例子中,BitField
结构体总共占用8位(1个字节),其中每个成员都分配了特定数量的位。
注意事项
- 位段的实际布局(例如跨越字节边界的行为)可能取决于特定的编译器和平台。
- 对位段的操作可能不如对常规整数成员的操作高效,因为可能涉及位掩码和位移。
- 位段不应该用于跨平台的数据交换,因为不同的编译器可能会以不同的方式布局同一个位段。
通过位段,可以在保持数据结构清晰的同时节省空间,特别适合于资源受限的环境,如嵌入式系统。