C++笔试高频考点精讲:字符串、排序与多线程实战

发布时间:2026/8/30 23:34:21
C++笔试高频考点精讲:字符串、排序与多线程实战 C笔试题这个系列写到第二期了。第一期我主要讲了一些语言层面的冷门考点比如变量声明、指针引用的区别、const关键字的几种用法后台留言里很多人说“笔试时就栽在这些细节上”。这一期我换个思路不按语法书目录来而是按笔试题目里最高频出现的几类场景来组织字符串处理、排序与算法、数据结构基础、新特性与多线程。每个题目都配了完整的代码和踩坑记录你可以直接拿去做自测也可以当作面试前的速查手册。先说下这套题适合谁。如果你正在准备C岗位的校招笔试或者刚学完语法想检验一下自己到底掌握到什么程度这份合集非常匹配。如果你已经工作两三年想快速回忆一下基础也可以扫一遍里面的多数易错点在实际代码 review 时同样会用到。1. 字符串与数组的经典题目字符串和数组是C笔试里绕不开的基础模块基本每套卷子都会出现。这里我挑了三个最有代表性的题目字符串转数组、字符串数组初始化、数字放大处理。这三个题覆盖面很广既能考到STL容器的熟练度又能考到内存管理和边界处理能力。1.1 字符串转数组的高频考法题目原题一般是这样的给定一个字符串内容是用逗号分隔的若干个整数比如12,34,567,89要求解析成一个整型数组并输出。这类题在笔试卷子里出现的概率极高因为它能一次性考察string的查找、截取、类型转换和容器操作。我见过不少人的第一反应是用循环加find函数手写解析这也是最稳妥的方案#include iostream #include string #include vector std::vectorint parseNumbers(const std::string input) { std::vectorint result; std::string::size_type start 0; while (start input.size()) { auto commaPos input.find(,, start); if (commaPos std::string::npos) { commaPos input.size(); } std::string token input.substr(start, commaPos - start); if (!token.empty()) { result.push_back(std::stoi(token)); } start commaPos 1; } return result; }如果你用的是C11以后的标准还可以用istringstream加getline来实现代码更简洁#include sstream std::vectorint parseNumbers(const std::string input) { std::vectorint result; std::istringstream iss(input); std::string token; while (std::getline(iss, token, ,)) { if (!token.empty()) { result.push_back(std::stoi(token)); } } return result; }两种写法笔试时都能拿分但有几个细节需要注意。第一个是空字符串的问题如果输入是或者,,这种连续分隔符的字符串直接stoi会抛异常所以一定要先判断token非空再转换。第二个是stoi可能抛std::invalid_argument和std::out_of_range异常严谨的做法包一层try-catch不过笔试题输入一般是合法的不强制要求。这道题我推荐用第二种写法因为getline配合分隔符参数是专门为这种场景设计的代码量少出bug概率低。面试官问起来还能顺便说一句“底层是基于流缓冲区的”显得有深度。1.2 字符串数组初始化的选择题陷阱字符串数组初始化是笔试选择题的重灾区看似简单但错误率很高。最常见的几种初始化方式如下// 方式一字符数组末尾自动补\0 char str1[] hello; // 长度6含\0 // 方式二字符数组手动指定大小 char str2[10] hello; // 剩余位置填\0 // 方式三string数组 std::string strArr[] {hello, world}; // 方式四const char* 数组 const char* strPtr[] {hello, world};这里最经典的一个陷阱就是sizeof和strlen的区别。sizeof(hello)是6因为包含了结尾的\0strlen(hello)是5不包含\0。很多人一看char str1[] hello就觉得长度是5结果在循环边界上多循环了一次把\0也当字符处理了。另一个坑是const char和char[]的区别。char[]是存储在栈上的字符数组内容可以修改const char指向的是字符串字面量位于只读区修改它会直接崩溃。笔试里经常给一段代码让你判断能否编译通过或者能不能修改字符串内容这个知识点一定要记牢。实际开发中我更推荐直接用std::string数组它的内存管理是自动的C11以后还能配合初始化列表一行搞定可读性也更强。只有在需要和C接口交互的场景下我才会考虑char数组。1.3 数字放大问题怎么处理“数字放大”这题我记得是某个在线平台的入门题但变形很多核心思想都是处理字符串形式的数字运算。原题大概是输入一个不含前导零的非负整数字符串可能非常大超出long long范围每位数字乘以2后相加得到新数或者将字符串中的每个数字字符替换为其ASCII码放大后的结果。以最常见的“大数每位数乘以2求和”为例#include iostream #include string int main() { std::string num; std::cin num; long long sum 0; for (char c : num) { sum (c - 0) * 2; } std::cout sum std::endl; return 0; }这道题的考点其实不是“乘以2”而是“字符转数字”的写法。字符0的ASCII码是48用c - 0就能得到对应的数字值。很多人习惯用int(c)直接把字符转成整数这样得到的是48、49这种ASCII码结果全错了。如果是进阶版“大数翻倍”即字符串表示的大整数乘以2并输出结果考察的就是进位处理了#include iostream #include string #include algorithm std::string doubleLargeNumber(const std::string num) { std::string result; int carry 0; for (int i num.size() - 1; i 0; --i) { int digit (num[i] - 0) * 2 carry; result.push_back(0 digit % 10); carry digit / 10; } if (carry) { result.push_back(0 carry); } std::reverse(result.begin(), result.end()); return result; }这题写的时候我要强调两点一是从低位往高位处理很多人习惯从下标0开始结果进位方向全反了二是循环结束后还要检查进位比如999乘以2最后还有一个进位1漏掉的话结果就少了最高位。做完再用999“1000”这种临界数据自测一下基本就不会翻车。2. 排序算法与经典算法题排序算法是笔试机的保留项目冒泡和选择是其中最基本的。但工作中很少手写排序所以不少人对细节的记忆很模糊尤其是复杂度分析和稳定性判断。我建议笔试前把每种排序的代码都亲手敲两遍闭上眼睛也要能写出来。2.1 冒泡排序的优化写法冒泡排序的基础版本相信大家都会两层循环相邻元素比较交换。但我建议笔试时直接写优化版本用一个flag标记本轮是否发生过交换void bubbleSort(std::vectorint arr) { int n arr.size(); for (int i 0; i n - 1; i) { bool swapped false; for (int j 0; j n - i - 1; j) { if (arr[j] arr[j 1]) { std::swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) { break; } } }加了这个flag之后如果数组本来就有序第一轮结束后就能提前退出最好时间复杂度从O(n²)优化到O(n)。笔试的时候很多题目会给一个几乎有序的数组这优化能直接体现你的功底。另外一个常被追问的知识点是稳定性。冒泡排序是稳定的排序算法因为只有当前一个元素严格大于后一个元素才交换相等的元素不会互换位置。选择排序则是不稳定的比如数组[5, 8, 5, 2]第一轮找到最小值2和第一个5交换后两个5的相对位置就变了。面试官一问到稳定性用这个例子一讲就清楚了。2.2 快速幂算法的二进制思路快速幂这个算法说难不难但新手第一次看到确实容易懵。题目一般是计算a的b次方模ma和b都可能很大直接循环会超时。核心思路是“指数看成二进制底数不断平方”。我举个例子计算3的13次方。13的二进制是1101也就是13 8 4 1所以3^13 3^8 * 3^4 * 3^1。我们可以从最低位开始一边遍历指数的二进制位一边维护当前位对应的幂值。迭代写法如下long long quickPow(long long base, long long exp, long long mod) { long long result 1; base % mod; while (exp 0) { if (exp 1) { result (result * base) % mod; } base (base * base) % mod; exp 1; } return result; }写这个算法的关键有几个。第一base要在最开始取模防止base本身已经超出long long范围。第二base base * base这一步是在每一轮都做的不管当前位是不是1因为下一个二进制位对应的幂值是当前位的平方。第三exp 1是判断exp的最低位是0还是1判断完记得右移一位exp 1。笔试如果考这个通常会要求输出模运算的结果因为大数乘方即使是long long也会溢出。代码里result * base这一步在极端情况下还是可能溢出求稳妥可以用__int128做中间值或者写一个快速乘法但入门笔试一般不要求这么深。2.3 n个整数的最小公倍数计算求两个数的最大公约数用辗转相除法欧几里得算法这个大家都熟。但笔试题目常升级成求n个数的最小公倍数LCM。两个数的最小公倍数等于两数乘积除以它们的最大公约数int gcd(int a, int b) { while (b ! 0) { int temp b; b a % b; a temp; } return a; } long long lcm(long long a, long long b) { return a / gcd(a, b) * b; }注意这里我先除后乘避免a * b在计算过程中溢出。这是一个非常典型的笔试细节陷阱直接用a*b/gcd在某些测试数据下会爆掉。n个数的最小公倍数的计算方式是逐个累积long long lcmOfAll(const std::vectorint nums) { long long result 1; for (int num : nums) { result lcm(result, num); } return result; }这个考点还会延伸出一个选择题为什么不直接计算两两的乘积再除以所有公约数因为多个数之间的公约数关系不能简单一次计算必须两两迭代。另一个容易问的是STL已经有了std::gcdC17起笔试如果允许用标准库可以省很多事但手写版更保险因为有些测评环境默认不是C17标准。3. 数据结构基础链表与单调栈数据结构这一块笔试的入门题最喜欢考链表的创建、遍历、反转以及单调栈解决“下一个更大元素”问题。链表题主要考察结构和指针操作的熟练度单调栈则考察算法思维。3.1 结构体链表的基本语法与操作这里先明确一个基础概念C里结构体是struct关键字定义的它和class基本一样只是默认访问权限是public。笔试写链表节点的时候我习惯这样定义struct ListNode { int val; ListNode* next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode* next) : val(x), next(next) {} };这里带构造函数的好处是创建新节点的时候可以直接ListNode* node new ListNode(5)不用手动给next赋nullptr。很多人因为忘写next nullptr导致遍历到链表尾部时访问了野指针瞬间崩溃。笔试里最常考的链表面试题是反转链表而且题目往往要求用迭代和递归两种方式实现。迭代版本的核心是三个指针prev、cur、next的协作ListNode* reverseList(ListNode* head) { ListNode* prev nullptr; ListNode* cur head; while (cur ! nullptr) { ListNode* next cur-next; cur-next prev; prev cur; cur next; } return prev; }这个代码的易错点是必须在修改cur-next之前先把cur-next保存到next指针否则链表就断掉了。我见过好多次这个错误指针操作题一定要在纸上画一下链表节点的指向变化再动手写。3.2 单调栈算法与经典应用场景单调栈是一个被低估的简单算法笔试常考工作中也经常能用到。它解决的问题类型很典型给定一个数组找出每个元素右边第一个比它大的元素。暴力法是双重循环时间复杂度O(n²)数据一大就超时。单调栈可以把时间复杂度优化到O(n)核心思想是维护一个栈内元素单调递增或递减的结构。以“每日温度”这道经典题为例给你一个温度数组temperatures返回一个数组answeranswer[i]表示要等多少天才能等到一个更高的温度。std::vectorint dailyTemperatures(const std::vectorint temperatures) { int n temperatures.size(); std::vectorint result(n, 0); std::stackint stk; for (int i 0; i n; i) { while (!stk.empty() temperatures[i] temperatures[stk.top()]) { result[stk.top()] i - stk.top(); stk.pop(); } stk.push(i); } return result; }栈里存的是数组下标而不是值这个细节很重要。为什么一定要存下标因为我们需要计算天数差差值是通过下标相减得到的。每次遇到比栈顶元素大的值时就说明找到了栈顶元素右边第一个更大的数出栈并记录结果。单调栈的易错点在于对“单调递增还是递减”的判断。这题栈内元素从栈底到栈顶是递减的温度因为一旦遇到更高的温度就会触发pop。如果你做题时搞不清楚就直接在草稿纸上跑一个简单例子比如[73, 74, 75, 71, 69]跟着代码走一遍逻辑立刻清晰。3.3 回调函数的函数指针写法回到函数这个基础话题。回调函数在C笔试里出现的频率也很高通常以选择题形式出现问你以下哪种写法是合法的。函数指针是最基础的实现方式#include iostream int add(int a, int b) { return a b; } int multiply(int a, int b) { return a * b; } int compute(int x, int y, int (*func)(int, int)) { return func(x, y); } int main() { std::cout compute(3, 4, add) std::endl; // 7 std::cout compute(3, 4, multiply) std::endl; // 12 return 0; }int (*func)(int, int)这种声明方式是C风格的回调看起来有点吓人但拆开看就简单了func是一个指针指向返回类型为int、参数为两个int的函数。函数名add、multiply本身就是函数指针所以在调用compute时直接传函数名即可。现代C更推荐用std::function它可以存储函数指针、lambda表达式、函数对象灵活性高很多#include functional int compute(int x, int y, std::functionint(int, int) func) { return func(x, y); } // 调用时可以传入lambda表达式 int result compute(3, 4, [](int a, int b) { return a b; });笔试题如果问“函数指针和std::function有什么区别”回答的要点是std::function是一个通用多态函数包装器底层可能涉及堆分配和类型擦除性能略低于裸函数指针但它的类型安全性和表达力更强。写业务代码用std::function没毛病写底层库或者热路径要用裸函数指针。4. C新特性与多线程基础很多入门笔试的最后一题会考一些C新特性的概念题比如constexpr从哪个版本引入、多线程的基本用法。这部分的题目通常不会太难但涉及的概念多容易混淆。4.1 constexpr在哪个C版本引入直接说答案constexpr是C11引入的关键字。到了C14放宽了函数体的限制C17又引入constexpr if和constexpr lambdaC20进一步支持constexpr的虚函数和try-catch。所以笔试问你“constexpr是哪个C版本引入的”答案是C11如果问“哪个版本支持constexpr if”则是C17。constexpr和const的区别也是高频选择题。const表示运行时不可修改但不保证在编译期求值constexpr强调编译期求值如果不能在编译期算出来程序会报错。举个例子const int size 10; // 可能是运行期常量 constexpr int compileSize 20; // 必须是编译期常量 int arr[compileSize]; // 合法编译期值已知 // int arr[size]; // 不合法如果size是运行期常量这个知识点要记住的是constexpr修饰的变量是强制的编译期常量const则没那么严格。C11之后constexpr函数的规则还比较死板只能有一句return语句C14放宽后可以包含循环和局部变量写起来舒服多了。4.2 C多线程的入门模板笔试考多线程一般是两种形式一种是概念题比如问join和detach的区别另一种是代码题要求多线程累加一个变量考察你对数据竞争的理解。先看最基础的线程创建模板#include iostream #include thread void worker(int id) { for (int i 0; i 3; i) { std::cout Thread id running std::endl; } } int main() { std::thread t1(worker, 1); std::thread t2(worker, 2); t1.join(); t2.join(); return 0; }join的作用是阻塞主线程直到该子线程结束。如果忘了调用join程序会在析构std::thread对象时直接调用std::terminate整个进程崩溃。detach则是让子线程在后台独立运行线程对象与线程分离但那之后就不能再join了也要注意主线程退出时子线程可能还没执行完。多线程共享数据是笔试的进阶题比如用两个线程各累加10万次共享计数器。如果不加锁结果很可能不是20万因为操作不是原子的两个线程可能读到相同值因此互相覆盖。正确的做法是用mutex加锁#include iostream #include thread #include mutex std::mutex mtx; int counter 0; void increment() { for (int i 0; i 100000; i) { std::lock_guardstd::mutex lock(mtx); counter; } }这里lock_guard是RAII风格的锁管理构造时上锁离开作用域时自动解锁避免了手动unlock的遗漏问题。笔试答题写到lock_guard是个加分项能体现你的现代C风格。想再进一步可以提一下std::atomic 也能解决这个场景但要注意atomic只能解决单独的读写原子性多个变量的复合操作场景还是需要锁。4.3 经典笔试选择题速查容器与八股C笔试最后一部分通常是选择题范围很杂。我根据自己的经验挑了些高频考点整理成表格方便你复习。这些内容不要求深入源码但概念上必须准确。考点正确答案常见错误理解vector底层数据结构动态数组链表map底层数据结构红黑树哈希表unordered_map底层数据结构哈希表红黑树vector扩容倍数通常是2倍部分实现是1.5倍固定增加N个槽位虚函数表指针存储在哪个区对象内部对象位于栈/堆区则跟随对象全局区/静态区C11引入的智能指针shared_ptr, unique_ptr, weak_ptrauto_ptrstd::move的作用转换为右值引用触发移动语义拷贝/强制转移所有权空类大小1字节0字节几个容易翻车的点单独说明一下。空类的大小是1字节因为每个对象必须有独一无二的地址。但如果空类里有virtual函数那么类里会有一个虚函数表指针大小变成8字节64位系统。笔试里经常考这个数字直接记结论即可。vector的扩容倍数不是C标准规定的只是主流实现的策略。GCC的libstdc是2倍MSVC是1.5倍。如果你被问到“vector为什么按倍数扩容”回答方向是保证push_back平摊时间复杂度为O(1)。如果每次只增加一个槽位那插入操作平摊下来是O(n)数据量大时性能不可接受。4.4 多选题中常见的设计模式概念虽然不是每个“入门级”笔试都会考设计模式但相关热词搜索量一直很高所以这里也简单带一句。最常见的考法是在代码题中让你找出哪里违反了单一职责原则或者问你单例模式怎么实现线程安全。这里给出一个笔试常考的双重检查锁单例模板#include mutex class Singleton { public: static Singleton getInstance() { static Singleton instance; return instance; } Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; private: Singleton() {} }; Singleton instance Singleton::getInstance();C11标准以后函数内的局部静态变量初始化是线程安全的所以上面这种写法就是最简洁的线程安全单例。很多老教材讲的DCLP双重检查锁定在C11以前有内存序问题现在笔试回答时直接用静态局部变量版本准没错。5. 笔试前的自测清单与经验总结这期合集的内容集中在字符串、排序、链表、单调栈、多线程和几个C新特性上。准备笔试时建议按这个顺序推进先把每个题目的代码手动敲一遍确认无报错然后合上代码凭记忆在纸上画出核心流程比如反转链表的三指针变化最后再做几个变种题看能不能举一反三。我在实际刷题过程中发现很多题目之所以做错不是因为算法不会而是因为对语言细节不熟。比如stoi和atoi的区别、strlen和sizeof的区别、vector的size()函数返回的是size_t无符号拿它和负数比较会出现意想不到的结果。这些坑我先踩了一遍现在整理出来大家可以直接避开。最后再分享一个小技巧笔试开始前先把这几个模板默写在草稿纸上——快速幂、反转链表、单调栈、快速排序的partition函数。这几个代码几乎是可以背下来的万能模板写一遍也就两三分钟但能让你在正式答题时快速进入状态。考试时卡住的题回头看看这些模板很多时候就能联想到正确思路。这套题刷完真正的效果要看你能否脱离编译器、独立写出正确代码。我的建议是给自己限时45分钟模拟笔试环境做完了再看看哪些地方和标准答案不一致。哪里卡住哪里就是你基础不牢的地方针对性地加强就好。C的笔试内容再多核心也还是这些基本功稳扎稳打全部吃透比盲目刷几十道偏题怪题有用得多。