C 语言基础:结构体、联合体与枚举

结构体(Struct)

Java 中有 class 类,在 C 语言中则有 struct 结构体。

结构体的定义与操作

我们先来看看结构体的基本使用:

c 复制代码
#include <stdio.h>
#include <string.h>

// 定义一个结构体
struct Student {
    char name[16];
    int age;
    float score;
};

void printStudent(struct Student stu) {
    printf("name: %s\nage: %d\nscore: %.2f\n\n", stu.name, stu.age, stu.score);
}

int main() {
    // 定义结构体变量
    struct Student stu1 = { "Martin", 21, 94.0f };
    printStudent(stu1);

    printf("====================\n");

    // 修改成员属性
    stu1.age = 22;
    stu1.score = 89.0f;
    const char* p = "Jack";

    // 清空原字符串并拷贝新字符串
    memset(stu1.name, 0, sizeof(stu1.name));
    strcpy_s(stu1.name, sizeof(stu1.name), p);

    printStudent(stu1);
    return 0;
}

注意:在标准的 C 语言中(.c),定义变量时的 struct 关键字不能省略 ,我们必须这样 struct Student stu1;。但如果是在 .cpp 文件中就不会报错, 因为 C++ 允许省略。

我们也可以在定义结构体时,顺便定义出变量,方便使用。

c 复制代码
struct Student {
    char name[16];
    int age;
    float score;
} stu1 = { "Brian", 23, 85.0f }, stu2;

int main() {
    printStudent(stu1);

    printf("======================\n");

    stu2.age = 25;
    stu2.score = 77;
    char name[] = "Mark";
    strcpy_s(stu2.name, sizeof(stu2.name), name);
    printStudent(stu2);
    return 0;
}

与直接定义一个结构体变量(struct Student stu1;)不同的是,顺便定义出的 stu2 会有默认值,换个说法:每个属性都会是其默认值。以 name 属性为例,值是 "" 空字符串。

结构体的嵌套

结构体之间还可能会进行嵌套:

c 复制代码
// 地址信息
struct Address {
    char city[20]; // 城市
    char district[20]; // 区
};

struct Student {
    char name[16];
    int age;
    float score;
    struct Address addr;
};

void printStudent(struct Student stu) {
    printf("student info:\n");
    printf("name: %s\nage: %d\nscore: %.2f\n", stu.name, stu.age, stu.score);
    printf("city: %s\ndistrict: %s\n\n", stu.addr.city, stu.addr.district);
}

int main() {
    struct Student stu = {
        "Brian",
        23,
        85.0f,
        {"Shanghai", "Pudong"}
    };

    printStudent(stu);
    return 0;
}

嵌套的结构体初始化时,只需使用 {} 包裹数据即可,这点和普通结构体的初始化相同。

结构体的定义也可以放到结构体内部:

c 复制代码
struct Student {
    char name[16];
    int age;
    float score;

    struct Address {
        char city[20];
        char district[20];
    };
    
    struct Address addr;
};

也能顺手定义出变量:

c 复制代码
// 也能顺手定义出变量:
struct Student {
    char name[16];
    int age;
    float score;

    struct Address {
        char city[20];
        char district[20];
    } addr; // 顺手定义出变量 addr
};

这样嵌套定义后,如果我们有一个学生变量 struct Student stu;,我们还是可以通过 stu.addr.city 来正常访问嵌套结构体里的数据。

结构体指针

结构体指针(变量)的定义方式是这样的:

c 复制代码
struct Student stu = {
"Alan",
26,
96.0f,
};
struct Student* stu_p = &stu; // 注意:这里的 struct 同样不能省略

它与结构体变量的区别在于,访问成员时,结构体变量用的是 .,结构体指针用的是 -> 箭头运算符,例如:

c 复制代码
void printStudent(struct Student* stu_p) {
    struct Student* p = stu_p;
    printf("student info:\n");
    // 使用 -> 或 (*p). 访问
    printf("name: %s\nage: %d\nscore: %.2f\n\n", p->name, p->age, (*p).score);
}

结构体指针除了取一个结构体变量的地址外,我们可以直接为指针开辟空间(之前学指针的时候就开始用了):

c 复制代码
struct Student* stu_p = (struct Student*)malloc(sizeof(struct Student)); // 需要导入:#include <malloc.h>
// 省略分配内存失败及释放内存的代码

结构体数组

我们再来看看结构体数组,它的定义与使用和普通数组类似:

c 复制代码
int main() {
    // 静态开辟
    struct Student stu_arr[3] = {
        {"Jack", 21, 88},
        {"Mark", 24, 92},
        {"Leon", 19, 76}
    };

    for (size_t i = 0; i < 3; i++)
    {
        printStudent(stu_arr[i]);
    }

    // 动态开辟,stu_arr_2指向首元素的地址
    struct Student* stu_arr_2 = (struct Student*)malloc(sizeof(struct Student) * 3);
    if (stu_arr_2 == NULL) {
        free(stu_arr_2);
        stu_arr_2 = NULL;
        return -1;
    }
    strcpy_s(stu_arr_2[0].name, sizeof(stu_arr_2[0].name), "Brain");
    stu_arr_2->age = 25;
    stu_arr_2->score = 78.0;

    printStudent(*stu_arr_2);
    return 0;
}

字节对齐(内存对齐)

int 类型的指针步长是 4 个字节,那结构体指针的步长呢?

当然就是结构体的大小,以之前的 Student 为例,它的指针步长是 32。这...Student 的大小不是 28 吗?别急,这涉及到了字节对齐

对齐有几个要求:

  1. 每个成员的起始偏移,必须是自身类型大小的整数倍。
  2. 对于嵌套的结构体,必须按其内部最大基础类型的大小进行对齐。(注意对齐基准不是它自身的总大小)
  3. 数组必须按其元素类型的大小进行对齐。(因为数组在内存中是连续分配的,所以只要首元素对齐了,后面的元素自然也就对齐了)
  4. 整个结构体的大小,必须是内部最大对齐数的整数倍。(最大对齐数是所有成员的对齐要求的最大值,包括嵌套结构体中的基础成员)

编译器默认有最大对齐上限(通常是 8 字节,相当于使用 #pragma pack(8)),如果成员对齐数不超过 8,就按成员自身的类型大小对齐,否则会截断为上限大小。

现在应该就明白了:正常来说,Student 的大小是 16+4+8=28。但根据规则三得知 name 的对齐要求是 1 字节;根据规则一得知:age 的对齐要求是 4 字节、score 的对齐要求是 8 字节;根据规则四,得知整体的对齐基准是 8 字节(最大的基础类型是 double)。

然后我们开始算大小,首先是成员进行对齐:name 的起始偏移是 0,正是自身对齐要求 1 的倍数。接着摆放 age,因为 name 占 16 个字节,所以 age 的起始偏移能放到 16,也符合 4 的倍数。接着放 score,我们要填充 4 个字节,这样 score 的起始偏移才能为 8 的倍数(24)。

成员都对齐后,再来看整体的对齐,结构体此时的大小是 32,是 8 的倍数,所以尾部无需额外填充。因此 Student 的大小是 32 字节。

c 复制代码
struct Student {
    char name[16]; // 1
    int age; // 4
    double score; // 8
};

再来看一个嵌套的例子:

c 复制代码
struct BirthDate {
    int year;
    int month;
    int day;
};

struct Student {
    char name[10];
    int age;
    int score;
    char sex;
    struct BirthDate birth;
};

首先看 BirthDate 的大小:

year 放在 0~3,month 放在 4~7,day 放在 8~11,每个成员的偏移都是其类型大小的倍数(4),接着看整体,整体的大小(12)是内部最大基础成员(4)的倍数,无需进行尾部填充。

然后看 Student 的大小:

name 放 0~9,age 放 12~15,name 和 age 的中间填充两个字节(10、11),score 放 16~19,sex 放 20,最后 sex 和 birth 之间填充 3 个字节,birth 放 24~35,这样偏移才是 4 的倍数。

最终 Student 的大小就是 36,你学会了吗?

那为什么需要字节对齐呢?

答:为了提高存储的访问效率。硬件访问数据是一次性访问一块数据,字节对齐可以让数据尽可能排布到较少的数据块上,从而减少硬件访问存储的次数。

typedef 取别名

前面我们多次提到定义结构体变量时不能省略 struct 关键字,为了解决这个问题,我们可以使用 typedef 关键字给结构体取别名:

c 复制代码
typedef struct Student {
    char name[16];
    int age;
    double score;
} Student; // 这里定义的是类型别名,不是变量名

除了在定义结构体时直接取别名,我们也可以给已经存在的结构体或指针单独取别名

c 复制代码
// 语法:typedef 原类型 别名;
typedef struct SchoolStudent Student; // 给 struct SchoolStudent 取别名为 Student
typedef struct Student* StudentPtr;   // 给结构体指针取别名为 StudentPtr

这么做的目的是什么?

  1. 省略定义结构体变量时的 struct 关键字,让代码更加简洁(Student student;)。
  2. 方便使用,将一大串的结构体名称进行缩短。

联合体(Union)

再来看看联合体,它和结构体很像,定义方式如下:

c 复制代码
union Teacher {
    char name[16];
    int age;
    char professional_title[26]; // 职称
};

联合体和结构体的使用几乎一致,只是它的成员同一时间只能存在一个,例如:

c 复制代码
int main() {
    // union Teacher t1 = { "Color", 59 }; // 报错:初始值设定项值太多
    union Teacher t2 = { .age = 59 }; // 或者 { "Color" };
    strcpy_s(t2.name, sizeof(t2.name), "Color");
    printf("%s-%d\n", t2.name, t2.age); // 结果可能是 Color-1869377347
    return 0;
}

这里 age 打印出来的是未初始化的随机值,不是因为内存被释放了,而是内存覆盖 。在联合体中所有成员会共享同一块内存地址 ,所以我们在给 t2.name 赋值为 "Color" 时,这几个字符的 ASCII 值覆盖了 age 之前所在的内存空间,所以此时打印 age,实际上是 "Color" 的前 4 个字符的内存二进制数据,直接当做整型解析出来的一个数值,看起来有点像乱码而已。

联合体的大小计算

  1. 成员对齐后的大小是最大成员的大小,在我们的例子中是 26,因为同一时刻,只能存在一个成员。

  2. 然后是联合体整体的对齐,联合体大小必须是内部最大对齐数的整数倍,这里是 4,所以联合体会填充 2 个字节,最终大小是 28。

枚举(Enum)

最后是枚举。枚举在 C 语言中其实是 int 类型,第一个枚举变量的值默认是 0,后续的枚举变量会依次递增(+1)。

c 复制代码
enum Result {
    Success, // 0
    Fail // 1
};

我们也可以手动改值:

c 复制代码
enum Week {
    MONDAY = 1,
    TUESDAY = 2,
    WEDNESDAY = 3,
    THURSDAY = 4,
    FRIDAY = 5,
    SATURDAY = 6,
    SUNDAY = 7
};

要么全都不赋值(从 0 开始自动递增),要么全都手动赋值,或者只给第一个枚举成员赋值(比如 MONDAY = 1)。这样不容易出错,因为不同枚举变量的值是可以相同的。

相关推荐
阿巴斯甜2 小时前
Android 代码混淆
android
apihz4 小时前
台风实时与历史详情查询免费 API 接口完整教程
android·开发语言·tcp/ip·dubbo·台风·天气预报
浪客川5 小时前
AOSP源码隐藏状态栏
android·aosp
没有了遇见5 小时前
AI Agent 是什么?—— 一文理解 LLM、Memory、Skills、Tools、MCP、Workflow,Context
android·前端·程序员
御坂嘀喵6 小时前
Speed Tools:一套低侵入的 Android 插件化 + 动态换肤 + 字体切换框架
android·gitee
浮江雾6 小时前
Flutter第三节----Dart中的数据类型
android·开发语言·学习·flutter·入门·函数
风和先行7 小时前
Android 数据库相关学习总结
android·数据库·学习
杉氧7 小时前
Compose 渲染内核 (3):Slot Table 与 Gap Buffer —— 极速重组的数学艺术
android·架构·android jetpack
AFinalStone8 小时前
Android 7系统网络(五)Framework层(上)—ConnectivityService核心机制
android·网络