第一部分:C语言结构体(Struct)
1.1 什么是结构体
⭐ 老师强调 :结构体是C语言对基本数据类型的重要补充。就像用
int定义整数、float定义浮点数一样,结构体允许我们将不同类型的数据组合成一个新的复合数据类型 。比如可以把int、float、char[]等多种类型打包在一起,形成自定义的数据结构。
结构体 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;
}