基本概念
结构体(struct)是一种用户自定义的数据类型,它允许程序员将多个不同类型的变量封装到一个单一的实体中。这种封装有助于管理和组织相关数据,特别是当这些数据在概念上属于一个整体或者需要一起操作时。
组成与成员
结构体由一系列称为成员(或域、元素)的数据项组成,每个成员可以具有不同的数据类型(如整型、字符型、浮点型、指针等)。成员之间通过逗号分隔,并且可以指定各自的名称以方便访问和管理。每个成员在结构体内占据一定的存储空间,所有成员的空间总和即为整个结构体所占的内存大小。
定义结构体
结构体的定义通常采用如下形式:
1struct struct_name {
2 member_type member1;
3 member_type member2;
4 // ...
5 member_type memberN;
6};
其中:
struct_name
是自定义的结构体标签,用于标识该结构体类型。member_type
表示每个成员的数据类型。member1
,member2
, ...,memberN
是成员的名称,用于在程序中引用这些成员。
例如,定义一个表示学生的结构体可能如下:
1struct Student {
2 char name[50]; // 姓名,用一个固定长度的字符数组表示
3 int age; // 年龄,用整型表示
4 float height; // 身高,用浮点型表示
5};
结构体的定义其他方式
1. 定义结构体类型的同时声明变量
在定义结构体时,可以直接同时声明一个或多个该结构体类型的变量。这种做法将结构体类型定义与变量声明合并在一条语句中,简化了代码:
1struct struct_name {
2 member_type member1;
3 // ...
4} variable1, variable2;
例如:
1struct Date {
2 int day;
3 int month;
4 int year;
5} today, birthday;
这里,Date
结构体类型被定义,同时声明了 today
和 birthday
两个 Date
类型的变量。
2. 匿名结构体与联合体
C语言允许定义没有标签(名称)的结构体或联合体,它们通常出现在变量声明中,仅用于一次性声明特定类型的变量。匿名结构体的声明如下:
1struct {
2 member_type member1;
3 // ...
4} variable;
匿名结构体的特点是不能单独引用其类型,只能在声明变量时使用。这种形式适用于不需要多次声明同类型变量或者类型本身无需复用的场景。由于没有类型名称,匿名结构体变量之间的赋值或传递通常需要借助指针或 memcpy() 函数。
3. 结构体嵌套
结构体可以包含另一个结构体或联合体作为其成员,形成嵌套结构。这允许构建更复杂的复合数据类型,比如一个员工结构体可能包含一个地址结构体作为其成员:
1struct Address {
2 char street[50];
3 char city[30];
4 char postal_code[10];
5};
6
7struct Employee {
8 char name[50];
9 int id;
10 struct Address work_address;
11};
4. 结构体指针类型
虽然不是直接定义结构体的方式,但在某些情况下,特别是在函数参数传递和动态内存分配中,会使用结构体指针类型代替结构体本身。结构体指针类型定义如下:
1struct struct_name *pointer_variable;
这定义了一个指向 struct_name
类型结构体的指针变量,通过它来间接访问和操作结构体实例。
5. 使用 typedef
创建结构体类型别名
为了简化结构体类型的使用,可以使用 typedef
关键字为已定义的结构体创建一个类型别名:
1typedef struct struct_name {
2 member_type member1;
3 // ...
4} AliasName;
此后,可以直接使用 AliasName
代替 struct struct_name
来声明变量:
声明结构体变量
定义了结构体类型后,可以声明该类型的变量来创建结构体实例。声明结构体变量的方式如下:
1 struct struct_name variable_name;
或者,如果已经定义了结构体类型,可以使用typedef
为结构体创建一个别名,简化后续的声明:
1 typedef struct {
2 // 成员定义...
3} StructAlias;
4 StructAlias variable;
访问结构体成员
访问结构体成员时,使用.
(成员访问运算符)连接结构体变量名和成员名:
1struct Student s;
2s.name = "Alice";
3s.age = 20;
4s.height = 1.75f;
结构体的初始化
1. 零初始化
使用 {}
初始化结构体时,所有成员会被自动初始化为它们各自类型的零值。这对于包含指针或其他需要初始化为 NULL 或 0 的成员特别有用。例如:
1struct Point {
2 int x;
3 int y;
4};
5
6struct Point p = {};
在此例中,p.x
和 p.y
都会被初始化为 0。
2. 列表初始化
使用列表初始化(也称作聚合初始化)可以为结构体的每个成员指定初始值。成员按照它们在结构体中的声明顺序进行初始化。例如:
1struct Person {
2 char name[50];
3 int age;
4 float height;
5};
6
7struct Person person = {"Alice", 30, 1.68f};
在这里,person.name
被初始化为字符串 "Alice",person.age
为 30,person.height
为 1.68f。
3. 分步初始化
对于已声明的结构体变量,可以逐个初始化其成员:
1struct Point p;
2p.x = 10;
3p.y = 20;
4. 结构体数组的初始化
结构体数组也可以使用上述方法进行初始化。例如,对数组中的每个元素分别进行列表初始化:
1struct Person people[3] = {
2 {"Alice", 30, 1.68f},
3 {"Bob", 35, 1.80f},
4 {"Charlie", 25, 1.75f}
5};
5. 结构体指针的初始化
对于结构体指针,首先需要分配内存(通常是通过 malloc()
),然后对分配的内存进行初始化:
1struct Person *person = malloc(sizeof(struct Person));
2if (person != NULL) {
3 person->name = "Alice";
4 person->age = 30;
5 person->height = 1.68f;
6}
注意,在使用完动态分配的结构体内存后,应调用 free()
进行释放。
结构体数组
1.定义结构体数组
定义结构体数组的基本语法与定义普通数组相似,只是数组元素类型换成了已定义的结构体类型:
1struct StructureType {
2 // 成员定义
3};
4
5// 定义结构体数组
6struct StructureType arrayName[arraySize];
其中:
StructureType
是先前定义的结构体类型。arrayName
是为数组赋予的名称。arraySize
是数组中元素的数量,即结构体实例的个数。
例如,定义一个包含10个学生的结构体数组:
1struct Student {
2 char name[50];
3 int age;
4 float gpa;
5};
6
7struct Student students[10];
2.初始化结构体数组
结构体数组的初始化可以采用以下几种方式:
列表初始化:
为数组中的每个元素提供初始值,按顺序对应结构体成员:
1struct Student students[3] = {
2 {"Alice", 19, 3..jpg},
3 {"Bob", 20, 3.5},
4 {"Charlie", 21, 3.8}
5};
部分初始化:
只对数组的部分元素进行初始化,未初始化的元素将自动填充为相应类型的零值:
1struct Student students[5] = {
2 {"Alice", 19, 3.8},
3 {"Bob", 20, 3.5}
4};
5// students[2]、students[3] 和 students[4] 自动零初始化
动态初始化:
对于动态分配的结构体数组(通过 malloc()
或 calloc()
),可以逐个初始化数组元素:
1struct Student *students = malloc(5 * sizeof(struct Student));
2if (students != NULL) {
3 strcpy(students[0].name, "Alice");
4 students[0].age = 19;
5 students[0].gpa = 3.8;
6 // ... 初始化其他元素
7}
3.访问与操作结构体数组
结构体数组的访问与普通数组类似,通过索引来访问或修改特定元素:
1// 访问第 i 个学生的姓名
2printf("Student %d: Name = %s\n", i, students[i].name);
3
4// 修改第 j 个学生的年龄
5students[j].age = newAge;
4.遍历结构体数组
通常使用循环来遍历结构体数组,处理或显示每个元素的信息:
1for (size_t i = 0; i < arraySize; ++i) {
2 printf("Student %d:\n", i + 1);
3 printf("Name: %s\n", students[i].name);
4 printf("Age: %d\n", students[i].age);
5 printf("GPA: %.2f\n", students[i].gpa);
6 printf("\n");
7}
结构体指针
1..定义结构体指针
结构体指针的定义语法如下:
1struct StructureType *pointerName;
其中:
StructureType
是已定义的结构体类型。pointerName
是为结构体指针赋予的名称。
例如,定义一个指向 Student
结构体的指针:
1struct Student {
2 char name[50];
3 int age;
4 float gpa;
5};
6
7struct Student *studentPtr;
2.初始化结构体指针
结构体指针需要被初始化为一个有效的结构体变量地址才能进行后续操作。初始化方式有以下几种:
指向已声明的结构体变量:
1struct Student s1 = {"Alice", 19, 3.8};
2struct Student *studentPtr = &s1; // studentPtr 现在指向 s1
指向动态分配的内存:
1struct Student *studentPtr = malloc(sizeof(struct Student));
2if (studentPtr != NULL) {
3 // 可以使用 studentPtr 指向的内存来初始化结构体
4}
初始化为空指针:
1struct Student *studentPtr = NULL; // 指针尚未指向任何结构体变量
3.通过指针访问结构体成员
结构体指针通过使用间接访问运算符 ->
来访问其指向的结构体变量的成员:
1studentPtr->name = "Alice"; // 相当于 (*studentPtr).name = "Alice"
2studentPtr->age = 19;
3studentPtr->gpa = 3.8;
4.结构体指针作为函数参数
将结构体指针作为函数参数可以避免复制整个结构体,提高效率。函数通过指针参数修改结构体成员时,对调用者可见,实现“传址”效果:
1void updateStudent(struct Student *student, const char *newName, int newAge, float newGpa) {
2 strcpy(student->name, newName);
3 student->age = newAge;
4 student->gpa = newGpa;
5}
6
7// 调用示例
8struct Student s = {"Alice", 19, 3.8};
9updateStudent(&s, "Bob", 20, 3.7); // s 的信息已被更新
5.结构体指针数组与指针的指针
可以定义结构体指针数组,用于存储多个结构体变量的地址。另外,可以使用指针的指针(即二级指针)来接收或返回指向结构体变量的指针:
1struct Student *students[10]; // 结构体指针数组
2
3// 函数返回指向结构体的指针
4struct Student *findStudentByName(const char *name) {
5 // ...
6 return foundStudentPtr;
7}
8
9// 函数接收指向结构体指针的指针作为参数
10void allocateAndInitializeStudent(struct Student **studentPtr) {
11 *studentPtr = malloc(sizeof(struct Student));
12 if (*studentPtr != NULL) {
13 // 初始化结构体...
14 }
15}
结构体和函数
结构体做函数参数有三种传递方式
一是传递结构体变量,这是值传递,二是传递结构体指针,这是地址传递,三是传递结构体成员,当然这也分为值传递和地址传递。
1. 传递结构体变量(值传递)
1#include <stdio.h>
2
3// 定义结构体
4struct Point {
5 int x;
6 int y;
7};
8
9// 函数接受结构体变量作为参数(值传递)
10void printPoint(struct Point p) {
11 printf("Point: (%d, %d)\n", p.x, p.y);
12}
13
14int main() {
15 struct Point origin = {0, 0};
16
17 printPoint(origin); // 传递结构体变量的副本
18
19 return 0;
20}
在此例中,函数printPoint
接受一个struct Point
类型的参数p
。当我们在main
函数中调用printPoint
时,会创建origin
结构体的一个副本,并将其传递给函数。由于是值传递,函数内部对p
的修改不会影响到main
函数中的origin
变量。
2. 传递结构体指针(地址传递)
1#include <stdio.h>
2
3struct Person {
4 char name[50];
5 int age;
6};
7
8// 函数接受结构体指针作为参数(地址传递)
9void setAge(struct Person *person, int new_age) {
10 person->age = new_age;
11}
12
13int main() {
14 struct Person john = {"John Doe", 30};
15
16 setAge(&john, 35); // 传递结构体变量的地址
17
18 printf("Updated age: %d\n", john.age);
19
20 return 0;
21}
此处,函数setAge
接受一个指向struct Person
的指针person
。当我们调用setAge
时,传递的是john
结构体变量的地址。函数内部通过指针间接访问和修改原始结构体的成员,因此对age
的修改会影响main
函数中的john
变量。
3. 传递结构体成员(值传递和地址传递)
传递结构体成员有两种情况:
a) 成员值传递
1#include <stdio.h>
2
3struct Rectangle {
4 int length;
5 int width;
6};
7
8// 函数接受结构体成员(整数)作为参数(值传递)
9void printArea(int area) {
10 printf("Area: %d\n", area);
11}
12
13int main() {
14 struct Rectangle rect = {10, 5};
15
16 // 计算并打印矩形面积
17 printArea(rect.length * rect.width);
18
19 return 0;
20}
在这个例子中,我们没有直接传递结构体,而是计算了结构体rect
的length
和width
成员的乘积,并将这个整数值传递给printArea
函数。这是成员的值传递。
b) 成员地址传递
1#include <stdio.h>
2
3struct Coordinate {
4 double x;
5 double y;
6};
7
8// 函数接受结构体成员(double指针)作为参数(地址传递)
9void setCoordinateValue(double *coordinate, double value) {
10 *coordinate = value;
11}
12
13int main() {
14 struct Coordinate point = {0.0, 0.0};
15
16 setCoordinateValue(&point.x, 3.14); // 传递结构体成员x的地址
17
18 printf("Updated x coordinate: %.2f\n", point.x);
19
20 return 0;
21}
这里,函数setCoordinateValue
接受一个指向double
的指针coordinate
。在main
函数中,我们传递了point.x
成员的地址。函数内部通过指针修改了原始结构体的x
成员,实现了对成员的地址传递。