C++ 基础常见面试题总结(下)
本文采用面试问答形式,系统梳理 C++ 模板、STL、现代特性、多线程的核心知识。⭐ 标记为高频重点题。
一、模板与泛型编程
⭐ 什么是模板?函数模板和类模板的区别?
模板是 C++ 的泛型编程机制,允许编写类型无关的代码,在编译期生成具体类型的代码。
// 函数模板
template <typename T>
T Max(T a, T b) {
return (a > b) ? a : b;
}
Max(3, 5); // 编译器自动推导 T = int
Max(3.14, 2.72); // T = double
Max<int>(3, 5); // 显式指定
// 类模板
template <typename T>
class Stack {
std::vector<T> data_;
public:
void Push(const T& val) { data_.push_back(val); }
T Pop() { T val = data_.back(); data_.pop_back(); return val; }
bool Empty() const { return data_.empty(); }
};
Stack<int> intStack; // 必须显式指定类型
Stack<std::string> strStack;
| 特性 | 函数模板 | 类模板 |
|---|---|---|
| 类型推导 | ✅ 可以自动推导 | ❌ C++17 前必须显式指定 |
| 特化 | 支持全特化 | 支持全特化和偏特化 |
| 默认参数 | C++11 起支持 | 一直支持 |
⭐ 模板的编译原理?为什么模板通常要写在头文件中?
模板在编译时进行实例化(instantiation),编译器看到具体类型调用时,才生成对应类型的代码:
模板定义(template<typename T> T Max(T,T))
↓
调用 Max(3, 5) → 编译器生成 int Max(int, int)
调用 Max(1.0, 2.0) → 编译器生成 double Max(double, double)
为什么要写在头文件中?
- 编译器在每个编译单元(.cpp 文件)独立编译
- 如果模板定义在
.cpp中,其他.cpp文件看不到模板的实现 - 无法实例化 → 链接错误
// ✅ 正确:模板定义在头文件
// stack.h
template <typename T>
class Stack {
public:
void Push(const T& val) { /* 实现 */ }
};
// ❌ 错误:模板实现在 .cpp 中
// stack.h
template <typename T>
class Stack {
void Push(const T& val);
};
// stack.cpp
template <typename T>
void Stack<T>::Push(const T& val) { /* 实现 */ }
// 其他 .cpp 文件 #include "stack.h" 时无法看到实现 → 链接错误
⭐ 什么是模板特化(Template Specialization)?
为特定类型提供不同的实现:
// 通用版本
template <typename T>
class Printer {
public:
void Print(const T& val) {
std::cout << val << "\n";
}
};
// 全特化(Full Specialization):针对 bool 类型
template <>
class Printer<bool> {
public:
void Print(const bool& val) {
std::cout << (val ? "true" : "false") << "\n";
}
};
// 偏特化(Partial Specialization):针对指针类型
template <typename T>
class Printer<T*> {
public:
void Print(T* val) {
if (val) std::cout << *val << "\n";
else std::cout << "nullptr\n";
}
};
Printer<int> p1; // 通用版本
Printer<bool> p2; // 全特化版本
Printer<int*> p3; // 偏特化版本
什么是可变参数模板(Variadic Templates)?
C++11 引入的可变参数模板允许接受任意数量和类型的参数:
// 递归终止条件
void Print() {
std::cout << "\n";
}
// 可变参数模板
template <typename T, typename... Args>
void Print(const T& first, const Args&... rest) {
std::cout << first << " ";
Print(rest...); // 递归展开
}
Print(1, "hello", 3.14, true);
// 输出: 1 hello 3.14 1
// C++17 折叠表达式(更简洁)
template <typename... Args>
void Print17(const Args&... args) {
((std::cout << args << " "), ...);
std::cout << "\n";
}
⭐ SFINAE 是什么?
SFINAE(Substitution Failure Is Not An Error):模板参数替换失败不是错误,编译器会尝试其他重载。
#include <type_traits>
// 仅当 T 是整数类型时启用
template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
SafeDiv(T a, T b) {
return b != 0 ? a / b : 0;
}
// 仅当 T 是浮点类型时启用
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
SafeDiv(T a, T b) {
return b != 0.0 ? a / b : 0.0;
}
SafeDiv(10, 3); // 调用整数版本
SafeDiv(10.0, 3.0); // 调用浮点版本
// C++20 更简洁的写法(Concepts)
template <std::integral T>
T SafeDiv20(T a, T b) { return b != 0 ? a / b : 0; }
二、STL 容器
⭐ STL 的主要组件有哪些?
| 组件 | 说明 | 示例 |
|---|---|---|
| 容器 | 存储数据 | vector、map、set |
| 迭代器 | 统一的遍历接口 | begin()、end() |
| 算法 | 操作数据 | sort、find、transform |
| 仿函数 | 可调用对象 | std::less、std::greater |
| 适配器 | 接口封装 | stack、queue、priority_queue |
| 分配器 | 内存管理 | std::allocator |
⭐ 各容器的底层数据结构和时间复杂度?
| 容器 | 底层结构 | 随机访问 | 插入/删除(头) | 插入/删除(尾) | 查找 |
|---|---|---|---|---|---|
vector | 动态数组 | O(1) | O(n) | 均摊 O(1) | O(n) |
deque | 分段数组 | O(1) | O(1) | O(1) | O(n) |
list | 双向链表 | O(n) | O(1) | O(1) | O(n) |
forward_list | 单向链表 | O(n) | O(1) | O(n) | O(n) |
set / map | 红黑树 | — | — | — | O(log n) |
unordered_set / unordered_map | 哈希表 | — | — | — | 均摊 O(1) |
array | 固定数组 | O(1) | — | — | O(n) |
⭐ vector 的扩容机制是什么?
当 vector 的 size() 达到 capacity() 时,会自动扩容:
- 分配一块更大的内存(通常是当前容量的 2 倍,GCC 实现;MSVC 是 1.5 倍)
- 将旧元素移动/拷贝到新内存
- 释放旧内存
std::vector<int> vec;
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
std::cout << "size=" << vec.size()
<< " capacity=" << vec.capacity() << "\n";
}
// 典型输出(GCC):
// size=1 capacity=1
// size=2 capacity=2
// size=3 capacity=4
// size=5 capacity=8
// size=9 capacity=16
优化方法:
// 方法1:预分配容量
std::vector<int> vec;
vec.reserve(1000); // 预分配,避免多次扩容
// 方法2:初始化时指定大小
std::vector<int> vec(1000, 0);
// 释放多余内存
vec.shrink_to_fit(); // 请求将 capacity 缩减到 size
⚠️ 扩容会导致所有迭代器、指针、引用失效!
⭐ vector 和 list 如何选择?
| 特性 | vector | list |
|---|---|---|
| 内存布局 | 连续(缓存友好) | 非连续(节点分散) |
| 随机访问 | ✅ O(1) | ❌ O(n) |
| 头部插入/删除 | ❌ O(n) | ✅ O(1) |
| 尾部插入/删除 | ✅ 均摊 O(1) | ✅ O(1) |
| 中间插入/删除 | ❌ O(n) | ✅ O(1)(已知位置) |
| 迭代器失效 | 插入/扩容时全部失效 | 仅删除的节点失效 |
| 内存开销 | 小 | 大(每个节点额外两个指针) |
🌈 经验法则:绝大多数情况下优先使用
vector。即使需要中间插入,vector的缓存友好性往往使其性能优于list。
⭐ map 和 unordered_map 的区别?
| 特性 | map | unordered_map |
|---|---|---|
| 底层 | 红黑树 | 哈希表 |
| 有序性 | ✅ 按 key 排序 | ❌ 无序 |
| 查找 | O(log n) | 均摊 O(1) |
| 插入 | O(log n) | 均摊 O(1) |
| key 要求 | 需要 < 运算符 | 需要 hash 函数和 == |
| 最坏情况 | O(log n) | O(n)(哈希冲突严重时) |
| 内存 | 较小 | 较大(哈希表开销) |
// map:有序,适合需要排序遍历的场景
std::map<std::string, int> m;
m["banana"] = 2;
m["apple"] = 1;
for (auto& [k, v] : m) {
// 输出按 key 字典序:apple → banana
}
// unordered_map:无序,适合纯查找的场景
std::unordered_map<std::string, int> um;
um["banana"] = 2;
um["apple"] = 1;
// 遍历顺序不确定
迭代器失效的常见场景?
| 容器 | 触发操作 | 失效范围 |
|---|---|---|
vector | push_back(触发扩容) | 所有迭代器失效 |
vector | insert / erase | 插入/删除点之后的迭代器失效 |
deque | 头/尾插入 | 所有迭代器失效(元素引用有效) |
list | erase | 仅被删除节点的迭代器失效 |
map / set | erase | 仅被删除节点的迭代器失效 |
unordered_map | insert(触发 rehash) | 所有迭代器失效 |
// ❌ 错误:遍历中删除
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it % 2 == 0) {
vec.erase(it); // ❌ it 失效!未定义行为
}
}
// ✅ 正确:erase 返回下一个有效迭代器
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it % 2 == 0) {
it = vec.erase(it); // ✅ 使用返回值
} else {
++it;
}
}
// ✅ 更好的写法(erase-remove idiom)
vec.erase(
std::remove_if(vec.begin(), vec.end(), [](int x) { return x % 2 == 0; }),
vec.end()
);
// ✅ C++20 更简洁
std::erase_if(vec, [](int x) { return x % 2 == 0; });
三、STL 算法
⭐ 常用的 STL 算法有哪些?
#include <algorithm>
#include <numeric>
#include <vector>
std::vector<int> v = {5, 3, 1, 4, 2};
// === 排序相关 ===
std::sort(v.begin(), v.end()); // 升序
std::sort(v.begin(), v.end(), std::greater<>()); // 降序
std::stable_sort(v.begin(), v.end()); // 稳定排序
std::partial_sort(v.begin(), v.begin()+3, v.end()); // 前 3 个有序
std::nth_element(v.begin(), v.begin()+2, v.end()); // 第 3 小的在正确位置
// === 查找相关 ===
auto it = std::find(v.begin(), v.end(), 3); // 线性查找
auto it2 = std::find_if(v.begin(), v.end(), [](int x) { return x > 3; });
bool found = std::binary_search(v.begin(), v.end(), 3); // 二分查找(需排序)
auto lb = std::lower_bound(v.begin(), v.end(), 3); // 下界
auto ub = std::upper_bound(v.begin(), v.end(), 3); // 上界
// === 统计相关 ===
int cnt = std::count(v.begin(), v.end(), 3);
int cnt2 = std::count_if(v.begin(), v.end(), [](int x) { return x > 2; });
auto [minIt, maxIt] = std::minmax_element(v.begin(), v.end());
int sum = std::accumulate(v.begin(), v.end(), 0);
// === 变换相关 ===
std::vector<int> result(v.size());
std::transform(v.begin(), v.end(), result.begin(), [](int x) { return x * 2; });
std::for_each(v.begin(), v.end(), [](int& x) { x += 10; });
// === 修改相关 ===
std::reverse(v.begin(), v.end());
std::rotate(v.begin(), v.begin()+2, v.end());
std::fill(v.begin(), v.end(), 0);
std::replace(v.begin(), v.end(), 0, -1);
// === 去重 ===
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());
// === 集合操作(需排序) ===
std::vector<int> a = {1,2,3,4}, b = {3,4,5,6}, out;
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(), std::back_inserter(out));
// out = {3, 4}
sort 的底层实现是什么?
大多数标准库的 std::sort 使用 IntroSort(内省排序):
| 算法 | 使用场景 |
|---|---|
| 快速排序 | 默认使用,平均 O(n log n) |
| 堆排序 | 递归深度超过阈值时切换,防止最坏 O(n²) |
| 插入排序 | 元素数量很少(≤16)时切换,减少开销 |
- 时间复杂度:O(n log n)
- 空间复杂度:O(log n)(递归栈)
- 不稳定排序(相等元素可能改变相对顺序,需要稳定排序用
stable_sort)
四、现代 C++ 特性
⭐ C++11 最重要的新特性有哪些?
| 特性 | 说明 | 示例 |
|---|---|---|
auto | 自动类型推导 | auto x = 42; |
| 范围 for | 遍历容器 | for (auto& v : vec) {} |
nullptr | 类型安全的空指针 | 替代 NULL |
| 智能指针 | 自动内存管理 | unique_ptr / shared_ptr |
| 右值引用 | 移动语义 | T&& / std::move |
| Lambda | 匿名函数 | [](int x) { return x; } |
constexpr | 编译期常量 | constexpr int N = 10; |
override / final | 虚函数检查 | 防止重写错误 |
| 强类型枚举 | enum class | 不隐式转换为 int |
| 初始化列表 | 统一初始化 | vector<int> v = {1,2,3}; |
std::thread | 线程支持 | 标准多线程库 |
std::function | 函数包装器 | 统一可调用对象 |
static_assert | 编译期断言 | static_assert(sizeof(int)==4); |
⭐ Lambda 表达式的捕获方式有哪些?
int x = 10, y = 20;
// 值捕获(拷贝一份,Lambda 内不能修改)
auto f1 = [x]() { return x; };
// 引用捕获(可修改原变量)
auto f2 = [&x]() { x += 10; };
// 全部值捕获
auto f3 = [=]() { return x + y; };
// 全部引用捕获
auto f4 = [&]() { x += 10; y += 20; };
// 混合捕获
auto f5 = [=, &x]() { x += y; }; // x 引用捕获,y 值捕获
auto f6 = [&, x]() { y += x; }; // x 值捕获,其余引用捕获
// mutable:允许修改值捕获的副本
auto f7 = [x]() mutable { x += 10; return x; };
// 注意:只修改了副本,原始 x 不变
// 初始化捕获(C++14):移动捕获
auto ptr = std::make_unique<int>(42);
auto f8 = [p = std::move(ptr)]() { return *p; };
// 泛型 Lambda(C++14)
auto f9 = [](auto a, auto b) { return a + b; };
⭐ std::optional 的用法?(C++17)
std::optional 表示一个值可能存在也可能不存在,替代"哨兵值"和指针:
#include <optional>
// 返回可选值
std::optional<int> FindUser(const std::string& name) {
if (name == "admin") return 42;
return std::nullopt; // 表示"没有值"
}
auto result = FindUser("admin");
if (result.has_value()) {
std::cout << result.value(); // 42
}
// 或者
if (result) {
std::cout << *result; // 42
}
// 默认值
int id = result.value_or(-1); // 有值返回值,无值返回 -1
std::variant 的用法?(C++17)
std::variant 是类型安全的联合体,可以存储多种类型之一:
#include <variant>
std::variant<int, double, std::string> v;
v = 42; // 存储 int
v = 3.14; // 存储 double
v = "hello"; // 存储 string
// 获取值
std::string s = std::get<std::string>(v); // "hello"
// std::get<int>(v); // ❌ 抛出 std::bad_variant_access
// 安全获取
if (auto* p = std::get_if<int>(&v)) {
std::cout << *p;
}
// 访问者模式
std::visit([](auto&& val) {
std::cout << val << "\n";
}, v);
⭐ string_view 的用法和注意事项?(C++17)
std::string_view 是字符串的轻量级只读视图,不拥有数据,不分配内存:
#include <string_view>
void Process(std::string_view sv) { // 不拷贝,零开销
std::cout << sv.substr(0, 5) << "\n";
std::cout << sv.size() << "\n";
}
Process("Hello, World!"); // 从字面量
std::string s = "Hello";
Process(s); // 从 string
// 子串操作不分配内存
std::string_view sv = "Hello, World!";
auto sub = sv.substr(0, 5); // 只是移动指针,不拷贝
⚠️ 注意:
string_view不拥有数据,必须确保原始字符串的生命周期长于string_view:cppstd::string_view Dangerous() { std::string temp = "hello"; return temp; // ❌ temp 被销毁,string_view 变成悬空引用! }
结构化绑定的用法?(C++17)
// 解构 pair
std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
for (const auto& [key, value] : m) {
std::cout << key << ": " << value << "\n";
}
// 解构 tuple
auto [x, y, z] = std::make_tuple(1, 2.0, "three");
// 解构结构体
struct Point { double x, y; };
Point p{3.0, 4.0};
auto [px, py] = p;
// 配合 if 语句
if (auto [iter, inserted] = m.insert({"c", 3}); inserted) {
std::cout << "Inserted: " << iter->first << "\n";
}
⭐ Concepts 是什么?(C++20)
Concepts 为模板参数定义约束条件,让模板的错误信息更清晰:
#include <concepts>
// 定义 Concept
template <typename T>
concept Numeric = std::is_arithmetic_v<T>;
// 使用 Concept 约束模板
template <Numeric T>
T Add(T a, T b) {
return a + b;
}
Add(1, 2); // ✅ int 满足 Numeric
Add(1.0, 2.0); // ✅ double 满足 Numeric
// Add("a", "b"); // ❌ string 不满足 Numeric,错误信息清晰
// 另一种写法
template <typename T>
requires std::integral<T>
T Square(T x) {
return x * x;
}
// 简写(C++20)
auto Triple(std::integral auto x) {
return x * 3;
}
Ranges 库的用法?(C++20)
Ranges 为 STL 算法提供了管道式的写法:
#include <ranges>
#include <vector>
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 传统写法
std::vector<int> result;
for (auto x : vec) {
if (x % 2 == 0) {
result.push_back(x * x);
}
}
// Ranges 管道写法(C++20)
auto result2 = vec
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
for (int x : result2) {
std::cout << x << " "; // 4 16 36 64 100
}
// 直接使用 ranges 版本的算法
std::ranges::sort(vec);
auto it = std::ranges::find(vec, 5);
五、多线程与并发
⭐ C++ 中创建线程的方式有哪些?
#include <thread>
// 方式1:普通函数
void Task(int id) {
std::cout << "Thread " << id << "\n";
}
std::thread t1(Task, 1);
// 方式2:Lambda
std::thread t2([]() {
std::cout << "Lambda thread\n";
});
// 方式3:成员函数
class Worker {
public:
void Run(int id) { std::cout << "Worker " << id << "\n"; }
};
Worker w;
std::thread t3(&Worker::Run, &w, 42);
// 方式4:可调用对象
class Functor {
public:
void operator()() { std::cout << "Functor thread\n"; }
};
std::thread t4(Functor{});
// 必须 join 或 detach
t1.join(); // 等待线程结束
t2.join();
t3.join();
t4.detach(); // 分离线程(后台运行)
⚠️ 线程对象析构前必须
join()或detach(),否则程序会调用std::terminate()。
⭐ join() 和 detach() 的区别?
join() | detach() | |
|---|---|---|
| 行为 | 阻塞等待线程结束 | 线程在后台运行 |
| 主线程 | 会等待 | 不等待 |
| 资源回收 | join 后自动回收 | 线程结束时自动回收 |
| 适用场景 | 需要线程结果 | 不关心线程何时结束 |
⭐ 互斥锁有哪些?区别是什么?
| 类型 | 说明 | 特点 |
|---|---|---|
std::mutex | 基本互斥锁 | 非递归,同一线程重复锁定会死锁 |
std::recursive_mutex | 递归互斥锁 | 同一线程可重复锁定 |
std::timed_mutex | 超时互斥锁 | 支持 try_lock_for() 超时等待 |
std::shared_mutex(C++17) | 读写锁 | 允许多个读者,只允许一个写者 |
⭐ lock_guard、unique_lock 和 scoped_lock 的区别?
| 特性 | lock_guard | unique_lock | scoped_lock(C++17) |
|---|---|---|---|
| 自动加解锁 | ✅ | ✅ | ✅ |
| 手动解锁 | ❌ | ✅ unlock() | ❌ |
| 延迟加锁 | ❌ | ✅ std::defer_lock | ❌ |
| 条件变量 | ❌ | ✅ 配合 condition_variable | ❌ |
| 多锁 | ❌ | ❌(单锁) | ✅ 同时锁多个 |
| 移动 | ❌ | ✅ | ❌ |
std::mutex mtx1, mtx2;
// lock_guard:最简单,适合大多数场景
{
std::lock_guard<std::mutex> lock(mtx1);
// 临界区
} // 自动解锁
// unique_lock:更灵活
{
std::unique_lock<std::mutex> lock(mtx1);
// ...
lock.unlock(); // 手动解锁
// 做不需要锁的操作
lock.lock(); // 再次加锁
}
// scoped_lock:同时锁多个(避免死锁)
{
std::scoped_lock lock(mtx1, mtx2); // 同时锁定,内部避免死锁
}
⭐ 什么是死锁?如何避免?
死锁:两个或多个线程互相等待对方释放锁,导致永久阻塞。
线程A:持有锁1 → 等待锁2
线程B:持有锁2 → 等待锁1
→ 互相等待 → 死锁!
// ❌ 可能死锁
std::mutex m1, m2;
void ThreadA() {
std::lock_guard<std::mutex> lock1(m1);
std::lock_guard<std::mutex> lock2(m2); // 等待 m2
}
void ThreadB() {
std::lock_guard<std::mutex> lock2(m2);
std::lock_guard<std::mutex> lock1(m1); // 等待 m1
}
避免方法:
| 方法 | 说明 |
|---|---|
| 固定加锁顺序 | 所有线程按相同顺序加锁 |
std::scoped_lock | 同时锁定多个互斥量,内部自动避免死锁 |
std::lock() | 原子地锁定多个互斥量 |
| 超时锁 | 使用 try_lock_for() 设置超时 |
| 减少锁粒度 | 尽量缩小临界区 |
// ✅ 使用 scoped_lock 避免死锁
void ThreadA() {
std::scoped_lock lock(m1, m2);
// ...
}
void ThreadB() {
std::scoped_lock lock(m1, m2); // 顺序无所谓,scoped_lock 会处理
// ...
}
⭐ 条件变量(condition_variable)的用法?
#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> taskQueue;
bool finished = false;
// 生产者
void Producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
taskQueue.push(i);
}
cv.notify_one(); // 通知一个等待的消费者
}
{
std::lock_guard<std::mutex> lock(mtx);
finished = true;
}
cv.notify_all(); // 通知所有消费者
}
// 消费者
void Consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] {
return !taskQueue.empty() || finished; // 防止虚假唤醒
});
if (taskQueue.empty() && finished) break;
int task = taskQueue.front();
taskQueue.pop();
lock.unlock();
std::cout << "Processing: " << task << "\n";
}
}
⚠️
cv.wait()的第二个参数(谓词)用于防止虚假唤醒(spurious wakeup)。
⭐ std::async 和 std::future 的用法?
#include <future>
int Compute(int x) {
std::this_thread::sleep_for(std::chrono::seconds(1));
return x * x;
}
// 异步执行
std::future<int> result = std::async(std::launch::async, Compute, 42);
// 做其他工作...
std::cout << "Doing other work...\n";
// 获取结果(阻塞等待)
int val = result.get(); // 1764
std::cout << "Result: " << val << "\n";
| 启动策略 | 说明 |
|---|---|
std::launch::async | 立即在新线程中执行 |
std::launch::deferred | 延迟执行,调用 .get() 时才执行(在当前线程) |
| 默认(两者都可) | 由实现决定 |
⭐ std::atomic 的用法?
std::atomic 提供无锁的原子操作:
#include <atomic>
#include <thread>
std::atomic<int> counter{0};
void Increment() {
for (int i = 0; i < 100000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
// 或简写:++counter;
}
}
int main() {
std::thread t1(Increment);
std::thread t2(Increment);
t1.join();
t2.join();
std::cout << counter.load() << "\n"; // 200000(保证正确)
return 0;
}
| 原子操作 | 说明 |
|---|---|
load() | 读取值 |
store(val) | 写入值 |
fetch_add(n) | 原子加 |
fetch_sub(n) | 原子减 |
compare_exchange_weak/strong | CAS 操作 |
exchange(val) | 原子交换 |
六、其他高频知识点
⭐ 什么是 RAII?
RAII(Resource Acquisition Is Initialization):资源获取即初始化。在构造函数中获取资源,在析构函数中释放资源,利用栈对象的生命周期管理资源。
// 经典 RAII 模式
class FileHandle {
FILE* file_;
public:
explicit FileHandle(const char* path)
: file_(fopen(path, "r")) {
if (!file_) throw std::runtime_error("Cannot open file");
}
~FileHandle() { if (file_) fclose(file_); }
// 禁止拷贝
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
// 标准库中的 RAII 示例
std::unique_ptr<int> p = std::make_unique<int>(42); // 自动释放内存
std::lock_guard<std::mutex> lock(mtx); // 自动解锁
std::fstream file("data.txt"); // 自动关闭文件
🌈 RAII 是 C++ 最重要的编程范式之一,是智能指针、锁守卫、文件流等的基础。
⭐ noexcept 的作用?
void Foo() noexcept {
// 承诺不抛出异常
// 如果真的抛出了,程序会调用 std::terminate() 终止
}
void Bar() noexcept(true) {} // 等同于 noexcept
void Baz() noexcept(false) {} // 可能抛异常
// 条件 noexcept
template <typename T>
void Swap(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
为什么重要?
| 场景 | 作用 |
|---|---|
| 移动构造/赋值 | 标记 noexcept 后,vector 扩容时会使用移动而非拷贝 |
| 析构函数 | 默认就是 noexcept,析构中不应抛异常 |
| 编译器优化 | noexcept 允许编译器做更多优化 |
⭐ static_cast、dynamic_cast、const_cast、reinterpret_cast 的区别?
| 类型转换 | 用途 | 安全性 | 运行时检查 |
|---|---|---|---|
static_cast | 基本类型转换、向上/向下转型 | 中等 | ❌ |
dynamic_cast | 安全的向下转型(需要虚函数) | 高 | ✅ |
const_cast | 添加/移除 const | 低 | ❌ |
reinterpret_cast | 底层位模式转换 | 最低 | ❌ |
// static_cast —— 最常用
double d = 3.14;
int i = static_cast<int>(d); // 3(编译期转换)
Base* base = new Derived();
Derived* derived = static_cast<Derived*>(base); // 不检查,不安全
// dynamic_cast —— 安全向下转型
Base* base2 = new Derived();
Derived* d2 = dynamic_cast<Derived*>(base2); // 成功:返回 Derived*
Other* o = dynamic_cast<Other*>(base2); // 失败:返回 nullptr
try {
Derived& ref = dynamic_cast<Derived&>(*base2); // 引用版本
} catch (std::bad_cast& e) {
// 失败抛异常
}
// const_cast —— 移除 const(谨慎使用)
const int* cp = &i;
int* p = const_cast<int*>(cp);
// reinterpret_cast —— 底层转换(危险)
int* ip = new int(42);
uintptr_t addr = reinterpret_cast<uintptr_t>(ip);
🌈 优先级:
static_cast>dynamic_cast>const_cast>reinterpret_cast
⭐ C++ 异常处理的机制和最佳实践?
#include <stdexcept>
// 抛出和捕获异常
try {
if (errorCondition)
throw std::runtime_error("Something went wrong");
} catch (const std::runtime_error& e) {
std::cerr << "Runtime error: " << e.what() << "\n";
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
} catch (...) {
std::cerr << "Unknown exception\n";
}
异常类层次:
std::exception
├── std::logic_error
│ ├── std::invalid_argument
│ ├── std::out_of_range
│ └── std::domain_error
├── std::runtime_error
│ ├── std::overflow_error
│ ├── std::underflow_error
│ └── std::range_error
├── std::bad_alloc (new 失败)
├── std::bad_cast (dynamic_cast 失败)
└── std::bad_typeid
最佳实践:
| 规则 | 说明 |
|---|---|
| 按引用捕获 | catch (const std::exception& e),避免切片 |
| 先捕获子类 | 派生类 catch 放在基类前面 |
| 不要在析构中抛异常 | 可能导致 terminate |
| 用 RAII 保证资源安全 | 异常发生时栈对象自动析构 |
| 只在异常情况使用 | 不要用异常做流程控制 |
附:C++ 面试高频知识点速查表
| 主题 | 核心要点 |
|---|---|
| 指针 vs 引用 | 引用是别名、不可空、不可变;指针可空、可变 |
| 智能指针 | unique_ptr(独占)、shared_ptr(共享)、weak_ptr(弱引用) |
| 移动语义 | 右值引用 T&&、std::move、避免不必要的深拷贝 |
| 虚函数 | vtable + vptr、运行时多态、基类析构必须 virtual |
| 深拷贝 vs 浅拷贝 | 浅拷贝共享内存(危险),深拷贝独立内存 |
| Rule of Five | 析构、拷贝构造/赋值、移动构造/赋值 |
| RAII | 构造获取资源、析构释放资源 |
| 模板 | 编译期实例化、写在头文件、特化、SFINAE |
| STL | vector(动态数组)、map(红黑树)、unordered_map(哈希表) |
| 多线程 | mutex、lock_guard、condition_variable、atomic、async |
| 内存管理 | 栈/堆/全局区、new/delete、内存对齐、内存泄漏 |
| 类型转换 | static_cast > dynamic_cast > const_cast > reinterpret_cast |
💬 评论