C++基础练习题集:语法、算法与易错点全覆盖,稳拿期末基础分
每年期末都会有学弟学妹跑来问我“华农的C考试到底考什么有没有那种最基础、最保底的练习题”说实话这门课虽然挂科率不算高但很多同学的痛点是——上课听懂了一到自己写代码就卡壳尤其是考试里那些看似简单的基础题反而因为细节丢分。我当年也踩过不少坑所以陆陆续续帮人辅导过不少次这次干脆把平时答疑时反复讲的重点整理成一套基础练习题集配上解题思路、易错点和扩展思考希望能帮你把基础分稳稳拿到手。这套练习并不是要去押题而是围绕华农C课程常见的教学重点和考试范围来设计的变量与类型、控制流、数组与字符串、函数与递归、结构体与类、模板以及排序查找这类算法基础。每个部分我都按“题目→思路→参考代码→避坑提示”的节奏来写既适合考前突击也适合平时上机练习。如果你能把下面这些题都独立写出来、跑通并讲明白为什么那这门课的“基础分”基本就是你的了。1. 华农C课程的基础题都有哪些固定套路1.1 从课程主线看考试重点华农的C课程一般是C语言的进阶前几周会快速复习C的部分内容然后进入类和对象、继承多态、模板、STL等C特有机制。期末考试通常分两大块一是基础语法和编程能力题二是面向对象和综合应用。前者更依赖基本功也是大多数学生觉得“明明会但没写完”的部分。基础题的常见来源包括教材课后习题、实验指导书里的任务、历年学长学姐传下来的旧卷。把它们归纳起来其实就三类程序控制流题、数据组织题、算法思维题。控制流题围绕分支和循环比如统计数字、打印图形、穷举求解数据组织题围绕数组、字符串和结构体比如排序、查找、链表操作算法思维题则偏向递归、二分、快速幂这些带有“模型”意义的题目。我建议复习的时候不要只看不练。很多同学觉得“这个我懂”可一上机就报错。C是一门“手熟”的课光看代码或背概念没有用一定要亲手编译运行把报错处理掉才算真正掌握。1.2 考试难度拆解送分题、拉分题和压轴题如果给基础题分个层大概是这样送分题变量定义、基本输入输出、if/switch、for/while、一维数组遍历等只要语法对就能拿分。拉分题字符串处理、二维数组、函数参数传递方式值传递、引用传递、const的用法、结构体数组排序这需要理解数据在内存里是怎么存的。压轴题递归调用、类设计、运算符重载、模板函数这类题既考语法又考设计思维通常放在最后。这篇文章主要解决送分题和一部分拉分题。因为压轴题也建立在基础语法上基础不牢后面全是空中楼阁。2. 语法基本功变量、表达式与程序控制流2.1 运算符优先级和类型转换最容易大意丢分的地方先看一道非常经典的送分题#include iostream using namespace std; int main() { int a 5; int b a a; cout a b endl; return 0; }这段代码在考试里经常以“问结果”的形式出现。但我必须提醒你a a这类表达式在C标准里属于未定义行为不同编译器可能算出不同结果所以正规考试不会真的让你算这种题。真正会考的是下面这种int a 7, b 2; double x a / b; double y (double)a / b; cout x y endl;输出是3 3.5因为a / b是两个整数相除结果仍是整数小数点直接丢弃而(double)a / b先把a转成double再参与除法结果才是浮点数。这就是隐式类型转换和强制类型转换的区别。练习建议自己写几个混合运算的表达式比如5 / 2 * 2.0、5.0 / 2 * 2、5 / (double)2先猜结果再上机验证。把整型提升、算术转换的规则搞清楚比背十遍知识点都管用。2.2 分支与循环的典型考法打印图形和数字统计控制流题最常考的题型之一就是“用嵌套循环打印图形”。比如打印一个5行的等腰三角形* *** ***** ******* *********参考写法#include iostream using namespace std; int main() { int n 5; for (int i 1; i n; i) { for (int j 1; j n - i; j) cout ; for (int j 1; j 2 * i - 1; j) cout *; cout endl; } return 0; }这类题考察的是for循环的边界控制外层循环管行数内层循环管空格数和星号数。很多人把j n - i写成j n - i导致图形错位这就是典型的差一错误off-by-one。另一种高频题是“统计特定条件的数字”。例如统计100到999之间有多少个各位数字之和为奇数的数。#include iostream using namespace std; int main() { int count 0; for (int n 100; n 999; n) { int sum 0, temp n; while (temp 0) { sum temp % 10; temp / 10; } if (sum % 2 1) count; } cout count endl; return 0; }这类题的核心套路就是“拆数字”用% 10取个位用/ 10去掉个位。循环、分支、取模运算都练到了也是很多后续题目的基础。2.3 从环境开始VS Code配置C/C环境如果你准备在自己的电脑上刷题第一步先把环境装好。我推荐VS Code MinGW-w64的组合轻量、免费调试也方便。网上有很多教程但最简单稳妥的做法是下载MinGW-w64并解压到C:\mingw64把C:\mingw64\bin添加进系统PATH。VS Code里安装C/C扩展ms-vscode.cpptools。新建一个.cpp文件后用快捷键CtrlShiftB配置编译器为g或者直接用终端命令g test.cpp -o test编译运行。这里特别提醒安装编译器时如果看到“Downloading”或“Installing”卡住多半是网络问题换一个国内镜像地址即可。环境配置看似和考试无关但如果你连代码都跑不起来复习效率会大打折扣。考前每天上机写两三个小程序比只看书有效得多。3. 数组、字符串与指针实操性最强的拿分区块3.1 字符串数组初始化与“字符串转数组”的坑C里字符串有两种风格C风格字符串字符数组和std::string。考试常常把两者混着考。先看初始化char str1[] hello; // 正确str1是一个字符数组长度6含\0 char str2[5] hello; // 错误空间不够存放结尾的\0 char* str3 hello; // 在C里已不推荐字符串字面量是const char* string str4 hello; // 正确C风格很多人在char str2[5] hello;上报错就是因为忘记C风格字符串末尾有\0。数组大小至少要写成6。另外“字符串转数组”是个很常见的需求比如把string里的每个字符拷贝到一个char数组里#include iostream #include cstring using namespace std; int main() { string s abc; char buf[10]; strcpy(buf, s.c_str()); // 方式一通过c_str()拷贝 for (int i 0; i s.size(); i) buf[i] s[i]; // 方式二逐字符赋值 buf[s.size()] \0; cout buf endl; return 0; }用strcpy要记得包含cstring并且保证目标数组够大否则会有缓冲区溢出风险。逐字符赋值则要记得手动加\0不然打印时会输出乱码。3.2 流I/Ocin/cout之外的小考点cin 和cout 大部分人都会用但考试还喜欢考带空格的输入。比如输入一行含有空格的句子cin s只能读到一个单词必须用getline#include iostream #include string using namespace std; int main() { string line; getline(cin, line); // 读取整行 cout line endl; return 0; }注意getline和cin 混用时如果前面用cin读过一个数字留在输入缓冲区里的换行符会被getline读到导致getline直接返回空行。解决办法是在它们之间加一句cin.ignore()。文件流也是基础题里的常客虽然不一定上机考但笔试可能会让你看代码说结果。基本读写套路#include fstream ifstream fin(in.txt); ofstream fout(out.txt); int x; fin x; fout x endl; fin.close(); fout.close();练习时可以创建一个in.txt读入几个整数再排序后写入out.txt这能顺带复习文件流的打开与关闭。我见过不少同学忘记close()其实程序退出时流会自动关闭但显式关闭是良好习惯。3.3 指针与引用覆盖和隐藏其实是“同名函数”问题“覆盖”和“隐藏”在考试里常以辨析题出现它们和指针、虚函数强相关。覆盖override派生类中定义一个与基类完全相同原型的虚函数编译器在运行时根据对象实际类型决定调用哪个。隐藏hide只要派生类函数与基类同名无论参数是否相同基类版本就会被隐藏甚至不管基类函数是不是虚函数。看个例子#include iostream using namespace std; class Base { public: virtual void f(int x) { cout Base::f(int) x endl; } void g() { cout Base::g() endl; } }; class Derived : public Base { public: virtual void f(int x) override { cout Derived::f(int) x endl; } void g() { cout Derived::g() endl; } }; int main() { Derived d; Base* p d; p-f(3); // 虚函数动态绑定 - Derived::f(int) 3 p-g(); // 非虚函数静态绑定 - Base::g() return 0; }f是覆盖g是隐藏也叫重定义。如果你在Derived里写了void g(int a)那Base::g()也会被隐藏此时d.g()会编译报错因为派生类的g(int)掩盖了基类的无参g()。这是很经典的陷阱题。复习这块时建议自己动手写一个基类和一个派生类分别试一下“基类指针指向派生类对象”时虚函数和非虚函数的调用结果会理解得更透。4. 函数、递归与经典算法基础题里的“思维担当”4.1 判断质数从朴素到优化的演变质数判断几乎每个学校都会考华农也不例外。朴素写法是从2循环到n-1但这样效率太低。考试如果只是输入一个数让你判断可能不会卡时间但一旦放进循环里多次调用你就需要优化。优化第一步循环到sqrt(n)即可因为如果n存在因子a那么a sqrt(n)或n/a sqrt(n)必有一个成立。#include iostream #include cmath using namespace std; bool isPrime(long long n) { if (n 2) return false; for (long long i 2; i * i n; i) { if (n % i 0) return false; } return true; }注意i * i n比i sqrt(n)更好既避免了每次循环都算sqrt也防止了精度问题。再进一步可以直接排除偶数和3的倍数只检查6k±1形式的数。这样在大量判断时更快。这类代码在笔试里不一定要求写但在上机题里能明显减少运行时间是一个很好的加分项。4.2 快速幂用递归理解用循环实现快速幂是“看起来难其实代码很短”的题。它解决的问题是高效计算a^b % mod。朴素做法是乘b次但b到1e9就超时了。快速幂的核心思想是把指数按二进制拆解a^b a^(2^k) * a^(...)每次把底数平方指数右移一位。递归版本long long fastPow(long long a, long long b, long long mod) { if (b 0) return 1 % mod; long long half fastPow(a, b / 2, mod); half half * half % mod; if (b % 2 1) half half * a % mod; return half; }迭代版本更推荐long long fastPow(long long a, long long b, long long mod) { long long res 1; a % mod; while (b 0) { if (b 1) res res * a % mod; a a * a % mod; b 1; } return res; }笔试如果出快速幂通常会给mod防止溢出。面试和竞赛里这是基础代码建议背熟。理解时可以用“翻倍翻倍、遇到1就乘”的口诀。4.3 排序与查找冒泡排序和二分查找必须能默写冒泡排序是C考试里最高频的算法题之一没有“之一”。它每轮把相邻元素比较并交换最大的数像气泡一样浮到末尾。void bubbleSort(int arr[], int n) { for (int i 0; i n - 1; i) { bool swapped false; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) break; // 本轮没有交换说明已经有序 } }加了swapped标记后最好情况下数组已经有序复杂度可以降到O(n)。这是很多人忽略的细节笔试里写上可能加分。二分查找则要求数组必须有序。经典写法int binarySearch(int arr[], int n, int target) { int left 0, right n - 1; while (left right) { int mid left (right - left) / 2; if (arr[mid] target) return mid; else if (arr[mid] target) left mid 1; else right mid - 1; } return -1; }注意mid left (right - left) / 2要写成这样而不是(left right) / 2因为后者在极端情况下可能整型溢出。这也是面试官爱问的细节。练习时可以把前面写的bubbleSort和binarySearch串起来生成一个乱序数组排序后查找某个元素输出下标。这样一道综合题就把两个考点都覆盖了。4.4 进阶一点从二分思想到单调栈雏形虽然华农基础考试一般不直接考单调栈但如果你学有余力理解其思想对后续数据结构课会有帮助。单调栈解决的问题是“求数组中每个元素右边第一个比它大的数”。典型写法vectorint nextGreater(vectorint nums) { int n nums.size(); vectorint res(n, -1); stackint st; // 存下标 for (int i 0; i n; i) { while (!st.empty() nums[i] nums[st.top()]) { res[st.top()] nums[i]; st.pop(); } st.push(i); } return res; }核心是让栈内元素保持单调递减下标对应的值每次遇到更大的数就不断弹出并更新答案。这个思想并不难就算考试不考也能帮你加深对“栈”这个数据结构的理解性价比很高。5. 结构体、类与模板面向对象基础题怎么练5.1 结构体链表基础语法中的“数据组织”代表结构体链表是考试中的常客既考结构体定义又考动态内存管理。遇到“创建链表头插法/尾插法”的题你要能写出以下关键步骤。#include iostream using namespace std; struct Node { int data; Node* next; }; int main() { Node* head nullptr; for (int i 1; i 5; i) { Node* newNode new Node(); newNode-data i; newNode-next head; // 头插法 head newNode; } for (Node* p head; p ! nullptr; p p-next) { cout p-data ; } cout endl; return 0; }这里有几个易错点定义Node后别忘了给指针初始化new Node()返回的是指针不是对象。遍历时循环条件是p ! nullptr不是p-next ! nullptr否则会漏掉最后一个节点。程序结束前最好手动释放内存虽然考试不一定会检查内存泄漏但良好的习惯是加分项。如果考到“删除指定节点”或“反转链表”原理也是从这些基础操作延伸出来的先把增删遍历练熟就有底了。5.2 类的前置声明写代码时最容易一头雾水的地方在C里如果两个类互相引用比如A类有B类指针B类有A类指针你就需要“前置声明”。典型场景class B; // 前置声明 class A { public: B* b; // 正确可以声明指针 // B obj; // 错误不能定义对象因为B还不完整 }; class B { public: A* a; };前置声明告诉编译器“B这个类存在但先别管它的细节”这样就能先定义指针成员。只有在类定义完整后才能定义对象或调用它的成员。这个考点经常以“判断下列代码是否有错”的形式出现核心就是记住可以声明指针或引用不能定义对象也不能解引用访问成员除非已经包含完整定义。另外如果你真的在两个头文件里互相#include也会出现循环包含的问题。解决办法就是用前置声明配合指针。5.3 模板初体验写一个通用的max函数模板是C基础到面向对象过渡的桥梁考试常让你写一个简单的模板函数或模板类。比如实现一个自己的“比较大小”函数#include iostream using namespace std; template typename T T myMax(T a, T b) { return a b ? a : b; } int main() { cout myMax(3, 5) endl; cout myMax(2.7, 1.8) endl; cout myMax(a, z) endl; return 0; }练习时要注意模板函数编译时并不是真的生成一个通用函数而是根据调用类型生成对应的实例。如果你在.cpp里定义模板、在另一个.cpp里用可能会报链接错误因为模板只有在实例化的时刻才生成代码。所以考试里一般会让你在同一个文件内写完整代码。如果时间和精力允许还可以练习一下简单类模板template typename T class Box { private: T value; public: Box(T v) : value(v) {} T get() const { return value; } };知道基本语法考试遇到“定义模板类并实例化”就能从容应对。6. 易错点复盘与复习建议6.1 考场上常见的编译和运行错误清单我把这几年帮人辅导时遇到最多的错误整理成一个表格你考试前扫一眼能避开不少坑错误示例原因分析正确做法char s[5]hello忘记字符串结尾的\0占一个位置数组长度至少为6while (cin n)无法结束输入不知道如何触发EOFWindows下按CtrlZ再回车Linux/Mac按CtrlDint a 5/2;得到2整数除法截断把除数或被除数转为浮点数if (s abc)对char数组使用C风格字符串不能直接用比较使用strcmp或std::string函数里修改了实参的值但不生效默认是值传递使用引用传递或指针传递类内成员函数使用override但基类不是虚函数覆盖的前提是基类函数为虚函数给基类函数加virtual模板函数定义和调用分离导致链接错误模板不支持分离编译在头文件或同一文件内定义实现vector下标越界使用了[]访问不存在的元素使用at()并捕获异常或先检查大小每一项都可以展开成一个上机练习。我不建议你只看表格最好亲手把每个错误复现一遍再改成正确的。记住错误比记住正确代码更牢固。6.2 刷题策略基础题怎么刷才高效最后讲讲刷题方法。我见过很多同学在期末前两周疯狂刷题但效果一般原因是“看题→看答案”而不是“做题→调错”。正确做法是打开一个空白的.cpp文件不看任何参考代码把题目从头到尾写一遍。编译报错了先自己读报错信息尝试修复。修不了再查。程序跑通后再问自己三个问题这段代码还有没有更简洁的写法有没有漏掉的边界条件如果输入特别大或特别小会崩溃吗每个题目至少用三组测试用例验证包括正常值、边界值和极端值。比如冒泡排序就试n1的情况。二分查找就试目标值在开头、结尾、不存在的三种情况。质数判断就试2、3、4、9、25这些数。这样刷一道题顶得上盲目刷三道。如果你能坚持用这个流程把上面那套题目练完考试时遇到任何基础题都不会慌。不管是不是华农的卷子只要覆盖了C基础语法、数组字符串、函数递归、结构体和类模板这套练习就值得反复咀嚼。C这门课练到最后拼的其实就是“稳”谁的基础更牢、谁的错误更少谁就能拿高分。