2026/8/29 5:07:30

C++入门级笔试题高频失分点全解析:从内存到STL

C++入门级笔试题高频失分点全解析:从内存到STL 又到一年集中招聘的季节我最近帮做技术负责人的朋友筛了一批C入门级笔试题里面还真有不少值得聊的东西。很多同学拿到题目之后的第一反应是“这题我见过很简单”结果卷子一改通过率低得吓人。问题基本不是不会写代码而是基础概念里的那些“坑”根本没踩过一遍。“C入门级笔试题”这个关键词下面大家搜得最多的其实不是难题偏题而是字符串初始化、数组和指针的关系、const和constexpr、冒泡排序、快速幂这种基础题。这也是我把这个系列做成合集的原因入门级笔试题看起来不难但正因为不难面试官才敢往上堆细节、堆边界条件用来筛掉那些只背过答案、没理解原理的人。这篇是合集第二篇专题集中在内存与初始化、指针和数组的辨析、手写算法题、面向对象与STL基础以及几道容易被忽略的“八股”题。适合正在准备C校招笔试、想系统梳理基础概念、或者只学过C语言想转C的读者。上一篇聊了开发环境搭建、编译流程和一些基础语法题这篇直接进高频丢分点。1. 备考C笔试先想清楚的一件事入门级到底在考什么刷题之前先把方向搞清楚。我见过太多人一上来就背“C八股文”背了设计模式、背了多线程、背了一堆STL源码级细节结果笔试里考了个sizeof(hello)直接懵了。不是不该背而是顺序错了。热搜词里反复出“c编程基础知识”“c面试题”“c八股文”说明大家真的在认真准备但方向普遍偏了。入门级笔试的核心考察点其实非常集中内存布局、生命周期、类型系统、基础算法实现。不夸张地说大部分入门级笔试的通过率取决于你到底理不理解“变量在内存里到底长什么样”而不是你背了多少高级特性。举一个最典型的反直觉例子很多同学以为char* p hello;和char arr[] hello;是一回事因为平时写代码的时候都能用下标访问、都能传进函数里。但在笔试里这一行看似无差的代码会直接导致一道题全军覆没——因为sizeof的结果完全不同而且p指向的字符串常量根本不允许修改。这种题考的不是“会不会写代码”而是“知不知道代码背后的存储位置”。所以这篇文章的每个专题我都会按照这个思路来组织先给题再讲为什么最后说这个知识点在真实笔试里会以什么变种出现。你在刷题的时候也应该这样要求自己不要只记答案。2. 变量、初始化和内存布局看似送分实则送命的题目这一节选的几道题都是我在这轮筛卷子时发现错误率极高的。它们有一个共同特点表面上是“基础知识题”实际上考察的是内存模型的底层理解尤其字符串数组初始化、const常量表达式和隐式类型转换三个热点全占。2.1 字符串数组初始化一行代码分出三个层次先看这道出现频率极高的题char str1[] hello; const char* str2 hello; std::string str3 hello;问sizeof(str1)、sizeof(str2)、sizeof(str3)分别是多少str2[0] H;能编译通过吗运行会怎样三者分别可以/不可以修改哪些内容这道题的基础版本是问sizeof(str1)很多人知道str1是字符数组长度是6不是5因为要算上结尾的\0。但一牵扯到指针和std::string就开始乱了。str2是指针在64位系统上任何指针的sizeof都是8跟你指向的是字符还是结构体没关系。str3是std::string对象它的sizeof取决于标准库实现通常是32字节左右三个指针大小但不同版本实现不一样笔试里一般不要求背具体值知道“它是一个类对象大小不等于字符串长度”就够了。第二个问题更有意思。const char* str2意味着你不能通过这个指针修改指向的内容所以str2[0] H;在编译阶段就会被拦下来。但如果写成char* p hello;旧式写法新标准已经不允许编译能过运行时会崩——因为在大多数平台下字符串字面量存放在只读数据段。这不是C标准强制的但主流平台都这么做你在笔试里按“未定义行为”来回答一定是对的。我自己的经验是这种题一定要从存储模型入手理解数组是“在栈上划一块空间把内容复制进去”空间在栈上内容可改生命周期和当前作用域一致。指针指向的是“字面量所在的位置”字面量本身在只读数据段生命周期是整个程序。std::string是“栈上的对象 堆上的缓冲区”内容可改并且由对象自己管理内存。记住这三句话无论这道题怎么变比如换成“字符串转数组”“用memcpy拷贝字符串”你都能拆解到同一套底层逻辑上。2.2 const和constexpr只读和编译期常量不是一回事热搜词里有一条“constexpr哪个c版本引入的”问得非常到位。constexpr是C11引入的但很多人把const和constexpr混为一谈这是笔试里又一道经典陷阱。看下面对比声明含义能否用于数组大小能否用于模板参数const int a 10;运行期只读变量值可能直到运行时才确定能C11后因为它是整型常量表达式不能除非值在编译期已知但编译器不一定认constexpr int b 10;编译期常量必须在编译期就能算出来能能最经典的坑是这样的int x; std::cin x; const int a x; // 合法a是运行期只读变量你没法把一个运行时才读到的值变成constexpr。但很多人会以为“有了const值就是编译期常量”接着往下写const int a x; int arr[a]; // 如果编译器不支持VLA这可能编译失败标准里对变长数组VLA的支持是C99的事C一直没把VLA纳入标准GCC的扩展除外。所以遇到这种写法最稳妥的回答是“const int a x;之后a是运行期常量不能作为数组大小。”而constexpr int b 5; int arr[b];则毫无问题因为编译期就知道b是5。再结合constexpr面试题里常问的一个变种constexpr函数能不能有副作用不能。它要求能直接在编译期求值函数体里不能出现运行时才能确定的行为。这也是为什么很多人写了一个“以为能用constexpr结果报错”的函数——通常是在函数体里用了全局变量、std::cout之类的东西。2.3 有符号和无符号比较编译器不报错但你错了这一小节不是C特有的但C笔试特别爱考因为它结合了隐式类型转换的规则。题目一般是int a -1; unsigned int b 1; if (a b) { std::cout a b std::endl; } else { std::cout a b std::endl; }输出是什么答案是a b。原因是有符号和无符号整数比较时编译器会把有符号数隐式转换成无符号数再比较。-1转换成无符号整数是一个很大的数unsigned int是32位的话就是4294967295自然大于1。这类题和“n个整数的最小公倍数怎么求c”里的坑有异曲同工之处很多人用int存中间计算结果算完最小公倍数之后发现溢出才意识到类型的问题。笔试里写这类代码的时候建议从一开始就用long long计算中间值并且在涉及无符号运算时显式static_cast不要依赖隐式转换。一个简单规则循环下标用size_t没问题但不要把有符号和无符号混在一个表达式里。编译器开启-Wsign-compare会给出警告你在VS Code里配置C/C环境时把警告等级拉满这类问题能提前暴露一大半。3. 指针与数组入门笔试的“分水岭”也是热搜词里最扎堆的方向“数组和指针笔试题”几乎刷屏了所有搜索平台不是没有原因。这两个概念在C里既高度相关又完全不同而笔试题目就是围绕这对关系反复变着法出题。这一节讲三道最经典的思路比答案重要。3.1 数组名到底是不是指针退化的规则要记清先说结论数组名不是指针但在大多数表达式中会“退化”decay成指向首元素的指针。看这道题int arr[5] {1, 2, 3, 4, 5}; std::cout sizeof(arr) std::endl; // 输出 20假设 int 4字节 std::cout sizeof(arr[0]) std::endl; // 输出 8 int* p arr; std::cout sizeof(p) std::endl; // 输出 8同一个arrsizeof的结果不同就是因为sizeof(数组名)计算的是整个数组占用的字节数而一旦数组名被赋值给指针它就已经退化成指向首元素的指针sizeof算的就是指针大小了。还有一个经常一起考的int arr[5] {1, 2, 3, 4, 5}; int (*parr)[5] arr; std::cout sizeof(*parr) std::endl; // 输出 20arr的类型是“指向整个数组的指针”对它解引用得到的是整个数组所以sizeof(*parr)还是20。很多人一看到arr就觉得是指针直接填8这就是没搞清楚指针类型和数组类型的区别。我的记忆方式很粗暴数组名只有在两个场景下保持“数组”身份——sizeof(数组名)和数组名。其他场景一律退化成指针。遇到了就套这个规则基本不会错。3.2 sizeof与strlen一个是类型信息一个是运行时计算有字符串的情况下还有一道变种题char str[] hello; std::cout sizeof(str) std::endl; // 6包含末尾 \0 std::cout strlen(str) std::endl; // 5不包含 \0这道题错误率高在于很多人分不清sizeof是编译期运算符、算的是类型占用内存的大小而strlen是运行时函数、数的是字符直到遇\0。更隐蔽的坑是void func(char str[]) { std::cout sizeof(str) std::endl; // 8退化成指针了 std::cout strlen(str) std::endl; // 5遇到调用方传进来的 \0 }函数参数写成char str[]并不会让参数变成数组它会被调整成char* str。所以在函数内部sizeof(str)永远是8这是笔试里出错率最高的一个点。建议看到“数组作为函数参数”时立刻反应过来这个参数其实是指针应该传一个长度进来。3.3 函数传参想改外部指针二级指针和引用怎么选有一道经典题叫swap但入门笔试里通常不会只考交换两个int而是会绕一下void swap(int* a, int* b) { int tmp *a; *a *b; *b tmp; }这个函数没问题交换的是指针指向的值。但如果题目改成“交换两个指针本身”void swap_pointer(int* a, int* b) { int* tmp a; a b; b tmp; // 没用外面的指针根本没变 }为什么没用因为指针传入函数时也是值传递函数内的a和b是外部指针的副本你改副本不影响外部。要想改外部的指针必须用二级指针或者指针的引用void swap_pointer(int* a, int* b) { int* tmp a; a b; b tmp; // 引用方式直接改外部指针 }这道题很容易让人栽在“指针不是传地址吗”这个误解上。记住一句话任何类型的变量在值传递时都会复制一份指针变量也不例外。传入函数的是“指针的值”——也就是那个地址——你修改这个地址指向的内容是有效的但修改指针变量本身存储的地址是无效的。想改“指针本身”就必须多传一层。这类题在笔试里会以“链表反转”“二叉树遍历传根节点指针”等形式出现但追根到底都是同一个原理。把这道基础题吃透后面看复杂代码会有豁然开朗的感觉。4. 手写算法题冒泡排序、快速幂和格式化输出的实战细节入门级笔试的编程题一般不会太难但出题人喜欢在“简单题”里埋边界。热搜词里的“冒泡排序算法c”“快速幂算法c”“c数字放大”“n个整数的最小公倍数怎么求c”每一类我都见过对应的真实笔试题。4.1 冒泡排序会背不等于能写对要求“手动实现冒泡排序”几乎是入门级笔试的家常菜。但很多人一写就出问题出在哪边界。第一版错误示范void bubbleSort(int arr[], int n) { for (int i 0; i n; i) { for (int j 0; j n - 1; j) { if (arr[j] arr[j 1]) { std::swap(arr[j], arr[j 1]); } } } }这个版本能跑但问题有三个内层循环每次都从0到n-1已经排好的尾部元素也会被反复比较浪费。没有“是否交换”的标记最好情况下数组已经有序时间复杂度依然是O(n²)。外层循环i的边界写i n没问题但写i n-1也足够因为最后一个元素不需要再冒泡。这两种写法不是大错但改卷时会看你的边界意识。推荐的版本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]) { std::swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) break; // 没有交换说明已经有序 } }这样写最好情况下时间复杂度可以降到O(n)而且内层比较次数逐轮减少。笔试里除了写代码还会追问“冒泡排序稳定吗为什么”答案是稳定因为只有时才交换相等的元素不会互换位置。这是面试官考察你是否理解“稳定性”的常见手法。4.2 快速幂代码短、考点密、性价比高快速幂在入门级笔试里出现频率极高因为代码量小却能考察递归、位运算、模运算三个点。题目基本是“计算a的b次方模m”。先写递归版本long long quickPow(long long a, long long b, long long m) { if (b 0) return 1 % m; long long half quickPow(a, b / 2, m); half half * half % m; if (b % 2 1) half half * a % m; return half; }原理一句话a的b次方可以拆成a的b/2次方的平方若b是奇数再乘一个a。这样指数b每轮减半时间复杂度是O(log b)而不是O(b)。再给一个非递归写法笔试里写这个更稳避免递归爆栈long long quickPow(long long a, long long b, long long m) { long long res 1 % m; a % m; while (b 0) { if (b 1) { res res * a % m; } a a * a % m; b 1; } return res; }有几个容易忽略的点0次幂的情况要返回1 % m因为如果m1答案是0。a%m提前处理是为了防止a本身太大造成乘法溢出。中间乘法res * a和a * a都可能超过long long最好再套一层取模平时练习时可以开-fsanitizeundefined检查一下。4.3 数字放大与格式化输出入门级里意想不到的失分点“c数字放大”看起来是很小的一个点但确实是不少笔试编程题的第一小问。比如要求把输入的数字每位之间加空格输出或者保留指定位数小数。代码很简单但很多人会在这个环节丢掉“格式分”。比如按指定顺序输出一个数的各位数字热搜里的“c指定顺序输出”就是这个意思题目要求把12345输出成1 2 3 4 5。常见的错了思路是先把数转成字符串再遍历std::string s std::to_string(12345); for (char c : s) { std::cout c ; }这在简单场景下没问题但一旦数字前面有符号、有前导零或者数是0字符串处理和数字处理的边界就不同了。更通用的思路是用std::ostringstream或sprintf格式化或者直接按数学方式逐位提取。笔试改卷时输出的空格和换行都要和预期一致建议统一用“先拼字符串再一次性输出”的策略避免大量cout造成的性能问题和格式错误。C里格式化输出我个人的习惯是#include iomanip std::cout std::fixed std::setprecision(2) value std::endl;这样写出来的固定两位小数不会有多余的尾随零问题。很多人背了printf(%.2f)换到C的cout却忘了加std::fixed输出的结果就变成了科学计数法或多余的小数位白白扣分。5. 面向对象与STL基础题入门级也躲不过的设计感很多人以为“入门级笔试不考面向对象”这是误解。实际上C入门级笔试对面向对象和STL的考察集中在“你有没有用过”和“你知不知道背后机制”两个层面。范围不算深但覆盖面很广。5.1 多态与虚函数从概念到内存布局“c 设计模式”和“c八股文”里多态必然有名字。入门级笔试里最经典的虚函数题目是基类析构函数为什么要定义为虚函数答案不是“这样能多态”而是“如果不定义成虚函数通过基类指针delete派生类对象时只会调用基类析构函数派生类中申请的资源就可能泄漏”。一个例子就能说明白class Base { public: ~Base() {} // 非虚析构 }; class Derived : public Base { int* data; public: Derived() { data new int[100]; } ~Derived() { delete[] data; } }; Base* p new Derived(); delete p; // 只调用 Base::~Base()Derived::~Derived() 不会被调用这种情况必须把基类析构函数声明为virtual。这是笔试中最常考的多态问题。再进阶一点会问“虚函数怎么实现的”。标准没有规定必须用虚函数表vtbl但几乎所有主流编译器都是这么实现的每个包含虚函数的类有一个虚函数表存放函数指针每个对象里有一个隐藏的虚表指针vptr指向所属类的虚函数表。调用虚函数时通过vptr找到虚表再从虚表里取函数指针调用。所以虚函数调用比普通函数调用多一次间接寻址这也是为什么不能把构造函数设为虚函数——构造对象时vptr还没初始化。5.2 vector扩容与迭代器失效STL最常被问的底层机制STL基础题里std::vector是最常考的一环。两个必考点size()和capacity()的区别以及扩容为什么通常按倍数增长。size()是元素个数capacity()是已经分配的内存能容纳的元素个数。vector装满了要继续push_back时会分配一块更大的新内存把旧元素搬过去再释放旧内存。这个“搬”的代价很高所以常见实现都是按一定倍数扩容常用的就是2倍有的库是1.5倍。扩容的均摊复杂度是O(1)因为扩容次数是O(log n)。迭代器失效是一道经典题std::vectorint v {1, 2, 3, 4, 5}; for (auto it v.begin(); it ! v.end(); it) { if (*it 3) { v.erase(it); // 这样写就错了erase之后it已经失效 } }erase之后被删除位置及其之后的迭代器都失效了正确做法是利用erase的返回值for (auto it v.begin(); it ! v.end();) { if (*it 3) { it v.erase(it); } else { it; } }笔试里如果只让说结论你就说“erase返回下一个有效迭代器”如果让写代码就用上面的写法。5.3 设计模式在入门级怎么出现单例模式是最高频的入门笔试对“c设计模式”的考察基本不会让你完整实现一个工厂模式或观察者模式而是拿单例模式来验基本功。原因很简单单例模式代码量小但要涉及静态成员、私有构造函数、线程安全。一个C11推荐的线程安全写法class Singleton { public: static Singleton getInstance() { static Singleton instance; return instance; } Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; private: Singleton() default; ~Singleton() default; };这里的核心知识点是C11标准规定函数内的局部静态变量初始化是线程安全的。所以static Singleton instance;这行代码在多线程环境下只会被初始化一次不需要再加锁。这是很多人不知道或者不敢写的关键点。如果笔试里不允许用C11特性怎么办那就要写出双检锁加std::mutex的版本但这里有个坑std::mutex本身也是C11才有的更老的写法要自己封装。所以遇到这类题先问清楚考的是C哪个标准能省很多冤枉代码。6. 容易被忽略的“八股题”从回调函数到C多线程最后一节聊几个热搜里出现频率很高、但很多同学准备时容易忽略的概念。它们可能不是笔试的“压轴题”但一旦出现用来拉差距的效果立竿见影。6.1 回调函数概念题里最抽象的一个“c回调函数例子”之所以成为热搜是因为这个概念太容易讲成玄学。回调函数本质上就是“把一个函数作为参数传给另一个函数让它在一定时机调用”。C里最简单的实现方式是函数指针void onEvent(int code) { std::cout event code: code std::endl; } void process(void (*callback)(int)) { callback(42); } int main() { process(onEvent); return 0; }C11以后更常用std::function#include functional void process(const std::functionvoid(int) callback) { callback(42); }区别在于std::function可以接受函数指针、lambda表达式、仿函数等等灵活得多。笔试里如果问“回调函数有什么用”你就拿按钮点击事件举例界面框架不知道你点击后要干嘛但你给它一个回调函数它就知道点击后调你的代码。这个例子一出来比背定义清楚得多。6.2 C多线程与ABA问题入门级怎么体现并发意识“aba问题c”这个话题通常是在讨论无锁编程时出现的看起来和入门级八竿子打不着但笔试里确实出现过简化的版本。ABA问题是什么在无锁数据结构中线程A读到共享变量的值是A然后被调度走了线程B把值改成B又改回A线程A恢复执行后发现值还是A就认为“没人动过”于是继续操作。但中间其实发生了两次修改这个“没人动过”的假设是错的。入门级笔试一般不会让你实现无锁队列但会用一道简化的题来考察你有没有并发意识比如“两个线程对一个int变量做操作循环10000次最终结果一定是20000吗”答案是“不一定”因为v不是原子操作它包含读取、加一、写回三个步骤两个线程交错执行就会丢失更新。如果追问“怎么解决”最常见的是std::atomicintstd::atomicint counter{0}; counter.fetch_add(1); // 原子操作这不需要你掌握ABA问题的全部细节但如果你能顺着“原子操作”“CAS”“ABA问题”这个链路答下去面试官对你的评价会明显提高。6.3 从CSP-J到笔试竞赛入门题其实是笔试题库的宝藏最后聊一个容易被忽略的备考资源。热搜里大量出现“CSP-J 2021/2023/2024第一轮答案与解析”“CCF非专业级别软件能力认证第一轮(CSP-J)入门级详解”这不是巧合——很多企业的入门级笔试题尤其是算法和逻辑部分往往参考了CSP-J入门级的题型。CSP-J第一轮是笔试机试形式的选择和判断第二轮是上机编程。其中第一轮的基础知识题覆盖了C语法、数据结构、算法复杂度、简单图论等入门级笔试的高频考点。第二轮的前两题通常是模拟题、枚举题、简单排序题难度也正好落在“入门级笔试题”的射程范围内。比如热搜里的“y2178 [CSP-J 2025 入门级 T2] 座位(seat)”大概率是一道模拟或贪心题这类题非常适合用来练手。我的建议是准备企业笔试时把近三年的CSP-J第一轮和第二轮T1/T2题目刷一遍然后再去刷企业真题会顺手很多。这个过程也能帮你快速补齐“c编程基础知识”的盲区。顺带一提竞赛题和笔试有一个共同点代码不是写得越花哨越好而是越能正确覆盖边界越好。在VS Code里配置好C/C环境装上C插件自定义构建任务时加上-Wall -Wextra -fsanitizeaddress,undefined自己刷题时就能提前捕获大量越界和未定义行为这笔时间花得非常值。我个人在这些年的笔试和面试里有一个很深的体会入门级题目真正的难度不在于解法而在于你愿不愿意把每一个基础知识点都当成“值得追究到内存模型”的东西。字符串数组初始化背后是只读数据段智能指针背后是资源所有权vector扩容背后是均摊复杂度。把这些底层逻辑串成一条线之后你会发现自己看面试题的眼神都会不一样——因为很多题目你已经能自己“编”出来了。最后再分享一个我这几年改卷时总结的小技巧。做题的时候如果你对某道题“感觉会做但又不完全确定”最好在代码旁边用注释写一下你的思路。笔试改卷时这种注释能帮你挽回不少分数因为阅卷人能看到你不是在瞎蒙而是真的理解边界条件。别觉得注释浪费时间入职之后写代码注释的比例只会更高。