C: 结构体进阶
结构体进阶就像装修房子:不只是搭个框架,还要讲究空间利用(对齐)、节省材料(位域)、灵活扩展(柔性数组)。
1. 结构体与函数的高级用法
(1) 返回结构体的函数
C 语言允许函数直接返回结构体,编译器会处理拷贝:
C
struct Point make_point(int x, int y) {
struct Point p = {x, y};
return p;
}
int main(void) {
struct Point pt = make_point(3, 4);
printf("(%d, %d)\n", pt.x, pt.y);
return 0;
}
💡 小结构体返回值效率可接受;大结构体建议传指针填充,避免拷贝开销。
(2) 用指针填充结构体
C
void fill_student(struct Student *s, const char *name, int age, float score) {
strncpy(s->name, name, 19);
s->name[19] = '\0';
s->age = age;
s->score = score;
}
int main(void) {
struct Student stu;
fill_student(&stu, "张三", 20, 88.5);
return 0;
}
(3) const 保护结构体参数
不想让函数修改结构体,用 const 修饰指针参数:
C
void print_student(const struct Student *s) {
printf("%s %d %.1f\n", s->name, s->age, s->score);
}
⚠️
const struct Student *s 表示通过 s 不能修改指向的结构体,但 s 本身可以指向别的。
2. typedef 简化类型名
typedef 给类型起别名,减少重复书写 struct 关键字。
(1) 基本用法
C
typedef struct {
char name[20];
int age;
float score;
} Student;
Student s1 = {"张三", 20, 89.5};
Student *ps = &s1;
💡 不用再写
struct Student,直接用 Student 即可。
(2) typedef 与结构体指针
C
typedef struct Node {
int data;
struct Node *next;
} Node, *NodePtr;
Node n1 = {10, NULL};
NodePtr head = &n1;
⚠️ 结构体内部自引用时,仍须用
struct Node *next,因为此时别名还没生效。
(3) typedef 的其他用途
C
typedef unsigned char Byte;
typedef int (*Comparator)(const void *, const void *);
Byte flag = 0xFF;
Comparator cmp = my_compare;
▶ 示例
C
#include <stdio.h>
#include <string.h>
typedef struct {
char title[50];
int pages;
float price;
} Book;
Book create_book(const char *title, int pages, float price) {
Book b;
strncpy(b.title, title, 49);
b.title[49] = '\0';
b.pages = pages;
b.price = price;
return b;
}
void discount(Book *b, float rate) {
b->price *= rate;
}
void print_book(const Book *b) {
printf("《%s》%d页 ¥%.2f\n", b->title, b->pages, b->price);
}
int main(void) {
Book b1 = create_book("C程序设计", 320, 59.0);
print_book(&b1);
discount(&b1, 0.8);
print_book(&b1);
return 0;
}
TEXT
📖 仅展示
《C程序设计》320页 ¥59.00
《C程序设计》320页 ¥47.20
3. 结构体内存对齐
结构体成员在内存中并不是紧密排列的,编译器会按照对齐规则插入填充字节。
(1) 对齐规则
- 每个成员的偏移量必须是该成员大小的整数倍
- 结构体总大小必须是最大成员大小的整数倍
C
struct Align1 {
char a;
int b;
char c;
};
struct Align2 {
char a;
char c;
int b;
};
C
printf("%zu\n", sizeof(struct Align1));
printf("%zu\n", sizeof(struct Align2));
TEXT
📖 仅展示
12
8
Align1 的布局:a(1 字节)+ 3 字节填充 + b(4 字节)+ c(1 字节)+ 3 字节填充 = 12。
Align2 的布局:a(1 字节)+ c(1 字节)+ 2 字节填充 + b(4 字节)= 8。
💡 将占空间小的成员集中放在一起,可以减少填充字节——节省内存的实用技巧。
(2) #pragma pack
可以指定对齐边界来压缩结构体:
C
#pragma pack(push, 1)
struct Packed {
char a;
int b;
char c;
};
#pragma pack(pop)
printf("%zu\n", sizeof(struct Packed));
TEXT
📖 仅展示
6
⚠️ 压缩对齐会降低访问效率,某些平台甚至会出错。只在协议解析、文件格式等必须按字节对齐的场景使用。
4. 位域
位域(bit-field)让成员按位分配空间,节省内存。
C
struct Flags {
unsigned int ready : 1;
unsigned int error : 1;
unsigned int mode : 3;
unsigned int : 0;
unsigned int count : 12;
};
ready占 1 位(0 或 1)error占 1 位mode占 3 位(0-7): 0强制对齐到下一个存储单元count占 12 位
C
struct Flags f = {1, 0, 5, 1024};
printf("ready=%u error=%u mode=%u count=%u\n", f.ready, f.error, f.mode, f.count);
printf("结构体大小: %zu\n", sizeof(f));
TEXT
📖 仅展示
ready=1 error=0 mode=5 count=1024
结构体大小: 8
⚠️ 位域的布局与编译器相关,不可移植。不要对位域成员取地址(
&f.ready 是非法的)。
5. 柔性数组成员
C99 允许结构体最后一个成员是长度为 0 的数组,称为柔性数组成员(Flexible Array Member)。
C
typedef struct {
int len;
int data[];
} IntVec;
使用时根据需要分配额外空间:
C
int n = 5;
IntVec *v = (IntVec *)malloc(sizeof(IntVec) + sizeof(int) * n);
v->len = n;
for (int i = 0; i < n; i++) {
v->data[i] = i * 100;
}
for (int i = 0; i < v->len; i++) {
printf("%d ", v->data[i]);
}
free(v);
TEXT
📖 仅展示
0 100 200 300 400
💡 柔性数组成员常用来实现变长结构体。相比用指针指向另一块内存,数据是连续的,一次 free 就够了。
⚠️ 柔性数组必须放在结构体最后,且结构体至少还有一个其他成员。不能用
sizeof 获取柔性数组的大小。
❓ 常见问题
Q typedef 和 #define 定义别名有什么区别?
A typedef 由编译器处理,遵循作用域规则,可以正确处理指针类型;#define 是预处理器简单替换,容易出错。优先用 typedef。
Q 为什么要内存对齐?
A CPU 按对齐边界读取内存效率更高,某些架构不对齐直接报错。编译器自动填充是为了兼容性和性能。
Q 位域能取地址吗?
A 不能。位域成员可能不足一个字节,没有独立地址。不能用
& 运算符。Q 柔性数组和指针成员哪个好?
A 柔性数组数据连续存储,一次分配一次释放,缓存友好;指针成员可以指向任意位置,更灵活但需两次分配和释放。选柔性数组更简洁。
📖 小节
- 函数可返回结构体,大结构体建议传指针填充以提高效率
typedef给类型起别名,简化结构体类型书写- 结构体存在内存对齐,成员排列顺序影响总大小
#pragma pack可改变对齐规则,用于协议解析等场景- 位域按位分配空间,节省内存但不可取地址、不可移植
- 柔性数组成员实现变长结构体,数据连续,分配释放简洁
📝 作业
- 定义包含 char、short、int、double 的结构体,分别用两种成员排列顺序,用 sizeof 验证大小差异
- 用 typedef 定义链表节点类型,编写创建和遍历单链表的函数
- 使用柔性数组成员,实现一个动态字符串结构体(含 len 和 data[]),支持追加字符操作