C 语言之复合数据类型:结构体、联合体、枚举

在 C 语言中,基础数据类型(intcharfloat等)仅能描述单一属性的数据(如数字、字符),但实际开发中,我们常需要描述包含多个不同属性的复杂对象(如学生、商品、设备信息),或一组有明确含义的常量(如星期、颜色)。

结构体(struct)、联合体(union)、枚举(enum)作为 C 语言的三大复合数据类型,正是为解决这类问题而生。本文将从定义、使用、核心特性到实战场景,全方位拆解这三种类型,帮你掌握复合数据类型的核心用法。

一、结构体

结构体是 C 语言中最常用的复合类型,它允许你将不同类型的变量打包成一个整体,用于描述具有多个属性的对象(如学生、图书、员工)。

1、结构体的定义和使用
核心语法

结构体的使用分为两步:定义结构体类型(相当于创建一个新的 “数据模板”)和实例化结构体变量(根据模板创建具体对象)。

#include <stdio.h>
#include <string.h> // 用于strcpy

// 第一步:定义结构体类型(模板)
// struct是关键字,Student是结构体名(可自定义),{}内是成员列表
struct Student {
    // 成员可以是不同类型的变量
    char name[20];  // 姓名
    int age;        // 年龄
    float score;    // 成绩
}; // 注意:结构体定义末尾必须加分号

int main() {
    // 第二步:实例化结构体变量(创建具体对象)
    // 方式1:定义时直接初始化
    struct Student stu1 = {"张三", 18, 92.5};
    
    // 方式2:先定义变量,再逐个赋值(用.访问成员)
    struct Student stu2;
    // 字符串赋值不能直接用=,需用strcpy
    strcpy(stu2.name, "李四");
    stu2.age = 19;
    stu2.score = 88.0;

    // 访问结构体成员并输出
    printf("学生1:姓名=%s,年龄=%d,成绩=%.1f\n", stu1.name, stu1.age, stu1.score);
    printf("学生2:姓名=%s,年龄=%d,成绩=%.1f\n", stu2.name, stu2.age, stu2.score);

    return 0;
}
深度解读
  1. 类型与变量的区别:结构体定义(struct Student {...})只是声明了一个新的类型,不占用内存;只有实例化变量(如stu1stu2)时,才会在内存中分配空间。

  2. 成员访问符·:必须通过结构体变量名.成员名的方式访问成员,这是结构体变量访问成员的唯一方式。

  3. 简化写法:typedef 重命名:反复写struct Student会繁琐,可通过typedef简化类型名:

// 重命名后,Student等价于struct Student
typedef struct {
    char name[20];
    int age;
    float score;
} Student;

// 实例化时无需写struct
Student stu = {"王五", 20, 90.0};

      4.内存对齐:结构体的总大小并非成员大小的简单相加(如char[20]+int+float理论上是 28 字节),编译器会为了访问效率进行内存对齐(比如int成员会对齐到 4 字节边界),实际大小可能略大。

2、结构体数组:存储多个结构体对象

当需要存储一组同类型的结构体对象(如一个班级的学生)时,结构体数组是最佳选择 —— 数组的每个元素都是一个完整的结构体变量。

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

// 用typedef简化结构体类型
typedef struct {
    char name[20];
    int age;
    float score;
} Student;

int main() {
    // 定义结构体数组并初始化(3个学生)
    Student stu_arr[3] = {
        {"张三", 18, 92.5},
        {"李四", 19, 88.0},
        {"王五", 20, 90.0}
    };

    // 遍历结构体数组
    printf("班级学生列表:\n");
    for (int i = 0; i < 3; i++) {
        printf("第%d个学生:姓名=%s,年龄=%d,成绩=%.1f\n",
               i+1, stu_arr[i].name, stu_arr[i].age, stu_arr[i].score);
    }

    // 修改数组中某个元素的成员
    strcpy(stu_arr[1].name, "李华");
    stu_arr[1].score = 95.5;
    printf("\n修改后第2个学生:姓名=%s,成绩=%.1f\n", stu_arr[1].name, stu_arr[1].score);

    return 0;
}
深度解读
  • 结构体数组的内存是连续的:每个数组元素(结构体)的内存紧密排列,可通过下标快速访问。
  • 访问规则:数组名[下标].成员名,本质是先通过下标找到结构体元素,再通过.访问成员。
  • 适用场景:批量存储同类型的复杂对象(如学生列表、商品列表、员工信息)。
3、结构体指针:指向结构体的 “快捷方式”

结构体指针存储的是结构体变量的首地址,通过指针访问结构体成员,既能节省内存(避免拷贝整个结构体),又能提高程序效率。

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

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

int main() {
    Student stu = {"张三", 18, 92.5};
    // 定义结构体指针,指向结构体变量的地址
    Student *p_stu = &stu;

    // 方式1:通过指针访问成员(推荐):指针名->成员名
    printf("通过指针访问:姓名=%s,年龄=%d,成绩=%.1f\n",
           p_stu->name, p_stu->age, p_stu->score);

    // 方式2:等价写法(解引用指针后用.访问):(*指针名).成员名
    // 注意:*p_stu必须加括号,因为.的优先级高于*
    printf("等价写法:姓名=%s,年龄=%d,成绩=%.1f\n",
           (*p_stu).name, (*p_stu).age, (*p_stu).score);

    // 通过指针修改结构体成员
    p_stu->age = 19;
    p_stu->score = 95.0;
    printf("\n修改后:年龄=%d,成绩=%.1f\n", stu.age, stu.score);

    return 0;
}
深度解读
  1. 核心操作符->:结构体指针访问成员的专用操作符,等价于(*指针).成员,是实际开发中最常用的写法。

  2. 结构体指针的优势

    • 占用内存少:指针仅占 4/8 字节(32/64 位系统),而结构体可能占几十 / 上百字节;

    • 支持修改原数据:指针直接指向原结构体的内存,修改会同步到原变量。

  3. 指向结构体数组的指针:指针 ++ 会自动偏移一个结构体的大小,可用于遍历数组:

Student stu_arr[2] = {{"张三",18,92.5}, {"李四",19,88.0}};
Student *p = stu_arr; // 数组名是首地址,无需&
// 遍历第一个元素
printf("%s\n", p->name); // 张三
p++; // 指针偏移到第二个元素
printf("%s\n", p->name); // 李四
4、嵌套结构体:描述更复杂的对象

当结构体的某个成员本身也是一个复杂对象时,可将该成员定义为另一个结构体类型,这就是嵌套结构体(比如学生包含 “地址” 结构体,订单包含 “商品” 结构体)。

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

// 第一步:定义被嵌套的结构体(地址)
typedef struct {
    char province[20]; // 省份
    char city[20];     // 城市
    char detail[50];   // 详细地址
} Address;

// 第二步:定义主结构体(学生),包含Address类型的成员
typedef struct {
    char name[20];
    int age;
    Address addr; // 嵌套结构体成员
} Student;

int main() {
    // 初始化嵌套结构体
    Student stu;
    strcpy(stu.name, "张三");
    stu.age = 18;
    // 访问嵌套成员:外层成员.内层成员
    strcpy(stu.addr.province, "江苏省");
    strcpy(stu.addr.city, "南京市");
    strcpy(stu.addr.detail, "玄武区XX路XX号");

    // 输出嵌套结构体内容
    printf("学生信息:\n");
    printf("姓名:%s,年龄:%d\n", stu.name, stu.age);
    printf("地址:%s-%s-%s\n", stu.addr.province, stu.addr.city, stu.addr.detail);

    return 0;
}
深度解读
  • 访问规则:层层递进外层结构体.内层结构体.内层成员,每一层都需要用.访问。

  • 内存布局:嵌套结构体的内存是 “扁平化” 的 —— 主结构体的内存包含了嵌套结构体的所有成员,没有额外的指针开销。

  • 初始化技巧:嵌套结构体也可在定义时直接初始化:

Student stu = {
    "张三", 18,
    {"江苏省", "南京市", "玄武区XX路XX号"} // 对应Address成员
};
5、结构体传参:值传递 vs 地址传递

结构体作为函数参数传递时,有两种方式,核心区别在于是否拷贝整个结构体,以及是否能修改原数据。

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

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

// 方式1:值传递(拷贝整个结构体)
void modify_stu1(Student stu) {
    stu.age = 20;
    stu.score = 99.0;
    printf("函数内(值传递):年龄=%d,成绩=%.1f\n", stu.age, stu.score);
}

// 方式2:地址传递(传递指针,仅拷贝地址)
void modify_stu2(Student *p_stu) {
    p_stu->age = 20;
    p_stu->score = 99.0;
    printf("函数内(地址传递):年龄=%d,成绩=%.1f\n", p_stu->age, p_stu->score);
}

// 方式3:地址传递+const(保护数据不被修改)
void print_stu(const Student *p_stu) {
    // p_stu->age = 21; // 报错:const修饰的指针无法修改原数据
    printf("学生信息:姓名=%s,年龄=%d,成绩=%.1f\n", p_stu->name, p_stu->age, p_stu->score);
}

int main() {
    Student stu = {"张三", 18, 92.5};

    // 测试值传递:修改的是拷贝的副本,原数据不变
    modify_stu1(stu);
    printf("主函数(值传递后):年龄=%d,成绩=%.1f\n\n", stu.age, stu.score);

    // 测试地址传递:修改的是原数据
    modify_stu2(&stu);
    printf("主函数(地址传递后):年龄=%d,成绩=%.1f\n\n", stu.age, stu.score);

    // 测试const保护的地址传递
    print_stu(&stu);

    return 0;
}
深度解读
  1. 值传递的缺点

    • 内存开销大:结构体越大,拷贝的成本越高(比如一个包含 10 个成员的结构体,值传递会拷贝所有成员);

    • 无法修改原数据:函数内修改的是 “副本”,主函数的原变量不受影响。

  2. 地址传递的优势

    • 高效:仅传递 4/8 字节的指针,无拷贝开销;

    • 可修改原数据:指针直接指向原结构体的内存。

  3. const 修饰的最佳实践:如果函数仅需读取结构体数据(无需修改),务必用const Student *修饰指针 —— 既享受地址传递的高效,又能防止误修改原数据,提升代码安全性。

二、联合体:共享内存的复合类型

联合体(也叫共用体,union)与结构体语法相似,但核心特性完全不同:联合体的所有成员共享同一块内存空间,同一时间只能有效使用一个成员。

1、联合体的定义和使用
核心语法

联合体的定义和使用与结构体几乎一致,但内存分配规则完全不同。

#include <stdio.h>

// 定义联合体类型:所有成员共享内存
union Data {
    int i;     // 4字节
    float f;   // 4字节
    char c;    // 1字节
}; // 总大小为最大成员的大小(4字节)

int main() {
    // 实例化联合体变量
    union Data data;

    // 赋值第一个成员
    data.i = 100;
    printf("赋值int后:i=%d,f=%.1f,c=%c\n", data.i, data.f, data.c);
    // 输出:i=100,f=0.0(乱码),c=d(ASCII 100对应'd')

    // 赋值第二个成员(覆盖内存)
    data.f = 3.14f;
    printf("赋值float后:i=%d,f=%.1f,c=%c\n", data.i, data.f, data.c);
    // 输出:i=1078523331(乱码),f=3.1,c=?(乱码)

    // 赋值第三个成员(覆盖内存)
    data.c = 'A';
    printf("赋值char后:i=%d,f=%.1f,c=%c\n", data.i, data.f, data.c);
    // 输出:i=65(ASCII 'A'),f=0.0(乱码),c=A

    // 查看联合体大小
    printf("\n联合体Data的大小:%zu字节\n", sizeof(union Data)); // 输出4

    return 0;
}
深度解读
  • 内存共享规则:联合体的所有成员从同一块内存地址开始存储,赋值一个成员会覆盖其他成员的内容 —— 因此同一时间只有最后赋值的成员是有效的。

  • 大小规则:联合体的总大小等于最大成员的大小(如上例中intfloat都是 4 字节,因此联合体大小为 4),目的是容纳最大的成员。

与结构体的核心区别
类型内存分配方式总大小成员有效性
结构体成员各自占独立内存成员大小之和(含对齐)所有成员同时有效
联合体所有成员共享同一块内存最大成员的大小同一时间仅一个有效
2、联合体的核心特点与应用场景
核心特点
  1. 内存复用:极大节省内存,适合 “同一时间仅需使用一个成员” 的场景;

  2. 类型共用:同一块内存可存储不同类型的数据,灵活适配不同场景;

  3. 初始化规则:只能初始化第一个成员(C 语言标准),后续成员需手动赋值。

经典应用场景:判断系统大小端

大小端是指多字节数据在内存中的存储顺序:

  • 大端:高字节存低地址,低字节存高地址(如0x1234 → 地址 0 存0x12,地址 1 存0x34);

  • 小端:低字节存低地址,高字节存高地址(如0x1234 → 地址 0 存0x34,地址 1 存0x12)。

利用联合体共享内存的特性,可快速判断系统的大小端:

#include <stdio.h>

union EndianTest {
    int i;      // 4字节
    char c;     // 1字节(共享i的低地址字节)
};

int main() {
    union EndianTest test;
    test.i = 1; // 二进制:00000000 00000000 00000000 00000001

    // 如果c=1,说明低地址存的是0x01 → 小端;否则大端
    if (test.c == 1) {
        printf("当前系统是小端模式\n");
    } else {
        printf("当前系统是大端模式\n");
    }

    return 0;
}
深度解读
  • 小端模式是目前主流(x86 架构、ARM 默认),大端模式多用于网络传输、嵌入式设备;

  • 联合体的这一用法是 C 语言面试高频考点,核心原理就是 “共享内存,访问低地址字节”。

三、枚举:给常量命名,提升代码可读性

枚举(enum)是用于定义一组有名字的整数常量的类型,目的是替代魔法数字(如012),让代码更易读、易维护。

1、枚举的定义和使用
核心语法

枚举的核心是 “给整数常量起名字”,默认从 0 开始递增,也可手动指定值。

#include <stdio.h>

// 定义枚举类型:Weekday是枚举名,{}内是枚举常量
// 默认:Mon=0, Tue=1, Wed=2, ..., Sun=6
enum Weekday {
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat,
    Sun
};

// 手动指定枚举常量的值:Red=10, Green=20, Blue=30
enum Color {
    Red = 10,
    Green = 20,
    Blue = 30
};

int main() {
    // 实例化枚举变量(本质是int类型)
    enum Weekday today = Wed;
    enum Color favorite = Green;

    // 输出枚举常量的值
    printf("Wed的值:%d\n", Wed);       // 输出2
    printf("today(Wed):%d\n", today); // 输出2
    printf("Green的值:%d\n", Green);   // 输出20
    printf("favorite(Green):%d\n", favorite); // 输出20

    // 枚举的典型用法:switch语句(提升可读性)
    switch (today) {
        case Mon:
        case Tue:
        case Wed:
        case Thu:
        case Fri:
            printf("今天是工作日\n");
            break;
        case Sat:
        case Sun:
            printf("今天是周末\n");
            break;
        default:
            printf("无效的日期\n");
    }

    return 0;
}
深度解读
  1. 枚举的本质:枚举常量是不可修改的整数常量,枚举变量本质是int类型(可直接赋值整数,如today = 2)。

  2. 赋值规则

    • 未手动指定值的枚举常量,值为前一个常量 + 1;

    • 部分指定值:enum Num {A=5, B, C=10, D} → A=5, B=6, C=10, D=11。

  3. 对比 #define 的优势

    • #define只是简单的文本替换,无类型检查;

    • 枚举是类型,编译器会进行基础的类型检查,且能批量定义相关常量。

  4. 适用场景

    • 表示状态(如Success=0, Error=1);

    • 表示选项(如Male=0, Female=1);

    • 表示固定范围的常量(如星期、月份、颜色)。

总结

  1. 结构体:核心是 “打包不同类型数据”,成员各自占内存,用于描述复杂对象;传参优先用地址传递(高效 + 可修改),只读场景加const保护。

  2. 联合体:核心是 “共享内存”,同一时间仅一个成员有效,大小为最大成员的大小;典型用法是节省内存、判断系统大小端。

  3. 枚举:核心是 “给整数常量命名”,替代魔法数字,提升代码可读性;本质是 int 类型,默认从 0 递增,可手动指定值。

以上便是我对于C语言中的复合数据类型的见解,欢迎各位一起探讨。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值