C/C++八股文

发布于:2025-06-11 ⋅ 阅读:(24) ⋅ 点赞:(0)
一.指针和引用的区别
1.指针和引用的定义是什么?

指针是一个变量,其值是另一个变量的地址。声明指针时用*符号。

引用是一个别名,它是在已存在的变量上创建的。引用在声明的时候用&符号

2.指针和引用在汇编层面上的表现是什么 ?

在汇编层面上,指针和引用本质上都是通过存储目标对象的地址实现间接访问。

引用通常被编译器认为是指向对象的指针,两者的底层表现几乎是一致的。

 无论是指针还是引用,访问其数据的时候,都会取出存储的地址,再访问对应的内存内容

 我通过gdb调试工具,调试指针和引用的时候,发现无论是指针还是引用,gdb显示的地址和内容都是基本一致的,进一步说明了它们底层是没有本质区别的。

 二.const关键字
1. const关键字的作用是什么?

const 用于限定变量的值不可被修改,可以修饰变量、指针、成员函数、函数参数和返回值等,还可以避免多次内存分配。

2. const int *pint *const p 和 const int *const p 有什么区别? 

const int *p:指向常量的指针,不能通过 p 修改指向的值,但可以改变 p 的指向。

int *const p:常量指针,p 的指向不能变,但可以通过 p 修改指向的值。

const int *const p:指向常量的常量指针,既不能修改指向的值,也不能改变指向。

3. 为什么要用 const 修饰函数参数和返回值? 

防止函数内部修改参数值,提高代码安全性和可读性。

对返回值加 const 可以防止调用者修改返回对象(如返回引用或指针时)。

4. const 成员函数的作用是什么? 

const 成员函数保证不会修改类的成员变量(除 mutable 修饰的成员外),只能调用其他 const 成员函数。

5.const 和 #define 有什么区别? 

const 有类型检查,作用于编译阶段;

#define 是预处理器宏替换,没有类型检查,仅做简单文本替换。

三.static关键字 
1. static 关键字的作用有哪些?

修饰局部变量:使变量在函数调用结束后依然保留其值(静态存储期)。

修饰全局变量/函数:限制变量或函数的作用域只在当前文件(内部链接)。

修饰类成员:使成员变量或成员函数属于类本身,而不是某个对象。

 2. static 修饰的局部变量和普通局部变量有什么区别?

普通局部变量每次进入作用域都会重新创建,离开作用域销毁。

static 局部变量只初始化一次,生命周期贯穿程序始终,离开作用域不销毁。

3. static 修饰的全局变量和函数有什么作用? 

使全局变量或函数只在当前源文件内可见,不能被其他文件访问,实现“文件私有”。

 4. 类的 static 成员变量和成员函数有什么特点?

属于类本身,不属于某个对象。

所有对象共享同一份静态成员变量。

静态成员函数不能访问非静态成员变量和成员函数。

5. 静态成员变量如何初始化? 

类内声明,类外初始化。

 四.new malloc delete free 关键字
1. new/delete 和 malloc/free 有什么区别?

new/delete 是 C++ 的操作符,malloc/free 是 C 的库函数。

new 自动计算所需内存并返回对象类型指针,malloc 需手动计算字节数并返回 void*,需强制类型转换。

new 分配失败会抛出异常,malloc 失败返回 NULL

new 在自由存储区(free store)分配内存,malloc 在堆(heap)分配内存。delete 释放内存时需要对象类型指针,free 需要 void* 指针。

new/delete 会调用构造和析构函数,malloc/free 不会。

2. 什么时候应该用 new/delete,什么时候用 malloc/free? 

在 C++ 中建议使用 new/delete,因为它们支持对象的构造和析构。

在 C 代码或需要与 C 库兼容时使用 malloc/free

3. 释放内存后为什么要将指针置为 nullptr 或 NULL? 

防止出现野指针(悬空指针)问题,避免后续误用已释放的内存 

4. delete 和 free 释放内存后,内存会立即释放吗?指针会自动变为 nullptr 吗?

内存释放由操作系统管理,不一定立即回收。

指针不会自动变为 nullptr,需要手动赋值。

 5. 用 new[] 分配的数组可以用 delete 释放吗?

不可以。用 new[] 分配的数组必须用 delete[] 释放,否则会导致未定义行为。

 五.extern 关键字
1. extern 关键字的作用是什么?

用于声明变量或函数在其他文件或作用域中定义,不是定义,只做声明。

让编译器知道该符号存在,链接时再去找具体实现。

2. C++ 调用 C 代码时 extern "C" 的作用? 

extern "C" 用于告诉编译器用 C 的方式进行符号修饰(防止C++的 name mangling),实现 C/C++ 混合编程。

 六.strcpy  sprintf  memcpy 关键字
1.请简述 strcpy、sprintf 和 memcpy 的主要区别及各自适用的场景。

strcpy 用于字符串拷贝,遇到 \0 结束,适合拷贝 C 风格字符串。

sprintf 用于格式化字符串,可以将多个变量格式化为字符串。

memcpy 用于内存块拷贝,按指定字节数复制,适合拷贝任意类型的数据(包括二进制数据),不会遇到 \0 停止。

2.如果要拷贝一个包含二进制数据(可能包含 \0)的缓冲区,应该使用 strcpy、sprintf 还是 memcpy?为什么?

应该使用 memcpy,因为 strcpy 和 sprintf 都会在遇到 \0 时停止拷贝,无法正确处理包含 \0 的二进制数据。而 memcpy 可以按照指定的字节数完整拷贝所有内容,包括中间的 \0 字节。

 

 六.C和C++中的类型转换
1.C中的类型转换是怎么转换?

(T)exp 或 exp(T) —— 语法简单,但不安全,容易出错。

2.C++中的类型转换是什么样的? 

static_cast<T>(exp) —— 最常用,类型安全,编译器检查。  (就用这个就对了)

const_cast<T>(exp) —— 改变const/volatile属性。  (不建议用)

dynamic_cast<T>(exp) —— 用于多态类型的安全转换。  (不建议用)

reinterpret_cast<T>(exp) —— 底层指针或类型强制转换,风险大。(别用)

七.默认拷贝构造函数 
1.如果类成员变量是另一个类对象,什么时候必须生成默认拷贝构造函数?

 当成员变量是类对象且该成员类有默认拷贝构造函数时,为了让成员类的拷贝构造函数能被正确调用,必须为当前类生成默认拷贝构造函数。

 2.类继承自一个基类,基类有默认拷贝构造函数,子类拷贝时会发生什么?

子类执行拷贝构造时,会先调用父类的拷贝构造函数。如果没有为子类生成默认拷贝构造函数,父类的拷贝构造函数将无法被调用。

3.什么时候不能只依赖编译器生成的默认拷贝构造函数? 

当类成员变量或基类有自定义拷贝构造函数,或者类中有指针、资源管理等特殊需求时,不能只依赖默认拷贝构造函数,需要自定义以保证拷贝行为符合预期。

 八.深拷贝,浅拷贝
1.什么是浅拷贝和深拷贝?它们的区别是什么?

浅拷贝只复制指针的值(地址),拷贝后多个对象的指针成员指向同一块内存;深拷贝会分配新内存,并复制实际数据,拷贝后对象互不影响。

2.在什么情况下会发生浅拷贝?会带来什么问题? 

默认拷贝构造函数和赋值操作符执行的是浅拷贝。如果类中有指针成员,浅拷贝会导致多个对象指向同一内存,可能出现重复释放、数据混乱等问题。

3.请举例说明函数参数为类对象(值传递)时,浅拷贝和深拷贝的影响。 

值传递会调用拷贝构造函数。如果是浅拷贝,函数内外的对象指针成员指向同一内存,函数结束时析构会导致外部对象的指针悬挂。深拷贝则不会有此问题。

4.如何实现深拷贝? 

需要自定义拷贝构造函数和赋值操作符,在其中为指针成员分配新内存,并复制原对象的数据内容。

5.返回类对象时(如 return obj;),浅拷贝和深拷贝有何影响? 

返回类对象会调用拷贝构造函数。浅拷贝会导致多个对象共享同一内存,析构时可能重复释放。深拷贝则每个对象有独立内存,安全可靠。

 九.struct 和 class 关键字
1.C++ 中 struct 和 class 的主要区别是什么?

C++ 中 struct 和 class 的唯一区别是默认访问权限不同。struct 的成员默认是 public,class 的成员默认是 private。除此之外,struct 和 class 在继承、成员函数、构造析构等方面完全等价。

2.struct 能否有成员函数、构造函数、继承和访问控制? 

可以。C++ 中 struct 和 class 一样,可以有成员函数、构造函数、析构函数、继承、访问控制(public/protected/private)等特性。

3.实际开发中 struct 和 class 应该如何选择? 

一般用于只包含数据成员、没有复杂行为的数据结构时用 struct;有封装、行为和访问控制需求时用 class。但两者功能上没有本质区别,主要是风格和可读性。

十.C++内存管理 
1.什么是内存泄漏?如何避免?

程序分配了内存但未释放,导致内存无法再被使用。避免方法:及时释放内存、使用智能指针(如 unique_ptrshared_ptr)、养成良好编码习惯。

2.什么是野指针?如何产生?如何避免? 

指向已释放或未初始化内存的指针。产生原因:指针未初始化、释放后未置空、返回局部变量地址。避免方法:指针初始化为 nullptr,释放后置空,避免返回局部变量地址。

3.C++11 引入了哪些内存管理相关的新特性? 

智能指针(unique_ptrshared_ptrweak_ptr)、移动语义(std::move)、内存对齐(alignasalignof)等。

4.什么是内存对齐?为什么需要内存对齐? 

内存对齐是指数据在内存中的存储地址要按一定规则排列,提高访问效率,避免硬件异常。C++ 可用 alignasalignof 控制对齐方式。

5.nullptr和NULL的区别,为什么C++更推荐用nullptr? 

nullptr 是 C++11 引入的专用空指针类型,类型安全,能够明确区分指针和整数,避免与整型混淆。
使用 nullptr 可以让编译器准确识别空指针,防止因 NULL 被当作 0 导致的重载解析歧义和类型不安全等问题,提升代码的可读性和健壮性,因此推荐在现代 C++ 中用 nullptr 替代传统的 NULL。

 十一.vector容器
1.vector 的底层数据结构是什么?

vector 本质上是一个可以动态扩容的连续数组。它通过三个指针(_M_start, _M_finish, _M_end_of_storage)管理内存的起始、结束和容量边界,实现高效的随机访问和动态扩容。

2. vector 插入和删除元素时,底层会发生什么? 

插入元素时,vector 会检查空间是否足够,不够则分配更大的内存(通常翻倍),并将原有元素拷贝到新内存。删除元素时,只是移动指针或元素,内存不会立即释放,只有通过 swap 空容器或 shrink_to_fit 才能释放未用内存。

3. vector 的内存增长机制是怎样的? 

vector 的内存通常按倍数增长(如 在Linux下gcc/g++ 下按2倍增长,在Windows下是按1.5倍增长),每次扩容会分配更大的内存,将原有数据拷贝过去,然后释放旧内存。内存空间只会增加不会自动减少。

4. vector 的 reserve 和 resize 有什么区别? 

reserve 只增加 capacity,不改变 size,不会初始化新元素。

resize 会改变 size,增加时新元素有初始值,减少时多余元素被析构。

reserve 适合预分配空间,避免多次扩容;resize 适合需要保证元素数量时使用。

5. vector 为什么不能存放引用类型? 

引用不是对象,不能分配内存,也不能改变绑定,vector 需要管理元素的内存和生命周期,因此不能存放引用类型。

6. vector 的元素是指针时,析构时会自动释放指针指向的内存吗? 

不会。vector 只会析构指针本身,不会释放指针指向的堆内存,需要手动释放。

7. vector 的 at 和操作符[] 有什么区别? 

操作符[] 不检查越界,at 会检查越界并抛出异常。两者都返回元素的引用,可以直接修改元素。

8. 如何释放 vector 未使用的内存? 

可以通过 swap 一个空容器或调用 shrink_to_fit 来释放未使用的内存空间。

9. vector 支持随机访问吗?为什么? 

支持。因为 vector 底层是连续内存,可以通过下标直接访问任意元素,时间复杂度为 O(1)。

10. vector 扩容时为什么要拷贝元素? 

因为新分配的内存地址不同,必须将原有元素拷贝到新内存,保证数据连续性和正确性。

 11.请简述空间配置器(Allocator)在 C++ STL vector 中的作用

空间配置器(Allocator)是 STL 容器用于管理底层内存分配和释放的机制。vector 通过 allocator 动态分配、释放、构造和析构元素对象,保证元素在一块连续内存中存储。 

12.请简述 STL 容器(如 vector)空间配置器(Allocator)常用的方法及其作用。 

allocate(size_t n)
分配 n 个元素所需的原始内存(但不构造对象)。

deallocate(pointer p, size_t n)
释放通过 allocate 获得的内存。

construct(pointer p, Args&&... args)
在指定内存地址 p 上使用 placement new 构造对象。

destroy(pointer p)
显式调用对象的析构函数,销毁对象但不释放内存。

address(reference x)
获取对象 x 的实际地址。

max_size()
返回分配器能分配的最大元素个数。

 十二.list容器
1.list 的底层数据结构是什么?

list 底层实现是一个双向循环链表,每个节点有指向前后节点的指针。

2.list 的节点结构包含哪些主要成员? 

节点结构包含存储数据的 _M_storage,指向下一个节点的 _M_next,指向上一个节点的 _M_prev

 3.list 为什么需要一个空节点/头节点?它的作用是什么?

空节点用于表示双向循环链表的头尾,方便链表的插入、删除等操作,避免特殊情况的判断。

4.list 的迭代器如何实现++和--操作? 

++ 操作让迭代器指向下一个节点,-- 操作让迭代器指向上一个节点。

5.如何通过迭代器获取 list 的第一个和最后一个元素? 

第一个元素是空节点的下一个节点,最后一个元素是空节点的上一个节点。

6.list 的插入和删除操作的时间复杂度是多少? 

在任意位置插入和删除元素的时间复杂度都是 O(1)。

7.list 支持随机访问吗?为什么? 

不支持。因为链表节点不连续,无法通过下标直接访问,只能顺序遍历。

8.list 的 splice 操作有什么特点? 

splice 可以在常数时间内把一个 list 的部分或全部节点移动到另一个 list,无需拷贝或移动元素。

9.list 的 remove 和 remove_if 有什么区别? 

remove 删除所有等于指定值的元素,remove_if 根据谓词删除满足条件的元素。

10.list 的 clear 操作会释放节点内存吗? 

会,clear 会销毁所有节点并释放其占用的内存。

 十三.deque容器
1.deque 的底层数据结构是什么?

deque 底层是由一组指针数组(map)管理的多个定长连续内存块组成,形成双向开口的连续线性空间,实现双端数组。 

2.deque 的 _M_map、_M_start、_M_finish 分别是什么作用?

_M_map 是指针数组,存储各个连续空间的指针;_M_start 记录首个连续空间的信息;_M_finish 记录最后一个连续空间的信息。 

 

3.deque 的迭代器结构包含哪些主要成员? 

含 _M_cur(当前元素)、_M_first(当前块首地址)、_M_last(当前块末尾地址)、_M_node(指向 map 中当前块指针)。

4.deque 在 push_back 或 pop_back 时,底层会发生什么? 

push_back 时会检查当前连续空间和 map 是否足够,不够则分配新空间或扩展 map;pop_back 时如果当前连续空间无数据则释放该空间。

 十四.vector && list && deque @compare
1.在什么场景下应该优先选择 vector?为什么?

当需要高效的随机访问(如通过下标访问元素),且对插入和删除效率要求不高时,应选择 vector,因为其底层是连续内存,支持 O(1) 随机访问。

2.什么时候应该使用 list? 

当需要频繁在任意位置插入和删除元素,且不关心随机访问效率时,应使用 list,因为其底层是双向链表,插入和删除效率高。

3.deque 适合什么场景? 

当既需要高效的随机访问,又需要在两端高效插入和删除元素时,应使用 deque,因为其底层支持双端操作和 O(1) 随机访问。

4.vector、list、deque 的插入和删除操作的时间复杂度分别是多少? 

vector 在尾部插入/删除为 O(1),中间为 O(n);list 任意位置插入/删除为 O(1)(已知位置);deque 在两端插入/删除为 O(1)。

5.vector、list、deque 是否支持随机访问? 

vector 和 deque 支持 O(1) 随机访问,list 不支持随机访问,只能顺序遍历。

6.如果需要频繁在容器头部插入元素,vector、list、deque 哪个效率最高? 

list 和 deque 头部插入效率高,vector 头部插入效率低(需整体移动元素)。

 十五.priority_queue
1.priority_queue 的底层数据结构是如何实现的?

底层用堆(完全二叉树)实现,通常用数组(如 vector)存储,保证根节点是最大(最大堆)或最小(最小堆)元素。

 2.priority_queue 的 push 和 pop 操作底层是如何实现的?

push 时先把新元素加到数组末尾,再上浮重构堆;pop 时将首元素与末元素交换,删除末元素,再下沉重构堆。

3.priority_queue 默认使用什么容器?可以换成什么? 

默认使用 vector,也可以用 deque,但不能用 list,因为 list 不支持随机访问。

4.priority_queue 如何实现最大堆和最小堆? 

通过 less<T>(最大堆)或 greater<T>(最小堆)作为比较器,决定父子节点大小关系。

5.priority_queue 的堆结构中,父子节点的索引关系是什么? 

对于索引为 n 的节点,左子节点索引为 2n+1,右子节点索引为 2n+2。

6.priority_queue 能否遍历所有元素?为什么? 

不能直接遍历,因为 priority_queue 只保证堆顶元素有序,其他元素无序。

7.priority_queue 的底层堆是如何构建的? 

通过 STL 的 make_heap、push_heap、pop_heap 算法,根据比较器 comp 构建和维护堆结构。

8.priority_queue 适合哪些应用场景? 

适合需要频繁获取最大(或最小)元素的场景,如任务调度、优先级管理、堆排序、图的最短路径算法等。

9.priority_queue 和普通 queue 有什么区别? 

queue 是先进先出,priority_queue 每次弹出的是优先级最高的元素(堆顶),不是按插入顺序。

 十六.set && multiset && map && multimap 容器
1.set 和 multiset 的底层数据结构是什么?为什么选择这种结构?

底层是红黑树(平衡二叉搜索树),因为它能保证元素有序且插入、删除、查找的时间复杂度为 O(log n)。

2.set 和 multiset 有什么区别? 

set 不允许重复元素,multiset 允许重复元素,二者都自动有序。

3.map 和 multimap 有什么区别? 

map 的 key 唯一,multimap 的 key 可以重复,二者都自动有序,底层都是红黑树。

4.set、multiset、map、multimap 的查找、插入、删除的时间复杂度是多少? 
都是 O(log n)。
5.set/multiset 的元素能否修改?为什么?

不能直接修改,因为修改后可能破坏红黑树的有序性。

我们应该先用迭代器找到要修改的元素;删除该元素;插入修改后的新元素。

6.map 和 multimap 的 value_type 是什么?

都是 pair<const Key, T>。

7.set/multiset 的 insert 返回值是什么? 

set 的 insert 返回 pair<iterator, bool>,multiset 的 insert 返回 iterator。

8.map/multimap 的 insert 返回值是什么? 

map 的 insert 返回 pair<iterator, bool>,multimap 的 insert 返回 iterator。

9.set/multiset 支持 emplace 吗?和 insert 有什么区别? 

支持。emplace 直接在容器内部原地构造元素,避免不必要的拷贝或移动。

10.set/multiset 的 key 可以是自定义类型吗?需要注意什么?

可以,但必须提供 < 运算符或自定义比较器,保证元素可排序。

 十七.unordered_set && unordered_multiset  && unordered_map  && unordered_multimap  容器

 1.unordered_set 的底层实现是什么?

哈希表

2.unordered_map 的底层实现是什么? 

哈希表(通常是开链地址法)。

3.unordered_map 如何处理哈希冲突? 

unordered_map 通过开链法处理哈希冲突,每个桶存储一个链表,冲突元素插入链表,查找和删除时在链表中顺序查找。负载因子过高时自动 rehash,保证性能。

4.unordered_map 的 rehash 机制是什么? 

负载因子超阈值时自动扩容并重新分配桶

5.unordered_map/unordered_set 的查找、插入、删除的时间复杂度是多少? 

平均 O(1),最坏 O(n)。

 十八.iterator 迭代器
1.迭代器的主要作用是什么?

提供一种访问容器内元素而不暴露容器内部实现的方式,实现解引用和成员访问,统一不同容器的访问方式。

2.STL 迭代器底层实现的核心思想是什么? 

不是面向对象思想,而是范式思想,通过泛型编程实现通用算法与容器解耦。

3.STL 迭代器有哪些类型?请简要说明各自的特点。 

输入迭代器(只读,单向遍历)、输出迭代器(只写,单向遍历)、前向迭代器(可读写,单向遍历)、双向迭代器(可读写,双向遍历)、随机访问迭代器(支持下标、加减法,功能最强)。

4.为什么需要不同类型的迭代器? 

不同容器和算法对遍历和操作元素的能力需求不同,迭代器类型决定了可用的操作和算法适用性。

5.迭代器与指针有什么关系? 

代器本质上是一种广义的指针,很多容器的迭代器实现就是指针,但也可以是自定义对象,统一了容器访问接口。

6.STL 算法如 find、sort、reverse 等为何能适用于不同容器? 

因为这些算法基于迭代器编写,只要容器提供相应类型的迭代器即可适配。

7.迭代器类型之间的关系是什么? 

随机访问迭代器 > 双向迭代器 > 前向迭代器 > 输入/输出迭代器,功能逐级增强,兼容性逐级递减。

 十九.STL容器线程安全
1.STL 容器默认是线程安全的吗?为什么?

不是线程安全的,STL 容器内部没有加锁,多个线程同时操作同一个容器会有数据竞争。

2.vector、deque 等容器在多线程下存在哪些安全隐患? 

扩缩容时会移动或重分配内存,插入和删除会导致节点关系变化,可能引发数据竞争和未定义行为。

3.红黑树四兄弟(set、map、multiset、multimap)在多线程下插入/删除会发生什么? 

插入和删除会引起 rebalance(重平衡),导致结构变化,线程不安全。 

4.unordered 系列容器在多线程下如何保证线程安全? 

可以提前拿到所有 key,避免并发修改同一个 key 时加锁,或使用原子操作。

5.读多写少的场景下,如何提升 STL 容器的并发性能? 

可以使用读写锁,读操作共享锁,写操作独占锁。

6.list 容器在多线程下如何优化并发插入/删除? 

可以采用生产者-消费者模型,每个线程操作自己的部分,减少锁竞争。

7.为什么 deque 及基于 deque 的容器不适合生产者-消费者模型? 

因为 deque 的扩缩容和节点关系复杂,容易引发线程安全问题。

8.除了加锁,还有哪些避免加锁的并发优化方法? 

可以提前分配好节点,将数据分成多份,每个线程只操作自己的数据,缺点是可能导致线程饿死。

9. 在多线程下,如何安全地对同一个 key 进行并发修改?

需要对同一个 key 加锁,或使用原子操作。

10.vector 的 resize 和 reserve 能否解决多线程下的所有安全问题? 

不能,只能解决扩缩容问题,不能解决节点关系变化导致的线程安全问题。

 二十.面向对象思想
1.什么是封装?C++ 如何实现封装?

封装是隐藏实现细节、实现模块化的机制。C++ 通过类的访问权限(public、protected、private)实现封装,友元类可以打破 private 限定。

2.C++ 中 public、protected、private 有什么区别? 

public 对所有对象开放,protected 只对子类开放,private 只对本类开放。

 3.什么是继承?继承的主要目的是什么?

继承是子类在无需修改原有类的情况下扩展功能的机制,主要目的是代码复用和功能扩展。

4.C++ 支持哪几种继承方式? 

支持 public、protected、private 继承,影响基类成员在子类中的访问权限。

5.基类成员在子类中的最高权限是什么?如何通过 using 修改? 

基类成员在子类中的最高权限由继承方式决定,可以通过 using 语句修改基类成员在子类中的权限。

6.C++ 支持多继承吗?多继承可能带来什么问题? 

支持多继承,可能带来二义性和菱形继承问题。

7.什么是接口继承?C++ 如何实现接口继承? 

接口继承是只继承接口(纯虚函数),C++ 通过纯虚函数实现接口继承。

8.什么是多态?多态的目的是什么? 

多态是一个接口多种实现形态,目的是通过接口重用和增强扩展性。

9.C++ 中的静态多态和动态多态分别如何实现? 

静态多态通过函数重载实现,动态多态通过虚函数重写实现。

10.什么是虚函数?如何实现运行时多态? 

函数是用 virtual 修饰的成员函数,基类指针/引用调用虚函数时会根据实际对象类型调用对应实现,实现运行时多态。

 二十一.多态(补充)
1.什么是早绑定和晚绑定?

早绑定是编译时确定函数地址,晚绑定是运行时通过虚函数表确定函数地址。

2.C++ 中虚函数表(vtable)和虚表指针(vptr)是什么? 

vtable 是存放虚函数地址的一维数组,每个含虚函数的类有一个,vptr 是对象中的指针,指向该类的 vtable,构造函数中初始化。

3.为什么虚函数表指针要在构造函数中初始化? 

保证对象创建时能正确指向对应类的虚函数表,实现多态调用。

 二十二.菱形继承
1.什么是菱形继承?请举例说明。

菱形继承是指一个子类继承多个父类,而这些父类又继承自同一个基类,导致继承关系呈菱形结构。

2.菱形继承会带来哪些问题? 

会造成数据成员重复(浪费存储空间)和成员访问二义性(不知道访问哪一份基类成员)。

3.C++ 如何解决菱形继承问题? 

通过虚继承(virtual 继承),让子类只继承一份共同基类的数据和成员。

4.虚继承的语法是什么? 

在继承基类时加上 virtual 关键字,例如:class B : virtual public A {}

5.虚继承的底层实现原理是什么? 

通过虚表偏移,父类的虚表指针(vptr)中保存到共同基类的偏移量,保证多重继承时只指向同一个基类实例。

6.什么是虚基类?它的作用是什么? 

用 virtual 关键字修饰的基类称为虚基类,主要用于解决多重继承中的菱形继承问题,避免数据成员重复和二义性。