C 结构体
C 没有"类"和"面向对象"(那是 C++ 干的事),但有结构体(struct):把多个相关字段打包成一个新类型。这是 C 组织数据的基本手段,几乎所有 C 程序都靠 struct 搭建骨架——内核里的 task_struct、数据库里的 Row、网络包的 header,全是 struct。
一、定义与使用
结构体用 struct 关键字定义,列出所有字段的类型和名字:
#include <stdio.h>
#include <string.h>
// 定义结构体类型
struct Student {
char name[20];
int age;
float gpa;
};
int main(void) {
// 声明并初始化(顺序要与定义一致)
struct Student s1 = {"小明", 20, 3.85};
struct Student s2 = {"小红"}; // 部分初始化,其余补 0
// 点操作符访问成员
printf("%s,%d岁,GPA %.2f\n", s1.name, s1.age, s1.gpa);
printf("%s,%d岁,GPA %.2f\n", s2.name, s2.age, s2.gpa);
// 修改成员
s2.age = 22;
strcpy(s2.name, "王小红"); // 字符串成员用 strcpy 改
printf("%s,%d岁\n", s2.name, s2.age);
// 指定初始化(C99,推荐!顺序无关)
struct Student s3 = {.age = 18, .name = "小李", .gpa = 3.9};
printf("%s,%d岁\n", s3.name, s3.age);
return 0;
}几个要点:
- 定义在函数外:通常写在文件顶部或头文件,让所有函数都能用。
- 声明变量必须带
struct:struct Student s1;(不像 C++ 可以省略 struct)。 - 字符串成员用 strcpy 修改:不能直接
s.name = "新名字"(数组不能赋值)。 - C99 指定初始化:用点号给指定字段赋值(如
.name、.age),顺序无关,清晰安全。
二、typedef 起别名
每次写 struct Student 很啰嗦,用 typedef 给类型起个短别名:
#include <stdio.h>
// 没用 typedef:每次都要写 struct Student,啰嗦
struct Point1 {
int x, y;
};
struct Point1 p1; // 必须带 struct 关键字
// 用 typedef 起别名,之后不用写 struct
typedef struct {
int x, y;
} Point;
Point p2; // 简洁!
// 经典写法:struct + typedef 一起
typedef struct Node {
int data;
struct Node *next; // 自引用(链表)
} Node;
// 注意:自引用时必须用 struct Node*,不能用 Node*
// 因为此时 Node 还没定义完
int main(void) {
Point a = {3, 4};
printf("(%d, %d)\n", a.x, a.y);
Node n1 = {1, NULL};
Node n2 = {2, &n1}; // n2 指向 n1,组成链表
printf("%d -> %d\n", n2.data, n2.next->data); // 2 -> 1
return 0;
}typedef 的本质是给类型起别名,不是创建新类型。常见用法:
typedef struct {...} Name;:省去写 struct。typedef unsigned int uint;:简化长类型名。typedef int (*FuncPtr)(int, int);:给复杂的函数指针类型起名。
注意自引用结构体(链表、树的节点)里,自引用指针必须用 struct Node *,不能用 Node *——因为此时 typedef 的别名 Node 还没定义完,编译器不认识。
三、结构体指针与 -> 操作符
结构体经常通过指针传递(避免拷贝大对象)。指针访问成员用箭头操作符 ->:
#include <stdio.h>
struct Book {
char title[50];
double price;
};
int main(void) {
struct Book b = {"C 程序设计", 59.9};
// 普通变量用 . 访问成员
printf("%s: %.2f 元\n", b.title, b.price);
// 指针变量用 -> 访问成员(箭头操作符)
struct Book *p = &b;
printf("%s: %.2f 元\n", p->title, p->price);
// p->title 等价于 (*p).title
printf("%s\n", (*p).title); // 一样
// 通过指针修改
p->price = 49.9;
printf("%.2f\n", b.price); // 49.9(同一个对象)
// 一句话记忆:. 用于对象,-> 用于指针
return 0;
}记忆规则:
- 对象(变量)用点
.:b.title - 指针用箭头
->:p->title p->title完全等价于(*p).title——箭头只是简写
这是 C 里最高频的语法之一,Linux 内核代码里到处都是 p->field。务必熟练。
四、嵌套结构体
结构体的成员可以是另一个结构体,用来表达"组合"关系:
#include <stdio.h>
// 结构体可以嵌套:成员是另一个结构体
struct Date {
int year, month, day;
};
struct Employee {
char name[20];
struct Date birthday; // 嵌套结构体
struct Date hire_date;
};
int main(void) {
struct Employee e = {
"张三",
{1990, 5, 15}, // birthday
{2020, 8, 1} // hire_date
};
// 嵌套访问:连续的点
printf("%s 生于 %d-%d-%d\n",
e.name,
e.birthday.year, e.birthday.month, e.birthday.day);
printf("入职: %d-%d-%d\n",
e.hire_date.year, e.hire_date.month, e.hire_date.day);
// C99 指定初始化(嵌套)
struct Employee e2 = {
.name = "李四",
.birthday = {.year = 1995, .month = 8, .day = 20}
};
printf("%s 生于 %d\n", e2.name, e2.birthday.year);
return 0;
}嵌套访问就是连续的点:e.birthday.year。初始化时用嵌套花括号,或者 C99 的嵌套指定初始化(.birthday.year = 1990)。
五、结构体数组
管理多个同类对象时,用结构体数组——比如一个班级的学生、一个公司的员工:
#include <stdio.h>
struct Student {
char name[20];
int score;
};
int main(void) {
// 结构体数组:管理多个对象
struct Student class[] = {
{"小明", 85},
{"小红", 92},
{"小李", 78},
{"小张", 88}
};
int n = sizeof(class) / sizeof(class[0]);
// 遍历
for (int i = 0; i < n; i++) {
printf("%s: %d\n", class[i].name, class[i].score);
}
// 求平均分
int total = 0;
for (int i = 0; i < n; i++) {
total += class[i].score;
}
printf("平均: %.1f\n", (double)total / n);
// 按分数排序(冒泡)
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (class[j].score < class[j+1].score) {
struct Student tmp = class[j];
class[j] = class[j+1];
class[j+1] = tmp;
}
}
}
printf("---排序后---\n");
for (int i = 0; i < n; i++) {
printf("%s: %d\n", class[i].name, class[i].score);
}
return 0;
}结构体数组是 C 里"数据库"的基础形态。Redis 的早期版本就用结构体数组管理所有 key-value。访问元素 class[i].score,先取数组元素再取成员。
六、结构体与函数
结构体作为函数参数,有三种传递方式:
#include <stdio.h>
typedef struct {
int x, y;
} Point;
// 1. 按值传递(拷贝整个结构体,小结构体OK)
void print_point(Point p) {
printf("(%d, %d)\n", p.x, p.y);
}
// 2. 传指针(避免拷贝,大结构体推荐;还能修改原对象)
void move_point(Point *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
// 3. 返回结构体
Point make_point(int x, int y) {
Point p = {x, y};
return p;
}
// 4. const 指针:不拷贝又防修改
int distance_squared(const Point *a, const Point *b) {
int dx = a->x - b->x;
int dy = a->y - b->y;
return dx * dx + dy * dy;
}
int main(void) {
Point p = make_point(3, 4);
print_point(p); // (3, 4)
move_point(&p, 10, 20);
print_point(p); // (13, 24)
Point origin = {0, 0};
printf("距离平方: %d\n", distance_squared(&p, &origin));
return 0;
}- 按值传递:拷贝整个结构体。小结构体(几十字节)可以,大的(几 KB)就低效。
- 传指针:不拷贝,高效;还能修改原对象。大结构体一律用指针。
- const 指针:不拷贝又防修改——只读参数的标准写法。
返回结构体也是允许的(C99 起保证返回值优化,不会真的拷贝)。但永远不要返回栈上局部变量的地址——函数返回后栈帧销毁,指针变悬垂。
七、内存对齐(进阶)
结构体在内存里不是紧凑排列的——编译器为了 CPU 访问效率,会在成员之间插入"填充字节",这叫内存对齐。看个例子:
struct { char c; int i; }:sizeof 通常是 8,不是 5!char 后面填了 3 字节填充。struct { int i; char c; }:sizeof 通常是 8(还是对齐),但实际用 5 字节。
规则:每个成员的偏移量必须是其大小的整数倍,整个结构体大小是最大成员的整数倍。优化技巧:把成员按大小从大到小排列,能减少填充,省内存。
需要精确控制布局时(网络协议、二进制文件),用 #pragma pack(1) 或 __attribute__((packed)) 关闭对齐——但会损失访问性能。
八、联合体 union 与位域(扩展)
C 还有两个相关的构造类型:
- union 联合体:所有成员共享同一块内存,同一时刻只存一个成员。用于"同一块数据的不同解读"(如 IP 头部的不同视图),或节省内存。
- 位域 bit-field:
struct { int a : 3; int b : 5; };——精确到二进制位,用于硬件寄存器映射。
日常编程用得少,知道有这些东西即可,需要时查手册。
小结
struct 把多个字段打包成自定义类型。声明变量带 struct,或用 typedef 起别名简化。对象用点 .、指针用箭头 ->。结构体传函数优先用 const 指针(高效且安全)。下一篇看 C 的动态内存——运行时分配内存。
← 上一篇 C 字符串
下一篇 C 动态内存 →