零基础也能学会: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 &gt;= 60) {
    printf("及格");
} else if (score &gt;= 85) {
    printf("优秀");
} else {
    printf("不及格");
}

// switch 多分支:适合判断离散的固定值
switch (grade) {
    case 'A': printf("优秀"); break;
    case 'B': printf("良好"); break;
    default:  printf("未知"); break;
}

循环结构:重复执行一段代码。

// for:知道循环次数时用
for (int i = 1; i &lt;= 10; i++) {
    printf("%d ", i);
}

// while:不知道次数、只看条件时用
int n = 1;
while (n &lt;= 10) {
    printf("%d ", n);
    n++;
}

// do-while:至少执行一次
int m = 1;
do {
    printf("%d ", m);
    m++;
} while (m &lt;= 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 &lt; 3; i++)
    for (int j = 0; j &lt; 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 &lt;string.h&gt;)
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 = &amp;x;    // &amp; 取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 = &amp;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 = &amp;s1;   // 结构体指针
printf("%s", ps-&gt;name);      // 用 -&gt; 访问成员

共用体 union:多个成员共享同一块内存,同一时刻只保存一个成员的值,大小取最大成员的大小。

3. 动态内存:malloc 与 free

静态数组长度写死了,不够灵活。动态内存可以在程序运行期间按需要申请内存,用完手动释放。

#include &lt;stdlib.h&gt;
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", &amp;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 &lt; n; i++) {
        struct Node *p = (struct Node*)malloc(sizeof(struct Node));
        p-&gt;data = arr[i];
        p-&gt;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题)

  1. 输出一行文字:Hello World。

  2. 定义变量存自己的姓名、年龄、学号并输出。

  3. 输入两个整数,输出它们的和、差、积、商(保留两位小数)。

  4. 输入3个整数,输出最大值和最小值。

  5. 输入一个整数,判断它是奇数还是偶数。

  6. 输入一个年份,判断是不是闰年(能被4整除且不能被100整除,或能被400整除)。

  7. 交换两个变量的值(要求不使用第三个变量)。

  8. 输入一个三位数,输出它的百位、十位、个位数字。

  9. 求 1+2+3+...+100 的和。

  10. 求 1~100 之间所有偶数的和。

  11. 输出 1~100 之间所有能被3整除的数,并统计个数。

  12. 输出九九乘法表。

  13. 输入一个百分制成绩,输出等级:90以上A、80以上B、70以上C、60以上D、60以下E。

  14. 输入年月,输出这个月有多少天(要考虑闰年2月)。

  15. 输入n,求 n 的阶乘(用循环,n≤20)。

  16. 输出斐波那契数列前20项(1,1,2,3,5,...)。

  17. 输入两个正整数,用辗转相除法求最大公约数。

  18. 输入两个正整数,求最小公倍数。

  19. 输入一个整数,判断它是不是回文数(如12321)。

  20. 输出100~999之间所有的水仙花数(各位数字立方和等于本身)。

模块二 数组与字符串(第21–45题)

  1. 输入10个整数存入数组,逆序输出。

  2. 输入10个整数,求最大值、最小值、平均值。

  3. 用冒泡排序把10个整数从小到大排序。

  4. 用选择排序把10个整数从小到大排序。

  5. 在有序数组中插入一个数,保持有序。

  6. 用二分法在有序数组中查找某个数,找到输出下标,找不到输出-1。

  7. 去掉数组中重复的元素(只保留第一个出现的)。

  8. 把数组元素循环右移n位。

  9. 合并两个升序数组为一个升序数组。

  10. 输入一个3×3矩阵,输出它的转置矩阵。

  11. 求一个3×3矩阵两条对角线元素之和。

  12. 输出杨辉三角前10行。

  13. 不调用库函数,求字符串长度。

  14. 不调用库函数,实现字符串拷贝。

  15. 不调用库函数,实现字符串拼接。

  16. 不调用库函数,实现字符串比较。

  17. 不调用库函数,实现字符串反转。

  18. 统计一段字符串中字母、数字、空格、其他字符各有多少个。

  19. 删除字符串中指定的字符。

  20. 判断一个字符串是不是回文串(正读反读一样)。

  21. 把字符串中的小写字母转大写,大写转小写。

  22. 统计一句话里有多少个单词(单词间用空格分隔)。

  23. 查找子串在主串中第一次出现的位置。

  24. 去掉字符串首尾的空格。

  25. 输入n个学生的姓名,按字典序排序输出。

模块三 函数(第46–60题)

  1. 写函数用指针交换两个整数。

  2. 写函数求 n 的阶乘(递归实现)。

  3. 写函数判断一个数是不是素数。

  4. 写递归函数求斐波那契数列第n项。

  5. 写递归函数求 1+2+...+n。

  6. 写递归函数求最大公约数。

  7. 写汉诺塔问题:把n个盘子从A移到C,输出移动步骤。

  8. 写函数求字符串长度、把字符串拷贝(封装前面模块二的实现)。

  9. 写函数对数组做冒泡排序。

  10. 写函数求数组元素之和。

  11. 用静态局部变量统计函数被调用的次数。

  12. 写函数,通过指针参数一次性返回一个数的商和余数。

  13. 写函数计算组合数 C(n,m)。

  14. 用牛顿迭代法求一个数的平方根(误差1e-6)。

  15. 写函数输出1~n之间的所有素数(调用判断素数函数)。

模块四 指针(第61–80题)

  1. 定义变量并输出它的值和地址。

  2. 用指针交换两个整数。

  3. 用指针遍历数组并输出每个元素。

  4. 用指针把数组逆序。

  5. 用指针实现字符串反转。

  6. 用指针实现字符串拷贝。

  7. 用指针数组保存多个字符串,并按字典序排序。

  8. 定义函数指针,调用加法、减法函数。

  9. 用行指针遍历二维数组。

  10. 说出并演示指针数组和数组指针的区别。

  11. 判断并演示什么是空指针、野指针、悬垂指针。

  12. 演示 const int *p、int *const p、const int *const p 三种写法的区别。

  13. 写排序函数,参数接收函数指针(实现升序/降序切换)。

  14. 用二级指针修改一级指针的指向。

  15. 写函数用二级指针在函数内给指针分配内存。

  16. 用malloc动态创建数组,输入n个成绩并求和。

  17. 用malloc创建动态二维数组(矩阵),输入并输出。

  18. 动态创建学生成绩数组,求平均分并输出不及格的学生。

  19. 演示并解释内存泄漏:申请后不释放,观察后果。

  20. 用指针偏移访问结构体数组的每个成员。

模块五 结构体与动态内存(第81–90题)

  1. 定义学生结构体(学号、姓名、成绩),输入3个学生并输出。

  2. 把学生结构体数组按成绩从高到低排序。

  3. 用结构体指针访问并修改学生信息。

  4. 定义结构体嵌套:班级结构体里包含学生结构体数组。

  5. 对比结构体传值和传指针,说明哪个效率高、为什么。

  6. 用malloc动态创建学生结构体数组。

  7. 定义链表节点结构体,创建5个节点并连成链表。

  8. 用头插法创建链表,输出所有节点。

  9. 遍历链表,统计节点个数。

  10. 删除链表中指定值的节点。

模块六 文件操作(第91–100题)

  1. 把一段文字写入文本文件。

  2. 读取文本文件内容并输出到屏幕。

  3. 复制一个文件到另一个文件。

  4. 统计一个文件里的字符个数。

  5. 向文件末尾追加一行内容(不覆盖原有内容)。

  6. 读取文件里的若干整数,求它们的和。

  7. 把3个学生信息写入文件。

  8. 从文件读回学生信息并打印。

  9. 把文件内容按行倒序输出。

  10. 用二进制方式写入一个整数数组,再读出来。

五、C语言100道题答案解析(先做题再看这里)

所有代码都完整可运行。建议每题先自己写,写不出来再看答案,对照解析理解思路。

模块一 基础语法答案(第1–20题)

【第1题】输出Hello World

#include &lt;stdio.h&gt;
int main() { printf("Hello World\n"); return 0; }

解析:固定结构:头文件 + main函数 + 分号。printf的\n是换行符。

【第2题】输出个人信息

#include &lt;stdio.h&gt;
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 &lt;stdio.h&gt;
int main() {
    int a, b;
    scanf("%d%d", &amp;a, &amp;b);
    printf("和:%d 差:%d 积:%d\n", a+b, a-b, a*b);
    printf("商:%.2f\n", (double)a / b);   // 转成double再除
    return 0;
}

解析:要得到小数,必须先转成浮点数再除,%.2f保留两位小数。

【第4题】最大值最小值

#include &lt;stdio.h&gt;
int main() {
    int a, b, c, max, min;
    scanf("%d%d%d", &amp;a, &amp;b, &amp;c);
    max = min = a;
    if (b &gt; max) max = b;
    if (c &gt; max) max = c;
    if (b &lt; min) min = b;
    if (c &lt; min) min = c;
    printf("最大值:%d 最小值:%d\n", max, min);
    return 0;
}

解析:先假设第一个数最大最小,再逐个比较更新。

【第5题】奇偶判断

#include &lt;stdio.h&gt;
int main() {
    int n; scanf("%d", &amp;n);
    if (n % 2 == 0) printf("偶数\n");
    else printf("奇数\n");
    return 0;
}

解析:对2取余,余0是偶数。注意==才是判断。

【第6题】闰年判断

#include &lt;stdio.h&gt;
int main() {
    int y; scanf("%d", &amp;y);
    if ((y%4==0 &amp;&amp; y%100!=0) || y%400==0)
        printf("闰年\n");
    else
        printf("平年\n");
    return 0;
}

解析:闰年规则:能被4整除且不能被100整除,或者能被400整除。

【第7题】不用第三变量交换

#include &lt;stdio.h&gt;
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 &lt;stdio.h&gt;
int main() {
    int n; scanf("%d", &amp;n);
    printf("百位:%d 十位:%d 个位:%d\n", n/100, n/10%10, n%10);
    return 0;
}

解析:整除100得百位;先整除10再取余10得十位;取余10得个位。

【第9题】1加到100

#include &lt;stdio.h&gt;
int main() {
    int sum = 0;
    for (int i = 1; i &lt;= 100; i++) sum += i;
    printf("%d\n", sum);
    return 0;
}

解析:累加器sum初值为0,循环里sum += i。结果是5050。

【第10题】偶数和

#include &lt;stdio.h&gt;
int main() {
    int sum = 0;
    for (int i = 2; i &lt;= 100; i += 2) sum += i;
    printf("%d\n", sum);
    return 0;
}

解析:直接从2开始每次加2,比判断%2更高效。

【第11题】能被3整除的数及个数

#include &lt;stdio.h&gt;
int main() {
    int count = 0;
    for (int i = 1; i &lt;= 100; i++)
        if (i % 3 == 0) { printf("%d ", i); count++; }
    printf("\n共%d个\n", count);
    return 0;
}

解析:count做计数器,每命中一次加1。

【第12题】九九乘法表

#include &lt;stdio.h&gt;
int main() {
    for (int i = 1; i &lt;= 9; i++) {
        for (int j = 1; j &lt;= i; j++)
            printf("%d*%d=%-2d ", j, i, i*j);
        printf("\n");
    }
    return 0;
}

解析:外层控制行,内层控制列,内层循环到i为止形成三角形,%-2d左对齐占2位。

【第13题】成绩等级

#include &lt;stdio.h&gt;
int main() {
    int s; scanf("%d", &amp;s);
    if (s &gt;= 90) printf("A\n");
    else if (s &gt;= 80) printf("B\n");
    else if (s &gt;= 70) printf("C\n");
    else if (s &gt;= 60) printf("D\n");
    else printf("E\n");
    return 0;
}

解析:else if 从上往下匹配,命中一个就结束,顺序不能乱。

【第14题】月份天数

#include &lt;stdio.h&gt;
int main() {
    int y, m, days;
    scanf("%d%d", &amp;y, &amp;m);
    if (m == 2) {
        if ((y%4==0 &amp;&amp; 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 &lt;stdio.h&gt;
int main() {
    int n; long long f = 1;
    scanf("%d", &amp;n);
    for (int i = 2; i &lt;= n; i++) f *= i;
    printf("%lld\n", f);
    return 0;
}

解析:n=20时20!超过int范围,用long long存结果,%lld输出。

【第16题】斐波那契数列

#include &lt;stdio.h&gt;
int main() {
    int a = 1, b = 1;
    printf("%d %d ", a, b);
    for (int i = 3; i &lt;= 20; i++) {
        int c = a + b;
        printf("%d ", c);
        a = b; b = c;
    }
    printf("\n");
    return 0;
}

解析:每项等于前两项之和,用三个变量滚动更新。

【第17题】最大公约数(辗转相除法)

#include &lt;stdio.h&gt;
int main() {
    int a, b, r;
    scanf("%d%d", &amp;a, &amp;b);
    while (b != 0) { r = a % b; a = b; b = r; }
    printf("%d\n", a);
    return 0;
}

解析:辗转相除:大数对小数取余,反复交换,余数为0时a就是最大公约数。

【第18题】最小公倍数

#include &lt;stdio.h&gt;
int main() {
    int a, b, x, y, r;
    scanf("%d%d", &amp;a, &amp;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 &lt;stdio.h&gt;
int main() {
    int n, m, rev = 0;
    scanf("%d", &amp;n);
    m = n;
    while (m &gt; 0) { rev = rev * 10 + m % 10; m /= 10; }
    if (rev == n) printf("是回文数\n");
    else printf("不是回文数\n");
    return 0;
}

解析:把数字倒过来(每次取末位,拼到rev后面),和原数比较。

【第20题】水仙花数

#include &lt;stdio.h&gt;
int main() {
    for (int n = 100; n &lt;= 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 &lt;stdio.h&gt;
int main() {
    int a[10];
    for (int i = 0; i &lt; 10; i++) scanf("%d", &amp;a[i]);
    for (int i = 9; i &gt;= 0; i--) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:从最后一个下标9往前输出即可。

【第22题】最大值最小值平均值

#include &lt;stdio.h&gt;
int main() {
    int a[10], max, min, sum = 0;
    for (int i = 0; i &lt; 10; i++) scanf("%d", &amp;a[i]);
    max = min = a[0];
    for (int i = 1; i &lt; 10; i++) {
        if (a[i] &gt; max) max = a[i];
        if (a[i] &lt; 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 &lt;stdio.h&gt;
int main() {
    int a[10] = {5,2,9,1,5,6,3,8,0,7};
    for (int i = 0; i &lt; 9; i++)              // 共9轮
        for (int j = 0; j &lt; 9 - i; j++)      // 每轮少比较一个
            if (a[j] &gt; a[j+1]) {
                int t = a[j]; a[j] = a[j+1]; a[j+1] = t;
            }
    for (int i = 0; i &lt; 10; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:相邻两两比较,大的往后冒,每轮确定一个最大值,内层长度递减。

【第24题】选择排序

#include &lt;stdio.h&gt;
int main() {
    int a[10] = {5,2,9,1,5,6,3,8,0,7};
    for (int i = 0; i &lt; 9; i++) {
        int min = i;
        for (int j = i+1; j &lt; 10; j++)
            if (a[j] &lt; a[min]) min = j;    // 找最小下标
        int t = a[i]; a[i] = a[min]; a[min] = t;  // 交换到前面
    }
    for (int i = 0; i &lt; 10; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:每轮找剩余部分的最小值,放到前面,比冒泡交换次数少。

【第25题】有序数组插入

#include &lt;stdio.h&gt;
int main() {
    int a[11] = {1,3,5,7,9,11,13,15,17,19};
    int x, i;
    scanf("%d", &amp;x);
    for (i = 9; i &gt;= 0 &amp;&amp; a[i] &gt; x; i--)
        a[i+1] = a[i];           // 大于x的元素往后挪
    a[i+1] = x;                  // 插入到腾出的位置
    for (i = 0; i &lt; 11; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:从后往前找插入点,边找边把元素后移,最后插入。

【第26题】二分查找

#include &lt;stdio.h&gt;
int main() {
    int a[10] = {1,3,5,7,9,11,13,15,17,19};
    int x, low = 0, high = 9, mid;
    scanf("%d", &amp;x);
    while (low &lt;= high) {
        mid = (low + high) / 2;
        if (a[mid] == x) { printf("下标:%d\n", mid); return 0; }
        else if (a[mid] &lt; x) low = mid + 1;
        else high = mid - 1;
    }
    printf("-1\n");
    return 0;
}

解析:前提是有序。每次砍掉一半,log2(n)次就能找到,比顺序查找快得多。

【第27题】数组去重

#include &lt;stdio.h&gt;
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 &lt; n; i++) {
        int dup = 0;
        for (int j = 0; j &lt; k; j++)
            if (a[j] == a[i]) { dup = 1; break; }
        if (!dup) a[k++] = a[i];   // 不重复就保留到前面
    }
    for (int i = 0; i &lt; k; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:新数组里逐个查重,没出现过的往前放,k记录有效长度。

【第28题】数组循环右移n位

#include &lt;stdio.h&gt;
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 &lt; 10; i++)
        temp[(i + n) % 10] = a[i];   // 每个元素搬到新位置
    for (int i = 0; i &lt; 10; i++) { a[i] = temp[i]; printf("%d ", a[i]); }
    printf("\n");
    return 0;
}

解析:借助临时数组,下标(i+n)%10实现循环搬移。也可以三次反转实现原地右移。

【第29题】合并两个升序数组

#include &lt;stdio.h&gt;
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 &lt; 5 &amp;&amp; j &lt; 5) {
        if (a[i] &lt; b[j]) c[k++] = a[i++];
        else c[k++] = b[j++];
    }
    while (i &lt; 5) c[k++] = a[i++];   // 剩余部分
    while (j &lt; 5) c[k++] = b[j++];
    for (i = 0; i &lt; 10; i++) printf("%d ", c[i]);
    printf("\n");
    return 0;
}

解析:双指针归并:谁小谁进结果,最后把剩余的接上。

【第30题】矩阵转置

#include &lt;stdio.h&gt;
int main() {
    int a[3][3] = {{1,2,3},{4,5,6},{7,8,9}}, t[3][3];
    for (int i = 0; i &lt; 3; i++)
        for (int j = 0; j &lt; 3; j++)
            t[j][i] = a[i][j];      // 行列互换
    for (int i = 0; i &lt; 3; i++) {
        for (int j = 0; j &lt; 3; j++) printf("%d ", t[i][j]);
        printf("\n");
    }
    return 0;
}

解析:转置就是把 a[i][j] 放到 t[j][i],行变列、列变行。

【第31题】对角线之和

#include &lt;stdio.h&gt;
int main() {
    int a[3][3] = {{1,2,3},{4,5,6},{7,8,9}}, sum = 0;
    for (int i = 0; i &lt; 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 &lt;stdio.h&gt;
int main() {
    int a[10][10] = {0};
    for (int i = 0; i &lt; 10; i++) {
        a[i][0] = a[i][i] = 1;            // 两边都是1
        for (int j = 1; j &lt; i; j++)
            a[i][j] = a[i-1][j-1] + a[i-1][j];  // 肩上两数之和
    }
    for (int i = 0; i &lt; 10; i++) {
        for (int j = 0; j &lt;= i; j++) printf("%4d", a[i][j]);
        printf("\n");
    }
    return 0;
}

解析:每个数等于它左上方和右上方两数之和,两边补1。

【第33题】字符串长度

#include &lt;stdio.h&gt;
int myStrlen(char *s) {
    int len = 0;
    while (*s != '\0') { len++; s++; }
    return len;
}

解析:指针从头走,遇到结束符\0停下,走过的步数就是长度。

【第34题】字符串拷贝

#include &lt;stdio.h&gt;
void myStrcpy(char *dest, char *src) {
    while (*src) *dest++ = *src++;
    *dest = '\0';   // 别忘了补结束符
}

解析:逐字符搬移,最后手动补\0,否则打印会越界。

【第35题】字符串拼接

#include &lt;stdio.h&gt;
void myStrcat(char *dest, char *src) {
    while (*dest) dest++;        // 先走到dest末尾
    while (*src) *dest++ = *src++; // 再把src接上去
    *dest = '\0';
}

解析:先让指针走到第一个字符串末尾,再把第二个字符串接上。

【第36题】字符串比较

#include &lt;stdio.h&gt;
int myStrcmp(char *a, char *b) {
    while (*a &amp;&amp; *b &amp;&amp; *a == *b) { a++; b++; }
    return *a - *b;   // 0相等,正数a大,负数b大
}

解析:逐字符比较,遇到不同或结束就返回差值。

【第37题】字符串反转

#include &lt;stdio.h&gt;
void reverse(char *s) {
    char *end = s;
    while (*end) end++;   // end指向末尾\0
    end--;
    while (s &lt; end) {
        char t = *s; *s = *end; *end = t;
        s++; end--;
    }
}

解析:首尾双指针向中间靠拢,交换对应字符。

【第38题】字符分类统计

#include &lt;stdio.h&gt;
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]&gt;='a'&amp;&amp;s[i]&lt;='z')||(s[i]&gt;='A'&amp;&amp;s[i]&lt;='Z')) letter++;
        else if (s[i]&gt;='0' &amp;&amp; s[i]&lt;='9') digit++;
        else if (s[i]==' ') space++;
        else other++;
    }
    printf("字母%d 数字%d 空格%d 其他%d\n", letter, digit, space, other);
    return 0;
}

解析:用ASCII范围判断类型,四个计数器分别累加。

【第39题】删除指定字符

#include &lt;stdio.h&gt;
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 &lt;stdio.h&gt;
#include &lt;string.h&gt;
int main() {
    char s[100];
    gets(s);   // 练习时可用,实际项目用fgets
    int i = 0, j = strlen(s) - 1, ok = 1;
    while (i &lt; j) {
        if (s[i] != s[j]) { ok = 0; break; }
        i++; j--;
    }
    printf(ok ? "是回文\n" : "不是回文\n");
    return 0;
}

解析:首尾字符逐个比较,全部相等就是回文串。

【第41题】大小写转换

#include &lt;stdio.h&gt;
int main() {
    char s[100] = "Hello World";
    for (int i = 0; s[i]; i++) {
        if (s[i]&gt;='a' &amp;&amp; s[i]&lt;='z') s[i] -= 32;   // 小写转大写
        else if (s[i]&gt;='A' &amp;&amp; s[i]&lt;='Z') s[i] += 32; // 大写转小写
    }
    printf("%s\n", s);
    return 0;
}

解析:ASCII中大写和小写相差32,加32或减32即可互换。

【第42题】单词计数

#include &lt;stdio.h&gt;
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 &lt;stdio.h&gt;
#include &lt;string.h&gt;
int main() {
    char s[100] = "hello world", sub[20] = "world";
    int n = strlen(s), m = strlen(sub);
    for (int i = 0; i &lt;= n - m; i++) {
        int j = 0;
        while (j &lt; m &amp;&amp; s[i+j] == sub[j]) j++;
        if (j == m) { printf("位置:%d\n", i); return 0; }
    }
    printf("-1\n");
    return 0;
}

解析:暴力匹配:从每个位置开始逐字符比对子串。

【第44题】去掉首尾空格

#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
void trim(char *s) {
    int i = 0, len = strlen(s);
    while (s[i] == ' ') i++;          // 跳过开头空格
    int j = len - 1;
    while (j &gt;= i &amp;&amp; s[j] == ' ') j--;  // 跳过结尾空格
    int k = 0;
    while (i &lt;= j) s[k++] = s[i++];
    s[k] = '\0';
}

解析:分别找到第一个和最后一个非空格位置,把中间部分前移。

【第45题】姓名按字典序排序

#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
int main() {
    char name[5][20] = {"Tom","Alice","Bob","David","Cathy"};
    for (int i = 0; i &lt; 4; i++)
        for (int j = 0; j &lt; 4 - i; j++)
            if (strcmp(name[j], name[j+1]) &gt; 0) {
                char t[20]; strcpy(t, name[j]);
                strcpy(name[j], name[j+1]);
                strcpy(name[j+1], t);
            }
    for (int i = 0; i &lt; 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 &lt;= 1) return 1;        // 递归出口
    return n * fact(n - 1);      // 递归调用
}

解析:递归两要素:出口(n=1时返回1)+ 递推式(n * fact(n-1))。

【第48题】判断素数

#include &lt;math.h&gt;
int isPrime(int n) {
    if (n &lt; 2) return 0;
    for (int i = 2; i * i &lt;= n; i++)   // 只试到根号n
        if (n % i == 0) return 0;
    return 1;
}

解析:一个数若有因子,必有一个不超过它的平方根,所以只需试到根号n。

【第49题】递归求斐波那契第n项

int fib(int n) {
    if (n &lt;= 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 &lt;stdio.h&gt;
void hanoi(int n, char A, char B, char C) {
    if (n == 1) { printf("%c-&gt;%c\n", A, C); return; }
    hanoi(n-1, A, C, B);   // n-1个盘子A移到B,C当辅助
    printf("%c-&gt;%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 &lt; n-1; i++)
        for (int j = 0; j &lt; n-1-i; j++)
            if (a[j] &gt; 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 &lt; n; i++) s += a[i];
    return s;
}

解析:遍历累加,返回总和。注意数组长度要作为参数传入。

【第56题】静态变量统计调用次数

#include &lt;stdio.h&gt;
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 &lt;math.h&gt;
double sqrt_newton(double x) {
    double g = x;
    while (fabs(g*g - x) &gt; 1e-6)
        g = (g + x/g) / 2;    // 迭代公式
    return g;
}

解析:猜测值g不断用公式 (g + x/g)/2 逼近真实平方根,直到误差足够小。

【第60题】输出1~n的素数

#include &lt;stdio.h&gt;
int isPrime(int n) {
    if (n &lt; 2) return 0;
    for (int i = 2; i * i &lt;= n; i++)
        if (n % i == 0) return 0;
    return 1;
}
int main() {
    int n; scanf("%d", &amp;n);
    for (int i = 2; i &lt;= n; i++)
        if (isPrime(i)) printf("%d ", i);
    printf("\n");
    return 0;
}

解析:复用第48题的判断函数,主函数只管遍历和输出,体现模块化。

模块四 指针答案(第61–80题)

【第61题】输出值和地址

#include &lt;stdio.h&gt;
int main() {
    int x = 10;
    int *p = &amp;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 &lt;stdio.h&gt;
int main() {
    int a[5] = {10,20,30,40,50};
    for (int *p = a; p &lt; a+5; p++)
        printf("%d ", *p);
    printf("\n");
    return 0;
}

解析:指针从数组头走到尾,每步+1跳过一个int。

【第64题】指针逆序数组

#include &lt;stdio.h&gt;
int main() {
    int a[5] = {1,2,3,4,5};
    int *s = a, *e = a+4;
    while (s &lt; e) {
        int t = *s; *s = *e; *e = t;
        s++; e--;
    }
    for (int i = 0; i &lt; 5; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:首尾双指针交换,和字符串反转同思路。

【第65题】指针实现字符串反转

#include &lt;stdio.h&gt;
void reverse(char *s) {
    char *e = s;
    while (*e) e++;
    e--;
    while (s &lt; 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 &lt;stdio.h&gt;
#include &lt;string.h&gt;
int main() {
    char *names[] = {"Tom","Alice","Bob","David"};
    for (int i = 0; i &lt; 3; i++)
        for (int j = 0; j &lt; 3-i; j++)
            if (strcmp(names[j], names[j+1]) &gt; 0) {
                char *t = names[j];
                names[j] = names[j+1];
                names[j+1] = t;   // 只换指针,不用搬字符串
            }
    for (int i = 0; i &lt; 4; i++) printf("%s\n", names[i]);
    return 0;
}

解析:指针数组里每个元素是char*,交换时只交换指针,效率高。

【第68题】函数指针

#include &lt;stdio.h&gt;
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 &lt;stdio.h&gt;
int main() {
    int a[2][3] = {{1,2,3},{4,5,6}};
    int (*p)[3] = a;        // 行指针:指向“含3个int的一行”
    for (int i = 0; i &lt; 2; i++)
        for (int j = 0; j &lt; 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 &lt;stdio.h&gt;
int asc(int a, int b) { return a &gt; b; }
int desc(int a, int b) { return a &lt; b; }
void sort(int a[], int n, int (*cmp)(int,int)) {
    for (int i = 0; i &lt; n-1; i++)
        for (int j = 0; j &lt; 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 &lt;stdio.h&gt;
void change(int **pp, int *target) {
    *pp = target;   // 通过二级指针修改一级指针的指向
}
int main() {
    int a = 1, b = 2, *p = &amp;a;
    change(&amp;p, &amp;b);   // 传一级指针的地址
    printf("%d\n", *p);  // 输出2
    return 0;
}

解析:要修改“指针变量本身”,必须传它的地址,即二级指针。

【第75题】函数内用二级指针分配内存

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
void alloc(int **pp, int n) {
    *pp = (int*)malloc(n * sizeof(int));
}
int main() {
    int *p = NULL;
    alloc(&amp;p, 5);      // 函数内给p分配内存
    if (p) { p[0] = 100; printf("%d\n", p[0]); }
    free(p);
    return 0;
}

解析:malloc必须配合二级指针才能把新地址带回调用方。

【第76题】malloc动态数组求和

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
int main() {
    int n, sum = 0;
    scanf("%d", &amp;n);
    int *a = (int*)malloc(n * sizeof(int));
    if (a == NULL) return -1;
    for (int i = 0; i &lt; n; i++) { scanf("%d", &amp;a[i]); sum += a[i]; }
    printf("%d\n", sum);
    free(a);
    return 0;
}

解析:长度n由运行期决定,malloc申请后当数组用,记得释放。

【第77题】动态二维数组

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
int main() {
    int rows = 3, cols = 4;
    int **m = (int**)malloc(rows * sizeof(int*));
    for (int i = 0; i &lt; rows; i++)
        m[i] = (int*)malloc(cols * sizeof(int));
    for (int i = 0; i &lt; rows; i++)
        for (int j = 0; j &lt; cols; j++)
            m[i][j] = i * cols + j;
    for (int i = 0; i &lt; rows; i++) {
        for (int j = 0; j &lt; cols; j++) printf("%d ", m[i][j]);
        printf("\n");
    }
    for (int i = 0; i &lt; rows; i++) free(m[i]);
    free(m);
    return 0;
}

解析:先给“行指针数组”分配内存,再给每行分配;释放顺序反过来。

【第78题】动态成绩数组求平均并输出不及格

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
int main() {
    int n; float sum = 0;
    scanf("%d", &amp;n);
    float *s = (float*)malloc(n * sizeof(float));
    for (int i = 0; i &lt; n; i++) { scanf("%f", &amp;s[i]); sum += s[i]; }
    printf("平均:%.1f\n不及格:", sum / n);
    for (int i = 0; i &lt; n; i++)
        if (s[i] &lt; 60) printf("%.0f ", s[i]);
    printf("\n");
    free(s);
    return 0;
}

解析:人数不确定时动态申请最合适,两道问题一次解决。

【第79题】内存泄漏演示

#include &lt;stdlib.h&gt;
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 &lt;stdio.h&gt;
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 &lt; 3; i++)
        printf("%d %s\n", (p+i)-&gt;id, (p+i)-&gt;name);  // 指针偏移
    return 0;
}

解析:结构体指针+1跳过整个结构体大小,用->访问成员。

模块五 结构体与动态内存答案(第81–90题)

【第81题】学生结构体输入输出

#include &lt;stdio.h&gt;
struct Student { int id; char name[20]; float score; };
int main() {
    struct Student s[3];
    for (int i = 0; i &lt; 3; i++)
        scanf("%d %s %f", &amp;s[i].id, s[i].name, &amp;s[i].score);
    for (int i = 0; i &lt; 3; i++)
        printf("%d %s %.1f\n", s[i].id, s[i].name, s[i].score);
    return 0;
}

解析:name是数组名本身就是地址,scanf里不用加&,其他要加。

【第82题】结构体按成绩排序

#include &lt;stdio.h&gt;
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 &lt; 2; i++)
        for (int j = 0; j &lt; 2-i; j++)
            if (s[j].score &lt; s[j+1].score) {   // 按成绩降序
                struct Student t = s[j];
                s[j] = s[j+1]; s[j+1] = t;      // 整个结构体交换
            }
    for (int i = 0; i &lt; 3; i++)
        printf("%d %s %.1f\n", s[i].id, s[i].name, s[i].score);
    return 0;
}

解析:结构体可以整体赋值交换(C语言允许),比较时取成员score。

【第83题】结构体指针修改信息

#include &lt;stdio.h&gt;
struct Student { int id; char name[20]; float score; };
int main() {
    struct Student s = {1, "A", 70};
    struct Student *p = &amp;s;
    p-&gt;score = 95;             // 通过指针改成员
    strcpy(p-&gt;name, "B");
    printf("%d %s %.1f\n", s.id, s.name, s.score);
    return 0;
}

解析:->是“指针访问成员”的专用运算符,字符串成员要用strcpy改。

【第84题】结构体嵌套

#include &lt;stdio.h&gt;
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-&gt;score = 0; }
// 大结构体应传指针;只想读不想改,加const保护

解析:结构体可能很大,传值会整块拷贝;传指针只传地址,更快。

【第86题】动态学生结构体数组

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
struct Student { int id; char name[20]; float score; };
int main() {
    int n; scanf("%d", &amp;n);
    struct Student *s = (struct Student*)malloc(n * sizeof(struct Student));
    for (int i = 0; i &lt; n; i++)
        scanf("%d %s %f", &amp;s[i].id, s[i].name, &amp;s[i].score);
    for (int i = 0; i &lt; n; i++)
        printf("%d %s %.1f\n", s[i].id, s[i].name, s[i].score);
    free(s);
    return 0;
}

解析:malloc按结构体大小申请n份,之后完全当数组用。

【第87题】链表节点与创建

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
struct Node { int data; struct Node *next; };
struct Node* createNode(int v) {
    struct Node *p = (struct Node*)malloc(sizeof(struct Node));
    p-&gt;data = v;
    p-&gt;next = NULL;
    return p;
}

解析:每个节点自己malloc,next初始指向NULL表示链表结尾。

【第88题】头插法创建链表

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
struct Node { int data; struct Node *next; };
int main() {
    struct Node *head = NULL;
    int v;
    while (scanf("%d", &amp;v) == 1 &amp;&amp; v != -1) {
        struct Node *p = (struct Node*)malloc(sizeof(struct Node));
        p-&gt;data = v;
        p-&gt;next = head;   // 新节点指向原头
        head = p;         // 新节点成为头
    }
    for (struct Node *p = head; p; p = p-&gt;next)
        printf("%d ", p-&gt;data);
    printf("\n");
    return 0;
}

解析:新节点永远插在最前面,所以输入顺序和输出顺序相反。

【第89题】统计节点个数

int count(struct Node *head) {
    int n = 0;
    while (head) { n++; head = head-&gt;next; }
    return n;
}

解析:从头走到尾,每过一个节点加1,走到NULL结束。

【第90题】删除指定值的节点

struct Node* delNode(struct Node *head, int v) {
    struct Node *cur = head, *prev = NULL;
    while (cur &amp;&amp; cur-&gt;data != v) { prev = cur; cur = cur-&gt;next; }
    if (cur == NULL) return head;       // 没找到
    if (prev == NULL) head = cur-&gt;next; // 删的是头节点
    else prev-&gt;next = cur-&gt;next;        // 中间节点:跳过它
    free(cur);
    return head;
}

解析:要删除必须记住前一个节点prev,改它的next跳过当前节点,再free。

模块六 文件操作答案(第91–100题)

【第91题】写入文本文件

#include &lt;stdio.h&gt;
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 &lt;stdio.h&gt;
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 &lt;stdio.h&gt;
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 &lt;stdio.h&gt;
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 &lt;stdio.h&gt;
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 &lt;stdio.h&gt;
int main() {
    FILE *fp = fopen("nums.txt", "r");
    if (fp == NULL) return -1;
    int x, sum = 0;
    while (fscanf(fp, "%d", &amp;x) == 1) sum += x;
    fclose(fp);
    printf("%d\n", sum);
    return 0;
}

解析:fscanf返回成功读到的个数,读不到整数时返回0或EOF,循环结束。

【第97题】学生信息写入文件

#include &lt;stdio.h&gt;
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 &lt; 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 &lt;stdio.h&gt;
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 &lt; 3; i++)
        fscanf(fp, "%d %s %f", &amp;s[i].id, s[i].name, &amp;s[i].score);
    for (int i = 0; i &lt; 3; i++)
        printf("%d %s %.1f\n", s[i].id, s[i].name, s[i].score);
    fclose(fp);
    return 0;
}

解析:格式和写入时完全一致才能正确读回,形成“存—取”闭环。

【第99题】文件内容按行倒序输出

#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
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 &gt;= 0; i--) printf("%s", lines[i]);
    fclose(fp);
    return 0;
}

解析:先整文件读进二维数组,再倒着输出。

【第100题】二进制读写整数数组

#include &lt;stdio.h&gt;
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 &lt; 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 &lt;iostream&gt;
using namespace std;
int main() {
    int n;
    cin &gt;&gt; n;          // 输入,不用写&amp;和格式符
    cout &lt;&lt; "n=" &lt;&lt; n &lt;&lt; endl;  // 输出,endl是换行
    return 0;
}

优点:不用记%d、%s 这些格式符,自动匹配类型;缺点:默认比 scanf/printf 慢,大数据量时加一行 ios::sync_with_stdio(false); 加速。

3. 引用:变量的别名

int x = 10;
int &amp;r = x;    // r是x的别名,同一块内存
r = 20;        // x也变成20

void swap(int &amp;a, int &amp;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 &lt; times; i++) cout &lt;&lt; s;
}

易忽略:重载只认参数,不认返回值;默认参数必须从右往左连续写。

5. 类与对象:封装

#include &lt;iostream&gt;
#include &lt;string&gt;
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 &lt;&lt; id &lt;&lt; " " &lt;&lt; name &lt;&lt; " " &lt;&lt; score &lt;&lt; endl;
    }
};

int main() {
    Student s(1, "张三", 88.5);   // 创建对象,自动调构造函数
    s.show();
    return 0;
}

三个核心概念:是模板,对象是实例;封装把数据私有化,只通过公有方法访问;构造函数在对象创建时自动执行,负责初始化。

6. 构造与析构

class Test {
public:
    Test() { cout &lt;&lt; "构造\n"; }       // 无参构造
    Test(int x) { cout &lt;&lt; "带参构造\n"; }
    ~Test() { cout &lt;&lt; "析构\n"; }      // 析构:对象销毁时自动调用,做清理
};

易忽略:构造函数和析构函数都没有返回值,连 void 都不能写;对象离开作用域时自动调用析构。

二、核心知识点(高级阶段)

1. 继承:代码复用

class Animal {                 // 基类(父类)
public:
    string name;
    void eat() { cout &lt;&lt; "吃东西\n"; }
};

class Dog : public Animal {    // 派生类(子类):继承父类
public:
    void bark() { cout &lt;&lt; "汪汪\n"; }
};

// Dog 对象同时拥有 name、eat()(来自父类)和 bark()(自己新增的)

继承让子类自动拥有父类的属性和方法,不用重写;还可以在子类里增加新功能或重写父类方法。

2. 多态:同一接口,不同实现

class Animal {
public:
    virtual void cry() { cout &lt;&lt; "动物叫\n"; }   // 虚函数
    virtual ~Animal() {}                          // 析构也加virtual
};
class Cat : public Animal {
public:
    void cry() override { cout &lt;&lt; "喵喵\n"; }     // 重写
};
class Dog : public Animal {
public:
    void cry() override { cout &lt;&lt; "汪汪\n"; }
};

int main() {
    Animal *p = new Cat();   // 父类指针指向子类对象
    p-&gt;cry();                // 输出"喵喵"——运行时才决定调谁
    delete p;
    return 0;
}

多态三要素:父类指针/引用 + 虚函数 virtual + 子类重写 override。少一个都不叫多态。

纯虚函数与抽象类:只声明不实现的虚函数叫纯虚函数(virtual void f() = 0;),含纯虚函数的类不能创建对象,只能当接口被继承,子类必须实现它。

3. STL 标准模板库:现成的好工具

容器 特点 典型用法
string 字符串 拼接、查找、截取、替换,比 char 数组好用
vector 动态数组 自动扩容,尾部增删快,支持随机访问
list 双向链表 任意位置插入删除快,不能随机访问
map 键值对(有序)
set 集合 元素唯一、自动排序,判断是否存在
algorithm 算法库 sort 排序、find 查找、reverse 反转
#include &lt;vector&gt;
#include &lt;string&gt;
#include &lt;map&gt;
#include &lt;algorithm&gt;
#include &lt;iostream&gt;
using namespace std;

int main() {
    vector&lt;int&gt; v = {5,2,8,1,9};
    sort(v.begin(), v.end());          // 排序:1 2 5 8 9
    v.push_back(100);                  // 尾部加元素
    cout &lt;&lt; v.size() &lt;&lt; endl;          // 元素个数

    string s = "hello";
    s += " world";                     // 拼接
    s.find("world");                   // 查找位置,找不到返回npos
    s.substr(6, 5);                    // 截取从6开始5个字符

    map&lt;int, string&gt; m;
    m[101] = "张三";                    // 增
    m[101] = "李四";                    // 改(key唯一)
    cout &lt;&lt; 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 &lt;memory&gt;
unique_ptr&lt;int&gt; u(new int(5));   // 独占
shared_ptr&lt;int&gt; s = make_shared&lt;int&gt;(5);  // 共享,引用计数归零自动释放

new/delete 和 malloc/free 不要混用:new 会调构造函数、delete 会调析构函数,malloc/free 不会。

5. 模板:写一次,处处通用

template &lt;typename T&gt;
T myMax(T a, T b) {
    return a &gt; b ? a : b;
}
// 用法:myMax(3, 5) 传int,myMax(3.5, 2.1) 传double,自动匹配

6. 异常处理

try {
    int x; cin &gt;&gt; x;
    if (x &lt; 0) throw "负数不允许";     // 抛出异常
} catch (const char *msg) {
    cout &lt;&lt; "捕获到:" &lt;&lt; msg &lt;&lt; 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. 写学生类:私有属性学号、姓名、成绩,提供设置和打印方法。

  2. 用构造函数初始化学生信息,并验证对象创建时自动调用。

  3. 用引用实现两个整数交换。

  4. 函数重载:分别实现整数、浮点数、字符串三个版本的求和/拼接输出。

  5. 默认参数:写一个打印函数,次数参数默认打印1次。

  6. 用 vector 存5个整数,排序后正序、逆序各输出一次。

  7. 用 string 完成:拼接、查找子串、截取、替换、判断是否包含。

  8. 用 map 实现学号到姓名的增删查改。

  9. 写一个计数器类,用静态成员统计创建了多少个对象。

  10. 写一个矩形类,成员函数求面积和周长。

  11. 写一个复数类,用重载实现两个复数相加。

  12. 写一个友元函数,访问类的私有成员。

  13. 用模板写一个通用的 swap 函数。

  14. 用 vector 存若干学生对象,按成绩排序输出。

  15. 用文件流把学生信息写入文件,再读出来。

进阶模块(第16–30题)

  1. 定义动物基类,派生猫、狗,虚函数实现多态叫声。

  2. 定义图形抽象类(纯虚函数求面积),派生圆形、矩形并实现。

  3. 写一个形状基类指针数组,分别指向圆形、矩形,统一输出面积(多态)。

  4. 用 vector 模拟栈:实现入栈、出栈、判断空。

  5. 用 map 统计一段文字中每个单词出现的次数。

  6. 用 set 对数组去重并排序。

  7. 用 list 实现任意位置插入、删除。

  8. 写一个类管理动态数组,实现深拷贝(拷贝构造+赋值运算符)。

  9. 用智能指针管理动态对象,验证自动释放。

  10. 写异常处理:输入为负数时抛出并捕获异常。

  11. 实现运算符重载:比较两个矩形大小。

  12. 写一个字符串类,重载 + 实现拼接。

  13. 实现继承:学生类继承人类,人类有姓名年龄,学生增加学号成绩。

  14. 实现模板类:通用的数组容器,支持存取。

  15. 综合题:设计图书管理类(增删查改),用 vector 存储并持久化到文件。

五、C++经典题答案解析(先做题再看这里)

基础模块答案(第1–15题)

【第1题】学生类封装

#include &lt;iostream&gt;
#include &lt;string&gt;
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 &lt;&lt; id &lt;&lt; " " &lt;&lt; name &lt;&lt; " " &lt;&lt; score &lt;&lt; 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 &lt;&lt; "对象创建\n";
    }
private:
    int id; string name; double score;
};
int main() {
    Student s(1, "张三", 88.5);   // 创建对象自动调用构造
    return 0;
}

解析:构造函数在对象创建那一刻自动执行,负责初始化,无需手动调用。

【第3题】引用交换

void swap(int &amp;a, int &amp;b) {
    int t = a; a = b; b = t;
}

解析:引用是别名,直接操作实参,比指针写法简洁安全。

【第4题】函数重载

#include &lt;iostream&gt;
#include &lt;string&gt;
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 &lt;&lt; add(1, 2) &lt;&lt; endl;          // 3
    cout &lt;&lt; add(1.5, 2.5) &lt;&lt; endl;      // 4
    cout &lt;&lt; add("ab", "cd") &lt;&lt; endl;    // abcd
    return 0;
}

解析:同名函数参数类型不同,编译器按实参自动选版本。注意:返回值不同不算重载。

【第5题】默认参数

#include &lt;iostream&gt;
using namespace std;
void printMsg(string s, int times = 1) {   // times默认1
    for (int i = 0; i &lt; times; i++) cout &lt;&lt; s &lt;&lt; endl;
}
int main() {
    printMsg("hi");        // 打印1次
    printMsg("hi", 3);     // 打印3次
    return 0;
}

解析:默认参数从右往左连续设置,调用时可省略有默认值的参数。

【第6题】vector 排序正逆序

#include &lt;vector&gt;
#include &lt;algorithm&gt;
#include &lt;iostream&gt;
using namespace std;
int main() {
    vector&lt;int&gt; v = {5,2,8,1,9};
    sort(v.begin(), v.end());                 // 升序
    for (int x : v) cout &lt;&lt; x &lt;&lt; " ";
    cout &lt;&lt; endl;
    sort(v.rbegin(), v.rend());               // 逆序迭代器=降序
    for (int x : v) cout &lt;&lt; x &lt;&lt; " ";
    cout &lt;&lt; endl;
    return 0;
}

解析:sort 默认升序;用 rbegin/rend 逆序迭代器直接降序,不用写比较函数。

【第7题】string 常用操作

#include &lt;iostream&gt;
#include &lt;string&gt;
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 &lt;&lt; "包含" &lt;&lt; endl;
    cout &lt;&lt; s &lt;&lt; endl;
    return 0;
}

解析:find 找不到返回 string::npos;substr(起始, 长度);replace(起始, 长度, 新内容)。

【第8题】map 增删查改

#include &lt;map&gt;
#include &lt;iostream&gt;
#include &lt;string&gt;
using namespace std;
int main() {
    map&lt;int, string&gt; m;
    m[101] = "张三";            // 增
    m[102] = "李四";
    m[101] = "张大三";          // 改:key存在就覆盖
    cout &lt;&lt; m[101] &lt;&lt; endl;     // 查
    m.erase(102);               // 删
    if (m.count(101)) cout &lt;&lt; "101存在" &lt;&lt; endl;  // 判断是否存在
    return 0;
}

解析:map 按键自动排序,key 唯一;[] 不存在时会自动创建默认值。

【第9题】静态成员统计对象个数

#include &lt;iostream&gt;
using namespace std;
class Counter {
public:
    Counter() { count++; }
    ~Counter() { count--; }
    static int count;            // 静态成员:所有对象共享
};
int Counter::count = 0;          // 类外初始化(必须)
int main() {
    Counter a, b, c;
    cout &lt;&lt; Counter::count &lt;&lt; endl;   // 3
    return 0;
}

解析:静态成员属于类而不属于某个对象,构造+1、析构-1,就能统计存活对象数。

【第10题】矩形类面积周长

class Rect {
private:
    double w, h;
public:
    Rect(double w, double h) { this-&gt;w = w; this-&gt;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-&gt;r = r; this-&gt;i = i; }
    Complex operator+(const Complex &amp;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 &lt;iostream&gt;
using namespace std;
class Box {
private:
    int secret = 42;
    friend void showSecret(Box b);   // 声明友元
};
void showSecret(Box b) {             // 友元实现:可访问私有成员
    cout &lt;&lt; b.secret &lt;&lt; endl;
}

解析:友元函数不是成员函数,但被授权可以访问私有成员。谨慎使用,破坏封装。

【第13题】模板 swap

template &lt;typename T&gt;
void mySwap(T &amp;a, T &amp;b) {
    T t = a; a = b; b = t;
}
// 用法:mySwap(x, y); 传int、double、string都能用

解析:模板让函数适用于任意类型,编译器按调用类型自动生成对应版本。

【第14题】vector 存对象排序

#include &lt;vector&gt;
#include &lt;algorithm&gt;
#include &lt;iostream&gt;
using namespace std;
class Student {
public:
    string name; double score;
    Student(string n, double s) { name = n; score = s; }
};
bool cmp(const Student &amp;a, const Student &amp;b) {
    return a.score &gt; b.score;    // 按成绩降序
}
int main() {
    vector&lt;Student&gt; v;
    v.push_back(Student("张三", 88));
    v.push_back(Student("李四", 76));
    v.push_back(Student("王五", 92));
    sort(v.begin(), v.end(), cmp);
    for (auto &amp;s : v) cout &lt;&lt; s.name &lt;&lt; " " &lt;&lt; s.score &lt;&lt; endl;
    return 0;
}

解析:sort 第三个参数传自定义比较函数,就能按自己的规则排序对象。

【第15题】文件流读写

#include &lt;fstream&gt;
#include &lt;iostream&gt;
#include &lt;string&gt;
using namespace std;
int main() {
    ofstream out("stu.txt");          // 输出文件流:写
    out &lt;&lt; 1 &lt;&lt; " " &lt;&lt; "张三" &lt;&lt; " " &lt;&lt; 88.5 &lt;&lt; endl;
    out.close();

    ifstream in("stu.txt");           // 输入文件流:读
    int id; string name; double score;
    in &gt;&gt; id &gt;&gt; name &gt;&gt; score;
    cout &lt;&lt; id &lt;&lt; " " &lt;&lt; name &lt;&lt; " " &lt;&lt; score &lt;&lt; endl;
    in.close();
    return 0;
}

解析:fstream 让文件读写像 cin/cout 一样简单,不用记格式符。

进阶模块答案(第16–30题)

【第16题】多态动物叫声

#include &lt;iostream&gt;
using namespace std;
class Animal {
public:
    virtual void cry() { cout &lt;&lt; "动物叫" &lt;&lt; endl; }
    virtual ~Animal() {}
};
class Cat : public Animal {
public:
    void cry() override { cout &lt;&lt; "喵喵" &lt;&lt; endl; }
};
class Dog : public Animal {
public:
    void cry() override { cout &lt;&lt; "汪汪" &lt;&lt; endl; }
};
int main() {
    Animal *p = new Cat();
    p-&gt;cry();      // 喵喵
    delete p;
    p = new Dog();
    p-&gt;cry();      // 汪汪
    delete p;
    return 0;
}

解析:父类指针调用虚函数,运行期根据实际对象类型决定调用哪个版本,这就是多态。

【第17题】抽象类求面积

#include &lt;iostream&gt;
using namespace std;
class Shape {
public:
    virtual double area() = 0;   // 纯虚函数:抽象类
    virtual ~Shape() {}
};
class Circle : public Shape {
private:
    double r;
public:
    Circle(double r) { this-&gt;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-&gt;w = w; this-&gt;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 &lt; 2; i++)
        cout &lt;&lt; shapes[i]-&gt;area() &lt;&lt; endl;   // 12.566 12
    for (int i = 0; i &lt; 2; i++) delete shapes[i];
    return 0;
}

解析:一个基类指针数组存不同子类对象,统一调用同一个接口,各自执行自己的实现。

【第19题】vector 模拟栈

#include &lt;vector&gt;
#include &lt;iostream&gt;
using namespace std;
class Stack {
private:
    vector&lt;int&gt; 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 &lt;map&gt;
#include &lt;sstream&gt;
#include &lt;iostream&gt;
#include &lt;string&gt;
using namespace std;
int main() {
    string text = "I love C++ C++ is fun";
    map&lt;string, int&gt; words;
    stringstream ss(text);          // 用流拆分单词
    string w;
    while (ss &gt;&gt; w) words[w]++;     // 每出现一次计数+1
    for (auto &amp;p : words)
        cout &lt;&lt; p.first &lt;&lt; ":" &lt;&lt; p.second &lt;&lt; endl;
    return 0;
}

解析:map<string,int> 天然适合“词→次数”,words[w]++ 一行完成统计。

【第21题】set 去重排序

#include &lt;set&gt;
#include &lt;iostream&gt;
using namespace std;
int main() {
    set&lt;int&gt; s = {5,2,8,1,9,2,5};   // 重复的自动去掉
    for (int x : s) cout &lt;&lt; x &lt;&lt; " ";  // 自动升序:1 2 5 8 9
    cout &lt;&lt; endl;
    return 0;
}

解析:set 元素唯一且自动有序,插入时自动去重排序。

【第22题】list 插入删除

#include &lt;list&gt;
#include &lt;iostream&gt;
using namespace std;
int main() {
    list&lt;int&gt; 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 &lt;&lt; x &lt;&lt; " ";
    cout &lt;&lt; endl;
    return 0;
}

解析:list 是链表,任意位置插入删除 O(1),但没有随机访问,用 advance 移动迭代器。

【第23题】深拷贝

#include &lt;iostream&gt;
using namespace std;
class Arr {
private:
    int *p; int n;
public:
    Arr(int n) { this-&gt;n = n; p = new int[n]; }
    Arr(const Arr &amp;other) {              // 拷贝构造:深拷贝
        n = other.n;
        p = new int[n];                  // 重新申请内存
        for (int i = 0; i &lt; 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 &lt;iostream&gt;
#include &lt;memory&gt;
using namespace std;
class Demo {
public:
    Demo() { cout &lt;&lt; "构造" &lt;&lt; endl; }
    ~Demo() { cout &lt;&lt; "析构" &lt;&lt; endl; }
};
int main() {
    { unique_ptr&lt;Demo&gt; u(new Demo()); }   // 出作用域自动析构
    shared_ptr&lt;Demo&gt; s1 = make_shared&lt;Demo&gt;();
    shared_ptr&lt;Demo&gt; s2 = s1;             // 引用计数+1
    // 全部退出后自动析构
    return 0;
}

解析:智能指针在作用域结束时自动 delete,不用手动管理,从根上避免内存泄漏。

【第25题】异常处理

#include &lt;iostream&gt;
#include &lt;stdexcept&gt;
using namespace std;
int main() {
    try {
        int x; cin &gt;&gt; x;
        if (x &lt; 0) throw runtime_error("负数不允许");
        cout &lt;&lt; x * 2 &lt;&lt; endl;
    } catch (const exception &amp;e) {
        cout &lt;&lt; "捕获:" &lt;&lt; e.what() &lt;&lt; endl;
    }
    return 0;
}

解析:throw 抛出、catch 捕获,程序不会因错误直接崩溃,错误处理更优雅。

【第26题】比较矩形大小

#include &lt;iostream&gt;
using namespace std;
class Rect {
public:
    double w, h;
    Rect(double w, double h) { this-&gt;w = w; this-&gt;h = h; }
    double area() { return w * h; }
    bool operator&gt;(const Rect &amp;r) {   // 重载&gt;
        return area() &gt; r.area();
    }
};
// 用法:if (a &gt; b) cout &lt;&lt; "a大";

解析:重载关系运算符后,对象之间可以直接用 > < 比较。

【第27题】字符串类重载+

#include &lt;iostream&gt;
#include &lt;string&gt;
using namespace std;
class MyStr {
public:
    string s;
    MyStr(string s) { this-&gt;s = s; }
    MyStr operator+(const MyStr &amp;o) { return MyStr(s + o.s); }
    void show() { cout &lt;&lt; s &lt;&lt; endl; }
};
int main() {
    MyStr a("hello "), b("world");
    MyStr c = a + b;    // 用重载的+
    c.show();
    return 0;
}

解析:重载 + 返回新对象,两个自定义字符串对象就能直接相加。

【第28题】继承:学生类继承人类

#include &lt;iostream&gt;
#include &lt;string&gt;
using namespace std;
class Person {
protected:                    // protected:子类可访问
    string name; int age;
public:
    Person(string n, int a) { name = n; age = a; }
    void show() { cout &lt;&lt; name &lt;&lt; " " &lt;&lt; age &lt;&lt; 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 &lt;&lt; id &lt;&lt; " " &lt;&lt; score &lt;&lt; endl;
    }
};

解析:子类构造必须先调父类构造(初始化列表 : Person(n,a));protected 让父类成员对子类可见。

【第29题】模板类数组容器

#include &lt;iostream&gt;
using namespace std;
template &lt;typename T, int N&gt;
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&lt;int, 10&gt; a; a.set(0, 100);

解析:模板参数可以是类型也可以是常量,让容器适配任意元素类型和容量。

【第30题】图书管理类(综合)

#include &lt;vector&gt;
#include &lt;fstream&gt;
#include &lt;iostream&gt;
#include &lt;string&gt;
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&lt;Book&gt; books;
public:
    void add(Book b) { books.push_back(b); }
    void del(int id) {
        for (auto it = books.begin(); it != books.end(); )
            if (it-&gt;id == id) it = books.erase(it);   // 接收返回值
            else ++it;
    }
    void find(int id) {
        for (auto &amp;b : books)
            if (b.id == id) cout &lt;&lt; b.title &lt;&lt; " " &lt;&lt; b.price &lt;&lt; endl;
    }
    void save() {
        ofstream out("books.txt");
        for (auto &amp;b : books)
            out &lt;&lt; b.id &lt;&lt; " " &lt;&lt; b.title &lt;&lt; " " &lt;&lt; b.price &lt;&lt; endl;
    }
    void load() {
        ifstream in("books.txt");
        int id; string t; double p;
        while (in &gt;&gt; id &gt;&gt; t &gt;&gt; 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 &gt; 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) &gt; 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 &gt; (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 &gt; 80;

4. 索引:查询加速器

CREATE INDEX idx_score ON student(score);   -- 普通索引
CREATE UNIQUE INDEX idx_name ON student(name);  -- 唯一索引

-- 查看是否走索引(执行计划)
EXPLAIN SELECT * FROM student WHERE score &gt; 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;  回滚,什么都不变

事务四大特性 ACIDA原子性(要么全做要么全不做)、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题)

  1. 创建数据库 student_db(utf8mb4),并进入该库。

  2. 创建学生表:id主键自增、姓名非空、性别默认'男'、成绩默认0。

  3. 插入5条学生数据。

  4. 查询所有学生,按成绩从高到低排序。

  5. 查询成绩大于等于80分的学生姓名和成绩。

  6. 查询姓“张”的学生。

  7. 查询成绩在70到90之间(含)的学生。

  8. 统计学生总人数、平均分、最高分、最低分。

  9. 按性别分组,统计每组的平均成绩,只显示平均分大于80的组。

  10. 查询成绩前3名的学生(降序取3条)。

  11. 把id=1的学生成绩改为95。

  12. 删除成绩小于60分的学生记录。

  13. 新增一列 phone(VARCHAR(20))。

  14. 给 score 字段创建索引,并查看执行计划。

  15. 查询成绩最高的学生信息(用子查询)。

进阶多表与事务模块(第16–30题)

  1. 创建班级表(cid主键、cname),给学生表加 cid 字段。

  2. 用内连接查询“有班级”的学生姓名和班级名。

  3. 用左连接查询所有班级及其学生(没有学生的班级也要显示)。

  4. 查询每个班级的学生人数。

  5. 查询每个班级里成绩最高的学生。

  6. 查询没有班级的学生(孤儿数据)。

  7. 创建一个视图:学生姓名+班级名+成绩,成绩大于80的查询该视图。

  8. 创建索引 idx_name,验证模糊查询'张%'是否走索引。

  9. 模拟转账事务:A扣100、B加100,成功提交;再模拟出错回滚。

  10. 创建存储过程:按班级id查询学生。

  11. 创建触发器:删除班级时自动删除该班学生。

  12. 用自连接查员工表:每个员工及其上级姓名。

  13. 统计每个班级的平均分,并按平均分降序。

  14. 查询成绩低于全班平均分的学生。

  15. 综合题:设计一个“图书借阅”系统表结构(图书表、读者表、借阅表),并写出“查询借了书还没还的读者”的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) &gt; 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 &gt; 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 &gt; 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 &lt; (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 生成)

posted @ 2026-09-22 18:27  Lucre1a  阅读(4)  评论(0)    收藏  举报