C++ 基础常见面试题总结(下)

✍️ Demo User·📅 2026年4月24日·👁 7 次阅读
c++开发
📚 系列:现代 C++ 学习之路

本文采用面试问答形式,系统梳理 C++ 模板、STL、现代特性、多线程的核心知识。⭐ 标记为高频重点题。


一、模板与泛型编程

⭐ 什么是模板?函数模板和类模板的区别?

模板是 C++ 的泛型编程机制,允许编写类型无关的代码,在编译期生成具体类型的代码。

cpp
// 函数模板
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 文件看不到模板的实现
  • 无法实例化 → 链接错误
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)?

为特定类型提供不同的实现:

cpp
// 通用版本
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 引入的可变参数模板允许接受任意数量和类型的参数:

cpp
// 递归终止条件
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):模板参数替换失败不是错误,编译器会尝试其他重载。

cpp
#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 的主要组件有哪些?

组件说明示例
容器存储数据vectormapset
迭代器统一的遍历接口begin()end()
算法操作数据sortfindtransform
仿函数可调用对象std::lessstd::greater
适配器接口封装stackqueuepriority_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 的扩容机制是什么?

vectorsize() 达到 capacity() 时,会自动扩容:

  1. 分配一块更大的内存(通常是当前容量的 2 倍,GCC 实现;MSVC 是 1.5 倍
  2. 将旧元素移动/拷贝到新内存
  3. 释放旧内存
cpp
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

优化方法

cpp
// 方法1:预分配容量
std::vector<int> vec;
vec.reserve(1000);          // 预分配,避免多次扩容

// 方法2:初始化时指定大小
std::vector<int> vec(1000, 0);

// 释放多余内存
vec.shrink_to_fit();        // 请求将 capacity 缩减到 size

⚠️ 扩容会导致所有迭代器、指针、引用失效

vectorlist 如何选择?

特性vectorlist
内存布局连续(缓存友好)非连续(节点分散)
随机访问✅ O(1)❌ O(n)
头部插入/删除❌ O(n)✅ O(1)
尾部插入/删除✅ 均摊 O(1)✅ O(1)
中间插入/删除❌ O(n)✅ O(1)(已知位置)
迭代器失效插入/扩容时全部失效仅删除的节点失效
内存开销大(每个节点额外两个指针)

🌈 经验法则绝大多数情况下优先使用 vector。即使需要中间插入,vector 的缓存友好性往往使其性能优于 list

mapunordered_map 的区别?

特性mapunordered_map
底层红黑树哈希表
有序性✅ 按 key 排序❌ 无序
查找O(log n)均摊 O(1)
插入O(log n)均摊 O(1)
key 要求需要 < 运算符需要 hash 函数和 ==
最坏情况O(log n)O(n)(哈希冲突严重时)
内存较小较大(哈希表开销)
cpp
// 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;
// 遍历顺序不确定

迭代器失效的常见场景?

容器触发操作失效范围
vectorpush_back(触发扩容)所有迭代器失效
vectorinsert / erase插入/删除点之后的迭代器失效
deque头/尾插入所有迭代器失效(元素引用有效)
listerase仅被删除节点的迭代器失效
map / seterase仅被删除节点的迭代器失效
unordered_mapinsert(触发 rehash)所有迭代器失效
cpp
// ❌ 错误:遍历中删除
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 算法有哪些?

cpp
#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 表达式的捕获方式有哪些?

cpp
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 表示一个值可能存在也可能不存在,替代"哨兵值"和指针:

cpp
#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类型安全的联合体,可以存储多种类型之一:

cpp
#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 是字符串的轻量级只读视图,不拥有数据,不分配内存:

cpp
#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

cpp
std::string_view Dangerous() {
    std::string temp = "hello";
    return temp;   // ❌ temp 被销毁,string_view 变成悬空引用!
}

结构化绑定的用法?(C++17)

cpp
// 解构 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 为模板参数定义约束条件,让模板的错误信息更清晰:

cpp
#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 算法提供了管道式的写法:

cpp
#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++ 中创建线程的方式有哪些?

cpp
#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_guardunique_lockscoped_lock 的区别?

特性lock_guardunique_lockscoped_lock(C++17)
自动加解锁
手动解锁unlock()
延迟加锁std::defer_lock
条件变量✅ 配合 condition_variable
多锁❌(单锁)✅ 同时锁多个
移动
cpp
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
→ 互相等待 → 死锁!
cpp
// ❌ 可能死锁
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() 设置超时
减少锁粒度尽量缩小临界区
cpp
// ✅ 使用 scoped_lock 避免死锁
void ThreadA() {
    std::scoped_lock lock(m1, m2);
    // ...
}

void ThreadB() {
    std::scoped_lock lock(m1, m2);  // 顺序无所谓,scoped_lock 会处理
    // ...
}

⭐ 条件变量(condition_variable)的用法?

cpp
#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::asyncstd::future 的用法?

cpp
#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 提供无锁的原子操作:

cpp
#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/strongCAS 操作
exchange(val)原子交换

六、其他高频知识点

⭐ 什么是 RAII?

RAII(Resource Acquisition Is Initialization):资源获取即初始化。在构造函数中获取资源,在析构函数中释放资源,利用栈对象的生命周期管理资源。

cpp
// 经典 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 的作用?

cpp
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_castdynamic_castconst_castreinterpret_cast 的区别?

类型转换用途安全性运行时检查
static_cast基本类型转换、向上/向下转型中等
dynamic_cast安全的向下转型(需要虚函数)
const_cast添加/移除 const
reinterpret_cast底层位模式转换最低
cpp
// 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++ 异常处理的机制和最佳实践?

cpp
#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
STLvector(动态数组)、map(红黑树)、unordered_map(哈希表)
多线程mutexlock_guardcondition_variableatomicasync
内存管理栈/堆/全局区、new/delete、内存对齐、内存泄漏
类型转换static_cast > dynamic_cast > const_cast > reinterpret_cast

💬 评论

加载评论中...