智臾科技_2.28面经(一面)


面试过程

22min

  • 自我介绍
  • 讲一下虚函数
  • 与运行时多态相对的是什么
  • C++是否可以实现编译时多态
  • 对虚函数底层实现有了解吗 —— 不会
  • 有用过智能指针吗?大概讲解下
  • 怎么初始化unique_ptr指针 —— 不会
  • share_ptr指针为什么要计数?计数为0时会执行什么动作?
  • 对C++11还有什么了解吗?
  • 有用过lambda函数吗? —— 不会
  • unordered_map与map的区别?时间复杂度有什么区别?
  • vector的实现原理?底层是什么?在内存里是连续的吗?
  • vector的push_back()的时间复杂度?为什么会是常量时间?
  • 对多线程有了解吗?
  • 怎么解决并发问题?如两个线程同时访问一个资源?
  • 用锁会有什么问题?
  • 什么时候会发生死锁?
  • 平时开发是否用Linux?
  • 列举一些排序算法?
  • 说一下快排的实现?
  • 项目
  • 反问

下面是问题答案整理

虚函数

如何理解虚函数

  • 对面向对象的程序设计而言,就是当对象接收到某一消息时,才去寻找和连接相应的方法,所以只能采用动态联编。
  • 而 C++ 为了保持C语言的高效性,仍是编译型的,采用静态联编。利用虚函数机制,C++ 可部分地采用动态联编,C++通过虚函数实现运行时多态性
  • 如果某类中的一个成员函数被说明为虚函数,这就意味着该成员函数在派生类中可能有不同的实现。
  • 当子类重新定义了父类的虚函数后,当父类的指针指向子类对象的地址时,父类指针根据赋给它的不同子类指针,动态的调用子类的该函数,而不是父类的函数

虚函数和纯虚函数的区别

  • 定义一个函数为虚函数,不代表函数为不被实现的函数。
  • 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
  • 定义一个函数为纯虚函数,才代表函数没有被实现。
  • 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

编译时多态

C++编译期多态与运行期多态

对模板参数而言,多态是通过模板具现化和函数重载解析实现的。以不同的模板参数具现化导致调用不同的函数,这就是所谓的编译期多态。

虚函数底层实现

编译器将为实现了虚函数的基类和覆盖了虚函数的派生类分别创建一个虚函数表(Virtual Function Table,VFT)。也就是说Base和Derived类都将有自己的虚函数表。实例化这些类的对象时,将创建一个隐藏的虚表指针VFT*,它指向相应的VFT。可将VFT视为一个包含函数指针的静态数组,其中每个指针都指向相应的虚函数。

举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:

  • 如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。
  • 如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。

image

智能指针

为什么要使用智能指针

使用智能指针可以很大程度上的避免内存泄露问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。

unique_ptr

unique_ptr 用于取代 c++98 的 auto_ptr

  • 在 c++98 的时候还没有移动语义(move semantics)的支持,因此对于auto_ptr的控制权转移的实现没有核心元素的支持,但是还是实现了auto_ptr的移动语义,这样带来的一些问题是拷贝构造函数和复制操作重载函数不够完美
  • auto_ptr不支持传入deleter,所以只能支持单对象(delete object),而unique_ptr对数组类型有偏特化重载,并且还做了相应的优化,比如用[]访问相应元素等.

unique_ptr是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

  • 1、拥有它指向的对象
  • 2、无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作
  • 3、保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

unique_ptr可以实现如下功能:

  • 1、为动态申请的内存提供异常安全
  • 2、讲动态申请的内存所有权传递给某函数
  • 3、从某个函数返回动态申请内存的所有权
  • 4、在容器中保存指针
  • 5、auto_ptr 应该具有的功能

如何初始化unique_ptr

unique_ptr的使用和陷阱

  • shared_ptr不同,unique_ptr没有定义类似make_shared的操作,因此只可以使用new来分配内存,并且由于unique_ptr不可拷贝和赋值,初始化unique_ptr必须使用直接初始化的方式。
unique_ptr<int> up1(new int());    //okay,直接初始化
unique_ptr<int> up2 = new int();   //error! 构造函数是explicit
unique_ptr<int> up3(up1);          //error! 不允许拷贝
  • shared_ptr不同,unique_ptr拥有它所指向的对象,在某一时刻,只能有一个unique_ptr指向特定的对象。当unique_ptr被销毁时,它所指向的对象也会被销毁。因此不允许多个unique_ptr指向同一个对象,所以不允许拷贝与赋值。

unique_ptr的操作

  • unique_ptr<T> up 空的unique_ptr,可以指向类型为T的对象,默认使用delete来释放内存
  • unique_ptr<T,D> up(d) 空的unique_ptr同上,接受一个D类型的删除器d,使用删除器d来释放内存
  • up = nullptr 释放up指向的对象,将up置为空
  • up.release() up放弃对它所指对象的控制权,并返回保存的指针,将up置为空,不会释放内存
  • up.reset(...)参数可以为空、内置指针,先将up所指对象释放,然后重置up的值.

share_ptr

  • 从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享
  • 可以通过成员函数use_count()来查看资源的所有者个数。
  • 除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。
  • 当我们调用release()时,当前指针释放资源所有权,计数减一。当计数等于0时,资源会被释放

weak_ptr

  • weak_ptr是用来解决shared_ptr相互引用时的死锁问题
  • 如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。
  • 它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr

C++ 11

1. Auto

  • 第一,非常明显,能够避免重复输入我们已经声明的并且编译器已经认识的类型名称,这是非常方便的。
  • 第二,当有遇到一个你不知道或者无法用语言表达的类型时,auto就不仅仅是使用方便这么简单了,比如,大多数lambda函数的类型,你不能够容易的将其类型拼写出来甚至根本就不能够写出来。

注意,使用auto并没有修改代码的语义。代码仍然是静态类型(statically typed),并且每个表达式都干净利落;只是不再强制我们多余的重新声明类型的名称。

2. 智能指针,no delete

总是使用智能指针,不要用原生指针和delete。除非需要实现你自己的底层数据结构(把原生指针很好的封装在类(class boundary)中

  • 如果你知道你是另外一个对象的唯一拥有者,使用unique_ptr来表示唯一的拥有权。一个"new T"表达式能很快的初始化一个拥有 这个智能指针的对象,特别是unique_ptr
  • 使用shared_ptr来表示共享所有权(shared ownership)。使用make_shared来创建共享对象更好。
  • 使用weak_ptr打破循环和表示可选性(比如实现一个对象缓存)
  • 如果你了解到另外一个对象比你的生存周期要长,并且你想观察这个对象,那么使用原生指针(raw pointer)。

3. Nullptr

用nullptr来表示一个空指针,不要再使用数字0或者宏NULL来表示空指针了,因为这些是模棱两可的,既能表示整形也可表示指针。

4. Range for

对一个范围内的元素进行有序访问,基于range的for循环会是更方便的用法。

5. 非成员begin和end

使用非成员函数begin(x)和end(x)(不是x.begin()和x.end()),因为begin(x)和end(x)是可扩展的,能同所有容器类型一块工作——甚至数组也可以——并不是只针对提供了STL风格的x.begin()和x.end()成员函数的容器。

如果你正在使用一个非STL集合类型,这个类型提供迭代器但不是STL风格的x.begin()和x.end(),你可以对他的非成员函数begin()和end()进行重载,这样你就可以使用同STL容器同样的风格进行编码。

6. Lambda函数

C++11中的匿名函数(lambda函数,lambda表达式)

Lambda表达式具体形式如下:

[capture](parameters)->return-type{body}

如果没有参数,空的圆括号()可以省略.返回值也可以省略,如果函数体只由一条return语句组成或返回类型为void的话.形如:

[capture](parameters){body}
[](int x, int y) { return x + y; } // 隐式返回类型
[](int& x) { ++x; }   // 没有return语句 -> lambda 函数的返回类型是'void'
[]() { ++global_x; }  // 没有参数,仅访问某个全局变量
[]{ ++global_x; }     // 与上一个相同,省略了()
[](int x, int y) -> int { int z = x + y; return z; }    // 显示指定返回类型
  • Lambda函数中创建的临时变量, 和普通函数一样不会保存到下次调用
  • 什么也不返回的Lambda函数可以省略返回类型, 而不需要使用 -> void 形式.

Lambda函数可以引用在它之外声明的变量,这些变量的集合叫做一个闭包

闭包被定义在Lambda表达式声明中的方括号[]内,这个机制允许这些变量被按或按引用捕获。

[]        //未定义变量.试图在Lambda内使用任何外部变量都是错误的.
[x, &y]   //x 按值捕获, y 按引用捕获.
[&]       //用到的任何外部变量都隐式按引用捕获
[=]       //用到的任何外部变量都隐式按值捕获
[&, x]    //x显式地按值捕获. 其它变量按引用捕获
[=, &z]   //z按引用捕获. 其它变量按值捕获

7. Move/&&

把move当作是对拷贝的优化最合适不过了,虽然它也包含其他方面的东西(像完美转发(perfect forwarding))

move语义改变了我们设计API的方式。我们会越来越多的将函数设计成return by value。

8. 统一的初始化和初始化列表

  • 没有发生变化的:当初始化一个non-POD或者auto的本地变量时,继续使用熟悉的不带额外花括号{}的=语法。
  • 在其他情况中(特别是随处可见的使用()来构造对象),使用花括号{}会更好。

使用花括号{}能避免一些潜在的问题:

  • 你不会突然得到一个收缩转换(narrowing conversions)后的值(比如,float转换成int)
  • 也不会有偶尔突发的未初始化POD成员变量或者数组的存在
  • 也能避免在c++98中会碰到的奇怪事:你的代码编译没问题,你需要的是变量但实际上你声明了一个函数

最后,有时候传递不带(type-named temporary)的函数参数是很方便的

9. 显示重写(覆盖)override和final

  • 在 C++ 03中,很容易让你在本想重写基类某个函数的时候却意外地创建了另一个虚函数。
  • C++11引入了 override 这个特殊的标识符意味编译器将去检查基类中有没有一个具有相同签名的虚函数,如果没有,编译器就会报错!

C++11还增加了防止基类被继承和防止子类重写函数的能力。这是由特殊的标识符final来完成的,

需要注意的是,override和final都不是C++语言的关键字。他们是技术上的标识符,只有在它们被用在上面这些特定的上下文在才有特殊意义。用在其它地方他们仍然是有效标识符。

10. 新增容器std::array

  • std::array 保存在栈内存中,相比堆内存中的 std::vector,我们能够灵活的访问这里面的元素,从而获得更高的性能。
  • std::array 会在编译时创建一个固定大小的数组,std::array 不能够被隐式的转换成指针,使用 std::array只需指定其类型和大小即可:

11. 字符串类 新增与其他类型互换的方法

字符串类 字新增与其他类型互换的方法,如to_string(),stoi(),stol等

12. STL标准模板库新增unordered_map

  • map的底层原理,是通过红黑树(一种非严格意义上的平衡二叉树)来实现的,因此map内部所有的数据都是有序的,map的查询、插入、删除操作的时间复杂度都是O(logn)
    • 根结点是黑的。
    • 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
    • 如果一个结点是红的,那么它的两个儿子都是黑的。
    • 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。
  • unordered_map的底层是一个防冗余的哈希表(采用除留余数法)。哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1);而代价仅仅是消耗比较多的内存。
  • 使用时map的key需要定义operator<。而unordered_map需要定义hash_value函数并且重载operator==。

数据结构与算法

vector

Vector 是一个封装了动态大小数组的顺序容器。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组

push_back

  • 该函数首先检查是否还有备用空间,如果有就直接在备用空间上构造元素,并调整迭代器 finish,使 vector变大。如果没有备用空间了,就扩充空间,重新配置、移动数据,释放原空间。
  • 当执行 push_back 操作,该 vector 需要分配更多空间时,它的容量(capacity)会增大到原来的 m 倍。

现在我们来均摊分析方法来计算 push_back 操作的时间复杂度。

  • 假定有 n 个元素,倍增因子为 m。那么完成这 n 个元素往一个 vector 中的push_back 操作,需要重新分配内存的次数大约为 \(\log_m(n)\)
  • 第 i 次重新分配将会导致复制 \(m^i\) (也就是当前的vector.size() 大小)个旧空间中元素
  • 因此 n 次 push_back操作所花费的总时间约为 n*m/(m - 1),那么 n 个元素,n 次操作,每一次操作需要花费时间为 m / (m - 1),这是一个常量.

所以,我们采用均摊分析的方法可知,vector 中 push_back 操作的时间复杂度为常量时间.

快排

八大排序-快速排序(搞定面试之手写快排)

快速排序的核心思想是分治:选择数组中某个数作为基数,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数都比基数小,另外一部分的所有数都都比基数大,然后再按此方法对这两部分数据分别进行快速排序,循环递归,最终使整个数组变成有序。

以最常见的使用数组首元素作为基数进行快速排序原理说明。

  • 以第一个数字作为基数,使用双指针i,j进行双向遍历:
  • 、i从左往右寻找第一位大于基数(6)的数字,j从右往左寻找第一位小于基数(6)的数字;‘’找到后将两个数字进行交换。继续循环交换直到i>=j结束循环;
  • 最终指针i=j,此时交换基数和i(j)指向的数字即可将数组划分为小于基数(6)/基数(6)/大于基数(6)的三部分,即完成一趟快排

多线程

死锁

死锁面试题(什么是死锁,产生死锁的原因及必要条件)

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

产生死锁的必要条件:

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

Linux

gdb 调用栈命令

  • backtrace/bt  bt  用来打印栈帧指针,也可以在该命令后加上要打印的栈帧指针的个数,查看程序执行到此时,是经过哪些函数呼叫的程序,程序“调用堆栈”是当前函数之前的所有已调用函数的列表(包括当前函数)。每个函数及其变量都被分配了一个“帧”,最近调用的函数在 0 号帧中(“底部”帧)
  • frame  frame 1  用于打印指定栈帧
  • info reg  info reg  查看寄存器使用情况
  • info stack  info stack  查看堆栈使用情况
  • up/down  up/down  跳到上一层/下一层函数


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM