1. 结构体基础概念与定义方式
结构体(struct)是C语言中最重要的复合数据类型之一,它允许我们将不同类型的数据组合成一个整体。在实际开发中,结构体常用于表示具有多个属性的实体,比如学生信息、商品信息等。
1.1 常规结构体定义
最基本的定义方式如下:
c复制struct Student {
char name[20]; // 姓名,字符数组
int num; // 学号,整型
int age; // 年龄,整型
};
这种定义方式有几个关键点需要注意:
struct是关键字,表示开始定义一个结构体Student是结构体标签(tag),用于标识这个结构体类型- 大括号内是结构体的成员列表,每个成员都有自己的类型和名称
提示:结构体定义本身不会分配内存,只有在声明结构体变量时才会分配内存空间。
1.2 使用typedef定义结构体别名
在实际开发中,我们经常使用typedef来为结构体创建别名,这样可以简化代码:
c复制typedef struct Student1 {
char name[20];
int num;
int age;
} STU, *STUP;
这里做了两件事:
- 定义了一个结构体类型
struct Student1 - 使用typedef创建了两个别名:
STU代表struct Student1STUP代表struct Student1*(指向该结构体的指针)
这种写法的优势在于:
- 简化了变量声明,不需要每次都写
struct关键字 - 指针类型有明确的命名,提高代码可读性
- 符合现代C语言的编码习惯
2. 结构体变量的声明与初始化
2.1 结构体变量的声明方式
根据是否使用typedef别名,声明方式有所不同:
不使用typedef的情况
c复制struct Student s; // 声明一个结构体变量
struct Student *p; // 声明一个结构体指针
使用typedef的情况
c复制STU stu1; // 等价于 struct Student1 stu1
STUP stu2 = &stu1; // 等价于 struct Student1* stu2
2.2 结构体变量的初始化方法
结构体初始化有多种方式,各有优缺点:
列表初始化
c复制struct Student s = {"刘备", 1, 45};
特点:
- 按照成员定义的顺序初始化
- 新增成员时可能导致初始化数据错位
- 适合结构体成员固定不变的场景
指定成员初始化(C99标准)
c复制struct Student s1 = {.name = "张飞", .num = 2, .age = 25};
优势:
- 初始化顺序可以任意调整
- 新增成员不影响现有初始化代码
- 可以只初始化部分成员
混合初始化
c复制struct Student s3 = {.name = "张飞", 2, .age = 25};
注意:
- 混合使用时,未指定成员的初始化必须按顺序
- 不建议这种写法,容易出错
逐个成员赋值
c复制struct Student s5;
strcpy(s5.name, "赵云");
s5.num = 3;
s5.age = 34;
适用场景:
- 需要运行时动态赋值的情况
- 部分成员需要计算后才能赋值的情况
2.3 结构体指针的初始化
结构体指针在使用前必须指向有效的内存地址:
c复制// 方式1:指向已存在的结构体变量
struct Student *p = &s5;
// 方式2:动态分配内存
struct Student *p1 = malloc(sizeof(struct Student));
if (p1 == NULL) {
// 错误处理
}
strcpy(p1->name, "马超");
p1->age = 25;
p1->num = 4;
重要提示:使用malloc分配的内存必须在使用后free,否则会导致内存泄漏。
3. 结构体成员的访问方式
结构体成员的访问有两种方式,取决于操作的是结构体变量还是指针:
3.1 结构体变量访问成员
使用点操作符(.):
c复制struct Student s5;
s5.age = 34; // 直接访问
strcpy(s5.name, "赵云"); // 对于数组成员需要使用strcpy
3.2 结构体指针访问成员
使用箭头操作符(->):
c复制struct Student *p1 = malloc(sizeof(struct Student));
p1->age = 25; // 指针访问
strcpy(p1->name, "马超");
3.3 嵌套结构体的访问
当结构体包含其他结构体时,访问需要逐级进行:
c复制struct Score {
int chinese;
int math;
int english;
};
struct Student {
char name[20];
struct Score s; // 嵌套结构体
};
// 访问嵌套成员
struct Student stu = {"诸葛亮", {90, 85, 95}};
printf("数学成绩:%d\n", stu.s.math); // 输出85
4. 结构体作为函数参数和返回值
4.1 结构体作为函数参数
结构体可以作为参数传递给函数,有两种传递方式:
传值调用
c复制void display1(struct Student s1) {
printf("姓名:%s\n", s1.name);
// 其他操作...
}
特点:
- 函数内操作的是副本
- 不会影响原结构体
- 但会复制整个结构体,性能开销较大
传址调用
c复制void display2(struct Student *p1) {
printf("姓名:%s\n", p1->name);
// 其他操作...
}
优势:
- 只传递指针,效率高
- 可以修改原结构体内容
- 适合大型结构体
4.2 结构体作为函数返回值
函数可以返回结构体或结构体指针:
返回结构体值
c复制struct Student createStudent() {
struct Student s = {"黄忠", 5, 60};
return s;
}
注意:
- 返回的是副本
- C语言会对小结构体做返回值优化(RVO)
返回结构体指针
c复制STUP createStudentPtr() {
STUP p = malloc(sizeof(STU));
if (p) {
strcpy(p->name, "魏延");
p->num = 6;
p->age = 35;
}
return p;
}
重要事项:
- 返回的指针必须指向有效内存
- 动态分配的内存需要调用者负责释放
- 不要返回指向局部变量的指针
5. 结构体应用实例:学生成绩排序
下面通过一个完整案例展示结构体的实际应用:
5.1 问题描述
定义一个学生结构体,包含学号、姓名、年龄和成绩。创建包含5个学生的数组,根据成绩进行降序排序并输出。
5.2 完整实现代码
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct student {
int id;
char name[20];
int age;
float score;
} STU, *STUP;
// 交换两个学生结构体
void swap(STU *a, STU *b) {
STU tmp = *a;
*a = *b;
*b = tmp;
}
// 冒泡排序实现
STUP bubbleSort(STU *stu, int n, int order) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
int condition = (order == 1) ?
(stu[j].score < stu[j+1].score) :
(stu[j].score > stu[j+1].score);
if (condition) {
swap(&stu[j], &stu[j+1]);
}
}
}
return stu;
}
int main() {
STU stu[5];
// 输入学生信息
printf("请输入学号\t姓名\t年龄\t成绩\n");
for (int i = 0; i < 5; i++) {
scanf("%d %s %d %f", &stu[i].id, stu[i].name, &stu[i].age, &stu[i].score);
}
// 按成绩降序排序
bubbleSort(stu, 5, 1);
// 输出结果
printf("\n排序结果:\n");
printf("学号\t姓名\t年龄\t成绩\n");
for (int i = 0; i < 5; i++) {
printf("%d\t%s\t%d\t%.1f\n", stu[i].id, stu[i].name, stu[i].age, stu[i].score);
}
return 0;
}
5.3 代码解析
-
结构体定义:
- 使用typedef定义了STU和STUP两个别名
- 包含学号、姓名、年龄和成绩四个字段
-
swap函数:
- 通过指针交换两个结构体的内容
- 使用临时变量完成交换操作
-
bubbleSort函数:
- 参数order控制排序顺序(1降序/0升序)
- 双重循环实现冒泡算法
- 直接操作原数组,不需要额外空间
-
输入输出处理:
- 使用循环读取5个学生信息
- 格式化输出排序结果
- 注意字符串(name)直接读取,不需要取地址
5.4 执行结果示例
code复制请输入学号 姓名 年龄 成绩
1 张三 18 90.5
2 李四 19 88.0
3 王五 20 92.5
4 赵六 19 85.0
5 钱七 18 95.5
排序结果:
学号 姓名 年龄 成绩
5 钱七 18 95.5
3 王五 20 92.5
1 张三 18 90.5
2 李四 19 88.0
4 赵六 19 85.0
6. 结构体使用中的常见问题与技巧
6.1 内存对齐问题
结构体在内存中的布局会受到对齐规则影响:
c复制struct Example {
char a; // 1字节
int b; // 4字节
short c; // 2字节
};
在32位系统上,这个结构体的大小可能是12字节而非7字节,因为编译器会插入填充字节来满足对齐要求。
解决方法:
- 使用
#pragma pack指令调整对齐方式 - 合理安排成员顺序(从大到小或从小到大)
6.2 结构体赋值与比较
结构体可以直接赋值,但不能直接比较:
c复制STU a = {"赵云", 1, 25};
STU b = a; // 正确,结构体复制
if (a == b) { // 错误!不能直接比较
// ...
}
正确做法:
- 逐个比较成员
- 使用memcmp比较内存块(需确保没有填充字节差异)
6.3 动态结构体数组
创建动态大小的结构体数组:
c复制int count = 10;
STU *students = malloc(count * sizeof(STU));
if (students == NULL) {
// 错误处理
}
// 使用...
free(students); // 释放内存
6.4 结构体与文件I/O
将结构体写入文件或从文件读取:
c复制// 写入单个结构体
STU s = {"关羽", 2, 40};
FILE *fp = fopen("data.bin", "wb");
fwrite(&s, sizeof(STU), 1, fp);
fclose(fp);
// 读取结构体数组
STU arr[5];
fp = fopen("array.bin", "rb");
fread(arr, sizeof(STU), 5, fp);
fclose(fp);
注意事项:
- 二进制I/O比文本I/O效率高
- 不同平台可能有字节序差异
- 指针成员不能直接写入文件
6.5 结构体中的柔性数组
C99支持柔性数组成员:
c复制struct FlexArray {
int length;
double data[]; // 柔性数组成员
};
// 使用
struct FlexArray *fa = malloc(sizeof(struct FlexArray) + 10*sizeof(double));
fa->length = 10;
for (int i = 0; i < 10; i++) {
fa->data[i] = i * 1.1;
}
特点:
- 柔性数组必须是最后一个成员
- 不计入结构体大小
- 需要手动分配足够内存
7. 结构体在实际项目中的应用建议
-
命名规范:
- 结构体标签使用大驼峰命名法(如StudentInfo)
- 成员变量使用小驼峰命名法(如studentName)
-
设计原则:
- 保持结构体单一职责
- 避免过大的结构体
- 嵌套层次不宜过深
-
性能考虑:
- 频繁传递的结构体使用指针
- 只读结构体可以加上const限定符
- 热点代码考虑内存局部性
-
可维护性:
- 为复杂结构体提供初始化函数
- 添加必要的注释说明成员用途
- 考虑向前兼容性(新增成员不影响旧代码)
-
高级用法:
- 结构体结合函数指针实现简单面向对象
- 使用联合体(union)实现变体记录
- 位域(bit-field)处理紧凑数据
在实际C语言项目中,结构体是最基础也是最重要的数据结构之一。掌握结构体的各种用法,能够帮助我们更好地组织和管理复杂数据。从系统编程到应用开发,结构体都扮演着不可或缺的角色。
