服务端6月1日 10:35
C++ 指针和引用有什么区别?各自适用什么场景?指针是一个变量,存储另一个变量的内存地址,本身占用内存;引用是已存在变量的别名,与被引用对象共享同一内存地址。指针声明后可不初始化,也可以置为nullptr;引用必须在声明时绑定到一个对象,且不存在"空引用"。指针可以重新指向其他对象,引用一旦绑定就无法更改绑定目标——对引用赋值实际是修改所引用对象的值。指针支持算术运算(加减偏移、指针减法算距离),引用不支持任何算术操作。作为函数参数时,指针传递显式取地址,调用处能看出可能修改实参;引用传递语法上像值传递,但实际可修改实参,语义更隐蔽。返回值方面,返回指针要考虑空指针风险,返回引用要保证所引对象生命周期超出函数作用域,返回局部变量的引用是未定义行为。
## 追问
### 引用在底层是怎么实现的?
编译器通常用指针实现引用,即引用变量底层也是一个地址。但C++标准不要求这样实现,只要语义正确即可。所以sizeof(引用)等于所引类型的大小,而非指针大小。
### 什么时候用指针,什么时候用引用?
函数参数优先用引用——语义更清晰,不存在空值问题。需要"可能不指向任何对象"或"需要重新指向"时用指针。C++惯用法中,函数参数用const引用避免拷贝,返回值用指针表达"可能失败"。
### const引用绑定到临时对象会发生什么?
const引用可绑定到右值,编译器会延长临时对象的生命周期至引用的生命周期结束。这是C++中少有的生命周期延长规则,非常量引用不允许绑定到右值。
### 引用能实现多态吗?
可以。基类引用引用派生类对象,通过引用调用虚函数同样走vtable动态绑定,和指针多态行为一致。标签
C++
C++ 是一种通用的、静态类型的编程语言,它具有高效性、灵活性和可移植性等特点。C++ 基于 C 语言,同时支持面向对象编程和泛型编程,可以用于开发各种类型的应用程序,如系统软件、游戏、桌面应用程序、移动应用程序等。 C++ 的主要特点包括: 高效性:C++ 是一种编译型语言,可以生成高效的本地代码,在性能要求高的应用程序中得到广泛应用; 面向对象编程:C++ 支持面向对象编程,包括封装、继承、多态等特性,使得开发人员可以更加灵活和高效地构建复杂的软件系统; 泛型编程:C++ 支持泛型编程,包括模板和泛型算法等特性,使得开发人员可以编写可重用的代码和算法; 可移植性:C++ 可以在多种平台和操作系统上运行,具有很高的可移植性; 标准化:C++ 有一个国际标准,称为 C++ 标准,规范了语言的语法、语义和库函数等方面,使得 C++ 的代码更加规范和可靠。 C++ 作为一种通用的编程语言,可以用于多种应用场景。在系统软件开发中,C++ 可以用于操作系统内核、驱动程序、网络协议栈等方面;在游戏开发中,C++ 可以用于游戏引擎、物理引擎、图形渲染等方面;在桌面应用程序和移动应用程序开发中,C++ 可以用于开发各种类型的应用程序,如音频和视频编辑、图像处理、数据库管理等方面。 如果您想要成为一名优秀的程序员,C++ 是一个非常有用的编程语言,它具有广泛的应用场景和丰富的编程资源,可以帮助您更加高效和灵活地解决实际问题。

服务端6月1日 10:35
C++ 虚函数底层怎么实现?vtable/vptr 与动态绑定原理C++虚函数通过虚函数表(vtable)和虚指针(vptr)实现运行时多态。每个含有虚函数的类,编译器会为其生成一个vtable——一个静态数组,按声明顺序存放该类所有虚函数的函数指针。每个对象实例在内存布局的起始位置持有一个vptr,指向所属类的vtable。调用虚函数时,程序通过vptr找到vtable,再从表中取出对应槽位的函数指针进行间接调用,这就实现了动态绑定。单继承下对象只有一个vptr,多继承下会有多个vptr,分别指向不同基类的vtable。虚函数调用的开销主要是一次额外的间接寻址,现代CPU分支预测能大部分消除这个代价。纯虚函数在vtable中通常放入一个抛出异常的纯虚调用占位函数,防止误调。析构函数声明为虚函数是必要的——否则通过基类指针delete派生类对象时,只会调用基类析构函数,派生类资源泄漏。
## 追问
### vtable存放在内存的哪个区域?
vtable是编译期生成的只读数据,通常放在.rodata段,同一类的所有对象共享同一个vtable。
### 构造函数可以是虚函数吗?
不行。构造时对象类型尚未完整,vptr还未指向最终类的vtable。构造函数执行期间,vptr逐步从基类向派生类更新,此时虚函数机制尚未就绪。
### 虚函数调用和普通函数调用性能差距大吗?
单次间接寻址开销约1-2个CPU周期,现代分支预测器命中率极高,实际差距可忽略。真正影响性能的是虚函数阻碍了编译器内联优化。
### 为什么析构函数必须虚函数?
通过基类指针delete派生类对象时,若析构非虚,只调用基类析构函数,派生类析构不执行,资源泄漏。这是C++最常见的内存泄漏来源之一。服务端6月1日 10:30
C++ 智能指针怎么选?unique_ptr/shared_ptr/weak_ptr 使用场景对比C++ 提供三种智能指针,均位于 `<memory>` 头文件。`unique_ptr` 独占所有权,不可拷贝只能移动,开销接近裸指针,是默认首选。`shared_ptr` 共享所有权,内部维护引用计数,最后一个持有者销毁时释放资源,拷贝和赋值增加计数。`weak_ptr` 是 `shared_ptr` 的观察者,不增加引用计数,通过 `lock()` 获取临时 `shared_ptr` 访问对象,用于打破循环引用。`make_unique` 和 `make_shared` 在单次分配中同时构造对象和控制块(后者),比单独 new 更高效且异常安全。自定义删除器允许在析构时执行特定操作,如 `unique_ptr<FILE, decltype(&fclose)>` 或传入 lambda 关闭文件句柄、释放 C 库资源。选择原则:独占用 `unique_ptr`,共享用 `shared_ptr`,观察用 `weak_ptr`,尽量避免混用裸指针持有所有权。
## 追问
### make_shared 比 shared_ptr(new T) 好在哪?
`make_shared` 一次分配同时容纳对象和控制块,减少一次内存分配,且提高缓存局部性。但会延迟对象内存释放:即使引用计数归零,若 `weak_ptr` 仍存在,对象内存不会回收,直到 `weak_ptr` 也销毁。
### unique_ptr 的自定义删除器为什么比 shared_ptr 更重?
`unique_ptr` 的删除器类型是模板参数的一部分,影响指针类型本身,可能导致不同的 `unique_ptr` 类型不兼容。`shared_ptr` 的删除器类型在运行时擦除,不影响 `shared_ptr<T>` 类型,使用更灵活。
### 智能指针线程安全吗?
控制块(引用计数)的修改是原子的,`shared_ptr` 拷贝/析构线程安全。但访问所指对象不是线程安全的,多线程读写对象本身仍需加锁。`unique_ptr` 整体非线程安全,移动操作需外部同步。
### 什么时候仍需要裸指针?
不涉及所有权时用裸指针或引用作为观察者,如函数参数传递、遍历容器元素。裸指针不管理生命周期,只是访问地址,比 `weak_ptr` 更轻量。服务端6月1日 10:30
C++ 内存泄漏怎么避免?RAII/智能指针/检测工具实战指南C++ 程序的内存分为栈(自动管理,函数结束释放)、堆(手动 new/delete)、全局/静态区(程序生命周期)和常量区(只读)。堆内存需开发者显式申请和释放,是内存泄漏的重灾区。泄漏的常见原因:new 后忘记 delete、异常导致跳过 delete、指针被覆写丢失地址。检测工具中,Valgrind 在运行时插桩检查,AddressSanitizer(ASan)由编译器插桩,开销更小、报错更直接。现代 C++ 的核心防御手段是 RAII 原则:资源获取即初始化,将资源生命周期绑定到栈对象,析构时自动释放。智能指针是 RAII 的典型应用:`unique_ptr` 独占所有权离开作用域自动释放,`shared_ptr` 引用计数归零释放,基本取代裸 new/delete。常见陷阱包括:悬空指针(访问已释放内存)、双重 delete(同一指针释放两次)、`delete` 配 `new[]` 或 `delete[]` 配 `new` 导致未定义行为。
## 追问
### 栈和堆的分配速度差异为何这么大?
栈分配只需移动栈顶指针,相当于一次整数加法。堆分配需遍历空闲链表查找合适块,可能触发系统调用(brk/mmap),还涉及内存碎片整理。
### AddressSanitizer 和 Valgrind 怎么选?
开发阶段优先 ASan:编译时加 `-fsanitize=address`,运行开销约 2x,报错精确到源码行。Valgrind 无需重编译,但开销 10-20x,适合测试第三方二进制或无法重编译的场景。
### 循环引用导致 shared_ptr 泄漏怎么办?
将其中一方改为 `weak_ptr`,它不增加引用计数,打破循环。访问时通过 `lock()` 提升为 `shared_ptr`,若对象已释放则返回空。
### RAII 只适用于内存吗?
不是。文件句柄、锁(lock_guard)、网络连接、数据库连接等有限资源都适用 RAII。核心思想是资源生命周期与对象绑定,析构函数负责释放。服务端6月1日 10:30
C++ 模板特化和偏特化怎么用?全特化/偏特化/SFINAE 详解模板特化允许为特定类型提供与通用版本不同的实现。全特化指定所有模板参数,为某一具体类型提供完全定制版本,全特化后的模板不再有模板参数。偏特化仅适用于类模板和变量模板,对部分参数做约束,保留剩余参数的通用性。例如 `template<typename T> class A<T*>` 为所有指针类型提供统一实现,T 仍为参数。函数模板不支持偏特化,只能用重载或全特化替代。SFINAE(替换失败并非错误)是模板元编程的基础规则:在模板实参替换阶段,若某个候选的替换导致类型错误,该候选被静默剔除而非编译报错。`std::enable_if` 利用 SFINAE,通过条件编译控制模板是否参与重载决议,是 C++17 之前约束模板的主要手段。
## 追问
### 全特化和偏特化的实例化优先级如何?
编译器优先选择最特化的版本。全特化比偏特化更特化,偏特化比通用模板更特化。若两个偏特化同等特化则二义报错。
### 函数模板为什么不能偏特化?
函数模板支持重载,重载已能实现类似效果。若允许偏特化,与重载决议的交互会带来复杂歧义,标准因此禁止。
### enable_if 的 ::type 在条件为 false 时是什么?
不存在。`enable_if<false, T>` 没有嵌套 `type` 类型,导致替换失败触发 SFINAE,该模板被剔除。C++14 提供了 `enable_if_t` 别名简化写法。
### C++17 的 if constexpr 能替代 SFINAE 吗?
部分场景可以。`if constexpr` 在编译期根据条件丢弃分支代码,比 SFINAE 更直观。但它无法控制函数签名层面的重载决议,仍需 SFINAE 或 C++20 concepts。服务端6月1日 10:30
C++ 移动语义和完美转发怎么用?std::move/forward 原理详解C++11 引入移动语义解决不必要的深拷贝问题。核心在于区分左值(可取地址、具名)和右值(临时、不可取地址)。右值引用 `T&&` 可绑定到右值,表示该对象资源可被"窃取"而非复制。移动构造函数和移动赋值运算符通过转移资源(如指针、文件句柄)而非复制,显著提升性能。`std::move` 本质是无条件转为右值引用的 static_cast,并不移动任何东西,只是让重载决议选择移动版本。完美转发指函数模板将参数以原始值类别(左值/右值)传递给另一函数,借助 `std::forward<T>` 实现。`T&&` 在模板推导中是万能引用:实参为左值时 T 推导为左值引用,实参为右值时 T 推导为非引用类型,配合 `std::forward<T>` 按原始类别转发。引用折叠规则决定了最终类型:只有 `T&& &&` 折叠为 `T&&`,其余均折叠为 `T&`。
## 追问
### std::move 之后原对象还能用吗?
可以,但处于"有效但未指定"状态。可安全赋值或析构,不应读取其值。移动后应将原对象置空或重置,保持语义清晰。
### 万能引用和右值引用怎么区分?
看 T 是否需要推导:`template<typename T> void f(T&& x)` 中 T&& 是万能引用;`void f(std::string&& x)` 中是右值引用。auto&& 也是万能引用。
### 完美转发中为什么必须用 std::forward 而非 std::move?
`std::move` 总是转为右值,会错误地将左值参数也转为右值。`std::forward<T>` 仅当 T 为非引用类型时才转为右值,保留原始值类别。
### 引用折叠的四种情况是什么?
`T& & → T&`、`T& && → T&`、`T&& & → T&`、`T&& && → T&&`。只有右值引用到右值引用折叠为右值引用,其余均折叠为左值引用。服务端6月1日 10:30
C++ 多态怎么实现?vtable/vptr 与动态绑定原理详解C++ 多态分编译时和运行时两种。编译时多态通过函数重载和模板实现,编译器根据参数类型或模板实参在编译期确定调用目标,零运行开销。运行时多态依赖虚函数机制:每个含虚函数的类拥有一张虚函数表(vtable),存储各虚函数的入口地址;每个对象实例持有指向所属类 vtable 的虚指针(vptr)。调用虚函数时,通过 vptr 查 vtable 完成动态绑定,实现了根据实际对象类型调用对应版本。纯虚函数使类成为抽象类,强制子类必须实现。析构函数声明为虚函数可确保 delete 基类指针时正确调用派生类析构,避免资源泄漏。多重继承下对象可能包含多个 vptr,分别指向不同基类的 vtable。
## 追问
### vtable 存在哪里?每个对象都有独立的 vtable 吗?
vtable 通常在编译期生成,存于只读数据段。同一类的所有对象共享同一张 vtable,各对象只存一个 vptr 指向它。
### 构造函数可以是虚函数吗?
不能。构造时对象类型尚未完整,vptr 还未初始化完成,虚函数机制尚未就绪,无法进行动态绑定。
### 虚函数调用一定有运行开销吗?
大多数情况有一次间接寻址开销。但编译器若能在调用点确定实际类型(如通过 final 类或显式限定),会进行去虚化(devirtualization)优化为直接调用。
### 多重继承下 vptr 怎么布局?
对象按继承顺序排列各基类子对象,每个有虚函数的基类子对象各有一个 vptr。通过不同基类指针访问时,指针值可能需要偏移调整(this adjustment)。服务端6月1日 09:46
C++ 多线程怎么写?mutex/条件变量/atomic/future 详解C++11 起 <thread> <mutex> <condition_variable> <atomic> <future> 是并发编程的核心。std::thread 创建线程,必须 join() 或 detach(),否则析构时 terminate。共享数据用 std::mutex 保护,lock_guard 在作用域自动加解锁,unique_lock 支持手动加解锁和配合条件变量。condition_variable 解决生产者-消费者问题:wait 释放锁并阻塞,notify_one/all 唤醒等待线程,注意用谓词版本 wait(lock, pred) 防虚假唤醒。std::atomic 无锁原子操作,支持 fetch_add、compare_exchange_weak(CAS),可选内存顺序。std::async 启动异步任务返回 future,get() 阻塞等结果。死锁避免:固定加锁顺序或用 std::lock 同时加多个锁。
## 追问
### lock_guard 和 unique_lock 有什么区别?
lock_guard 构造加锁析构解锁,不可手动控制,轻量。unique_lock 同样 RAII 但支持手动 lock/unlock、defer_lock 延迟加锁、配合 condition_variable 的 wait。只用简单的临界区保护用 lock_guard,需要条件变量或灵活控制用 unique_lock。
### 条件变量为什么会有虚假唤醒?
POSIX 标准允许条件变量在没有 notify 的情况下唤醒,这是实现上的妥协。所以必须用 while 循环或谓词版本 wait(lock, pred) 检查条件,不能假设被唤醒就代表条件成立。
### memory_order_relaxed 和 memory_order_seq_cst 有什么区别?
relaxed 只保证原子操作本身的原子性,不保证操作间的顺序可见性,性能最好但容易出 bug。seq_cst 是默认顺序,全局顺序一致,所有线程看到相同的操作顺序。绝大多数情况用默认 seq_cst,只有确认安全的性能热点才降级到 acquire/release 或 relaxed。
### std::async 和直接创建 thread 有什么区别?
async 返回 future,能拿到返回值和异常;thread 拿不到返回值,异常会导致 terminate。async 的 launch::policy:async 强制新线程,deferred 延迟到 get() 时在当前线程执行。默认策略由实现决定,要保证异步就传 launch::async。服务端6月1日 09:46
C++ STL 容器怎么选?vector/map/unordered_map 对比与算法STL 容器分四类。序列容器:vector 连续内存随机访问 O(1) 尾插 O(1) 中间插 O(n);deque 双端队列两端 O(1);list 双向链表任意位置 O(1) 但不支持随机访问。关联容器:map/set 基于红黑树有序 O(log n),map 存键值对 set 存键。无序容器:unordered_map/unordered_set 基于哈希表平均 O(1) 最坏 O(n)。容器适配器:stack(默认 deque)、queue(默认 deque)、priority_queue(默认 vector+堆算法)。常用算法:sort O(nlogn)、find O(n)、copy、transform、accumulate。选容器先看需求:随机访问选 vector,频繁头插选 deque,频繁中间插选 list,查找为主选 unordered_map/set,需要有序选 map/set。
## 追问
### vector 扩容机制是什么?怎么避免?
vector 满了重新分配更大内存(通常翻倍),把旧元素拷贝/移动过去,再释放旧内存。这会导致迭代器失效和性能抖动。提前 reserve(预估大小) 可避免运行时扩容,shrink_to_fit 可释放多余容量。
### map 和 unordered_map 怎么选?
需要键有序遍历、范围查询用 map。只做查找/插入/删除不关心顺序用 unordered_map,平均 O(1) 比 O(log n) 快。unordered_map 的坑:最坏 O(n)(哈希冲突严重时)、自定义类型要手写 hash 函数和相等判断。
### 什么操作会使迭代器失效?
vector:insert/erase 当前位置及之后失效,reserve 后全部失效。list:只有被删除位置的失效,其他不受影响。map/set:只有被删除元素的失效。遍历中删除元素要用 it = container.erase(it) 模式。
### emplace_back 和 push_back 有什么区别?
push_back 先构造临时对象再移动/拷贝进容器。emplace_back 直接在容器内存中原地构造参数,省去一次移动。对于简单类型差别不大,对复杂对象 emplace_back 更高效。服务端6月1日 09:46
C++ 异常处理怎么用?try/catch/noexcept 与异常安全详解C++ 异常用 try/catch/throw 处理运行时错误。throw 抛出异常对象,catch 按类型匹配捕获,catch(...) 捕获所有异常。标准异常继承自 std::exception,分 logic_error(编程错误)和 runtime_error(运行时错误)两大分支。自定义异常继承 std::runtime_error 或 std::exception,重写 what() 返回错误信息。异常安全三个级别:基本保证(异常后对象仍有效)、强保证(异常后状态回滚)、不抛保证(noexcept)。RAII 是异常安全的基石——栈展开时局部对象的析构函数保证执行,智能指针和 lock_guard 就是典型应用。析构函数绝不能抛异常,否则栈展开期间会调 terminate。noexcept 告诉编译器函数不抛异常,vector 扩容时会根据移动构造是否 noexcept 决定用移动还是拷贝。
## 追问
### 析构函数为什么不能抛异常?
栈展开时如果析构函数再抛异常,C++ 运行时无法同时处理两个异常,直接调 std::terminate 程序崩溃。如果析构中调用的函数可能抛异常,用 try/catch 在析构函数内部吞掉。
### catch 的匹配顺序是怎样的?
按出现顺序从上到下匹配,匹配第一个符合条件的 catch 就执行,不会找"最佳匹配"。所以要把最具体的异常类型放前面,std::exception 放后面,catch(...) 放最后。
### noexcept 函数里抛了异常会怎样?
运行时调 std::terminate 直接结束程序,不会栈展开。所以给函数加 noexcept 要确认真的不会抛,或者内部全部 catch 了。但 noexcept 让编译器优化更激进,移动构造加 noexcept 后 vector 扩容会用移动而非拷贝,性能更好。
### 异常和错误码怎么选?
异常用于不可预期的错误(内存分配失败、文件打不开),错误码用于可预期的分支逻辑(查找失败、无效输入)。异常的缺点是有性能开销(栈展开、异常对象构造),且跨动态库边界可能出问题。性能敏感路径用 error_code 或 optional。服务端6月1日 09:46
C++11 到 C++20 新特性有哪些?面试高频考点速览C++ 每个版本都有高频考点。C++11 最重要:auto 类型推导、range-for 循环、lambda 表达式(捕获列表是重点)、智能指针三件套(unique_ptr 独占、shared_ptr 共享引用计数、weak_ptr 打破循环)、右值引用与移动语义(避免深拷贝)、nullptr 替代 NULL、constexpr 编译期计算。C++14 补充了泛型 lambda(参数用 auto)和 make_unique。C++17 重点:结构化绑定、if constexpr 编译期分支、optional/variant/any 三种可选类型、string_view 避免字符串拷贝。C++20 大更新:concepts 约束模板参数、ranges 组合式数据处理、协程(co_yield/co_await)、modules 替代头文件、三路比较运算符 <=>。
## 追问
### unique_ptr 和 shared_ptr 的主要区别是什么?
unique_ptr 独占所有权,不可拷贝只能移动,零开销。shared_ptr 引用计数共享所有权,拷贝时计数加一,有原子操作开销。优先用 unique_ptr,需要共享时才用 shared_ptr,weak_ptr 解决 shared_ptr 循环引用。
### 移动语义解决了什么问题?
右值引用(T&&)绑定到临时对象,移动构造/赋值直接"窃取"资源指针而非深拷贝。典型场景:函数返回大对象、vector 扩容时移动而非拷贝元素。std::move 本身不移动,只是把左值转成右值引用。
### lambda 捕获列表 [=] 和 [&] 有什么坑?
[=] 按值捕获,若捕获 this 指针,实际捕获的是指针而非对象副本,对象析构后 this 悬空。[&] 按引用捕获,引用的局部变量出了作用域就失效。异步回调中用这两种捕获极易出 bug,推荐显式列出要捕获的变量。
### C++20 concepts 相比 SFINAE 有什么优势?
concepts 用 requires 子句或 concept 名直接约束模板参数,编译错误信息可读,报错位置准确。SFINAE 用 enable_if,报错信息是一堆模板展开的乱码,调试困难。concepts 还能作为类型标注放在参数位置,代码更清晰。服务端6月1日 09:46
C++ 常用设计模式怎么实现?单例/工厂/观察者/策略C++ 设计模式面试常考的是单例、工厂、观察者和策略这四个。单例用 Meyer's Singleton(局部静态变量),线程安全且简洁。工厂模式分简单工厂和抽象工厂,核心是用虚函数延迟到子类决定创建哪种对象。观察者模式在事件系统中到处都是,注意生命周期管理。策略模式用组合替代继承,运行时切换算法。
## 追问
### 单例模式的线程安全怎么保证?
Meyer's Singleton(函数内 static 局部变量)C++11 起保证线程安全,编译器自动加锁。不要用双重检查锁定(DCLP),早期 C++ 下有指令重排问题,现代 C++ 直接用 `static` 就行。
### 抽象工厂和简单工厂有什么区别?
简单工厂一个类通过参数决定创建哪种产品,违反开闭原则。抽象工厂定义接口,每个子类创建一族产品,新增产品族加子类即可,但新增产品类型要改所有工厂。
### 观察者模式有什么坑?
观察者持有主题的引用或指针,主题析构时观察者可能收到通知访问野指针。解注册要在观察者析构时做,推荐用 weak_ptr 打破循环引用。
### 策略模式和模板方法模式怎么选?
策略是组合,运行时通过指针/引用切换算法,灵活但有虚函数开销。模板方法是继承,编译期确定算法骨架,子类只重写部分步骤。需要运行时切换用策略,算法骨架固定用模板方法。服务端6月1日 09:35
C++ 性能怎么优化?编译器/缓存/并发三层面实战指南C++ 性能优化的核心原则是"先测量,再优化"。编译器层面,`-O2` 是发布标配,`-O3` 会启用更激进的自动向量化和循环展开但可能引入浮点精度变化;`-flto` 链接时优化让编译器跨翻译单元内联,`-march=native` 生成针对当前 CPU 的指令,非同构集群部署时禁用。内存层面,缓存命中率是第一瓶颈——CPU 从 L1 读数据约 1ns,从主存读约 100ns,差两个数量级;数据结构要按访问模式紧凑排列,避免伪共享(多线程各自原子递增同一缓存行的不同变量,每次都要跨核同步缓存行)。算法层面,选对容器比微优化重要百倍:`vector` 连续内存缓存友好,`unordered_map` 查找 O(1) 但每个桶都是堆分配的链表节点,随机访存对缓存不友好。并发层面,`atomic` 比 `mutex` 轻量但要选对 `memory_order`,无锁不等于高性能——CAS 自旋在竞争激烈时比互斥锁更慢。
## 追问
### -O2 和 -O3 实际差多少?
`-O3` 相比 `-O2` 额外开启循环向量化、循环内条件外提等优化。数值密集型计算在 `-O3` 下可能快 10-30%,但 `-O3` 会把 `memcpy` 语义的 struct 拷贝优化成逐成员赋值,可能破坏 `volatile` 语义;浮点运算会重排顺序导致精度差异。建议默认 `-O2`,对确定的热点模块单独开 `-O3`,配合 benchmark 验证。
### 缓存友好的数据布局具体怎么做?
核心思路:把经常一起访问的字段放在同一条缓存行(64 字节)。SoA(Structure of Arrays)——`float x[N], y[N], z[N]`,比 AoS(Array of Structures)缓存利用率更高。避免 `vector<bool>`,它用位压缩存储,比 `vector<char>` 慢 5-10 倍。伪共享问题用 `alignas(64)` 对齐解决。
### 移动语义什么时候不生效?
几种情况:对象没有移动构造函数;移动构造函数被隐式删除(类里有 `const` 或引用成员);返回值优化(RVO)已经省去了拷贝/移动,此时 `return std::move(local)` 反而阻止了 RVO——直接 `return local` 让编译器做消除更好。
### atomic 的 memory_order 怎么选?
默认 `memory_order_seq_cst` 最强但开销最大。`acquire`(读)+ `release`(写)是最常用的放松组合,适合生产者-消费者模型。`relaxed` 只保证原子性不保证顺序,适合计数器,绝对不能用来做同步标志。
### 性能分析工具怎么用?
`perf record -g` 采集调用栈,`perf report` 看热点函数。`perf stat` 看 IPC,IPC < 1 说明 CPU 在等内存,优化方向是缓存友好。`valgrind --tool=cachegrind` 模拟缓存行为。先看总耗时排名,再看 call count 判断单次调用是否才是瓶颈。服务端6月1日 09:35
C++ 网络编程怎么实现?socket/epoll/多路复用详解C++ 网络编程的核心是套接字(socket)加 I/O 多路复用。TCP 服务端流程:`socket()` 创建监听套接字 → `bind()` 绑定地址端口 → `listen()` 开始监听 → `accept()` 取出已连接套接字 → `recv()/send()` 读写数据。UDP 不需要 `listen/accept`,直接 `recvfrom()/sendto()` 收发数据报。当并发连接数上来后,需要 I/O 多路复用:`select` 用位图监控 fd 集合,每次调用都要重新构建,有 1024 个 fd 的上限;`poll` 用数组替代位图,没有数量限制,但仍然是 O(n) 遍历;`epoll` 是 Linux 专属方案,内核维护就绪队列,`epoll_wait` 只返回就绪的 fd,O(1) 复杂度,配合 `EPOLLET` 边缘触发模式可以进一步减少系统调用次数。生产环境几乎都选 `epoll`(Linux)或 `kqueue`(macOS/FreeBSD)。
## 追问
### select、poll、epoll 各自适用什么场景?
`select` 的优势是跨平台,连接数少于 100 且短连接为主时够用。`poll` 去掉了 1024 限制,但和 `select` 一样每次调用都要把全部 fd 从用户态拷贝到内核态,fd 数量到几千时性能明显下降。`epoll` 适合 C10K 甚至 C100K 场景:`epoll_ctl` 只在 fd 增删时调用一次,`epoll_wait` 只拷贝就绪 fd,不遍历全部。但 epoll 是 Linux 专有 API,跨平台项目用 Boost.Asio 或 libuv 封装层更好。
### LT(水平触发)和 ET(边缘触发)有什么区别?
LT 模式下,只要 socket 上有数据没读完,`epoll_wait` 每次都会返回该 fd——你可以每次只读一部分,下次继续。ET 模式只在 socket 状态变化时通知一次,如果没读完数据,下次 `epoll_wait` 不会再返回,必须循环 `recv` 直到 `EAGAIN`。ET 模式效率更高(减少 `epoll_wait` 调用次数),但编程难度大:必须设非阻塞 + 循环读到底,漏读就会丢数据。Nginx 用 ET 模式,Redis 用 LT 模式。
### 非阻塞 I/O 为什么要配合多路复用?
单独设 `O_NONBLOCK` 后,`recv` 没数据时立刻返回 `EAGAIN` 而不是阻塞等待。但你不该用忙轮询来等数据——这会 100% 占满 CPU。多路复用的作用就是替你等待:`epoll_wait` 阻塞直到某个 fd 有数据可读,然后你调一次 `recv` 就能拿到数据。非阻塞 + epoll 的组合意义在于:epoll 通知你数据到了,但 `recv` 可能只读了一部分,非阻塞模式下你可以安全地循环读,不会因为没有更多数据而卡住。
### 线程池模型怎么设计比较合理?
常见模型:主线程 epoll_wait 检测到新连接,把已连接 fd 分发给工作线程处理。更好的方案是 One Loop Per Thread(muduo 模型):每个线程各自跑一个 epoll 循环,主线程 accept 后通过 `eventfd` 唤醒某个工作线程注册该 fd,线程间无共享 fd 集合,扩展性更好。线程数一般设为 CPU 核心数,I/O 密集型可以翻倍。服务端6月1日 09:35
C++ 智能指针怎么用?unique_ptr/shared_ptr/weak_ptr 区别与陷阱C++ 智能指针通过 RAII 原则自动管理堆内存生命周期:对象在构造时获取资源,析构时释放资源,即使异常抛出也能保证析构函数执行。`unique_ptr` 表达独占所有权,不可拷贝只能移动,零开销抽象,性能等价于裸指针;`shared_ptr` 表达共享所有权,内部维护引用计数,最后一个 `shared_ptr` 析构时释放资源,引用计数操作是原子的所以线程安全,但指向对象本身的并发访问需要额外同步;`weak_ptr` 是 `shared_ptr` 的弱引用观察者,不增加引用计数,用来打破循环引用和实现缓存/观察者模式。优先用 `make_unique`/`make_shared` 创建智能指针:`make_shared` 一次分配同时构造对象和控制块,减少内存碎片且异常安全;直接 `new` 传给 `shared_ptr` 构造函数则分两次分配,且在函数参数求值顺序未定义时可能泄漏。
## 追问
### unique_ptr 和 shared_ptr 性能差异有多大?
`unique_ptr` 零开销,析构直接 `delete`,编译后和裸指针一样。`shared_ptr` 的开销来自三处:一是控制块(强引用计数 + 弱引用计数)占 16 字节;二是拷贝/析构时原子操作引用计数,每次 `fetch_add`/`fetch_sub` 约 20-50ns;三是 `make_shared` 合并分配导致对象和控制块在同一内存块,即使所有 `shared_ptr` 释放了,只要还有 `weak_ptr` 持有控制块,对象占的内存就不会归还——这叫"对象悬挂"问题。高频场景下 `shared_ptr` 明显慢于 `unique_ptr`,但绝大多数业务代码中差异可以忽略。
### 什么时候用 weak_ptr?不只是循环引用?
最常见的场景确实是打破循环引用:双向链表 `next` 用 `shared_ptr`、`prev` 用 `weak_ptr`;观察者模式中 Subject 持有 `vector<weak_ptr<Observer>>`,通知时先 `lock()` 再调用,已销毁的 Observer 自动跳过。但 `weak_ptr` 还有两个实用场景:缓存系统用 `weak_ptr` 持有缓存对象,内存紧张时对象可以被释放,下次访问时 `lock()` 返回空再重新加载;工厂函数返回 `weak_ptr` 给调用者,工厂内部用 `shared_ptr` 管理生命周期,调用者通过 `lock()` 检查对象是否还活着。
### enable_shared_from_this 是怎么回事?
当一个对象已经被 `shared_ptr` 管理,在成员函数里需要把自己作为 `shared_ptr` 传出去时,必须用 `enable_shared_from_this`。直接 `shared_ptr<this>(this)` 会创建第二个控制块,引用计数从 1 开始,导致对象被 delete 两次。典型场景:异步回调里捕获 `shared_from_this()` 延长对象生命周期,确保回调执行时对象还没被销毁。前提是调用 `shared_from_this()` 时对象已经被 `shared_ptr` 管理——构造函数里调会抛 `bad_weak_ptr` 异常。
### 自定义删除器怎么用?
`unique_ptr` 的删除器是模板参数的一部分,类型不同就不能互相赋值。`shared_ptr` 的删除器是运行时参数,构造时传入就行,类型相同可以放在同一容器里。实际场景:管理 `FILE*` 用 `fclose` 作删除器、管理 POSIX 文件描述符用 `close`、管理 `sqlite3*` 用 `sqlite3_close`。