在C语言中,结构体是一种用户自定义的数据类型,它允许我们将不同类型的数据组合成一个单一的数据结构。通过使用结构体,我们可以更方便地管理和操作相关的数据集合。
结构体的定义
结构体是通过关键字struct来定义的。其基本语法如下:
struct 结构体名 {
数据类型 成员变量1;
数据类型 成员变量2;
...
数据类型 成员变量n;
};
举个例子,我们可以用一下方式定义一个表示学生的结构体:
struct Student {
char name[50];
int age;
float score;
};
结构体的使用
我们在初始化结构体的时候,初始化的内容要与结构体中定义的内容对应,初始化之后,可以通过.运算符来访问结构体,此外还可以通过指针来操作结构体,那么此时就需要用到->运算符与指针搭配来使用,具体看以下示例:
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student stu1 = { "李华", 18, 98.5 }; // 使用初始化列表赋值
struct Student stu2 = { "Tom", 19, 90.5 }; // 使用初始化列表赋值
struct Student* pStu = &stu2; // 使用指针操作结构体
printf("名字: %s\n", stu1.name); // 使用`.`运算符访问成员变量
printf("名字: %s\n", pStu->name); // 使用`->`运算符访问成员变量
return 0;
}
匿名结构体
匿名结构体指的是在声明结构体时省略结构体名称,直接定义其成员变量。
struct {
char name[50];
int age;
} stu = { "张三",20 }; // 匿名结构体变量stu
需要注意的是,匿名结构体没有为结构体指定名称,因此它只能在声明时使用,无法在其他地方引用。
结构体的自引用
结构体自引用指的是结构体中包含一个指向该结构体类型的指针成员。这种结构常用于创建链表、树等数据结构。例如,我们可以定义一个表示链表的节点的结构体:
struct Node {
int data;
struct Node* next; // 指向下一个节点的指针
};
在这个例子中,Node结构体包含一个整型成员data和一个指向Node类型的指针成员next。next指针用于指向链表中的下一个节点,从而实现链表的链接。
结构体内存对齐
在之前,我们都是用sizeof()来计算一个类型的大小,结构体里面又有各种类型,那么结构体的大小是怎么计算的呢,这时我们就需要引出内存对齐的概念了
现代计算机中,内存空间是按照字节来划分的。虽然从理论上讲,我们可以从任何地址开始访问任何类型的变量,但实际上,在访问特定变量时,我们经常在特定的内存地址进行访问。这就需要各种类型的数据按照一定的规则在空间上排列,而不是顺序的一个接一个地排放,这就是对齐。
对齐规则
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员变量大小的的较小值
vs编译器默认是8
Linux中gcc没有默认对齐数,对齐数就是成员本身的大小
3.结构体总大小为最大对齐数 (结构体中每个成员变量中对齐数最大的)的整数倍。
4.如果嵌套了结构体,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍,结构体的整体大小就是其中所有对齐数中最大的整数倍。
我们下面具体来解释一下:
例如这个结构体的大小是多少呢,先说结论
那么为什么是12个字节呢
首先,c1是一个字节,根据对齐规则需要放在0的位置,c1是四个字节,但之后的1,2,3,位置并不是4的整数倍,所以要放在4的位置,之后c2也是一样的道理,这三个放完之后就是9个字节,但是并不是最大对齐数(int 类型–四个字节)的整数倍,所以还要加到12。
那么为什么会有内存对齐呢,这样不是浪费内存吗
是因为CPU在访问内存时,通常是以固定大小的块(如4字节或8字节)为单位进行操作的。如果数据的存储没有对齐到这些块的大小,CPU可能需要进行额外的操作来读取或写入数据,这称为“非对齐访问”。非对齐访问可能导致性能下降,甚至在某些硬件上可能引发异常或错误。通过对齐数据,CPU可以一次性读取或写入整个数据块,从而提高访问效率。
修改默认对齐数
我们可以通过两个预处理命令来实现对默认对齐数的修改
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S));//输出6
return 0;
}
当我们把默认对齐数修改为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()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
在使用的时候推荐使用第二种传参方式,当结构体较大且成员较多时,如果采用传值方式(即直接传递结构体的完整副本),会涉及到大量的数据复制操作,这既会浪费大量的时间和计算资源,也可能导致栈空间的过度使用,进而影响程序的性能。特别是当函数调用层次较深时,这种影响会更加明显。