1. 为什么需要结构体?
在C语言中,我们经常需要处理一组相关联的数据。比如要表示一个学生的信息,可能需要记录姓名、学号、年龄、成绩等多个属性。如果使用基本数据类型单独定义这些变量,代码会变得非常零散:
c复制char name[20];
int student_id;
int age;
float score;
这种方式存在几个明显问题:
- 变量之间缺乏逻辑关联,难以体现它们同属一个学生
- 当需要处理多个学生时,变量命名会变得混乱(name1, name2...)
- 函数参数列表会变得冗长(需要传递多个独立参数)
结构体(struct)正是为了解决这些问题而设计的复合数据类型。它允许我们将多个不同类型的变量组合成一个整体,就像把零散的零件组装成一个完整的机器。
提示:结构体是C语言面向对象编程的基础,虽然C不是面向对象语言,但通过结构体+函数指针的方式,可以实现简单的封装和多态特性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 结构体的定义与声明
2.1 基本语法
结构体定义使用struct关键字,基本格式如下:
c复制struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
// ...
};
例如定义一个表示学生的结构体:
c复制struct Student {
char name[20];
int id;
int age;
float score;
};
2.2 结构体变量的声明
定义结构体类型后,可以像基本类型一样声明变量:
c复制// 方式1:先定义类型,再声明变量
struct Student stu1;
// 方式2:定义类型的同时声明变量
struct Student {
// 成员定义
} stu1, stu2;
// 方式3:使用typedef创建别名
typedef struct {
// 成员定义
} Student;
Student stu3; // 无需再写struct关键字
2.3 结构体的内存布局
理解结构体的内存分配对编写高效代码很重要。结构体在内存中是连续存储的,各成员按照定义顺序排列。但编译器可能会在成员之间插入填充字节(padding)以实现内存对齐,这会导致结构体的大小不等于各成员大小之和。
c复制struct Example {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};
// 在32位系统上,sizeof(struct Example)可能是12字节而非6字节
注意:对齐规则因编译器和平台而异,在嵌入式开发中尤其需要注意这点。可以使用
#pragma pack指令调整对齐方式。
3. 结构体的使用
3.1 成员访问
使用点运算符(.)访问结构体变量的成员:
c复制struct Student stu;
strcpy(stu.name, "张三");
stu.id = 1001;
stu.age = 20;
stu.score = 89.5;
3.2 结构体指针
当结构体作为函数参数传递或动态分配时,通常使用指针:
c复制struct Student *pStu = &stu;
// 通过指针访问成员
(*pStu).age = 21; // 写法1
pStu->age = 21; // 写法2(更常用)
3.3 结构体初始化
C语言提供了多种初始化结构体的方式:
c复制// 顺序初始化
struct Student stu1 = {"李四", 1002, 19, 92.5};
// 指定成员初始化(C99起支持)
struct Student stu2 = {
.name = "王五",
.id = 1003,
.score = 85.0 // 未指定的成员自动初始化为0
};
// 结构体数组初始化
struct Student class[3] = {
{"张三", 1001, 20, 89.5},
{"李四", 1002, 19, 92.5},
{"王五", 1003, 21, 85.0}
};
3.4 结构体赋值
C语言允许直接对同类型的结构体变量进行赋值:
c复制struct Student stu1 = {"张三", 1001, 20, 89.5};
struct Student stu2;
stu2 = stu1; // 复制所有成员的值
但要注意,这种赋值是浅拷贝,如果结构体包含指针成员,只会复制指针值而非指向的内容。
4. 结构体的高级用法
4.1 结构体嵌套
结构体可以包含其他结构体作为成员:
c复制struct Date {
int year;
int month;
int day;
};
struct Student {
char name[20];
int id;
struct Date birthday; // 嵌套结构体
float score;
};
// 访问嵌套成员
struct Student stu;
stu.birthday.year = 2000;
4.2 结构体与函数
结构体可以作为函数参数和返回值:
c复制// 结构体作为参数(传值)
void printStudent(struct Student s) {
printf("姓名:%s\n", s.name);
// ...
}
// 结构体指针作为参数(传址,更高效)
void modifyStudent(struct Student *ps) {
strcpy(ps->name, "修改后的名字");
// ...
}
// 返回结构体
struct Student createStudent() {
struct Student s = {"新建学生", 0, 0, 0};
return s;
}
4.3 结构体与动态内存
结构体经常与动态内存分配一起使用:
c复制struct Student *createStudents(int count) {
struct Student *p = (struct Student*)malloc(count * sizeof(struct Student));
if (p == NULL) {
// 处理内存分配失败
}
return p;
}
// 使用
struct Student *class = createStudents(30);
class[0].id = 1001;
// ...
free(class); // 记得释放内存
4.4 位域
结构体支持位域(bit-field),可以精确控制成员占用的位数:
c复制struct {
unsigned int isReady : 1; // 1位
unsigned int count : 4; // 4位
unsigned int : 3; // 3位填充
unsigned int mode : 2; // 2位
} status;
位域常用于嵌入式开发中与硬件寄存器交互,或需要极致节省内存的场景。
5. 结构体的实际应用案例
5.1 学生管理系统
c复制#include <stdio.h>
#include <string.h>
#define MAX_STUDENTS 100
typedef struct {
char name[20];
int id;
float score;
} Student;
void inputStudents(Student *students, int n) {
for (int i = 0; i < n; i++) {
printf("输入第%d个学生的信息(姓名 学号 成绩): ", i+1);
scanf("%s %d %f", students[i].name, &students[i].id, &students[i].score);
}
}
void printStudents(Student *students, int n) {
printf("\n学生列表:\n");
printf("%-10s %-10s %-10s\n", "姓名", "学号", "成绩");
for (int i = 0; i < n; i++) {
printf("%-10s %-10d %-10.1f\n",
students[i].name, students[i].id, students[i].score);
}
}
int main() {
Student students[MAX_STUDENTS];
int n;
printf("输入学生人数: ");
scanf("%d", &n);
inputStudents(students, n);
printStudents(students, n);
return 0;
}
5.2 链表实现
结构体非常适合实现链表等数据结构:
c复制#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
Node *createNode(int data) {
Node *newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败\n");
exit(1);
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
void appendNode(Node **head, int data) {
Node *newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
} else {
Node *current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = newNode;
}
}
void printList(Node *head) {
Node *current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
int main() {
Node *head = NULL;
appendNode(&head, 10);
appendNode(&head, 20);
appendNode(&head, 30);
printList(head);
// 实际应用中记得释放链表内存
return 0;
}
5.3 与文件I/O结合
结构体常用于文件读写操作:
c复制#include <stdio.h>
typedef struct {
char name[20];
int age;
float salary;
} Employee;
void writeEmployees(const char *filename, Employee *emps, int count) {
FILE *fp = fopen(filename, "wb");
if (fp == NULL) {
perror("打开文件失败");
return;
}
fwrite(emps, sizeof(Employee), count, fp);
fclose(fp);
}
void readEmployees(const char *filename, Employee *emps, int count) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) {
perror("打开文件失败");
return;
}
fread(emps, sizeof(Employee), count, fp);
fclose(fp);
}
int main() {
Employee emps[3] = {
{"张三", 30, 8000.0},
{"李四", 35, 10000.0},
{"王五", 28, 7500.0}
};
writeEmployees("employees.dat", emps, 3);
Employee readEmps[3];
readEmployees("employees.dat", readEmps, 3);
for (int i = 0; i < 3; i++) {
printf("%s, %d, %.2f\n", readEmps[i].name, readEmps[i].age, readEmps[i].salary);
}
return 0;
}
6. 结构体使用中的常见问题
6.1 内存对齐问题
结构体的内存对齐可能导致以下问题:
- 结构体大小大于成员大小之和
- 不同平台上的结构体大小可能不同
- 直接读写结构体到文件可能导致兼容性问题
解决方案:
- 使用
#pragma pack指令控制对齐方式 - 手动序列化结构体成员而非直接读写整个结构体
- 在跨平台项目中明确定义对齐规则
6.2 深浅拷贝问题
c复制typedef struct {
char *name; // 指针成员
int age;
} Person;
Person p1;
p1.name = malloc(20);
strcpy(p1.name, "张三");
Person p2 = p1; // 浅拷贝,p2.name与p1.name指向同一内存
// 正确的深拷贝方式:
Person p3;
p3.age = p1.age;
p3.name = malloc(strlen(p1.name) + 1);
strcpy(p3.name, p1.name);
6.3 自引用结构体
定义包含自身指针的结构体(如链表节点)时,需要使用结构体标签:
c复制// 正确写法
struct Node {
int data;
struct Node *next; // 使用struct Node
};
// 错误写法
typedef struct {
int data;
Node *next; // 此时Node还未定义
} Node;
6.4 柔性数组成员
C99支持柔性数组成员(flexible array member),用于创建可变长度的结构体:
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. 结构体在嵌入式开发中的应用
在STM32等嵌入式开发中,结构体常用于寄存器映射:
c复制typedef struct {
volatile uint32_t CR; // 控制寄存器
volatile uint32_t SR; // 状态寄存器
volatile uint32_t DR; // 数据寄存器
} USART_TypeDef;
#define USART1 ((USART_TypeDef *)0x40011000)
void USART_Init() {
USART1->CR |= 0x2000; // 使能USART
// 其他配置...
}
这种用法使得寄存器访问更加清晰和安全,编译器会检查类型,避免直接操作内存地址可能带来的错误。
在嵌入式开发中还常用结构体来组织配置参数:
c复制typedef struct {
uint32_t baudRate;
uint8_t dataBits;
uint8_t stopBits;
uint8_t parity;
} UART_Config;
const UART_Config uart1Config = {
.baudRate = 115200,
.dataBits = 8,
.stopBits = 1,
.parity = 0
};
8. 结构体与其他语言的对比
8.1 C++中的结构体
C++中的struct与class几乎相同,主要区别是默认访问权限:
- struct默认public
- class默认private
C++还扩展了结构体的功能:
- 可以包含成员函数
- 支持继承
- 可以定义访问控制
8.2 与Java类的比较
Java没有结构体概念,但类可以实现类似功能。主要区别:
- Java类总是引用类型,而C结构体是值类型
- Java类包含方法,C结构体只有数据成员
- Java有垃圾回收,C需要手动管理结构体内存
8.3 与Python字典的比较
Python字典可以存储键值对,类似于结构体,但:
- 字典是动态类型,结构体成员类型固定
- 字典查找速度比结构体成员访问慢
- 结构体更节省内存,特别是包含多个实例时
9. 结构体的最佳实践
-
命名规范:
- 使用有意义的名称,避免简写
- 可以采用
PascalCase或snake_case风格 - 为结构体类型定义typedef别名
-
组织原则:
- 将相关的数据组织在一起
- 避免过大的结构体(考虑拆分)
- 将频繁一起访问的成员放在靠近的位置
-
内存考虑:
- 按对齐要求排列成员(从大到小)
- 考虑使用位域节省空间
- 注意跨平台时的字节序问题
-
API设计:
- 提供初始化函数
- 考虑封装操作结构体的函数
- 对于复杂结构体,提供深拷贝函数
-
文档注释:
- 为每个结构体和成员添加注释
- 说明各成员的取值范围和单位
- 示例:
c复制/* 表示二维坐标点 */ typedef struct { float x; /* 横坐标,单位:米 */ float y; /* 纵坐标,单位:米 */ } Point;
10. 现代C语言中的结构体新特性
C11标准引入了一些与结构体相关的新特性:
10.1 匿名结构体和联合
c复制struct Person {
char name[20];
struct { // 匿名结构体
int year;
int month;
int day;
} birth;
union { // 匿名联合
int student_id;
int employee_id;
} id;
};
// 使用
struct Person p;
p.birth.year = 1990; // 直接访问
p.id.student_id = 1001;
10.2 指定初始化器的增强
C99引入的指定初始化器在C11中得到增强:
c复制struct Point {
int x;
int y;
int z;
};
// 可以任意顺序初始化
struct Point p = {
.z = 10,
.x = 5,
.y = 20
};
10.3 结构体的静态断言
使用_Static_assert检查结构体大小:
c复制struct Packet {
uint16_t header;
uint32_t data;
uint16_t footer;
};
_Static_assert(sizeof(struct Packet) == 8, "Packet结构体大小不正确");
这个特性在协议开发和嵌入式系统中特别有用,可以确保结构体布局符合预期。
