C++面试题整理

C++常见面试题总结 阅读详情

C++ 面试题大全(2025-2026 最新版)

涵盖 C++98 到 C++23,从基础到架构师级别,按主题分类整理。


目录

  1. 基础必考篇(TOP 10)
  2. 面向对象:虚函数与多态
  3. 现代 C++ 新特性(C++11/14/17/20/23)
  4. 智能指针
  5. 移动语义与完美转发
  6. STL 容器底层原理
  7. 并发编程与无锁编程
  8. 设计模式
  9. 高频手撕代码题
  10. 大厂面试差异
  11. 分岗位高频题
  12. Lambda 表达式深入
  13. 操作系统基础
  14. IO 多路复用
  15. 平台与架构
  16. 调试与版本控制

一、基础必考篇

Q1: 指针与引用的区别

特性指针引用
是否可为空可以为 nullptr不可为空
可否重新绑定可以指向不同对象一旦绑定不可更改
是否需要解引用需要 * 或 ->直接使用
是否占用内存占用(存地址)不占(只是别名,编译器优化)
可否多级二级/多级指针没有多级引用
底层实现存地址本质也是存地址(编译器视角)

Q2: new/delete vs malloc/free

特性new/deletemalloc/free
本质C++ 运算符C 库函数
调用构造函数✅ 自动调用❌ 不调用
调用析构函数✅ 自动调用❌ 不调用
失败行为抛 std::bad_alloc 异常返回 NULL
申请大小编译器自动计算需手动计算字节数
重载可重载不可重载
内存来源自由存储区堆

Q3: const 关键字的多种用法

const int a = 10;           // 1. 常量:值不可改
int const b = 20;           // 同上,等价写法

const int* p1 = &a;         // 2. 指向常量的指针:*p1不可改,p1可改
int const* p2 = &a;         // 同上,等价写法

int* const p3 = &a;         // 3. 常量指针:p3不可改,*p3可改

const int* const p4 = &a;   // 4. 常量指针指向常量:都不能改

void foo() const;           // 5. const成员函数:不能修改成员变量(mutable除外)
const std::string& bar();   // 6. const返回值:防止调用者修改返回值

Q4: static 关键字的作用

作用域效果
局部静态变量生命周期为整个程序,首次执行到定义处初始化(C++11起线程安全)
全局静态变量/函数作用域限制在当前文件(内部链接),外部文件不可见
静态成员变量属于类而非对象,所有对象共享,需在类外定义
静态成员函数无 this 指针,只能访问静态成员

Q5: 堆与栈的区别

特性栈 (Stack)堆 (Heap)
分配方式编译器自动程序员手动(new/malloc)
大小限制较小(通常几MB)较大(受系统内存限制)
分配速度极快(一条CPU指令)较慢(需查找空闲块)
生命周期作用域结束自动释放需手动释放
碎片问题无可能产生
线程安全天然线程安全需同步

Q6: 深拷贝与浅拷贝

class String {
    char* data;
public:
    // 浅拷贝(默认拷贝构造):只复制指针
    // String(const String& s) = default;  // data 指向同一块内存
    
    // 深拷贝:复制指针指向的内容
    String(const String& s) {
        data = new char[strlen(s.data) + 1];
        strcpy(data, s.data);
    }
};

Q7: 四种强制类型转换

转换用途特点
static_cast相关类型转换、非多态父子转换编译期检查
dynamic_cast多态父子转换(含运行时类型检查)运行时检查,失败返回nullptr/抛异常
const_cast移除/添加const属性仅改const属性
reinterpret_cast任意类型间转换(二进制重解释)最危险,慎用

Q8: extern "C" 的作用

C++ 编译器会做名称修饰(name mangling),extern "C" 告诉编译器按 C 链接方式处理,避免名称修饰。用于 C/C++ 混合编程。

extern "C" {
    void c_function(int a);
}

Q9: inline 函数的理解

编译器将函数体展开到调用处,减少函数调用开销。现代编译器会自行判断是否内联,inline 关键字只是建议。主要解决头文件中函数重复定义的问题(内联函数允许多个编译单元内重复定义)。

Q10: struct 与 class 的区别

只有一点:默认访问权限不同。struct 默认 public,class 默认 private。其他完全相同。


二、面向对象:虚函数与多态

多态实现原理:vtable + vptr

对象内存布局:
┌───────────────┐
│   vptr        │──→ vtable(虚函数表,代码段/只读数据段)
├───────────────┤     ┌──────────────────────┐
│   成员变量    │     │ &ClassName::func1    │
└───────────────┘     │ &ClassName::func2    │
                      └──────────────────────┘
概念说明
vtable(虚函数表)编译期生成,类级别,同类型对象共享,存于代码段
vptr(虚表指针)对象级别,在构造函数中初始化
动态绑定流程对象 → vptr → vtable[索引] → 函数地址 → 调用

Q: 为什么构造函数不能是虚函数?

对象中的 vptr 是在构造函数执行期间才初始化的。如果构造函数是虚函数,调用前需要通过 vptr 查找,但此时 vptr 还未初始化——鸡生蛋问题。

Q: 为什么析构函数建议为虚函数?

当通过基类指针 delete 子类对象时,如果基类析构函数不是虚函数,则只会调用基类析构,子类资源泄漏。

Base* p = new Derived();
delete p;  // ~Base() 非虚函数 → 只调 Base 析构 → 泄漏!

Q: 重载、重写、隐藏的区别

特性重载 (Overload)重写/覆盖 (Override)隐藏 (Hide)
作用域同一类中基类-派生类之间基类-派生类之间
函数名相同相同相同
参数必须不同必须相同只需同名
virtual不要求必须非虚函数

Q: 什么是对象切片(Object Slicing)?

子类对象按值传递给父类参数时,子类特有部分被"切掉",vptr 也变为父类的 vptr,多态消失。

Q: override 和 final 的作用

  • override:显式声明重写,编译器检查签名是否匹配
  • final:禁止子类进一步重写此函数,或禁止类被继承

三、现代 C++ 新特性

C++11 核心特性

特性说明
auto / decltype类型自动推导
右值引用 &&移动语义基础
std::move / std::forward移动 + 完美转发
智能指针unique_ptr, shared_ptr, weak_ptr
Lambda 表达式[capture](params) -> ret { body }
nullptr类型安全的空指针
for 范围循环for (auto& x : container)
= delete / = default显式禁止/默认特殊函数
constexpr编译期常量表达式
线程库std::thread, std::mutex, std::condition_variable

C++14 增强

  • 泛型 Lambda:[](auto a, auto b) { return a + b; }
  • std::make_unique<T>() 工厂函数
  • constexpr 支持更多语法(循环、分支)

C++17 核心特性

特性说明用法
std::optional<T>可能无值的值类型替代指针表示"可能为空"
std::variant<T...>类型安全的联合体替代 union
std::string_view非拥有字符串视图不拷贝字符串的高性能传递
if constexpr编译期条件分支模板中根据类型走不同逻辑
结构化绑定解构元组/结构体auto [x, y, z] = tuple;
std::any可存任意类型的容器类型安全的 void*
CTAD类模板参数推导std::pair p{1, 2.0}; 无需写模板参数
Fold Expressions参数包折叠(args + ...)
// if constexpr 示例
template<typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>)
        return *t;         // 编译期选此分支
    else
        return t;          // 或此分支
}

C++20 核心特性

1. Concepts(概念)

编译期约束模板参数,让模板错误信息更清晰。

#include <concepts>

template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

template<Addable T>
T add(T a, T b) { return a + b; }

// 四种写法:
// 1. requires 子句
template<typename T> requires std::integral<T>
T f1(T a);

// 2. 约束模板参数
template<std::integral T>
T f2(T a);

// 3. 尾部 requires
template<typename T>
T f3(T a) requires std::integral<T>;

// 4. 缩写函数模板
std::integral auto f4(std::integral auto a);
2. Ranges(范围库)

管道式操作,惰性求值,零中间容器。

#include <ranges>

std::vector<int> nums = {1, 2, 3, 4, 5, 6};

auto result = nums
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * 10; });

// result: 20, 40, 60(遍历时才计算,无临时容器)
std::ranges::sort(nums);  // 直接传容器,无需 begin()/end()
View Adaptor功能
views::filter(pred)过滤
views::transform(fn)映射
views::take(n)取前n个
views::drop(n)跳过前n个
views::reverse反向
views::iota生成序列
3. Coroutines(协程)

三个关键字:co_await、co_yield、co_return。无栈协程,挂起时状态存于堆分配的协程帧。

Generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int tmp = a;
        a = b;
        b = tmp + b;
    }
}
4. Modules(模块)

替代 #include 头文件机制,提升编译速度,隔离命名空间。

5. std::span<T>

轻量级视图,不拥有数据,安全替代 T* + size。

C++23 重点

特性说明
std::expected<T, E>返回值带错误信息(替代异常)
std::flat_map / std::flat_set连续内存存储的有序容器
std::mdspan多维视图
std::generator同步协程生成器
std::ranges::to<>将 View 收集到容器
views::enumerate带索引遍历

四、智能指针

总览对比

特性unique_ptrshared_ptrweak_ptr
所有权独占共享无(观察者)
引用计数无有(use_count)只增 weak_count
拷贝禁止允许允许
移动允许允许允许
大小裸指针级别(不含删除器)2 个指针(对象+控制块)2 个指针(同上)
内存开销最小额外控制块同 shared_ptr
典型场景工厂函数、独占资源、PIMPL多处共享、异步任务打破循环引用、观察者模式

一、std::unique_ptr — 独占所有权

核心语义:同一时刻只有一个 unique_ptr 拥有对象。禁止拷贝,只能移动。

创建方式
#include <memory>

// 1. make_unique(C++14,推荐方式)
auto p1 = std::make_unique<int>(42);       // 类型自动推导
auto p2 = std::make_unique<std::string>(10, 'x');  // 对应 string(10, 'x')

// 2. 从裸指针构造(不推荐直接写 new)
std::unique_ptr<int> p3(new int(100));

// 3. C++11 无 make_unique 时的写法
std::unique_ptr<int> p4(new int(200));

// 4. 数组(C++14 起专用重载)
auto arr = std::make_unique<int[]>(10);    // int[10]
arr[0] = 1;  // 支持 operator[]
所有权转移 — 只能移动
auto p1 = std::make_unique<int>(42);

// std::unique_ptr<int> p2 = p1;     // ❌ 编译错误!拷贝构造已删除
std::unique_ptr<int> p2 = std::move(p1);  // ✅ 移动构造
// p1 == nullptr,所有权转移给了 p2

// 作为函数参数和返回值
std::unique_ptr<int> process(std::unique_ptr<int> input) {
    *input += 1;
    return input;  // 编译器自动 move(RVO + 移动语义)
}

auto result = process(std::make_unique<int>(10));  // 临时对象直接移动
常用操作
auto p = std::make_unique<int>(42);

// 解引用
int val = *p;         // 获取值
std::string s = p->c_str();  // 对指向对象的成员访问

// 获取裸指针(不转移所有权)
int* raw = p.get();   // 获取裸指针,unique_ptr 仍拥有对象
// ⚠️ 不要 delete raw!也不要保存 raw 到另一个智能指针中

// 释放所有权(返回裸指针,unique_ptr 变为空)
int* raw2 = p.release();  // p 不再拥有对象,调用者负责 delete
delete raw2;              // 必须手动释放

// 重置(销毁旧对象,可选接管新对象)
p.reset();                   // 直接销毁,p == nullptr
p.reset(new int(100));       // 销毁旧对象,接管新对象

// 交换
auto p2 = std::make_unique<int>(200);
p.swap(p2);  // 交换两个 unique_ptr 所管理的对象

// 判空
if (p)      { /* 非空 */ }
if (!p)     { /* 为空 */ }
if (p == nullptr) { /* 为空 */ }
自定义删除器
// 场景:FILE* 需要 fclose 而非 delete
auto file_deleter = [](FILE* f) {
    if (f) fclose(f);
};
std::unique_ptr<FILE, decltype(file_deleter)> fp(fopen("test.txt", "r"), file_deleter);
// 离开作用域自动调用 fclose

// 场景:malloc 分配的内存需要 free
auto free_deleter = [](void* p) { std::free(p); };
std::unique_ptr<char, decltype(free_deleter)> buf(
    static_cast<char*>(std::malloc(1024)), free_deleter
);

// 场景:管理第三方资源
struct Connection { void close(); };
auto conn_deleter = [](Connection* c) { c->close(); };
std::unique_ptr<Connection, decltype(conn_deleter)> conn(new Connection, conn_deleter);
实现原理(极简版)
template<typename T>
class SimpleUniquePtr {
public:
    explicit SimpleUniquePtr(T* ptr = nullptr) : ptr_(ptr) {}
    ~SimpleUniquePtr() { if (ptr_) delete ptr_; }

    SimpleUniquePtr(const SimpleUniquePtr&) = delete;            // 禁止拷贝
    SimpleUniquePtr& operator=(const SimpleUniquePtr&) = delete; // 禁止拷贝赋值

    SimpleUniquePtr(SimpleUniquePtr&& other) noexcept            // 移动构造
        : ptr_(other.ptr_) { other.ptr_ = nullptr; }

    SimpleUniquePtr& operator=(SimpleUniquePtr&& other) noexcept { // 移动赋值
        if (this != &other) {
            if (ptr_) delete ptr_;
            ptr_ = other.ptr_;
            other.ptr_ = nullptr;
        }
        return *this;
    }

    T& operator*()  const { return *ptr_; }
    T* operator->() const { return ptr_; }
    T* get()        const { return ptr_; }
    explicit operator bool() const { return ptr_ != nullptr; }

    T* release() { T* tmp = ptr_; ptr_ = nullptr; return tmp; }
    void reset(T* ptr = nullptr) { if (ptr_) delete ptr_; ptr_ = ptr; }

private:
    T* ptr_;
};

二、std::shared_ptr — 共享所有权

核心语义:多个 shared_ptr 可共享同一对象的所有权,通过引用计数管理生命周期。最后一个 shared_ptr 销毁时释放对象。

内存模型
┌─────────────────┐     ┌─────────────┐     ┌──────────────────────┐
│   shared_ptr A  │────→│  控制块       │     │       对象 T          │
├─────────────────┤     ├─────────────┤     ├──────────────────────┤
│  ptr_ ──────────┼──→  │ use_count=2 │     │   ... 数据成员 ...    │
└─────────────────┘     │ weak_count=0│     └──────────────────────┘
                         │ deleter     │
┌─────────────────┐     │ allocator   │
│   shared_ptr B  │     └─────────────┘
├─────────────────┤           ↑
│  ptr_ ──────────┼───────────┘
│  ctrl_ ─────────┼──→ 控制块
└─────────────────┘

释放时机:
- use_count → 0:销毁对象 T(调用 deleter)
- use_count=0 且 weak_count=0:销毁控制块
创建方式
// 1. make_shared(强烈推荐)
auto sp1 = std::make_shared<int>(42);
auto sp2 = std::make_shared<std::string>("hello");
auto sp3 = std::make_shared<std::vector<int>>(100, 0);  // vector(100, 0)

// 2. make_shared 用于数组(C++20)
auto arr = std::make_shared<int[]>(10);

// 3. 从裸指针构造(不推荐)
std::shared_ptr<int> sp4(new int(100));  // 两次内存分配

// 4. 从 unique_ptr 移动构造(转移所有权)
auto up = std::make_unique<int>(42);
std::shared_ptr<int> sp5 = std::move(up);  // up == nullptr
拷贝与引用计数
auto sp1 = std::make_shared<int>(42);
std::cout << sp1.use_count() << "\n";  // 1

{
    std::shared_ptr<int> sp2 = sp1;     // 引用计数 +1
    std::cout << sp1.use_count() << "\n";  // 2

    std::shared_ptr<int> sp3(sp1);      // +1
    std::cout << sp1.use_count() << "\n";  // 3

    auto sp4 = sp1;                     // +1
    std::cout << sp1.use_count() << "\n";  // 4
}  // sp2, sp3, sp4 析构,引用计数回到 1

std::cout << sp1.use_count() << "\n";  // 1
// sp1 析构 → use_count 变为 0 → 对象被释放
常用操作
auto sp = std::make_shared<int>(42);

// 访问对象
int val = *sp;
// sp->member;  // 访问成员

// 获取裸指针
int* raw = sp.get();

// 重置
sp.reset();                  // 释放当前对象,sp 变空(use_count 减 1)
sp.reset(new int(200));      // 释放当前对象,接管新对象

// 判空
if (sp) { /* 非空 */ }
if (sp == nullptr) { /* 为空 */ }

// 检查引用计数(主要用于调试)
long n = sp.use_count();     // 当前 shared_ptr 数量
bool uniq = sp.unique();     // use_count == 1?(C++20 起废弃)
自定义删除器
// 自定义删除器(与 unique_ptr 不同,删除器不改变 shared_ptr 类型)
auto deleter = [](int* p) { delete p; };
std::shared_ptr<int> sp1(new int(42), deleter);
std::shared_ptr<int> sp2(new int(100), [](int* p) { delete p; });

// make_shared 不支持自定义删除器,有需要时用 new + 删除器
// 但 make_shared 结合默认删除器在所有其他场景都是首选
线程安全
┌──────────────────────────────────────────────┐
│  shared_ptr 的线程安全性分两层:               │
├──────────────────────────────────────────────┤
│  1. 控制块/引用计数:线程安全(atomic 操作)    │
│     → 多个线程拷贝/销毁同一个 shared_ptr 安全   │
│  2. 管理的对象数据:不保证线程安全              │
│     → 多线程访问对象成员需要额外 mutex          │
│  3. 同一个 shared_ptr 对象:非线程安全          │
│     → 多线程同时赋值给同一个 shared_ptr 需同步  │
└──────────────────────────────────────────────┘
// ✅ 安全:不同线程持有各自的 shared_ptr 拷贝
void worker(std::shared_ptr<Data> sp) { /* 对象生命周期受保护 */ }
auto sp = std::make_shared<Data>();
std::thread t1(worker, sp);  // 传值时拷贝,引用计数原子递增
std::thread t2(worker, sp);  // 同上

// ❌ 不安全:多线程访问对象内部数据
std::mutex mtx;
void safe_worker(std::shared_ptr<Data> sp) {
    std::lock_guard<std::mutex> lock(mtx);
    sp->modify();  // 对象数据仍需 mutex 保护
}
enable_shared_from_this

问题:在成员函数中如何正确返回 this 的 shared_ptr?

// ❌ 错误的做法
struct Bad {
    std::shared_ptr<Bad> get_shared() {
        return std::shared_ptr<Bad>(this);  // 危险!创建新的控制块
    }
};
// 调用两次 get_shared() 会创建两个控制块 → double free!

// ✅ 正确的做法
struct Good : public std::enable_shared_from_this<Good> {
    std::shared_ptr<Good> get_shared() {
        return shared_from_this();  // 复用已有的控制块,引用计数 +1
    }
    static std::shared_ptr<Good> create() {
        return std::make_shared<Good>();
    }
};

auto obj = Good::create();     // shared_ptr<Good>
auto obj2 = obj->get_shared(); // 共享同一个控制块,use_count = 2

重要约束:调用 shared_from_this() 前,对象必须已被 shared_ptr 管理。通常将构造函数设为 private + 提供静态工厂方法。

make_shared vs new 深入
特性new + shared_ptrmake_shared
内存分配2 次(对象 + 控制块)1 次(合并分配)
异常安全f(shared_ptr<T>(new T), g()) 可能泄漏完全安全
缓存局部性差(两块内存分散)好(连续内存)
自定义删除器✅ 支持❌ 不支持
内存释放延迟无延迟对象内存随控制块一起释放(weak_ptr 存活时)
弱指针下的内存占用只占用对象大小占用对象+控制块大小(直到所有 weak_ptr 也释放)

三、std::weak_ptr — 观察者 / 弱引用

核心语义:不拥有对象所有权,只是"观察" shared_ptr 管理的对象。weak_ptr 不影响引用计数中的 use_count,只增加 weak_count。

为什么需要 weak_ptr?
  1. 打破循环引用:shared_ptr 循环引用导致内存泄漏
  2. 缓存/观察者模式:知道对象可能已被销毁,需要先检查
  3. 安全的悬垂检查:将 shared_ptr 赋值给 weak_ptr 后,即使原对象被释放也不会 crash
创建 weak_ptr
auto sp = std::make_shared<int>(42);

// 只能从 shared_ptr 或另一个 weak_ptr 创建
std::weak_ptr<int> wp1(sp);           // 从 shared_ptr
std::weak_ptr<int> wp2(wp1);          // 从另一个 weak_ptr(拷贝)
std::weak_ptr<int> wp3 = sp;          // 隐式转换

// ⚠️ 不能直接从裸指针创建
// std::weak_ptr<int> wp(new int(42));  // ❌ 编译错误
核心方法
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;

// 1. expired() — 检查对象是否已释放
if (!wp.expired()) {
    // 对象仍然存活(非原子检查,存在 TOCTOU 问题)
}

// 2. lock() — 尝试获取 shared_ptr(推荐方式)
if (auto locked = wp.lock()) {
    // locked 是 shared_ptr<int>,确保对象在作用域内不会释放
    *locked = 100;
    // use_count 临时 +1,离开 if 块后 -1
} else {
    // 对象已释放
}

// 3. use_count() — 当前 shared_ptr 数量(主要用于调试)
long n = wp.use_count();   // 返回 1(sp 还在)

// 4. reset() — 清空弱引用
wp.reset();                // wp 不再观察任何对象
打破循环引用(详细示例)
#include <memory>
#include <iostream>

class B;  // 前向声明

class A {
public:
    std::shared_ptr<B> bptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> aptr;  // ← 关键:用 weak_ptr 而非 shared_ptr
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->bptr = b;   // A 持有 B 的 shared_ptr
    b->aptr = a;   // B 持有 A 的 weak_ptr(弱引用,不影响引用计数)

    // 离开作用域:
    // a 的 use_count = 1(仅 a 自己持有)→ 销毁 A
    // A 析构 → bptr 释放 → b 的 use_count = 1 → 销毁 B
    // ✅ 两个对象都正确释放!
}
内存泄漏版(全用 shared_ptr):        安全版(一端 weak_ptr):

A ←────── B                           A ←────── B
│          │                           │          │
└─── shared ──→ B                      └─── shared ──→ B
     shared ──┘                            weak ──→ A (不影响计数)

use_count(A) = 2  ⇢ 永远不为 0        use_count(A) = 1  ⇢ 正确释放
use_count(B) = 1                       use_count(B) = 1  ⇢ 正确释放
实用场景:观察者模式 + 自动清理
class Subject {
    std::vector<std::weak_ptr<IObserver>> observers_;
public:
    void attach(std::shared_ptr<IObserver> obs) {
        observers_.push_back(obs);  // 存 weak_ptr
    }

    void notify(const Event& ev) {
        for (auto it = observers_.begin(); it != observers_.end(); ) {
            if (auto obs = it->lock()) {    // 尝试获取 shared_ptr
                obs->onEvent(ev);           // 观察者仍存活,通知
                ++it;
            } else {
                it = observers_.erase(it);  // 观察者已销毁,自动清理
            }
        }
    }
};
// 优点:观察者销毁后无需手动取消注册,Subject 自动跳过
实用场景:缓存
class ImageCache {
    std::unordered_map<std::string, std::weak_ptr<Image>> cache_;
public:
    std::shared_ptr<Image> get(const std::string& key) {
        auto& wp = cache_[key];
        if (auto img = wp.lock()) {
            return img;  // 缓存命中,图片仍在内存中
        }
        auto img = std::make_shared<Image>(loadFromDisk(key));
        wp = img;        // 更新缓存
        return img;
    }
    // 当外部不再使用某张图片时,缓存项自动失效(weak_ptr expired)
    // 不会因缓存持有 shared_ptr 而导致图片永远无法释放
};

四、三种智能指针选择指南

                     是否需要共享所有权?
                      /            \
                    否              是
                    /                \
          ┌──────────┐        ┌───────────────┐
          │unique_ptr │   是否可能产生循环引用? │
          └──────────┘      /              \
                           是                否
                          /                  \
                  ┌──────────────┐    ┌────────────┐
                  │shared_ptr    │    │ shared_ptr │
                  │+ weak_ptr    │    │            │
                  │(一端用weak) │    └────────────┘
                  └──────────────┘

核心使用原则

原则说明
优先 unique_ptr90% 场景都用独占所有权即可
需要共享才用 shared_ptr引用计数有开销(atomic ±1)
用 make_shared/make_unique异常安全 + 性能更好(一次分配)
禁止裸 newC++14 起,new 只出现在工厂函数/make 函数内部
不要从同一个裸指针创建多个 shared_ptr导致多个控制块 → double free
weak_ptr 不负责释放weak_ptr 不能直接解引用,必须 lock() 先
继承 enable_shared_from_this需要从 this 安全获取 shared_ptr 时使用

五、移动语义与完美转发

值类别分类

类别说明例如
左值 (lvalue)有名字、可寻址、可重复访问int a = 10; 中 a
纯右值 (prvalue)临时对象、字面量42、std::string("hello")
将亡值 (xvalue)资源即将被转移std::move(x) 的返回值

std::move vs std::forward

特性std::movestd::forward
功能无条件转为右值有条件转发(保持原值类别)
本质static_cast<T&&>依赖引用折叠的智能转换
使用场景资源所有权转移模板中完美转发参数
参数推导自动推导必须显式指定模板参数

核心误区

std::move 不移动任何东西! 它只是一个无条件的类型转换,真正的移动发生在移动构造函数/移动赋值运算符中。

具名右值引用变量是左值! 任何有名字的变量都是左值,即使类型是右值引用。

void foo(std::string&& s) {
    data = s;              // ❌ s 是左值,调拷贝
    data = std::move(s);   // ✅ 强制转右值,调移动
}

引用折叠规则

组合结果
T& &T&
T& &&T&
T&& &T&
T&& &&T&&

规则:有一个是左值引用,结果就是左值引用;两个都是右值引用才是右值引用。

完美转发失败的情况

  • 大括号初始化列表 {}(无法推导类型)
  • 0 或 NULL 作为空指针常量
  • 位域(bit-fields)
  • 仅声明但未定义的 static const 成员

六、STL 容器底层原理

vector

项目说明
底层结构连续内存的动态数组
内部指针start(首), finish(尾+1), end_of_storage(容量尾)
扩容倍数MSVC: 1.5倍, GCC: 2倍
随机访问O(1)
尾部插入均摊 O(1),偶尔扩容时为 O(n)
中间插入/删除O(n)
迭代器失效扩容时全部失效;insert/erase 当前位置及之后失效
size()      = finish - start           // 元素个数
capacity()  = end_of_storage - start   // 已分配空间

扩容为何 1.5 倍而非 2 倍? 2 倍扩容后,释放的旧内存永远无法被新的扩容请求复用,产生碎片。1.5 倍则可累积复用。

map

项目说明
底层结构红黑树(自平衡二叉搜索树)
插入/查找/删除O(log n)
元素顺序按 key 自动排序
迭代器类型双向迭代器
适用场景需有序遍历、范围查询、性能稳定性

红黑树 vs AVL 树: 红黑树非严格平衡,插入删除旋转次数更少,综合场景性能更优。STL 因此选择红黑树。

unordered_map

项目说明
底层结构哈希表(开链法/拉链法)
bucket 结构vector + 单向链表
查找平均 O(1),最坏 O(n)
Rehash 时机load_factor() > max_load_factor() 时
迭代器单向迭代器(forward iterator)
key 要求必须支持 std::hash<Key> 和 operator==

负载因子 (load factor): size() / bucket_count(),默认最大为 1.0。

map vs unordered_map 选择

场景推荐
需要有序map
范围查询map
实时系统(稳定性能)map
纯快速查找unordered_map
海量数据平均性能unordered_map
内存受限map

vector 的 resize() vs reserve()

特性resize(n)reserve(n)
修改 size✅❌
新增元素✅(默认值填充)❌
用途改变元素个数预留空间减少扩容

vector vs list 深入对比

特性vectorlist
底层结构连续内存(动态数组)双向链表(非连续节点)
随机访问O(1)O(n)
头部插入/删除O(n)O(1)
尾部插入/删除均摊 O(1)O(1)
中间插入/删除O(n)(需移动元素)O(1)(已有迭代器定位)
内存占用紧凑,无额外指针开销每节点额外 2 个指针(前驱+后继)
缓存友好✅ 极好(连续内存,预读命中)❌ 差(节点分散,cache miss 频繁)
迭代器失效扩容全失效;插入删除后部分失效仅被删除节点失效,其余稳定
遍历性能极高(指针自增,CPU 预取)较慢(指针跳转,每次可能 cache miss)

选型原则:

  • 默认选 vector——连续内存带来的缓存优势在绝大多数场景胜过链表
  • 只有频繁在中间做 O(1) 插入删除且数据量大到移动成本不可接受时,才考虑 list
  • 需要迭代器长期稳定(插入不导致失效)时用 list

手写队列(数组实现 + 链表实现)

// 数组实现(环形队列)
template<typename T>
class ArrayQueue {
public:
    explicit ArrayQueue(size_t cap) : data_(cap), head_(0), tail_(0), size_(0), cap_(cap) {}

    bool push(const T& val) {
        if (size_ == cap_) return false;   // 满
        data_[tail_] = val;
        tail_ = (tail_ + 1) % cap_;
        size_++;
        return true;
    }

    bool pop(T& val) {
        if (size_ == 0) return false;      // 空
        val = data_[head_];
        head_ = (head_ + 1) % cap_;
        size_--;
        return true;
    }

    bool empty() const { return size_ == 0; }
    size_t size() const { return size_; }

private:
    std::vector<T> data_;
    size_t head_, tail_, size_, cap_;
};

// 链表实现
template<typename T>
class ListQueue {
public:
    void push(const T& val) {
        auto node = std::make_unique<Node>(val);
        if (!tail_) {
            head_ = std::move(node);
            tail_ = head_.get();
        } else {
            tail_->next = std::move(node);
            tail_ = tail_->next.get();
        }
        size_++;
    }

    bool pop(T& val) {
        if (!head_) return false;
        val = head_->data;
        head_ = std::move(head_->next);
        if (!head_) tail_ = nullptr;
        size_--;
        return true;
    }

private:
    struct Node {
        T data;
        std::unique_ptr<Node> next;
        Node(const T& d) : data(d) {}
    };
    std::unique_ptr<Node> head_;
    Node* tail_ = nullptr;  // 裸指针观察者,不拥有所有权
    size_t size_ = 0;
};

七、并发编程与无锁编程

std::atomic 六大内存序

内存序含义典型场景
memory_order_relaxed只保证原子性,无顺序约束引用计数自增
memory_order_acquire之后的读写不能重排到此之前消费者读取标志
memory_order_release之前的读写不能重排到此之后生产者写入标志
memory_order_acq_rel兼具 acquire + releaseCAS 循环
memory_order_seq_cst全局顺序一致性(最强最慢)mutex 底层实现
memory_order_consume仅依赖排序(已基本废弃)几乎不用

volatile vs atomic

特性volatilestd::atomic
保证原子性❌✅
防止编译器重排❌(仅防寄存器缓存)✅
防止 CPU 乱序❌✅(生成内存屏障)
用途硬件寄存器、信号处理多线程同步

release-acquire 同步(必考)

std::atomic<bool> ready{false};
int data = 0;

// 生产者
void producer() {
    data = 42;
    ready.store(true, std::memory_order_release);  // 之前的所有写入对 acquire 可见
}

// 消费者
void consumer() {
    while (!ready.load(std::memory_order_acquire))  // 与 release 配对
        ;
    assert(data == 42);  // 一定成功!
}

无锁自旋锁

class SpinLock {
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
    void lock() {
        while (flag.test_and_set(std::memory_order_acquire))
            ;  // 自旋等待
    }
    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

无锁栈 push(CAS)

template<typename T>
class LockFreeStack {
    struct Node { T data; Node* next; Node(const T& d) : data(d), next(nullptr) {} };
    std::atomic<Node*> head{nullptr};

public:
    void push(const T& val) {
        Node* node = new Node(val);
        node->next = head.load(std::memory_order_relaxed);
        while (!head.compare_exchange_weak(
                   node->next, node,
                   std::memory_order_release,
                   std::memory_order_relaxed))
            ;  // CAS 失败则重试
    }
};

compare_exchange_weak vs compare_exchange_strong

  • weak:可能伪失败(spurious failure),性能更好,必须放在循环中
  • strong:保证只有值不匹配时才失败,但某些平台有额外开销

x86 vs ARM 差异

平台内存模型acquire/release 开销
x86强内存模型几乎零开销(只约束编译器)
ARM/RISC-V弱内存模型生成 DMB/DSB 屏障指令,有真实开销

互斥锁(std::mutex)用法详解

#include <mutex>

// 1. 裸 mutex(不推荐直接用)
std::mutex mtx;
void bad_usage() {
    mtx.lock();
    // ... 若中间抛异常,锁永远不会释放!
    mtx.unlock();
}

// 2. lock_guard:最简单 RAII 封装(不可手动解锁)
void good_with_guard() {
    std::lock_guard<std::mutex> lock(mtx);  // 构造时加锁,析构时解锁
    // 临界区代码...
}  // 出作用域自动解锁,异常安全

// 3. unique_lock:可延迟锁定、手动解锁、配合条件变量
void good_with_unique_lock() {
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);  // 延迟加锁
    // ... 做一些无需锁的操作 ...
    lock.lock();       // 显式加锁
    // 临界区...
    lock.unlock();     // 提前解锁
    // ... 无需锁的操作 ...
    lock.lock();       // 再次加锁
}  // 析构时自动解锁

// 4. scoped_lock (C++17):同时锁多个 mutex,避免死锁
std::mutex mtx1, mtx2;
void transfer() {
    std::scoped_lock lock(mtx1, mtx2);  // 原子地锁定两个 mutex
    // 临界区...
}
互斥锁类型特点
std::mutex基础互斥锁,不可递归
std::recursive_mutex同一线程可多次加锁
std::timed_mutex支持 try_lock_for() / try_lock_until()
std::shared_mutex (C++17)读写锁:多读单写
std::lock_guard最简单 RAII,不可手动解锁
std::unique_lock灵活 RAII,可延迟/手动解锁/配合条件变量
std::scoped_lock (C++17)多锁 RAII,死锁避免

线程 vs 进程

特性进程 (Process)线程 (Thread)
定义资源分配的基本单位CPU 调度的基本单位
地址空间独立,进程间默认隔离共享,同一进程内线程共享地址空间
通信方式管道、消息队列、共享内存、socket共享内存(需同步保护)
创建开销大(复制页表、分配资源)小(只需分配栈和寄存器)
切换开销大(切换页表、刷新 TLB)小(同进程只需切换寄存器)
安全性一个崩溃不影响其他进程一个崩溃可能导致整个进程崩溃
数据共享困难,需要 IPC容易,但需同步保护

线程池(Thread Pool)

为什么要用线程池? 线程创建/销毁有开销,频繁创建会降低性能。线程池预先创建一组工作线程,任务到来时分配给空闲线程执行,任务完成后线程不销毁而是等待新任务。

#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>

class ThreadPool {
public:
    explicit ThreadPool(size_t num_threads) : stop_(false) {
        for (size_t i = 0; i < num_threads; ++i) {
            workers_.emplace_back([this] {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(queue_mtx_);
                        // 等待任务或停止信号
                        condition_.wait(lock, [this] {
                            return stop_ || !tasks_.empty();
                        });
                        if (stop_ && tasks_.empty()) return;
                        task = std::move(tasks_.front());
                        tasks_.pop();
                    }
                    task();  // 执行任务(不在锁内)
                }
            });
        }
    }

    // 提交任务,返回 future 以获取结果
    template<typename F, typename... Args>
    auto submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
        using ReturnType = decltype(f(args...));
        auto task = std::make_shared<std::packaged_task<ReturnType()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );
        std::future<ReturnType> result = task->get_future();
        {
            std::lock_guard<std::mutex> lock(queue_mtx_);
            if (stop_) throw std::runtime_error("submit on stopped ThreadPool");
            tasks_.emplace([task] { (*task)(); });
        }
        condition_.notify_one();
        return result;
    }

    ~ThreadPool() {
        {
            std::lock_guard<std::mutex> lock(queue_mtx_);
            stop_ = true;
        }
        condition_.notify_all();
        for (std::thread& worker : workers_) {
            if (worker.joinable()) worker.join();
        }
    }

private:
    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex queue_mtx_;
    std::condition_variable condition_;
    bool stop_;
};

八、设计模式

1. 单例模式(Singleton)

现代 C++ 推荐写法(C++11 局部静态变量线程安全):

class Singleton {
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton& getInstance() {
        static Singleton instance;  // C++11保证线程安全,Meyers Singleton
        return instance;
    }

private:
    Singleton() = default;
};
饿汉 vs 懒汉
方式初始化时机线程安全特点
饿汉类加载时✅ 天生安全空间换时间
懒汉首次调用时需同步保护时间换空间,延迟加载

2. 工厂模式(Factory)

三种工厂对比:

模式说明优点缺点
简单工厂一个工厂类按参数创建产品简单新增产品需改工厂类
工厂方法定义创建接口,子类决定实例化符合开闭原则每增产品需增工厂类
抽象工厂创建一系列相关对象保证产品族一致性扩展产品族困难

现代 C++ 可注册工厂:

class Factory {
public:
    using Creator = std::function<std::unique_ptr<Product>()>;
    
    void registerProduct(const std::string& type, Creator creator) {
        creators_[type] = std::move(creator);
    }
    
    std::unique_ptr<Product> createProduct(const std::string& type) {
        if (auto it = creators_.find(type); it != creators_.end())
            return it->second();
        throw std::runtime_error("Unknown type: " + type);
    }

private:
    std::unordered_map<std::string, Creator> creators_;
};

3. 观察者模式(Observer)

现代 C++ 实现(std::function + std::weak_ptr):

class Subject {
    std::vector<std::weak_ptr<IObserver>> observers_;
public:
    void attach(std::shared_ptr<IObserver> obs) {
        observers_.push_back(obs);
    }
    void notify(const Data& data) {
        for (auto it = observers_.begin(); it != observers_.end(); ) {
            if (auto obs = it->lock()) {
                obs->update(data);
                ++it;
            } else {
                it = observers_.erase(it);  // 自动清理已销毁的观察者
            }
        }
    }
};

SOLID 六大原则

原则核心思想
单一职责(SRP)一个类只负责一项职责
开闭原则(OCP)对扩展开放,对修改封闭
里氏替换(LSP)子类可以替换父类出现
依赖倒置(DIP)依赖抽象而非具体实现
接口隔离(ISP)多个专用接口优于单一接口
迪米特法则(LoD)最少知道原则,降低耦合

九、高频手撕代码题

1. 手写 String 类(必考🔥)

class String {
public:
    String(const char* str = "") {
        if (str == nullptr) str = "";
        len_ = strlen(str);
        data_ = new char[len_ + 1];
        strcpy(data_, str);
    }

    String(const String& other) {           // 拷贝构造
        len_ = other.len_;
        data_ = new char[len_ + 1];
        strcpy(data_, other.data_);
    }

    String(String&& other) noexcept {       // 移动构造
        len_ = other.len_;
        data_ = other.data_;
        other.data_ = nullptr;
        other.len_ = 0;
    }

    String& operator=(const String& other) { // 拷贝赋值
        if (this != &other) {
            delete[] data_;
            len_ = other.len_;
            data_ = new char[len_ + 1];
            strcpy(data_, other.data_);
        }
        return *this;
    }

    String& operator=(String&& other) noexcept { // 移动赋值
        if (this != &other) {
            delete[] data_;
            len_ = other.len_;
            data_ = other.data_;
            other.data_ = nullptr;
            other.len_ = 0;
        }
        return *this;
    }

    ~String() { delete[] data_; }

private:
    char* data_;
    size_t len_;
};

2. 手写简化版 shared_ptr

template<typename T>
class SimpleSharedPtr {
public:
    explicit SimpleSharedPtr(T* ptr = nullptr) 
        : ptr_(ptr), ref_count_(ptr ? new int(1) : nullptr) {}

    SimpleSharedPtr(const SimpleSharedPtr& other)
        : ptr_(other.ptr_), ref_count_(other.ref_count_) {
        if (ref_count_) (*ref_count_)++;
    }

    SimpleSharedPtr& operator=(const SimpleSharedPtr& other) {
        if (this != &other) {
            release();
            ptr_ = other.ptr_;
            ref_count_ = other.ref_count_;
            if (ref_count_) (*ref_count_)++;
        }
        return *this;
    }

    ~SimpleSharedPtr() { release(); }

    T* operator->() const { return ptr_; }
    T& operator*() const { return *ptr_; }
    int use_count() const { return ref_count_ ? *ref_count_ : 0; }

private:
    void release() {
        if (ref_count_ && --(*ref_count_) == 0) {
            delete ptr_;
            delete ref_count_;
        }
        ptr_ = nullptr;
        ref_count_ = nullptr;
    }

    T* ptr_;
    int* ref_count_;
};

3. 线程安全单例(Meyers Singleton)

class Singleton {
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }

private:
    Singleton() = default;
};

4. 生产者-消费者模型

#include <mutex>
#include <condition_variable>
#include <queue>

template<typename T>
class ProducerConsumer {
public:
    void produce(T item) {
        std::unique_lock<std::mutex> lock(mtx_);
        not_full_.wait(lock, [this] { return queue_.size() < capacity_; });
        queue_.push(std::move(item));
        not_empty_.notify_one();
    }

    T consume() {
        std::unique_lock<std::mutex> lock(mtx_);
        not_empty_.wait(lock, [this] { return !queue_.empty(); });
        T item = std::move(queue_.front());
        queue_.pop();
        not_full_.notify_one();
        return item;
    }

private:
    std::queue<T> queue_;
    std::mutex mtx_;
    std::condition_variable not_empty_;
    std::condition_variable not_full_;
    size_t capacity_ = 100;
};

5. 线程安全 LRU 缓存(O(1) get/put)

class LRUCache {
public:
    LRUCache(int capacity) : capacity_(capacity) {}

    int get(int key) {
        std::lock_guard<std::mutex> lock(mtx_);
        auto it = map_.find(key);
        if (it == map_.end()) return -1;
        
        // 移到链表头部(最近使用)
        list_.splice(list_.begin(), list_, it->second);
        return it->second->second;
    }

    void put(int key, int value) {
        std::lock_guard<std::mutex> lock(mtx_);
        auto it = map_.find(key);
        
        if (it != map_.end()) {
            it->second->second = value;
            list_.splice(list_.begin(), list_, it->second);
            return;
        }
        
        if (list_.size() >= capacity_) {
            int old_key = list_.back().first;
            list_.pop_back();
            map_.erase(old_key);
        }
        
        list_.emplace_front(key, value);
        map_[key] = list_.begin();
    }

private:
    int capacity_;
    std::list<std::pair<int, int>> list_;
    std::unordered_map<int, std::list<std::pair<int, int>>::iterator> map_;
    std::mutex mtx_;
};

6. 线程安全环形缓冲区(Ring Buffer)

template<typename T>
class RingBuffer {
public:
    explicit RingBuffer(size_t size)
        : buffer_(size), head_(0), tail_(0), count_(0) {}

    bool push(const T& item) {
        if (count_ == buffer_.size()) return false;  // 满
        buffer_[head_] = item;
        head_ = (head_ + 1) % buffer_.size();
        count_++;
        return true;
    }

    bool pop(T& item) {
        if (count_ == 0) return false;  // 空
        item = buffer_[tail_];
        tail_ = (tail_ + 1) % buffer_.size();
        count_--;
        return true;
    }

private:
    std::vector<T> buffer_;
    std::atomic<size_t> head_;
    std::atomic<size_t> tail_;
    std::atomic<size_t> count_;
};

7. 内存池(简易版本)

class MemoryPool {
public:
    MemoryPool(size_t block_size, size_t block_count)
        : block_size_(block_size) {
        pool_ = ::operator new(block_size * block_count);
        for (size_t i = 0; i < block_count; ++i) {
            void* block = static_cast<char*>(pool_) + i * block_size;
            free_list_.push_back(block);
        }
    }

    void* allocate() {
        if (free_list_.empty()) throw std::bad_alloc();
        void* ptr = free_list_.back();
        free_list_.pop_back();
        return ptr;
    }

    void deallocate(void* ptr) {
        free_list_.push_back(ptr);
    }

    ~MemoryPool() { ::operator delete(pool_); }

private:
    void* pool_;
    size_t block_size_;
    std::vector<void*> free_list_;
};

更多手写题清单

序号题目难度考察点
1手写 String 类⭐⭐⭐拷贝/移动构造、赋值、析构、RAII
2手写 shared_ptr⭐⭐⭐⭐引用计数、控制块、线程安全
3线程安全单例⭐⭐局部静态变量、双重检查锁定
4生产者-消费者⭐⭐⭐条件变量、mutex、队列
5环形缓冲区⭐⭐⭐原子操作、取模索引
6无锁队列(MPMC)⭐⭐⭐⭐⭐CAS、ABA 问题、内存序
7内存池⭐⭐⭐⭐固定大小分配、链表管理
8LRU 缓存⭐⭐⭐list + unordered_map、线程安全
9快速排序(模板版)⭐⭐模板、递归、partition
10线程池⭐⭐⭐⭐future/promise、任务队列、条件变量

十、大厂面试差异

阿里 vs 腾讯 vs 字节

维度阿里巴巴腾讯字节跳动
算法难度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
C++ 基础深度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
操作系统⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
网络编程⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
STL 源码深度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
现代 C++ 新特性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
并发编程⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
设计模式⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

十一、分岗位高频题

后端/高性能服务器方向

  • epoll 原理、ET/LT 模式、Reactor/Proactor 模式
  • 零拷贝(sendfile/mmap/io_uring)
  • C++20 协程调度器设计
  • 手写线程池、内存池
  • TCP 三次握手/四次挥手、TIME_WAIT
  • 分布式锁(Redis/ZK)、雪花算法 ID 生成

嵌入式/物联网方向

  • volatile 深入理解、内存对齐
  • 中断服务程序(ISR)注意事项
  • 为什么嵌入式慎用异常和虚函数(代码体积、确定性)
  • RTOS 任务调度、优先级反转
  • 位操作技巧、寄存器编程
  • static_assert 编译期检查

游戏开发/图形引擎方向

  • 缓存友好性设计、SIMD 优化
  • 面向数据设计(DOD)vs 面向对象设计(OOP)
  • ECS 架构(组件-实体-系统)
  • 渲染管线、矩阵/四元数运算
  • 内存分配策略(栈分配器、池分配器)

高频交易/金融系统方向

  • 纳秒级低延迟优化
  • 缓存行对齐(alignas(64))
  • 分支预测优化(likely/unlikely)
  • NUMA 架构优化
  • 无锁数据结构(RCU、Hazard Pointer)
  • memory_order 深入理解

附录:推荐准备路线

阶段时间内容
第1阶段1-2周精读《Effective C++》,手写 String/shared_ptr/LRU,完成基础题 100 道
第2阶段2-3周按岗位方向专精:后端(网络+并发)、嵌入式(RTOS+驱动)、游戏(图形学)
第3阶段1-2周准备 2 个深度项目,能清晰讲解技术选型和性能优化证据;重点复习 C++20/23 新特性

2025-2026 年趋势总结:C++ 面试呈现 基础深度 + 现代特性 + 岗位专精 三管齐下的趋势。C++20 的 Concepts、协程、Ranges 以及 C++23 的 std::expected 等新特性在字节等大厂面试中权重越来越高,无锁编程、内存模型、协程调度等高并发主题是高级岗位的必备考点。


十二、Lambda 表达式深入

Lambda 表达式在函数中的使用场景

// 1. 作为回调/谓词直接传入 STL 算法
std::vector<int> v = {1, 2, 3, 4, 5};
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

// 2. 捕获局部变量作为回调(替代 std::bind)
int threshold = 10;
auto it = std::find_if(v.begin(), v.end(), [threshold](int x) {
    return x > threshold;  // 捕获外部变量 threshold
});

// 3. 作为异步任务的回调
auto future = std::async(std::launch::async, [&data]() {
    return processData(data);  // 引用捕获,直接操作外部数据
});

// 4. 在类成员函数中使用,捕获 this
class Widget {
    int threshold_ = 5;
public:
    void filter(std::vector<int>& input) {
        input.erase(
            std::remove_if(input.begin(), input.end(),
                [this](int x) { return x < threshold_; }  // 捕获 this 访问成员
            ),
            input.end()
        );
    }
};

Lambda 捕获方式

捕获方式说明对捕获变量的影响
[]不捕获只能访问全局/静态变量
[=]按值捕获所有Lambda 内部有一份拷贝,不影响外部
[&]按引用捕获所有修改会影响外部,注意悬挂引用
[x, &y]x 按值,y 按引用混合捕获
[this]捕获 this 指针可访问类成员
[*this] (C++17)拷贝整个对象Lambda 持有对象的拷贝
[x = std::move(obj)]初始化捕获 (C++14)移动语义捕获

Lambda 本质

编译器将 lambda 展开为一个匿名函数对象(仿函数),捕获列表对应成员变量,operator() 对应函数体。


十三、操作系统基础

虚拟内存 vs 物理内存

特性虚拟内存物理内存
定义每个进程看到的连续地址空间(逻辑概念)实际安装在硬件上的 RAM 条
大小32位系统 4GB,64位可达 256TB有限,如 8GB/16GB
连续性逻辑上连续通过页表映射,物理上可以不连续
隔离性进程间天然隔离(A 进程地址 0x1000 ≠ B 进程地址 0x1000)共享同一物理空间
管理单元页面 (Page),通常 4KB页面帧 (Page Frame)

核心机制——页表映射:

虚拟地址 ──→ [页表] ──→ 物理地址
0x7fff1234    MMU查询   0xa3b41234

页表项包含:
- 物理页帧号
- 有效位(是否在物理内存中)
- 脏位(是否被修改过)
- 访问位(LRU 换出用)

缺页中断(Page Fault):访问的虚拟页不在物理内存时触发,OS 从磁盘(swap)换入到物理内存。

TLB(快表):页表缓存,加速虚拟地址翻译。TLB miss 开销很大(需多次内存访问查页表)。

内存管理理解

堆上内存通过 new/malloc 分配,程序员手动管理生命周期。现代 C++ 通过 RAII + 智能指针将动态内存自动回收。栈上内存编译器自动管理,作用域结束自动释放。

常见问题:

  • 内存泄漏:new 了没 delete → 用智能指针解决
  • 悬垂指针:指向已释放内存的指针 → 释放后置 nullptr
  • 重复释放:对同一地址 delete 两次 → 智能指针的引用计数解决
  • 缓冲区溢出:越界写入 → 用 std::vector、std::string 替代裸数组
  • 内存碎片:频繁分配释放小对象 → 内存池

可重入函数(Reentrant Function)

特性可重入函数不可重入函数
定义被中断后再次调用仍正确重入会导致数据错乱
静态/全局变量不使用使用
动态内存分配不使用非原子的 malloc可能使用
标准库函数strcpy、memcpy(只读输入)strtok(静态缓冲区)
典型场景中断服务程序 (ISR)、信号处理函数普通应用代码
// ❌ 不可重入:使用了静态变量保存状态
char* strtok(char* str, const char* delim) {
    static char* saved;  // 静态变量保存进度,重入时会被覆盖
    // ...
}

// ✅ 可重入版本:状态由调用者传入
char* strtok_r(char* str, const char* delim, char** saveptr) {
    // 状态保存在调用者提供的指针中
}

十四、IO 多路复用

select / poll / epoll 对比

特性selectpollepoll
fd 数量限制默认 1024(FD_SETSIZE)无限制无限制
数据结构fd_set(位图)pollfd 数组红黑树 + 就绪链表
扫描方式每次遍历所有 fd → O(n)每次遍历所有 fd → O(n)只遍历就绪 fd → O(1)
内核态/用户态拷贝每次调用拷贝整个 fd 集合每次调用拷贝整个 pollfd 数组fd 只注册一次,epoll_wait 只返回就绪事件
触发模式水平触发 (LT)水平触发 (LT)LT + ET(边缘触发)
适用场景fd 少、精确时间中等规模大规模并发连接(C10K)

水平触发 (LT) vs 边缘触发 (ET)

特性LT (Level Triggered)ET (Edge Triggered)
通知方式数据未读完下次继续通知只在状态变化时通知一次
编程复杂度低高(需循环读直到 EAGAIN)
惊群问题更容易出现相对少
适用场景简单可靠高性能场景
// epoll 基本流程
int epfd = epoll_create1(0);

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // 边缘触发
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

struct epoll_event events[MAX_EVENTS];
while (running) {
    int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout);
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == listen_fd) {
            // 处理新连接
            int client = accept(listen_fd, ...);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = client;
            epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev);
        } else {
            // 处理客户端数据
            char buf[4096];
            while (true) {  // ET 模式必须循环读完
                int n = read(events[i].data.fd, buf, sizeof(buf));
                if (n <= 0) break;
            }
        }
    }
}

十五、平台与架构

Windows / Linux / ARM / x86 的关系

┌─────────────────────────────────────────────┐
│                应用层(C++ 代码)              │
├─────────────────────────────────────────────┤
│              操作系统 (OS)                    │
│   ┌──────────────┐  ┌──────────────┐         │
│   │   Windows    │  │   Linux      │         │
│   │ (NT 内核)    │  │ (宏内核)      │         │
│   └──────┬───────┘  └──────┬───────┘         │
├──────────┼─────────────────┼─────────────────┤
│          │    指令集架构     │                 │
│   ┌──────┴───────┐  ┌──────┴───────┐         │
│   │    x86-64    │  │  ARM (aarch64)│         │
│   │ (Intel/AMD)  │  │ (Apple M/高通) │         │
│   └──────────────┘  └──────────────┘         │
└─────────────────────────────────────────────┘
维度WindowsLinux
内核类型NT 混合内核宏内核 (Monolithic)
线程模型系统线程为主pthread / std::thread
异步 IOIOCP (IO Completion Port)epoll / io_uring
动态库.dll.so
路径分隔符\/
换行符\r\n\n
ABIMSVC ABI(不兼容跨编译器)Itanium ABI(GCC/Clang 兼容)
维度x86-64ARM (aarch64)
设计哲学CISC(复杂指令集)RISC(精简指令集)
指令编码变长(1-15 字节)定长 4 字节
内存模型强内存模型(TSO)弱内存模型
典型设备桌面/服务器 PC手机、嵌入式、Mac (M1-4)
功耗较高较低

对 C++ 开发的影响:

  • 不同平台/架构的 sizeof(long) 可能不同(x86 Linux 8 字节,Win64 4 字节)
  • 原子操作在 ARM 上有真实内存屏障开销,x86 上 acquire/release 几乎零开销
  • std::thread::native_handle() 返回不同类型(Windows: HANDLE,Linux: pthread_t)

十六、调试与版本控制

GDB 常用命令

命令说明
gdb ./program启动调试
gdb ./program core分析 core dump
run / r运行程序
break file.cpp:42 / b main设置断点
continue / c继续执行
next / n单步执行(不进入函数)
step / s单步执行(进入函数)
finish执行到当前函数返回
print x / p x打印变量值
backtrace / bt查看调用栈
frame N切换到第 N 帧
info locals查看当前帧所有局部变量
info args查看当前帧函数参数
info threads查看所有线程
thread N切换到第 N 号线程
watch x监视变量 x 的变化
list显示源码
disassemble反汇编
quit退出

在 GDB 中查看崩溃点变量状态

# 1. 打开 core dump 文件
gdb ./program core

# 2. 查看崩溃时的调用栈
bt
bt full        # 显示所有帧的局部变量

# 3. 切换到崩溃帧
frame 0        # 0 号帧通常是崩溃点

# 4. 查看所有局部变量
info locals

# 5. 查看函数参数
info args

# 6. 打印具体变量
print var_name
print *ptr          # 解引用指针
print vec.size()    # 可调用 STL 容器方法
print/x var         # 十六进制格式打印

# 7. 查看寄存器状态
info registers

# 8. 查看内存内容
x/16x $rsp          # 查看栈顶 16 个四字节
x/s str_ptr         # 以字符串形式查看内存

# 9. 多线程崩溃分析
info threads        # 列出所有线程
thread apply all bt # 查看所有线程的调用栈

前提:需要有调试符号(编译时加 -g)且 core dump 大小限制未被关闭(ulimit -c unlimited)。

Git 常用操作

回退到之前的 commit
# 方式1:git reset — 修改 HEAD 位置
git reset --soft HEAD~1     # 回退1个提交,改动保留在暂存区
git reset --mixed HEAD~1    # 回退1个提交,改动保留在工作区(默认)
git reset --hard HEAD~1     # 回退1个提交,改动完全丢弃(危险!)

git reset --hard <commit-hash>  # 回退到指定 commit

# 方式2:git revert — 生成新 commit 撤销旧 commit(安全,适合已有 push 的分支)
git revert HEAD              # 创建一个新 commit 来撤销最近一次提交
git revert <commit-hash>     # 撤销指定 commit
操作是否改写历史适用场景
git reset是(本地)本地 commit 未 push
git revert否(新增 commit)已经 push 到远程
创建分支与合并
# 创建分支
git branch feature-xxx           # 创建分支(不切换)
git checkout -b feature-xxx      # 创建并切换到新分支
git switch -c feature-xxx        # 同上,Git 2.23+ 推荐

# 推送分支到远程
git push -u origin feature-xxx

# 合并分支
git checkout main                 # 先切到目标分支
git merge feature-xxx             # 将 feature-xxx 合并到当前分支

# 解决冲突后
git add <冲突文件>
git commit                       # 完成合并

# 变基合并(保持线性历史)
git checkout feature-xxx
git rebase main                   # 将 feature-xxx 的提交放到 main 最新提交之后
git checkout main
git merge feature-xxx             # 此时是 fast-forward,无额外 merge commit
Git 分支管理策略
main / master        ← 生产就绪代码,只通过 PR 合并
  └── develop        ← 开发主线
       ├── feat/xxx  ← 功能分支(短生命周期,合入后删除)
       ├── fix/xxx   ← Bug 修复分支
       └── release/x ← 发布分支

常见分支数量:日常同时活跃 2-5 个分支(1 个开发主线 + 若干功能/修复分支)。遵循"分支宜短命"原则——功能完成后尽快合入主线并删除旧分支。

C++笔试面试题整理 overload:将语义相近的几个函数用同一个名字表示,但是参数列表(参数的类型,个数,顺序不同)不同,这就是函数重载,返回值类型可以不同特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无override:派生类覆盖基类的虚函数,实现接口的重用,返回值类型必须相同特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)overwrite:派生类屏蔽了其同名的基类函数,返回值类型可以不同。 阅读详情

相关推荐

考研复试:cpp面试题整理

C和C++和java的区别? ①c是面向过程的语言,cpp和java是面向对象的语言。这是他们最本质的区别。 ②C和cpp是直接编译运行,java是在虚拟机中运行的。 ③指针的方面,c和c++可以直接用指针来访问内存,java没有指针。 C++里的结构体和类的区别: 结构体默认访问权限全是public 类访问权限是public 、private、protected,默认访问权限是private Stl含义: 标准模板库Standard Template Library是容器和算法的集合 eg:Vector(

baiye 1273

50个C/C++面试题

面试题1:变量的声明和定义有什么区别  为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,但是只在一个地方定义。加入extern修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。  说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。  面试题2:写出bool 、int、 float、

沙漠中的一粒沙 1万+

C++经典面试题100例及答案

答:把一些具有共性的对象归类后形成一个集合,也就是所谓的类。答:这样可以提高编译效率,因为分开的话只需要编译一次生成对应的.obj文件后,再次应用该类的地方,这个类就不会被再次编译,从而大大的提高了编译效率。答:这种函数会自动为内联函数,这种函数在函数调用的地方在编译阶段都会进行代码替换。答:常对象是指在任何场合都不能对其成员的值进行修改的对象。答:所谓的回调函数,就是预先在系统的对函数进行注册,让系统知道这个函数的存在,以后,当某个事件发生时,再调用这个函数对事件进行响应。

usstmiracle的博客 3万+

【附答案】C/C++ 最常见50道面试题

在编程中,变量的声明指的是告知编译器变量的名称和类型,但不分配内存空间。声明可以多次,常见于头文件中,用于模块间的接口声明。使用extern关键字声明的变量,意味着其定义在别处,通常在另一个文件中。相对地,定义则是创建一个具有存储空间的变量实例。定义只能有一次,通常在源文件中,确保为变量分配内存空间。例如,全局变量和局部变量的定义就是分配内存并初始化的过程。

叫好与叫座虽然不是对立面,但想在同一个作品中达到双重效果很难。 1万+

C++面试题集锦(非常详细)

面试题集锦,网上摘抄到一些,自己整理一部分,形成文档。看完面试基本能通过一半。

铭飞MCMS源码部署

文章目录前言一、下载铭飞源码二、部署源码1.使用IDEA打开源码2.配置tomcat环境2.1下载tomcat并测试是否可以成功启动(此处不赘述)2.2在IDEA中安装tomcat环境并配置3.添加数据库,并配置3.1新建数据库3.2 导入sql配置三、启动测试 前言 铭飞MCMS基于IDEA、tomcat和mysql的环境搭建。 提示:以下是本篇文章正文内容,下面案例可供参考 一、下载铭飞源码 gitee:https://gitee.com/mingSoft/MCMS/releases(各种版本按需下

MARS_ZORO的博客 4066

11道C++常见面试题以及答案(2022)

11道C++常见面试题以及参考答案,答案不唯一,仅限参考。

m0_57037805的博客 1万+

原形网络(Prototypical Networks)基于PyTorch的实现

我在Jay2coomzz的基础上修改了数据处理方式和evaluation_model()方法,作为初学者若有不当之处恳请大家批评指正,原文链接如下: https://blog.youkuaiyun.com/weixin_38471579/article/details/102631018 数据集和源码链接将放到评论区 文章目录1 数据集一个简单的数据集介绍数据集处理2 搭建网络CNNProtonets 1 数据集 一个简单的数据集介绍 Omniglot数据集一共包含1623 类手写体,每一类中包含20 个样本。其中.

weixin_43872532的博客 6636

【C++】面试题整理(未完待续)

最近面试题,感觉有些比较基础的好久不用就记不得了,整理一下。题目的答案是个人整理的不能保证是标准答案。

Loup&卡普的博客 885

国网DLT698.45协议——采集系统、数据交换(三)

物理层规定了通信单元或其他终端设备与多功能电表的通信接口,接口的物理与电气特性。常见的物理层传输方式有对于RS485、红外、载波通信等通信信道。将数据包装成可传输的帧格式。1、根据通信配置参数完成端口的初始化配置,通信状态初始化为准备接收状态,初始化相关计数器及缓冲区指针。2、链路层及控制子模块负责通信数据的收发控制,各类超时问题的处理。无通信数据接收时,定时初始化通信端口。3、完成协议帧的解析及应答。

一个bug是bug,多个bug能work 8052

45 - Android系统源码-crosvm 实战:基于 KVM 的 Rust 安全虚拟机监控器核心技术

摘要:crosvm 是 Google 开发的基于 KVM 的轻量级虚拟机监控器(VMM),采用 Rust 语言编写,专为 ChromeOS 和 Android 设计。其核心特点包括: 安全架构:通过 Rust 的内存安全特性和多进程隔离(每个设备运行在独立沙箱进程)大幅降低攻击面 性能优化:基于 KVM 的硬件虚拟化支持,结合异步 I/O (io_uring/epoll) 实现高效设备模拟 模块化设计:清晰的 trait 抽象层(Hypervisor/Vm/Vcpu)支持跨平台扩展 Android 集成

保持专注,保持热爱 226

C++面试题&知识点整理

近期在找工作,面的基本上是C/C++相关岗位,整理了一些网上提到的面试题或者知识点,慢慢补充吧,有错误的地方欢迎指出。 下面整理归纳了面试中常问到的题目,分为5大类: - C++知识点; - 操作系统; - 多线程编程; - 网络; - 算法; - 其他。 1. C++知识点 1.1 构造函数 类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。 构造函数的...

augfun的博客 2414

SDMA 如何实现设备对主机内存的访问

摘要:SDMA(System Direct Memory Access)是PCIe设备直接访问主机内存的核心机制,使外设无需CPU干预即可高效读写系统RAM。其实现原理包括:设备作为主控发起DMA请求,通过PCIe Memory TLP访问主机物理地址;完整通路涉及驱动分配DMA安全内存、设备配置、地址转换(IOMMU)和中断通知等步骤;关键技术涵盖地址类型(HPA/IOVA)、TLP格式、缓存一致性和安全防护机制。SDMA广泛应用于GPU、网卡、NVMe等高性能场景,是现代异构计算的基础技术,需软硬件协同

MHD0815的博客 240

C++ Qt 面试题整理:助你轻松应对技术面试

C++ Qt 面试题整理:助你轻松应对技术面试 去发现同类优质开源项目:https://gitcode.com/ 项目介绍 在技术面试的准备过程中,掌握常见面试题及其答案是至关重要的。为了帮助初中级程序员更好地准备C++和Qt相关的面试,我们推出了这个开源项目——C++ Qt 面试题整理。该项目提供了一个名为C++_Qt面试题整理.txt的资源文件,其中整理了大量关于Qt和基于Qt的C++常见面试...

gitblog_06630的博客 1160

基于Simulink的异步电机参数在线辨识与自适应控制仿真

本文基于Simulink平台,详细介绍了异步电机参数在线辨识与自适应控制系统的仿真实现。针对异步电机运行中参数时变问题,采用模型参考自适应系统(MRAS)实时辨识转子电阻和互感,并结合自适应PI控制策略动态调整控制器参数。通过搭建完整的矢量控制(FOC)仿真模型,验证了系统在稳态辨识、参数摄动和动态响应等工况下的性能:参数辨识误差<1.2%,转速跟踪误差<20rpm,动态响应时间<10ms。文章还提供了详细的Simulink建模步骤、核心代码实现及性能分析,为高级电机控制系统的设计与优化提

amy_mhd的博客 73

【AI 赋能:Python 人工智能应用实战】16. BERT微调实战:中文新闻分类全流程(附模型蒸馏+ONNX部署完整代码)

中文新闻分类是NLP领域的经典实用任务,BERT模型凭借双向上下文建模能力,能有效解决中文歧义与长文本依赖问题,但存在推理速度慢、资源占用高的痛点。本文以THUCNews中文新闻数据集为核心,提供从数据预处理、BERT微调、模型蒸馏到生产部署的全流程实操指南。首先详解THUCNews的多编码读取与清洗(解决gbk/utf-8兼容问题);然后实现BERT微调,加入早停机制与混淆矩阵可视化,验证准确率达93.8%;接着用DistilBERT完成模型蒸馏,在仅损失0.6%准确率的前提下,将推理速度提升2.7倍。

专注于人工智能、软件开发、工控自动化、工厂数字化及智能化等领域,希望和大家共同进步! 1098

【亲测免费】 C++ Qt 面试题整理

C++ Qt 面试题整理 去发现同类优质开源项目:https://gitcode.com/ 资源介绍 本仓库提供了一个名为 C++_Qt面试题整理.txt 的资源文件,该文件整理了一些关于 Qt 和基于 Qt 的 C++ 常见面试题。这些面试题适用于初中级程序员在面试前进行自我检查和准备。 适用对象 正在准备 C++ 和 Qt 相关面试的初中级程序员 希望提升 C++ 和 Qt 编程技能的开发者...

gitblog_06649的博客 520

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

【c++】经典面试题吐血整理

c++ 程序从源程序到生成可执行文件的过程 源程序 编译预处理:处理宏定义,头文件,特殊符号之类的 编译:处理关键字,确认所有指令是否符合语法 优化:删除公共表达式,循环优化之类的,优化之后得到汇编程序 汇编:将汇编程序翻译成目标机器指令 链接:将多个目标文件及所需要的库连接成最终的可执行目标文件 静态链接: 时机:在形成可执行文件之前链接 过程:将函数对应的目标文件要一份副本,将所有的程序模块连接成一个单独的可执行文件 优点:执行速度快 缺点: 浪费空间(同一个目标文件在内存中可能存在多个副本) 更新困难

vector的博客 2万+

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

c++ 常用面试题整理(不定时更新)

面试中常见的C++面试题总结,快来看看,是否对你有帮助!

qq_28928005的博客 1万+

嵌入式面试题 C/C++常见面试题整理_7

不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序错误。因为如果在使用了该头文件的每个C语言文件中定义静态变量,按照编译的步骤,在每个头文件中都会单独存在一个静态变量,从而会引起空间浪费或者程序错误所以,不推荐在头文件中定义任何变量,当然也包括静态变量。

weixin_49303682的博客 1573

【备战秋招】C++音视频开发经典面试题整理

本文摘要围绕音视频编解码技术展开,主要包含以下内容: H.264标准解析:详细介绍H.264的码流结构(VCL/NAL分层)、封装模式(AnnexB/AVCC)及与H.265的区别,包括编码单元扩展至64x64像素等优化。 帧类型与同步机制:阐述I/P/B帧的编解码顺序、音视频同步策略(以音频时钟为基准)及IDR帧的特殊作用。 传输协议对比:分析RTMP(TCP)与RTSP(UDP)的特性差异,提出UDP环境下通过NACK/FEC保证传输质量的方案。 关键技术实现:涵盖YUV格式转换原理、QP值控制方法、

2401_86353562的博客 1825

全面整理的C++面试题

C++面试题1.是不是一个父类写了一个virtual 函数,如果子类覆盖它的函数不加virtual ,也能实现多态?virtual修饰符会被隐形继承的。private 也被集成,只事派生类没有访问权限而已virtual可加可不加子类的空间里有父类的所有变量(static除外)同一个函数只存在一个实体(inline除外)子类覆盖它的函数不加virtual ,也能实现多态。

ljz的专栏 13万+

后端校招 | 高分简历 + 高频 C++ 面试题整理(附GitHub题库推荐)

每道 C++ 高频题都有详细答案和笔记 支持一键搜索你想复习的知识点 包含腾讯、字节、阿里真实面经+题目 github.com/0voice/Campus_recruitment_interview_questions

qq_64978731的博客 1247
上一篇: 一个本地部署的写简历的工具
下一篇: macos上一个好用的PDF文字提取工具方案
小米渣的逆袭
博客等级 码龄17年 62粉丝 101原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值