- 基本语法高频:
static五种语义、指针 vs 引用、四种 cast、智能指针、内存分区、sizeofvsstrlen - 容器高频:vector/list 底层与选择、map/set 红黑树、迭代器失效、
resizevsreserve、二级配置器 - 类高频:空类六大默认成员、三大特性、虚函数表与多态、深浅拷贝、虚析构、重载/重写/隐藏
- 编译底层:源文件四阶段(预处理→编译→汇编→链接)、内存六段、
malloc的 brk/mmap、内存泄漏 - C++11 高频:
auto、nullptr、智能指针、右值引用与移动语义、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_ptr、shared_ptr、weak_ptr、unique_ptr,后三个是 C++11 引入,auto_ptr 已被弃用。
| 智能指针 | 语义 |
|---|---|
auto_ptr | C++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_ptr。weak_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 语句
// 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不受作用域约束。 - 指针:
typedef和define定义指针时区别很大(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 之前运行
__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/deque:erase(it)后其后所有元素迭代器失效,但erase返回下一个有效迭代器;map/set:erase(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 的底层原理
动态数组,含三个迭代器:start 与 finish 之间是已用空间,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++ 泛型编程的基础,把类型参数化,使函数或类能处理任意类型。分为函数模板和类模板:
// 函数模板
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 的区别
模板参数声明中 typename 和 class 等价。但 typename 还用于显式指明「依赖类型」——依赖模板参数的类型名必须加 typename,否则编译器可能误认为是静态成员。
template <typename T>
void foo() {
typename T::value_type x; // 必须加 typename
}3. 什么是模板特化
模板特化为特定类型提供专门实现,分全特化和偏特化:
// 全特化
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_cast 和 typeid。MSVC 中虚函数表 -1 位置存放指向 type_info 的指针;对含虚函数的类型,typeid 和 dynamic_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/new 未 free/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_ptr、weak_ptr等; - 初始化列表:
std::initializer_list初始化类; - 右值引用:实现移动语义和完美转发,消除不必要的对象拷贝;
- atomic:原子操作用于多线程互斥;
- 新增 STL 容器
array、tuple; - 另有
lambda、override/final、constexpr、using别名、thread等。
2. 什么是右值引用,和左值有什么区别
右值引用(T&&)是 C++11 新特性,实现转移语义(移动语义)和完美转发,目的:
- 消除对象交互时不必要的拷贝,提高效率;
- 更简洁明确地定义泛型函数。
- 左值:能取地址、具名、表达式结束后依然存在的持久对象;
- 右值:不能取地址、匿名、表达式结束后即销毁的临时对象。
区别:左值能寻址、能赋值、可变;右值不能寻址、不能赋值、不可变(基础类型;用户自定义类型的右值引用可通过成员函数改变)。