面试问题整理

项目相关 项目中用到的C++技术栈

1.vector扩容机制(扩容用到的STL器件?没答出来)

  • 两倍扩容问题:
    • 为什么呈倍数扩容(时间复杂度更优)
      • 对于n次插入操作, 采用成倍方式扩容可以保证时间复杂度O(n), 而指定大小扩容的时间复杂度为O(n^2)
    • 为什么是1.5倍扩容(空间可重用)
      • k == 2时:
        • 第n次扩容的时候需要分配的内存是:an = a1*q^(n-1) = 4*2^(n-1)
        • 而前n-1项的内存和为:Sn = a1*(1-q^(n-1))/(1-q) = 4*(1-2^(n-1)) /(1-2) = 4*2^(n-1)-4
        • 差值 = an - Sn = 4 > 0
        • 所以第n次扩容需要的空间恰好比前n-1扩容要求的空间总和要大,那么即使在前n-1次分配空间都是连续排列的最好情况下,也无法实现之前的内存空间重用
      • k = 1.5时:
        • n次扩容的时候需要分配的内存是:an = a1*q^(n-1) = 4*1.5^(n-1)
        • 而前n-1项的内存和为:Sn = a1*(1-q^(n-1))/(1-q) = 4*(1-1.5^(n-1)) /(1-1.5) = 8*1.5^(n-1)-8
        • 差值 = an - Sn = 8 - 4*1.5^(n-1)
        • n增长到一定的数值后,差值就会变为小于0,那么如果前n-1次分配的空间都是连续的情况下, 就可以实现内存空间复用

      1) 新增元素:vector 通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素;

    2) 对vector 的任何操作,一旦引起空间重新配置,指向原vector 的所有迭代器就都失效了;

    3) 初始时刻vector 的capacity 为0,塞入第一个元素后capacity 增加为1;

    4) 不同的编译器实现的扩容方式不一样,VS2015 中以1.5 倍扩容,GCC 以2 倍扩容。对比可以发现采用采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容。 

2.讲讲static关键字

  • 主要可以分为五个类型: 全局静态变量, 局部静态变量, 静态函数, 静态成员变量, 静态成员函数
    1. 全局静态变量
    • 在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
    • 内存中的位置:静态存储区,在整个程序运行期间一直存在。
    • 初始化:未经初始化的全局静态变量会被自动初始化为0(对于自动对象,如果没有显示初始化,会调用零参数构造函数,如不存在则编译失败);
    • 作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
      1. 局部静态变量
    • 在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
    • 内存中的位置:静态存储区
    • 初始化:未经初始化的全局静态变量会被自动初始化为0(对于自动对象,如果没有显示初始化,会调用零参数构造函数,如不存在则编译失败);
    • 作用域:作用域仍为局部作用域,
      • 当定义它的函数或者语句块结束的时候,作用域结束。
      • 但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
  1. 静态函数
    • 在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
    • 函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
    • warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
  2. 类的静态成员
    • 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。
    • 因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
  3. 类的静态函数
    • 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
    • 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。*如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:::();*参数表>静态成员函数名>类名>
    • 不能被virtual修饰,静态成员函数没有this 指针,虚函数的实现是为每一个对象分配一个vptr 指针,而vptr 是通过this 指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function

3.C++ STL容器和区别 迭代器

  • vector 底层数据结构为数组,支持快速随机访问
  • list 底层数据结构为双向链表,支持快速增删
  • deque 底层数据结构为一个中央控制器和多个缓冲区,详细见STL 源码剖析P146,支持首尾(中间不能)快速增删,也支持随机访问, deque 是一个双端队列(double-ended queue),也是在堆中保存内容的.它的保存形式 如下:[堆1] --> [堆2] -->[堆3] --> ..., 每个堆保存好几个元素,然后堆和堆之间有指针指向,看起来像是list 和vector 的结合品.
  • stack 底层一般用list deque 实现,封闭头部即可,不用vector 的原因应该是容量大小有限制,扩容耗时
  • queue 底层一般用list deque 实现,封闭头部即可,不用vector 的原因应该是容量大小有限制,扩容耗时(stack queue 其实是适配器,而不叫容器,因为是对容器的再封装)
  • priority_queue 的底层数据结构一般为vector 为底层容器,堆heap 为处理规则来管理底层容器实现
  • set 底层数据结构为红黑树,有序,不重复
  • multiset 底层数据结构为红黑树,有序,可重复
  • map 底层数据结构为红黑树,有序,不重复
  • multimap 底层数据结构为红黑树,有序,可重复
  • hash_set 底层数据结构为hash 表,无序,不重复
  • hash_multiset 底层数据结构为hash 表,无序,可重复
  • hash_map 底层数据结构为hash 表,无序,不重复
  • hash_multimap 底层数据结构为hash 表,无序,可重复

4.智能指针,shared_ptr 怎么解决其线程安全问题(weak_ptr)

5.程序从源代码到可执行文件的过程

  • 预处理 
  • 编译
  • 汇编
  • 链接

 

6.防止内存泄露的方法

使用智能指针

7.C和C++申请动态内存的方法和不同

 new 和malloc new会执行构造函数,malloc只会返回指向一片固定大小的空闲区域的指针。

 

8.C++内存空间

  • 栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等其操作方式类似于数据结构中的栈。
  • 堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
  • 全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
  • 文字常量区—常量字符串就是放在这里的。程序结束后由系统释放。
  • 程序代码区—存放函数体的二进制代码

9.数组和链表的区别

10.C++多态的几种方式

理解的虚函数和多态

  • 多态的实现主要分为动态多态和静态多态, 静态多态就是重载, 在编译的时候就已经确定了, 动态多态是用虚函数机制实现的, 在运行期间动态绑定. 例如如果父类中定义了虚函数,而派生类重写了此函数,此时通过一个指向派生类对象的父类指针调用此函数式, 子类中重写的函数将被调用.
  • 虚函数的实现: 在存在虚函数的类中会存在一个指向虚函数表的指针, 虚函数表中存放了虚函数的地址,当子类继承父类时也会继承其虚函数表, 当子类重写父类的虚函数时, 会将其继承的虚函数表中的地址替换为重写的函数地址. 使用虚函数,会增加访问内存的开销, 降低效率.
  • 虚表指针初始化在初始化列表前

11.基类析构函数可以声明为虚函数吗 

可以,有时候必须,否则子类的执行析构函数是执行基类的析构函数会造成内存泄露。

12.线程同步方式

线程同步主要包括四种方式:

  • 互斥量pthread_mutex_
  • 读写锁pthread_rwlock_
  • 条件变量pthread_cond_
  • 信号量sem_

13 产生死锁的条件

  •  互斥条件,共享条件X和Y只能被一个线程占用
  • 请求和保持条件,线程T1已经去的共享资源X,在等待共享资源Y的时候,不释放共享资源X
  • 不可抢占条件,其他线程不能强行抢占线程,T1占有的资源
  • 循环等待条件,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源就是循环等待

14 如何避免死锁
产生死锁的时候,只能通过人工干预来解决,比如重启服务或者kill掉这个线程,所以我们只能在写代码的时候,去规避可能出现的死锁问题,而按照死锁产生的四个条件,我们只能破坏其中的任何一种就可以解决它,但是互斥条件是没有办法被破坏的,因为它是互斥锁的基本约束,而其他的三个条件都有办法来破坏

  • 请求和保持这个条件,我们可以一次性申请所有的资源,这样就不存在锁要等待了。
  • 不可抢占这个条件,占用部分资源的线程, 在进一步申请其他资源的时候,如果申请不到,我们可以主动去释放它占有的资源,这样不可抢占这个条件,就会被破坏掉了
  • 循环等待条件,可以按序申请资源来预防,所谓按序申请,就是指资源是有线性顺序,申请的时候,可以先申请资源序号小的,然后再去申请资源序号大的,这样线性化之后,自然不存在循环了

15 IO多路复用

16顶层const和底层const 

const int *p1 可看作是const修饰的类型是int,修饰的内容是*p1,即*p1不允许改变。
int const *p2 可以看作const修饰的类型是int,修饰的内容是*p2,即*p2不允许改变。
int *const p3 可以看作const修饰的类型是int *,修饰的内容是p3,即p3不允许改变

//底层const-指针所指的对象是一个常量
//顶层const-指针本身是个常量
int i =0;
int *const p1 =&i;//顶层
const int ci =42;//顶层
const int *p2 =&ci;//底层
const int *const p3 =p2;//顶层
const int &r =ci;//底层

17函数签名

C++中的函数签名(function signature):包含了一个函数的信息,包括函数名、参数类型、参数个数、顺序以及它所在的类和命名空间

18红黑树

红黑树的5个性质:

  1. 每个结点要么是红的要么是黑的。  
  2. 根结点是黑的。  
  3. 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。  
  4. 如果一个结点是红的,那么它的两个儿子都是黑的。  
  5.  对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点

 正是红黑树的这5条性质,使一棵n个结点的红黑树始终保持了logn的高度,从而也就解释了上面所说的“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论成立的原因。

红黑树插入

如果插入的是根结点,由于原树是空树,此情况只会违反性质2,因此直接把此结点涂为黑色;如果插入的结点的父结点是黑色,由于此不会违反性质2和性质4,红黑树没有被破坏,所以此时什么也不做。

  • ● 插入修复情况1:如果当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色

    ● 插入修复情况2:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的右子

    ● 插入修复情况3:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点

红黑树的删除和删除修复

继续讲解之前,补充说明下二叉树结点删除的几种情况,待删除的节点按照儿子的个数可以分为三种:

  • 没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
  • 只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
  • 有两个儿子。这是最麻烦的情况,因为你删除节点之后,还要保证满足搜索二叉树的结构。其实也比较容易,我们可以选择左儿子中的最大元素或者右儿子中的最小元素放到待删除节点的位置,就可以保证结构的不变。当然,你要记得调整子树,毕竟又出现了节点删除。习惯上大家选择左儿子中的最大元素,其实选择右儿子的最小元素也一样,没有任何差别,只是人们习惯从左向右。这里咱们也选择左儿子的最大元素,将它放到待删结点的位置。左儿子的最大元素其实很好找,只要顺着左儿子不断的去搜索右子树就可以了,直到找到一个没有右子树的结点。那就是最大的了。

在删除节点后,原红黑树的性质可能被改变,如果删除的是红色节点,那么原红黑树的性质依旧保持,此时不用做修正操作,如果删除的节点是黑色节点,原红黑树的性质可能会被改变,我们要对其做修正操作。那么哪些树的性质会发生变化呢,如果删除节点不是树唯一节点,那么删除节点的那一个支的到各叶节点的黑色节点数会发生变化,此时性质5被破坏。如果被删节点的唯一非空子节点是红色,而被删节点的父节点也是红色,那么性质4被破坏。如果被删节点是根节点,而它的唯一非空子节点是红色,则删除后新根节点将变成红色,违背性质2。

上面的修复情况看起来有些复杂,下面我们用一个分析技巧:我们从被删节点后来顶替它的那个节点开始调整,并认为它有额外的一重黑色。这里额外一重黑色是什么意思呢,我们不是把红黑树的节点加上除红与黑的另一种颜色,这里只是一种假设,我们认为我们当前指向它,因此空有额外一种黑色,可以认为它的黑色是从它的父节点被删除后继承给它的,它现在可以容纳两种颜色,如果它原来是红色,那么现在是红+黑,如果原来是黑色,那么它现在的颜色是黑+黑。有了这重额外的黑色,原红黑树性质5就能保持不变。现在只要恢复其它性质就可以了,做法还是尽量向根移动和穷举所有可能性。"--saturnman。

如果是以下情况,恢复比较简单:

a)当前节点是红+黑色
解法,直接把当前节点染成黑色,结束此时红黑树性质全部恢复。
b)当前节点是黑+黑且是根节点, 解法:什么都不做,结束。
但如果是以下情况呢?:

删除修复情况1当前节点颜色是黑-黑色brother为黑色,且brother有一个与其方向一致的红色子结点son

图(c )中,白色代表随便是黑或是红,方形结点存储的是一个游离的黑色权值。将brother和father旋转(是左旋还是右旋自己根据情景体会,下同),并重新上色后,变成了图(d),将游离的黑色权值扔掉,此时不违背任何红黑树的性质,删除操作完成。

 

 

 

 

删除修复情况2:当前节点颜色是黑-黑色brother为黑色,且brother有一个与其方向不一致的红色子结点son

 

 

图(e)中,将son和brother旋转,重新上色后,变成了图(f),转化为情形一。

删除修复情况3:当前节点颜色是黑-黑色brother为黑色,且brother无红色子结点

此时若father为红,则重新着色即可,删除操作完成。如图下图(g)和(h)

 

 

 

此时若father为黑,则重新着色,将游离的黑色权值存到father(此时father的黑色权重为2),将father作为新的结点进行情形判断,遇到情形一、情形二,则进行相应的调整,完成删除操作;如果没有,则结点一直上溯,直到根结点存储额外的黑色,此时将该额外的黑色扔掉,即完成了删除操作。

 

 

 


删除修复情况4:当前节点颜色是黑-黑色brother为红色,则father必为黑色

 

 图(i)中,将brother和father旋转,重新上色后,变成了图(j),新的brother(原来的son)变成了黑色,这样就成了情形一、二、三中的一种。如果将son和brother旋转,无论怎么重新上色,都会破坏红黑树的性质4或5,例如图(k)。

19: c++模板的特化与偏特化
20:左值与右值

21:菱形基础的内存布局

 

posted @ 2022-09-07 20:41  lhclqslove  阅读(50)  评论(0编辑  收藏  举报