当前位置: 首页 > news >正文

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 等)。

函数返回了局部对象 resultc_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) 插入单个元素为例):

  1. 检查容量:判断当前 vector 的容量(capacity)是否能容纳新元素。若剩余空间不足(size == capacity),则触发扩容。
  2. 扩容逻辑(若需扩容):分配新内存(通常是原容量的 1.5 或 2 倍,如原容量 n → 新容量 2n)。
    1. 将原数组中 [begin(), it) 区间的元素拷贝 / 移动到新内存的起始位置。
    2. 释放原内存,更新 vector 的数据指针指向新内存。
  3. 元素后移:将插入位置 it 及之后的元素(从原数据或新内存中)向后移动 1 个位置,腾出插入空间。
  4. 插入新元素:在腾好的位置构造新元素(调用拷贝 / 移动构造函数)。
  5. 更新状态: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 为玩家复杂数据的结构体 / 类)

理由:

  1. 快速查找:unordered_map 基于哈希表,按玩家 ID(唯一整数)查找的时间复杂度为 O(1),满足 “500 + 次 / 秒” 的快速查找需求。
  2. 遍历效率:需高频遍历所有玩家(1000 + 次 / 秒),unordered_map 的遍历虽然不如 vector 连续内存高效,但结合哈希表的缓存友好性(节点分散但查询快),能平衡 “查找” 和 “遍历” 的需求;若用 vector 需额外维护 ID 索引,增删时效率更低(需移动元素)。

场景2回答

选择容器:deque(双端队列) + 多线程安全封装(如加锁或用无锁队列)

理由:

  1. FIFO 与内存效率:deque 支持首尾高效操作(入队 / 出队均为 O(1) amortized),且内存分配更紧凑(分段连续),比 list 更节省内存、减少碎片。
  2. 并发适配: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>

理由:

  1. 有序导出:map 自动维护键的有序性,每 5 分钟导出时直接遍历即为字母顺序,无需额外排序开销。
  2. 读写平衡: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 的元数据)。
  • 堆内存:
    • localVec.push_back(i) 时,vector 会在堆上动态分配内存存储 int 元素:初始可能分配小容量(如 capacity=1),扩容时(超过当前容量)会重新分配更大的堆内存(如 capacity=2→4→... ),并拷贝旧数据到新堆空间。
      • 最终 localVec 的堆内存存储 [0,1,2]。

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] 的空间)。

关键总结

  1. 栈 vs 堆:栈存 vector 的元数据(指针、大小、容量),堆存实际元素数据。
  2. C++11 优化:拷贝构造默认深拷贝,但返回值优化(RVO/NRVO)可跳过额外拷贝
  3. 释放逻辑:栈内存随函数栈帧销毁自动释放,堆内存由 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) 解释以下代码的输出原因:

  1. int nums[] 输出 9 的原因:
    1. 调用通用版本 maxElement<int>,逻辑是比较值的大小。
    2. 遍历数组 {5, 9, 2},依次比较得到 9 是最大值,因此输出 9。
  2. const char *strs[] 输出 banana 的原因:
    1. 调用特化版本 maxElement<const char*>,逻辑是比较字符串长度。
    2. 遍历数组:"apple" 长度为 5 → 初始 maxStr = "apple"
      1. "banana" 长度为 6 → 替换 maxStr = "banana"
      2. "cherry" 长度为 6 → 与当前 maxStr 长度相同,不替换
    3. 最终最长字符串是 "banana",因此输出 banana。

关键说明

  • 通用模板:通过 > 运算符比较值,支持所有定义了 > 的类型(如 int、double 等)。
  • 特化模板:对 const char* 单独处理,改为比较字符串长度(通过 strlen),避免直接比较指针地址(无意义)。
  • 输出差异:通用版本比较 “值大小”,特化版本比较 “字符串长度”,因此结果不同。

以下情况用 size_t

  1. 数组长度:int arr[10]; size_t len = sizeof(arr)/sizeof(arr[0]);
  2. 容器大小:vector<int> v; size_t s = v.size();
  3. 循环计数:for (size_t i=0; i<v.size(); ++i)
  4. 内存操作: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;
}

http://www.dtcms.com/a/272417.html

相关文章:

  • 如何看待java开发和AI的关系?
  • GO启动一个视频下载接口 前端可以边下边放
  • 【PyTorch】PyTorch中的数据预处理操作
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | DoubleVerticalSlider(双垂直滑块)
  • 图解LeetCode:79递归实现单词搜索
  • Django+DRF 实战:自定义异常处理流程
  • 20.4 量子安全加密算法
  • 案例分享--福建洋柄水库大桥智慧桥梁安全监测(二)之数字孪生和系统平台
  • 机器学习13——支持向量机下
  • TCP传输控制层协议深入理解
  • 当CCLinkIE撞上Modbus TCP:照明控制系统的“方言战争”终结术
  • VIP可读
  • 线性回归与正则化
  • Django专家成长路线知识点——AI教你学Django
  • 【PTA数据结构 | C语言版】顺序栈的3个操作
  • 【深度学习系列--经典论文解读】Gradient-Based Learning Applied to Document Recognition
  • LINUX710 MYSQL
  • linux-用户与用户组管理
  • serialVersionUID
  • 配置 msvsmon.exe 以无身份验证启动
  • 力扣打卡第23天 二叉搜索树中的众数
  • 算法题(171):组合型枚举
  • Shusen Wang推荐系统学习 --召回 矩阵补充 双塔模型
  • 深度探索:实时交互与增强现实翻译技术(第六篇)
  • Win10用camke+gcc编译opencv库报错error: ‘_hypot‘ has not been declared in ‘std‘
  • 什么是 领域偏好学习(DPO)与多目标强化学习(PPO)
  • 在 Ubuntu 22 部署 vLLM + Qwen3 32B 模型
  • EPLAN 电气制图(六):电机正反转副勾主电路绘制
  • STM32第十九天 ESP8266-01S和电脑实现串口通信(2)
  • 代理模式——Java