ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++笔试入门必刷题:从语法到实战的核心考点解析

C++笔试入门必刷题:从语法到实战的核心考点解析 C笔试入门最怕的不是不会写而是会写但没拿到分。很多刚入门的同学刷题时经常遇到这种情况题目一看就懂代码也能跑通但一到笔试现场就处处碰壁。这份C入门级笔试题合集一就是我结合这些年带新人、改考卷、自己面试的经验把最常出现、最容易被忽略的C基础考点重新梳理了一遍。它不是什么高深算法合集而是适合正在准备C岗位笔试、刚学完C语法想检验水平、或者自学C遇到瓶颈的同学帮你把那些“看似简单但一考就错”的知识点一次性理清。1. 输入输出与基础语法类问题1.1 cin/cout与scanf/printf的选择之争笔试里第一题往往是输入输出看起来毫无技术含量但栽在这里的人真不少。很多同学习惯性用cin/cout因为平时写代码方便。可到了笔试环境数据量一大cin/cout的效率问题就暴露出来了。先说结论在入门级笔试中直接用cin/cout问题不大但你要知道背后的机制和优化方式。cin/cout慢核心原因是C为了兼容C语言默认让iostream和stdio同步这就导致每次输入输出都有额外的同步开销。如果你确定代码里只用cin/cout不会混用scanf/printf可以在main函数开头加上这一行ios::sync_with_stdio(false); cin.tie(nullptr);这行代码的作用就是切断和C标准库的同步同时解除cin和cout的绑定。很多同学不知道cin和cout默认是绑定的意思是每次cin读入之前cout都会先刷新缓冲区这也会拖慢速度。但这里有个操作禁忌要记住如果你关闭了同步就绝对不能混用cin和scanf否则数据读入顺序会乱掉这是个很隐蔽的坑。我见过一个同学在笔试中前面用cin后面突然用scanf读一个字符结果程序跑挂找了好久才找到原因。如果你刷题量大数据上百万级别的读入推荐用自己封装的快读函数或者直接用getchar逐字符读取。下面给一个常见的快读模板int read() { int x 0, f 1; char ch getchar(); while (ch 0 || ch 9) { if (ch -) f -1; ch getchar(); } while (ch 0 ch 9) { x x * 10 ch - 0; ch getchar(); } return x * f; }这个函数用getchar逐字符读入自己处理数字和负号效率比cin高很多。笔试中如果遇到大数据的题目这个模板能帮你稳定通过时间限制。1.2 字符串输入的三种方式和它们的坑字符串处理是入门级笔试题的常客而字符串输入又是最容易出问题的地方。很多同学连“读入一行带空格的字符串”都搞不定。先说最基础的三种读入方式第一种cin str。这种方式读入字符串时遇到空格就停所以无法读入包含空格的字符串。如果题目要求读入一个英文句子你用cin就错了。第二种getline(cin, str)。这种方式可以读入一整行包括空格。但注意如果在getline之前用过cin 那cin读完后缓冲区里会残留一个换行符getline会直接读到这个换行符并结束导致你后面读到的字符串是空的。解决办法是getline前加一个cin.ignore()int n; cin n; cin.ignore(); string line; getline(cin, line);第三种C语言风格的fgets或gets。在C里虽然不推荐但笔试中偶尔也会看到。gets函数因为无法限制读入长度在较新的编译环境中已经被移除了如果遇到老题库还有gets建议用fgets替代char buf[256]; fgets(buf, sizeof(buf), stdin);另外字符串转数字在笔试中也很常见。C11之后可以用stoi、stol、stod等函数但注意stoi在转换失败时会抛出invalid_argument异常如果不做异常处理程序会直接终止。稳妥的做法是try { int num stoi(str); } catch (const std::invalid_argument e) { // 处理转换失败的情况 }笔试里字符串和数组之间的转换也是一个高频考点。比如把字符串按逗号分隔成数组这需要自己实现split逻辑因为C标准库没有直接的split函数。一个简单实现是用stringstream配合getline的第三个参数vectorint splitStringToInt(const string s, char delimiter) { vectorint result; stringstream ss(s); string item; while (getline(ss, item, delimiter)) { result.push_back(stoi(item)); } return result; }2. 数组、指针与内存管理基础2.1 数组名和指针的区别——笔试常考的迷惑点数组和指针是C入门笔试的重灾区。很多人觉得数组名就是指针这个理解不完全是错的但在笔试里要区分清楚。先跑一个测试int main() { int arr[5] {1, 2, 3, 4, 5}; int* p arr; cout sizeof(arr) sizeof(arr) endl; // 输出20 cout sizeof(p) sizeof(p) endl; // 输出864位系统 return 0; }数组名被用于sizeof操作符时返回的是整个数组占用的字节数而不是指针的大小。但在大多数表达式中数组名会退化为指向首元素的指针。这个“退化”规则是笔试的经典考点。具体来说数组名在以下两种情况下不退化作为sizeof的操作数作为操作符的操作数取数组名地址得到的是指向整个数组的指针还有一个常见考点二维数组的指针操作。比如int a[3][4], a1指向的是第二行的首地址步长是一行而不是一个元素。这个知识点很多人一开始搞不清楚可以用下面的代码来理解int a[3][4] {0}; cout a[0] endl; // 第一行首地址 cout a[1] endl; // 第二行首地址 cout a 1 endl; // 也是第二行首地址 cout *a 1 endl; // 第一行第二个元素的地址笔试中如果考到了数组指针核心就是搞清楚指针的步长。对于int来说1移动4个字节对于int()[4]来说1移动16个字节一行的长度。2.2 字符串数组初始化两个容易混淆的写法字符串数组的初始化也是笔试题中比较容易设陷阱的地方。看下面两行char str1[] hello; char str2[] {h, e, l, l, o};str1的长度是6因为编译器自动在末尾加了\0str2的长度是5没有结束符。如果你用strlen去测量strlen(str1)返回5但strlen(str2)会继续向后扫描直到遇到随机位置上的\0结果不确定。笔试中经常考到在末尾追加字符char str[10] hello; str[strlen(str)] !; str[strlen(str) 1] \0;如果漏掉最后一行的结束符手动添加后续字符串操作函数可能会读取越界行为未定义这是笔试中的隐蔽扣分点。对于C中的string类转字符数组时也有类似问题。使用c_str()函数获取的是const char*如果你需要修改必须拷贝到新的数组中string s hello; vectorchar buf(s.begin(), s.end()); buf.push_back(\0); char* mutable_data buf.data();笔试里还有一个常见的字符串数组初始化问题就是指针数组和字符数组的区别const char* ptr hello; // 字符串常量可能存储在只读区修改会崩溃 char arr[] hello; // 可修改的数组如果要修改字符串内容必须用数组方式而不是指针方式。这个问题在一些实际操作中被测试过很容易被忽略。2.3 内存管理的基本约定入门级笔试对内存管理的要求不算深但new和delete、malloc和free这两组的区别基本是必考。核心区别有几点new自动计算分配大小并调用构造函数malloc需要手动指定大小且不调用构造函数new返回类型安全的指针malloc返回void*需要强转new失败抛出bad_alloc异常malloc失败返回NULL笔试中常写的答案框架是// C风格 int* p new int(10); delete p; // C风格 int* q (int*)malloc(sizeof(int)); if (q ! NULL) { *q 10; free(q); }还有一个高频追问new[]和delete必须配对malloc和free必须配对。混用是未定义行为程序可能正常运行也可能会崩溃不要存侥幸心理。3. 算法基础排序、递归与幂运算3.1 冒泡排序从写法到优化的完整演进冒泡排序是入门级笔试题里出现频率最高的排序算法。基本逻辑就是相邻元素两两比较大的往后移每轮确定一个最大值的位置。最基础的写法void bubbleSort(vectorint arr) { int n arr.size(); for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { swap(arr[j], arr[j 1]); } } } }笔试中如果只是写出这个版本只能算及格。更吸引眼球的是在基础版本上加一个优化——提前退出标志void bubbleSort(vectorint arr) { int n arr.size(); 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; // 某一轮没有任何交换说明已经有序 } }这个优化在数组基本有序的情况下时间复杂度可以从O(n^2)直接降到O(n)。笔试题里如果给出一个“已经排好序但你还不知道”的数组这个优化版本可以显著缩短运行时间。3.2 选择排序的理解和实记技巧选择排序的核心思想是每轮从未排序区间中选出最小值放到已排序区间的末尾。void selectionSort(vectorint arr) { int n arr.size(); for (int i 0; i n - 1; i) { int minIdx i; for (int j i 1; j n; j) { if (arr[j] arr[minIdx]) { minIdx j; } } if (minIdx ! i) { swap(arr[i], arr[minIdx]); } } }选择排序之所以适合入门是因为它的交换次数只有O(n)次最多n-1轮。笔试里如果题目要求“只能使用O(1)额外空间”选择排序是一个有力的选项内存占用极小。我实际批改笔试时发现很多同学能写出冒泡排序但选择排序会搞混内层循环的起始位置。记法很简单外层循环i表示要确认的位置内层循环从i1开始找剩下的最小值位置。这个“先找下标最后交换”的模式就是选择和冒泡的本质区别。3.3 快速幂入门算法中性价比最高的一个快速幂在入门级笔试中出现频率高而且考察难度不大只要理解了递归公式就能写出来。核心公式当n为偶数时a^n (a^(n/2))^2当n为奇数时a^n a * (a^((n-1)/2))^2利用这个公式计算a^16只需要4次乘法而朴素计算需要15次。笔试里遇到“求a^b mod m”这种题目用快速幂是基本要求。迭代写法long long quickPow(long long base, long long exp, long long mod) { long long result 1 % mod; while (exp 0) { if (exp 1) { result (result * base) % mod; } base (base * base) % mod; exp 1; } return result; }这个实现里有一个细节result初始化为1 % mod。原因是在mod1时任何数的模运算结果都是0如果初始化为1算出的答案就会是错误的1。笔试中这个边界情况容易被忽略就是那些专门针对边界想法设计的题目。4. 面向对象与设计模式入门4.1 构造函数、析构函数和拷贝控制面向对象的入门笔试基本都会考构造函数和析构函数的调用顺序。很多同学死记硬背但遇到继承体系就懵了。核心规则如下构造时先构造基类子对象再构造派生类成员最后执行派生类构造体析构时顺序完全相反先执行派生类析构体再析构成员最后析构基类子对象下面这个例子在笔试题中反复出现class A { public: A() { cout A endl; } virtual ~A() { cout ~A endl; } }; class B : public A { public: B() { cout B endl; } ~B() { cout ~B endl; } }; int main() { A* p new B(); delete p; return 0; }输出结果应该是A、B、~B、~A。关键在于析构函数必须声明为virtual。如果基类析构函数不是virtual那么delete p时只会调用A的析构函数而不会调用B的析构函数这样就可能造成派生类资源没有正确释放。这个知识点在笔试中考过很多次基本是必背项。不过在C11以后更建议使用智能指针而不是手动管理析构。用unique_ptr后不需要手动delete异常发生时也能正确释放安全性更高unique_ptrA p make_uniqueB();4.2 常见设计模式在笔试中的出现方式入门级笔试对设计模式的考察通常不会太深但单例模式和观察者模式是最常出现的两个。其中单例模式因为代码量少、考点明确几乎成了笔试常客。单例模式的经典实现方式是双重检查锁DCLP但在C11之后更推荐使用Magic Static魔法静态变量的方式class Singleton { public: static Singleton getInstance() { static Singleton instance; return instance; } Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; private: Singleton() {} ~Singleton() {} };这种方法利用了C11中局部静态变量初始化是线程安全的特点既简洁又安全。笔试中若问到“怎么实现线程安全的单例”能被完整写出来基本就很稳了。观察者模式的考题一般不会让完整实现而是给一个场景让你说出模式名称和核心思想。比如“多个对象依赖某个对象的状态变化如何解耦”答“观察者模式”并说清楚发布-订阅的关系即可。4.3 回调函数的基本理解C中的回调函数在笔试中出现的频率越来越高特别是涉及异步和事件处理的题目。入门级的要求是掌握函数指针和std::function两种写法。函数指针写法void process(int x, void (*callback)(int)) { callback(x); } void printNum(int n) { cout n endl; } int main() { process(42, printNum); return 0; }std::function写法更灵活可以绑定lambda表达式、函数对象等#include functional void process(int x, std::functionvoid(int) callback) { callback(x); } int main() { process(42, [](int n) { cout n * 2 endl; }); return 0; }笔试中考回调函数时一般会问“如何把函数当参数传递”能写出std::function版本说明你了解现代C的写法。不过别只背一种函数指针的基础还是要熟悉。5. C11及以后的关键特性5.1 constexpr版本演进与使用场景constexpr是C中一个比较特殊的关键字笔试中经常看到“constexpr是哪个版本的C引入的”这类问题。答案是C11引入的。但在C14和C17中constexpr的能力被大幅扩展。C11的constexpr相对保守函数体内只能有一条return语句。从C14开始constexpr函数内可以使用循环、局部变量和条件判断这让它可以处理更复杂的逻辑。举个例子// C11版本只能这样写 constexpr int square(int x) { return x * x; } // C14及以后可以这样写 constexpr int factorial(int n) { int result 1; for (int i 2; i n; i) { result * i; } return result; }笔试中如果考到constexpr大概率会追问“constexpr和const的区别”。重点在于const强调的是不可修改的约束但不保证编译期求值constexpr强调编译期求值隐含了const的性质。两者定位不同必须明确区分。5.2 移动语义与右值引用这是C11中非常重要的特性虽然入门级笔试很少直接考深层的实现原理但常见的题目是让你说清楚std::move的作用。很多同学刚开始以为std::move是“移动”了对象其实不是这样的——它只是把左值转换为右值引用让编译器可以调用移动构造函数或移动赋值操作符从而避免深拷贝。一个简单的对比// 使用std::move避免不必要的拷贝 std::vectorint v1 {1, 2, 3, 4}; std::vectorint v2 std::move(v1); // v1被移动后处于有效但未指定的状态不能再指望它保持原内容笔试中常见追问是“移动后源对象处于什么状态”标准答案是处于有效但未指定的状态应避免继续使用。如果你在笔试中写出自定义类的移动构造函数代码可以是这样class MyString { public: MyString(MyString other) noexcept : data(other.data), len(other.len) { other.data nullptr; other.len 0; } private: char* data; size_t len; };注意移动构造函数通常要标记为noexcept这样在容器扩容时才会优先使用移动而不是拷贝。这一点在stl容器相关的题目中是一个常见的考察点。5.3 多线程基础笔试考点C多线程在入门级笔试中不会考太深但“线程创建的基本写法”和“数据竞争的基本概念”是比较常见的题目。线程创建的基本写法#include thread #include iostream void worker() { cout worker thread\n; } int main() { std::thread t(worker); t.join(); // 等待线程结束 return 0; }笔试中的追问往往是“如果没有调用join或detach会发生什么”。答案是当std::thread对象被析构时如果线程仍然joinable程序会调用std::terminate导致崩溃。解决办法是在异常发生时也要确保join或者detach被调用正确使用方法是在对象析构前完成线程生命周期管理。入门级笔试中数据竞争相关的问题通常考察互斥锁的使用std::mutex mtx; int counter 0; void increment() { std::lock_guardstd::mutex lock(mtx); counter; }lock_guard和unique_lock的区别也是容易被问到的问题。简单记lock_guard构造时加锁、析构时解锁不可手动操作unique_lock更灵活可以手动unlock和lock适合需要条件变量的场景。笔试中写简单同步逻辑用lock_guard就够了。6. 编译环境与笔试现场实战经验6.1 在VS Code中配置C/C环境的实用方法笔试前不少人会在本地环境上栽跟头特别是刚接触C的同学。VS Code是很多人用到的编辑器但部署配置经常有偏差这里说一下我推荐的配置流程。VS Code本身只是一个编辑器编译任务需要靠插件和后端的编译器来完成。你需要安装的插件是“C/C Extension Pack”由微软官方维护同时建议搭配“Code Runner”来快速运行代码。编译器方面Windows系统可以用MinGW-w64macOS直接用clang系统自带Linux用g。配置的关键步骤其实不多但有一个容易踩坑的地方MinGW安装完成后bin目录的路径必须添加到系统的PATH环境变量中。很多人装完了才发现命令行里输g找不到命令就是这个原因。添加PATH之后在终端里输入g --version如果能看到版本号说明环境已经就绪。接下来在VS Code中按F5选择“C (GDB/LLDB)”环境然后生成launch.json和tasks.json程序就能运行了。tasks.json中需要配置的核心内容是编译命令。一个常用的配置是{ version: 2.0.0, tasks: [ { type: cppbuild, label: C/C: g.exe build active file, command: g, args: [ -fdiagnostics-coloralways, -g, -stdc17, ${file}, -o, ${fileDirname}\\${fileBasenameNoExtension}.exe ], options: { cwd: ${fileDirname} }, problemMatcher: [$gcc], group: build } ] }注意到args里我明确写了-stdc17。如果不指定g默认使用C14那你想用C17的特性比如std::filesystem就会报错。笔试刷题时遇到莫名其妙的编译错误先看看是不是标准版本的问题。6.2 笔试现场的时间分配和调试策略笔试现场的时间管理是很多人容易忽略但影响很大的因素。我的建议是“三分钟定生死”策略拿到题目后先用三分钟判断这道题是不是你熟悉的套路题。如果是直接动手写如果不是先在草稿纸上把逻辑流程梳理清楚不要急着敲代码。很多同学在笔试中犯的最大错误是——拿到题目就开始写代码写到一半发现思路有问题又全部删掉重写。在练题阶段建议培养一个习惯先写注释把算法步骤列出来再在注释下填充代码。这样能大幅减少思路跑偏的概率。另外一个容易踩的坑是笔试提交前不检查变量类型溢出。C笔试题中如果题目的数值范围到了10^9量级int类型很可能放不下。int的上限大约是2.1×10^9但中间运算过程中可能会溢出这时需要改用long long。一个快速判断方法是看到参与乘法或累加的变量先问自己“这个数会不会超过10位数”。批量测试时建议用自定义数据测试而不是只依赖题目给出的例子。题目给的例子往往比较简单边界情况空输入、单元素、重复元素、等比数列需要自己构造。测试时我习惯把测试数据放在一个.txt文件里然后通过输入重定向来测试g -o sol sol.cpp ./sol test1.txt这样测试数据可以重复利用比每次手动输入方便很多。6.3 常见编译错误的排查顺序笔试现场碰到编译错误很多人会反复看代码却找不到原因。这里给一个排查顺序先看错误行号再往上追一行。因为C编译器报错时有时候实际出错的位置会在报错行的上一行比如缺少分号、括号不匹配。另一个高频报错是linker command failed通常是因为函数声明了但没定义或者源文件没有正确编译。在VS Code中如果遇到这个问题检查一下tasks.json中的args列表有没有把当前文件包含进去。还有一种情况是包含头文件的顺序导致的问题。比如在同一个翻译单元中先包含了某个库的头文件这个头文件又间接包含了另一个头文件顺序反了会导致找不到定义。虽然这属于较冷门的点但笔试中如果遇上了会很浪费时间。我个人总结了一个“三不原则”不盲改没确定原因之前不要乱改代码、不硬扛超过15分钟卡在同一处就换思路、不留隐患输出的结果如果不确定就多用几组数据验证。这三位一体地在实战中帮我把不稳定因素处理掉。7. 从笔试题看C学习的本质写到这里你会发现“入门级笔试题”这个说法其实涵盖的内容远不止语法。它涉及输入输出的底层机制、内存管理的原则、面向对象的抽象思维、泛型与STL的使用、并发编程的基本素养还有工程层面的环境配置能力。我个人在学习C过程中的体会是笔试刷题只能帮你积累“知识点数量”真正让你对C有感觉的是把每个知识点背后的“为什么”想明白。比如为什么cin慢、为什么析构函数要virtual、为什么移动语义能提高性能——每一个问题背后都牵扯到计算机底层的工作原理。想透了这层再去做题很多题目就变得有章可循。还有一个建议写每题时都主动去尝试跟标准答案不同的写法。比如冒泡排序可以用递归实现字符串分割可以用双指针实现。这些扩展练习能让你在考场上有更多选择而不是只会一个固定模板。这套合集是为系列准备的后续会继续整理更复杂的题目类型比如STL容器进阶、模板元编程入门、算法题中的C技巧、笔试中常见的多线程与网络编程题目。如果你也有遇到过比较好的题目欢迎在评论区分享。
返回列表