结构体与联合体

在C语言中,基本数据类型(int、float、char等)只能表示单一的数据。然而,现实世界中的实体往往包含多个属性,比如一个学生有学号、姓名、成绩等。 结构体(struct) 允许我们将不同类型的数据组合成一个新的数据类型,方便组织和操作复杂数据。 而联合体(union) 则是一种特殊的结构,它允许同一块内存存储不同类型的数据,但同一时刻只能存储其中一种。 本章将详细介绍结构体和联合体的定义、使用以及它们的区别。

📦 结构体(struct)

结构体是一种用户自定义的数据类型,它可以将多个不同类型的数据项组合成一个整体。结构体的成员可以是基本类型、数组、指针,甚至其他结构体。

🔹 结构体的定义

struct 结构体名 {
    数据类型 成员1;
    数据类型 成员2;
    // ...
};  // 注意分号

例如,定义一个学生结构体:

struct Student {
    int id;
    char name[50];
    float score;
};

🔹 结构体变量的声明与初始化

#include <stdio.h>

struct Student {
    int id;
    char name[50];
    float score;
};

int main() {
    // 方式1:先声明变量,再逐个赋值
    struct Student stu1;
    stu1.id = 1001;
    // stu1.name = "张三";  // 错误!数组不能直接赋值,需要使用strcpy
    strcpy(stu1.name, "张三");
    stu1.score = 88.5;

    // 方式2:声明时初始化(按成员顺序)
    struct Student stu2 = {1002, "李四", 92.0};

    // 方式3:指定成员初始化(C99)
    struct Student stu3 = {.id = 1003, .score = 78.5, .name = "王五"};

    return 0;
}
注意: 字符数组成员不能直接用字符串赋值,应使用 strcpy 函数(需包含 <string.h>)。

🔹 访问结构体成员

使用点运算符 . 访问结构体变量的成员。

printf("学号: %d\n", stu1.id);
printf("姓名: %s\n", stu1.name);
printf("成绩: %.1f\n", stu1.score);

📚 结构体数组

结构体数组可以存储多个相同类型的结构体对象,方便批量处理。

#include <stdio.h>
#include <string.h>

struct Student {
    int id;
    char name[20];
    float score;
};

int main() {
    struct Student class[3] = {
        {1001, "张三", 85.5},
        {1002, "李四", 92.0},
        {1003, "王五", 78.5}
    };

    for (int i = 0; i < 3; i++) {
        printf("%d\t%s\t%.1f\n", class[i].id, class[i].name, class[i].score);
    }
    return 0;
}

📌 结构体指针

结构体指针指向结构体变量的地址,通过箭头运算符 -> 访问成员。

#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point p1 = {10, 20};
    struct Point *ptr = &p1;

    // 使用指针访问成员
    printf("x = %d, y = %d\n", ptr->x, ptr->y);   // 等价于 (*ptr).x

    ptr->x = 30;
    printf("修改后 x = %d\n", p1.x);

    return 0;
}

🔗 嵌套结构体

结构体的成员可以是另一个结构体,实现更复杂的数据组织。

#include <stdio.h>

struct Date {
    int year;
    int month;
    int day;
};

struct Student {
    int id;
    char name[50];
    struct Date birthday;   // 嵌套结构体
    float score;
};

int main() {
    struct Student stu = {1001, "张三", {2000, 5, 20}, 88.5};

    printf("生日: %d年%d月%d日\n", stu.birthday.year, stu.birthday.month, stu.birthday.day);

    return 0;
}

📞 结构体作为函数参数

结构体可以作为函数参数传递,但通常有两种方式:传值(复制整个结构体)和传指针(传递地址)。传指针效率更高,且可以修改原结构体。

#include <stdio.h>
#include <string.h>

struct Student {
    int id;
    char name[50];
    float score;
};

// 传值(复制结构体)
void printStudent(struct Student s) {
    printf("学号: %d, 姓名: %s, 成绩: %.1f\n", s.id, s.name, s.score);
}

// 传指针(可修改原结构体)
void updateScore(struct Student *s, float newScore) {
    s->score = newScore;   // 修改原结构体的成绩
}

int main() {
    struct Student stu = {1001, "张三", 85.0};

    printStudent(stu);
    updateScore(&stu, 95.0);
    printStudent(stu);

    return 0;
}
建议: 当结构体较大时,应尽量使用指针传递,避免复制开销。

🔀 联合体(union)

联合体是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型。所有成员共享同一块内存,同一时刻只能使用其中一个成员。

🔹 联合体的定义与使用

union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    union Data data;
    data.i = 10;
    printf("data.i = %d\n", data.i);

    data.f = 3.14;
    printf("data.f = %f\n", data.f);
    printf("data.i = %d(被覆盖)\n", data.i);   // 此时 i 的值已改变

    return 0;
}
注意: 联合体的大小等于其最大成员的大小。对某个成员赋值会覆盖其他成员的值。

🔹 联合体的应用场景

联合体常用于节省内存(如嵌入式系统)或实现变体类型(如数据包解析)。

#include <stdio.h>

// 用联合体存储不同类型的数据
union Value {
    int intVal;
    float floatVal;
    char charVal;
};

struct Variant {
    int type;   // 0:int, 1:float, 2:char
    union Value val;
};

void printVariant(struct Variant v) {
    switch (v.type) {
        case 0: printf("整数: %d\n", v.val.intVal); break;
        case 1: printf("浮点数: %f\n", v.val.floatVal); break;
        case 2: printf("字符: %c\n", v.val.charVal); break;
    }
}

int main() {
    struct Variant v1 = {0, {.intVal = 100}};
    struct Variant v2 = {1, {.floatVal = 3.14}};
    struct Variant v3 = {2, {.charVal = 'A'}};

    printVariant(v1);
    printVariant(v2);
    printVariant(v3);

    return 0;
}

📊 结构体与联合体的区别

3232 3232 3232 3232
特性结构体(struct)联合体(union)
内存分配每个成员独立分配内存,总大小≥各成员大小之和所有成员共享同一块内存,大小等于最大成员大小成员访问所有成员可同时使用同一时刻只能使用一个成员,赋值会覆盖其他成员用途组织不同类型的数据,形成记录节省内存,或同一块内存不同解释

📝 typedef - 类型别名

typedef 用于为已有类型创建别名,提高代码可读性,常用于简化结构体声明。

// 不使用 typedef
struct Student {
    int id;
    char name[50];
};
struct Student stu1;   // 每次都要写 struct

// 使用 typedef
typedef struct {
    int id;
    char name[50];
} Student;   // 现在 Student 就是一个类型名

Student stu2;   // 直接使用

// 也可以分开写
typedef struct Point {
    int x;
    int y;
} Point;

Point p1;       // 可以直接用
struct Point p2; // 原来的也可以

📋 综合示例:学生成绩管理系统

#include <stdio.h>
#include <string.h>

#define MAX_STUDENTS 100

typedef struct {
    int id;
    char name[50];
    float score;
} Student;

void inputStudent(Student *s) {
    printf("请输入学号: ");
    scanf("%d", &s->id);
    printf("请输入姓名: ");
    scanf("%s", s->name);
    printf("请输入成绩: ");
    scanf("%f", &s->score);
}

void printStudent(Student s) {
    printf("%d\t%s\t%.1f\n", s.id, s.name, s.score);
}

float calculateAverage(Student stu[], int n) {
    float sum = 0;
    for (int i = 0; i < n; i++) {
        sum += stu[i].score;
    }
    return sum / n;
}

void findTop(Student stu[], int n) {
    int maxIndex = 0;
    for (int i = 1; i < n; i++) {
        if (stu[i].score > stu[maxIndex].score) {
            maxIndex = i;
        }
    }
    printf("最高分学生: ");
    printStudent(stu[maxIndex]);
}

int main() {
    Student class[MAX_STUDENTS];
    int count;

    printf("请输入学生人数: ");
    scanf("%d", &count);

    for (int i = 0; i < count; i++) {
        printf("\n输入第%d个学生信息:\n", i + 1);
        inputStudent(&class[i]);
    }

    printf("\n学生信息:\n");
    printf("学号\t姓名\t成绩\n");
    for (int i = 0; i < count; i++) {
        printStudent(class[i]);
    }

    printf("\n平均分: %.2f\n", calculateAverage(class, count));
    findTop(class, count);

    return 0;
}

⚠️ 常见错误与注意事项

  • 结构体类型定义后忘记分号struct Student { ... } 后面必须有分号。
  • 混淆点运算符和箭头运算符:结构体变量用 .,结构体指针用 ->
  • 结构体直接赋值:C语言允许结构体变量之间直接赋值(如 s1 = s2;),但浅拷贝,如果包含指针成员需注意。
  • 联合体成员的覆盖:使用联合体时,必须清楚当前存储的是哪个成员,否则会得到错误数据。
  • 结构体作为函数参数传递开销大:大结构体传值会复制整个结构,应使用指针传递。
  • 返回局部结构体的地址:函数返回局部结构体变量的指针是危险的,该内存会在函数返回后被释放。
  • 结构体对齐与填充:编译器可能会在结构体成员之间插入填充字节以提高访问效率,因此 sizeof(结构体) 可能大于各成员大小之和。使用 #pragma pack 可控制对齐(谨慎使用)。

结构体和联合体是C语言中组织复杂数据的重要工具。结构体用于组合不同类型的数据形成记录,联合体用于节省内存或实现多态。掌握它们的用法,可以为后续学习链表、树等数据结构打下坚实基础。 下一章我们将学习文件操作,实现数据的持久化存储。