#include<GUIQU.h>
int main {
上期回顾: 【C语言回顾】数据在内存中的存储
个人主页:C_GUIQU
专栏:【C语言学习】
return 一键三连;
}
前言
各位小伙伴大家好!上期小编给大家讲解了C语言中数据在内存中的存储,接下来我们讲解一下结构体!
1. 结构体类型的声明
在C语言中,结构体(struct)是一种用户自定义的数据类型,允许将不同类型的数据项组合成一个单一的复合类型。
结构体类型的声明通常使用 struct
关键字,并定义一个结构体标签,然后是一对大括号,括号内包含一个或多个成员变量。
下面是一个结构体声明的例子:
struct Student {
char name[50];
int age;
float score;
};
在这个例子中,struct Student
是一个包含三个成员的结构体:一个字符数组 name
用于存储学生的名字,一个整数 age
用于存储学生的年龄,以及一个浮点数 score
用于存储学生的分数。
一旦声明了结构体类型,就可以使用 struct
关键字来声明该类型的变量:
struct Student student1;
此外,C语言允许在声明结构体类型的同时声明变量:
struct {
char name[50];
int age;
float score;
} student2;
在这种情况下,我们没有为结构体类型提供标签,这样的结构体称为匿名结构体,student2
变量就是这种匿名结构体类型的一个实例。由于没有标签,这种形式的结构体声明通常只能使用一次,即在声明的同时创建变量。
结构体成员的访问是通过点操作符(.
)来完成的,例如:
strcpy(student1.name, "张三");
student1.age = 20;
student1.score = 92.5;
这些语句分别设置 student1
结构体的 name
、age
和 score
成员。
结构体类型声明:
- 结构体标签:在声明结构体时,可以为其指定一个标签,例如
struct Student
。这个标签在后续声明结构体变量时使用。 - 成员变量:结构体内部可以包含一个或多个成员变量,这些成员可以是基本数据类型(如 int、float、char 等),也可以是其他复合数据类型(如数组、指针、其他结构体等)。
- 匿名结构体:如果省略结构体标签,则称为匿名结构体。这种结构体只能在声明时直接创建变量,不能在其他地方使用。
- 结构体变量的声明:使用
struct
关键字和结构体标签来声明结构体变量。例如,struct Student student1;
声明了一个名为student1
的struct Student
类型的变量。 - 结构体成员的访问:使用点操作符(
.
)来访问结构体成员。例如,student1.age = 20;
将student1
结构体的age
成员设置为 20。 - 结构体数组:可以声明结构体数组来存储多个结构体变量。例如,
struct Student students[100];
声明了一个包含 100 个struct Student
类型元素的数组。 - 结构体指针:可以使用指针来存储结构体变量的地址。例如,
struct Student *ptr = &student1;
声明了一个指向student1
结构体变量的指针。结构体成员的访问可以使用箭头操作符(->
):ptr->age = 20;
。 - 结构体初始化:可以在声明结构体变量时进行初始化。例如:
struct Student student1 = {"张三", 20, 92.5};
或者使用 C99 标准的指定初始化器:
struct Student student1 = {.name = "张三", .age = 20, .score = 92.5};
2. 结构体变量的创建和初始化
在C语言中,结构体变量可以通过多种方式进行创建和初始化。
创建结构体变量
- 声明结构体类型后创建变量:
首先,你需要声明一个结构体类型,然后使用该类型来创建变量。struct Student { char name[50]; int age; float score; }; struct Student student1;
- 在声明结构体类型的同时创建变量:
你可以在声明结构体类型的同时直接创建变量,无需指定结构体标签。
这种方式创建的变量struct { char name[50]; int age; float score; } student2;
student2
是一个匿名结构体变量,你不能在其他地方使用结构体类型来创建更多变量。 - 使用指针创建结构体变量:
你可以使用malloc
或其他内存分配函数来动态分配结构体变量的内存。
注意,使用动态分配的内存后,需要手动释放内存以避免内存泄漏。struct Student *student3 = malloc(sizeof(struct Student));
初始化结构体变量
- 在创建时初始化:
你可以在创建结构体变量时直接初始化其成员。
这种初始化方式适用于静态分配的结构体变量。struct Student student1 = {"张三", 20, 92.5};
- 使用指定初始化器(C99及以后版本):
C99标准引入了指定初始化器,允许你初始化结构体的特定成员。
这种方式可以只初始化结构体中的部分成员,未指定的成员将被自动初始化为0或空字符串。struct Student student1 = {.name = "张三", .age = 20, .score = 92.5};
- 动态分配时初始化:
如果你使用malloc
分配结构体变量,可以在分配后手动初始化成员。struct Student *student3 = malloc(sizeof(struct Student)); strcpy(student3->name, "张三"); student3->age = 20; student3->score = 92.5;
- 使用函数初始化:
你可以编写一个函数来初始化结构体变量,这在处理复杂结构体时特别有用。void initializeStudent(struct Student *student) { strcpy(student->name, "张三"); student->age = 20; student->score = 92.5; } struct Student student1; initializeStudent(&student1);
注意事项
- 初始化结构体数组时,每个元素都必须单独初始化。
- 对于动态分配的结构体,记得在使用完毕后释放内存,以避免内存泄漏。
- 如果结构体中有指针成员,确保正确地管理指向的内存,避免悬挂指针和内存泄漏。
3. 结构成员访问操作符
在C语言中,结构体成员访问操作符用于访问结构体变量的成员。有两种主要的成员访问操作符:点操作符(.
)和箭头操作符(->
)。
点操作符(.
)
点操作符用于直接访问结构体变量的成员。当你有一个结构体变量的实例时,你可以使用点操作符来访问其成员。
struct Student {
char name[50];
int age;
float score;
};
struct Student student1;
// 使用点操作符来访问和赋值成员
strcpy(student1.name, "张三");
student1.age = 20;
student1.score = 92.5;
在上面的例子中,student1
是一个 struct Student
类型的结构体变量。我们使用点操作符来设置其 name
、age
和 score
成员的值。
箭头操作符(->
)
箭头操作符用于通过指向结构体的指针来访问结构体的成员。当有一个指向结构体的指针时,可以使用箭头操作符来访问其成员。
struct Student {
char name[50];
int age;
float score;
};
struct Student *ptr = &student1;
// 使用箭头操作符来访问和赋值成员
strcpy(ptr->name, "张三");
ptr->age = 20;
ptr->score = 92.5;
在上面的例子中,ptr
是一个指向 struct Student
类型变量的指针。我们使用箭头操作符来设置其所指向的结构体变量的 name
、age
和 score
成员的值。
注意事项
- 点操作符用于结构体变量本身,而箭头操作符用于结构体指针。
- 当你使用点操作符时,你不需要解引用指针,因为点操作符会自动处理。
- 当你使用箭头操作符时,你不需要使用箭头左侧的指针的地址,因为箭头操作符会自动解引用指针。
示例
下面是一个完整的示例,展示了如何使用点操作符和箭头操作符来访问结构体成员:
#include <stdio.h>
#include <string.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
// 创建结构体变量
struct Student student1;
// 使用点操作符访问和赋值成员
strcpy(student1.name, "张三");
student1.age = 20;
student1.score = 92.5;
// 创建指向结构体的指针
struct Student *ptr = &student1;
// 使用箭头操作符访问和打印成员
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
printf("Score: %.2f\n", ptr->score);
return 0;
}
在这个示例中,我们首先使用点操作符来设置 student1
的成员,然后使用箭头操作符通过 ptr
指针来访问和打印这些成员的值。
4. 结构体内存对齐
在C语言中,结构体的内存对齐是指结构体成员在内存中的排列方式,这种排列方式受到编译器和目标平台的对齐要求的影响。理解结构体的内存对齐对于优化内存使用和访问速度非常重要。
内存对齐的原因
内存对齐的主要原因是提高内存访问效率。许多计算机系统的内存访问是按照字(word)边界对齐的,这意味着访问一个对齐的字比访问未对齐的数据快得多。字的大小通常是处理器架构的一个特性,例如,32位架构上的字大小通常是4字节。
对齐规则
结构体的内存对齐通常遵循以下规则:
- 成员对齐:结构体的每个成员按照其类型的自然对齐要求进行对齐。例如,int类型通常在4字节边界对齐,而char类型可以在任何字节边界对齐。
- 结构体对齐:整个结构体的大小是其最宽成员的宽度的倍数,最宽成员是指占用最大空间的成员。
- 填充(Padding):为了满足对齐要求,编译器可能会在结构体的成员之间插入额外的未使用空间,这称为填充。
- 数组对齐:数组元素的内存对齐取决于元素类型。整个数组的起始地址必须满足元素类型的对齐要求。
示例
下面是一个结构体的例子,以及它如何在内存中对齐的说明:
struct Example {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
};
在32位系统上,这个结构体的内存布局可能如下:
+------+------+------+------+------+
| a | pad | pad | pad | b |
+------+------+------+------+------+
| b | b | b | c | pad |
+------+------+------+------+------+
| pad | pad | pad | pad | pad |
+------+------+------+------+------+
这里,a
后面有3字节的填充,以确保 b
在4字节边界上对齐。c
后面也有3字节的填充,以确保整个结构体的大小是最大成员 int b
的宽度的倍数,即4字节。
对齐属性
C语言提供了offsetof
宏,用于获取结构体成员的偏移量,以及sizeof
运算符,用于获取结构体的大小。这些可以帮助理解结构体的内存布局。
printf("Offset of 'a' = %zu\n", offsetof(struct Example, a));
printf("Offset of 'b' = %zu\n", offsetof(struct Example, b));
printf("Offset of 'c' = %zu\n", offsetof(struct Example, c));
printf("Size of struct = %zu\n", sizeof(struct Example));
对齐控制
在C语言中,可以使用pragma pack
指令来控制结构体的对齐方式。这可以改变默认的对齐规则,但应该谨慎使用,因为它可能会影响性能。
#pragma pack(push, 1) // 设置最大对齐为1字节
struct Example {
char a;
int b;
char c;
};
#pragma pack(pop) // 恢复之前的对齐设置
在这个例子中,pragma pack
指令使得结构体成员之间没有填充,结构体的大小将是所有成员大小的总和。
总结
结构体的内存对齐是由编译器和目标平台决定的,它对程序的性能和内存使用有重要影响。理解对齐规则可以帮助程序员优化数据结构和内存访问模式。
5. 结构体传参
在C语言中,结构体可以作为函数的参数传递。传递结构体参数有几种不同的方式,每种方式都有其特点和用途。
传递结构体值
当你将结构体作为函数参数传递时,默认情况下是按值传递的。这意味着函数接收的是结构体参数的一个副本,对参数的任何修改都不会影响原始结构体。
struct Point {
int x;
int y;
};
void printPoint(struct Point p) {
printf("Point: (%d, %d)\n", p.x, p.y);
}
int main() {
struct Point myPoint = {3, 4};
printPoint(myPoint); // 传递结构体值的副本
return 0;
}
在上面的例子中,printPoint
函数接收 struct Point
的一个副本。在函数内部对 p
的任何修改都不会影响 myPoint
。
传递结构体指针
另一种方式是传递指向结构体的指针。这样,函数接收的是结构体地址,因此函数内部对结构体的任何修改都会影响原始结构体。
void modifyPoint(struct Point *p) {
p->x = 10;
p->y = 20;
}
int main() {
struct Point myPoint = {3, 4};
modifyPoint(&myPoint); // 传递结构体的地址
printf("Modified Point: (%d, %d)\n", myPoint.x, myPoint.y);
return 0;
}
在上面的例子中,modifyPoint
函数接收一个指向 struct Point
的指针。函数内部对 p
的修改会改变 myPoint
的值。
传递结构体引用
C语言本身不支持引用传递,但可以通过指针实现类似的效果。传递结构体指针实际上就是传递结构体的引用。
void modifyPoint(struct Point *p) {
p->x = 10;
p->y = 20;
}
int main() {
struct Point myPoint = {3, 4};
modifyPoint(&myPoint); // 传递结构体的引用
printf("Modified Point: (%d, %d)\n", myPoint.x, myPoint.y);
return 0;
}
在上面的例子中,modifyPoint
函数通过指针 p
操作原始的 myPoint
结构体。
注意事项
- 当结构体较大时,按值传递可能会导致性能问题,因为需要复制整个结构体。
- 传递指针可以避免复制结构体,但需要小心处理内存管理,避免悬挂指针和内存泄漏。
- 如果函数不需要修改结构体,可以将参数声明为
const
指针,以确保结构体不被意外修改。
示例
下面是一个完整的示例,展示了如何通过值和指针传递结构体:
#include <stdio.h>
struct Point {
int x;
int y;
};
void printPoint(struct Point p) {
printf("Point: (%d, %d)\n", p.x, p.y);
}
void modifyPoint(struct Point *p) {
p->x = 10;
p->y = 20;
}
int main() {
struct Point myPoint = {3, 4};
printPoint(myPoint); // 按值传递
modifyPoint(&myPoint); // 按指针传递
printf("Modified Point: (%d, %d)\n", myPoint.x, myPoint.y);
return 0;
}
在这个示例中,我们首先通过值传递打印 myPoint
,然后通过指针传递修改 myPoint
的值,并再次打印修改后的值。
6. 结构体实现位段
在C语言中,位段(bit field)是一种特殊的结构体成员,它允许你以位为单位来分配内存空间。位段特别有用,当你需要节省内存或者在数据结构中处理二进制数据时。
位段的基本语法
位段的语法是在结构体定义中,使用 :
来指定成员的位数。例如:
struct BitField {
unsigned int a : 3; // 占用3位
unsigned int b : 2; // 占用2位
unsigned int c : 1; // 占用1位
};
在上面的例子中,struct BitField
包含三个位段成员 a
、b
和 c
,它们分别占用3位、2位和1位。
位段的存储和对齐
位段通常按照成员声明的顺序存储,从一个字节的开头开始,如果当前字节的剩余空间不足以存储下一个位段,则从下一个字节继续。位段的存储顺序和字节对齐取决于具体的编译器和目标平台。
位段的注意事项
- 位段必须是
int
类型的,可以是signed
或unsigned
。 - 位段的大小不能超过
int
的大小。例如,在一个32位系统中,位段的大小不能超过32位。 - 位段的地址是不可见的,也就是说,你不能取位段的地址。
- 位段通常用于存储标志或者枚举值,而不适合用于大量数据的存储。
位段的应用
位段常用于以下场景:
- 当你需要表示一组布尔标志时,可以使用位段来节省空间。
- 当你需要处理硬件寄存器时,位段可以帮助你映射寄存器的位域。
示例
下面是一个使用位段的示例:
#include <stdio.h>
structBitFields {
unsigned int a : 3; // 占用3位
unsigned int b : 2; // 占用2位
unsigned int c : 1; // 占用1位
// 3 + 2 + 1 = 6位,占用1个字节
};
int main() {
structBitFields bf;
bf.a = 5; // 101
bf.b = 3; // 11
bf.c = 1; // 1
printf("Size of struct: %zu bytes\n", sizeof(structBitFields));
printf("BitFields: a = %u, b = %u, c = %u\n", bf.a, bf.b, bf.c);
return 0;
}
在这个示例中,我们定义了一个包含三个位段的结构体 BitFields
。我们设置了这些位段的值,并打印了结构体的大小和位段的值。由于位段的总和是6位,所以结构体占用1个字节的内存。
位段是一种强大的特性,可以让我们以非常精细的方式控制数据的存储和布局。然而,由于位段的实现依赖于具体的编译器和平台,因此在跨平台编程时需要特别小心。
结语
以上就是小编对结构体的详细讲解。
如果觉得小编讲的还可以,还请一键三连。互三必回!
持续更新中~!