📋 核心要点
  • 基本语法高频:static 五种语义、指针 vs 引用、四种 cast、智能指针、内存分区、sizeof vs strlen
  • 容器高频:vector/list 底层与选择、map/set 红黑树、迭代器失效、resize vs reserve、二级配置器
  • 类高频:空类六大默认成员、三大特性、虚函数表与多态、深浅拷贝、虚析构、重载/重写/隐藏
  • 编译底层:源文件四阶段(预处理→编译→汇编→链接)、内存六段、malloc 的 brk/mmap、内存泄漏
  • C++11 高频:autonullptr、智能指针、右值引用与移动语义、atomic、初始化列表

基本语法

1. static 关键字的作用

static 出现在不同位置,含义不同:

  • 全局静态变量:加 static 后为全局静态变量。内存位于静态存储区,程序运行期间一直存在;未初始化时自动初始化为 0;作用域限于声明它的文件内(从定义处到文件结尾)。
  • 局部静态变量:加 static 后为局部静态变量。内存仍位于静态存储区,未初始化自动为 0;作用域仍是局部作用域,但离开作用域后不销毁、驻留内存,下次进入函数值保持不变。
  • 静态函数:返回值类型前加 static。默认函数是 extern 的,静态函数只在声明它的文件内可见,不会与其他 cpp 的同名函数冲突。
    • 注意:不要在头文件中声明 static 全局函数;要在多个 cpp 复用的函数,把声明提到头文件里,否则 cpp 内部声明需加 static
  • 类的静态成员:静态成员实现多个对象间的数据共享,且不破坏封装。静态数据成员只存储一处,供所有对象共用;对静态成员的引用不需要对象名。静态成员函数不能直接引用非静态成员,只能引用静态成员(若要引用非静态成员,需通过对象引用)。调用格式:<类名>::<静态成员函数名>(<参数表>)

2. C++ 和 C 的区别

  • 设计上:C 是面向过程的结构化语言;C++ 是 C 的超集,包含面向过程、面向对象、模板化编程。
  • 语法上:C++ 具有封装、继承、多态三大特性;增加了类型安全功能(如强制类型转换);支持泛型编程(模板类、模板函数等)。

3. C++ 中的四种 cast 转换

  • static_cast:用于相关类型间的显式转换(编译期检查)。
  • dynamic_cast:用于继承体系中的安全向下转型(运行时检查,需虚函数支持)。
  • const_cast:去掉或添加 const / volatile 限定。
  • reinterpret_cast:任意指针/整数间的低级位模式转换(最危险,慎用)。

4. C++ 中指针和引用的区别

  • 指针有自己的内存空间,引用只是别名;
  • sizeof 指针大小固定(如 4/8 字节),sizeof 引用返回被引用对象的大小;
  • 指针可初始化为 NULL,引用必须初始化且必须绑定已有对象;
  • 作为参数传递时,指针需解引用才能操作对象,引用直接操作;
  • 可以有 const 指针,但没有 const 引用(引用本身不可变);
  • 指针可指向其它对象,引用一旦绑定不可改变;
  • 指针可多级(**p),引用只有一级;
  • 指针和引用 ++ 语义不同;
  • 返回动态内存分配的对象必须用指针,引用可能引起内存泄漏。

5. C++ 的智能指针

智能指针把普通指针封装为栈对象,栈对象生命周期结束后在析构函数中释放堆内存,从而防止内存泄漏。C++ 提供四种:auto_ptrshared_ptrweak_ptrunique_ptr,后三个是 C++11 引入,auto_ptr 已被弃用。

智能指针语义
auto_ptrC++98 方案,转移所有权,C++11 已弃用
shared_ptr共享所有权,引用计数为 0 时释放
unique_ptr独占所有权,不可拷贝(可移动)
weak_ptr弱引用,监视 shared_ptr,不增加引用计数

参考:shared_ptr / unique_ptr / weak_ptr

6. 数组和指针的区别

指针数组
保存数据的地址保存数据
间接访问数据(先取指针内容再作为地址取值)直接访问数据
通常用于动态数据结构通常用于固定数目且类型相同的元素
通过 malloc 分配、free 释放隐式分配和释放
通常指向匿名数据自身即为数据名

7. 什么是野指针,什么是空指针

野指针是指向已删除对象或未申请、受限内存区域的指针。避免野指针:

  • 指针变量声明时初始化为 NULL
  • 指针被 free/delete 后置为 NULL
  • 指针超出变量作用域前释放地址空间并指向 NULL

8. 智能指针会不会内存泄漏?如何解决?

会。当两个对象相互持有对方的 shared_ptr 成员时,会造成循环引用,引用计数无法归零,导致内存泄漏。

解决:引入 weak_ptrweak_ptr 的构造不修改引用计数,不参与内存管理,但能检测所管理对象是否已释放,从而打破循环引用、避免非法访问。

9. 什么是函数指针、指针函数

  • 函数指针:指向函数的指针变量,一般用于调用函数和做函数参数(如回调函数)。
  • 指针函数:返回值为指针的函数。

10. 什么是指针数组、数组指针

  • 指针数组int *p[n][] 优先级高,先与 p 结合成数组,即含 n 个指针元素的数组;p++ 指向下一个数组元素。
  • 数组指针(行指针)int (*p)[n]() 优先级高,p 先是指针,指向长度为 n 的一维数组;p++ 跨过 n 个整型数据的长度;二维数组的行指针 ++ 指向下一行。

11. 指针常量和常量指针的区别

  • 指针常量int *const p):指针本身不可变,只能定义时初始化,之后不能指向别处。
  • 常量指针const int *p):指向的对象只读,不能通过该指针修改对象值。

指针常量强调「指针本身不可变」,常量指针强调「所指对象不可变」。二者最大用途都是作函数形参,保证实参在被调函数中不被改变。

12. sizeof 和 strlen 的区别

  • sizeof 是操作符,strlen 是库函数;
  • sizeof 参数可以是类型或变量,strlen 只能以 '\0' 结尾的字符串为参数;
  • sizeof 编译期计算结果,strlen 运行时计算;
  • sizeof 计算类型/对象占内存大小,strlen 计算字符串实际长度;
  • 数组做 sizeof 参数不退化,传给 strlen 退化为指针。

13. ++i 和 i++ 的区别

++i 先自增再返回(返回引用),i++ 先返回再自增(返回临时值,多一次拷贝)。在自定义类型中,++i 通常效率更高。

14. extern “C” 的含义

extern "C" 用于让 C++ 正确调用 C 代码。加上后指示编译器按 C 语言(而非 C++)方式编译。因为 C++ 支持函数重载,编译时会把参数类型编入符号名;C 不支持重载,符号名只含函数名。extern "C" 保证两者符号一致,便于 C++ 调用已有的 C 库,一般用于 C++ 调用 C 代码或在 C++ 头文件中。

15. new/delete 和 malloc/free 的区别

  • new/delete 是操作符(可重载),仅 C++ 可用;malloc/free 是函数(可覆盖),C/C++ 均可用;
  • new 会调用构造函数,delete 调用析构函数;malloc/free 只分配/回收内存,不执行构造析构;
  • new/delete 返回具体类型指针,malloc/free 返回 void*

注意malloc 申请的内存用 free 释放,new 申请的用 delete 释放,不要混用。

16. 变量的声明和定义有什么区别

定义分配地址和存储空间,声明不分配地址。一个变量可多处声明,但只能一处定义。加 extern 修饰的是声明,说明该变量在文件外或文件后面定义。

17. #ifdef、#else、#endif 和 #ifndef 的作用

  • #ifdef/#endif 包裹功能模块,向特定用户提供该功能;
  • 调试代码前加标记便于追踪;
  • 应对硬件限制,本地缺乏设备时绕过硬件直接写出预期结果(条件编译)。

18. int、bool、float、指针变量与「零值」比较的 if 语句

C++
// int 与零值比较
if (n == 0)
if (n != 0)

// bool 与零值比较
if (flag)    // 真
if (!flag)   // 假

// float 与零值比较(用允许误差,不能直接用 ==)
const float EPSINON = 0.00001;
if ((x >= -EPSINON) && (x <= EPSINON))

// 指针与零值比较
if (p == NULL)
if (p != NULL)

19. 结构体可以直接赋值吗

可以。声明时可初始化,同一结构体的不同对象之间也可直接赋值。但结构体含指针成员时要小心:多个指针指向同一段内存时,某个指针释放该内存会导致其他指针非法访问,释放前必须确保其他指针不再使用。

20. volatile 有什么作用

volatile 提示编译器该变量可能被外部改变,每次使用时从内存重新读取,不缓存到寄存器。适用场景:

  • 状态寄存器等并行设备的硬件寄存器;
  • 中断服务程序会访问的非自动变量;
  • 多线程间共享的变量。

21. 一个参数可以既是 const 又是 volatile 吗

可以。const + volatile 修饰表示该变量在程序内部只读,只在外部条件变化下改变,且编译器不优化,每次使用都从内存读取而不是读寄存器备份。

注意const 只是不允许程序代码改变变量,在编译期起作用,并未实际禁止某段内存的读写特性。

22. 结构体内存对齐问题

编译器会自动对齐成员变量以提高运算效率。默认按自然对齐条件分配:各成员按声明顺序存储,第一个成员地址与结构体地址相同,向 size 最大的成员对齐。许多系统要求基本类型首地址是某个数 k(通常 4 或 8)的倍数,k 即该类型的对齐模数。

23. 全局变量和局部变量的区别

  • 全局变量整个程序可访问,生存期从程序运行到结束(结束时释放内存);
  • 局部变量只在所在模块(函数)内可访问,模块结束时消失、内存释放;
  • 分配位置不同:全局变量在全局数据段、程序启动时加载;局部变量在栈中分配。

24. C/C++ 程序编译的内存分配情况

程序编译时内存分 5 大存储区:堆区、栈区、全局区、文字常量区、程序代码区。

  • 静态存储区:编译时已分配,整个运行期间存在,速度快、不易出错(全局变量、static 变量、常量字符串)。
  • :函数局部变量在栈上创建,函数结束自动释放;分配运算内置于处理器指令集,效率高,但容量有限(约 2M)。
  • :动态内存分配,运行时用 malloc/new 申请任意大小内存,程序员负责 free/delete 释放;灵活但易泄漏,频繁分配释放不同大小空间会产生堆碎片。

25. typedef 和 define 的区别

  • 用法typedef 定义类型别名;define 定义常量或复杂宏。
  • 执行时间typedef 是编译过程的一部分,有类型检查;define 是预处理,编译前简单字符串替换,无类型检查。
  • 作用域typedef 有作用域限定;define 不受作用域约束。
  • 指针typedefdefine 定义指针时区别很大(define 简单替换易出错)。

注意typedef 是语句,句尾加 ;define 不是语句,句尾不能加 ;

26. strcpy、sprintf 与 memcpy 的区别

  • 操作对象strcpy 两端都是字符串;sprintf 源可以是多种类型、目标是字符串;memcpy 是任意内存地址间的拷贝。
  • 效率memcpy 最高,strcpy 次之,sprintf 最低。
  • 功能strcpy 字符串拷贝;sprintf 格式化输出到字符串;memcpy 内存块拷贝。

27. 解析 (*(void (*)())0)() 的含义

  • void (*)():返回值为 void、无参数的函数指针;
  • (void (*)())0:把 0 强转为上述函数指针;
  • *(void (*)())0:对函数指针解引用得到函数名;
  • (*(void (*)())0)():调用该函数(调用地址 0 处的函数)。

28. C 语言结构体和 C++ 的区别

  • C 结构体不能有函数成员,C++ 类可以;
  • C 结构体成员无 private/public/protected 限定,C++ 类有;
  • C 结构体无继承,C++ 类有继承关系。

类是实现面向对象的基础,结构体可简单理解为类的前身。

29. C++ 拷贝构造形参为什么必须是 const 引用

  • 防止拷贝构造内部修改原始对象;
  • 可接受临时对象或右值;
  • 拷贝构造目的是创建副本,const 符合语义;
  • 若非 const,无法传入 const 对象。

(若形参是值而非引用,会造成无限递归拷贝,因此必须是引用。)

容器和算法

1. 实现一个函数在 main 之前运行

C++
__attribute__((constructor)) void before()
{
    printf("before main\n");
}

2. 谈一下 shared_ptr 的实现

核心是引用计数:拷贝构造、赋值时计数加一,析构时计数减一,当底层指针的引用计数为 0 时才真正释放内存。

3. 同时定义两个函数,一个带 const 一个不带,会有问题吗

不会,这是函数重载(const 成员函数与普通成员函数因 this 指针类型不同而构成重载)。

4. map 和 set 的区别,分别如何实现

map/set/multiset/multimap 底层都是红黑树;epoll、Linux CFS 进程调度也用红黑树。

红黑树特性:每个结点红或黑;根黑;叶结点黑;红结点的两个儿子均黑;任一结点到子孙结点的所有路径包含相同数目的黑结点。

区别

  • map 元素是 key-value 对,set 是 key 的简单集合;
  • set 迭代器是 const,不允许改元素值;map 允许改 value 但不允许改 key(否则破坏有序性,使 iterator 失效);
  • map 支持下标 []set 不支持。map[key] 若 key 不存在会插入默认值元素,故 const map、只查询不插入、无默认值时都慎用,优先用 find

5. STL 中的 allocator

allocator 封装 STL 容器内存管理细节:

  • new 两阶段:调用 ::operator new 配置内存 → 调用构造函数构造对象;
  • delete 两阶段:调用析构函数 → 调用 ::operator delete 释放内存。

STL allocator 将两阶段分离:内存配置 alloc::allocate(),内存释放 alloc::deallocate(),对象构造 ::construct(),对象析构 ::destroy()

为减少小内存碎片,SGI STL 采用两级配置器:>128B 用第一级(malloc/realloc/free);≤128B 用第二级(内存池 + 空闲链表)。

6. STL 可以用迭代器删除元素吗

考察迭代器失效:

  • vector/dequeerase(it) 后其后所有元素迭代器失效,但 erase 返回下一个有效迭代器;
  • map/seterase(it) 后仅当前迭代器失效(红黑树删除不影响下一个),删除前记录下一个迭代器即可;
  • list:内存不连续分配,erase 也返回下一个有效迭代器,两种方法均可用。

7. STL 中 map 的数据存放形式

红黑树。unordered_map 底层是哈希表。

8. STL 的基本组成

容器、迭代器、仿函数、算法、分配器、配接器。关系:分配器给容器分配空间,算法通过迭代器获取容器内容,仿函数协助算法完成操作,配接器适配仿函数。

9. map 和 unordered_map

  • map:元素都是 pair,按 key 自动排序,key 不重复;底层红黑树;适合有序、不重复映射。
  • multimap:同上但 key 可重复。
  • unordered_map:底层哈希表;无序、查找 O(1) 平均;适合无序、高频查找。

10. vector 和 list 的区别

vector:连续存储的动态数组,堆上分配,底层是数组;容量不足时 2 倍扩容(复制旧元素、释放旧空间、迭代器失效)。

  • 访问 O(1);尾插(空间够)快,空间不够需申请+拷贝;中间插入/删除需内存拷贝。
  • 适用:频繁随机访问,不常对非尾节点插入删除。

list:双向链表,堆上分配,每个元素单独分配/释放;随机访问差(只能快速访问头尾),插入/删除常数开销。

  • 适用:频繁插入删除大量数据。

区别:底层(数组 vs 双向链表)、随机访问、内存连续性、中间插入删除是否内存拷贝、扩容策略(vector 一次性分配 2 倍扩容 vs list 每次插入申请)。

11. STL 迭代器的作用,有指针为何还要迭代器

迭代器(Iterator/Cursor 模式)提供顺序访问聚合对象元素的方法,而不暴露内部表示。迭代器不是指针,是类模板,模拟了指针功能(重载 ->*++-- 等),是「指针概念的提升」,能按不同数据结构实现不同的 ++/-- 语义;返回对象引用而非对象值。

产生原因:把不同集合类的访问逻辑抽象出来,不暴露内部结构即可遍历。

12. resize 和 reserve 的区别

  • reserve(n):只扩充容量(capacity 至少到 n),减少多次开辟/释放与拷贝,优化 push_back;不改变有效元素个数(size)。
  • resize(n):改变有效空间大小(size),可指定默认值,capacity 随之改变。

13. vector 的底层原理

动态数组,含三个迭代器:startfinish 之间是已用空间,end_of_storage 是整块连续空间(含备用)尾部。空间不足时申请更大空间(1.5 或 2 倍),拷贝旧数据、释放旧空间。clear() 只清数据、不释放存储空间。任何引起空间重配置的操作都会使原迭代器全部失效。

14. 什么情况用 vector、list、deque

  • vector:随机访问频繁、对象简单、数量变化不大;非尾部插入删除效率低。优先 vector(deque 迭代器复杂)。
  • list:对象大、数量变化频繁、插入删除频繁(写多读少)。
  • deque:需首尾两端插入删除。

15. priority_queue 的底层原理

优先队列,底层用堆实现,队首元素是优先级最高的元素。

16. 为何 map/set 插入删除效率比其他序列容器高

因为不需要内存拷贝和内存移动(红黑树只改结点指针)。

17. 为何 map/set 每次 insert 后迭代器不失效

插入只改变结点指针,结点内存不变;迭代器类似指向结点的指针,内存没变指向也不变。

18. 为何 map/set 不能像 vector 一样有 reserve 预分配

map/set 内部存的是结点(node)而非元素本身,其 Alloc 不是 map<Key, Data, Compare, Alloc> 传入的那个 Alloc。

19. hash_map 与 map 的区别,何时用哪个

  • 构造函数:hash_map 需 hash function 和相等函数,map 需比较函数;
  • 结构:hash_map 底层 hashtable,map 底层红黑树;
  • 查找:hash_map 接近常数级、与数据量无关;map 是 logN 级(但 hash 有哈希函数耗时)。

元素量大、重效率用 hash_map;重内存或元素少用 map

20. 迭代器失效问题

插入

  • vector/string:若内存重分配,iterators/pointers/references 失效;未重分配则插入点之前有效、之后失效;
  • deque:插入点在非 front/back 处失效;front/back 处迭代器失效但 reference/pointer 有效;
  • list/forward_list:全部有效。

删除

  • vector/string:删除点之前有效,off-the-end 总失效;
  • deque:非 front/back 处失效;front/back 处 off-the-end 失效,其他有效;
  • list/forward_list:全部有效;
  • 关联容器 map:被删除元素的迭代器失效,不应再使用。

21. STL 线程不安全的情况

  • 对同一容器多线程读写/写写;
  • 调用容器成员函数期间需锁定容器;
  • 容器返回的迭代器生存期内需锁定;
  • 在容器上调用的算法执行期间需锁定。

类和数据抽象

1. C++ 可以定义引用数据成员吗

可以,必须通过成员初始化列表初始化。

2. 面向对象的三大特性

  • 封装:将客观事物抽象为类,对数据和方法实行访问控制(private/protected/public);
  • 继承:实现继承(复用基类属性和方法)、可视继承(子窗体用父窗体外观)、接口继承(仅用接口,实现滞后到子类);
  • 多态:父类对象可表现为不同子对象特性,按赋值给它的子对象以不同方式运作。

3. C++ 空类有哪些成员函数

默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值运算符、默认取址运算符、默认取址运算符 const。后两者常被忽略,但也是默认函数;且只有实际使用时编译器才定义。

4. 拷贝构造函数和赋值运算符

  • 拷贝构造生成新的类对象,赋值运算符不能;
  • 拷贝构造直接构造新对象,无需检查源对象是否相同;赋值需检查自赋值,且原对象有内存分配时要先释放。

注意:类含指针成员时,必须重写拷贝构造和赋值运算符,不要用默认的。

5. C++ 析构函数的作用

对象生命周期结束时系统自动调用析构函数。函数名与类名相同、前加 ~,不能带参、无返回值、不能重载、只能一个。未自定义时编译器合成默认析构函数(自定义后也总合成,且执行时先调自定义再调合成)。

类析构顺序:派生类析构 → 对象成员析构 → 基类析构。

6. 静态函数和虚函数的区别

静态函数编译期确定运行时机;虚函数运行期动态绑定(虚函数表机制,调用时增加一次内存开销)。

7. 什么是重载和覆盖(重写)

  • 重载:函数名相同、参数列表不同(个数/类型),返回值类型无要求,在同一作用域中;
  • 重写(覆盖):子类重定义父类的虚函数,参数列表相同。

8. 虚函数和多态

多态分静态多态(重载,编译期确定)和动态多态(虚函数机制,运行期绑定)。父类指针指向子类对象并调用子类重写的虚函数时,调用子类版本。

实现:含虚函数的类对象开头是虚函数表指针,指向存放虚函数地址的虚函数表(虚函数在 .text 代码段)。子类继承虚函数表,重写虚函数时替换表中的地址。使用虚函数增加内存开销、降低效率。

9. const 修饰成员函数的目的

表明该函数不修改对象。确认不修改对象就应加 const,这样 const 对象和普通对象都能调用。

10. 虚函数表如何实现运行时多态

子类重写父类虚函数时,虚函数表中该函数地址被替换;含虚函数的对象模型头部存放指向虚函数表的指针,由此实现多态。

11. C++ 类成员的访问权限

public/protected/private 控制访问权限。类内部三者可互相访问、无限制;类外部只能通过对象访问 public 成员。

12. C++ 中 struct 和 class 的区别

  • 默认继承权限不同:class 默认 private 继承,struct 默认 public 继承;
  • class 可作模板参数关键字,struct 不能;
  • 保留 struct 是为了与 C 向下兼容。

13. 虚函数指针的初始化过程

  • 类自身定义虚函数或基类有虚函数时,编译器添加虚函数指针 vptr,一般放在对象内存布局第一个位置;
  • vptr 地址与对象地址一致,指向虚函数表地址;表中 slot 存各虚函数地址;
  • 运行时根据对象类型初始化 vptr,使其正确指向所属类的虚表。

对象初始化顺序:无虚基类时,按继承声明顺序调基类构造 → 按成员声明顺序初始化成员对象 → 执行子类构造函数体;有虚基类时,先按定义位置从上到下、从左到右初始化所有虚基类子对象。

14. 类的静态变量如何初始化

类内声明、类外定义并初始化。静态成员编译期存放于静态数据区,定义在编译期完成,必须在类外定义。

15. 深拷贝和浅拷贝

  • 浅拷贝:未定义拷贝构造时编译器自动生成,只拷贝指针指向(指针成员只复制地址),会导致多次释放同一内存的问题;
  • 深拷贝:自己写拷贝构造,把原对象指针指向的内容拷贝到新申请的内存。

函数参数为对象、函数返回对象都会调用拷贝构造。浅拷贝的核心问题是析构多次释放堆内存,可用 std::shared_ptr 解决。

16. 析构函数可以为虚函数吗,为什么

析构函数必须是虚函数(当类会被继承时),但 C++ 默认析构函数不是虚函数。将可能被继承的父类析构设为虚函数,可保证用基类指针释放子类对象时能释放子类空间、防止内存泄漏。默认非虚是因为虚函数需要额外虚表指针、占内存;不会被继承的类设为虚析构是浪费。

17. 类成员函数的重写、重载和隐藏

  • 重写 vs 重载:范围(重写跨两个类,重载在同一类)、参数(重写参数列表相同,重载不同)、virtual(重写的基类函数须 virtual,重载可有无);
  • 隐藏 vs 重载:隐藏跨类,参数可同可不同但函数名同;参数不同时无论基类是否 virtual 都是隐藏而非重写。

重载是静态绑定多态,覆盖(重写)是动态绑定多态。

泛型编程

1. 什么是模板

模板是 C++ 泛型编程的基础,把类型参数化,使函数或类能处理任意类型。分为函数模板和类模板:

C++
// 函数模板
template <typename T>
T max(T a, T b) { return a > b ? a : b; }

// 类模板
template <typename T>
class Stack {
    std::vector<T> elems;
public:
    void push(const T& e) { elems.push_back(e); }
};

2. typename 和 class 的区别

模板参数声明中 typenameclass 等价。但 typename 还用于显式指明「依赖类型」——依赖模板参数的类型名必须加 typename,否则编译器可能误认为是静态成员。

C++
template <typename T>
void foo() {
    typename T::value_type x;  // 必须加 typename
}

3. 什么是模板特化

模板特化为特定类型提供专门实现,分全特化和偏特化:

C++
// 全特化
template <>
class Stack<bool> { /* 用位存储 */ };

// 偏特化(部分特化参数)
template <typename T>
class Stack<T*> { /* 指针版本 */ };

编译和底层

1. 如何对 STL 进行内存优化

STL 内存管理用二级配置器:

  • 第一级配置器:用 malloc/free/realloc 执行实际内存操作,分配 >128 字节;失败时调用指定函数释放部分内存,再失败抛异常。
  • 第二级配置器:管理 ≤128 字节,用内存池 + 16 个空闲链表(管理 8、16、24…128 字节的块),避免碎片和小额区块的额外负担。空闲链表节点用联合体复用:既表示下一空闲块地址,也表示用户数据块地址。
    • allocate:>128B 走第一级;否则查对应空闲链表,有块直接取,无块调 refill 重填;
    • deallocate:>128B 走第一级;否则按大小插入对应空闲链表;
    • refill:空闲链表无块时从内存池取(默认 20 块,不足则有多少取多少),内存池耗尽则向堆申请。

2. C++ 函数栈空间最大值是多少

默认约 1M(Linux 下可用 ulimit -s 查看/调整),可调整。

3. 说说你了解的 RTTI

运行时类型识别(Run-Time Type Identification),C++ 层面主要体现为 dynamic_casttypeid。MSVC 中虚函数表 -1 位置存放指向 type_info 的指针;对含虚函数的类型,typeiddynamic_cast 都查询 type_info

4. C++ 源文件从文本到可执行文件的过程

  • 预处理:处理文件包含、宏定义,生成预编译文件;
  • 编译:预编译文件转汇编代码,生成汇编文件;
  • 汇编:汇编代码转机器码,生成可重定位目标文件;
  • 链接:多个目标文件及库连接成可执行文件。

5. include 头文件顺序和双引号/尖括号的区别

头文件顺序:若 a.h 声明了 b.h 中定义的变量,则在 a.c 中应先引用 b.h、后引用 a.h,否则报变量未声明错误。

双引号 vs 尖括号:查找路径顺序不同。双引号 "" 先查当前目录,再查 -I 指定路径,再查系统变量 CPLUS_INCLUDE_PATH/C_INCLUDE_PATH;尖括号 <> 只查后两者(不查当前目录)。

6. malloc 的原理,brk 和 mmap 系统调用作用

malloc 采用内存池方式:先申请大块内存作为堆区,再分为多个内存块,以块为基本单位。用隐式链表组织连续块(含已分配/未分配),用显式链表(双向链表)管理空闲块;合并时用边界标记法判断前后块是否已分配。

申请内存时,<128K 用 brk 在堆区分配(移动堆顶指针);>128K 用 mmap 在映射区分配。

7. C++ 内存是如何管理的

虚拟内存分六部分:

  • 代码段:只读存储区(字符串常量)+ 文本区(机器代码);
  • 数据段:已初始化的全局变量和静态变量;
  • BSS 段:未初始化的全局/静态变量(局部+全局)及初始化为 0 的全局/静态变量;
  • 堆区new/malloc 动态分配,需 delete/free 释放;
  • 映射区:动态链接库及 mmap 文件映射;
  • :函数返回地址、参数、局部变量、返回值。

8. 什么是 memory leak,如何判断和处理

内存泄漏指程序未能释放不再使用的内存(失去对该段内存的控制)。分类:堆内存泄漏(malloc/newfree/delete)、系统资源泄漏(Bitmap/handle/SOCKET 等未释放)、基类析构非虚导致子类资源未释放。

判断:Linux 用 Valgrind;或在代码中添加内存申请/释放统计,对比是否一致。处理:用 Valgrind、mtrace 检测。

9. 什么时候发生段错误

访问非法内存地址时,常见:

  • 使用野指针;
  • 试图修改字符串常量的内容。

10. 什么是 reactor 模型

Reactor(反应器)多线程模型:基于事件驱动 + I/O 复用,单线程事件循环监听多个连接的事件并分发处理。

11. 如何用单线程处理高并发

单线程模型中,用 I/O 复用(select/poll/epoll)提高单线程处理多个请求的能力,再配合事件驱动模型、基于异步回调处理事件。

12. C++ 内存分配情况(32 位)

32bit CPU 可寻址 4G 线性空间,每个进程独立 4G 逻辑地址:03G 用户态,34G 内核空间。逻辑地址划分:

  • 静态区域:text 段(代码段,含只读存储区+文本区)、data 段(已初始化全局/静态变量)、bss 段(未初始化及初始化为 0 的全局/静态变量,main 前统一清零);
  • 动态区域:heap(调用 malloc 时分配,从低到高增长,起始地址 start_brk、结束 brk)、映射区(动态链接库、大内存 mmap)、stack(返回地址/参数/局部变量/返回值,从高到低增长,最大栈大小可 ulimit 指定)。

C++11

1. C++11 有哪些新特性

  • auto:根据初始值自动推导类型(不能用于函数传参和数组类型推导);
  • nullptr:可转任意指针类型;NULL 一般宏定义为 0,重载时可能出问题;
  • 智能指针shared_ptrweak_ptr 等;
  • 初始化列表std::initializer_list 初始化类;
  • 右值引用:实现移动语义和完美转发,消除不必要的对象拷贝;
  • atomic:原子操作用于多线程互斥;
  • 新增 STL 容器 arraytuple
  • 另有 lambdaoverride/finalconstexprusing 别名、thread 等。

2. 什么是右值引用,和左值有什么区别

右值引用(T&&)是 C++11 新特性,实现转移语义(移动语义)和完美转发,目的:

  1. 消除对象交互时不必要的拷贝,提高效率;
  2. 更简洁明确地定义泛型函数。
  • 左值:能取地址、具名、表达式结束后依然存在的持久对象;
  • 右值:不能取地址、匿名、表达式结束后即销毁的临时对象。

区别:左值能寻址、能赋值、可变;右值不能寻址、不能赋值、不可变(基础类型;用户自定义类型的右值引用可通过成员函数改变)。