C++ 面试重点复习:类型转换、并发、Linux、Git 与回溯
本文先介绍 C++ 的四种命名类型转换,再补充并发、Linux 运维排查、Git 协作、回溯算法和 const 对象等面试高频问题。回答时不仅要说出名词,还要说明使用边界、失败表现和实际排查步骤。
一、四种 cast 的快速对比
| 转换方式 | 主要用途 | 是否运行时检查 | 典型风险 |
|---|---|---|---|
static_cast |
相关类型的编译期转换、数值转换、显式调用转换构造函数 | 否 | 向下转型时实际类型不匹配会产生未定义行为 |
dynamic_cast |
多态继承体系中的安全向下转型、交叉转型 | 是 | 需要多态源类型;失败时返回空指针或抛异常 |
const_cast |
添加或移除 const/volatile 属性 |
否 | 修改原本定义为 const 的对象会产生未定义行为 |
reinterpret_cast |
低级别地重新解释指针、整数或函数指针 | 否 | 对齐、生命周期、严格别名和可移植性问题 |
| C 风格转换 | 兼容旧代码的综合转换 | 视实际转换而定 | 规则不透明,可能隐藏危险转换 |
优先使用能够表达意图的命名 cast。看到 reinterpret_cast 或 const_cast 时,应额外检查其使用是否有充分理由。
二、static_cast
static_cast 主要在编译期完成检查,适用于编译器能够根据静态类型判断的转换。
1. 数值类型转换
double price = 19.99;
int value = static_cast<int>(price); // value 为 19,小数部分被截断
数值转换可能丢失精度或发生溢出,因此“能够编译”不等于“数值一定正确”。
int count = 100;
std::size_t size = static_cast<std::size_t>(count);
从有符号类型转换到无符号类型时,负数会按照无符号规则转换,必须确认输入范围。
2. 派生类向基类转换
class Base {};
class Derived : public Base {};
Derived derived;
Base* base = static_cast<Base*>(&derived); // 向上转型,安全
3. 基类向派生类转换
Base* base = /* 实际指向 Derived 对象,或其他对象 */;
Derived* derived = static_cast<Derived*>(base);
这种转换只根据静态类型编译,不检查 base 实际指向的对象是否真的是 Derived。如果实际对象不是 Derived,后续通过 derived 访问派生类成员可能产生未定义行为。
因此,多态场景下通常应该使用 dynamic_cast。
4. 枚举和整数转换
enum class State { Ready = 0, Running = 1 };
int raw = 1;
State state = static_cast<State>(raw);
转换不会自动验证 raw 是否对应有效枚举值,必要时应先自行检查范围。
5. 显式调用转换构造函数或转换运算符
class UserId {
public:
explicit UserId(int value) : value_(value) {}
private:
int value_;
};
UserId id = static_cast<UserId>(42);
面试表达:
static_cast主要进行编译期可确定的转换,不做运行时多态检查。数值转换和向下转型虽然可能编译成功,但仍然需要调用者保证范围和实际类型正确。
三、dynamic_cast
dynamic_cast 用于带继承关系的类型之间进行运行时安全转换,典型场景是多态基类指针向派生类指针的向下转型,或者两个兄弟派生类之间的交叉转型。
class Base {
public:
virtual ~Base() = default; // 使 Base 成为多态类型
};
class Cat : public Base {};
class Dog : public Base {};
Base* object = new Cat;
Cat* cat = dynamic_cast<Cat*>(object); // 成功
Dog* dog = dynamic_cast<Dog*>(object); // 失败,得到 nullptr
delete object;
1. 虚函数要求
进行运行时类型识别时,源类型必须是多态类型,也就是至少包含一个虚函数:
class NonPolymorphicBase {};
class Child : public NonPolymorphicBase {};
NonPolymorphicBase* base = nullptr;
// dynamic_cast<Child*>(base); // 编译错误:源类型不是多态类型
常见做法是在基类中声明虚析构函数:
virtual ~Base() = default;
需要注意,普通的派生类向基类的向上转换是静态可确定的,不依赖运行时类型识别;“所有 dynamic_cast 都要求虚函数”是过于绝对的说法。真正需要多态源类型的是向下转型、交叉转型等运行时检查场景。
2. 指针和引用的失败表现
Base* base = new Cat;
if (Dog* dog = dynamic_cast<Dog*>(base)) {
// 转换成功
}
try {
Dog& dog = dynamic_cast<Dog&>(*base);
} catch (const std::bad_cast&) {
// 引用转换失败
}
delete base;
规则是:
- 指针转换失败,返回
nullptr; - 引用转换失败,抛出
std::bad_cast; - 转换成功后,结果指向同一个对象的对应子对象。
面试表达:
dynamic_cast在多态继承体系中进行运行时类型检查。向下转型和交叉转型要求源类型具有虚函数。指针失败返回nullptr,引用失败抛出std::bad_cast。它比static_cast安全,但有运行时开销,也可能说明设计过度依赖类型判断。
四、const_cast
const_cast 只能用于调整类型的 const 或 volatile 属性,不能把一个整数直接转换成指针,也不能改变对象真正的存储类型。
void print(char* text) {
// 旧接口错误地要求 char*,但函数实际只读数据
}
const char* message = "hello";
print(const_cast<char*>(message));
上面的调用只有在 print 确实不会修改字符串时才可能安全。更好的设计是修改接口:
void print(const char* text);
关键区别:原对象是否本来就是 const
通过 const_cast 得到非 const 指针并不一定立刻产生未定义行为。关键看原对象的定义:
int value = 10;
const int* readOnlyView = &value;
int* writableView = const_cast<int*>(readOnlyView);
*writableView = 20; // 合法,因为原对象 value 本身不是 const
但如果对象从定义开始就是 const:
const int value = 10;
int* writableView = const_cast<int*>(&value);
*writableView = 20; // 未定义行为
面试表达:
const_cast只调整const或volatile属性。移除类型上的const不代表可以安全修改对象;如果对象本身定义为const,通过转换后的指针修改它是未定义行为。优先修正被调用函数的参数类型,而不是滥用const_cast。
五、reinterpret_cast
reinterpret_cast 用于低级别地重新解释一个值的类型,常见于系统编程、内存映射、硬件接口和与 C API 对接。
std::uintptr_t address = 0x1000;
int* pointer = reinterpret_cast<int*>(address);
它通常不执行运行时类型检查,也不保证目标地址满足:
- 正确的内存对齐;
- 对象生命周期已经开始;
- 目标类型允许通过该表达式访问;
- 指针在当前平台上具有可移植的表示。
因此,“转换成功”与“解引用安全”是两回事。
1. 指针类型重新解释
int value = 42;
unsigned char* bytes = reinterpret_cast<unsigned char*>(&value);
通过字符类型访问对象的底层字节通常有特殊规则,但字节顺序、int 大小和表示方式都可能随平台变化。
2. 不要把它等同于按位拷贝
reinterpret_cast 主要改变表达式的解释方式,并不等于把源对象的比特位复制到一个新对象。
如果目标是安全地复制位模式,应使用 C++20 的 std::bit_cast:
#include <bit>
#include <cstdint>
float number = 1.0f;
std::uint32_t bits = std::bit_cast<std::uint32_t>(number);
std::bit_cast 要求两种类型大小相同,并且满足可平凡复制等约束;它表达的是“按位复制”,比指针重解释更清晰。
3. 严格别名和生命周期问题
double value = 3.14;
int* pointer = reinterpret_cast<int*>(&value);
// int result = *pointer; // 不应这样通过不相关类型访问对象
这种写法可能违反严格别名规则,导致未定义行为。对于对象表示的复制,使用 std::bit_cast、std::memcpy 或正确的序列化方案。
面试表达:
reinterpret_cast是低级别的类型重新解释,不做运行时安全检查。它不是通用的按位拷贝工具,使用时必须确认对齐、对象生命周期、严格别名和平台表示规则,否则可能产生未定义行为。
六、C 风格转换
C 风格转换写法如下:
double value = 3.14;
int result = (int)value;
在 C++ 中,C 风格转换可能尝试执行多种转换,包括类似 const_cast、static_cast、reinterpret_cast,甚至继承体系中的相关转换。具体规则不直观,不利于代码审查。
建议改成:
int result = static_cast<int>(value);
这样读代码的人可以直接知道作者的意图,也更容易由编译器和工具发现问题。
七、如何选择
可以按照下面的顺序判断:
- 只是数值、枚举或明确的相关类型转换:优先
static_cast。 - 需要在多态继承体系中确认实际对象类型:使用
dynamic_cast。 - 只是适配旧接口的
const参数:谨慎使用const_cast,优先修改接口。 - 需要访问底层地址或平台相关表示:才考虑
reinterpret_cast。 - 不要在新代码中使用无法表达意图的 C 风格转换。
八、面试高频陷阱
陷阱 1:static_cast 向下转型一定安全吗?
不安全。它不检查实际动态类型,只有调用者能够证明对象确实是目标派生类时才可以使用。
陷阱 2:dynamic_cast 一定需要虚函数吗?
运行时向下转型和交叉转型需要源类型是多态类型;普通的向上转换不应简单概括为“必须有虚函数”。
陷阱 3:const_cast 去掉 const 后就能修改对象吗?
不一定。如果对象本身定义为 const,修改它是未定义行为。
陷阱 4:reinterpret_cast 就是复制二进制位吗?
不是。需要按位复制时使用 std::bit_cast 或 memcpy,而不是先把指针转成另一个类型再解引用。
陷阱 5:四种 cast 只是写法不同吗?
不是。它们分别对应编译期转换、运行时多态检查、限定符调整和低级别表示重解释,安全性和适用边界完全不同。
九、最终记忆版
static_cast 编译期、常规转换、不做运行时类型检查
dynamic_cast 多态体系、运行时检查、失败可识别
const_cast 调整 const/volatile,不能改变原对象的本质
reinterpret_cast 低级重解释,风险最高,不等于按位拷贝
C 风格转换 规则混杂,新代码尽量避免
写 C++ 转换代码时,最重要的不是“能不能编译”,而是明确回答两个问题:转换后的对象是否真的具有目标类型,以及是否满足对齐、生命周期、别名和可修改性要求。
十、signal 与 semaphore:信号和信号量不是一回事
1. signal(信号)
信号是操作系统发送给进程或线程的异步事件通知。例如:
| 信号 | 含义 |
|---|---|
SIGINT |
终端按下 Ctrl+C |
SIGTERM |
请求进程正常退出 |
SIGSEGV |
非法内存访问 |
SIGKILL |
强制终止,不能捕获或忽略 |
进程可以使用默认处理方式、忽略信号,或者通过处理函数响应信号:
#include <csignal>
void onSignal(int signo) {
// 这里只做异步信号安全的操作
}
int main() {
std::signal(SIGINT, onSignal);
}
信号处理函数运行在异步时机,不能随意调用 malloc、new、普通锁或复杂的非线程安全库函数。实际项目中通常只在处理函数里设置一个原子标志,再由主循环完成真正的清理工作。
2. semaphore(信号量)
信号量是线程或进程之间的同步原语,本质上是一个受保护的计数器:
wait、P、down:计数减一,资源不足时阻塞;post、V、up:计数加一,必要时唤醒等待者。
#include <semaphore>
std::counting_semaphore<3> slots(3);
void work() {
slots.acquire();
// 最多三个线程同时使用资源
slots.release();
}
面试回答可以概括为:
signal 是操作系统的异步事件通知;semaphore 是用于同步和资源计数的并发工具。signal 关注“发生了什么事件”,semaphore 关注“有多少资源或某个事件是否已经发生”。
十一、条件变量为什么必须配合互斥锁
条件变量本身不保存业务条件,它只负责阻塞和唤醒。业务条件由共享状态表示,互斥锁负责保护这个状态。
#include <condition_variable>
#include <mutex>
std::mutex mutex;
std::condition_variable cv;
bool ready = false;
void consumer() {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [] {
return ready;
});
// 获得锁且 ready 为 true
}
void producer() {
{
std::lock_guard<std::mutex> lock(mutex);
ready = true;
}
cv.notify_one();
}
wait 会原子地释放锁并进入等待,被唤醒后再重新加锁。这避免了“检查条件后、真正睡眠前恰好错过通知”的竞态。
条件变量允许虚假唤醒,所以必须循环检查条件,不能只写一次 if:
while (!ready) {
cv.wait(lock);
}
带谓词的 cv.wait(lock, predicate) 已经封装了这个循环。互斥锁保护的是 ready 等共享状态,条件变量不负责解决数据竞争。
十二、Linux 线程 CPU 占用高的排查
先找到进程:
top
ps aux --sort=-%cpu | head
假设进程 PID 是 1234,查看进程内各线程:
top -H -p 1234
ps -L -p 1234 -o pid,tid,psr,pcpu,stat,comm
pidstat -t -p 1234 1
top -H 将线程展开显示,先记录 CPU 最高线程的 TID,再用 perf 看热点:
sudo perf top -p 1234
sudo perf record -F 99 -g -p 1234 -- sleep 30
sudo perf report
排查思路:
- 线程持续占满 CPU,先看
perf report中占比最高的函数。 - 若热点是业务计算,检查死循环、重复计算和算法复杂度。
- 若热点是锁或自旋函数,检查锁竞争和忙等。
- 若热点是系统调用,检查频繁 I/O、网络调用或上下文切换。
- 没有符号时安装调试符号,或使用
-g编译;为了获得更可靠的调用栈,可使用-fno-omit-frame-pointer。
十三、Linux 网络流量排查
nethogs 主要按进程统计实时流量:
sudo nethogs
sudo nethogs eth0
如果问题是“哪个远端 IP 或连接流量最大”,使用 iftop:
sudo iftop -nP -i eth0
查看套接字、端口和所属进程:
ss -tunap
常见工具的定位如下:
nethogs:按进程查看流量;iftop:按连接、IP 和端口查看流量;ss:查看 TCP/UDP 连接状态;sar -n DEV 1:查看网卡整体吞吐量;tcpdump:抓包进行协议级分析。
因此,面试中不要说“nethogs 按 IP 统计流量”;更准确的说法是“nethogs 按进程统计,iftop 按连接/IP 统计”。
十四、Git:本地脏工作区同步远端代码
推荐先确认工作区,再暂存本地修改:
git status
git stash push -u -m "before pulling remote changes"
git pull --rebase
git stash pop
git status
-u 会同时保存未跟踪文件;如果只想保存已跟踪文件,可以省略它。
发生冲突时
冲突文件会包含类似标记:
<<<<<<< Updated upstream
远端内容
=======
本地 stash 内容
>>>>>>> Stashed changes
手工选择并整理内容后:
git add <冲突文件>
如果冲突发生在 git pull --rebase 阶段,继续变基:
git rebase --continue
如果冲突发生在 git stash pop 阶段,解决并 git add 后,把恢复后的修改纳入后续提交。stash pop 发生冲突时,Git 通常不会删除原 stash,可以用下面命令确认:
git stash list
更稳妥的恢复方式是先使用 git stash apply,确认无误后再执行 git stash drop。完整流程的核心顺序是:
git stash→ 同步远端代码 →git stash pop/apply→ 解决冲突 → 检查并提交。
十五、回溯算法:组合总和、剪枝与去重
以“每个候选数字可以重复使用”为例:
#include <algorithm>
#include <vector>
class Solution {
public:
std::vector<std::vector<int>> combinationSum(
std::vector<int>& candidates,
int target) {
std::sort(candidates.begin(), candidates.end());
dfs(candidates, target, 0);
return result;
}
private:
std::vector<std::vector<int>> result;
std::vector<int> path;
void dfs(const std::vector<int>& candidates,
int remaining,
int start) {
if (remaining == 0) {
result.push_back(path);
return;
}
for (int i = start; i < static_cast<int>(candidates.size()); ++i) {
// 同一树层跳过重复值,避免得到重复组合
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
// 已排序,后面的数也不可能满足条件
if (candidates[i] > remaining) {
break;
}
path.push_back(candidates[i]);
// 传 i:当前数字下一层仍然可以继续使用
dfs(candidates, remaining - candidates[i], i);
path.pop_back();
}
}
};
1. start 如何避免排列重复
假设目标是 8,组合 [2, 3, 3] 只应该出现一次。选择 2 后,下一层仍然从 2 及其后面选择;一旦选择了 3,就不能回头选择 2。这样不会生成 [3, 2, 3] 或 [3, 3, 2]。
因此:
start限制搜索方向,去掉同一组合的不同排列;i > start && candidates[i] == candidates[i - 1]跳过同一树层的重复候选值;path.pop_back()撤销当前选择,恢复现场。
2. 剪枝为什么成立
先排序后,如果 candidates[i] > remaining,那么 i 后面的候选值只会更大,继续搜索不可能得到合法解,可以直接 break。
3. 每个数字只能使用一次
如果题目要求每个元素最多使用一次,下一层不能再次使用当前下标,应改为:
dfs(candidates, remaining - candidates[i], i + 1);
这正是组合总和 I 和组合总和 II 中最容易混淆的区别:允许重复时传 i,不允许重复时传 i + 1。
十六、const 对象与 this 指针
普通成员函数:
class User {
public:
void setId(int value);
};
可以近似理解为:
void setId(User* const this, int value);
this 指针本身不能改为指向另一个对象,但可以通过它修改当前对象。
const 成员函数:
class User {
public:
int id() const;
};
可以近似理解为:
int id(const User* const this);
所以:
const User user;
user.id(); // 正确
user.setId(1); // 编译错误
本质是 const User 对象只能把自身作为 const User* 传给成员函数,而普通成员函数需要 User*,这会丢失 const 限定,编译器会拒绝。
const 成员函数不能修改普通成员,但可以修改 mutable 成员:
class Cache {
public:
int value() const {
++accessCount;
return data;
}
private:
int data = 0;
mutable int accessCount = 0;
};
面试回答:
const成员函数的隐式this指针可近似看作const T* const,因此不能通过它修改普通数据成员。const对象不能调用普通成员函数,本质是因为const T*不能转换成要求修改权限的T*。
十七、最终复习清单
signal 操作系统异步事件通知
semaphore 并发同步和资源计数
condition_variable 配合 mutex 保护条件,并用谓词防止虚假唤醒
top -H 展开查看进程内线程
nethogs 按进程统计网络流量
iftop 按连接/IP/端口观察流量
perf 采样 CPU 热点和调用栈
git stash 保存本地修改
git pull 同步远端代码
git stash pop 恢复本地修改并处理冲突
回溯 start 不回头遍历,避免排列重复
回溯剪枝 排序后超过 remaining 直接 break
const this const 成员函数中的类型近似为 const T* const
评论
评论由 GitHub Discussions 托管。