C语言结构体与共用体的深度解析及实战应用

C语言结构体与共用体的深度解析及实战应用

在这里插入图片描述

💡 学习目标:掌握结构体与共用体的定义和使用方法,理解二者的内存布局差异,能够在实际开发中灵活运用结构体和共用体组织复杂数据;学习重点:结构体的定义与初始化、结构体指针的使用、结构体嵌套、共用体的特性及应用场景。

42.1 结构体的基本概念与定义

在C语言中,基础数据类型只能存储单一类型的数据。当需要描述一个包含多种属性的复杂对象时,就需要用到结构体。结构体可以将不同类型的数据组合成一个有机的整体。

42.1.1 结构体的定义语法

💡 结构体的定义使用 struct 关键字,语法格式如下:

struct 结构体名 {
    数据类型 成员名1;
    数据类型 成员名2;
    // ... 更多成员
};

参数说明

  • 结构体名:自定义的标识符,用于标识结构体类型。
  • 成员名:结构体内部的变量名称,成员可以是任意基础数据类型或自定义类型。

示例:定义学生结构体

#include <stdio.h>

// 定义学生结构体
struct Student {
    // 姓名
    char name[20];
    // 年龄
    int age;
    // 学号
    int id;
    // 成绩
    float score;
};

int main() {
    printf("学生结构体大小:%zu 字节\n", sizeof(struct Student));
    return 0;
}

运行结果:

学生结构体大小:28 字节

💡 技巧:结构体的大小不是成员变量大小的简单相加,而是存在内存对齐机制。目的是为了提高CPU的访问效率,这是我们后面要重点讲的内容。

42.1.2 结构体的内存对齐规则

⚠️ 注意:内存对齐是结构体的核心特性,也是面试的高频考点。C语言默认的对齐规则如下:

  1. 结构体的第一个成员,对齐到结构体变量起始地址的0偏移处。
  2. 其他成员变量,要对齐到自身数据类型大小的整数倍地址处。
  3. 结构体的总大小,必须是结构体中最大基本类型成员大小的整数倍。

示例:验证内存对齐

#include <stdio.h>

struct Test1 {
    // 占1字节
    char a;
    // 占4字节
    int b;
    // 占2字节
    short c;
};

struct Test2 {
    char a;
    short c;
    int b;
};

int main() {
    printf("struct Test1 大小:%zu 字节\n", sizeof(struct Test1));
    printf("struct Test2 大小:%zu 字节\n", sizeof(struct Test2));
    return 0;
}

运行结果:

struct Test1 大小:12 字节
struct Test2 大小:8 字节

✅ 结论:调整结构体成员的顺序,可以改变结构体的总大小。在实际开发中,合理排列成员顺序能够节省内存空间。

42.2 结构体变量的定义与初始化

定义结构体类型后,我们就可以创建结构体变量,并对其进行初始化操作。结构体变量的初始化有多种方式,我们逐一讲解。

42.2.1 结构体变量的定义方式

💡 结构体变量的定义有三种常见方式:

  1. 先定义结构体类型,再定义变量
  2. 定义结构体类型的同时定义变量
  3. 匿名结构体定义变量

示例:三种定义方式对比

#include <stdio.h>

// 方式1:先定义类型,再定义变量
struct Person {
    char name[20];
    int age;
    float height;
};

// 方式2:定义类型的同时定义变量
struct Book {
    char title[50];
    char author[20];
    float price;
} book1, book2;

// 方式3:匿名结构体定义变量
struct {
    int x;
    int y;
} point1, point2;

int main() {
    // 方式1定义的变量
    struct Person p1;
    return 0;
}

💡 技巧:匿名结构体的缺点是只能使用一次,无法在其他地方定义新的变量。适合只需要创建少量变量的场景。

42.2.2 结构体变量的初始化

结构体变量的初始化可以使用 {} 进行赋值,支持顺序初始化和指定成员初始化两种方式。

示例:结构体变量的初始化

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

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

int main() {
    // 方式1:顺序初始化,必须与结构体成员顺序一致
    struct Student s1 = {"张三", 18, 2024001, 95.5};

    // 方式2:指定成员初始化,顺序可以任意
    struct Student s2 = {
        .id = 2024002,
        .name = "李四",
        .score = 88.0,
        .age = 19
    };

    // 方式3:先定义,后赋值
    struct Student s3;
    strcpy(s3.name, "王五");
    s3.age = 20;
    s3.id = 2024003;
    s3.score = 92.0;

    // 输出结构体内容
    printf("学生1:%s %d %d %.1f\n", s1.name, s1.age, s1.id, s1.score);
    printf("学生2:%s %d %d %.1f\n", s2.name, s2.age, s2.id, s2.score);
    printf("学生3:%s %d %d %.1f\n", s3.name, s3.age, s3.id, s3.score);

    return 0;
}

运行结果:

学生1:张三 18 2024001 95.5
学生2:李四 19 2024002 88.0
学生3:王五 20 2024003 92.0

⚠️ 注意:使用字符串给 char 数组成员赋值时,不能直接用 =,需要使用 strcpy 函数。否则会导致编译错误。

42.3 结构体指针的使用

结构体指针是指向结构体变量的指针,它存储的是结构体变量的首地址。使用结构体指针访问成员可以提高程序的执行效率,是实际开发中的常用方式。

42.3.1 结构体指针的定义与成员访问

💡 结构体指针的定义语法:struct 结构体名 *指针变量名;
访问结构体指针成员的两种方式:

  1. 解引用方式:(*指针变量名).成员名
  2. 箭头方式:指针变量名->成员名

示例:结构体指针的使用

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

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

int main() {
    struct Student s;
    // 定义结构体指针,指向结构体变量s
    struct Student *p = &s;

    // 方式1:通过结构体变量访问成员
    strcpy(s.name, "赵六");
    s.age = 21;
    s.score = 90.5;

    printf("方式1:%s %d %.1f\n", s.name, s.age, s.score);

    // 方式2:通过结构体指针解引用访问成员
    strcpy((*p).name, "孙七");
    (*p).age = 22;
    (*p).score = 87.0;

    printf("方式2:%s %d %.1f\n", (*p).name, (*p).age, (*p).score);

    // 方式3:通过箭头访问成员(推荐使用)
    strcpy(p->name, "周八");
    p->age = 23;
    p->score = 93.0;

    printf("方式3:%s %d %.1f\n", p->name, p->age, p->score);

    return 0;
}

运行结果:

方式1:赵六 21 90.5
方式2:孙七 22 87.0
方式3:周八 23 93.0

✅ 结论:箭头运算符 -> 是访问结构体指针成员的简洁方式,在实际开发中推荐使用。

42.3.2 动态结构体指针

结合前面学习的动态内存分配,我们可以创建动态结构体指针,在堆区分配内存,灵活管理结构体的生命周期。

示例:动态结构体指针的使用

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

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

int main() {
    // 动态分配内存
    struct Student *p = (struct Student *)malloc(sizeof(struct Student));
    if (p == NULL) {
        printf("内存申请失败!\n");
        return 1;
    }

    // 给动态结构体赋值
    strcpy(p->name, "吴九");
    p->age = 24;
    p->score = 89.5;

    printf("动态结构体:%s %d %.1f\n", p->name, p->age, p->score);

    // 释放内存
    free(p);
    p = NULL;

    return 0;
}

运行结果:

动态结构体:吴九 24 89.5

💡 技巧:动态结构体指针是实现链表、树等复杂数据结构的基础。掌握它的使用方法,是C语言进阶的关键一步。

42.4 结构体嵌套与结构体数组

结构体的成员可以是另一个结构体,这就是结构体嵌套。当需要存储多个同类型结构体数据时,可以使用结构体数组

42.4.1 结构体嵌套

示例:嵌套结构体表示学生信息

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

// 定义日期结构体
struct Date {
    int year;
    int month;
    int day;
};

// 定义学生结构体,嵌套日期结构体
struct Student {
    char name[20];
    int age;
    // 生日
    struct Date birthday;
    float score;
};

int main() {
    struct Student s;
    strcpy(s.name, "郑十");
    s.age = 25;
    // 访问嵌套结构体成员
    s.birthday.year = 1999;
    s.birthday.month = 10;
    s.birthday.day = 1;
    s.score = 91.0;

    printf("姓名:%s\n", s.name);
    printf("年龄:%d\n", s.age);
    printf("生日:%d-%d-%d\n", s.birthday.year, s.birthday.month, s.birthday.day);
    printf("成绩:%.1f\n", s.score);

    return 0;
}

运行结果:

姓名:郑十
年龄:25
生日:1999-10-1
成绩:91.0

42.4.2 结构体数组

示例:结构体数组存储多个学生信息

#include <stdio.h>

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

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

    // 遍历结构体数组
    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);
    }

    // 使用指针遍历结构体数组
    struct Student *p = stu_arr;
    for (int i = 0; i < 3; i++) {
        printf("指针访问:%s %d %.1f\n", p->name, p->age, p->score);
        p++;
    }

    return 0;
}

运行结果:

第1个学生:张三 18 95.5
第2个学生:李四 19 88.0
第3个学生:王五 20 92.0
指针访问:张三 18 95.5
指针访问:李四 19 88.0
指针访问:王五 20 92.0

💡 技巧:结构体数组的数组名同样是常量指针,指向数组的第一个元素。使用指针遍历结构体数组可以提高代码效率。

42.5 共用体的定义与特性

共用体(Union)也是一种自定义数据类型,它可以存储不同类型的数据,但同一时刻只能存储其中一种类型的数据。共用体的大小等于最大成员的大小。

42.5.1 共用体的定义与使用

💡 共用体的定义使用 union 关键字,语法格式与结构体类似:

union 共用体名 {
    数据类型 成员名1;
    数据类型 成员名2;
    // ... 更多成员
};

示例:共用体的基本使用

#include <stdio.h>

union Data {
    char c;
    int i;
    float f;
};

int main() {
    union Data d;
    printf("共用体大小:%zu 字节\n", sizeof(d));

    // 存储字符类型
    d.c = 'A';
    printf("存储字符:%c\n", d.c);

    // 存储整型,覆盖之前的字符数据
    d.i = 100;
    printf("存储整数:%d\n", d.i);
    // 此时访问c,数据已经被覆盖
    printf("覆盖后访问c:%c\n", d.c);

    // 存储浮点型,覆盖之前的整数数据
    d.f = 3.14f;
    printf("存储浮点数:%.2f\n", d.f);
    printf("覆盖后访问i:%d\n", d.i);

    return 0;
}

运行结果:

共用体大小:4 字节
存储字符:A
存储整数:100
覆盖后访问c:d
存储浮点数:3.14
覆盖后访问i:1078523331

✅ 结论:共用体的所有成员共享同一块内存空间,修改一个成员会影响其他成员的值。

42.5.2 共用体的应用场景

⚠️ 注意:共用体的核心优势是节省内存空间,常用于以下场景:

  1. 数据类型转换:利用共用体的内存共享特性,实现不同数据类型之间的转换。
  2. 节省内存:当数据有多种类型,但同一时刻只使用其中一种时,使用共用体可以节省内存。
  3. 判断系统大小端:这是共用体的经典应用,也是面试高频考点。

示例:使用共用体判断系统大小端

#include <stdio.h>

union EndianTest {
    int i;
    char c;
};

int main() {
    union EndianTest et;
    et.i = 1;
    // 小端:低字节存低地址;大端:高字节存低地址
    if (et.c == 1) {
        printf("当前系统是小端模式\n");
    } else {
        printf("当前系统是大端模式\n");
    }
    return 0;
}

运行结果:

当前系统是小端模式

💡 技巧:目前绝大多数计算机系统都采用小端模式,如x86架构。大端模式主要用于一些嵌入式设备和网络传输中。

42.6 结构体与共用体的实战案例

42.6.1 案例1:通讯录管理系统(结构体数组版)

🔧 需求:实现一个简单的通讯录管理系统,支持添加、显示、查询联系人信息。

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

#define MAX 100

// 定义联系人结构体
struct Contact {
    char name[20];
    char phone[12];
    char email[30];
};

// 定义通讯录结构体
struct AddressBook {
    // 联系人数组
    struct Contact contacts[MAX];
    // 当前联系人数量
    int count;
};

// 添加联系人
void addContact(struct AddressBook *ab) {
    if (ab->count >= MAX) {
        printf("通讯录已满,无法添加!\n");
        return;
    }
    struct Contact c;
    printf("请输入姓名:");
    scanf("%s", c.name);
    printf("请输入电话:");
    scanf("%s", c.phone);
    printf("请输入邮箱:");
    scanf("%s", c.email);
    ab->contacts[ab->count] = c;
    ab->count++;
    printf("添加成功!\n");
}

// 显示所有联系人
void showContacts(struct AddressBook *ab) {
    if (ab->count == 0) {
        printf("通讯录为空!\n");
        return;
    }
    printf("\n通讯录列表(共%d人):\n", ab->count);
    printf("姓名\t\t电话\t\t邮箱\n");
    for (int i = 0; i < ab->count; i++) {
        printf("%s\t\t%s\t\t%s\n", 
               ab->contacts[i].name, 
               ab->contacts[i].phone, 
               ab->contacts[i].email);
    }
}

// 查询联系人
void searchContact(struct AddressBook *ab) {
    char name[20];
    printf("请输入要查询的姓名:");
    scanf("%s", name);
    for (int i = 0; i < ab->count; i++) {
        if (strcmp(ab->contacts[i].name, name) == 0) {
            printf("查询结果:\n");
            printf("姓名:%s\n", ab->contacts[i].name);
            printf("电话:%s\n", ab->contacts[i].phone);
            printf("邮箱:%s\n", ab->contacts[i].email);
            return;
        }
    }
    printf("未找到该联系人!\n");
}

int main() {
    struct AddressBook ab;
    // 初始化联系人数量为0
    ab.count = 0;
    int choice;
    while (1) {
        printf("\n===== 通讯录管理系统 =====\n");
        printf("1. 添加联系人\n");
        printf("2. 显示联系人\n");
        printf("3. 查询联系人\n");
        printf("0. 退出系统\n");
        printf("请输入您的选择:");
        scanf("%d", &choice);
        switch (choice) {
            case 1:
                addContact(&ab);
                break;
            case 2:
                showContacts(&ab);
                break;
            case 3:
                searchContact(&ab);
                break;
            case 0:
                printf("退出系统!\n");
                return 0;
            default:
                printf("输入错误,请重新选择!\n");
        }
    }
    return 0;
}

运行结果:

===== 通讯录管理系统 =====
1. 添加联系人
2. 显示联系人
3. 查询联系人
0. 退出系统
请输入您的选择:1
请输入姓名:张三
请输入电话:13800138000
请输入邮箱:zhangsan@example.com
添加成功!

===== 通讯录管理系统 =====
1. 添加联系人
2. 显示联系人
3. 查询联系人
0. 退出系统
请输入您的选择:2

通讯录列表(共1人):
姓名            电话            邮箱
张三            13800138000     zhangsan@example.com

===== 通讯录管理系统 =====
1. 添加联系人
2. 显示联系人
3. 查询联系人
0. 退出系统
请输入您的选择:0
退出系统!

42.6.2 案例2:共用体实现数据类型转换

🔧 需求:使用共用体将浮点数转换为二进制表示,查看其在内存中的存储形式。

#include <stdio.h>

union FloatUnion {
    float f;
    unsigned char bytes[4];
};

void printBinary(unsigned char c) {
    for (int i = 7; i >= 0; i--) {
        printf("%d", (c >> i) & 1);
    }
    printf(" ");
}

int main() {
    union FloatUnion fu;
    fu.f = 3.14f;

    printf("浮点数 %.2f 的十六进制表示:\n", fu.f);
    for (int i = 0; i < 4; i++) {
        printf("%02x ", fu.bytes[i]);
    }
    printf("\n");

    printf("浮点数 %.2f 的二进制表示:\n", fu.f);
    for (int i = 0; i < 4; i++) {
        printBinary(fu.bytes[i]);
    }
    printf("\n");

    return 0;
}

运行结果:

浮点数 3.14 的十六进制表示:
c8 f5 48 40
浮点数 3.14 的二进制表示:
11001000 11110101 01001000 01000000

42.7 结构体与共用体的常见问题与解决方案

42.7.1 问题1:结构体赋值错误

❌ 错误代码:

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

struct Student s1 = {"张三", 18};
struct Student s2;
// 错误:数组不能直接用=赋值
s2.name = s1.name;

✅ 解决方案:

  1. 使用 strcpy 函数复制字符串成员。
  2. 直接赋值整个结构体变量,s2 = s1;

42.7.2 问题2:结构体传参效率低

❌ 错误原因:结构体传参时,如果直接传递结构体变量,会拷贝整个结构体,效率较低。
✅ 解决方案:传递结构体指针,避免拷贝开销。

42.7.3 问题3:混淆结构体与共用体

❌ 错误原因:不清楚结构体和共用体的内存布局差异,导致数据访问错误。
✅ 解决方案:

  1. 结构体成员各自拥有独立的内存空间。
  2. 共用体成员共享同一块内存空间。
  3. 结构体大小遵循内存对齐规则,共用体大小等于最大成员的大小。

42.8 本章小结

✅ 结构体是C语言中组织复杂数据的核心工具,其大小遵循内存对齐规则,合理排列成员顺序可以节省内存。
✅ 结构体指针是访问结构体成员的高效方式,箭头运算符 -> 是结构体指针的常用操作符。
✅ 共用体的所有成员共享同一块内存空间,同一时刻只能存储一种类型的数据,适合节省内存的场景。
✅ 结构体嵌套和结构体数组是实现复杂数据结构的基础,动态结构体指针是链表等数据结构的核心。
✅ 结构体和共用体是C语言的重点和难点,也是面试的高频考点,需要熟练掌握。

评论 68
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值