结构体的基本概念
结构体是一种用户自定义的数据类型,可以包含多个不同类型的变量。通过使用结构体,我们可以将相关联的数据组织在一起,便于管理和使用。
结构体的声明
正常的结构体声明
在C语言中,结构体(struct)指的是一种数据结构,是C语言中聚合数据类型的一类。
结构体可以包含多个不同类型的数据成员,例如:int、float、char等。
结构体的声明方式如下:
struct 结构体名 {
类型1 数据成员1;
类型2 数据成员2;
...
类型n 数据成员n;
};
例如:
struct student {
int id;
char name[50];
int age;
}; //注意,结构体定义后面的分号不要忘了
以上属于是正常的结构体声明↑↑↑↑↑↑↑↑↑↑↑↑
匿名结构体声明
既然有正常的,那就有特殊的结构体声明了↓↓↓↓↓↓↓↓↓↓↓↓↓↓
在声明结构体的时候,可以不完全的声明。(匿名结构体类型)
如下:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}*p;
- p是结构体指针,这里可以看到,上下两个结构体的成员内容都是一模一样的,
- 能不能实现
p = &x
呢?- 答案是不能,编译器会把上面的两个声明当成完全不同的结构体类型,所以p不能放x的地址!!!
上面两个结构体在声明的时候省略掉了结构体标签,这样声明也是可以的,但是!匿名的结构体类型,只能在创建的时候定义结构体变量,如果没有对结构体类型重命名的话,基本上只能使用一次。
typedef 结构体起别名
可以使用typedef关键字为结构体起别名,这样一些名字很长的结构体名字就可以简化,这样当我们需要频繁创建结构体变量的时候就比较方便。
例如:
#include <stdio.h>
struct Chinese_Student {
char name[50];
int age;
float score;
};
typedef struct Chinese_Student Student; // 为结构体起别名Student
int main() {
Student stu = {"Tom", 20, 88.5}; // 创建并初始化结构体变量,其实就等价于Chinese_Student stu = {"Tom", 20, 88.5};
printf("Name: %s\n", stu.name); // 访问结构体变量的成员
printf("Age: %d\n", stu.age);
printf("Score: %.1f\n", stu.score);
return 0;
}
结构体的自引用
在结构体中包含一个类型为结构体本身的成员可以吗?
比如下面这段代码:
struct Node
{
int data;
struct Node next;
}; //error报错
这段代码是会报错的,因为结构体中包含同一个类型的结构体作为成员,这样结构体变量的大小就会无穷的大,是不合理的。
正确的结构体自引用方式,使用结构体指针
struct Node
{
int data;
struct Node* next;
};
结构体变量的创建和初始化
对于结构体变量的创建和初始化,用示例代码来进行说明:
#include <stdio.h>
//创建结构体变量前,要先对结构体进行声明↓↓↓↓↓↓
struct student {
char name[50]; //这里声明一个student类型的结构体,有三个成员
int age; //分别是字符类型姓名,整型类型年龄,浮点型类型分数
float score;
};
int main() {
// 可以在创建结构体变量的时候同时按声明中的结构体成员顺序对结构体变量进行初始化操作↓↓
struct student stu1 = {"Tom", 20, 88.5};
//也可以用成员访问操作符,不按照成员顺序对结构体成员进行初始化操作↓↓↓↓↓
struct student stu2 = {.age=18, .name="weil", .score=99.9};
// 访问结构体变量的成员↓↓↓↓↓↓↓
printf("Name: %s\n", stu2.name);
printf("Age: %d\n", stu2.age);
printf("Score: %.1f\n", stu2.score);
return 0;
}
结构体内存对齐
对于结构体的大小,我们可以用sizeof 运算符进行计算
如下:
struct S1
{
char c1; //1字节
int i; //4字节
char c2; //1字节
};
int main()
{
printf("%zd\n", sizeof(struct S1));
return 0;
}
运行结果为
上面代码的结构体占的字节数大小为什么会是12个字节呢?
这就涉及到结构体内存对齐的知识点了↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
结构体内存对齐规定
- 结构体的第一个成员对齐到相对结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。
VS中默认的值为8
Linux中没有默认对齐数,对齐数就是成员自身的大小 - 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
为什么存在内存对齐
大部分的参考资料都是这样说的:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
举例解释
struct S1
{
char c1;
char c2;
int i;
};
struct S2
{
char c1;
int i;
char c2;
};
int main()
{
printf("%zd\n", sizeof(struct S1));
printf("%zd\n", sizeof(struct S2));
return 0;
}
以上代码运行结果(VS 2022):
结构体中的成员类型相同,不同的排列顺序也会影响结构体变量的大小,这就是结构体内存对齐的奥妙,接下来画内存布局图来进行解释:
那在设计结构体的时候,我们既要满足对齐,又要节省空间,就要让占用空间小的成员尽量集中在一起
结构体数组
↓↓↓↓↓↓↓↓↓↓↓↓↓创建结构体数组 & 遍历访问结构体数组↓↓↓↓↓↓↓↓↓↓↓↓↓
代码示例如下:
#include <stdio.h>
// 定义一个结构体类型
struct Student {
char name[20];
int age;
float score;
};
int main() {
// 创建结构体数组并初始化
struct Student students[] = {
{"Tom", 20, 88.5},
{"Jack", 21, 92.0},
{"Alice", 19, 95.5}
};
// 遍历访问打印结构体数组中的所有内容
int len = sizeof(students) / sizeof(struct Student); // 计算数组长度
for (int i = 0; i < len; i++) {
printf("Name: %s\n", students[i].name);
printf("Age: %d\n", students[i].age);
printf("Score: %.1f\n", students[i].score);
}
return 0;
}
在这个示例中,我们首先定义了一个名为Student的结构体类型,包含三个成员变量:name、age和score。然后,我们在main函数中创建了一个Student类型的结构体数组students,并初始化其中的元素。接着,我们使用一个for循环遍历访问数组中的每个元素,并使用printf函数打印出每个学生的姓名、年龄和分数。
在访问结构体数组元素时,我们使用了结构体变量名加上索引的方式来访问,例如students[i].name表示访问第i个元素的name成员变量。
结构体指针
↓↓↓↓↓↓↓↓↓↓↓↓↓结构体指针的创建 & 初始化 & 成员访问↓↓↓↓↓↓↓↓↓↓↓↓↓
#include <stdio.h>
// 定义一个结构体类型
struct Student {
char name[20];
int age;
float score;
};
int main() {
// 创建结构体变量并初始化
struct Student stu1 = {"Tom", 20, 88.5};
struct Student stu2 = {"Jack", 21, 92.0};
struct Student stu3 = {"Alice", 19, 95.5};
// 创建结构体指针变量
struct Student *stuPtr;
// 将结构体变量的地址赋给结构体指针变量
stuPtr = &stu1;
// 遍历打印结构体指针中的内容
printf("Name: %s\n", stuPtr->name);
printf("Age: %d\n", stuPtr->age);
printf("Score: %.1f\n", stuPtr->score);
// 将结构体指针变量指向下一个结构体变量
stuPtr = &stu2;
// 遍历打印结构体指针中的内容
printf("Name: %s\n", stuPtr->name);
printf("Age: %d\n", stuPtr->age);
printf("Score: %.1f\n", stuPtr->score);
// 将结构体指针变量指向下一个结构体变量
stuPtr = &stu3;
// 遍历打印结构体指针中的内容
printf("Name: %s\n", stuPtr->name);
printf("Age: %d\n", stuPtr->age);
printf("Score: %.1f\n", stuPtr->score);
return 0;
}
在这个示例中,我们首先定义了一个Student结构体类型,包含了学生的姓名、年龄和分数。在main函数中,我们创建了三个Student类型的结构体变量,并分别初始化它们。
然后,我们创建了一个Student类型的指针变量stuPtr
,用于指向这些结构体变量。我们将第一个结构体变量的地址赋给了stuPtr,并使用->运算符
通过指针访问结构体中的数据,并打印出学生的信息。接着,我们将指针变量指向下一个结构体变量,重复上述操作,直到遍历完所有的结构体变量。