深信服面试记录

1. 什么是哈希冲突?解决哈希冲突的常用方法有哪些?

  • 由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。
  • ①:开放地址法(再散列法):发生哈希冲突后,按照某一次序找到下一个空闲的单元,把冲突的元素放入。
  • ②:链地址法(拉链法):将哈希值相同的元素构成一个链表,head 放在散列表中。一般链表长度超过了 8 就转为红黑树,长度少于 6 个就变为链表。
  • ③:再哈希法:同时构造多个不同的哈希函数,Hi = RHi(key) i= 1,2,3 … k;
    H1 = RH1(key) 发生冲突时,再用 H2 = RH2(key) 进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。
  • ④:创建公共溢出区:把哈希表分为公共表和溢出表,如果发生了溢出,溢出的数据全部放在溢出区。

2. 快速排序的时间复杂度?如何防止其退化为冒泡排序?

  • 快速排序最优的情况下时间复杂度为:O( nlogn ),最差情况下时间复杂度:O( n^2 )
  • 快速排序的基准元素的选取非常重要,如果基准元素选取不当,可能影响排序过程的时间复杂度和空间复杂度。为了避免快速排序退化为冒泡排序以及递归栈过深等问题,通常依照“三者取中”的法则来选取基准元素。三者取中法是指在当前待排序的子序列中,将其首元素、尾元素和中间元素进行比较,在三者中取中值作为本趟排序的基准元素。
    3. 常见段错误的原因以及排错方法?
  • Linux 常见的 Segmentation Fault段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址、访问了系统保护的内存地址、访问了只读的内存地址等等情况。
  • ①:访问了不存在的地址:
    int *p = NULL;
    *p = 0;
    
  • ②:访问系统保护的内存地址:
     int *ptr = (int *)0;
     *ptr = 100;
    
  • ③:访问只读的内存地址:
    char *ptr = "test";
    strcpy(ptr, "TEST");
    
  • ④:栈溢出
    void main()
    {
        main();
    }
    
  • 段错误信息的获取
  • dmesg 可以在应用程序 crash 掉时,显示内核中保存的相关信息。如下所示,通过 dmesg 命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等。
  • 段错误的调试方法
  • 使用 printf 输出信息,使用 gccgdb

4. new 和 malloc 的区别?

  • new/deleteC++ 运算符关键字,需要编译器支持。malloc/free 是库函数,需要头文件支持。
  • 使用 new 操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而 malloc 则需要显式地指出所需内存的尺寸。
  • new 操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故 new 是符合类型安全性的操作符。而 malloc 内存分配成功则是返回 void * ,需要通过强制类型转换将 void *指针转换成我们需要的类型。
  • new 内存分配失败时,会抛出 bac_alloc 异常。malloc 分配内存失败时返回 NULL
  • new 操作符从自由存储区(free store)上为对象动态分配内存空间,而 malloc 函数从堆上动态分配内存。自由存储区是 C++ 基于 new 操作符的一个抽象概念,凡是通过 new 操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C 语言使用 malloc 从堆上分配内存,使用 free 释放已分配的对应内存。自由存储区不等于堆,如上所述,布局 new 就可以不位于堆中。
  • new 会先调用 operator new 函数,申请足够的内存(通常底层使用 malloc 实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete 先调用析构函数,然后调用 operator delete 函数释放内存(通常底层使用 free 实现)。malloc/free 是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
  • new 申请的内存通过 delete 释放;malloc 申请的内存通过 free 释放,new [ ] 申请的内存通过 delete [ ] 释放。

5. C 与 C++ 的混合编程?

  • 需要用 extern "C" 来强制编译器不要修改你的函数名。即指示编译器这部分函数按照 C 语言的标准来编译,而不是按照 C++ 的来。由于 CPP 支持多态性,也就是具有相同函数名的函数可以完成不同的功能,CPP 通常是通过参数区分具体调用的是哪一个函数。在编译的时候,CPP 编译器会将参数类型和函数名连接在一起,于是在程序编译成为目标文件以后, CPP 编译器可以直接根据目标文件中的符号名将多个目标文件连接成一个目标文件或者可执行文件。但是在 C 语言中,由于完全没有多态性的概念,C 编译器在编译时除了会在函数名前面添加一个下划线之外,什么也不会做(至少很多编译器都是这样干的)。

6. C++ 的多态

  • 多态就是不同对象对同一行为会有不同的状态,例如长方形类和圆形类同一个求面积函数名字,但计算方式不同;
  • 多态分为:静态多态(编译时的多态);动态多态(运行时的多态);
  • 函数重载就是一种静态多态:函数重载的规则:
  • ①:函数名称必须相同。
  • ②:参数列表必须不同(个数不同或类型不同或参数排列顺序不同)。
  • ③:函数的返回类型可以相同也可以不相同。
  • ④:仅仅返回类型不同不足以成为函数的重载。
  • C 语言只是在函数名前添加了下划线,因此当工程中存在相同函数名的函数,就会产生冲突; C++支持多态是因为,C++ 的编译器在底层实际使用的不是函数的名字,而是被重新修饰过的一个较复杂的名字,被重新修饰后的名字包含了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求其参数列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在最终的名字中,就可保证名字在底层的全局唯一性。
  • C++ 实现多态有两个条件: 一是虚函数重写,重写就是用来设置不同状态的;二是对象调用虚函数时必须是指针或者引用

7. 构造函数可以重载吗?
史立坤的回答

  • 从 C++ 之父 Bjarne 的回答我们应该知道 C++ 为什么不支持构造函数是虚函数了,简单讲就是没有意义。虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。
  • 网络上还有一个很普遍的解释是这样的:虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
  • 本人对这个观点并不认同,这主要是因为用什么方式实现虚函数是编译器的事情,使用Vtable只是大多数编译器采用的一种手段,并不代表编译器实现不了虚构造函数,编译器之所以不支持虚构造函数主要原因就是没有必要,所以正好这种实现方式也不支持,巧合而已。

8. 描述 TCP 连接的各个状态过程
引用dreamispossible的原文链接:我觉得写得不错,略加修改改成符合自己思维的版本:

  • 刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后:
  • 第一次握手:客户端给服务端发一个 SYN 报文(即同步序列号报文),并指明客户端的初始化序列号 ISN© 。此时客户端处于 SYN_Send 状态。
  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN 报文,此时服务器处于 SYN_REVD 的状态。
  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 establised 状态。
  • 服务器收到 ACK 报文之后,也处于 establised 状态,此时,双方以建立起了链接。
  • 在 socket 编程中,客户端执行 connect() 时,将触发三次握手。
  • hellomyshadow原文链接
  • 服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立起连接,服务器会把此种状态下的请求连接放入一个队列中,此队列称之为半连接队列。
  • 与之对应,全连接队列,就是已经完成了三次握手,建立起的连接被放入的队列。如果队列满了,就可能会出现丢包现象。
  • 服务器发送完 SYN_ACK 后,如果未收到客户端确认包,服务端进行首次重传;等待一段时间仍未收到确认包,则进行第二次重传;如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
    注意:每次重传所等待的时间不一定相同,一般会是指数增长,如1s、2s、4s、8s …
  • TCP 连接的拆除需要发送四个包,因此称为四次挥手,客户端或服务器均可主动发起挥手动作。
  • 刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求,四次挥手的过程如下:
  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态;
  • 即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭 TCP 连接,进入 FIN_WAIT1 (终止等待1)状态,等待服务端的确认。
  • 第二次挥手:服务端收到FIN包之后,会发送ACK报文,且把客户端的序列号值+1 作为ACK报文序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT状态;
  • 即服务端收到连接释放的报文段后,立即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入 CLOSE_WAIT(关闭等待)状态,此时的 TCP 处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入 FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段;
  • 第三次挥手:如果服务端也想断开连接,和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 状态。
  • 即服务端没有要向客户端发送的数据,服务端发出连接释放的报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入 LAST_ACK(最后确认)状态,等待客户端确认。
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值+1 作为自己的 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态,需要过一阵子才会进入 CLOSED 状态,为了确保服务端收到自己的 ACK 报文。服务端收到 ACK 报文之后,就会关闭连接,也进入 CLOSED 状态。
  • 即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入 TIME_WAIT(时间等待)状态。此时 TCP 未释放掉,需要经过时间等待计数器设置的时间 2MSL(报文最大生存时间)后,客户端才会进入 CLOSED 状态。
  • 在 socket 编程中,任何一方执行 close() 操作即可产生回收操作。

9. 高并发?忘了

  • 不会 23333

10. socket 编程的函数

  • 只说了一个 connect( ) 函数…
  • 还想说一个 close( ) 函数的但是说完 connect( ) 函数就问了我一个我不会也没有记住的问题了…

你可能感兴趣的:(C++,2020)