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) 对齐规则

  1. 每个成员的偏移量必须是该成员大小的整数倍
  2. 结构体总大小必须是最大成员大小的整数倍
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;
};
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 柔性数组数据连续存储,一次分配一次释放,缓存友好;指针成员可以指向任意位置,更灵活但需两次分配和释放。选柔性数组更简洁。

📖 小节

📝 作业

  1. 定义包含 char、short、int、double 的结构体,分别用两种成员排列顺序,用 sizeof 验证大小差异
  2. 用 typedef 定义链表节点类型,编写创建和遍历单链表的函数
  3. 使用柔性数组成员,实现一个动态字符串结构体(含 len 和 data[]),支持追加字符操作
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏