C 语言结构体:把相关数据组织成一个整体
在实际程序中,一个对象往往不只有一个属性。例如学生有姓名、年龄、性别和学号,坐标有横坐标和纵坐标,链表节点还需要保存数据和下一个节点的位置。如果把这些信息拆散成许多独立变量,代码很快会变得难以维护。
C 语言的结构体可以把多个相关的数据成员组合成一个新的类型。结构体的每个成员可以使用不同的数据类型,也可以是数组、指针,甚至另一个结构体。它既保留了 C 语言贴近内存的特点,又让数据表达更清晰。
一、结构体的基本概念
结构体是一组值的集合,这些值称为成员变量。与数组要求所有元素类型相同不同,结构体中的成员可以完全不同:
c
struct Student {
char name[20];
int age;
char sex[8];
char id[20];
};
上面的声明描述了一个名为 struct Student 的结构体类型。它定义了"学生应该包含哪些信息",但仅有类型声明并不会自动创建一个具体的学生对象。
结构体声明最后的分号不能省略。结构体标签 Student 只是类型名称的一部分,使用时需要写成 struct Student:
c
struct Student student;
如果希望像使用 int 一样省略 struct,可以配合 typedef:
c
typedef struct Student {
char name[20];
int age;
char id[20];
} Student;
Student student;
这里的 Student 是结构体类型的别名,后续声明变量时会更加简洁。
二、声明类型的同时定义变量
结构体类型声明和变量定义可以分开,也可以写在一起:
c
struct Point {
int x;
int y;
} point1;
struct Point point2;
point1 在声明结构体类型的同时被定义,point2 则是在类型已经存在后单独定义。两种方式得到的变量类型相同。
结构体变量可以在定义时初始化。初始化值按照成员声明的顺序排列:
c
struct Student student = {"Zhang San", 20, "M", "S001"};
也可以使用指定成员初始化,让代码更直观,并避免依赖成员顺序:
c
struct Student student = {
.name = "Zhang San",
.age = 20,
.sex = "M",
.id = "S001"
};
未提供初始值的成员会被初始化为零值。对字符数组等成员进行字符串初始化时,要确保数组容量能够容纳字符串结尾的 \0。
三、结构体成员可以是什么
结构体成员不仅可以是基本类型,还可以是多种复合类型:
c
struct Example {
int count; // 标量成员
char title[32]; // 数组成员
int *value_ptr; // 指针成员
struct Point position; // 另一个结构体成员
};
这种组合能力使结构体能够描述更真实的对象。例如链表节点可以同时保存一个整数、一个坐标和下一个节点的地址:
c
struct Node {
int data;
struct Point position;
struct Node *next;
};
其中 next 是指向同类型结构体的指针。结构体不能直接包含一个自身类型的成员,否则对象大小会无限递归;但可以包含指向自身的指针,因为指针大小是确定的。
四、如何访问结构体成员
1. 使用点运算符访问结构体变量
当手里有一个结构体变量时,使用点运算符 . 访问成员:
c
#include <stdio.h>
#include <string.h>
struct Student {
char name[20];
int age;
};
int main(void)
{
struct Student student;
strcpy(student.name, "Zhang San");
student.age = 20;
printf("name = %s, age = %d\n", student.name, student.age);
return 0;
}
student.name 表示 student 对象中的 name 成员,student.age 表示其中的 age 成员。数组成员不能直接用赋值运算符接收字符串,因此示例使用 strcpy 复制内容;在真实项目中还要注意目标数组的容量,避免越界。
2. 使用箭头运算符访问结构体指针指向的成员
如果手里保存的是结构体地址,通常使用箭头运算符 ->:
c
void print_student(const struct Student *student)
{
printf("name = %s, age = %d\n", student->name, student->age);
}
student->name 等价于 (*student).name,只是箭头写法更简洁,也更容易阅读。注意括号不能省略:
c
(*student).name // 正确
*student.name // 含义不同,因为 . 的优先级更高
使用结构体指针前,应确认指针指向一个有效对象,必要时先判断它是否为 NULL。
五、嵌套结构体与初始化
结构体成员可以是另一个结构体,因此可以自然地表达层次关系:
c
struct Point {
int x;
int y;
};
struct Node {
int data;
struct Point position;
struct Node *next;
};
初始化嵌套结构体时,可以按层次写出初始值:
c
struct Node node = {
10,
{4, 5},
NULL
};
也可以使用指定成员初始化,使层次更明显:
c
struct Node node = {
.data = 10,
.position = {.x = 4, .y = 5},
.next = NULL
};
访问嵌套成员时连续使用点运算符即可,例如 node.position.x。如果外层对象是指针,则可以写成 node_ptr->position.x。
六、结构体传参:值传递与地址传递
结构体可以像普通变量一样作为函数参数:
c
struct Record {
int data[1000];
int count;
};
void print_by_value(struct Record record)
{
printf("%d\n", record.count);
}
这种写法会把整个结构体复制一份传给函数。结构体较小时,复制成本通常不明显;但当结构体包含很大的数组或许多成员时,复制会增加参数传递的开销,而且函数内部修改的是副本,不会影响原对象。
更常见的方式是传递结构体地址:
c
void print_by_pointer(const struct Record *record)
{
printf("%d\n", record->count);
}
int main(void)
{
struct Record record = {{1, 2, 3, 4}, 4};
print_by_pointer(&record);
return 0;
}
传递地址只需要复制一个指针,通常更节省开销;函数通过指针访问原对象,也可以在需要时修改它。如果函数只负责读取数据,建议加上 const:
c
void show_student(const struct Student *student);
const 表明函数不会通过该指针修改结构体成员,既能表达意图,也能减少误操作。
如果确实需要让函数修改调用者的结构体,可以不使用 const:
c
void update_age(struct Student *student, int age)
{
student->age = age;
}
调用时传入地址:
c
update_age(&student, 21);
七、内存布局与可读性
结构体成员通常按照声明顺序排列,但编译器可能为了满足对齐要求,在成员之间插入填充字节。因此 sizeof(struct Type) 不一定等于所有成员 sizeof 之和。
c
printf("%zu\n", sizeof(struct Student));
在大多数业务代码中,不需要手动依赖具体布局;如果结构体用于文件格式、网络协议或硬件寄存器映射,则需要额外考虑对齐、字节序和编译器扩展,不能仅凭成员声明推断二进制格式。
良好的结构体设计还应做到:成员名称清晰、字段职责集中、避免把无关数据硬塞到同一个类型中。结构体的价值不只是"把变量放在一起",更是用一个有意义的类型表达一个完整对象。
八、几个实用习惯
- 结构体声明末尾始终保留分号,定义类型后再创建变量。
- 小结构体可以直接值传递,大结构体或只读参数优先传递指针。
- 只读的结构体指针参数尽量使用
const。 - 访问结构体变量使用
., 访问结构体指针使用->。 - 使用字符串成员时检查容量,避免
strcpy等操作造成越界。 - 使用结构体指针前检查指针有效性,释放动态内存后及时置为
NULL。 - 需要表达层次关系时使用嵌套结构体,需要连接同类对象时使用结构体指针。
结构体让 C 语言从"处理一组零散变量"变成"描述一个完整对象"。掌握类型声明、初始化、成员访问和指针传参之后,就可以进一步构建链表、树、哈希表以及各种更复杂的数据结构。