1. 三种多态的实现方式及其区别

静态多态和动态多态:函数重载、运算符重载、泛型编程与模板(特别的CRTP)(编译期多态);虚函数 动态多态

2. STL 容器在 for 循环中使用 erase 删除元素会导致什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 失效,后续 ++it 未定义行为
}
}

// 正确方式1:利用返回值
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it % 2 == 0) {
it = vec.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}

// 正确方式2:remove-erase惯用法
vec.erase(std::remove_if(vec.begin(), vec.end(),
[](int x) { return x % 2 == 0; }),
vec.end());

3. 什么是Perfec t Forwarding?为什么要使用它,它解决了什么问题?写一下

在函数模板中,保持参数的原有类型和特性(const)进行转发。没有完美转发时,泛型编程会遇到参数类别丢失的问题。传入右值时,T会被推导成非右值,const属性也会消失;

1
2
3
4
5
6
template<typename T>
void wrapper(T arg)
{
callee(arg);
callee(std::forward<T>(arg));
}

4. 完美转发和移动语义的实践场景

日志函数、装饰器模式、容器构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
void log_and_call(T arg) {
std::cout << "aaa";
call(std::forward<T>(arg));
}

// 比如我的项目中需要实现一个基于 new 的转发功能
template <typename T, typename... Args>
T* cxl_new(Args&&... args) {
void* raw = hmalloc(sizeof(T));
if (!raw) throw std::bad_alloc();
return new (raw) T(std::forward<Args>(args)...);
}

5. 虚函数表的存储位置和内存结构

vtable 只读数据段、vptr对象内部

6. 单继承、多继承、虚继承的vptr是怎样的?对应的C++内存模型是怎样的

单继承:对象内通常只有一个 vptr,指向该类的 vtable;基类子对象在对象起始位置,内存布局相对简单。

多继承:对象内包含多个基类子对象,每个含虚函数的基类子对象各自有一个 vptr;指针向上转型时需要调整 this 指针偏移。

虚继承:为共享虚基类子对象,对象内增加 vbptr 或相关偏移信息(实现相关);虚基类子对象位置通常不固定,需要通过表或偏移计算定位。

7. 虚函数能否是模板函数?为什么?

虚函数不能是模板函数。虚函数表要求在编译器固定布局,模板函数实例化在编译器按需生成。时机冲突

8. 析构函数声明为虚函数的原因是什么?

当一个派生类对象通过 基类指针 删除时,如果基类的析构函数 不是虚的,将只会调用基类析构函数,而派生类部分不会被析构,造成 资源泄漏未定义行为

9. C++类默认生成的成员函数有哪些?

默认构造、拷贝、析构、移动、拷贝赋值与移动赋值

10. 左值和右值的区别与应用场景

左值有可识别的存储位置、可取地址;右值通常是临时值、表达式结果或将亡值。

应用:

  • 左值用于持久对象传参、可多次使用的对象。
  • 右值用于移动语义与资源转移,避免深拷贝。
  • 通过重载 T&T&& 实现复制与移动的区分。

11. Name Mangling的作用及其带来的兼容性问题

作用:将函数名、参数类型、命名空间等编码到符号名中,实现重载和模板实例的链接。

问题:不同编译器/版本的编码规则不同,导致 ABI 不兼容;跨语言调用需用 extern "C" 关闭改名。

12. 动态库和静态库的优缺点对比

静态库:

  • 优点:发布单一可执行文件,部署简单,运行时无额外依赖。
  • 缺点:可执行文件体积大;库更新需要重新链接。

动态库:

  • 优点:复用共享,体积小,可独立更新。
  • 缺点:部署依赖复杂;加载/符号解析有额外开销;ABI 兼容要求高。

13. 什么是智能指针?什么是RAII?既然如此,全部用栈区不就可以了,为什么要堆区? 或者全部用堆区可以了,为什么要栈区?

堆适用 大小不确定,并且可以利用零散碎片,栈不行;栈适合函数,天生紧凑,堆不行;两者协作处理会使得内存更加紧凑高效。

工厂模式的一个优点:唯一化管理,比如LLVM中的Type类型管理

14. vector有什么缺点,为什么要有pvector?或者问移动构造的适用场景(如gapbs)

vector 缺点:

  • 扩容需要搬迁元素,可能触发大量移动/拷贝。
  • 迭代器/引用在扩容后失效。
  • 频繁小对象插入删除成本高。

pvector 常用于“指针向量”或“稳定地址”需求:

  • 保存指针或小对象句柄,扩容时仅移动指针,避免大对象搬迁。
  • 或用自定义分配器/分段存储,提高稳定性与局部性。

移动构造适用场景:

  • 临时对象返回值优化不足时的资源转移。
  • 容器扩容或重排时把资源从旧对象“搬走”,降低深拷贝成本。

15. 【打卡题】const与指针和引用

常见组合:

  • const int* p / int const* p:指向常量的指针,不能改 *p,可改 p。
  • int* const p:指针常量,不能改 p,可改 *p。
  • const int* const p:指向常量的常量指针。
  • const int& r:可绑定临时对象,延长生命周期,但不可修改所指对象。

16. 【打卡题】宏定义 defineconst 常量

  • 宏是预处理期文本替换,无类型检查,调试困难,易产生副作用。
  • const 有类型和作用域,受编译器检查,可被调试器识别;通常优先用 const/constexpr

17. new 和malloc的区别

  • new:分配内存并调用构造函数;失败抛 std::bad_alloc
  • malloc:仅分配原始内存;失败返回 nullptr
  • delete/delete[]free 配套,不可混用。
  • new 支持重载和自定义分配器。

18. atomic原子操作为什么比读写锁更快?

1.硬件指令实现
atomic原子操作是硬件级别的,直接在CPU指令和缓存层完成,会通过特定的硬件指令直接锁CPU的缓存行和内存总线,确保其他核心无法访问同一块内存。

而读写锁则是软件级别的,需要内核系统调用实现,基于互斥锁机制实现,线程无法获取锁时会通过系统调用将自己放入等待队列,涉及用户态内核态切换,导致比成本比硬件实现高出几个数量级。

19. vector和dequeue的区别,为什么遍历vector的效率更高。

vector 连续内存,deque 分段连续(块数组 + 小块)。

遍历 vector 更快的原因:

  • 连续内存更利于缓存命中和预取。
  • 迭代器计算简单;deque 需要跨块索引。

20. 怎样实现简化版本的智能指针?

核心要点:RAII + 禁止拷贝 + 支持移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template <typename T>
class UniquePtr {
public:
explicit UniquePtr(T* p = nullptr) : ptr_(p) {}
~UniquePtr() { delete ptr_; }

UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;

UniquePtr(UniquePtr&& other) noexcept : ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete ptr_;
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}

T* get() const { return ptr_; }
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }

private:
T* ptr_;
};

21. 多态是的原理?C++内存对象模型。

原理:通过 vptr 指向 vtable,在运行期根据对象实际类型分发虚函数调用。

对象模型(简化):

  • 对象内含数据成员 + vptr。
  • vtable 是函数指针数组(含 RTTI 指针等实现细节)。
  • 基类指针指向派生类对象时,vptr 指向派生类 vtable。

22. 能否允许在构造函数中调用虚函数?会生效吗

语法允许,但构造/析构期间不会发生动态分派:调用的是当前构造层级的实现。
原因:对象尚未构造完整,vptr 指向当前类的 vtable,派生类部分未初始化。

23. 内存对齐规则

常见规则:

  • 结构体成员按自身对齐要求对齐。
  • 结构体整体对齐为成员最大对齐值。
  • 末尾可能填充以满足整体对齐。
  • 可用 alignas/alignof 查询或指定对齐。

24.空类几个字节?为什么

通常为 1 字节,保证不同对象具有不同地址;若有继承或空基类优化(EBO),可能不占额外空间。

25.常量指针和指针常量是什么?this指针是什么指针?

  • 常量指针:const T* p,指向常量,不能改 *p。
  • 指针常量:T* const p,指针值不可改。
  • this 是指向当前对象的指针,非静态成员函数隐式参数;在 const 成员函数中类型为 const T* const

26.explicit 的作用

防止构造函数或转换运算符发生隐式类型转换,避免意外构造。

1
struct X { explicit X(int) {} };

27.extern 的作用

声明外部链接符号,告诉编译器该符号定义在别处;多文件共享全局变量或函数。与 extern "C" 配合可关闭 C++ 名字改编。

28.避免虚函数调用的开销?

CRTP(奇异递归模板模式):在编译时进行静态多态,避免虚函数表(vtable)查询和间接调用开销。

final 关键字:如果类不会被继承,将类或虚函数标记为 final,允许编译器去虚拟化(devirtualization),直接调用函数。

手工查表分发:用函数指针数组或 switch-case 替代虚函数,虽然灵活性稍低,但开销更确定。

29.Python与C++交互时,用过pybind11或ctypes吗?遇到过GIL问题吗?

pybind11:现代首选。基于C++11,自动处理类型转换,可轻松暴露C++类、函数、枚举,甚至支持NumPy数组的零拷贝访问。
ctypes:用于调用C库,需手动编写外部函数接口,较繁琐。
Cython:适合有大量数值计算且需要接近C性能的场景。

** GIL(全局解释器锁):**
现象:Python中多线程执行CPU密集型C++代码时,因为GIL的存在,同一时刻只有一个线程能执行Python字节码,导致无法利用多核。
解决:在C++函数中调用 py::gil_scoped_release 临时释放GIL,执行完耗时计算后再用 py::gil_scoped_acquire 重新获取。注意:释放GIL期间不能操作任何Python对象。
替代方案:使用 std::thread + pybind11::call_guardpy::gil_scoped_release,让C++线程完全脱离GIL运行。

30.实现一个内存池,如何减少碎片并提升算子执行效率?

设计要点:

  • 分级空闲链表(size class),按块大小分桶,减少外部碎片。
  • 大块用 slab/arena 分配,避免频繁系统调用。
  • 线程本地缓存(tcache)降低锁竞争。
  • 对齐与批量回收,提高缓存友好性。
  • 对热点算子预分配,复用内存,减少峰值抖动。

本站由 Zane Jiang 使用 Stellar 1.33.1 主题创建,一款很棒的 Hexo 主题!

总访问 次 || 本页访问
总访客 人 || 本页访客