C语言结构体与链表

第一部分:C语言结构体(Struct)

1.1 什么是结构体

老师强调 :结构体是C语言对基本数据类型的重要补充。就像用int定义整数、float定义浮点数一样,结构体允许我们将不同类型的数据组合成一个新的复合数据类型 。比如可以把intfloatchar[]等多种类型打包在一起,形成自定义的数据结构。

结构体 vs 基本类型:

类型 来源 内存布局 示例
基本类型(int、float等) C语言内置 由编译器固定 int a = 10;
结构体类型 用户自定义 由定义者决定 struct Student stu;

老师强调 :结构体的精妙之处在于自定义 。就像C语言官方定义了short/int/float这些基础类型,而结构体的"长相"完全由我们自己决定。这就像乐高积木------官方提供基础零件,但最终拼成什么造型由玩家自己决定。

1.2 结构体的定义

复制代码
struct student {
    int id;           // 学号
    char name[20];    // 姓名(字符数组)
    float score;      // 成绩
};  // ⭐ 注意:结尾必须有分号!

老师强调 :定义结构体时其实是在创造一个新的数据类型,这个类型在内存中的排列方式完全遵循我们定义的规则。结构体定义末尾必须加分号,就像写句子要写句号一样。

1.3 结构体的内存布局与对齐

老师强调 :结构体成员在内存中存储时,会遵循一定的对齐规则 ,通常是按成员的大小或计算机的字长(32位=4字节,64位=8字节)进行对齐。这种机制是为了优化CPU访问内存的效率,即使会占用一部分额外的内存空间。

对齐示例:

复制代码
struct example {
    char a;    // 1字节
    int b;     // 4字节
    char c;    // 1字节
};

内存布局(64位系统,8字节对齐):

复制代码
地址:0   1   2   3   4   5   6   7   8   9   10  11
      ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
      │ a │填充│填充│填充│   b    │ c │填充│填充│填充│填充│
      └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
      ←─ 1 ─→←── 3 ──→←── 4 ──→←─ 1 ─→←─── 4 ───→

对齐规则:

  • char a(1字节)→ 从地址0开始,占1字节
  • 为了int b(4字节)能从4的倍数地址开始,填充3个字节
  • int b → 从地址4开始,占4字节
  • char c(1字节)→ 从地址8开始,占1字节
  • 整个结构体大小为12字节(填充到8的倍数)

老师强调 :用少量内存空间换取更快的寻址速度是值得的。这就像把书按固定间距摆在书架上,虽然会浪费一点空间,但找起来特别快。时间效率的浪费比存储空间的浪费更关键

对齐规则速查:

系统位数 默认对齐单位 说明
32位系统 4字节 int对齐到4的倍数
64位系统 8字节 int对齐到4的倍数,整体对齐到8的倍数

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

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

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

int main()
{
    // ✅ 方式1:定义时直接初始化
    struct student stu = {1001, "张三", 65.5};
    
    // 访问成员(使用点运算符 .)
    printf("%d\n", stu.id);       // 输出 1001
    printf("%s\n", stu.name);     // 输出 张三
    printf("%.2f\n", stu.score);  // 输出 65.50
    
    // ✅ 方式2:定义后逐个赋值
    stu.id = 114514;
    printf("%d\n", stu.id);       // 输出 114514
    
    // ❌ 错误:字符数组不能直接用指针赋值
    // stu.name = "李四";  // 错误!
    
    // ✅ 正确:使用 strcpy 复制字符串
    strcpy(stu.name, "李四");
    
    return 0;
}

1.5 结构体指针

复制代码
struct student stu = {1001, "张三", 65.5};
struct student* p = &stu;   // p 指向 stu

// 方式1:通过指针访问成员(使用箭头运算符 ->)
p->id = 100;
printf("%d\n", p->id);      // 输出 100
printf("%s\n", p->name);    // 输出 张三

// 方式2:先解引用再访问(不推荐)
(*p).id = 200;              // 等价于 p->id = 200
变量类型 访问成员方式 示例
结构体变量 点运算符 . stu.id
结构体指针 箭头运算符 -> p->id

老师强调:三段式结构必须通过点调用,而指针则要用箭头。这两种形式是访问结构体数据的核心方法,需要根据变量类型正确选择。

1.6 动态内存分配

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

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

int main()
{
    // 动态分配内存
    struct student* p2 = (struct student*)malloc(sizeof(struct student));
    
    // ⭐ 必须检查分配是否成功
    if (p2 == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }
    
    // 通过指针赋值
    p2->id = 1919810;
    strcpy(p2->name, "网媒");   // ⭐ 字符串必须用 strcpy
    p2->score = 89.5;
    
    printf("%d\n", p2->id);     // 输出 1919810
    printf("%s\n", p2->name);   // 输出 网媒
    printf("%.2f\n", p2->score);
    
    // 释放内存
    free(p2);
    p2 = NULL;
    
    return 0;
}

老师强调malloc返回的是void*,需要通过强制类型转换 转换为目标结构体类型。分配后必须检查p != NULL,这是防止空指针操作的基本安全措施。用完必须free()释放内存。

1.7 字符串赋值:strcpy 的必要性

复制代码
struct student stu;

// ❌ 错误:name是字符数组,不能直接赋值
// stu.name = "张三";

// ✅ 正确:使用 strcpy 复制字符串
strcpy(stu.name, "张三");

老师强调 :字符数组不能直接用指针赋值,因为数组名是常量指针。必须用strcpy函数逐字节复制,同时需要包含<string.h>头文件。

第二部分:链表(Linked List)

2.1 为什么需要链表

数组 vs 链表对比:

特性 数组(顺序存储) 链表(链式存储)
存储方式 内存连续 内存分散
查询速度 (直接下标访问) (需遍历)
增删速度 (需移动元素) (改指针即可)
内存利用 固定大小,可能浪费 按需分配,灵活

老师强调 :数组就像固定座位的教室,调座位很麻烦;链表就像自由组合的火车车厢,随时可以断开重组。数组查询快但增删慢,链表增删快但查询慢,这个特性对比要记牢。

增删操作对比:

复制代码
数组删除(需移动元素):
[1][2][3][4][5] → 删除3 → [1][2][4][5]  (4和5要前移)

链表删除(改指针即可):
[1]→[2]→[3]→[4]→[5] → 删除3 → [1]→[2]→[4]→[5]  (直接改指针)

2.2 链表节点的定义

老师强调 :链表是由节点(Node)串起来的,每个节点包含两个部分:数据域 (存储数据)和指针域(指向下一个节点)。就像火车车厢一样,每个节点既要存数据,又要知道下一节车厢在哪。

复制代码
struct Node {
    int data;           // 数据域
    struct Node* next;  // 指针域:指向下一个节点
};

节点结构图示:

复制代码
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   节点1          │     │   节点2          │     │   节点3          │
│  ┌───────────┐  │     │  ┌───────────┐  │     │  ┌───────────┐  │
│  │ data = 10 │  │     │  │ data = 20 │  │     │  │ data = 30 │  │
│  ├───────────┤  │     │  ├───────────┤  │     │  ├───────────┤  │
│  │ next ─────┼──┼────▶│  │ next ─────┼──┼────▶│  │ next = NULL│  │
│  └───────────┘  │     │  └───────────┘  │     │  └───────────┘  │
└─────────────────┘     └─────────────────┘     └─────────────────┘
        ▲
    head指针(始终指向第一个节点)

2.3 创建节点

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

struct Node {
    int data;
    struct Node* next;
};

int main()
{
    // 创建3个节点(动态分配)
    struct Node* node1 = (struct Node*)malloc(sizeof(struct Node));
    struct Node* node2 = (struct Node*)malloc(sizeof(struct Node));
    struct Node* node3 = (struct Node*)malloc(sizeof(struct Node));
    
    // 赋值
    node1->data = 10;
    node2->data = 20;
    node3->data = 30;
    
    // 链接:node1 → node2 → node3 → NULL
    node1->next = node2;
    node2->next = node3;
    node3->next = NULL;
    
    return 0;
}

2.4 头指针(Head Pointer)

老师强调 :链表操作的核心在于维护一个头指针(head) ,它始终指向链表的第一个节点。没有它,我们就找不到整列火车了。头指针的类型和节点类型一致,通常命名为head

复制代码
struct Node* head = NULL;   // 初始为空链表

2.5 尾插法(插入到链表末尾)

老师强调:尾插法需要遍历链表找到最后一个节点,然后将新节点挂在末尾。就像排队一样,新来的人总要找到队伍最后面那个人才行。

复制代码
struct Node* insert(struct Node* head, int value) {
    // 1. 创建新节点
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = NULL;
    
    // 2. 空链表:新节点直接作为头节点
    if (head == NULL) {
        return newNode;
    }
    
    // 3. 遍历找到最后一个节点
    struct Node* index = head;
    while (index->next != NULL) {
        index = index->next;   // 指针后移
    }
    
    // 4. 将新节点挂在末尾
    index->next = newNode;
    return head;
}

尾插法过程图示:

复制代码
插入前:head → [10] → [20] → NULL
                    ↑
                  index(遍历到最后一个节点)

插入新节点(value=30):
head → [10] → [20] → [30] → NULL
                    ↑
                  index

2.6 头插法(插入到链表头部)

老师强调 :头插法将新节点插入到链表头部,新节点永远成为链表的第一个元素。只需两步:先让新节点的next指向原头节点,再更新head指向新节点。顺序不能错,否则会丢失原链表。

复制代码
struct Node* firstInsert(struct Node* head, int value) {
    // 1. 创建新节点
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = value;
    
    // 2. 新节点指向原头节点
    newNode->next = head;
    
    // 3. 更新头指针
    return newNode;
}

头插法过程图示:

复制代码
插入前:head → [10] → [20] → NULL
新节点:[30] → NULL

步骤1:newNode->next = head
[30] → [10] → [20] → NULL

步骤2:head = newNode
head → [30] → [10] → [20] → NULL

头插法 vs 尾插法:

对比项 头插法 尾插法
插入位置 链表头部 链表尾部
是否需要遍历 ❌ 不需要 ✅ 需要(找尾节点)
时间复杂度 O(1) O(n)
插入后顺序 与插入顺序相反 与插入顺序相同

2.7 删除节点

复制代码
struct Node* deleteNode(struct Node* head, int value) {
    // 1. 空链表
    if (head == NULL) {
        return NULL;
    }
    
    // 2. 删除头节点
    if (head->data == value) {
        struct Node* temp = head;
        head = head->next;
        free(temp);
        return head;
    }
    
    // 3. 删除中间或尾部节点
    struct Node* index = head;
    while (index->next != NULL) {
        if (index->next->data == value) {
            struct Node* temp = index->next;
            index->next = index->next->next;
            free(temp);
            return head;
        }
        index = index->next;
    }
    
    printf("没有找到该值\n");
    return head;
}

删除节点过程图示:

复制代码
删除前:head → [10] → [20] → [30] → NULL
删除 value=20:

步骤1:找到要删除节点的前一个节点(index指向10)
head → [10] → [20] → [30] → NULL
        ↑
      index

步骤2:跳过要删除的节点
head → [10] → [30] → NULL
        ↑
      index

步骤3:释放被删除节点的内存

2.8 遍历链表

复制代码
// 打印所有节点
struct Node* current = head;
while (current != NULL) {
    printf("%d\n", current->data);
    current = current->next;
}

2.9 链表完整示例

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

struct Node {
    int data;
    struct Node* next;
};

// 尾插法
struct Node* insert(struct Node* head, int value) {
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = NULL;
    
    if (head == NULL) {
        return newNode;
    }
    
    struct Node* index = head;
    while (index->next != NULL) {
        index = index->next;
    }
    index->next = newNode;
    return head;
}

// 头插法
struct Node* firstInsert(struct Node* head, int value) {
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = head;
    return newNode;
}

// 删除节点
struct Node* deleteNode(struct Node* head, int value) {
    if (head == NULL) {
        return NULL;
    }
    
    if (head->data == value) {
        struct Node* temp = head;
        head = head->next;
        free(temp);
        return head;
    }
    
    struct Node* index = head;
    while (index->next != NULL) {
        if (index->next->data == value) {
            struct Node* temp = index->next;
            index->next = index->next->next;
            free(temp);
            return head;
        }
        index = index->next;
    }
    printf("没有找到该值\n");
    return head;
}

int main()
{
    struct Node* head = NULL;
    
    // 尾插
    head = insert(head, 1);
    head = insert(head, 114514);
    
    // 头插
    head = firstInsert(head, 114514);
    
    // 删除
    head = deleteNode(head, 1);
    head = deleteNode(head, 12);
    
    // 遍历打印
    struct Node* current = head;
    while (current != NULL) {
        printf("%d\n", current->data);
        current = current->next;
    }
    
    return 0;
}

第三部分:猜数字小游戏

3.1 随机数生成

老师强调 :随机数的生成关键在于种子(seed)。如果不加种子,每次运行程序都会生成相同的随机数序列。用系统时间作为种子,由于时间不断变化,就能确保每次生成不同的随机数。

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main()
{
    // 1. 设置随机数种子(使用当前时间)
    srand(time(NULL));
    
    // 2. 生成 1~100 的随机数
    int target = rand() % 100 + 1;
    
    printf("目标值:%d\n", target);
    return 0;
}

rand() 函数说明:

函数 作用
srand(seed) 设置随机数种子
rand() 生成随机数(0 ~ RAND_MAX)
rand() % 100 生成 0~99 的随机数
rand() % 100 + 1 生成 1~100 的随机数

3.2 完整游戏代码

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    int target, guess;
    int low = 1, high = 100;
    
    // 生成随机数
    srand(time(NULL));
    target = rand() % 100 + 1;
    
    printf("游戏开始!范围:1~100\n");
    
    while (1) {
        printf("当前范围 [%d ~ %d],请输入:", low, high);
        scanf("%d", &guess);
        
        // 检查输入是否在范围内
        if (guess < low || guess > high) {
            printf("超出范围了!重新输入\n");
            continue;
        }
        
        // 判断大小
        if (guess > target) {
            printf("太大了!\n");
            high = guess - 1;   // 缩小上限
        } else if (guess < target) {
            printf("太小了!\n");
            low = guess + 1;    // 扩大下限
        } else {
            printf("你猜对了!\n");
            break;
        }
    }
    
    return 0;
}

3.3 游戏逻辑流程图

复制代码
开始
  │
  ▼
生成随机数(1~100)
  │
  ▼
进入循环
  │
  ▼
用户输入猜测数字
  │
  ▼
判断是否在范围内?
  │
  ├── 否 → 提示"超出范围",重新输入
  │
  └── 是 → 判断大小
            │
            ├── 大于目标 → 提示"太大了!",缩小上限
            │
            ├── 小于目标 → 提示"太小了!",扩大下限
            │
            └── 等于目标 → 提示"你猜对了!",退出循环

第四部分:简易计算器实战

4.1 完整代码

复制代码
#include "stdio.h"

double num1, num2, result;
char op;

int main()
{
    while(1) {
        printf("请输入(格式:运算符 数字1 数字2,输入 q 退出):");
        scanf(" %c %lf %lf", &op, &num1, &num2);  // %c前加空格
        
        if (op == '+') {
            printf("%.2lf\n", num1 + num2);
        } else if (op == '-') {
            printf("%.2lf\n", num1 - num2);
        } else if (op == '*') {
            printf("%.2lf\n", num1 * num2);
        } else if (op == '/') {
            if (num2 != 0)
                printf("%.2lf\n", num1 / num2);
            else
                printf("Error: Division by zero!\n");
        } else if (op == 'q') {
            printf("程序退出!\n");
            break;
        } else {
            printf("Invalid operator! (op = '%c')\n", op);
        }
    }
    return 0;
}

4.2 关键技术点

scanf 格式说明符:

格式符 含义 示例
%c 字符 scanf(" %c", &op)
%lf 双精度浮点数(double) scanf("%lf", &num1)
%f 单精度浮点数(float) scanf("%f", &num1)
%d 整数 scanf("%d", &num)

老师强调scanf%c前加空格可以跳过空白字符 (回车、空格等),避免读取到上一次输入的回车。%lf对应double类型,%f对应float类型,写错会导致数据读取失败。

输入格式示例:

复制代码
用户输入:+ 10.5 20.3
op = '+', num1 = 10.5, num2 = 20.3
输出:30.80

用户输入:q
op = 'q'
输出:程序退出!

4.3 核心逻辑:switch 或 if-else

复制代码
// 方式1:使用 if-else
if (op == '+') {
    printf("%.2lf\n", num1 + num2);
} else if (op == '-') {
    printf("%.2lf\n", num1 - num2);
} else if (op == '*') {
    printf("%.2lf\n", num1 * num2);
} else if (op == '/') {
    // 检查除数是否为0
    if (num2 != 0)
        printf("%.2lf\n", num1 / num2);
    else
        printf("Error: Division by zero!\n");
}

// 方式2:使用 switch-case
switch (op) {
    case '+':
        printf("%.2lf\n", num1 + num2);
        break;
    case '-':
        printf("%.2lf\n", num1 - num2);
        break;
    case '*':
        printf("%.2lf\n", num1 * num2);
        break;
    case '/':
        if (num2 != 0)
            printf("%.2lf\n", num1 / num2);
        else
            printf("Error: Division by zero!\n");
        break;
    default:
        printf("无效运算符!\n");
}

附录一:结构体完整示例代码

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

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

int main()
{
    // 1. 定义并初始化
    struct student stu = {1001, "张三", 65.5};
    printf("%d\n", stu.id);
    printf("%s\n", stu.name);
    printf("%0.2f\n", stu.score);
    
    // 2. 修改成员
    stu.id = 114514;
    printf("%d\n", stu.id);
    
    printf("-----------------------------------------------\n");
    
    // 3. 结构体指针
    struct student* p = &stu;
    p->id = 100;
    printf("%d\n", p->id);
    
    printf("-----------------------------------------------\n");
    
    // 4. 动态内存分配
    struct student* p2 = (struct student*)malloc(sizeof(struct student));
    p2->id = 1919810;
    strcpy(p2->name, "网媒");
    printf("%d\n", p2->id);
    printf("%s\n", p2->name);
    
    free(p2);
    
    return 0;
}
相关推荐
晚风醉蝶1 小时前
1-18-基数排序-RadixSort
python·算法·排序算法·基数排序
lch2011_yb1 小时前
CSP-J 2022 上升点列 题解
算法·深度优先
lvts_cs1 小时前
化工产业规划对城市发展的具体指导作用
算法·动态规划
AI服务老曹2 小时前
水位识别算法接入AI视频分析平台全流程与误报优化指南(附接口字段与排查表)
人工智能·算法·音视频
江畔柳前堤2 小时前
消息队列:从直觉到精通的完整认知地图
开发语言·人工智能·算法·机器学习·ruby on rails·scala
ai产品老杨2 小时前
明火识别算法接入AI视频分析平台全流程与误报优化指南(附接口字段与排查表)
人工智能·算法·音视频
讨厌吃蛋黄酥2 小时前
📚408数据结构|最短路径算法一篇彻底搞懂!BFS、Dijkstra、Floyd别再傻傻分不清了
算法
charlie1145141912 小时前
deque、list 与 forward_list:vector 之外的三个选择
开发语言·数据结构·c++·list·开源项目
动词ing3 小时前
【学习笔记】数据结构(数组滑动窗口+无重复字符的最长字串题目)
数据结构·笔记·学习