八、C语言构造类型

思维导图




一、 结构体:打包数据

1.1 语法结构:定义与初始化

定义模板:

c 复制代码
struct 结构体标签 {
    成员1类型 成员1名;
    成员2类型 成员2名;
    ...
}; // <--- 千万别忘了这个分号!

代码示例:学生信息系统

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

// 1. 定义"图纸"
struct Student {
    char name[50];
    int age;
    double gpa;
};

int main() {
    // 2. 声明并初始化(按顺序)
    struct Student s1 = {"Alice", 20, 3.8};
    
    // 3. 指定初始化 (C99 标准,推荐!不受顺序限制)
    struct Student s2 = {
        .age = 22, 
        .name = "Bob", 
        .gpa = 3.5
    };
    
    // 4. 访问成员:使用点号 (.)
    printf("姓名: %s, 年龄: %d\n", s1.name, s1.age);
    
    // 5. 修改成员
    s1.age += 1;
    // s1.name = "Alice Smith"; // 错!数组名不能直接赋值
    strcpy(s1.name, "Alice Smith"); // 正确:使用字符串拷贝
    
    return 0;
}

1.2 结构体传参:值传递的代价

核心痛点:

结构体变量和 int 一样,作为参数传递时是值传递 。如果结构体很大例如包含大数组,拷贝过程会极度消耗 CPU 和内存

最佳实践:
永远优先传递结构体指针。

c 复制代码
// 慢!发生内存拷贝
void print_student_slow(struct Student s) {
    printf("%s", s.name);
}

// 快!只传递 8 字节地址,加 const 防止意外修改
void print_student_fast(const struct Student *s) {
    // printf("%s", s->name); // 使用箭头访问
}

二、 结构体指针与 -> 运算符

2.1 指针访问的简写

如果 p 是指向结构体的指针,访问成员有两种写法:

1.(*p).name (语法繁琐,容易写错优先级,不推荐)

2.p->name箭头运算符,直观,强烈推荐)

2.2 动态分配结构体(堆上造对象)

这是后面学习链表、树等数据结构的基础。

代码示例:

c 复制代码
#include <stdlib.h>

struct Point {
    int x;
    int y;
};

int main() {
    // 1. 在堆区申请内存
    struct Point *p = (struct Point*)malloc(sizeof(struct Point));
    
    if (p == NULL) return 1; // 严谨:检查分配是否成功
    
    // 2. 赋值(使用箭头)
    p->x = 10;
    p->y = 20;
    
    printf("坐标: (%d, %d)\n", p->x, p->y);
    
    // 3. 释放内存
    free(p);
    p = NULL; // 规矩:防止悬空指针
    
    return 0;
}

三、 typedef 的工程意义

每次写 struct Student 都要敲两个词,太累了?typedef 可以给类型起个别名

3.1 语法对比

写法 A(原生写法):

c 复制代码
struct Book {
    char title[100];
    int price;
};
// 声明变量时必须带 struct 关键字
struct Book b1; 

写法 B(typedef 优化,工程标准):

c 复制代码
// 定义的同时起别名
typedef struct {
    char title[100];
    int price;
} Book; // 现在的 'Book' 等价于 'struct { ... }'

int main() {
    Book b1 = {"C Primer Plus", 60}; // 清爽!不用写 struct 了
    return 0;
}

四、 枚举:代码可读性救星

4.1 为什么要用枚举?

与其用 0 代表红色,1 代表绿色(魔术数字,难以维护),不如定义一个枚举,让代码自己说话。

4.2 语法与特性

枚举本质上就是 int 常量。

默认从 0 开始,依次递增。

代码示例:简单的状态机

c 复制代码
// 定义状态:IDLE=0, RUNNING=1, ERROR=2
typedef enum {
    IDLE,
    RUNNING,
    ERROR
} State;

int main() {
    State current_state = IDLE;
    
    // ... 模拟系统运行 ...
    current_state = RUNNING;
    
    // 配合 switch 使用非常优雅
    switch (current_state) {
        case IDLE:    printf("系统空闲中...\n"); break;
        case RUNNING: printf("正在处理任务...\n"); break;
        case ERROR:   printf("发生严重错误!\n"); break;
        default:      printf("未知状态\n");
    }
    return 0;
}

五、 联合体:内存共享的艺术

5.1 核心机制:一地多用

union 的所有成员共享同一块内存地址

大小: 至少能容纳最大的那个成员。
特性: 修改其中一个成员,会覆盖其他成员的值。

应用场景: 嵌入式通信协议、寄存器操作。

代码示例:IP 地址转换(小端序验证)

c 复制代码
typedef union {
    unsigned int ip_int;       // 视角1:看作一个 32 位整数
    unsigned char ip_bytes[4]; // 视角2:看作 4 个独立字节
} IPAddress;

int main() {
    IPAddress ip;
    ip.ip_int = 0x12345678; // 赋值整数
    
    // 通过字节数组访问同一块内存
    // 在常见的小端序机器(x86, ARM)上,低位存在低地址
    // 输出: 78 56 34 12
    printf("%x %x %x %x\n", 
           ip.ip_bytes[0], ip.ip_bytes[1], 
           ip.ip_bytes[2], ip.ip_bytes[3]);
    return 0;
}

六、 练习题

题目 1: struct S { int a; char b; };,在 32 位系统默认对齐下,sizeof(struct S) 是多少?是 5 吗?

题目 2: 给定指针 struct Student *p,如何访问成员 age?写出两种方式。

题目 3: 结构体变量可以直接赋值吗?例如 s1 = s2;?它是深拷贝还是浅拷贝?

题目 4: 结构体变量可以直接比较吗?例如 if (s1 == s2)?为什么?

题目 5: 下面枚举中 BLUE 的值是多少?

c 复制代码
enum Color { RED = 5, GREEN, BLUE };

题目 6: 为什么在定义链表节点时,不能在内部包含同类型的结构体变量,但可以包含同类型的指针?

c 复制代码
struct Node {
    int data;
    // struct Node next; // 写法 A
    struct Node *next;   // 写法 B
};

题目 7: union U { int i; char c; };,如果 i 被赋值为 0x1234c 的值是多少(假设小端序)?

题目 8: 使用 typedef 定义结构体指针别名(如 typedef struct Node* NodePtr;)是一个好习惯吗?

题目 9: 函数参数传递结构体时,什么时候用值传递,什么时候用指针?

题目 10: 什么是内存对齐?为什么要对齐?

题目 11: 编写一个结构体 Rect 表示矩形(包含宽和高),并写一个函数 area 计算面积,要求使用指针传参。

题目 12: 如何定义一个包含 10 个学生信息的结构体数组?

题目 13: struct { int x; } s1; 这种匿名结构体定义合法吗?有什么局限性?

题目 14: 联合体的大小取决于什么?是所有成员大小之和吗?

题目 15: 下面代码输出什么?

c 复制代码
struct { int a; int b; } s = {1, 2};
struct { int a; int b; } *p = &s;
printf("%d", p->b);

七、 解析

题 1 解析
答案: 8 字节。
详解:

这是一个经典的内存对齐 问题。int 占 4 字节,char 占 1 字节。为了让下一个结构体的 int 对齐到 4 的倍数地址,编译器会在 char 后面填充 3 个空字节 (Padding)。4 + 1 + (3) = 8

题 2 解析
答案: p->age (推荐) 或 (*p).age

题 3 解析
答案: 可以。
详解:

编译器会生成代码,将 s2 的内存按字节拷贝给 s1。这是一种浅拷贝。如果结构体里有指针,两个指针会指向同一块内存,需要小心。

题 4 解析
答案: 不可以。
详解:

C 语言没有为结构体定义 == 运算符。原因在于结构体可能包含填充字节 (Padding),其中的垃圾值不可控,直接比较内存是不可靠的。必须手动编写函数逐个成员比较。

题 5 解析
答案: 7。
详解:

RED=5,后续成员默认递增:GREEN=6BLUE=7

题 6 解析
答案: 必须用指针(写法 B)。
详解:

如果包含同类型变量(写法 A),结构体大小会无限递归膨胀(无穷大),编译器无法计算大小。而指针大小是固定的(4或8字节),所以可以作为成员。

题 7 解析
答案: 0x34
详解:

小端序机器上,低位数据存放在低地址。0x1234 的低字节是 0x34char c 对应起始地址(低地址),所以读到的是 0x34

题 8 解析
答案: 视情况而定,但通常不推荐 隐藏指针的星号。
详解:

NodePtr p; 会让使用者忘记 p 本质是个指针,从而忘记检查 NULL,或在对它取地址时产生逻辑混乱。Linux 内核代码规范中明确反对这种做法。

题 9 解析
答案:

结构体极小(如只包含两个 int 的坐标点)可用值传递。一般情况下优先用指针,避免昂贵的内存拷贝。

题 10 解析
答案:

CPU 访问对齐的内存地址(如 4 的倍数)速度最快。不对齐可能导致多次访存甚至硬件异常 (Bus Error)。编译器通过插入填充字节来实现对齐,以空间换时间。

题 11 解析
答案:

c 复制代码
typedef struct { int w, h; } Rect;
int area(const Rect *r) { 
    return r->w * r->h; 
}

题 12 解析
答案: struct Student class_1[10]; (如果用了 typedef,就是 Student class_1[10];)。

题 13 解析
答案: 合法。
详解:

这定义了一个匿名结构体 类型,并立即创建了变量 s1。缺点是以后没法再定义这种类型的其他变量了(除非用 typeof 等非标扩展)。

题 14 解析
答案: 至少是最大成员的大小。
详解:

并且通常要是最大成员类型的整数倍,以满足数组排列时的对齐要求。

题 15 解析
答案: 2。
详解:

指针 p 指向结构体 sp->b 访问第二个成员。虽然类型定义写了两遍,但在 C 中它们被视为兼容的布局(在同个作用域通常能跑通,但严格标准下是不同类型)。

日期:2025年2月13日

专栏:C语言

相关推荐
ytttr8731 小时前
图像配准技术及其Matlab编程实现
开发语言·matlab
小比特_蓝光1 小时前
STL小知识点——C++
java·开发语言·c++·python
阿猿收手吧!1 小时前
【C++】格式化库:告别繁琐,拥抱高效
开发语言·c++
消失的旧时光-19431 小时前
第二十二课:领域建模实战——订单系统最小闭环(实战篇)
java·开发语言·spring boot·后端
悲伤小伞2 小时前
Linux_应用层自定义协议与序列化——网络计算器
linux·服务器·c语言·c++·ubuntu
Y001112362 小时前
Day19—集合进阶-3
java·开发语言
2501_941982052 小时前
马年 Go 篇:高并发企微机器人开发实战
开发语言·golang·企业微信
郝学胜-神的一滴2 小时前
Python中的Dict子类:优雅扩展字典的无限可能
开发语言·python
康小庄2 小时前
Java读写锁降级
java·开发语言·spring boot·python·spring·java-ee