零基础也能学会:C语言、C++、MySQL 完整学习指南(100道经典题+答案解析+机器学习路线)
零基础也能学会:C语言、C++、MySQL 完整学习指南(100道经典题+答案解析+机器学习路线)
使用说明:这篇文档怎么学
这篇文档专门写给零基础的人,不讲虚的学习感悟,只讲三件事:知识点(从基础到高级,一步步来)、最容易犯错的地方和最容易忽略的细节(每条都带错误写法对照)、经典练习题(先看题、自己动手做,最后统一对照答案解析)。每一篇都按“知识点 → 易错点 → 练习题 → 答案解析”四段组织,和 Python100天 的刷题节奏一致。
学习方法建议:先通读知识点,再背熟易错点清单;然后合上答案做练习题,做完再打开答案解析逐题对照;最后按文末的学习路线继续往前走(含机器学习方向)。代码一定要自己敲一遍,光看不会。
第一篇 C语言:从零到能写小项目
一、核心知识点(基础阶段)
1. 环境准备与第一个程序
零基础第一步:装一个能编译C语言的工具。Windows推荐 Dev-C++ 或 VS Code + MinGW,macOS/Linux 直接装 gcc。第一个程序,记住它的固定结构:
#include <stdio.h> // 标准输入输出头文件
int main() // 主函数,程序从这里开始执行
{
printf("Hello World\n"); // 输出一行文字
return 0; // 返回0表示程序正常结束
}
固定结构三件事:① #include 引入头文件;② 必须有 main 主函数;③ 每条语句以分号 ; 结尾。任何一个漏了,程序都跑不起来。
2. 数据类型与变量
| 类型 | 关键字 | 存储内容 | 示例 |
|---|---|---|---|
| 整型 | int | 整数 | int age = 18; |
| 浮点型 | float / double | 小数 | float score = 88.5; |
| 字符型 | char | 单个字符 | char ch = 'A'; |
| 无符号整型 | unsigned int | 非负整数 | unsigned int n = 100; |
变量规则:先定义、再使用;变量名只能用字母、数字、下划线,且不能以数字开头;不能用关键字当变量名(如 int、for)。
3. 运算符
-
算术运算符:
+ - * / %(% 是取余数,只能用于整数) -
关系运算符:
> < >= <= == !=(结果是1真或0假) -
逻辑运算符:
&& || !(并且、或者、非) -
赋值运算符:
= += -= *= /= -
自增自减:
++ --(i++先用后加,++i先加后用)
4. 三大流程控制结构
顺序结构:代码从上到下逐行执行,没有分支没有循环。
分支结构:满足条件才执行对应代码。
if (score >= 60) {
printf("及格");
} else if (score >= 85) {
printf("优秀");
} else {
printf("不及格");
}
// switch 多分支:适合判断离散的固定值
switch (grade) {
case 'A': printf("优秀"); break;
case 'B': printf("良好"); break;
default: printf("未知"); break;
}
循环结构:重复执行一段代码。
// for:知道循环次数时用
for (int i = 1; i <= 10; i++) {
printf("%d ", i);
}
// while:不知道次数、只看条件时用
int n = 1;
while (n <= 10) {
printf("%d ", n);
n++;
}
// do-while:至少执行一次
int m = 1;
do {
printf("%d ", m);
m++;
} while (m <= 10);
break 立即跳出整个循环;continue 跳过本次循环剩下的代码,直接进入下一次。
5. 数组
数组是一组相同类型数据的集合,下标从 0 开始。
int a[5] = {10, 20, 30, 40, 50}; // 一维数组,长度5
a[0] = 99; // 修改第1个元素
printf("%d", a[4]); // 输出第5个元素:50
int b[3][4]; // 二维数组:3行4列
// 遍历二维数组
for (int i = 0; i < 3; i++)
for (int j = 0; j < 4; j++)
b[i][j] = i + j;
易忽略:长度为 n 的数组,合法下标是 0 到 n-1,写 a[n] 就是越界,程序可能崩溃或出现诡异结果。
6. 字符串
C语言没有专门的字符串类型,用 char 数组 存,并且末尾隐藏一个结束符 '\0'。
char str[20] = "hello"; // 实际存了 h e l l o \0 六个字符
char name[] = "张三"; // 长度自动按内容算
printf("%s", str); // %s 输出字符串
// 常用字符串函数(需要 #include <string.h>)
strlen(str); // 求长度(不含\0)
strcpy(a, b); // 把b拷贝到a
strcat(a, b); // 把b拼接到a末尾
strcmp(a, b); // 比较a、b大小,0表示相等
7. 函数
函数把一段功能封装起来,可以反复调用。结构:返回值类型 + 函数名 + 参数 + 函数体。
// 定义:返回两数之和
int add(int x, int y) {
return x + y;
}
int main() {
int sum = add(3, 5); // 调用
printf("%d", sum); // 输出8
return 0;
}
关键点:普通值传递是复制一份数值,函数里改参数不会影响外面的变量;要修改外面的变量,必须传指针(见下面高级部分)。
二、核心知识点(高级阶段)
1. 指针:C语言的灵魂
指针就是内存地址。内存像一排带编号的格子,变量住在格子里,指针存的是格子的编号。
int x = 10;
int *p = &x; // & 取x的地址,p保存x的地址
printf("%d", *p); // * 通过地址取里面的值,输出10
*p = 20; // 通过指针修改x的值
printf("%d", x); // 输出20
// 指针与数组:数组名就是首元素地址
int a[5] = {1,2,3,4,5};
int *q = a; // 等价于 int *q = &a[0]
printf("%d", *(q+2)); // 输出3,q+2跳到第3个元素
指针步长:指针加1,不是地址加1,而是加一个“它指向类型的大小”。int 指针 +1 移动4字节。
指针与函数:传指针就能修改函数外面的变量。
void swap(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
2. 结构体与共用体
结构体把不同类型的数据打包成一个整体,用来描述一个完整的事物(比如一个学生有学号、姓名、成绩)。
struct Student {
int id; // 学号
char name[20]; // 姓名
float score; // 成绩
};
struct Student s1 = {101, "张三", 88.5};
printf("%d %s %.1f", s1.id, s1.name, s1.score);
struct Student *ps = &s1; // 结构体指针
printf("%s", ps->name); // 用 -> 访问成员
共用体 union:多个成员共享同一块内存,同一时刻只保存一个成员的值,大小取最大成员的大小。
3. 动态内存:malloc 与 free
静态数组长度写死了,不够灵活。动态内存可以在程序运行期间按需要申请内存,用完手动释放。
#include <stdlib.h>
int *p = (int*)malloc(10 * sizeof(int)); // 申请10个int的空间
if (p == NULL) return -1; // 必须判空
p[0] = 100; // 当数组用
free(p); // 用完必须释放
p = NULL; // 释放后置空,防止野指针
三个函数:malloc 申请但不初始化;calloc 申请并清零;realloc 调整已申请内存的大小。所有动态内存都必须 free,否则内存泄漏。
4. 文件操作
文件操作把数据持久保存到硬盘,程序关闭后数据不消失。固定四步:打开 → 读写 → 关闭。
FILE *fp = fopen("data.txt", "w"); // w写 r读 a追加
if (fp == NULL) return -1; // 打开失败要处理
fprintf(fp, "%d %s", 101, "张三"); // 写入
fclose(fp); // 必须关闭
// 读文件
fp = fopen("data.txt", "r");
int id; char name[20];
fscanf(fp, "%d %s", &id, name); // 读入
fclose(fp);
5. 链表:动态数据结构入门
链表由一个个节点串起来,每个节点存数据和指向下一个节点的指针。它解决了数组“插入删除要搬动大量元素”的问题。
struct Node {
int data;
struct Node *next; // 指向下一个节点
};
// 头插法创建链表
struct Node* create(int arr[], int n) {
struct Node *head = NULL;
for (int i = 0; i < n; i++) {
struct Node *p = (struct Node*)malloc(sizeof(struct Node));
p->data = arr[i];
p->next = head;
head = p;
}
return head;
}
三、高频易错点&容易忽略的细节(C语言重点,建议背下来)
这一节是全文最重要的部分。下面每一条都是新手真正会踩的坑,全部带“错误写法 → 正确写法 → 原因”。
| 编号 | 错误写法(×) | 正确写法(√)与原因 |
|---|---|---|
| 1 | if (a = 10) |
if (a == 10)。= 是赋值,== 才是判断相等。写成赋值会把10塞给a,条件永远成立。 |
| 2 | printf("hi") |
每条语句末尾必须加分号 printf("hi");。漏分号是编译报错第一名。 |
| 3 | char s[3] = "abc"; |
char s[4] = "abc";。字符串末尾隐藏 '\0',数组长度必须比字符数多1。 |
| 4 | int a[5]; a[5] = 1; |
长度5的数组下标是0~4,a[5]越界,会改到别人的内存,程序可能崩溃。 |
| 5 | scanf("%d", a); |
scanf("%d", &a);。scanf要的是变量的地址,不是变量本身。字符串(char数组名)本身就是地址,不用加&。 |
| 6 | case 'A': printf("优"); |
每个 case 结尾要 break;,否则会“穿透”继续执行后面的 case,逻辑全乱。 |
| 7 | if (a > 5); printf("大"); |
if 后面不能加分号。分号会让 if 直接结束,后面的 printf 变成无条件执行。 |
| 8 | float avg = sum / 10; |
float avg = sum / 10.0;。两个整数相除结果还是整数(小数被截断),要有一个数是浮点数才出小数。 |
| 9 | int *p; *p = 10; |
p 没指向任何有效地址就使用,是野指针。先 p = &x; 或 p = malloc(...) 再使用。 |
| 10 | malloc(...); free(p); |
malloc 后必须判断 if (p == NULL);free 后要 p = NULL;。不判空可能崩溃,不释放会内存泄漏。 |
| 11 | void f(int x){ x = 100; } |
值传递改不了实参。要改外面的变量必须传指针 void f(int *x){ *x = 100; }。 |
| 12 | fopen 后忘记 fclose | 文件不关闭,写入的数据可能没真正落盘,还会占用文件句柄。打开必关闭。 |
| 13 | if (f == 0.1) |
浮点数在内存里有精度误差,不能直接用==比较。应比较差值:if (fabs(f - 0.1) < 1e-6)。 |
| 14 | 混淆 sizeof 和 strlen | sizeof("abc") 是4(含\0),strlen("abc") 是3(不含\0)。sizeof是编译期算的,strlen是运行时数的。 |
| 15 | int a[5]; a++; |
数组名是地址常量,不能自增。要用指针 int *p = a; p++;。 |
| 16 | char *p = "hello"; p[0]='H'; |
字符串字面量存在只读区,不能修改。要改就定义成字符数组 char p[] = "hello";。 |
| 17 | 二维数组传参写 void f(int a[][]) |
必须写清第二维:void f(int a[][4]),因为编译器要按行算地址。 |
| 18 | gets(str) 读取输入 | gets 不检查缓冲区长度,容易栈溢出,已被废弃。用 fgets(str, sizeof(str), stdin);。 |
| 19 | switch 里定义变量 | case 里直接定义变量会编译报错,要用花括号包起来:case 1: { int x; ... }。 |
| 20 | return 0; 写在循环里当结束 | return 是结束整个函数,break 才是跳出循环。用错会导致程序提前退出。 |
另外两个高频坑:忘记处理 scanf 返回值(输入类型不匹配会导致变量没被赋值);char 和 int 混用(char 本质是整数,'A' 的 ASCII 是65)。
四、C语言100道经典题(先做题,答案在下一节)
按“基础语法 → 数组与字符串 → 函数 → 指针 → 结构体与动态内存 → 文件操作”6个模块,共100题。前面已讲的知识点足够支撑你做出这些题,遇到不会的先翻上文再动手。
模块一 基础语法(第1–20题)
-
输出一行文字:Hello World。
-
定义变量存自己的姓名、年龄、学号并输出。
-
输入两个整数,输出它们的和、差、积、商(保留两位小数)。
-
输入3个整数,输出最大值和最小值。
-
输入一个整数,判断它是奇数还是偶数。
-
输入一个年份,判断是不是闰年(能被4整除且不能被100整除,或能被400整除)。
-
交换两个变量的值(要求不使用第三个变量)。
-
输入一个三位数,输出它的百位、十位、个位数字。
-
求 1+2+3+...+100 的和。
-
求 1~100 之间所有偶数的和。
-
输出 1~100 之间所有能被3整除的数,并统计个数。
-
输出九九乘法表。
-
输入一个百分制成绩,输出等级:90以上A、80以上B、70以上C、60以上D、60以下E。
-
输入年月,输出这个月有多少天(要考虑闰年2月)。
-
输入n,求 n 的阶乘(用循环,n≤20)。
-
输出斐波那契数列前20项(1,1,2,3,5,...)。
-
输入两个正整数,用辗转相除法求最大公约数。
-
输入两个正整数,求最小公倍数。
-
输入一个整数,判断它是不是回文数(如12321)。
-
输出100~999之间所有的水仙花数(各位数字立方和等于本身)。
模块二 数组与字符串(第21–45题)
-
输入10个整数存入数组,逆序输出。
-
输入10个整数,求最大值、最小值、平均值。
-
用冒泡排序把10个整数从小到大排序。
-
用选择排序把10个整数从小到大排序。
-
在有序数组中插入一个数,保持有序。
-
用二分法在有序数组中查找某个数,找到输出下标,找不到输出-1。
-
去掉数组中重复的元素(只保留第一个出现的)。
-
把数组元素循环右移n位。
-
合并两个升序数组为一个升序数组。
-
输入一个3×3矩阵,输出它的转置矩阵。
-
求一个3×3矩阵两条对角线元素之和。
-
输出杨辉三角前10行。
-
不调用库函数,求字符串长度。
-
不调用库函数,实现字符串拷贝。
-
不调用库函数,实现字符串拼接。
-
不调用库函数,实现字符串比较。
-
不调用库函数,实现字符串反转。
-
统计一段字符串中字母、数字、空格、其他字符各有多少个。
-
删除字符串中指定的字符。
-
判断一个字符串是不是回文串(正读反读一样)。
-
把字符串中的小写字母转大写,大写转小写。
-
统计一句话里有多少个单词(单词间用空格分隔)。
-
查找子串在主串中第一次出现的位置。
-
去掉字符串首尾的空格。
-
输入n个学生的姓名,按字典序排序输出。
模块三 函数(第46–60题)
-
写函数用指针交换两个整数。
-
写函数求 n 的阶乘(递归实现)。
-
写函数判断一个数是不是素数。
-
写递归函数求斐波那契数列第n项。
-
写递归函数求 1+2+...+n。
-
写递归函数求最大公约数。
-
写汉诺塔问题:把n个盘子从A移到C,输出移动步骤。
-
写函数求字符串长度、把字符串拷贝(封装前面模块二的实现)。
-
写函数对数组做冒泡排序。
-
写函数求数组元素之和。
-
用静态局部变量统计函数被调用的次数。
-
写函数,通过指针参数一次性返回一个数的商和余数。
-
写函数计算组合数 C(n,m)。
-
用牛顿迭代法求一个数的平方根(误差1e-6)。
-
写函数输出1~n之间的所有素数(调用判断素数函数)。
模块四 指针(第61–80题)
-
定义变量并输出它的值和地址。
-
用指针交换两个整数。
-
用指针遍历数组并输出每个元素。
-
用指针把数组逆序。
-
用指针实现字符串反转。
-
用指针实现字符串拷贝。
-
用指针数组保存多个字符串,并按字典序排序。
-
定义函数指针,调用加法、减法函数。
-
用行指针遍历二维数组。
-
说出并演示指针数组和数组指针的区别。
-
判断并演示什么是空指针、野指针、悬垂指针。
-
演示 const int *p、int *const p、const int *const p 三种写法的区别。
-
写排序函数,参数接收函数指针(实现升序/降序切换)。
-
用二级指针修改一级指针的指向。
-
写函数用二级指针在函数内给指针分配内存。
-
用malloc动态创建数组,输入n个成绩并求和。
-
用malloc创建动态二维数组(矩阵),输入并输出。
-
动态创建学生成绩数组,求平均分并输出不及格的学生。
-
演示并解释内存泄漏:申请后不释放,观察后果。
-
用指针偏移访问结构体数组的每个成员。
模块五 结构体与动态内存(第81–90题)
-
定义学生结构体(学号、姓名、成绩),输入3个学生并输出。
-
把学生结构体数组按成绩从高到低排序。
-
用结构体指针访问并修改学生信息。
-
定义结构体嵌套:班级结构体里包含学生结构体数组。
-
对比结构体传值和传指针,说明哪个效率高、为什么。
-
用malloc动态创建学生结构体数组。
-
定义链表节点结构体,创建5个节点并连成链表。
-
用头插法创建链表,输出所有节点。
-
遍历链表,统计节点个数。
-
删除链表中指定值的节点。
模块六 文件操作(第91–100题)
-
把一段文字写入文本文件。
-
读取文本文件内容并输出到屏幕。
-
复制一个文件到另一个文件。
-
统计一个文件里的字符个数。
-
向文件末尾追加一行内容(不覆盖原有内容)。
-
读取文件里的若干整数,求它们的和。
-
把3个学生信息写入文件。
-
从文件读回学生信息并打印。
-
把文件内容按行倒序输出。
-
用二进制方式写入一个整数数组,再读出来。
五、C语言100道题答案解析(先做题再看这里)
所有代码都完整可运行。建议每题先自己写,写不出来再看答案,对照解析理解思路。
模块一 基础语法答案(第1–20题)
【第1题】输出Hello World
#include <stdio.h>
int main() { printf("Hello World\n"); return 0; }
解析:固定结构:头文件 + main函数 + 分号。printf的\n是换行符。
【第2题】输出个人信息
#include <stdio.h>
int main() {
char name[20] = "张三";
int age = 18;
int id = 2025001;
printf("姓名:%s 年龄:%d 学号:%d\n", name, age, id);
return 0;
}
解析:%s输出字符串,%d输出整数,格式符和变量一一对应。
【第3题】四则运算
#include <stdio.h>
int main() {
int a, b;
scanf("%d%d", &a, &b);
printf("和:%d 差:%d 积:%d\n", a+b, a-b, a*b);
printf("商:%.2f\n", (double)a / b); // 转成double再除
return 0;
}
解析:要得到小数,必须先转成浮点数再除,%.2f保留两位小数。
【第4题】最大值最小值
#include <stdio.h>
int main() {
int a, b, c, max, min;
scanf("%d%d%d", &a, &b, &c);
max = min = a;
if (b > max) max = b;
if (c > max) max = c;
if (b < min) min = b;
if (c < min) min = c;
printf("最大值:%d 最小值:%d\n", max, min);
return 0;
}
解析:先假设第一个数最大最小,再逐个比较更新。
【第5题】奇偶判断
#include <stdio.h>
int main() {
int n; scanf("%d", &n);
if (n % 2 == 0) printf("偶数\n");
else printf("奇数\n");
return 0;
}
解析:对2取余,余0是偶数。注意==才是判断。
【第6题】闰年判断
#include <stdio.h>
int main() {
int y; scanf("%d", &y);
if ((y%4==0 && y%100!=0) || y%400==0)
printf("闰年\n");
else
printf("平年\n");
return 0;
}
解析:闰年规则:能被4整除且不能被100整除,或者能被400整除。
【第7题】不用第三变量交换
#include <stdio.h>
int main() {
int a = 3, b = 5;
a = a + b; // a=8
b = a - b; // b=3
a = a - b; // a=5
printf("a=%d b=%d\n", a, b);
return 0;
}
解析:用加减法把两个数“存”在a里,再分别还原。也可以用异或:a^=b; b^=a; a^=b。
【第8题】三位数拆位
#include <stdio.h>
int main() {
int n; scanf("%d", &n);
printf("百位:%d 十位:%d 个位:%d\n", n/100, n/10%10, n%10);
return 0;
}
解析:整除100得百位;先整除10再取余10得十位;取余10得个位。
【第9题】1加到100
#include <stdio.h>
int main() {
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
printf("%d\n", sum);
return 0;
}
解析:累加器sum初值为0,循环里sum += i。结果是5050。
【第10题】偶数和
#include <stdio.h>
int main() {
int sum = 0;
for (int i = 2; i <= 100; i += 2) sum += i;
printf("%d\n", sum);
return 0;
}
解析:直接从2开始每次加2,比判断%2更高效。
【第11题】能被3整除的数及个数
#include <stdio.h>
int main() {
int count = 0;
for (int i = 1; i <= 100; i++)
if (i % 3 == 0) { printf("%d ", i); count++; }
printf("\n共%d个\n", count);
return 0;
}
解析:count做计数器,每命中一次加1。
【第12题】九九乘法表
#include <stdio.h>
int main() {
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++)
printf("%d*%d=%-2d ", j, i, i*j);
printf("\n");
}
return 0;
}
解析:外层控制行,内层控制列,内层循环到i为止形成三角形,%-2d左对齐占2位。
【第13题】成绩等级
#include <stdio.h>
int main() {
int s; scanf("%d", &s);
if (s >= 90) printf("A\n");
else if (s >= 80) printf("B\n");
else if (s >= 70) printf("C\n");
else if (s >= 60) printf("D\n");
else printf("E\n");
return 0;
}
解析:else if 从上往下匹配,命中一个就结束,顺序不能乱。
【第14题】月份天数
#include <stdio.h>
int main() {
int y, m, days;
scanf("%d%d", &y, &m);
if (m == 2) {
if ((y%4==0 && y%100!=0) || y%400==0) days = 29;
else days = 28;
} else if (m==4 || m==6 || m==9 || m==11) days = 30;
else days = 31;
printf("%d\n", days);
return 0;
}
解析:2月特殊要判断闰年;4/6/9/11月30天,其余31天。
【第15题】阶乘
#include <stdio.h>
int main() {
int n; long long f = 1;
scanf("%d", &n);
for (int i = 2; i <= n; i++) f *= i;
printf("%lld\n", f);
return 0;
}
解析:n=20时20!超过int范围,用long long存结果,%lld输出。
【第16题】斐波那契数列
#include <stdio.h>
int main() {
int a = 1, b = 1;
printf("%d %d ", a, b);
for (int i = 3; i <= 20; i++) {
int c = a + b;
printf("%d ", c);
a = b; b = c;
}
printf("\n");
return 0;
}
解析:每项等于前两项之和,用三个变量滚动更新。
【第17题】最大公约数(辗转相除法)
#include <stdio.h>
int main() {
int a, b, r;
scanf("%d%d", &a, &b);
while (b != 0) { r = a % b; a = b; b = r; }
printf("%d\n", a);
return 0;
}
解析:辗转相除:大数对小数取余,反复交换,余数为0时a就是最大公约数。
【第18题】最小公倍数
#include <stdio.h>
int main() {
int a, b, x, y, r;
scanf("%d%d", &a, &b);
x = a; y = b;
while (y != 0) { r = x % y; x = y; y = r; }
printf("%d\n", a / x * b); // 两数之积 ÷ 最大公约数
return 0;
}
解析:先求最大公约数,再用公式:最小公倍数 = a*b/最大公约数。
【第19题】回文数判断
#include <stdio.h>
int main() {
int n, m, rev = 0;
scanf("%d", &n);
m = n;
while (m > 0) { rev = rev * 10 + m % 10; m /= 10; }
if (rev == n) printf("是回文数\n");
else printf("不是回文数\n");
return 0;
}
解析:把数字倒过来(每次取末位,拼到rev后面),和原数比较。
【第20题】水仙花数
#include <stdio.h>
int main() {
for (int n = 100; n <= 999; n++) {
int a = n/100, b = n/10%10, c = n%10;
if (a*a*a + b*b*b + c*c*c == n)
printf("%d ", n);
}
printf("\n");
return 0;
}
解析:拆出百十个位,判断各位立方和是否等于本身。结果是153、370、371、407。
模块二 数组与字符串答案(第21–45题)
【第21题】数组逆序输出
#include <stdio.h>
int main() {
int a[10];
for (int i = 0; i < 10; i++) scanf("%d", &a[i]);
for (int i = 9; i >= 0; i--) printf("%d ", a[i]);
printf("\n");
return 0;
}
解析:从最后一个下标9往前输出即可。
【第22题】最大值最小值平均值
#include <stdio.h>
int main() {
int a[10], max, min, sum = 0;
for (int i = 0; i < 10; i++) scanf("%d", &a[i]);
max = min = a[0];
for (int i = 1; i < 10; i++) {
if (a[i] > max) max = a[i];
if (a[i] < min) min = a[i];
sum += a[i];
}
sum += a[0];
printf("max=%d min=%d avg=%.2f\n", max, min, sum/10.0);
return 0;
}
解析:先都取第一个元素,再遍历比较;平均值注意除以10.0保留小数。
【第23题】冒泡排序
#include <stdio.h>
int main() {
int a[10] = {5,2,9,1,5,6,3,8,0,7};
for (int i = 0; i < 9; i++) // 共9轮
for (int j = 0; j < 9 - i; j++) // 每轮少比较一个
if (a[j] > a[j+1]) {
int t = a[j]; a[j] = a[j+1]; a[j+1] = t;
}
for (int i = 0; i < 10; i++) printf("%d ", a[i]);
printf("\n");
return 0;
}
解析:相邻两两比较,大的往后冒,每轮确定一个最大值,内层长度递减。
【第24题】选择排序
#include <stdio.h>
int main() {
int a[10] = {5,2,9,1,5,6,3,8,0,7};
for (int i = 0; i < 9; i++) {
int min = i;
for (int j = i+1; j < 10; j++)
if (a[j] < a[min]) min = j; // 找最小下标
int t = a[i]; a[i] = a[min]; a[min] = t; // 交换到前面
}
for (int i = 0; i < 10; i++) printf("%d ", a[i]);
printf("\n");
return 0;
}
解析:每轮找剩余部分的最小值,放到前面,比冒泡交换次数少。
【第25题】有序数组插入
#include <stdio.h>
int main() {
int a[11] = {1,3,5,7,9,11,13,15,17,19};
int x, i;
scanf("%d", &x);
for (i = 9; i >= 0 && a[i] > x; i--)
a[i+1] = a[i]; // 大于x的元素往后挪
a[i+1] = x; // 插入到腾出的位置
for (i = 0; i < 11; i++) printf("%d ", a[i]);
printf("\n");
return 0;
}
解析:从后往前找插入点,边找边把元素后移,最后插入。
【第26题】二分查找
#include <stdio.h>
int main() {
int a[10] = {1,3,5,7,9,11,13,15,17,19};
int x, low = 0, high = 9, mid;
scanf("%d", &x);
while (low <= high) {
mid = (low + high) / 2;
if (a[mid] == x) { printf("下标:%d\n", mid); return 0; }
else if (a[mid] < x) low = mid + 1;
else high = mid - 1;
}
printf("-1\n");
return 0;
}
解析:前提是有序。每次砍掉一半,log2(n)次就能找到,比顺序查找快得多。
【第27题】数组去重
#include <stdio.h>
int main() {
int a[10] = {1,2,2,3,3,3,4,5,5,6};
int n = 10, k = 0;
for (int i = 0; i < n; i++) {
int dup = 0;
for (int j = 0; j < k; j++)
if (a[j] == a[i]) { dup = 1; break; }
if (!dup) a[k++] = a[i]; // 不重复就保留到前面
}
for (int i = 0; i < k; i++) printf("%d ", a[i]);
printf("\n");
return 0;
}
解析:新数组里逐个查重,没出现过的往前放,k记录有效长度。
【第28题】数组循环右移n位
#include <stdio.h>
int main() {
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int n = 3, temp[10];
for (int i = 0; i < 10; i++)
temp[(i + n) % 10] = a[i]; // 每个元素搬到新位置
for (int i = 0; i < 10; i++) { a[i] = temp[i]; printf("%d ", a[i]); }
printf("\n");
return 0;
}
解析:借助临时数组,下标(i+n)%10实现循环搬移。也可以三次反转实现原地右移。
【第29题】合并两个升序数组
#include <stdio.h>
int main() {
int a[5] = {1,3,5,7,9}, b[5] = {2,4,6,8,10}, c[10];
int i = 0, j = 0, k = 0;
while (i < 5 && j < 5) {
if (a[i] < b[j]) c[k++] = a[i++];
else c[k++] = b[j++];
}
while (i < 5) c[k++] = a[i++]; // 剩余部分
while (j < 5) c[k++] = b[j++];
for (i = 0; i < 10; i++) printf("%d ", c[i]);
printf("\n");
return 0;
}
解析:双指针归并:谁小谁进结果,最后把剩余的接上。
【第30题】矩阵转置
#include <stdio.h>
int main() {
int a[3][3] = {{1,2,3},{4,5,6},{7,8,9}}, t[3][3];
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
t[j][i] = a[i][j]; // 行列互换
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) printf("%d ", t[i][j]);
printf("\n");
}
return 0;
}
解析:转置就是把 a[i][j] 放到 t[j][i],行变列、列变行。
【第31题】对角线之和
#include <stdio.h>
int main() {
int a[3][3] = {{1,2,3},{4,5,6},{7,8,9}}, sum = 0;
for (int i = 0; i < 3; i++) {
sum += a[i][i]; // 主对角线
sum += a[i][2-i]; // 副对角线
}
sum -= a[1][1]; // 中心元素加了两次,减一次
printf("%d\n", sum);
return 0;
}
解析:主对角线下标(i,i),副对角线下标(i,2-i),3×3时中心算了两遍要减掉。
【第32题】杨辉三角
#include <stdio.h>
int main() {
int a[10][10] = {0};
for (int i = 0; i < 10; i++) {
a[i][0] = a[i][i] = 1; // 两边都是1
for (int j = 1; j < i; j++)
a[i][j] = a[i-1][j-1] + a[i-1][j]; // 肩上两数之和
}
for (int i = 0; i < 10; i++) {
for (int j = 0; j <= i; j++) printf("%4d", a[i][j]);
printf("\n");
}
return 0;
}
解析:每个数等于它左上方和右上方两数之和,两边补1。
【第33题】字符串长度
#include <stdio.h>
int myStrlen(char *s) {
int len = 0;
while (*s != '\0') { len++; s++; }
return len;
}
解析:指针从头走,遇到结束符\0停下,走过的步数就是长度。
【第34题】字符串拷贝
#include <stdio.h>
void myStrcpy(char *dest, char *src) {
while (*src) *dest++ = *src++;
*dest = '\0'; // 别忘了补结束符
}
解析:逐字符搬移,最后手动补\0,否则打印会越界。
【第35题】字符串拼接
#include <stdio.h>
void myStrcat(char *dest, char *src) {
while (*dest) dest++; // 先走到dest末尾
while (*src) *dest++ = *src++; // 再把src接上去
*dest = '\0';
}
解析:先让指针走到第一个字符串末尾,再把第二个字符串接上。
【第36题】字符串比较
#include <stdio.h>
int myStrcmp(char *a, char *b) {
while (*a && *b && *a == *b) { a++; b++; }
return *a - *b; // 0相等,正数a大,负数b大
}
解析:逐字符比较,遇到不同或结束就返回差值。
【第37题】字符串反转
#include <stdio.h>
void reverse(char *s) {
char *end = s;
while (*end) end++; // end指向末尾\0
end--;
while (s < end) {
char t = *s; *s = *end; *end = t;
s++; end--;
}
}
解析:首尾双指针向中间靠拢,交换对应字符。
【第38题】字符分类统计
#include <stdio.h>
int main() {
char s[100] = "Hello 123 !@#";
int letter = 0, digit = 0, space = 0, other = 0;
for (int i = 0; s[i]; i++) {
if ((s[i]>='a'&&s[i]<='z')||(s[i]>='A'&&s[i]<='Z')) letter++;
else if (s[i]>='0' && s[i]<='9') digit++;
else if (s[i]==' ') space++;
else other++;
}
printf("字母%d 数字%d 空格%d 其他%d\n", letter, digit, space, other);
return 0;
}
解析:用ASCII范围判断类型,四个计数器分别累加。
【第39题】删除指定字符
#include <stdio.h>
void delChar(char *s, char c) {
int j = 0;
for (int i = 0; s[i]; i++)
if (s[i] != c) s[j++] = s[i]; // 不等的往前挪
s[j] = '\0';
}
解析:双下标,i负责遍历,j负责写保留的字符,最后截断。
【第40题】回文串判断
#include <stdio.h>
#include <string.h>
int main() {
char s[100];
gets(s); // 练习时可用,实际项目用fgets
int i = 0, j = strlen(s) - 1, ok = 1;
while (i < j) {
if (s[i] != s[j]) { ok = 0; break; }
i++; j--;
}
printf(ok ? "是回文\n" : "不是回文\n");
return 0;
}
解析:首尾字符逐个比较,全部相等就是回文串。
【第41题】大小写转换
#include <stdio.h>
int main() {
char s[100] = "Hello World";
for (int i = 0; s[i]; i++) {
if (s[i]>='a' && s[i]<='z') s[i] -= 32; // 小写转大写
else if (s[i]>='A' && s[i]<='Z') s[i] += 32; // 大写转小写
}
printf("%s\n", s);
return 0;
}
解析:ASCII中大写和小写相差32,加32或减32即可互换。
【第42题】单词计数
#include <stdio.h>
int main() {
char s[200] = "I love C language";
int count = 0, inWord = 0;
for (int i = 0; s[i]; i++) {
if (s[i] == ' ') inWord = 0;
else if (!inWord) { inWord = 1; count++; } // 进入新单词
}
printf("%d\n", count);
return 0;
}
解析:用标志位记录是否在单词里,从空格进入字母时计数加1。
【第43题】查找子串位置
#include <stdio.h>
#include <string.h>
int main() {
char s[100] = "hello world", sub[20] = "world";
int n = strlen(s), m = strlen(sub);
for (int i = 0; i <= n - m; i++) {
int j = 0;
while (j < m && s[i+j] == sub[j]) j++;
if (j == m) { printf("位置:%d\n", i); return 0; }
}
printf("-1\n");
return 0;
}
解析:暴力匹配:从每个位置开始逐字符比对子串。
【第44题】去掉首尾空格
#include <stdio.h>
#include <string.h>
void trim(char *s) {
int i = 0, len = strlen(s);
while (s[i] == ' ') i++; // 跳过开头空格
int j = len - 1;
while (j >= i && s[j] == ' ') j--; // 跳过结尾空格
int k = 0;
while (i <= j) s[k++] = s[i++];
s[k] = '\0';
}
解析:分别找到第一个和最后一个非空格位置,把中间部分前移。
【第45题】姓名按字典序排序
#include <stdio.h>
#include <string.h>
int main() {
char name[5][20] = {"Tom","Alice","Bob","David","Cathy"};
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4 - i; j++)
if (strcmp(name[j], name[j+1]) > 0) {
char t[20]; strcpy(t, name[j]);
strcpy(name[j], name[j+1]);
strcpy(name[j+1], t);
}
for (int i = 0; i < 5; i++) printf("%s\n", name[i]);
return 0;
}
解析:二维字符数组存名字,用strcmp比较、strcpy交换,套冒泡排序。
模块三 函数答案(第46–60题)
【第46题】指针交换
void swap(int *a, int *b) {
int t = *a; *a = *b; *b = t;
}
解析:传地址进去,通过解引用直接操作原变量内存,才能真的交换。
【第47题】递归求阶乘
long long fact(int n) {
if (n <= 1) return 1; // 递归出口
return n * fact(n - 1); // 递归调用
}
解析:递归两要素:出口(n=1时返回1)+ 递推式(n * fact(n-1))。
【第48题】判断素数
#include <math.h>
int isPrime(int n) {
if (n < 2) return 0;
for (int i = 2; i * i <= n; i++) // 只试到根号n
if (n % i == 0) return 0;
return 1;
}
解析:一个数若有因子,必有一个不超过它的平方根,所以只需试到根号n。
【第49题】递归求斐波那契第n项
int fib(int n) {
if (n <= 2) return 1;
return fib(n-1) + fib(n-2);
}
解析:前两项是1,后面的等于前两项之和。注意n大了会重复计算,效率低。
【第50题】递归求和
int sum(int n) {
if (n == 1) return 1;
return n + sum(n - 1);
}
解析:把“1加到n”拆成“n + (1加到n-1)”。
【第51题】递归求最大公约数
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
解析:辗转相除法的递归写法:gcd(a,b)=gcd(b,a%b),直到b为0。
【第52题】汉诺塔
#include <stdio.h>
void hanoi(int n, char A, char B, char C) {
if (n == 1) { printf("%c->%c\n", A, C); return; }
hanoi(n-1, A, C, B); // n-1个盘子A移到B,C当辅助
printf("%c->%c\n", A, C); // 最底下一个移到C
hanoi(n-1, B, A, C); // n-1个盘子B移到C,A当辅助
}
解析:三步法:上面n-1个先移走→最下面移到目标→n-1个再移上来。递归最经典的例子。
【第53题】函数封装字符串长度与拷贝
int myStrlen(char *s) {
int n = 0;
while (s[n]) n++;
return n;
}
void myStrcpy(char *d, char *s) {
int i = 0;
while ((d[i] = s[i]) != '\0') i++;
}
解析:把模块二的实现包成函数,体现“封装+复用”的思想。
【第54题】函数版冒泡排序
void sort(int a[], int n) {
for (int i = 0; i < n-1; i++)
for (int j = 0; j < n-1-i; j++)
if (a[j] > a[j+1]) {
int t = a[j]; a[j] = a[j+1]; a[j+1] = t;
}
}
解析:数组传参实际传的是首地址,函数里改的就是原数组。
【第55题】数组求和函数
int sumArray(int a[], int n) {
int s = 0;
for (int i = 0; i < n; i++) s += a[i];
return s;
}
解析:遍历累加,返回总和。注意数组长度要作为参数传入。
【第56题】静态变量统计调用次数
#include <stdio.h>
void call() {
static int count = 0; // 静态变量只初始化一次,生命周期贯穿整个程序
count++;
printf("第%d次调用\n", count);
}
解析:普通局部变量每次调用重新创建;static只创建一次,适合做计数器。
【第57题】一次返回商和余数
void divmod(int a, int b, int *q, int *r) {
*q = a / b;
*r = a % b;
}
解析:一个函数只能return一个值,要返回多个就用指针参数带出来。
【第58题】组合数 C(n,m)
long long C(int n, int m) {
if (m == 0 || m == n) return 1;
return C(n-1, m-1) + C(n-1, m); // 帕斯卡恒等式
}
解析:C(n,m)=C(n-1,m-1)+C(n-1,m),边界是m为0或n时等于1。
【第59题】牛顿迭代法求平方根
#include <math.h>
double sqrt_newton(double x) {
double g = x;
while (fabs(g*g - x) > 1e-6)
g = (g + x/g) / 2; // 迭代公式
return g;
}
解析:猜测值g不断用公式 (g + x/g)/2 逼近真实平方根,直到误差足够小。
【第60题】输出1~n的素数
#include <stdio.h>
int isPrime(int n) {
if (n < 2) return 0;
for (int i = 2; i * i <= n; i++)
if (n % i == 0) return 0;
return 1;
}
int main() {
int n; scanf("%d", &n);
for (int i = 2; i <= n; i++)
if (isPrime(i)) printf("%d ", i);
printf("\n");
return 0;
}
解析:复用第48题的判断函数,主函数只管遍历和输出,体现模块化。
模块四 指针答案(第61–80题)
【第61题】输出值和地址
#include <stdio.h>
int main() {
int x = 10;
int *p = &x;
printf("值:%d 地址:%p\n", x, (void*)p);
return 0;
}
解析:&取地址,%p按十六进制输出地址。
【第62题】指针交换
void swap(int *a, int *b) {
int t = *a; *a = *b; *b = t;
}
解析:和第46题一样,传地址修改实参。
【第63题】指针遍历数组
#include <stdio.h>
int main() {
int a[5] = {10,20,30,40,50};
for (int *p = a; p < a+5; p++)
printf("%d ", *p);
printf("\n");
return 0;
}
解析:指针从数组头走到尾,每步+1跳过一个int。
【第64题】指针逆序数组
#include <stdio.h>
int main() {
int a[5] = {1,2,3,4,5};
int *s = a, *e = a+4;
while (s < e) {
int t = *s; *s = *e; *e = t;
s++; e--;
}
for (int i = 0; i < 5; i++) printf("%d ", a[i]);
printf("\n");
return 0;
}
解析:首尾双指针交换,和字符串反转同思路。
【第65题】指针实现字符串反转
#include <stdio.h>
void reverse(char *s) {
char *e = s;
while (*e) e++;
e--;
while (s < e) {
char t = *s; *s = *e; *e = t;
s++; e--;
}
}
解析:先让尾指针走到\0前,再首尾交换向中间收拢。
【第66题】指针实现字符串拷贝
void myStrcpy(char *d, char *s) {
while ((*d++ = *s++) != '\0') ;
}
解析:边赋值边后移,把\0也一起拷过去,循环自然结束。
【第67题】指针数组按字典序排序
#include <stdio.h>
#include <string.h>
int main() {
char *names[] = {"Tom","Alice","Bob","David"};
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3-i; j++)
if (strcmp(names[j], names[j+1]) > 0) {
char *t = names[j];
names[j] = names[j+1];
names[j+1] = t; // 只换指针,不用搬字符串
}
for (int i = 0; i < 4; i++) printf("%s\n", names[i]);
return 0;
}
解析:指针数组里每个元素是char*,交换时只交换指针,效率高。
【第68题】函数指针
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int main() {
int (*fp)(int, int); // 函数指针声明
fp = add; // 指向add函数
printf("%d\n", fp(10, 3)); // 13
fp = sub;
printf("%d\n", fp(10, 3)); // 7
return 0;
}
解析:函数也有地址,函数指针让“把函数当参数传递”成为可能。
【第69题】行指针遍历二维数组
#include <stdio.h>
int main() {
int a[2][3] = {{1,2,3},{4,5,6}};
int (*p)[3] = a; // 行指针:指向“含3个int的一行”
for (int i = 0; i < 2; i++)
for (int j = 0; j < 3; j++)
printf("%d ", *(*(p+i)+j)); // p+i跳到第i行
printf("\n");
return 0;
}
解析:行指针int(*)[3]指向整行,*(p+i)是第i行,+j列再解引用取元素。
【第70题】指针数组 vs 数组指针
int *p[3]; // 指针数组:p是数组,里面3个int*元素
int (*p)[3]; // 数组指针:p是指针,指向含3个int的数组
// 读法:看p先跟谁结合。[ ]优先级高,p[3]是数组;(*p)说明p是指针。
解析:分清“存指针的数组”和“指向数组的指针”,写法就差一个括号。
【第71题】空指针/野指针/悬垂指针
int *p1 = NULL; // 空指针:不指向任何有效地址,判空后可安全释放
int *p2; // 野指针:未初始化,指向随机地址,危险
int *p3 = malloc(4);
free(p3); // 释放后p3成了悬垂指针,指向已回收的内存
p3 = NULL; // 释放后立刻置空,避免悬垂指针
解析:三个坑都来自指针“指错地方”,规范做法:初始化、判空、释放后置空。
【第72题】const与指针三种写法
const int *p; // 指向的内容不能改(指针可以换指向)
int *const p; // 指针本身不能改(指向的内容可以改)
const int *const p; // 两者都不能改
// 记忆:const在*左边,修饰的是*p(内容);在*右边,修饰p(指针)
解析:看const离谁近就修饰谁,这是高频面试考点。
【第73题】排序函数接收函数指针
#include <stdio.h>
int asc(int a, int b) { return a > b; }
int desc(int a, int b) { return a < b; }
void sort(int a[], int n, int (*cmp)(int,int)) {
for (int i = 0; i < n-1; i++)
for (int j = 0; j < n-1-i; j++)
if (cmp(a[j], a[j+1])) {
int t = a[j]; a[j] = a[j+1]; a[j+1] = t;
}
}
int main() {
int a[] = {3,1,4,1,5,9,2,6};
sort(a, 8, asc); // 升序
sort(a, 8, desc); // 降序
return 0;
}
解析:把比较规则作为函数指针传入,同一个排序函数能实现升序降序。
【第74题】二级指针修改一级指针指向
#include <stdio.h>
void change(int **pp, int *target) {
*pp = target; // 通过二级指针修改一级指针的指向
}
int main() {
int a = 1, b = 2, *p = &a;
change(&p, &b); // 传一级指针的地址
printf("%d\n", *p); // 输出2
return 0;
}
解析:要修改“指针变量本身”,必须传它的地址,即二级指针。
【第75题】函数内用二级指针分配内存
#include <stdio.h>
#include <stdlib.h>
void alloc(int **pp, int n) {
*pp = (int*)malloc(n * sizeof(int));
}
int main() {
int *p = NULL;
alloc(&p, 5); // 函数内给p分配内存
if (p) { p[0] = 100; printf("%d\n", p[0]); }
free(p);
return 0;
}
解析:malloc必须配合二级指针才能把新地址带回调用方。
【第76题】malloc动态数组求和
#include <stdio.h>
#include <stdlib.h>
int main() {
int n, sum = 0;
scanf("%d", &n);
int *a = (int*)malloc(n * sizeof(int));
if (a == NULL) return -1;
for (int i = 0; i < n; i++) { scanf("%d", &a[i]); sum += a[i]; }
printf("%d\n", sum);
free(a);
return 0;
}
解析:长度n由运行期决定,malloc申请后当数组用,记得释放。
【第77题】动态二维数组
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3, cols = 4;
int **m = (int**)malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++)
m[i] = (int*)malloc(cols * sizeof(int));
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
m[i][j] = i * cols + j;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) printf("%d ", m[i][j]);
printf("\n");
}
for (int i = 0; i < rows; i++) free(m[i]);
free(m);
return 0;
}
解析:先给“行指针数组”分配内存,再给每行分配;释放顺序反过来。
【第78题】动态成绩数组求平均并输出不及格
#include <stdio.h>
#include <stdlib.h>
int main() {
int n; float sum = 0;
scanf("%d", &n);
float *s = (float*)malloc(n * sizeof(float));
for (int i = 0; i < n; i++) { scanf("%f", &s[i]); sum += s[i]; }
printf("平均:%.1f\n不及格:", sum / n);
for (int i = 0; i < n; i++)
if (s[i] < 60) printf("%.0f ", s[i]);
printf("\n");
free(s);
return 0;
}
解析:人数不确定时动态申请最合适,两道问题一次解决。
【第79题】内存泄漏演示
#include <stdlib.h>
void leak() {
int *p = (int*)malloc(100 * sizeof(int));
// 忘记free(p),函数结束p丢失,内存再也无法释放
}
// 正确做法:
void noLeak() {
int *p = (int*)malloc(100 * sizeof(int));
if (p) { /* 使用 */ free(p); p = NULL; }
}
解析:长期运行的程序内存泄漏会越积越多,最终耗尽内存。谁申请谁释放。
【第80题】指针偏移访问结构体数组
#include <stdio.h>
struct Stu { int id; char name[20]; };
int main() {
struct Stu s[3] = {{1,"A"},{2,"B"},{3,"C"}};
struct Stu *p = s; // 指向第一个结构体
for (int i = 0; i < 3; i++)
printf("%d %s\n", (p+i)->id, (p+i)->name); // 指针偏移
return 0;
}
解析:结构体指针+1跳过整个结构体大小,用->访问成员。
模块五 结构体与动态内存答案(第81–90题)
【第81题】学生结构体输入输出
#include <stdio.h>
struct Student { int id; char name[20]; float score; };
int main() {
struct Student s[3];
for (int i = 0; i < 3; i++)
scanf("%d %s %f", &s[i].id, s[i].name, &s[i].score);
for (int i = 0; i < 3; i++)
printf("%d %s %.1f\n", s[i].id, s[i].name, s[i].score);
return 0;
}
解析:name是数组名本身就是地址,scanf里不用加&,其他要加。
【第82题】结构体按成绩排序
#include <stdio.h>
struct Student { int id; char name[20]; float score; };
int main() {
struct Student s[3] = {{1,"A",70},{2,"B",90},{3,"C",80}};
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2-i; j++)
if (s[j].score < s[j+1].score) { // 按成绩降序
struct Student t = s[j];
s[j] = s[j+1]; s[j+1] = t; // 整个结构体交换
}
for (int i = 0; i < 3; i++)
printf("%d %s %.1f\n", s[i].id, s[i].name, s[i].score);
return 0;
}
解析:结构体可以整体赋值交换(C语言允许),比较时取成员score。
【第83题】结构体指针修改信息
#include <stdio.h>
struct Student { int id; char name[20]; float score; };
int main() {
struct Student s = {1, "A", 70};
struct Student *p = &s;
p->score = 95; // 通过指针改成员
strcpy(p->name, "B");
printf("%d %s %.1f\n", s.id, s.name, s.score);
return 0;
}
解析:->是“指针访问成员”的专用运算符,字符串成员要用strcpy改。
【第84题】结构体嵌套
#include <stdio.h>
struct Student { int id; char name[20]; };
struct Class {
int cid;
char cname[20];
struct Student stu[30]; // 一个班级含学生数组
};
int main() {
struct Class c1 = {101, "计算机1班", {{1,"A"},{2,"B"}}};
printf("%s 第1个学生:%s\n", c1.cname, c1.stu[0].name);
return 0;
}
解析:结构体里可以再放结构体/结构体数组,一层层用点号访问。
【第85题】结构体传值vs传指针
// 传值:整个结构体被拷贝一份,浪费内存,改的也不是原对象
void f1(struct Student s) { s.score = 0; }
// 传指针:只传地址(4/8字节),效率高,能改原对象
void f2(struct Student *s) { s->score = 0; }
// 大结构体应传指针;只想读不想改,加const保护
解析:结构体可能很大,传值会整块拷贝;传指针只传地址,更快。
【第86题】动态学生结构体数组
#include <stdio.h>
#include <stdlib.h>
struct Student { int id; char name[20]; float score; };
int main() {
int n; scanf("%d", &n);
struct Student *s = (struct Student*)malloc(n * sizeof(struct Student));
for (int i = 0; i < n; i++)
scanf("%d %s %f", &s[i].id, s[i].name, &s[i].score);
for (int i = 0; i < n; i++)
printf("%d %s %.1f\n", s[i].id, s[i].name, s[i].score);
free(s);
return 0;
}
解析:malloc按结构体大小申请n份,之后完全当数组用。
【第87题】链表节点与创建
#include <stdio.h>
#include <stdlib.h>
struct Node { int data; struct Node *next; };
struct Node* createNode(int v) {
struct Node *p = (struct Node*)malloc(sizeof(struct Node));
p->data = v;
p->next = NULL;
return p;
}
解析:每个节点自己malloc,next初始指向NULL表示链表结尾。
【第88题】头插法创建链表
#include <stdio.h>
#include <stdlib.h>
struct Node { int data; struct Node *next; };
int main() {
struct Node *head = NULL;
int v;
while (scanf("%d", &v) == 1 && v != -1) {
struct Node *p = (struct Node*)malloc(sizeof(struct Node));
p->data = v;
p->next = head; // 新节点指向原头
head = p; // 新节点成为头
}
for (struct Node *p = head; p; p = p->next)
printf("%d ", p->data);
printf("\n");
return 0;
}
解析:新节点永远插在最前面,所以输入顺序和输出顺序相反。
【第89题】统计节点个数
int count(struct Node *head) {
int n = 0;
while (head) { n++; head = head->next; }
return n;
}
解析:从头走到尾,每过一个节点加1,走到NULL结束。
【第90题】删除指定值的节点
struct Node* delNode(struct Node *head, int v) {
struct Node *cur = head, *prev = NULL;
while (cur && cur->data != v) { prev = cur; cur = cur->next; }
if (cur == NULL) return head; // 没找到
if (prev == NULL) head = cur->next; // 删的是头节点
else prev->next = cur->next; // 中间节点:跳过它
free(cur);
return head;
}
解析:要删除必须记住前一个节点prev,改它的next跳过当前节点,再free。
模块六 文件操作答案(第91–100题)
【第91题】写入文本文件
#include <stdio.h>
int main() {
FILE *fp = fopen("data.txt", "w");
if (fp == NULL) return -1;
fprintf(fp, "hello file\n");
fclose(fp);
return 0;
}
解析:固定四步:fopen打开 → 判断失败 → fprintf写入 → fclose关闭。
【第92题】读取文件并输出
#include <stdio.h>
int main() {
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) return -1;
char line[200];
while (fgets(line, sizeof(line), fp)) // 逐行读
printf("%s", line);
fclose(fp);
return 0;
}
解析:fgets读到文件末尾返回NULL,while循环自然结束。
【第93题】复制文件
#include <stdio.h>
int main() {
FILE *src = fopen("a.txt", "r");
FILE *dst = fopen("b.txt", "w");
if (!src || !dst) return -1;
char ch;
while ((ch = fgetc(src)) != EOF) // 逐字符读
fputc(ch, dst); // 逐字符写
fclose(src); fclose(dst);
return 0;
}
解析:fgetc读到EOF(-1)表示结束,把读到的每个字符写入目标文件。
【第94题】统计字符个数
#include <stdio.h>
int main() {
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) return -1;
long n = 0;
while (fgetc(fp) != EOF) n++;
printf("%ld\n", n);
fclose(fp);
return 0;
}
解析:和复制一样逐字符读,读到EOF前每读一次计数加1。
【第95题】追加写入
#include <stdio.h>
int main() {
FILE *fp = fopen("data.txt", "a"); // a表示追加
if (fp == NULL) return -1;
fprintf(fp, "追加的一行\n");
fclose(fp);
return 0;
}
解析:模式"a"(append)不覆盖原内容,新内容写在末尾。
【第96题】读文件整数求和
#include <stdio.h>
int main() {
FILE *fp = fopen("nums.txt", "r");
if (fp == NULL) return -1;
int x, sum = 0;
while (fscanf(fp, "%d", &x) == 1) sum += x;
fclose(fp);
printf("%d\n", sum);
return 0;
}
解析:fscanf返回成功读到的个数,读不到整数时返回0或EOF,循环结束。
【第97题】学生信息写入文件
#include <stdio.h>
struct Student { int id; char name[20]; float score; };
int main() {
FILE *fp = fopen("stu.txt", "w");
if (fp == NULL) return -1;
struct Student s[3] = {{1,"A",80},{2,"B",90},{3,"C",70}};
for (int i = 0; i < 3; i++)
fprintf(fp, "%d %s %.1f\n", s[i].id, s[i].name, s[i].score);
fclose(fp);
return 0;
}
解析:和printf几乎一样,只是多了FILE*参数,数据落盘。
【第98题】从文件读回学生信息
#include <stdio.h>
struct Student { int id; char name[20]; float score; };
int main() {
FILE *fp = fopen("stu.txt", "r");
if (fp == NULL) return -1;
struct Student s[3];
for (int i = 0; i < 3; i++)
fscanf(fp, "%d %s %f", &s[i].id, s[i].name, &s[i].score);
for (int i = 0; i < 3; i++)
printf("%d %s %.1f\n", s[i].id, s[i].name, s[i].score);
fclose(fp);
return 0;
}
解析:格式和写入时完全一致才能正确读回,形成“存—取”闭环。
【第99题】文件内容按行倒序输出
#include <stdio.h>
#include <string.h>
int main() {
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) return -1;
char lines[100][200];
int n = 0;
while (fgets(lines[n], 200, fp)) n++; // 全部读进数组
for (int i = n-1; i >= 0; i--) printf("%s", lines[i]);
fclose(fp);
return 0;
}
解析:先整文件读进二维数组,再倒着输出。
【第100题】二进制读写整数数组
#include <stdio.h>
int main() {
int a[5] = {10,20,30,40,50};
FILE *fp = fopen("arr.dat", "wb"); // wb二进制写
fwrite(a, sizeof(int), 5, fp); // 整块写入
fclose(fp);
int b[5];
fp = fopen("arr.dat", "rb"); // rb二进制读
fread(b, sizeof(int), 5, fp);
fclose(fp);
for (int i = 0; i < 5; i++) printf("%d ", b[i]);
printf("\n");
return 0;
}
解析:fwrite/fread按字节块读写,适合结构体、数组等二进制数据,比文本快且无格式问题。
第二篇 C++:从C到面向对象
一、核心知识点(基础阶段)
1. C++ 和 C 的关系
C++ 完全兼容 C 语言,学完 C 再学 C++ 上手很快。C++ 在 C 之上主要增加了三样东西:面向对象(类、封装、继承、多态)、STL 标准模板库(现成的容器和算法)、更安全的机制(引用、new/delete、异常)。编译C++程序用 g++。
2. 输入输出:cin 和 cout
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n; // 输入,不用写&和格式符
cout << "n=" << n << endl; // 输出,endl是换行
return 0;
}
优点:不用记%d、%s 这些格式符,自动匹配类型;缺点:默认比 scanf/printf 慢,大数据量时加一行 ios::sync_with_stdio(false); 加速。
3. 引用:变量的别名
int x = 10;
int &r = x; // r是x的别名,同一块内存
r = 20; // x也变成20
void swap(int &a, int &b) { // 传引用
int t = a; a = b; b = t;
}
// 调用:swap(x, y); 直接改实参,比指针更安全(不用解引用)
引用和指针的区别:引用必须初始化、不能为空、不能换绑;指针可以不初始化(危险)、可以指向别处。
4. 函数重载与默认参数
// 函数重载:同名函数,参数不同即可
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
// 默认参数:调用时可以省略
void print(string s, int times = 1) { // times默认1
for (int i = 0; i < times; i++) cout << s;
}
易忽略:重载只认参数,不认返回值;默认参数必须从右往左连续写。
5. 类与对象:封装
#include <iostream>
#include <string>
using namespace std;
class Student {
private: // 私有:外部不能直接访问
int id;
string name;
double score;
public: // 公有:对外提供的接口
Student(int i, string n, double s) { // 构造函数
id = i; name = n; score = s;
}
void show() {
cout << id << " " << name << " " << score << endl;
}
};
int main() {
Student s(1, "张三", 88.5); // 创建对象,自动调构造函数
s.show();
return 0;
}
三个核心概念:类是模板,对象是实例;封装把数据私有化,只通过公有方法访问;构造函数在对象创建时自动执行,负责初始化。
6. 构造与析构
class Test {
public:
Test() { cout << "构造\n"; } // 无参构造
Test(int x) { cout << "带参构造\n"; }
~Test() { cout << "析构\n"; } // 析构:对象销毁时自动调用,做清理
};
易忽略:构造函数和析构函数都没有返回值,连 void 都不能写;对象离开作用域时自动调用析构。
二、核心知识点(高级阶段)
1. 继承:代码复用
class Animal { // 基类(父类)
public:
string name;
void eat() { cout << "吃东西\n"; }
};
class Dog : public Animal { // 派生类(子类):继承父类
public:
void bark() { cout << "汪汪\n"; }
};
// Dog 对象同时拥有 name、eat()(来自父类)和 bark()(自己新增的)
继承让子类自动拥有父类的属性和方法,不用重写;还可以在子类里增加新功能或重写父类方法。
2. 多态:同一接口,不同实现
class Animal {
public:
virtual void cry() { cout << "动物叫\n"; } // 虚函数
virtual ~Animal() {} // 析构也加virtual
};
class Cat : public Animal {
public:
void cry() override { cout << "喵喵\n"; } // 重写
};
class Dog : public Animal {
public:
void cry() override { cout << "汪汪\n"; }
};
int main() {
Animal *p = new Cat(); // 父类指针指向子类对象
p->cry(); // 输出"喵喵"——运行时才决定调谁
delete p;
return 0;
}
多态三要素:父类指针/引用 + 虚函数 virtual + 子类重写 override。少一个都不叫多态。
纯虚函数与抽象类:只声明不实现的虚函数叫纯虚函数(virtual void f() = 0;),含纯虚函数的类不能创建对象,只能当接口被继承,子类必须实现它。
3. STL 标准模板库:现成的好工具
| 容器 | 特点 | 典型用法 |
|---|---|---|
| string | 字符串 | 拼接、查找、截取、替换,比 char 数组好用 |
| vector | 动态数组 | 自动扩容,尾部增删快,支持随机访问 |
| list | 双向链表 | 任意位置插入删除快,不能随机访问 |
| map | 键值对(有序) | |
| set | 集合 | 元素唯一、自动排序,判断是否存在 |
| algorithm | 算法库 | sort 排序、find 查找、reverse 反转 |
#include <vector>
#include <string>
#include <map>
#include <algorithm>
#include <iostream>
using namespace std;
int main() {
vector<int> v = {5,2,8,1,9};
sort(v.begin(), v.end()); // 排序:1 2 5 8 9
v.push_back(100); // 尾部加元素
cout << v.size() << endl; // 元素个数
string s = "hello";
s += " world"; // 拼接
s.find("world"); // 查找位置,找不到返回npos
s.substr(6, 5); // 截取从6开始5个字符
map<int, string> m;
m[101] = "张三"; // 增
m[101] = "李四"; // 改(key唯一)
cout << m[101]; // 查
m.erase(101); // 删
return 0;
}
4. new/delete 与智能指针
int *p = new int(10); // new:申请内存并调用构造函数
delete p; // delete:调用析构并释放
int *a = new int[100]; // 动态数组
delete[] a; // 数组用 delete[]
// C++11 智能指针:自动释放,不用手动delete
#include <memory>
unique_ptr<int> u(new int(5)); // 独占
shared_ptr<int> s = make_shared<int>(5); // 共享,引用计数归零自动释放
new/delete 和 malloc/free 不要混用:new 会调构造函数、delete 会调析构函数,malloc/free 不会。
5. 模板:写一次,处处通用
template <typename T>
T myMax(T a, T b) {
return a > b ? a : b;
}
// 用法:myMax(3, 5) 传int,myMax(3.5, 2.1) 传double,自动匹配
6. 异常处理
try {
int x; cin >> x;
if (x < 0) throw "负数不允许"; // 抛出异常
} catch (const char *msg) {
cout << "捕获到:" << msg << endl; // 处理异常
}
三、高频易错点&容易忽略的细节(C++重点)
| 编号 | 错误写法(×) | 正确写法(√)与原因 |
|---|---|---|
| 1 | int &r; |
引用必须初始化:int x; int &r = x;。引用不能是空引用。 |
| 2 | 分不清 const 位置 | const int *p 是内容不能改;int *const p 是指针不能改。const在*左边修饰内容,右边修饰指针。 |
| 3 | 只改返回值想实现重载 | 重载只看参数个数/类型/顺序,返回值不同不算重载,会编译报错。 |
| 4 | void Student() {} 写构造函数 |
构造函数、析构函数没有返回值类型,连void都不能写。 |
| 5 | 类里直接初始化普通成员 | int id = 0; 在C++11之前的类内不合法;现在可以类内初始化,但推荐在构造函数里初始化。只有 static const 整型才能类内直接赋值。 |
| 6 | 父类函数没加 virtual | 多态前提:父类函数必须 virtual,子类加 override。不加就只是函数隐藏,父类指针调不到子类版本。 |
| 7 | 父类析构函数没加 virtual | 父类指针 delete 子类对象时,只调父类析构,子类资源没释放——内存泄漏。析构函数加 virtual 是规范。 |
| 8 | new 和 malloc 混用 | new 配 delete,malloc 配 free。new 调构造、delete 调析构,混用会行为未定义。 |
| 9 | 遍历时 erase 元素 | erase 会让迭代器失效。要接收返回值:it = v.erase(it);,否则崩溃或漏删。 |
| 10 | cin/cout 处理大数据 | 默认同步慢。加 ios::sync_with_stdio(false); cin.tie(0); 加速,或直接用 scanf/printf。 |
| 11 | 类里含指针却不写拷贝构造 | 默认是浅拷贝:两个对象共享同一块内存,析构时double free。含指针必须自定义深拷贝。 |
| 12 | return string.c_str(); |
c_str() 返回的指针在 string 被修改或销毁后就失效了,不能长期保存。 |
| 13 | vector 频繁 insert 头部 | vector 头插要搬动所有元素,O(n)。频繁头部插入用 list 或 deque。 |
| 14 | 头文件重复包含 | 加 #pragma once 或头文件守卫 #ifndef XXX_H / #define / #endif,防止重复定义。 |
| 15 | 子类构造不调父类构造 | 创建子类对象时先调父类构造再调子类构造;父类只有带参构造时,子类必须显式调用。 |
| 16 | 重载 << 时返回 void | 重载输出运算符要返回 ostream& 才能连续输出:cout << a << b;。 |
四、C++经典必刷题(先做题,答案在下一节)
基础模块(第1–15题)
-
写学生类:私有属性学号、姓名、成绩,提供设置和打印方法。
-
用构造函数初始化学生信息,并验证对象创建时自动调用。
-
用引用实现两个整数交换。
-
函数重载:分别实现整数、浮点数、字符串三个版本的求和/拼接输出。
-
默认参数:写一个打印函数,次数参数默认打印1次。
-
用 vector 存5个整数,排序后正序、逆序各输出一次。
-
用 string 完成:拼接、查找子串、截取、替换、判断是否包含。
-
用 map 实现学号到姓名的增删查改。
-
写一个计数器类,用静态成员统计创建了多少个对象。
-
写一个矩形类,成员函数求面积和周长。
-
写一个复数类,用重载实现两个复数相加。
-
写一个友元函数,访问类的私有成员。
-
用模板写一个通用的 swap 函数。
-
用 vector 存若干学生对象,按成绩排序输出。
-
用文件流把学生信息写入文件,再读出来。
进阶模块(第16–30题)
-
定义动物基类,派生猫、狗,虚函数实现多态叫声。
-
定义图形抽象类(纯虚函数求面积),派生圆形、矩形并实现。
-
写一个形状基类指针数组,分别指向圆形、矩形,统一输出面积(多态)。
-
用 vector 模拟栈:实现入栈、出栈、判断空。
-
用 map 统计一段文字中每个单词出现的次数。
-
用 set 对数组去重并排序。
-
用 list 实现任意位置插入、删除。
-
写一个类管理动态数组,实现深拷贝(拷贝构造+赋值运算符)。
-
用智能指针管理动态对象,验证自动释放。
-
写异常处理:输入为负数时抛出并捕获异常。
-
实现运算符重载:比较两个矩形大小。
-
写一个字符串类,重载 + 实现拼接。
-
实现继承:学生类继承人类,人类有姓名年龄,学生增加学号成绩。
-
实现模板类:通用的数组容器,支持存取。
-
综合题:设计图书管理类(增删查改),用 vector 存储并持久化到文件。
五、C++经典题答案解析(先做题再看这里)
基础模块答案(第1–15题)
【第1题】学生类封装
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
int id; string name; double score;
public:
void setInfo(int i, string n, double s) { id = i; name = n; score = s; }
void show() { cout << id << " " << name << " " << score << endl; }
};
int main() {
Student s;
s.setInfo(1, "张三", 88.5);
s.show();
return 0;
}
解析:private 私有成员外部不能直接碰,必须通过 public 方法操作,这就是封装。
【第2题】构造函数初始化
class Student {
public:
Student(int i, string n, double s) { // 构造函数
id = i; name = n; score = s;
cout << "对象创建\n";
}
private:
int id; string name; double score;
};
int main() {
Student s(1, "张三", 88.5); // 创建对象自动调用构造
return 0;
}
解析:构造函数在对象创建那一刻自动执行,负责初始化,无需手动调用。
【第3题】引用交换
void swap(int &a, int &b) {
int t = a; a = b; b = t;
}
解析:引用是别名,直接操作实参,比指针写法简洁安全。
【第4题】函数重载
#include <iostream>
#include <string>
using namespace std;
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
string add(string a, string b) { return a + b; } // 字符串拼接
int main() {
cout << add(1, 2) << endl; // 3
cout << add(1.5, 2.5) << endl; // 4
cout << add("ab", "cd") << endl; // abcd
return 0;
}
解析:同名函数参数类型不同,编译器按实参自动选版本。注意:返回值不同不算重载。
【第5题】默认参数
#include <iostream>
using namespace std;
void printMsg(string s, int times = 1) { // times默认1
for (int i = 0; i < times; i++) cout << s << endl;
}
int main() {
printMsg("hi"); // 打印1次
printMsg("hi", 3); // 打印3次
return 0;
}
解析:默认参数从右往左连续设置,调用时可省略有默认值的参数。
【第6题】vector 排序正逆序
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
int main() {
vector<int> v = {5,2,8,1,9};
sort(v.begin(), v.end()); // 升序
for (int x : v) cout << x << " ";
cout << endl;
sort(v.rbegin(), v.rend()); // 逆序迭代器=降序
for (int x : v) cout << x << " ";
cout << endl;
return 0;
}
解析:sort 默认升序;用 rbegin/rend 逆序迭代器直接降序,不用写比较函数。
【第7题】string 常用操作
#include <iostream>
#include <string>
using namespace std;
int main() {
string s = "hello";
s += " world"; // 拼接
size_t pos = s.find("world"); // 查找,返回下标6
string sub = s.substr(6, 5); // 截取 "world"
s.replace(6, 5, "C++"); // 替换成 "hello C++"
if (s.find("C++") != string::npos) // npos表示没找到
cout << "包含" << endl;
cout << s << endl;
return 0;
}
解析:find 找不到返回 string::npos;substr(起始, 长度);replace(起始, 长度, 新内容)。
【第8题】map 增删查改
#include <map>
#include <iostream>
#include <string>
using namespace std;
int main() {
map<int, string> m;
m[101] = "张三"; // 增
m[102] = "李四";
m[101] = "张大三"; // 改:key存在就覆盖
cout << m[101] << endl; // 查
m.erase(102); // 删
if (m.count(101)) cout << "101存在" << endl; // 判断是否存在
return 0;
}
解析:map 按键自动排序,key 唯一;[] 不存在时会自动创建默认值。
【第9题】静态成员统计对象个数
#include <iostream>
using namespace std;
class Counter {
public:
Counter() { count++; }
~Counter() { count--; }
static int count; // 静态成员:所有对象共享
};
int Counter::count = 0; // 类外初始化(必须)
int main() {
Counter a, b, c;
cout << Counter::count << endl; // 3
return 0;
}
解析:静态成员属于类而不属于某个对象,构造+1、析构-1,就能统计存活对象数。
【第10题】矩形类面积周长
class Rect {
private:
double w, h;
public:
Rect(double w, double h) { this->w = w; this->h = h; }
double area() { return w * h; }
double perimeter() { return 2 * (w + h); }
};
解析:成员函数可以直接访问私有成员;this 指向当前对象,解决参数重名问题。
【第11题】复数类重载加法
class Complex {
public:
double r, i;
Complex(double r, double i) { this->r = r; this->i = i; }
Complex operator+(const Complex &c) { // 重载+
return Complex(r + c.r, i + c.i);
}
};
// 用法:Complex a(1,2), b(3,4); Complex c = a + b; // c=(4,6)
解析:重载运算符让对象支持 a+b 的写法,本质是调用 operator+ 成员函数。
【第12题】友元函数
#include <iostream>
using namespace std;
class Box {
private:
int secret = 42;
friend void showSecret(Box b); // 声明友元
};
void showSecret(Box b) { // 友元实现:可访问私有成员
cout << b.secret << endl;
}
解析:友元函数不是成员函数,但被授权可以访问私有成员。谨慎使用,破坏封装。
【第13题】模板 swap
template <typename T>
void mySwap(T &a, T &b) {
T t = a; a = b; b = t;
}
// 用法:mySwap(x, y); 传int、double、string都能用
解析:模板让函数适用于任意类型,编译器按调用类型自动生成对应版本。
【第14题】vector 存对象排序
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
class Student {
public:
string name; double score;
Student(string n, double s) { name = n; score = s; }
};
bool cmp(const Student &a, const Student &b) {
return a.score > b.score; // 按成绩降序
}
int main() {
vector<Student> v;
v.push_back(Student("张三", 88));
v.push_back(Student("李四", 76));
v.push_back(Student("王五", 92));
sort(v.begin(), v.end(), cmp);
for (auto &s : v) cout << s.name << " " << s.score << endl;
return 0;
}
解析:sort 第三个参数传自定义比较函数,就能按自己的规则排序对象。
【第15题】文件流读写
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main() {
ofstream out("stu.txt"); // 输出文件流:写
out << 1 << " " << "张三" << " " << 88.5 << endl;
out.close();
ifstream in("stu.txt"); // 输入文件流:读
int id; string name; double score;
in >> id >> name >> score;
cout << id << " " << name << " " << score << endl;
in.close();
return 0;
}
解析:fstream 让文件读写像 cin/cout 一样简单,不用记格式符。
进阶模块答案(第16–30题)
【第16题】多态动物叫声
#include <iostream>
using namespace std;
class Animal {
public:
virtual void cry() { cout << "动物叫" << endl; }
virtual ~Animal() {}
};
class Cat : public Animal {
public:
void cry() override { cout << "喵喵" << endl; }
};
class Dog : public Animal {
public:
void cry() override { cout << "汪汪" << endl; }
};
int main() {
Animal *p = new Cat();
p->cry(); // 喵喵
delete p;
p = new Dog();
p->cry(); // 汪汪
delete p;
return 0;
}
解析:父类指针调用虚函数,运行期根据实际对象类型决定调用哪个版本,这就是多态。
【第17题】抽象类求面积
#include <iostream>
using namespace std;
class Shape {
public:
virtual double area() = 0; // 纯虚函数:抽象类
virtual ~Shape() {}
};
class Circle : public Shape {
private:
double r;
public:
Circle(double r) { this->r = r; }
double area() override { return 3.14159 * r * r; }
};
class Rect : public Shape {
private:
double w, h;
public:
Rect(double w, double h) { this->w = w; this->h = h; }
double area() override { return w * h; }
};
解析:抽象类只定接口(纯虚函数),不能实例化;子类必须实现全部纯虚函数才能创建对象。
【第18题】多态统一输出面积
int main() {
Shape *shapes[2]; // 基类指针数组
shapes[0] = new Circle(2);
shapes[1] = new Rect(3, 4);
for (int i = 0; i < 2; i++)
cout << shapes[i]->area() << endl; // 12.566 12
for (int i = 0; i < 2; i++) delete shapes[i];
return 0;
}
解析:一个基类指针数组存不同子类对象,统一调用同一个接口,各自执行自己的实现。
【第19题】vector 模拟栈
#include <vector>
#include <iostream>
using namespace std;
class Stack {
private:
vector<int> v;
public:
void push(int x) { v.push_back(x); }
void pop() { if (!v.empty()) v.pop_back(); }
int top() { return v.back(); }
bool empty() { return v.empty(); }
};
解析:vector 尾部增删正好对应栈的入栈出栈,back() 取栈顶。
【第20题】map 统计单词次数
#include <map>
#include <sstream>
#include <iostream>
#include <string>
using namespace std;
int main() {
string text = "I love C++ C++ is fun";
map<string, int> words;
stringstream ss(text); // 用流拆分单词
string w;
while (ss >> w) words[w]++; // 每出现一次计数+1
for (auto &p : words)
cout << p.first << ":" << p.second << endl;
return 0;
}
解析:map<string,int> 天然适合“词→次数”,words[w]++ 一行完成统计。
【第21题】set 去重排序
#include <set>
#include <iostream>
using namespace std;
int main() {
set<int> s = {5,2,8,1,9,2,5}; // 重复的自动去掉
for (int x : s) cout << x << " "; // 自动升序:1 2 5 8 9
cout << endl;
return 0;
}
解析:set 元素唯一且自动有序,插入时自动去重排序。
【第22题】list 插入删除
#include <list>
#include <iostream>
using namespace std;
int main() {
list<int> l = {1,2,3,4,5};
auto it = l.begin();
advance(it, 2); // 移动到第3个位置
l.insert(it, 99); // 插入
it = l.begin();
advance(it, 1);
l.erase(it); // 删除第2个
for (int x : l) cout << x << " ";
cout << endl;
return 0;
}
解析:list 是链表,任意位置插入删除 O(1),但没有随机访问,用 advance 移动迭代器。
【第23题】深拷贝
#include <iostream>
using namespace std;
class Arr {
private:
int *p; int n;
public:
Arr(int n) { this->n = n; p = new int[n]; }
Arr(const Arr &other) { // 拷贝构造:深拷贝
n = other.n;
p = new int[n]; // 重新申请内存
for (int i = 0; i < n; i++) p[i] = other.p[i];
}
~Arr() { delete[] p; }
};
int main() {
Arr a(5);
Arr b = a; // 触发拷贝构造,b有自己独立的内存
return 0;
}
解析:默认拷贝是浅拷贝(只复制指针),两个对象指向同一块内存,析构时 double free。含指针的类必须深拷贝。
【第24题】智能指针自动释放
#include <iostream>
#include <memory>
using namespace std;
class Demo {
public:
Demo() { cout << "构造" << endl; }
~Demo() { cout << "析构" << endl; }
};
int main() {
{ unique_ptr<Demo> u(new Demo()); } // 出作用域自动析构
shared_ptr<Demo> s1 = make_shared<Demo>();
shared_ptr<Demo> s2 = s1; // 引用计数+1
// 全部退出后自动析构
return 0;
}
解析:智能指针在作用域结束时自动 delete,不用手动管理,从根上避免内存泄漏。
【第25题】异常处理
#include <iostream>
#include <stdexcept>
using namespace std;
int main() {
try {
int x; cin >> x;
if (x < 0) throw runtime_error("负数不允许");
cout << x * 2 << endl;
} catch (const exception &e) {
cout << "捕获:" << e.what() << endl;
}
return 0;
}
解析:throw 抛出、catch 捕获,程序不会因错误直接崩溃,错误处理更优雅。
【第26题】比较矩形大小
#include <iostream>
using namespace std;
class Rect {
public:
double w, h;
Rect(double w, double h) { this->w = w; this->h = h; }
double area() { return w * h; }
bool operator>(const Rect &r) { // 重载>
return area() > r.area();
}
};
// 用法:if (a > b) cout << "a大";
解析:重载关系运算符后,对象之间可以直接用 > < 比较。
【第27题】字符串类重载+
#include <iostream>
#include <string>
using namespace std;
class MyStr {
public:
string s;
MyStr(string s) { this->s = s; }
MyStr operator+(const MyStr &o) { return MyStr(s + o.s); }
void show() { cout << s << endl; }
};
int main() {
MyStr a("hello "), b("world");
MyStr c = a + b; // 用重载的+
c.show();
return 0;
}
解析:重载 + 返回新对象,两个自定义字符串对象就能直接相加。
【第28题】继承:学生类继承人类
#include <iostream>
#include <string>
using namespace std;
class Person {
protected: // protected:子类可访问
string name; int age;
public:
Person(string n, int a) { name = n; age = a; }
void show() { cout << name << " " << age << endl; }
};
class Student : public Person {
private:
int id; double score;
public:
Student(string n, int a, int i, double s) : Person(n, a) {
id = i; score = s; // 显式调用父类构造
}
void showAll() {
show(); // 用父类的方法
cout << id << " " << score << endl;
}
};
解析:子类构造必须先调父类构造(初始化列表 : Person(n,a));protected 让父类成员对子类可见。
【第29题】模板类数组容器
#include <iostream>
using namespace std;
template <typename T, int N>
class MyArray {
private:
T data[N];
public:
void set(int i, T v) { data[i] = v; }
T get(int i) { return data[i]; }
int size() { return N; }
};
// 用法:MyArray<int, 10> a; a.set(0, 100);
解析:模板参数可以是类型也可以是常量,让容器适配任意元素类型和容量。
【第30题】图书管理类(综合)
#include <vector>
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
class Book {
public:
int id; string title; double price;
Book(int i, string t, double p) { id = i; title = t; price = p; }
};
class Library {
private:
vector<Book> books;
public:
void add(Book b) { books.push_back(b); }
void del(int id) {
for (auto it = books.begin(); it != books.end(); )
if (it->id == id) it = books.erase(it); // 接收返回值
else ++it;
}
void find(int id) {
for (auto &b : books)
if (b.id == id) cout << b.title << " " << b.price << endl;
}
void save() {
ofstream out("books.txt");
for (auto &b : books)
out << b.id << " " << b.title << " " << b.price << endl;
}
void load() {
ifstream in("books.txt");
int id; string t; double p;
while (in >> id >> t >> p) books.push_back(Book(id, t, p));
}
};
解析:综合运用 vector 存储、迭代器删除、文件流持久化,是 C++ 入门到实战的小结。
第三篇 MySQL:从建表到SQL优化
一、核心知识点(基础阶段)
1. 数据库基本概念
-
数据库(Database):装数据表的容器,一个项目一个库。
-
表(Table):由行和列组成,一行是一条记录,一列是一个字段。
-
SQL:操作数据库的标准语言,分四大类。
| 分类 | 全称 | 典型语句 |
|---|---|---|
| DDL | 数据定义语言 | CREATE / ALTER / DROP(建库建表改表删表) |
| DML | 数据操作语言 | INSERT / UPDATE / DELETE(增改删数据) |
| DQL | 数据查询语言 | SELECT(查数据,用的最多) |
| DCL | 数据控制语言 | GRANT / REVOKE(权限管理) |
2. 建库建表:DDL
-- 创建数据库(字符集用utf8mb4,支持中文和表情)
CREATE DATABASE IF NOT EXISTS school DEFAULT CHARSET utf8mb4;
USE school;
-- 创建表
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT, -- 主键,自增
name VARCHAR(20) NOT NULL, -- 非空
gender CHAR(1) DEFAULT '男', -- 默认值
score INT DEFAULT 0,
UNIQUE KEY uk_phone (phone) -- 唯一约束
);
常用约束:PRIMARY KEY 主键(唯一+非空)、NOT NULL 非空、UNIQUE 唯一、DEFAULT 默认值、FOREIGN KEY 外键(关联另一张表)。
3. 增删改:DML
-- 增
INSERT INTO student(name, gender, score) VALUES
('张三','男',85),('李四','女',76),('王五','男',92);
-- 改(必须带WHERE!)
UPDATE student SET score = 90 WHERE id = 1;
-- 删(必须带WHERE!)
DELETE FROM student WHERE id = 3;
4. 单表查询:DQL(重点中的重点)
-- 基础查询
SELECT name, score FROM student WHERE score > 80;
SELECT * FROM student ORDER BY score DESC LIMIT 3; -- 降序取前3
-- 聚合函数:SUM求和 AVG平均 MAX最大 MIN最小 COUNT计数
SELECT COUNT(*) 总人数, AVG(score) 平均分,
MAX(score) 最高分, MIN(score) 最低分
FROM student;
-- 分组:按性别统计人数和平均分
SELECT gender, COUNT(*) 人数, AVG(score) 平均分
FROM student
GROUP BY gender
HAVING AVG(score) > 80; -- 分组后过滤(HAVING可用聚合函数)
-- 去重、模糊查询、区间
SELECT DISTINCT gender FROM student;
SELECT * FROM student WHERE name LIKE '张%'; -- 张开头
SELECT * FROM student WHERE score BETWEEN 80 AND 90;
查询语句执行顺序(面试必考):FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。先确定数据来源,再逐层过滤,最后排序分页。
二、核心知识点(高级阶段)
1. 多表查询:连接
-- 建第二张表
CREATE TABLE class (
cid INT PRIMARY KEY AUTO_INCREMENT,
cname VARCHAR(20)
);
ALTER TABLE student ADD cid INT; -- 学生表加班级字段
-- 内连接:只显示两表都匹配的行
SELECT s.name, c.cname
FROM student s
INNER JOIN class c ON s.cid = c.cid;
-- 左连接:左表全部保留,右表匹配不上显示NULL
SELECT c.cname, s.name
FROM class c
LEFT JOIN student s ON s.cid = c.cid;
-- 右连接:右表全部保留
SELECT c.cname, s.name
FROM student s
RIGHT JOIN class c ON s.cid = c.cid;
-- 自连接:自己连自己(如员工表查上级姓名)
SELECT e.name 员工, m.name 上级
FROM emp e
LEFT JOIN emp m ON e.manager_id = m.id;
为什么加别名:多表同名字段(如id)必须用“表别名.字段”区分,也少打字。
2. 子查询
-- 查成绩高于平均分的学生
SELECT * FROM student
WHERE score > (SELECT AVG(score) FROM student);
-- 查每个班级里成绩最高的学生(关联子查询)
SELECT * FROM student s
WHERE score = (SELECT MAX(score) FROM student WHERE cid = s.cid);
3. 视图:虚拟表
CREATE VIEW v_stu_score AS
SELECT s.name, c.cname, s.score
FROM student s LEFT JOIN class c ON s.cid = c.cid;
-- 之后可以像查表一样查视图,封装复杂查询
SELECT * FROM v_stu_score WHERE score > 80;
4. 索引:查询加速器
CREATE INDEX idx_score ON student(score); -- 普通索引
CREATE UNIQUE INDEX idx_name ON student(name); -- 唯一索引
-- 查看是否走索引(执行计划)
EXPLAIN SELECT * FROM student WHERE score > 80;
索引像书的目录,能大幅加快查询;但不是越多越好,索引会占用磁盘空间,还会拖慢 INSERT/UPDATE/DELETE(每次写都要同步维护索引)。
5. 事务:要么全成功,要么全回滚
-- 模拟转账:A扣500,B加500,两步必须同时成功
START TRANSACTION;
UPDATE account SET money = money - 500 WHERE id = 1;
UPDATE account SET money = money + 500 WHERE id = 2;
COMMIT; -- 提交,永久生效
-- 如果中间出错:ROLLBACK; 回滚,什么都不变
事务四大特性 ACID:A原子性(要么全做要么全不做)、C一致性(数据始终合法)、I隔离性(事务之间互不干扰)、D持久性(提交后不丢)。
6. 存储过程与触发器(了解即可)
-- 存储过程:把一段SQL打包,反复调用
DELIMITER //
CREATE PROCEDURE p_get_stu(IN c INT)
BEGIN
SELECT * FROM student WHERE cid = c;
END //
DELIMITER ;
CALL p_get_stu(1); -- 调用
-- 触发器:某事件发生时自动执行(如删除班级时自动清空学生)
CREATE TRIGGER trg_del_class AFTER DELETE ON class
FOR EACH ROW DELETE FROM student WHERE cid = OLD.cid;
7. 三大范式与表设计
-
第一范式(1NF):每列不可再分(不要存“张三,男,18”这种复合列)。
-
第二范式(2NF):每行有唯一标识(主键),非主键列完全依赖主键。
-
第三范式(3NF):非主键列之间不能互相依赖(不存冗余的重复信息)。
设计口诀:一事一表,表间用主外键关联,避免数据冗余;但为了查询性能,有时会刻意保留少量冗余(反范式),这是高级话题。
三、最容易忽略的细节&高频坑(MySQL重点)
| 编号 | 错误写法(×) | 正确写法(√)与原因 |
|---|---|---|
| 1 | WHERE name = 张三 |
字符串必须加单引号:WHERE name = '张三'。不加会被当成列名。 |
| 2 | WHERE name = NULL |
NULL不能用=判断,要用 IS NULL / IS NOT NULL。NULL不是0也不是空串。 |
| 3 | HAVING 当 WHERE 用 | WHERE 在分组前过滤原始行,不能用聚合函数;HAVING 在分组后过滤,必须配合 GROUP BY,可以用聚合函数。 |
| 4 | UPDATE student SET score=0; |
忘写 WHERE 会更新全表!生产环境是灾难。先 SELECT 确认影响行数再 UPDATE。 |
| 5 | 多表连接漏写 ON | 漏写 ON 产生笛卡尔积:100行×100行=10000行,数据直接爆炸。连接必带 ON。 |
| 6 | 以为删除后自增ID会回退 | AUTO_INCREMENT 只增不减,删了id=5,下一条还是6。要重置得 ALTER TABLE ... AUTO_INCREMENT=1。 |
| 7 | 索引字段上做运算 | WHERE YEAR(create_time)=2025 索引失效;应写成 create_time >= '2025-01-01' AND create_time < '2026-01-01'。 |
| 8 | LIKE '%张%' 用索引 |
%开头的前缀模糊查询索引失效;'张%' 后缀开头还能用索引。 |
| 9 | 字段类型隐式转换 | 字符串字段查数字、数字字段查字符串,隐式转换会导致索引失效。类型要一致。 |
| 10 | 事务不 COMMIT | 开启事务后必须 COMMIT 才永久保存;不提交直接断开连接,数据就没了。 |
| 11 | 混淆 COUNT(*)、COUNT(列) | COUNT(*) 数所有行(含NULL);COUNT(列) 只数该列非NULL的行。统计总行数用 COUNT(*)。 |
| 12 | LEFT JOIN 后右表过滤放 WHERE | 右表条件放 WHERE 会过滤掉 NULL 行,等于变内连接;要保留左表全部行,条件放 ON 里。 |
| 13 | 金额用 FLOAT/DOUBLE | 浮点有精度误差,金额必须用 DECIMAL(10,2)。 |
| 14 | 表名/字段名撞关键字 | 如 order、group。用反引号包裹:SELECT * FROM order;,但更建议换名字。 |
| 15 | 字符集不统一 | 库、表、字段字符集要统一用 utf8mb4,否则中文乱码、emoji 存不进去。 |
| 16 | 大表 LIMIT 分页很慢 | LIMIT 1000000,10 要扫前面100万行。优化:用上次最后一条id做条件 WHERE id > 1000000 LIMIT 10。 |
| 17 | 外键随意加 | 外键保证数据一致性,但会降低写入性能、限制删除。互联网大厂普遍不用物理外键,用业务逻辑保证。 |
| 18 | DROP / TRUNCATE / DELETE 分不清 | DROP 删表结构;TRUNCATE 清空数据但保留表、不能回滚;DELETE 删行、可以带WHERE、可以回滚。 |
| 19 | GROUP BY 选列不规范 | SELECT 的非聚合列必须都在 GROUP BY 里(ONLY_FULL_GROUP_BY模式),否则结果不确定。 |
| 20 | 不备份直接跑 DELETE | 任何删除/修改前先备份(mysqldump 或 SELECT INTO 导出),这是保命习惯。 |
四、MySQL经典必刷题(先做题,答案在下一节)
基础单表模块(第1–15题)
-
创建数据库 student_db(utf8mb4),并进入该库。
-
创建学生表:id主键自增、姓名非空、性别默认'男'、成绩默认0。
-
插入5条学生数据。
-
查询所有学生,按成绩从高到低排序。
-
查询成绩大于等于80分的学生姓名和成绩。
-
查询姓“张”的学生。
-
查询成绩在70到90之间(含)的学生。
-
统计学生总人数、平均分、最高分、最低分。
-
按性别分组,统计每组的平均成绩,只显示平均分大于80的组。
-
查询成绩前3名的学生(降序取3条)。
-
把id=1的学生成绩改为95。
-
删除成绩小于60分的学生记录。
-
新增一列 phone(VARCHAR(20))。
-
给 score 字段创建索引,并查看执行计划。
-
查询成绩最高的学生信息(用子查询)。
进阶多表与事务模块(第16–30题)
-
创建班级表(cid主键、cname),给学生表加 cid 字段。
-
用内连接查询“有班级”的学生姓名和班级名。
-
用左连接查询所有班级及其学生(没有学生的班级也要显示)。
-
查询每个班级的学生人数。
-
查询每个班级里成绩最高的学生。
-
查询没有班级的学生(孤儿数据)。
-
创建一个视图:学生姓名+班级名+成绩,成绩大于80的查询该视图。
-
创建索引 idx_name,验证模糊查询'张%'是否走索引。
-
模拟转账事务:A扣100、B加100,成功提交;再模拟出错回滚。
-
创建存储过程:按班级id查询学生。
-
创建触发器:删除班级时自动删除该班学生。
-
用自连接查员工表:每个员工及其上级姓名。
-
统计每个班级的平均分,并按平均分降序。
-
查询成绩低于全班平均分的学生。
-
综合题:设计一个“图书借阅”系统表结构(图书表、读者表、借阅表),并写出“查询借了书还没还的读者”的SQL。
五、MySQL经典题答案解析(先做题再看这里)
基础单表模块答案(第1–15题)
【第1题】创建数据库
CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARSET utf8mb4;
USE student_db;
解析:IF NOT EXISTS 防止重复创建报错;utf8mb4 是中文环境的标配。
【第2题】建学生表
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
gender CHAR(1) DEFAULT '男',
score INT DEFAULT 0
);
解析:主键+自增保证每行唯一标识;NOT NULL 和 DEFAULT 是常用约束。
【第3题】插入数据
INSERT INTO student(name, gender, score) VALUES
('张三','男',85),('李四','女',76),('王五','男',92),
('赵六','女',58),('钱七','男',88);
解析:一次插多条用逗号分隔;id 自增不用写。
【第4题】成绩降序
SELECT * FROM student ORDER BY score DESC;
解析:ORDER BY 默认升序ASC,DESC 是降序。
【第5题】成绩过滤
SELECT name, score FROM student WHERE score >= 80;
解析:WHERE 只过滤原始行,>= 含等于。
【第6题】姓张的
SELECT * FROM student WHERE name LIKE '张%';
解析:LIKE 配合 % 通配符,'张%' 匹配以“张”开头的字符串。
【第7题】区间查询
SELECT * FROM student WHERE score BETWEEN 70 AND 90;
解析:BETWEEN 含两端,等价于 score>=70 AND score<=90。
【第8题】聚合统计
SELECT COUNT(*) 总人数, AVG(score) 平均分,
MAX(score) 最高分, MIN(score) 最低分
FROM student;
解析:聚合函数把多行压成一行结果,AS 别名可省略(这里用空格别名)。
【第9题】分组过滤
SELECT gender, AVG(score) 平均分
FROM student
GROUP BY gender
HAVING AVG(score) > 80;
解析:先 GROUP BY 分组,再用 HAVING 过滤分组结果(能用聚合函数,WHERE不能)。
【第10题】成绩前3
SELECT * FROM student ORDER BY score DESC LIMIT 3;
解析:先排序再 LIMIT 取前3条。
【第11题】改成绩
UPDATE student SET score = 95 WHERE id = 1;
解析:UPDATE 必须带 WHERE,否则全表改。
【第12题】删不及格
DELETE FROM student WHERE score < 60;
解析:DELETE 必须带 WHERE;想保留数据可加一个 status 字段做逻辑删除。
【第13题】加列
ALTER TABLE student ADD COLUMN phone VARCHAR(20);
解析:ALTER TABLE ... ADD COLUMN 给已有表加字段。
【第14题】建索引看执行计划
CREATE INDEX idx_score ON student(score);
EXPLAIN SELECT * FROM student WHERE score > 80;
解析:EXPLAIN 输出里 key 显示用到的索引名即走索引;type=ALL 就是全表扫描。
【第15题】成绩最高的学生
SELECT * FROM student
WHERE score = (SELECT MAX(score) FROM student);
解析:子查询先算出最高分,外层按最高分筛选。
进阶多表与事务模块答案(第16–30题)
【第16题】建班级表加字段
CREATE TABLE class (
cid INT PRIMARY KEY AUTO_INCREMENT,
cname VARCHAR(20)
);
ALTER TABLE student ADD COLUMN cid INT;
解析:学生表通过 cid 关联班级表,这就是一对多关系(一个班级多个学生)。
【第17题】内连接
SELECT s.name, c.cname
FROM student s
INNER JOIN class c ON s.cid = c.cid;
解析:INNER JOIN 只返回两表都匹配的行,没班级的学生不显示。
【第18题】左连接
SELECT c.cname, s.name
FROM class c
LEFT JOIN student s ON s.cid = c.cid;
解析:LEFT JOIN 左表(class)全部保留,没学生的班级 name 显示 NULL。
【第19题】每个班级人数
SELECT c.cname, COUNT(s.id) 人数
FROM class c
LEFT JOIN student s ON s.cid = c.cid
GROUP BY c.cid, c.cname;
解析:连接后按班级分组计数;COUNT(s.id) 对NULL行不计数,没学生的班显示0。
【第20题】每个班级成绩最高的学生
SELECT s.* FROM student s
WHERE s.score = (
SELECT MAX(score) FROM student WHERE cid = s.cid
);
解析:关联子查询:外层每行都用它自己的 cid 去内层查该班最高分。
【第21题】孤儿数据
SELECT * FROM student WHERE cid IS NULL;
解析:判断“没有”用 IS NULL,不是 = NULL。
【第22题】视图
CREATE VIEW v_stu_score AS
SELECT s.name, c.cname, s.score
FROM student s LEFT JOIN class c ON s.cid = c.cid;
SELECT * FROM v_stu_score WHERE score > 80;
解析:视图是“存起来的SELECT”,查询时像表一样用,封装复杂逻辑。
【第23题】前缀模糊查询走索引
CREATE INDEX idx_name ON student(name);
EXPLAIN SELECT * FROM student WHERE name LIKE '张%';
解析:'张%' 前缀固定,能利用索引;改成 '%张%' 或 '%张' 就无法用索引了。
【第24题】转账事务
-- 正常提交
START TRANSACTION;
UPDATE account SET money = money - 100 WHERE id = 1;
UPDATE account SET money = money + 100 WHERE id = 2;
COMMIT;
-- 出错回滚
START TRANSACTION;
UPDATE account SET money = money - 100 WHERE id = 1;
-- 假设这里发现余额不足
ROLLBACK; -- 所有修改撤销
解析:事务保证两步要么都生效(COMMIT),要么都不生效(ROLLBACK)。
【第25题】存储过程
DELIMITER //
CREATE PROCEDURE p_get_stu(IN c INT)
BEGIN
SELECT * FROM student WHERE cid = c;
END //
DELIMITER ;
CALL p_get_stu(1);
解析:IN 是输入参数;DELIMITER 改分隔符让 MySQL 认出 BEGIN...END 里的分号。
【第26题】触发器
CREATE TRIGGER trg_del_class
AFTER DELETE ON class
FOR EACH ROW
DELETE FROM student WHERE cid = OLD.cid;
解析:删除班级后自动删该班学生;OLD 指被删的那行旧数据。
【第27题】自连接
SELECT e.name 员工, m.name 上级
FROM emp e
LEFT JOIN emp m ON e.manager_id = m.id;
解析:同一张表起两个别名当两张表用,查上下级关系。
【第28题】班级平均分降序
SELECT c.cname, AVG(s.score) 平均分
FROM class c
LEFT JOIN student s ON s.cid = c.cid
GROUP BY c.cid, c.cname
ORDER BY 平均分 DESC;
解析:连接 → 分组 → 用别名排序。GROUP BY 的非聚合列要写全。
【第29题】低于平均分
SELECT * FROM student
WHERE score < (SELECT AVG(score) FROM student);
解析:子查询算全班平均,外层筛选低于它的。
【第30题】图书借阅系统(综合)
-- 三张表:图书、读者、借阅
CREATE TABLE book (
bid INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(50) NOT NULL
);
CREATE TABLE reader (
rid INT PRIMARY KEY AUTO_INCREMENT,
rname VARCHAR(20) NOT NULL
);
CREATE TABLE borrow (
id INT PRIMARY KEY AUTO_INCREMENT,
bid INT,
rid INT,
borrow_at DATE,
return_at DATE -- NULL表示还没还
);
-- 查询借了书还没还的读者
SELECT DISTINCT r.rname
FROM reader r
JOIN borrow b ON r.rid = b.rid
WHERE b.return_at IS NULL;
解析:典型的多对多关系(一本书可被多人借、一人可借多本),中间表 borrow 记录借还状态,return_at 为 NULL 就是没还。
第四篇 学完之后:后续学习方向与语言推荐
C/C++/MySQL 是地基,学完后按目标方向选择路线。每个方向都给出主语言和顺序,照着走不会迷路。
一、后端开发(岗位最多、最稳的选择)
-
主语言:Java 或 Go 或 Python,三选一深耕。
-
学习顺序:Java(语法→集合→多线程→JVM)或 Go(语法→并发→工程化)或 Python(语法→网络)→ Web框架(SpringBoot / Gin / Django / FastAPI)→ MySQL进阶(索引、事务、优化)→ Redis缓存 → 消息队列(Kafka/RabbitMQ)→ 微服务(注册中心、网关、链路追踪)→ 分布式理论(CAP、幂等、分布式事务)。
-
配套项目:商城系统、博客系统、秒杀系统。
二、嵌入式与底层开发(C的深度路线)
-
主语言:C 继续深入。
-
学习顺序:数据结构与算法 → 操作系统原理(进程线程、内存管理、文件系统)→ Linux系统编程(文件IO、进程、信号、网络编程)→ 驱动开发 / 单片机(STM32)→ ARM体系结构 → 实时操作系统RTOS。
-
配套:Linux环境、交叉编译、调试工具 gdb。
三、游戏开发(C++的深度路线)
-
主语言:C++ 继续深入。
-
学习顺序:C++内存模型与STL源码 → 数据结构与算法 → 计算机图形学(OpenGL/DirectX)→ 游戏引擎(Unity C# 或 Unreal C++)→ 物理引擎、动画系统 → 网络同步。
-
配套:独立小游戏项目(打砖块、坦克大战、3D小场景)。
四、机器学习与AI(Python路线,对标Python100天之后的进阶)
这是用户特别要求补全的方向。走机器学习,C/C++和MySQL都不会白学:C/C++帮你看懂底层实现,MySQL帮你管理训练数据。
阶段一 数学基础(1个月)
-
线性代数:矩阵、向量、特征值(推荐 3Blue1Brown 的线性代数可视化系列)。
-
概率统计:概率分布、期望方差、贝叶斯公式、最大似然估计。
-
微积分:导数、梯度、链式法则(深度学习的反向传播就是链式法则)。
阶段二 Python数据处理(1个月)
-
Python 语法:列表、字典、函数、类、文件(学完C++再学Python很快)。
-
NumPy:数组运算,是一切科学计算的基石。
-
Pandas:表格数据处理,增删改查、分组聚合,和SQL思路相通。
-
Matplotlib:画图可视化,理解数据分布。
阶段三 机器学习入门(2个月)
-
scikit-learn 经典算法:线性回归、逻辑回归、决策树、随机森林、KNN、K-Means、SVM。
-
核心概念:训练集/测试集、过拟合与欠拟合、交叉验证、评估指标(准确率、精确率、召回率、F1)。
-
实战:用Kaggle的泰坦尼克号、房价预测练手,跑通“数据清洗→训练→评估”全流程。
阶段四 深度学习(2个月)
-
PyTorch 基础:张量、自动求导、神经网络模块。
-
经典网络:全连接网络、CNN卷积网络(图像)、RNN/LSTM(序列)、Transformer(文本)。
-
实战:手写数字识别(MNIST)、图像分类(CIFAR-10)、简单文本分类。
阶段五 进阶方向(按兴趣选)
-
计算机视觉CV:目标检测(YOLO)、图像分割。
-
自然语言处理NLP:词向量、BERT、文本生成。
-
大模型方向:HuggingFace生态、LoRA微调、RAG检索增强、Prompt工程、部署(vLLM)。
-
数据挖掘/推荐系统:特征工程、排序模型、召回策略。
机器学习路线总结:数学 → Python数据处理 → 经典机器学习 → 深度学习 → 选定细分方向。过程中大量用到 MySQL(数据存储)、C/C++(源码阅读)、Python(主力语言)。
五、其他可选方向
-
全栈开发:前端(HTML/CSS/JavaScript → Vue/React)+ 后端任选一门 + MySQL,适合独立做产品。
-
数据库工程师/DBA:MySQL原理深挖:InnoDB引擎、主从复制、分库分表、高可用、性能调优。
-
测试开发:Python + 自动化测试框架(pytest/Selenium)+ 接口测试,需求量大。
-
云计算/运维:Linux → 容器Docker → Kubernetes → CI/CD,配合脚本语言。
六、给零基础的总路线图(一句话版)
| 阶段 | 内容 | 达成目标 |
|---|---|---|
| 第1个月 | C语言 | 过完本文100题,能独立写控制台小程序 |
| 第2个月 | C++ | 掌握类、继承、多态、STL,能写小型管理系统 |
| 第3个月 | MySQL | 熟练增删改查、多表查询、事务,能做简单表设计 |
| 第4–6个月 | 选方向深入 | 后端/嵌入式/游戏/AI 任选其一,做出第一个项目 |
最后的提醒:编程没有捷径,唯一有效的方法就是“看懂 → 敲一遍 → 出错 → 改对”。把本文的题目全部独立做完,你的C/C++/MySQL基础就超过了大多数初学者,可以放心进入下一阶段。
(注:部分内容可能由 AI 生成)

浙公网安备 33010602011771号