1、详细解释deque的底层原理
deque底层是动态开辟的二维数组
定义的2个宏:(不同的VS,g++,都有更改值)
#define MAP_SIZE 2
#define QUE_SIZE(T) 4096/sizeof(T)
一维数组的初始大小 MAP_SIZE (T*)
第二维数组默认开辟的大小就是QUE_SIZE(int) 1024
deque是双端队列 两端都有队头和队尾 两端都可以插入删除
时间复杂度是O(1)
当第一维所有的位置都开辟了第二维,再增加元素的话就要扩容
扩容: 把第一维数组按照2倍的方式进行扩容 2-4-8-16。。。
扩容以后,会把原来的第二维的数组,从新一维数组的第oldsize / 2 开始存放 也就是说,当它从2 扩容到 4的时候,在新一维数组的第 4/2 = 2处开始存放原来的第二维数组。
扩容之后,就是把中间2行放满,然后上下都留1行,为了以后头尾都要插入!
stack queue =》 底层都是依赖deque(deque内存利用率好,第二维是分段连续的, 刚开始就有一段内存可以供使用,默认就开辟了初始大小,但是vector是要从0-1-2-4-8,慢慢扩容!)
priority_queue(优先级队列,构建大根堆,需要内存都是连续的,才能通过下标计算左孩子右孩子和父节点的关系):底层是基于vector
2、虚函数,多态
虚函数:
一个类 如果有虚函数就要在编译阶段给该类产生一张虚函数表 =》 虚函数表运行时,加载到.rodata段
当用指针或者引用 调用 虚函数时,首先通过指针或者引用访问对象的头四个字节vfptr =》再去相应的vftable中取虚函数的地址,进行动态绑定调用。
多态:
- 设计函数接口的时候,可以都使用基类的指针或者引用来接收不同派生类对象的同名覆盖方法,当我们进行功能增加或者删除的时候,函数接口不需要任何改变。
- 参考OOP设计模式(高内聚,低耦合,软件设计的开闭原则,原有的接口不做任何改变,就可以支持任意功能的增加和减少)。
3、虚析构函数、智能指针
Base *p = new Derive();
delete p;
基类指针指向new出来的派生类对象时,delete 基类指针p的时候,只调用基类的析构,不调用派生类的析构,此时应该将基类的析构函数写成虚析构函数,派生类的析构函数也自动称为虚析构函数!此时对于析构函数的调用,动态绑定!
智能指针: 利用栈上对象出作用域自动释放资源,自动管理资源的生命周期(3种不带引用计数的,2种带引用计数的)
4、一个类,写了一个构造函数,还写了一个虚构造函数,可不可以,会发生什么?
-
虚函数的调用,需要对象的存在!需要访问对象的前4个字节vfptr,访问虚函数表,进而取出虚函数的地址!构造函数没调用完,对象没有产生,是不会进行动态绑定的!
-
一个派生类对象的构造,要先构造基类,如果有虚函数,也就是派生类将基类的构造函数重写了,当调用基类的虚函数的时候,即基类指针指向派生类对象,调用构造函数实际上是调用的是派生类的构造函数,这样是无法构造一个派生类对象的!
5、异常机制怎么回事儿?
try
{
可能会抛出异常的代码 throw
}
catch(const string &err)
{
捕获相应异常类型对象,进行处理,完成后,代码继续向下运行
}
好处: 当前代码抛出异常,如果在当前函数栈帧没有找到相应的catch块,就会这个异常抛给调用方函数,调用方函数依然是这样处理的,如果有处理异常的catch代码块,就向下运行,如果没有,就继续抛给调用方,直到到main函数还没有处理再抛给系统,系统发现有异常没有处理,就中止了。
如果在函数上有找到catch块,就继续运行。
异常的栈展开! 异常的好处是可以把代码中所有的异常抛到统一的地方进行处理(比如说抛到main函数中统一处理)!
6、早绑定和晚绑定?
早绑定:
早绑定(静态绑定,编译时期的绑定): 普通函数的调用,用对象调用虚函数 ,call 编译阶段已经知道调用哪个函数。
函数调用的地方打断点,转到反汇编,如果是call一个函数,就是静态绑定,如果是call一个寄存器,就是动态绑定。
晚绑定:
晚绑定(动态绑定): 用指针/引用调用虚函数的时候,都是动态绑定
p->vfptr->vftable->取virtual addr
把addr放到寄存器,=>然后 call eax 在编译阶段,是不知道最终调用哪个虚函数的。
最终从虚函数表取出的是哪个虚函数的地址,调用的就是哪个虚函数
7、指针和引用的区别(汇编分析)
int a = 10;
int *p = &a; lea eax, [a] mov dword ptr[ebp-8], eax
int &b = a; lea eax, [a] mov dword ptr[ebp-0Ch], eax
*p = 20; mov eax, dword ptr[ebp-8] mov dword ptr[eax], 14H
b = 20; mov eax, dword ptr[ebp-0Ch] mov dword ptr[eax], 14H
-
定义指针和定义引用变量说生成的汇编代码是完全相同的!都是先将a的内存拿出来放在寄存器中,再将寄存器的值拿出来放到底层的4字节的变量中!
-
通过引用变量和指针改变其值,生成的指令也都是一样的!
引用是更安全的指针,因为引用是需要初始化的。
使用指针,是不是野指针?从编码的正确性保障。
8、智能指针交叉引用问题怎么解决?
定义对象的时候用强智能指针shared_ptr,而引用对象的时候用弱智能指针weak_ptr;(强智能指针可以引起对象的引用计数的变化,弱智能指针不会!)
当通过weak_ptr访问对象成员时,需要先调用weak_ptr的lock提升方法,把weak_ptr提升成shared_ptr强智能指针,再进行对象成员调用。
9、重载的底层实现,虚函数的底层实现
重载:
- 因为C++生成函数符号,是依赖函数名字+参数列表
- 编译到函数调用点时,根据函数名字和传入的实参(个数和类型),和某一个函数重载匹配的话,那么就直接调用相应的函数重载版本(也叫静态的多态 都是在编译阶段处理的!)
虚函数:
虚函数 =》指针/引用调用虚函数的时候(动态绑定) =》 通过vfptr => 访问vftable => 取虚函数的地址 进行call eax的调用!
注意: 如果使用对象调用是一个静态绑定!