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;
}

几个要点:

二、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 的本质是给类型起别名,不是创建新类型。常见用法:

注意自引用结构体(链表、树的节点)里,自引用指针必须用 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;
}

记忆规则:

这是 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;
}

返回结构体也是允许的(C99 起保证返回值优化,不会真的拷贝)。但永远不要返回栈上局部变量的地址——函数返回后栈帧销毁,指针变悬垂。

七、内存对齐(进阶)

结构体在内存里不是紧凑排列的——编译器为了 CPU 访问效率,会在成员之间插入"填充字节",这叫内存对齐。看个例子:

规则:每个成员的偏移量必须是其大小的整数倍,整个结构体大小是最大成员的整数倍。优化技巧:把成员按大小从大到小排列,能减少填充,省内存。

需要精确控制布局时(网络协议、二进制文件),用 #pragma pack(1)__attribute__((packed)) 关闭对齐——但会损失访问性能。

八、联合体 union 与位域(扩展)

C 还有两个相关的构造类型:

日常编程用得少,知道有这些东西即可,需要时查手册。

小结

struct 把多个字段打包成自定义类型。声明变量带 struct,或用 typedef 起别名简化。对象用点 .、指针用箭头 ->。结构体传函数优先用 const 指针(高效且安全)。下一篇看 C 的动态内存——运行时分配内存。

← 上一篇 C 字符串

下一篇 C 动态内存

✈️💬