尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C++入门笔试题精讲:从指针内存到类与算法核心考点

C++入门笔试题精讲:从指针内存到类与算法核心考点 1. 写在前面这套笔试题到底在考什么C入门级笔试题听起来不算难但真正刷过一轮的人会发现入门级和“随便写写就能过”完全是两回事。我在帮团队筛简历、带新人、甚至自己准备跳槽的时候反复遇到过同一类现象候选人能聊框架、聊项目聊得头头是道但一落到指针、内存、构造析构这些基础题上反而露了怯。这套“C入门级笔试题合集一”就是冲着这个痛点来的。它覆盖的知识点主要集中在C最核心的语言特性上包括指针与引用、内存管理、类与对象、常用算法、字符串处理、函数重载、关键字语义等。出题风格偏向笔试和机试常见题型既有概念辨析也有代码输出题和手写算法题适合正在准备校招笔试、刚学完C想自测水平、或者需要带新人做基础考核的读者。我整理这套题的时候不只是把题目列出来而是把每一道题背后的考点、容易踩的坑、以及面试官到底想听什么答案都写清楚了。换句话说你看到的不仅是答案而是“为什么是这个答案”。这样刷一遍比闷头做十套题都管用。2. 入门级笔试题的常见出题套路2.1 为什么多数入门笔试题都在考“指针和引用”如果你翻过几套C入门级的笔试题大概率会发现一个规律指针和引用相关内容出现的频率高得离谱。这背后是有原因的——指针是C区别于很多高级语言的标志性特性也是新手最容易翻车的地方。面试官出这类题本质上是在考察你有没有建立起“内存模型”的概念而不仅仅是背会了几个语法。常见的考法有两种。第一种是概念辨析题比如“指针和引用的区别是什么”这种题看起来基础但答全并不容易。第二种是代码阅读题给出几行代码让你说输出结果这比概念题更狠直接暴露你到底是真懂还是假懂。我在实际评审中见过不少候选人在这种题上栽跟头明明平时写业务代码没问题一碰到指针运算就懵。这背后的原因其实不难理解现代C开发中智能指针和容器封装了大量底层操作很多人日常根本不会直接操作裸指针。但笔试不会因为你用不上就不考因为指针直接关系到一个开发者能否理解程序运行时数据是怎么存放和访问的。建议基础薄弱的朋友先把指针和引用的底层差异理清楚再看后面的题目。2.2 类与对象、内存管理是第二个高频区除了指针入门级笔试另一个绕不开的大类是“类与对象”包括构造函数、析构函数、拷贝构造、赋值运算符、深拷贝与浅拷贝等。这类题往往以“写出下列代码的输出”或者“指出程序中的问题”的形式出现看起来是在考语法实际上是在考你对对象生命周期的理解。举个例子很多人都知道“析构函数在对象销毁时调用”但一旦涉及到继承、静态对象、局部对象和堆对象混合存在时调用顺序就很容易搞错。还有一个经典考点是“浅拷贝带来的双重释放问题”如果类里有指针成员默认拷贝构造只是复制指针值两个对象析构时会对同一块内存重复释放程序直接崩溃。这类问题在笔试中反复出现本质上是因为它在实际项目中确实会造成严重bug。内存管理相关的题目也同样重要比如“new和malloc的区别”“delete和delete[]的区别”。这些知识点看起来简单但很多写了两三年C的人也不一定能答全。建议复习时不要只记结论而是从“谁分配内存、谁调用构造、谁负责释放、不匹配会怎样”这个链条去理解才能应对各种变形题。3. 经典笔试题逐个拆解一概念辨析类3.1 指针和引用的区别这道题几乎是C笔试的“必考题”出现概率极高而且经常以不同形式反复出现。我需要先说明一个容易被人忽视的点这道题没有标准答案——严格来说是答案有多个层面面试官会根据你回答的深度判断你的掌握程度。基础层面的答案是这样的指针是一个变量存储的是另一个变量的地址可以被重新赋值也可以为空nullptr。引用是一个变量的别名必须在定义时初始化之后不能再绑定到其他对象不存在空引用。但光答这些还不够。真正的加分项在于以下几点第一从内存角度看指针本身占用内存大小为sizeof(指针类型)在64位平台下通常是8字节而引用不单独占用内存它只是原变量的别名在底层实现上可能被编译器处理为指针但从语言语义上它不拥有独立的内存空间。第二从安全性角度看引用比指针更安全。因为引用必须初始化所以不存在“野引用”这种说法而指针如果声明后不初始化就是一个野指针不知道指向哪里一旦解引用就可能段错误。这也是为什么现代C风格更推荐用引用和智能指针而不是裸指针。第三从使用场景看函数参数传递时如果希望修改实参的值传引用比传指针更简洁如果参数可能为空或者需要表达“没有对象”这种语义就只能用指针比如链表节点、树节点通常用指针。下面我用一段代码来演示引用和指针的关键区别#include iostream using namespace std; void test(int *p, int r) { *p 20; // 通过指针修改实参 r 30; // 通过引用修改实参 } int main() { int a 10; int b 10; int *ptr a; // 指针指向a int ref b; // 引用绑定b *ptr 15; ref 25; cout a a endl; // a 15 cout b b endl; // b 25 // 指针可以重新指向其他变量 int c 99; ptr c; // 合法 // 引用不能重新绑定ref始终是b的别名 // ref c; 这行代码并不会让ref绑定c而是把c的值赋给b cout b b endl; // b 99 // nullptr测试 int *p2 nullptr; if (p2 nullptr) { cout p2 is null endl; } // int r2; // 编译错误引用必须初始化 return 0; }这里要特别注意“ref c”这一行。很多新手以为引用可以像指针一样改变绑定关系实际上引用一旦绑定就不能再改变这行代码的效果是把c的值赋给ref所引用的变量b所以b变成了99。这个坑在笔试的选择题里经常出现我见过不止一次。3.2 new/delete与malloc/free的区别这道题也是高频考点直接关系到你对C内存管理机制的理解。很多人的第一反应是“new是C的malloc是C的”这个说法没错但太表面了。面试官想听的是下面这些关键差异第一大区别是类型安全。malloc返回的是void*需要手动强制转换成目标类型的指针而new直接返回对应类型的指针类型是安全的不需要强转。这在C里是一个重要的改进减少了因为强转错误导致的bug。第二大区别是构造与析构。new在分配内存之后会自动调用构造函数来初始化对象delete在释放内存之前会自动调用析构函数。而malloc/free只是分配和释放原始内存完全不涉及构造和析构。这点对于内置类型int、char等来说差异不大但对于自定义类来说差异是本质性的。如果你用malloc给一个类对象分配内存构造函数根本不会被调用成员变量的初始化就要靠你自己处理。第三大区别是处理方式。malloc分配失败返回NULLnew分配失败默认抛出std::bad_alloc异常。这导致出错处理的逻辑完全不同。老式C代码里你要判断malloc的返回值是否为NULL而现代C里一般不需要检查new的返回值因为失败会直接抛异常。第四大区别是重载能力。new和delete是运算符可以被类重载从而实现自定义的内存分配策略而malloc/free是标准库函数无法被重载。还有一个特别容易考的搭配问题new和delete配套malloc和free配套不能混用。用new[]分配的内存必须用delete[]释放。如果只用delete而不是delete[]行为是未定义的虽然在许多编译器上看起来能运行但可能只析构了第一个元素其余元素的内存没有被正确释放。#include iostream #include cstdlib using namespace std; class Demo { public: Demo() { cout Demo constructor endl; } ~Demo() { cout Demo destructor endl; } }; int main() { // 对比 malloc 和 new Demo *d1 (Demo *)malloc(sizeof(Demo)); // 只分配内存不调用构造函数 Demo *d2 new Demo; // 分配内存并调用构造函数 free(d1); // 只释放内存不调用析构函数 delete d2; // 调用析构函数并释放内存 // 数组版本 Demo *arr1 new Demo[3]; // 创建3个对象调用3次构造函数 delete[] arr1; // 调用3次析构函数再释放内存 return 0; }这段代码的输出结果很直观d1用malloc分配后没有任何构造输出d2用new分配后有“Demo constructor”输出。这说明new不仅是“分配内存”更是一个“创建对象”的完整过程。数组版本也验证了new[]和delete[]的正确用法。3.3 数组和指针到底有什么区别“数组和指针”是入门级笔试的经典考点也是网上讨论最多的话题之一。很多人会背“数组名是常量指针”这句话但这句话其实是错的或者说不完全对。数组名在大多数表达式中会退化为指向首元素的指针但它本身并不是指针。核心区别要从三个角度来理解第一个角度是类型和内存。数组是相同类型元素的集合在内存中占用一段连续的空间而指针是一个变量保存的是某个对象的地址。举个例子int a[5]在栈上占用20字节假设int为4字节而int *p只是一个8字节的变量p可以指向a[0]也可以指向任何其他int变量。第二个角度是运算行为。虽然数组名和指针都支持下标操作和指针算术但数组的sizeof和指针的sizeof完全不同。sizeof(a)得到的是整个数组占用的字节数而sizeof(p)只是指针本身的大小64位平台下为8。这个区别经常出现在代码阅读题里是一个很隐蔽的坑。第三个角度是赋值行为。数组名不能被赋值因为它不是可修改的左值而指针可以随意重新赋值。我准备了一段比较有代表性的代码能把这几个区别串起来#include iostream using namespace std; 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位平台 cout arr[2] arr[2] endl; // 3 cout *(p 2) *(p 2) endl; // 3 cout p[2] p[2] endl; // 3指针也能用下标 // 二维数组的情况 int matrix[2][3] {{1, 2, 3}, {4, 5, 6}}; cout sizeof(matrix) sizeof(matrix) endl; // 24 cout sizeof(matrix[0]) sizeof(matrix[0]) endl; // 12 cout sizeof(matrix[0][0]) sizeof(matrix[0][0]) endl; // 4 return 0; }这里的关键是sizeof(matrix)算的是整个二维数组的大小也就是2×3×424字节matrix[0]相当于第一行的一维数组大小是3×412字节而matrix[0][0]是单个int大小是4字节。这个递进关系如果理解了二维数组和指针的关系也就基本掌握了。二维数组还有一个最常见的考点int matrix[2][3]的类型到底是什么它既不是int**也不是int*。matrix的类型是“指向长度为3的int数组的指针”也就是int(*)[3]。如果你把matrix赋给int**编译会报错或者给出警告。很多笔试题会故意让你判断类似写法是否合法实际上就是在考这个点。建议拿笔在纸上画一画内存布局别只靠背结论。4. 经典笔试题逐个拆解二程序输出与代码阅读4.1 函数重载和默认参数函数重载和默认参数是C入门级笔试里非常喜欢考的内容因为它能考察你对编译器解析规则的理解。函数重载允许同名函数拥有不同参数列表编译器根据调用时传入的参数类型和数量来决定调用哪个版本。默认参数则允许函数调用时省略部分参数编译器自动用默认值填充。笔试里最常见的坑是“重载和默认参数混在一起时的二义性”。比如void func(int a) { cout func(int) endl; } void func(int a, int b 10) { cout func(int, int) endl; } int main() { func(5); // 编译错误fun是调用哪个两个都匹配 func(5, 6); // 正确匹配第二个 return 0; }调用func(5)时第一个函数需要1个参数匹配第二个函数虽然有2个参数但第2个有默认值所以也只需要1个参数就能调用也匹配。两个都匹配编译器无法判断调用哪个于是报二义性错误。这个考点在选择题里反复出现理解后就不会再错。除了二义性还有一个考点是“默认参数只能从右往左提供”。比如void print(int a, int b 1, int c 2); // 合法 void print(int a 1, int b, int c 3); // 非法默认参数后面不能有非默认参数这个规则的原因很好理解编译器是按照实参从左到右匹配形参的如果中间某个参数没有默认值编译器无法确定该从哪个位置开始省略参数。另一类高频题是“重载的匹配优先级”。当调用一个函数时编译器会按以下顺序寻找最佳匹配完全匹配 提升匹配char提升为int、float提升为double 标准转换int转double等 用户自定义转换。这个优先级在很多复杂题目里都会用到。我见过一道很经典的变体题void test(char c) { cout char endl; } void test(int i) { cout int endl; } void test(double d) { cout double endl; } int main() { char c A; test(c); // char完全匹配 short s 1; test(s); // intshort需要提升为int float f 1.0f; test(f); // doublefloat提升为double return 0; }这里的核心是理解“提升和转换的区别”。char到int、short到int、float到double都属于提升是更优先的匹配而int到double、double到int都属于转换优先级低。实际笔试中你不需要记住所有细节规则但至少要知道传入实参的类型和形参类型完全一致时一定优先匹配。4.2 数组和指针笔试题的常见代码输出接下来我挑几道典型的数组与指针代码输出题这些在笔试中经常以“写出程序运行结果”的形式出现。第一道#include iostream using namespace std; int main() { int a[] {10, 20, 30, 40, 50}; int *p a; cout *p endl; // 10p指向a[0] cout *(p 1) endl; // 20 cout *p 1 endl; // 11注意优先级*p先取10再加1 cout *(p) endl; // 10先取*p的值10然后p自增 cout *p endl; // 20此时p指向a[1] cout *p endl; // 30p先自增指向a[2]再取值 return 0; }这道题关键有两个点。第一个是p 1因为的优先级高于所以先解引用再加1结果是11而不是指向a[1]再取值。第二个是*(p)和*p的区别p是后置自增先使用p当前的地址取值再让p指向下一个元素p是前置自增先让p指向下一个元素再取值。这里C的运算符优先级和自增自减行为混合在一起是新手的重灾区。第二道是二维数组相关的#include iostream using namespace std; int main() { int a[2][3] {{1, 2, 3}, {4, 5, 6}}; cout **a endl; // 1a指向第一行*a是a[0]**a是a[0][0] cout **(a 1) endl; // 4a1指向第二行 cout *(*(a 1) 2) endl; // 6第二行第三个元素 cout *(a[1] 1) endl; // 5a[1]指向第二行开头1后取第二个 return 0; }这道题在笔试中很常见因为它考察的是二维数组在内存中的连续排列方式。二维数组int a[2][3]在内存中并不是“两行独立的空间”而是一段连续的6个int。a的类型是int()[3]a1会跳过一整个长度为3的int数组所以**(a1)取到的是第二行第一个元素4。而a[1]的类型是inta[1]1是第二行第二个元素的地址解引用得到5。明白这个层级关系之后二维数组的题目就不再靠猜了。第三道是字符串数组初始化相关的#include iostream using namespace std; int main() { char str1[] hello; char str2[] {h, e, l, l, o}; char *p hello; cout sizeof(str1) endl; // 6包含末尾的\0 cout sizeof(str2) endl; // 5没有\0 cout strlen(str1) endl; // 5 // cout strlen(str2) endl; // 危险str2没有\0strlen会继续读到栈上后面的内存 return 0; }字符串初始化这道题特别容易踩坑。用字符串字面量初始化char数组时编译器会自动在末尾添加\0所以sizeof(str1)是6。用字符列表初始化时不会自动添加\0所以sizeof(str2)是5。如果你对str2调用strlen它会从一个没有终止符的数组开始向后找直到在内存的某个位置碰巧遇到0字节为止结果是未定义的。这种情况下可能输出一个非常离谱的大数甚至导致程序崩溃。笔试中如果碰到这类题一定要先数清楚有没有\0。4.3 static和const的常见考点static和const是C关键字里最容易被问出花样的两个。这里我挑几个入门级笔试中最常出现的角度来讲。先说static。static在不同语境下有完全不同的含义这是它最让新手抓狂的地方。类的static成员变量需要在类外单独定义和初始化因为它是属于类的不是属于某个对象的。所有对象共享同一个static成员变量。笔试里常见的坑是只写“static int count;”没写类外定义链接时报错。比如class Counter { public: static int count; }; int Counter::count 0; // 必须的类外定义 int main() { Counter c1; Counter c2; c1.count 5; cout c2.count endl; // 5c1和c2共享同一个count return 0; }这段代码的输出是5因为static成员变量不依赖具体的对象实例所以c1修改count后c2看到的count也变了。这个特性在单例模式、全局计数、对象实例数量统计等场景中很常用。static局部变量也经常考。static局部变量存放在静态存储区只初始化一次函数多次调用时它的值会保留void increment() { static int count 0; count; cout count ; } int main() { increment(); // 1 increment(); // 2 increment(); // 3 return 0; }输出是“1 2 3”。如果去掉static输出会变成“1 1 1”因为每次调用都会重新创建局部变量并初始化为0。这个区别很基础但很多人在笔试里一紧张就会写错。再说const。const的基本含义大家都很清楚被修饰的变量值不能被修改。但笔试更爱考的是const和指针的结合形式。const int *p表示p指向一个const int你不能通过p修改它指向的值但p本身可以指向其他地址。int *const p表示p是一个const指针p本身不能被重新赋值但可以通过p修改它指向的值。const int *const p两者都不可变。记忆技巧很简单const修饰的是它左边的内容如果左边没有内容就修饰它右边的内容。最后是const成员函数。在类的成员函数后面加const表示这个函数不会修改对象的状态class Person { private: int age; public: Person(int a) : age(a) {} int getAge() const { return age; } };笔试中经常问一个const对象能不能调用非const成员函数答案是“不能”。因为非const成员函数可能修改对象状态而const对象不允许被修改。反过来非const对象可以调用const成员函数const成员函数是更严格的承诺放宽了谁会调用它。4.4 构造函数、析构函数与拷贝控制构造函数和析构函数的调用顺序、拷贝构造和赋值运算符的区别、深拷贝与浅拷贝这三块内容几乎占据了入门级类相关笔试题的半壁江山。先说构造函数和析构函数的调用顺序。在继承关系中构造顺序是先基类后派生类析构顺序是先派生类后基类。打个比方盖房子要先打地基再盖屋顶拆房子要先拆屋顶再拆地基。这个顺序保证了在构造时基类部分先被初始化派生类可以使用基类的成员在析构时派生类先清理自己独有的资源再调用基类的析构函数清理基类部分避免基类资源和派生类资源相互依赖时出错。除了继承关系还有组合关系的调用顺序。一个类包含另一个类的对象作为成员时构造顺序是先按成员声明的顺序构造成员对象再执行类自身的构造函数体析构顺序则完全相反。笔试常考的一个例子#include iostream using namespace std; class A { public: A() { cout A construct endl; } ~A() { cout A destruct endl; } }; class B { public: B() { cout B construct endl; } ~B() { cout B destruct endl; } }; class C : public A { private: B b; public: C() { cout C construct endl; } ~C() { cout C destruct endl; } }; int main() { C c; return 0; }输出顺序是A construct、B construct、C construct、C destruct、B destruct、A destruct。这里要注意即使B成员变量在C的构造函数的初始化列表中排在其他位置之后它仍然在成员声明的位置被构造而不是按初始化列表的顺序。不过这里没有用到初始化列表所以表现更直观。再说拷贝构造和赋值运算符的区别。拷贝构造函数是在创建一个新对象时用一个已有对象初始化它A a2(a1);。赋值运算符是在两个对象都已存在的情况下把a1的内容赋给a2a2 a1;。笔试里常考的一个点是A a2 a1; // 这是拷贝构造不是赋值因为a2是新创建的对象所以即使你用了它调用的也是拷贝构造函数而不是赋值运算符。这个看起来简单的区别在笔试中经常被用来“挖坑”。最后是深浅拷贝。如果一个类含有指针成员默认的拷贝构造和赋值运算符执行的是浅拷贝也就是说只复制指针的值而不是指针指向的数据。这会导致两个对象指向同一块堆内存析构时发生双重释放程序崩溃。正确的做法是自定义拷贝构造和赋值运算符实现深拷贝——为指针成员分配新的内存复制原对象指向的数据。这类题在笔试中经常以“以下代码有什么问题”的形式出现如果你能看到双重释放基本上就能得分。5. 经典笔试题逐个拆解三手写算法与代码实现5.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]) { swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) { break; // 如果这一轮没有任何交换说明数组已经有序提前退出 } } }这里有一个优化点当某一轮循环没有发生任何交换时说明数组已经有序可以提前结束外层循环。这个优化在实际笔试中不一定需要写出但如果你写出来会给面试官留下更好的印象。冒泡排序的时间复杂度是O(n^2)稳定排序空间复杂度O(1)。其次是选择排序。它的思想是每一轮从未排序部分选出最小的元素放到已排序部分的末尾。和冒泡不同选择排序每轮最多只交换一次所以写起来更简洁void selectionSort(int arr[], int n) { for (int i 0; i n - 1; i) { int minIndex i; for (int j i 1; j n; j) { if (arr[j] arr[minIndex]) { minIndex j; } } if (minIndex ! i) { swap(arr[i], arr[minIndex]); } } }选择排序也是O(n^2)但它是不稳定排序。原因在于交换可能把相同值的相对顺序打乱。比如数组{5, 3, 5, 1}第一轮把最小的1和第一个5交换后两个5的相对位置就变了。这个不稳定性的例子在笔试中经常被问到。最后是插入排序。它的思想类似于打牌时整理手牌从第二个元素开始依次把它插入到前面已经有序的序列中正确的位置。void insertionSort(int arr[], int n) { for (int i 1; i n; i) { int key arr[i]; int j i - 1; while (j 0 arr[j] key) { arr[j 1] arr[j]; j--; } arr[j 1] key; } }插入排序在数据基本有序的情况下表现非常好时间复杂度可以接近O(n)但在最坏情况下逆序是O(n^2)。它是稳定排序。笔试中如果让你比较这三种排序的最佳情况、最坏情况以及稳定性除了记住表格更要理解“为什么”。5.2 链表基础操作反转、判环、合并链表的指针操作是C入门级笔试里算法题的重要分支。链表题通常代码不长但极其考察对指针的掌控力。这里我挑三道最经典的题来拆解。第一道是“反转单链表”。这是链表题里最常出现的题目没有之一。核心思路是使用三个指针prev、cur、next依次把每个节点的next指向前一个节点struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseList(ListNode* head) { ListNode *prev nullptr; ListNode *cur head; while (cur ! nullptr) { ListNode *next cur-next; // 先保存下一个节点 cur-next prev; // 反转指针 prev cur; // 移动prev cur next; // 移动cur } return prev; }这里的关键是“先保存下一个节点”。很多新手写着写着就发现cur-next在赋值之后原来指向下一个节点的指针丢失了导致循环无法继续。所以在改变cur-next之前必须先用一个临时变量保存cur-next。这个细节可以说是链表题的“头号坑”。第二道是“判断链表是否有环”。最常见的解法是快慢指针法。快指针每次走两步慢指针每次走一步。如果链表有环快慢指针最终一定会相遇如果没有环快指针会先到达链表末尾。bool hasCycle(ListNode *head) { if (head nullptr || head-next nullptr) { return false; } ListNode *slow head; ListNode *fast head-next; while (slow ! fast) { if (fast nullptr || fast-next nullptr) { return false; } slow slow-next; fast fast-next-next; } return true; }为什么快慢指针一定能相遇原因是快指针相对慢指针的速度是“每一步多走一个节点”也就是说快指针在逐步逼近慢指针每走一步差距减少1而环的长度是有限的所以最终一定追上。如果速度差是2或更大可能会出现恰好跳过的情况所以快慢指针法通常选择速度差为1。第三道是“合并两个有序链表”。这个题适合用递归来写代码简洁思路也清晰ListNode* mergeTwoLists(ListNode *l1, ListNode *l2) { if (l1 nullptr) return l2; if (l2 nullptr) return l1; if (l1-val l2-val) { l1-next mergeTwoLists(l1-next, l2); return l1; } else { l2-next mergeTwoLists(l1, l2-next); return l2; } }递归的核心在于“每一步取出较小值的节点然后递归合并剩下的部分”。这个思路只要想通写起来非常快。如果面试官要求不用递归也可以改用迭代哑节点的方式代码会稍微长一些但不复杂。建议两种都准备一下。5.3 快速幂算法入门级笔试里的“隐藏Boss”快速幂算法在入门级笔试题里出现的频率不如排序和链表高但一旦出现往往是区分度的关键。它的核心问题是如何高效计算a^n最简单的做法是循环乘n次时间复杂度O(n)。但快速幂可以做到O(log n)原理是把指数拆成二进制利用幂的乘法法则。快速幂背后的数学原理其实很朴素。把n写成二进制形式比如n13二进制是1101也就是13 8 4 1。那么a^13 a^8 * a^4 * a^1。我们只需要依次计算a^1、a^2、a^4、a^8然后把二进制位为1的部分乘起来就行了。下面是一个迭代版的快速幂实现long long fastPow(long long a, long long n) { long long result 1; while (n 0) { if (n 1) { // 当前二进制位是1 result * a; } a * a; // a平方对应二进制的下一位 n 1; // n右移一位 } return result; }举个例子计算2^13。n13的二进制是1101。初始result1a2。第一次循环n 1 1result 12 2a 22 4n右移变为6。第二次循环n 1 0result不更新a 4*4 16n右移变为3。第三次循环n 1 1result 216 32a 1616 256n右移变为1。第四次循环n 1 1result 32256 8192a 256256 65536n右移变为0。结果是8192而2^13确实等于8192。这里result的变化过程正好对应二进制1101也就是在二进制位为1的8、4、1三个位置乘上了对应的幂。如果n很大用这个算法只需要大约log2(n)次循环而不是n次。笔试里如果遇到n的取值范围很大的题目比如n可以达到10^9甚至10^18直接用循环乘会超时快速幂就是标准解法。如果题目还要求对结果取模只要在result * a和a * a之后都加上取模操作即可。5.4 字符串常见题反转、统计、字符数组字符串处理在C入门级笔试中出现的频率也相当高而且往往和数组、指针、字符数组的知识点交织在一起。这里我整理了三类最常见的字符串题。第一类是“字符串反转”。最简单的方式是双指针法一个指针从头部出发另一个从尾部出发交换两个位置的字符直到两个指针相遇。void reverseString(char s[], int len) { int left 0; int right len - 1; while (left right) { swap(s[left], s[right]); left; right--; } }如果处理的是std::string写法类似只是获取长度用s.size()。如果是string也可以直接用std::reverse(s.begin(), s.end())一行搞定。但在笔试中考官可能希望你手写实现过程不要只会用库函数。第二类是“字符统计”。给定一个字符串统计每个字符出现的次数。常用的容器是mapchar, int或者一个长度为256的int数组因为ASCII码的范围是0~127所以长度为256的数组足够覆盖所有ASCII字符。#include iostream #include string using namespace std; void countCharacters(const string s) { int count[128] {0}; for (char c : s) { count[(int)c]; } for (int i 0; i 128; i) { if (count[i] 0) { cout (char)i : count[i] endl; } } }这里的技巧是“用字符的ASCII码作为数组下标”。这个思路在很多算法题里都能用到可以大幅度降低代码复杂度。第三类是“判断回文串”。思路是双指针从两端向中间扫描每次比较left和right位置的字符是否相同只要有不同就不是回文。bool isPalindrome(const string s) { int left 0; int right s.size() - 1; while (left right) { if (tolower(s[left]) ! tolower(s[right])) { return false; } left; right--; } return true; }如果题目变体要求忽略空格和标点、只比较字母数字那么需要在每次比较前跳过非字母数字的字符。这属于“回文串”大家族里最常见的变形我建议把这个版本也练熟。6. 常见问题与避坑指南6.1 笔试中常见的C“陷阱题”总结入门级C笔试里有很多“陷阱题”看起来平平无奇实际上暗藏杀机。我把自己在评审和刷题中遇到的高频陷阱整理成了一张表方便考前快速过一遍。陷阱类型题目示例易错点正确理解运算符优先级*p 1以为是取p[1]的值先解引用再加法sizeof与strlensizeof(str) vs strlen(str)忽略末尾\0sizeof算\0strlen不算二维数组指针int a[2][3]的类型误以为是int**实际是int(*)[3]浅拷贝类含指针成员的默认拷贝忽略了双重释放需要自定义深拷贝后置自增(p)和p分不清何时自增后置先取值再自增前置先自增再取值new[]与delete[]new int[10]配delete只写了delete必须用delete[]默认参数二义性void f(int)和void f(int1)漏考虑两个都匹配调用f(1)会编译错误静态成员变量static int count;只声明没定义必须在类外定义一次const对象调方法const对象调非const方法以为可以调用编译错误拷贝构造vs赋值A a2 a1;误以为是赋值调用拷贝构造这张表建议打印出来贴在电脑前考前和面试前各过一遍。这些坑单独看都很简单但组合在一起出现在同一套笔试题里时很容易让人做完之后心里发虚。我自己在刚开始刷题的时候反反复复在这些位置扣分后来总结成表之后正确率明显提升。6.2 新人刷题时最容易忽视的三个问题第一个问题是“只做不写”。很多人刷笔试题喜欢在脑子里过一遍思路觉得“这题我会”然后就跳到下一题。但实际上C笔试题的许多坑是在手写代码时才会暴露出来的比如忘记包含头文件、忘记处理边界条件、指针操作顺序搞错等。我强烈建议把每一道题目都当成真正的笔试来对待在编辑器里写代码、编译、运行验证输出结果。如果连编译环境都不愿意打开那刷题效果至少打七折。第二个问题是“不关注编译错误信息”。有些读者遇到编译错误就慌随便改两行碰运气。其实编译器的错误信息里藏着大量线索仔细阅读错误信息不仅能帮你找到问题还能帮助你理解编译器的解析规则。比如“use of deleted function”“ambiguous call to overloaded function”这些报错都是学习C特性的绝佳素材。我见过很多候选人代码写得不错但一开口就说“我一般不看报错信息”这种习惯在笔试环境下很吃亏。第三个问题是“只学不总结”。每道题做完之后如果不对知识点做归纳那就只是在“做题”而不是在“复习”。我的建议是每做完一组题把错题和不会的题整理成一个文档标明考点、错因、正确思路和类似的题目链接。考前冲刺时只需要看这个文档效率远远高于重新刷一遍题。我自己在准备面试时就是靠这个错题本快速找回状态的。6.3 关于C学习路线的个人建议这套笔试题合集主要覆盖的是C入门和进阶起步阶段的知识。如果你的目标是准备笔试或面试我建议在刷题之外按照下面的顺序做系统复习第一步把语法基础打牢。包括基本数据类型、运算符、流程控制、函数、数组、指针、引用、结构体这些内容。不要觉得简单就跳过很多笔试题就是在这些基础知识点上做文章。第二步把类与对象的核心内容吃透。构造函数、析构函数、拷贝控制、static、const、友元、运算符重载这些都是C区别于C语言的核心特性也是笔试的高频区。第三步掌握STL常用容器和算法。vector、string、map、set、sort、find这些工具在笔试和实际开发中都用得上。入门级笔试题中直接考察STL深度的不多但知道它们能让你在写算法题时事半功倍。第四步练习手写代码。链表操作、二叉树遍历、排序算法这些手写题需要到“闭着眼睛也能写”的程度。如果你是在校学生建议把这套复习计划拉长到三个月左右每天保持至少一个小时的C编码时间。如果你是在职准备跳槽时间更紧张那就优先把指针、内存管理、类这三块啃下来因为它们出现频率最高。7. 写在最后的经验分享这套“C入门级笔试题合集一”里的内容都是我这些年筛选简历、组织笔试、带新人的经验积累同时也是我自己日常刷题时反复碰到的高频考点。我最大的体会是C笔试从来不考偏题怪题考的都是最核心、最基础、但最容易被忽视的知识。很多人觉得C难其实不是难在语法而是难在“你以为你懂了但真正动手写时发现细节全错了”。我建议你现在就做一件事打开编译器把文章里的代码复制下来去掉答案自己重新写一遍。如果你能默写出冒泡排序、单链表反转、快速幂这三个经典实现并且能说清楚new/delete与malloc/free的四个区别那么这套题你基本就过关了。后续我还会整理更多贴近真实笔试场景的题目尤其是STL、模板、多态这些进阶方向的内容继续保持这个练习节奏等到真正坐在笔试考场上的时候你会发现自己比想象中从容得多。
返回列表