C++实习面试题
1题
考虑如下函数:
void swap_ptr(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp;
} void swap_ref(int &a, int &b) { int tmp = a; a = b; b = tmp;
}
(1) 调用 swap_ptr(nullptr, nullptr) 时程序会怎样?如何修改解决?
(2) 编译 swap_ref(nullptr, nullptr) 能否通过?为什么?
(1) 调用 swap_ptr(nullptr, nullptr) 时程序会怎样?如何修改解决?
因为解引用空指针,会触发未定义行为 int a、int b 只是声明指针参数,不算解引用,不会报错 而* a(函数里操作指针指向内容时)才时解引用,会真正访问指针指向的内存。
- 解引用空指针:就是拿着一个 “无效地址标签”,强行去读写它号称指向的内存。
- 触发未定义行为:程序会进入一种 “C++ 标准不管、结果随机” 的状态,可能崩、可能乱,总之 绝对不能这么写 。
(2) 编译 swap_ref(nullptr, nullptr) 能否通过?为什么?
原因:C++中引用(int&)必须绑定到有效对象,nullptr是空指针常量,无法绑定到引用,编译器会直接报错。
C++里面,引用时变量的"别名",必须邦绑定到"已存在的有效的对象"(不能绑定空/非法内容)。
当调用 swap_ref(nullptr, nullptr) 时:
- nullptr 是 “空指针常量”,它不对应任何实际的 int 对象;
- 但 swap_ref 的参数要求是 int &(必须绑定到真实 int 变量的引用 )。
非法内容:
“非法内容” 指无法被引用正确绑定的、不存在或无效的实体,比如未初始化的内存、已释放的内存、空地址对应的 “内容” 等。
2题
分析以下代码
#include <iostream>
#include <vector>
using namespace std;void transform(int *&ptr, int **pptr, vector<int> &vec) {*pptr = &vec[0];ptr = *pptr + 1;vec.push_back(100);
}int main() {int x = 10;int *p1 = &x;int **p2 = &p1;vector<int> data = {1, 2, 3};cout << "Before: " << *p1 << "," << **p2 << endl;transform(p1, p2, data);cout << "After: " << *p1 << "," << **p2 << endl;
}
上面代码运行的实际输出结果是什么?有什么问题?应该如何修改?
输出结果:
Before输出:10,10(*p1是x的值,*p2解引用后也是x的值) After输出:行为未定义(因vec.push_back(100)可能会导致&vec[0]失效,触发野指针访问),导致崩溃
其中int*& ptr解释:
- int* ptr是指针的引用
- int* :表明这是一个引用(这里是指针类型的引用)。
- 这里指的是给一个指针变量起一个别名,通过这个别名能直接修改原指针的指向。
问题
*pptr = &vec[0];:将指针 pptr 解引用(访问其指向的指针),让它指向 vector vec 的首个元素地址。
ptr = *pptr + 1;:先解引用 pptr 拿到 vec[0] 地址,+1 后让 ptr 指向 vec[0] 相邻的下一个 int 位置(逻辑上是 vec[1] 位置,但后续 vector 扩容会让这变成野指针 )。
问题根源
vec.push_back(100) 可能引发 vector 扩容:
- 扩容时 vector 会重新分配内存、拷贝原数据,原 &vec[0] 地址失效;
- 但 *pptr = &vec[0]; 已让 p2 间接指向旧地址,后续访问 *p1,*p2 变成野指针操作,触发未定义行为。
如何修改
方案1:提前预留空间,避免扩容
在 push_back 前通过 reserve 预留足够容量,确保 vector 不重新分配内存。
void transform(int*& ptr, int** pptr, vector<int>& vec) {vec.reserve(vec.size() + 1); // 预留至少1个元素空间,防止push_back扩容*pptr = &vec[0]; // 记录首元素地址(此时地址不会因后续push_back改变)ptr = *pptr + 1; // ptr指向vec[1]的位置(假设存在)vec.push_back(100); // 安全:不会触发扩容
}
方案2:调整操作顺序,延迟指针赋值
先完成 push_back 操作(可能扩容),再更新指针指向新的有效地址。
void transform(int*& ptr, int** pptr, vector<int>& vec) {vec.push_back(100); // 先添加元素(可能扩容)*pptr = &vec[0]; // 扩容后再记录首元素地址(此时地址稳定)ptr = *pptr + 1; // ptr指向vec[1](即新添加的元素100)
}
3题
考虑下面函数
const char *combineString(const std::string &s1, const std::string &s2) {std::string result = s1 + ":" + s2; // ① 在栈上创建临时string对象return result.c_str(); // ② 返回指向临时对象内部的指针
} // ③ 函数结束,临时对象销毁,指针变成野指针!
指出此函数的问题,并给出两种安全的实现方案。
问题分析
c_str():把std:string对象里的字符串内容,转换成C风格的字符串(以\0结尾的const char*指针).
这里面c_str() 会返回一个指向 std::string 内部字符的 const char* 指针,这个指针以 \0 结尾,可直接用于需要以 \0 结尾的字符串的函数(如 printf、fopen 等)。
函数返回了局部对象 result 的 c_str() 指针:
- result 是函数内的局部 std::string,函数结束时会被销毁,其内部字符数组也会释放;
- 返回的 const char* 指向的内存已失效(野指针),后续使用会触发未定义行为(程序崩溃、乱码等)。
实现方案
方案1:返回std:string
直接返回std::string,利用C++的对象生命周期管理,避免裸指针问题:
std::string combineString(const std::string &s1, const std::string &s2) {std::string result = s1 + ":" + s2; // 在栈上创建临时string对象return result; // 返回一个string对象(而非指针)
}// 调用示例
std::string result = combineString("hello", "world"); // 接收返回值
const char* cstr = result.c_str(); // 需要时再转换为const char*
1. return result; 返回的是 std::string 对象本身
- std::string 是一个类,返回时会触发对象的拷贝 / 移动语义(通过拷贝构造函数或移动构造函数)。
- 即使原局部对象 result 被销毁,调用者接收的是新创建的对象副本(内容已复制 / 移动到新内存),生命周期由调用者控制。
2. return result.c_str(); 返回的是 const char*(指针)
- c_str() 返回的是指向 result 内部字符数组的指针,而非对象本身。
- 指针仅仅是一个地址值,返回时只复制了地址,但指针指向的内存(属于 result)仍在原对象中。
- 当 result 被销毁后,该地址指向的内存已无效,导致野指针。
方案2:动态分配内存(需手动管理)
核心思路:用new在堆上分配内存存储字符串,返回堆内存的指针,调用方负责delete[]释放。
const char *combineString(const std::string &s1, const std::string &s2) {std::string result = s1 + ":" + s2;char *buf = new char[result.size() + 1]; // 在堆上分配内存strcpy(buf, result.c_str()); // 复制字符串内容到堆内存return buf; // 返回堆内存的指针
}// 调用示例(必须手动释放!)
int main() {const char *p = combineString("a", "b");// 使用 p ...delete[] p; // 必须手动释放,否则内存泄漏
}
strcpy
是 C 标准库函数,用于 把源字符串(src)(含 \0
)完整复制到目标字符数组(dest),不检查目标空间,可能溢出,需手动确保目标足够大 。
- 头文件:
<cstring>
(C++)或<string.h>
(C) - 原型:
char* strcpy(char* dest, const char* src);
- 核心:复制
src
内容(含终止符\0
)到dest
,返回dest
地址
方案3:让调用方提供缓冲区
核心思路:调用方提前分配内存(栈或堆),传入缓冲区地址和大小,函数将结果写入缓冲区。
void combineString(const std::string &s1, const std::string &s2, char *buf, size_t bufSize) {std::string result = s1 + ":" + s2;if (bufSize > result.size()) { // ① 检查缓冲区是否足够大strcpy(buf, result.c_str());} else {// 处理缓冲区不足的情况(如截断、报错等)buf[0] = '\0'; // 安全起见,置空字符串}
}// 调用示例1(栈上分配缓冲区)
int main() {char buf[100]; // ① 栈上分配固定大小的缓冲区combineString("a", "b", buf, sizeof(buf));// 使用 buf ...
} // ② 栈内存自动回收,无需手动释放// 调用示例2(堆上分配缓冲区)
int main() {char *buf = new char[100]; // ① 堆上分配combineString("a", "b", buf, 100);// 使用 buf ...delete[] buf; // ② 必须手动释放
}
4题
分析以下代码
std::vector<std::string> createStrings() {return {"A", "BB", "CCC"};
}int main() {auto &&v1 = createStrings(); // ①const auto &v2 = createStrings(); // ②auto v3 = createStrings(); // ③
}
(1)分析三种声明方式的类型推导结果;
(2)哪种方式能避免不必要的拷贝
三种声明方式的类型推导结果
- 对于atuo &&v1 = createStrings()
- createStrings()返回的是std::vector<std::string>类型的临时对象(右值)。当使用auto&&(通用引用,在绑定右值场景下表现为右值引用)时,v1的类型会被推导为std::vector<std::string>&&,也就是std::vector<std::string>类型的右值引用,它直接绑定到createStrings()返回的临时vector对象上;
- 对于const auto &v2 = createStrings();
- createStrings() 返回的临时对象(右值)可以绑定到 const 左值引用 。此时 v2 的类型会被推导为 const std::vector<std::string> & ,即 std::vector<std::string> 类型的 const 左值引用,v2 引用着 createStrings() 返回的临时 vector 对象,并且由于是 const 引用,无法通过 v2 去修改 vector 及其元素内容。
- 为什么必须加const左值绑定右值(不加会报错):
- 右值是临时的销毁的对象,如果允许非 const 左值引用绑定右值,会导致 **“无意义的修改”**,这是 C++ 语法明确禁止的:语言规则通过禁止非 const 左值引用绑定右值,从语法层面避免了这种无意义的操作。
- 对于 auto v3 = createStrings();:
- 这里 auto 会推导为值类型 。v3 的类型是 std::vector<std::string> ,它会触发拷贝(在 C++11 及之后,编译器一般会优化为移动构造,但从语义和类型推导层面看是值拷贝语义 ),也就是会创建一个新的 std::vector<std::string> 对象,把 createStrings() 返回的临时对象的内容拷贝
- (或移动 )过来。
哪种方式避免不必要拷贝构造
auto &&v1 = createStrings(); 和 const auto &v2 = createStrings(); 这两种方式能避免不必要的拷贝,原因如下:
- auto &&v1 方式:通过右值引用直接绑定 createStrings() 返回的临时对象,没有新对象的拷贝构造或移动构造过程,直接复用了临时对象的资源,并且延长了临时对象的生命周期(让临时对象的生命周期和 v1 的生命周期一致 )。
- const auto &v2 方式:借助 const 左值引用绑定临时对象,同样不会产生新对象的拷贝,只是建立了对临时对象的引用,也延长了临时对象的生命周期,只是限制了通过 v2 对对象进行修改操作。
5题
考虑以下代码
constexpr int factorial(int n) {return (n <= 1) ? 1 : n * factorial(n - 1);
}int main() {constexpr int val1 = factorial(5); // ①int n;std::cin >> n;int val2 = factorial(n); // ②
}
(1) 解释计算 val1 和 val2 的区别;
(2) 为什么 constexpr 函数在运行时仍有效?
解释计算val1和val2的区别
- val1(编译时计算):
factorial(5) 的参数 5 是编译时常量,constexpr 函数会在编译阶段直接计算结果(5! = 120),结果嵌入到代码中(相当于 constexpr int val1 = 120;)。
编译阶段直接展开递归:
5! → 5×4×3×2×1 → 120
constexpr int val1 = 120; // 编译时已确定为 120
- val2(运行时计算):
n 是运行时通过 cin 输入的变量(编译时未知),factorial(n) 会退化为普通函数调用,在程序运行阶段根据输入的 n 动态计算阶乘结果。
编译器不知道 n 的值,生成完整的递归函数代码:
// 编译生成的伪代码(简化版)
int factorial_runtime(int n) {int result = 1;for (int i = 2; i <= n; i++) {result *= i;}return result;
}
为什么constexptr函数在运行时仍有效
constexpr 函数的设计目标是 “兼容编译时 + 运行时两种场景”:
- 当参数是编译时常量(如 factorial(5)),函数会在编译时执行,结果作为常量使用(如模板参数、constexpr 变量初始化)。
- 当参数是运行时变量(如 factorial(n) 中的 n),函数会退化为普通函数,在运行时动态执行,复用同一套逻辑。
6题
(1)解释vector在使用insert插入元素时,容器变化的详细过程;
(2)为什么红黑树实现的map在遍历时缓存命中率低?
解释vector在使用insert插入元素时,容器变化的详细过程;
vector 基于动态数组实现,插入元素时会触发以下步骤(以 insert(it, val) 插入单个元素为例):
- 检查容量:判断当前 vector 的容量(capacity)是否能容纳新元素。若剩余空间不足(size == capacity),则触发扩容。
- 扩容逻辑(若需扩容):分配新内存(通常是原容量的 1.5 或 2 倍,如原容量 n → 新容量 2n)。
- 将原数组中 [begin(), it) 区间的元素拷贝 / 移动到新内存的起始位置。
- 释放原内存,更新 vector 的数据指针指向新内存。
- 元素后移:将插入位置 it 及之后的元素(从原数据或新内存中)向后移动 1 个位置,腾出插入空间。
- 插入新元素:在腾好的位置构造新元素(调用拷贝 / 移动构造函数)。
- 更新状态:size 加 1,迭代器 it 失效(扩容后所有迭代器失效;未扩容时,插入位置之后的迭代器失效)。
为什么红黑树实现的map在遍历时缓存命中率低?
红黑树的节点在内存中通过指针链接,地址随机且不连续。遍历树时,需要顺着指针在这些随机地址间频繁跳转,而 CPU 缓存依赖 “连续地址批量预加载” 提升效率,这种随机跳转让缓存无法提前准备数据,每次访问节点都可能要从内存重新读取,因此缓存命中率低。
7题
根据以下具体场景需求,选择最合适的 STL 容器(vector、deque、list、map、unordered_map等),并简要说明选择理由(1-2 点关键因素):
场景 1:玩家管理系统
某大型多人在线游戏需要管理玩家数据,玩家总量约 10,000 人。
关键操作:
- 每秒需遍历所有玩家进行状态更新(1000 + 次 / 秒)
- 按玩家 ID 快速查找 / 修改玩家数据(500 + 次 / 秒)
- 玩家频繁登录 / 退出(每秒 50 + 次增删操作)
- 数据特性:玩家 ID 为唯一整数(范围 1-99999)
- 每个玩家包含复杂数据(位置、装备等)
问题:应选择什么容器存储玩家数据?
场景 2:实时消息队列
某社交平台的消息转发系统,每秒处理 200,000 + 条消息。
关键操作:
- 消息严格按接收顺序处理(FIFO)
- 高并发入队(多生产者线程)
- 高并发出队(多消费者线程)
- 内存限制:需控制内存碎片
- 避免频繁内存分配
问题:应选择什么容器实现消息队列?
场景 3:股票价格缓存
某高频交易系统的价格缓存,存储 3000 + 支股票的最新价格。
关键操作:
- 每秒查询股票价格 50,000 + 次(读取密集)
- 每秒更新价格 1,000 + 次
- 每 5 分钟需按股票代码字母顺序导出数据
- 数据特性:股票代码为字符串(如"AAPL")
- 价格数据为浮点数
问题:应选择什么容器存储股票价格映射?
场景1回答
选择容器:unordered_map<int, PlayerData>(PlayerData 为玩家复杂数据的结构体 / 类)
理由:
- 快速查找:unordered_map 基于哈希表,按玩家 ID(唯一整数)查找的时间复杂度为 O(1),满足 “500 + 次 / 秒” 的快速查找需求。
- 遍历效率:需高频遍历所有玩家(1000 + 次 / 秒),unordered_map 的遍历虽然不如 vector 连续内存高效,但结合哈希表的缓存友好性(节点分散但查询快),能平衡 “查找” 和 “遍历” 的需求;若用 vector 需额外维护 ID 索引,增删时效率更低(需移动元素)。
场景2回答
选择容器:deque(双端队列) + 多线程安全封装(如加锁或用无锁队列)
理由:
- FIFO 与内存效率:deque 支持首尾高效操作(入队 / 出队均为 O(1) amortized),且内存分配更紧凑(分段连续),比 list 更节省内存、减少碎片。
- 并发适配:deque 本身不是线程安全的,但可通过加锁(或无锁结构)适配 “多生产者 / 多消费者” 的高并发场景;若用 queue(基于 deque)需补充线程安全逻辑,deque 更灵活。
场景3回答
选择容器:map<string, double>(或 unordered_map<string, double> 结合定期排序)
关键分析:
- 若优先查询性能(50,000 + 次 / 秒读取):选 unordered_map<string, double>,哈希表查询 O(1),但无法直接按股票代码字母序导出。
- 若需按序导出(每 5 分钟字母顺序导出):选 map<string, double>,其基于红黑树,自动按键(股票代码字符串)排序,导出时直接遍历即为有序,满足 “字母顺序导出” 需求;虽然查询是 O(log n),但 3000 + 数据量下 log n 极小(约 12),50,000 + 次查询仍可接受。
最终选择:map<string, double>
理由:
- 有序导出:map 自动维护键的有序性,每 5 分钟导出时直接遍历即为字母顺序,无需额外排序开销。
- 读写平衡:map 的 O(log n) 查询在 3000 + 数据量下足够高效(50,000 + 次 / 秒可轻松处理),且更新(O(log n))也能满足 “1,000 + 次 / 秒” 的需求。
8题
分析函数在内存中创建数据和释放空间的详细过程:(考虑 C++11, 栈内存和堆内存,返回值优化)
#include <vector> std::vector<int> processData(int n) { std::vector<int> localVec; for (int i = 0; i < n; ++i) { localVec.push_back(i); } std::vector<int> temp = localVec; for (int &num : temp) { num += 10; } return temp;
} int main() { std::vector<int> result = processData(3); return 0;
}
详细过程
1. processData(3) 函数调用阶段
- 栈内存:
- 调用 processData(3) 时,函数栈帧创建,包含:形参 n = 3(栈上存储)。
- 局部变量 localVec(栈上存 vector 的元数据:如指向堆的指针、大小、容量)。
- 局部变量 temp(栈上存 vector 的元数据)。
- 调用 processData(3) 时,函数栈帧创建,包含:形参 n = 3(栈上存储)。
- 堆内存:
- localVec.push_back(i) 时,vector 会在堆上动态分配内存存储 int 元素:初始可能分配小容量(如 capacity=1),扩容时(超过当前容量)会重新分配更大的堆内存(如 capacity=2→4→... ),并拷贝旧数据到新堆空间。
- 最终 localVec 的堆内存存储 [0,1,2]。
- localVec.push_back(i) 时,vector 会在堆上动态分配内存存储 int 元素:初始可能分配小容量(如 capacity=1),扩容时(超过当前容量)会重新分配更大的堆内存(如 capacity=2→4→... ),并拷贝旧数据到新堆空间。
2. temp = localVec 阶段(拷贝构造)
- C++11 前:
- temp 深拷贝 localVec 的堆内存(重新分配堆空间,拷贝 [0,1,2] 到 temp 的堆内存)。
- C++11 后:
- 因 localVec 是左值(非临时对象),仍触发深拷贝(vector 的拷贝构造默认深拷贝堆数据)。
- 若 localVec 是右值(如 return localVec; 且开启 RVO),才会触发移动构造(转移堆内存所有权)。
3. return temp 阶段(返回值优化,RVO)(如果不考虑编译器优化就是拷贝构造)
- 返回值优化(RVO):编译器会检测到 temp 是直接返回的局部变量,触发具名返回值优化(NRVO):不实际拷贝 / 移动 temp 的堆内存,而是直接在调用方 result 的栈帧预留的堆内存空间中构造数据。
- 效果:跳过 temp 到 result 的拷贝 / 移动,直接复用最终数据的堆内存。
4. main 函数中 result 接收返回值
- 栈内存:
- result 的栈上元数据(指针、大小、容量)直接指向 processData 中构造好的堆内存(因 RVO 优化)。
- 堆内存:
- result 管理的堆内存存储 [10,11,12](temp 遍历加 10 后的数据)。
5. 内存释放阶段
- processData 栈帧销毁:
- localVec、temp 的栈上元数据销毁,但因 RVO,temp 的堆内存已被 result 接管,不会释放。
- main 函数结束:
- result 出栈,触发 vector 的析构函数,释放其管理的堆内存(存储 [10,11,12] 的空间)。
关键总结
- 栈 vs 堆:栈存 vector 的元数据(指针、大小、容量),堆存实际元素数据。
- C++11 优化:拷贝构造默认深拷贝,但返回值优化(RVO/NRVO)可跳过额外拷贝。
- 释放逻辑:栈内存随函数栈帧销毁自动释放,堆内存由 vector 析构函数自动释放(RAII 机制)。
9题
基于Mstr类实现
class MStr {
public: MStr(const char *str = nullptr) { if(str) { data = new char[strlen(str)+1]; strcpy(data,str); } else { data = nullptr; } } ~MStr() { delete[] data; } // 请实现以下函数 MStr(const MStr &x); // 拷贝构造函数 MStr &operator=(const MStr &x); // 拷贝赋值运算符 MStr(MStr &&x) noexcept; // 移动构造函数 const char *c_str() const { return data; } private: char *data;
};
(1) 实现深拷贝的拷贝构造函数;
(2) 实现深拷贝的拷贝赋值运算符(考虑自赋值);
(3) 实现移动构造函数;
(4) 编写一个简单程序展示以下操作:
- 创建对象s1;
- 使用拷贝构造创建s2;
- 使用拷贝赋值运算符;
- 使用移动构造创建s3;
- 输出所有字符串内容。
解答
#include <iostream>
#include <cstring>
using namespace std;class MStr {
public:// 普通构造函数 MStr(const char* str = nullptr) {if (str) {data = new char[strlen(str) + 1];strcpy(data, str);}else {data = nullptr;}}// 析构函数 ~MStr() { delete[] data; }// (1) 拷贝构造函数(深拷贝) MStr(const MStr& x) {if (x.data) {data = new char[strlen(x.data) + 1];strcpy(data, x.data);}else {data = nullptr;}}// (2) 拷贝赋值运算符(深拷贝 + 自赋值保护) MStr& operator=(const MStr& x) {// 自赋值保护:判断是否是自身 if (this == &x) return *this;// 释放当前对象的旧内存 delete[] data;data = nullptr;// 深拷贝源对象数据 if (x.data) {data = new char[strlen(x.data) + 1];strcpy(data, x.data);}return *this;}// (3) 移动构造函数(转移资源 + noexcept) MStr(MStr&& x) noexcept {//noexcept作用是编译器优先选择移动构造// 转移源对象的资源 data = x.data;x.data = nullptr; // 置空源对象,避免析构释放 }// 获取字符串 const char* c_str() const { return data; }private:char* data;
};// (4) 测试程序
int main() {// 1. 创建对象 s1 MStr s1("Hello");cout << "s1: " << s1.c_str() << endl;// 2. 使用拷贝构造创建 s2 MStr s2 = s1;cout << "s2: " << s2.c_str() << endl;// 3. 使用拷贝赋值运算符 MStr s3("World");s3 = s1;cout << "s3: " << s3.c_str() << endl;// 4. 使用移动构造创建 s4 MStr s4 = MStr("Move"); // 临时对象触发移动构造 cout << "s4: " << s4.c_str() << endl;return 0;
}
11题
实现下面函数模板
template<typename T>
T maxElement(T* arr, size_t size) {// 实现通用版本
}template<>
const char* maxElement<const char*>(const char** arr, size_t size) {// 特化版本:比较字符串长度
}
(1) 实现通用版本的maxElement函数;
(2) 实现const char *类型的特化版本;
(3) 解释以下代码的输出原因:
int nums[3] = {5,9,2};
cout << maxElement(nums,3); // 输出9
const char *strs[3] = {"apple","banana","cherry"};
cout << maxElement(strs,3); // 输出"banana"
解答
#include <iostream>
#include <cstring> // 用于 strlen
using namespace std;// (1) 通用版本:比较值的大小
template<typename T>
T maxElement(T *arr, size_t size) {T maxVal = arr[0];for (size_t i = 1; i < size; ++i) {if (arr[i] > maxVal) {maxVal = arr[i];}}return maxVal;
}// (2) 特化版本:比较字符串长度(const char* 类型)
template<>
const char *maxElement<const char *>(const char **arr, size_t size) {const char *maxStr = arr[0];for (size_t i = 1; i < size; ++i) {// 比较字符串长度if (strlen(arr[i]) > strlen(maxStr)) {maxStr = arr[i];}}return maxStr;
}// 测试代码
int main() {// 测试通用版本(int 类型)int nums[3] = {5, 9, 2};cout << "通用版本输出:" << maxElement(nums, 3) << endl; // 输出 9// 测试特化版本(const char* 类型)const char *strs[3] = {"apple", "banana", "cherry"};cout << "特化版本输出:" << maxElement(strs, 3) << endl; // 输出 "banana"return 0;
}
(3) 解释以下代码的输出原因:
- int nums[] 输出 9 的原因:
- 调用通用版本 maxElement<int>,逻辑是比较值的大小。
- 遍历数组 {5, 9, 2},依次比较得到 9 是最大值,因此输出 9。
- const char *strs[] 输出 banana 的原因:
- 调用特化版本 maxElement<const char*>,逻辑是比较字符串长度。
- 遍历数组:"apple" 长度为 5 → 初始 maxStr = "apple"
- "banana" 长度为 6 → 替换 maxStr = "banana"
- "cherry" 长度为 6 → 与当前 maxStr 长度相同,不替换
- 最终最长字符串是 "banana",因此输出 banana。
关键说明
- 通用模板:通过 > 运算符比较值,支持所有定义了 > 的类型(如 int、double 等)。
- 特化模板:对 const char* 单独处理,改为比较字符串长度(通过 strlen),避免直接比较指针地址(无意义)。
- 输出差异:通用版本比较 “值大小”,特化版本比较 “字符串长度”,因此结果不同。
以下情况用 size_t:
- 数组长度:int arr[10]; size_t len = sizeof(arr)/sizeof(arr[0]);
- 容器大小:vector<int> v; size_t s = v.size();
- 循环计数:for (size_t i=0; i<v.size(); ++i)
- 内存操作:malloc(1024); size_t bytes = 1024;
核心:只要是 非负的长度、大小、数量,可用 size_t。
12题
实现以下函数,要求安全删除容器中不满足条件的元素:
#include <iostream>
#include <vector>
#include <map>
#include <string>
using namespace std; // 任务1:删除vector中的负数
void remove1(vector<int> &nums) { // 补全代码:删除nums中所有负数元素 // 要求:使用迭代器遍历并删除
} // 任务2:删除map中值小于N的元素
void remove2(map<string, int> &scores, int N) { // 补全代码:删除scores中值小于N的元素 // 要求:使用迭代器遍历并删除
} int main() { // 测试vector删除 vector<int> data = {7, -2, 4, -5, 0, 9}; remove1(data); // data应变为 [7,4,0,9] // 测试map删除 map<string, int> student_scores = { {"Alice",95}, {"Bob",58}, {"Charlie",82}, {"Dave",45} }; remove2(student_scores, 60); // student_scores应保留 ({"Alice",95}, {"Charlie",82})
}
解答
#include <iostream>
#include <vector>
#include <map>
#include <string>
using namespace std; // 任务1:删除vector中的负数
void remove1(vector<int> &nums) { // 遍历迭代器,删除负数 for (auto it = nums.begin(); it != nums.end(); ) { if (*it < 0) { // 删除元素后,it 自动指向下一个有效元素 it = nums.erase(it); //删除当前元素,并通过返回值获取下一个有效位置。//更新迭代器,避免因删除导致的迭代器失效问题。} else { // 不删除,移动到下一个元素 ++it; } }
} // 任务2:删除map中值小于N的元素
void remove2(map<string, int> &scores, int N) { // 遍历迭代器,删除值 < N 的元素 for (auto it = scores.begin(); it != scores.end(); ) { if (it->second < N) { // 删除元素后,it 自动指向下一个有效元素 it = scores.erase(it); } else { // 不删除,移动到下一个元素 ++it; } }
} int main() { // 测试vector删除 vector<int> data = {7, -2, 4, -5, 0, 9}; remove1(data); // 输出结果 for (int num : data) { cout << num << " "; } cout << endl; // 测试map删除 map<string, int> student_scores = { {"Alice",95}, {"Bob",58}, {"Charlie",82}, {"Dave",45} }; remove2(student_scores, 60); // 输出结果 for (auto &pair : student_scores) { cout << pair.first << ": " << pair.second << endl; } return 0;
}
13题
实现一个函数,将链表按每 k 个节点一组进行反转,并返回反转后的链表头节点。若剩余节点不足 k 个,则保持原序。
示例:
- 输入链表 1->2->3->4->5,k = 2,输出 2->1->4->3->5。
- 输入链表 1->2->3->4->5,k = 3,输出 3->2->1->4->5。
struct Node {int val;Node* next;Node(int x) : val(x), next(nullptr) {}
};
解答(完整代码)
#include <iostream>
using namespace std;struct Node {int val;Node* next;Node(int x) : val(x), next(nullptr) {}
};Node* reverse(Node* head, int k) {// 递归终止条件:链表为空或剩余节点不足 k 个 if (head == nullptr) return nullptr;// 1. 检查剩余节点是否有 k 个 Node* cur = head;int count = 0;while (cur != nullptr && count < k) {cur = cur->next;count++;}if (count < k) return head; // 不足 k 个,保持原序 // 2. 反转当前 k 个节点(迭代法) cur = head;Node* pre = nullptr, * next = nullptr;count = 0;while (count < k) {next = cur->next; // 保存下一个节点 1 2 3 4 5cur->next = pre; // 反转指针 pre = cur; // 移动 pre cur = next; // 移动 cur count++;}// 3. 递归处理剩余链表,并连接到当前反转后的尾部(原 head) head->next = reverse(cur, k);// 4. 返回新的头节点(原 k 个节点的尾节点) return pre;
}// 辅助函数:打印链表
void printList(Node* head) {while (head != nullptr) {cout << head->val << "->";head = head->next;}cout << "nullptr" << endl;
}int main() {// 测试用例 1:k = 2 Node* head1 = new Node(1);head1->next = new Node(2);head1->next->next = new Node(3);head1->next->next->next = new Node(4);head1->next->next->next->next = new Node(5);cout << "原链表 1: ";printList(head1);Node* newHead1 = reverse(head1, 2);cout << "反转后 (k=2): ";printList(newHead1);// 测试用例 2:k = 3 Node* head2 = new Node(1);head2->next = new Node(2);head2->next->next = new Node(3);head2->next->next->next = new Node(4);head2->next->next->next->next = new Node(5);cout << "\n原链表 2: ";printList(head2);Node* newHead2 = reverse(head2, 3);cout << "反转后 (k=3): ";printList(newHead2);return 0;
}