文章目录

  • 0.前言
  • 1.找出数组中第 k 大的数(手写代码)
  • 2.从n个数中找出最小的k个数(n>>k),最优平均时间复杂度是多少
  • 4.C 如何模拟实现 C++ 的类?
  • 5.TCP 与 UDP 的区别?
  • 6.HTTP 是基于 TCP 还是 UDP 实现的?
  • 7.使用 UDP 传输数据时如何知道数据成功发送到接收方?
  • 8.如何解决 TCP 黏包问题?
  • 9.单播、多播和广播的区别?
  • 10.如何判断主机字节序?
  • 11.网络字节序是大端还是小端?
  • 12.ARM 架构的字节序是什么?
  • 13.Linux 内核的同步方式?
  • 14.fork 和 pthread 在创建子进程和线程时最终会调用哪个系统 API?
  • 15.C++ 构造函数为什么不能有返回值?
  • 16.C++ 构造函数无返回值,如何判断对象是否构造成功?
  • 17.不用循环打印 1 到 N?
  • 18.C++ list 容器实现的是单链表还是双链表?
  • 19.QT 中信号与槽的简介?
  • 小结
  • 参考文献

0.前言

2016-09-09 日下午再一次参加了 CVTE 的 C++ 后台开发岗的面试,面试经历了 1 小时 20 分左右的时间,被问及了很多问题,很多问题也没有回答出来,自己还是存在很多知识盲点,需要潜心复习修炼,查漏补缺。手写代码也是没做好,下次一定要坚持写出来。总体来说,这场面试的难度对我来说不简单,现将回忆起的面试题与大家分享共勉。

1.找出数组中第 k 大的数(手写代码)

这是一道很经典的问题,《算法导论》中也有详细的介绍。回答时我与面试官说了一下自己的思路,有三种思路:
(1)方法一
申请一个长度为 k 的中间数组,然后遍历数组,将前 k 大的数放入在中间数组即可,时间复杂度为O(kN),假定数组长度为 N;这个解法是不合理的,因为可以不需要中间数组,且看下面的解决办法。

(2)方法二:简单选择排序
使用简单选择排序找出第 k 大的数,时间复杂度为O(kN),空间复杂度为 0。

方法一读者自行实现,因第二种方法较为简单,面试时手写代码需要在几分钟的时间写出来,当然是越简单越好,下面给出具体的实现。

//array:数组;len:数组长度;k:第k大
int getKthMax(int array[], int len, int k) {int maxIndex=0;for(int i=0;i<n;++i){maxIndex=i;for(int j=i+1;j<len;++j) {if(array[j]>array[maxIndex]) maxIndex=j;}if(maxIndex!=i) {int tmp=array[maxIndex];array[maxIndex]=array[i];array[i]=tmp;}}return array[k-1];
}

下面说一下当时的具体场景。面对这道题,我和面试官说完思路后,他好像并不在意我的想法,而是补充了应用场景,场景是“数组很长,项目经理要求我实现这个功能,问我该怎么实现,需不需要给数组排序”。我的回答是并不需要排序,因为排序消耗的时间较长。面试官补充到说:“应该排序,因为会多次用到。”对于这个问题我应该主动询问面试官是否需要多次查询,因为如果只需要查询一次,得不偿失,如果需要经常查询第 n 大的数,则需要排序,做到一劳永逸。这样回答的话就应该较为全面了,在面试的过程中,如果对面试官提出的问题有疑问,应该主动与之沟通。

面试官说那写一下代码吧,看能不能写出来。于是我就思考决定用哪一方法,以及如何实现,当我思考还未达到 1 分钟时,自己也没有主动放弃,面试官就说:“写不出来,咱们下一题吧。感觉他并不希望我能够写出来,也不想给我时间去写。我应该坚持,但在那种场景我不能坚持,只能跟着面试官的意思走,因为他是面试官。还有一个教训就是,在手写代码题我犯了大忌:talk is cheap,show me the code(少逼逼,亮代码)。说了太多,却没有代码。对于手写代码题,建议不要与面试官讨论自己的实现思路,而是按照自己的实现思路去写代码,再拿着自己的代码与面试官讨论自己的实现方式和不同的解法,如此做,自己的 talk 将不会那么低贱,显得更有分量,即使自己的方法不是最优解或者不是面试官想要的解,但至少你向面试官证明了自己的代码书写能力。

(3)第三种方法:冒泡排序
既然能用简单选择排序的方法找出第 n 大的数,当然可以使用冒泡排序的方法。第一趟冒泡第一大,第二次冒泡第二大,第 n 次冒泡第 n 大,时间复杂度为O(nM),M 为数组长度。因为比较简单,这里也不具体实现了,具体可以参考冒泡排序的实现。

(4)正确的解法:使用快速排序寻找数组中第 k 大的数
面试官提示我用快速排序思想去解决这个问题,但是自己还是没能够在面试官的提示下说出面试官心中想要的答案。

我们都知道快速排序是对冒泡的改进,使排序时间复杂度降低到O(nlgn),为什么要用快排来解决这个问题呢?那么快排的时间复杂度又是多少呢?

这里以递减排序为例,因为快排每次将数组划分为两组和一个分区元素,每一趟划分你只需要将 k 与分元素的下标进行比较,如果比分区元素下标+1大就从右边的子数组中找,如果比分区元素下标+1小从左边的子数组中找,如果一样则就是分区元素。如果需要从左边或者右边的子数组中再查找的话,只需要递归一遍查找即可,无需像快排一样两边都需要递归,所以复杂度必然降低。

平均时间复杂度如下:假设快排每次都平均划分,但都不在分区元素上找到第k大,直至最后一趟找到。

第一趟快排没找到,时间复杂度为 O(n),第二趟也没找到,时间复杂度为 O(n/2),…,最后一趟 log2nlog_2nlog2​n 趟找到,时间复杂度为 O(n/2log2n)=O(1)O(n/2^{log_2n})=O(1)O(n/2log2​n)=O(1),所以总的时间复杂度为 O(n(1+1/2+1/4+....+1/n))=O(2(n−1))=O(n)O(n(1+1/2+1/4+....+1/n))=O(2(n-1))=O(n)O(n(1+1/2+1/4+....+1/n))=O(2(n−1))=O(n),很明显比上面提出的方法快,相比于快速排序,虽然递归深度是一样的,但每一趟时间复杂度降低了一半,整体是收敛的。

示例代码:

/****************************************************
*@brief:利用快速排序以递减排序的方式找出无序数组中第k大的数
*@param:array 无序数组;low 左起始下标;high 右结束下标
*@ret:成功返回第 k 大的数,失败返回 -1
****************************************************/
int getNthMaxByQuickSort(int array[], int low, int high, int k) {// 参数校验if(low>high) return -1;if( k < 1 || k > high - low + 1) return -1;int left=low;int right=high;int key=array[left]; /*用数组的第一个记录作为分区元素*/while(left<right) {while(right>left&&array[right]<=key) --right;array[left]=array[right];while(left<right&&array[left]>=key) ++left;array[right]=array[left];}array[left]=key;if(left+1==k) return array[left];// 在左边寻找if(left+1>k) getNthMaxByQuickSort(array, low, left-1, k);// 在右边寻找else getNthMaxByQuickSort(array, left+1, high, k);
}// 测试
int main() {int array[]={1,1,5,9,2,3,6,8,7,4,0};cout<<getNthMaxByQuickSort(array,0,10,1)<<endl;    //9cout<<getNthMaxByQuickSort(array,0,10,2)<<endl;    //8cout<<getNthMaxByQuickSort(array,0,10,11)<<endl;   //0
}

2.从n个数中找出最小的k个数(n>>k),最优平均时间复杂度是多少

根据上面的分析,可见最优平均时间复杂度是O(n)。

#3.假设你有100MB的内存,需要对1GB的数据进行排序,最合适的算法是什么
给定四个选项,时间复杂度如下:

算法 平均时间复杂度
冒泡排序 O(n2)O(n^2)O(n2),不能用于外部排序
快速排序 nlogn,不能用于外部排序
直接插入排序 O(n2)O(n^2)O(n2),不能用于外部排序
归并排序 nlogn

所以应该选择归并排序。本题的考点主要有两个。一个是外部排序,一个是排序算法的特点和时间复杂度。

外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。

外部排序一般用归并排序,空间复杂度是O(n)。一般来说外部排序分为两个步骤:预处理和归并。首先,根据可用内存的大小,将外存上含有n个纪录的文件分成若干长度为t的子文件(或段);其次,利用内部排序的方法,对每个子文件的t个纪录进行内部排序。这些经过排序的子文件(段)通常称为顺串,顺串生成后将其写入外存。这样在外存上就得到了m个顺串(m=⌈n/t⌉m=\lceil n/t\rceilm=⌈n/t⌉)。最后,对这些顺串进行归并,使顺串的长度逐渐增大,直到所有的待排序的记录成为一个顺串为止。

注意,归并时,需要使用额外文件来存储最终的顺串。归并过程如下,遍历每一个子文件指针当前所指元素,取出最大或最小的元素,写入最终文件中。循环上面的步骤,直至所有子文件元素均被有序地写入最终文件。

4.C 如何模拟实现 C++ 的类?

C++ 定义类最大的特点是使程序面向对象而不是面向过程,这在 C 中是没有体现的。 类的标志性特征是封装,继承,多态,对象化提供了模型化和信息隐藏的好处,类化提供了可重用性的好处。使用 C 模拟实现 C++ 的类,必须也要拥有上面三个基本特征。
(1)封装
C中可以使用struct来模拟C++的类,将成员变量作为struct的成员变量, 成员函数由struct的函数指针变量来表示。
(2)继承
继承的替代实现方案可以使用组合的方式。将父struct的对象作为子struct的数据成员并放置在第一个位置。
(3)多态
同名的函数指针变量赋予不同的实现函数的地址,实现多态。或者模拟真实的C++多态实现的机制,可以将所有virtual函数的入口地址使用函数指针存放在一个结构体中,然后在C的结构体类中的增加一个成员变量指向该函数指针结构体对象。

C 如何模拟实现 C++ 的类,可以参考简单的实例:C实现C++类

你可能会发现一个问题,因为C中struct的成员访问权限只能是public,上面的模拟实现,没有模拟出类成员的private和protected访问权限,该如何实现呢?

网上查找了很多资料,还是没有找到如何实现,于是在CSDN论坛上发帖咨询,有两位热心的网友提供了如下设计方案,感觉可行。

公有变量作为struct中的字段,公有方法可以在struct中设置一个函数指针。私有变量和私有方法移到提供类的实现的源码文件中,并且用static修饰,这样就无法在源文件之外被访问。

// 头文件CCLssA.h
struct classA {int public_a, public_b, public_c;  func_t public_f1, public_f2, public_f3;
};//源文件CCLssA.c
typedef void (*func_t)(void);
static int private_a, private_b, private_c;
static void private_f1(void) {}
static void private_f2(void) {}
static void private_f3(void) {}//主文件main.c
struct classA obj;
obj.public_a;
obj.public_b;
obj.f1();

如此做结构体对象本质上还是不存在private成员,只是逻辑上将static全局变量作为自己的私有成员变量。要真正实现面向对象机制中的封装,继承和多态是需要编译器支持的,不可能简单凭C语言的特性来实现。具体可参考原贴C如何实现C++类的私有和公共?

5.TCP 与 UDP 的区别?

TCP(Transmission Control Protocol,传输控制协议)。

当应用层向TCP层发送用于网间传输的、用8位字节表示的字节流,TCP则把数据流分割成适当长度的报文段,最大传输段大小(MSS)通常受该计算机连接的网络的数据链路层的最大传送单元(MTU)限制。之后TCP把数据包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。

TCP通过连接管理、序列号、检验和、确认应答、重发控制以及窗口控制等机制实现可靠性传输。
(1)连接管理。是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。一个TCP连接必须要经过三次“对话”才能建立连接。
(2)序列号与重发控制。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
(3)在数据正确性与合法性上,TCP用一个校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和。
(4)在流量控制上,采用滑动窗口协议,协议中规定,对于窗口内未经确认的分组需要重传。
(5)在拥塞控制上,采用广受好评的TCP拥塞控制算法(也称AIMD算法)。该算法主要包括三个主要部分:1)加性增、乘性减;2)慢启动;3)对超时事件做出反应。

UDP(User Datagram Protocol,用户数据报协议)
(1) UDP是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
(2) 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务机可同时向多个客户机传输相同的消息。
(3) UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。
(4) 吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制。
(5)UDP 是面向报文的。发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。

我们经常使用 ping 命令来测试两台主机之间 TCP/IP 通信是否正常,其实“ping”命令的原理就是向对方主机发送 ICMP(面向无连接的协议) 数据包,然后对方主机确认收到数据包,如果数据包是否到达的消息及时反馈回来,那么网络就是通的。

TCP 与 UDP 区别小结:
(1)TCP基于连接,UDP无连接;
(2)TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达。UDP尽最大努力交付,即不保证可靠交付;
(3)每一条TCP连接只能是点到点的,UDP支持一对一,一对多,多对一和多对多的交互通信;
(4)TCP首部开销20字节,UDP的首部开销小,只有8个字节;
(5)TCP面向字节流模式,因此可能出现黏包问题;UDP面向数据报模式,数据报保留边界,不会出现黏包问题;
(6)UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等);

基于 TCP 的应用层协议:

FTP:21 Telnet:23 SMTP:25 POP3:110 HTTP:80 HTTPS:443

基于 UDP 的应用层协议:

DNS:53 TFTP:69 SNMP:161 RIP:520

6.HTTP 是基于 TCP 还是 UDP 实现的?

很显然是 TCP,结果没答出来,应该二选一猜一个的。

7.使用 UDP 传输数据时如何知道数据成功发送到接收方?

接收方返回确认包。

8.如何解决 TCP 黏包问题?

什么是TCP黏包问题?
首先了解两个概念:
两个简单概念长连接与短连接:
(1)长连接
Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收。

(2)短连接
Client方与Server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此种方式常用于一点对多点的通讯,比如多个Client连接一个Server。

如果利用TCP每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,在这种短连接的情况下就不会出现粘包问题,因为只有一种包结构,比如http协议包。但是在长连接的情况下,如果如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
(1)“hello give me sth abour yourself”
(2)“Don’t give me sth abour yourself”
那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hello give me sth abour yourselfDon’t give me sth abour yourself" 这样接收方就傻了,不知道接收的数据是什么,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保成功接收,正确理解。

所以,TCP黏包问题指的是发送端以字节流的形式发送不同结构的数据包无明显边界导致接收方无法正确解析数据包

TCP黏包出现的原因?
TCP以字节流传输数据,字节流之间无明显边界,UDP以数据包传输数据,数据报保留消息边界,不会出现粘包。这是根本原因,直接原因是:
(1)发送端需要等缓冲区满才发送出去,发送方造成粘包;
(2)接收方不及时接收缓冲区的包,多个包接收,接收方造成黏包。

解决办法。
为了避免粘包现象,可采取以下几种措施。一是对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;二是对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;三是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。

以上提到的三种措施,都有其不足之处。第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。第三种方法虽然避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。

唯一解决办法:制定应用层协议。
处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。在应用中处理粘包的基础方法主要有两种,分别是以4节字描述消息大小或以结束符,实际上也有两者相结合的如HTTP,Redis的通讯协议等。于是这里就涉及到封包和拆包的操作。

封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入"包尾"内容)。包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义.根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

拆包就是根据包头信息正确解析数据包。

9.单播、多播和广播的区别?

单播(Unicast)、多播(Multicast)和广播(Broadcast)这三个术语都是用来描述网络节点之间通讯方式。

(1)单播
主机之间一对一的通讯模式,网络中的交换机和路由器对数据只进行转发不进行复制。如果10个客户机需要相同的数据,则服务器需要逐一传送,重复10次相同的工作。但由于其能够针对每个客户的及时响应,所以现在的网页浏览全部都是采用单播模式,具体的说就是IP单播协议。网络中的路由器和交换机根据其目标地址选择传输路径,将IP单播数据传送到其指定的目的地。

单播的优点:
1)服务器及时响应客户机的请求
2)服务器针对每个客户不通的请求发送不通的数据,容易实现个性化服务。

单播的缺点:
1)服务器针对每个客户机发送数据流,服务器流量=客户机数量×客户机流量;在客户数量大、每个客户机流量大的流媒体应用中服务器不堪重负。
2)现有的网络带宽是金字塔结构,城际省际主干带宽仅仅相当于其所有用户带宽之和的5%。如果全部使用单播协议,将造成网络主干不堪重负。现在的P2P应用就已经使主干经常阻塞。而将主干扩展20倍几乎是不可能。

(2)多播(组播)
主机之间一对一组的通讯模式,也就是加入了同一个组的主机可以接受到此组内的所有数据,网络中的交换机和路由器只向有需求者复制并转发其所需数据。主机可以向路由器请求加入或退出某个组,网络中的路由器和交换机有选择的复制并传输数据,即只将组内数据传输给那些加入组的主机。这样既能一次将数据传输给多个有需要(加入组)的主机,又能保证不影响其他不需要(未加入组)的主机的其他通讯。

组播的优点:
1)需要相同数据流的客户端加入相同的组共享一条数据流,节省了服务器的负载。具备广播所具备的优点。
2)由于组播协议是根据接受者的需要对数据流进行复制转发,所以服务端的服务总带宽不受客户接入端带宽的限制。IP协议允许有2亿6千多万个组播,所以其提供的服务可以非常丰富。
3)此协议和单播协议一样允许在Internet宽带网上传输。

组播的缺点:
1)与单播协议相比没有纠错机制,发生丢包错包后难以弥补,但可以通过一定的容错机制和QOS加以弥补。
2)现行网络虽然都支持组播的传输,但在客户认证、QOS等方面还需要完善,这些缺点在理论上都有成熟的解决方案,只是需要逐步推广应用到现存网络当中。

(3)广播
主机之间一对所有的通讯模式,网络对其中每一台主机发出的信号都进行无条件复制并转发,所有主机都可以接收到所有信息(不管你是否需要),由于其不用路径选择,所以其网络成本可以很低廉。有线电视网就是典型的广播型网络,我们的电视机实际上是接受到所有频道的信号,但只将一个频道的信号还原成画面。在数据网络中也允许广播的存在,但其被限制在二层交换机的局域网范围内,禁止广播数据穿过路由器,防止广播数据影响大面积的主机。

广播的优点:
1)网络设备简单,维护简单,布网成本低廉
2)由于服务器不用向每个客户机单独发送数据,所以服务器流量负载极低。

广播的缺点:
1)无法针对每个客户的要求和时间及时提供个性化服务。
2)网络允许服务器提供数据的带宽有限,客户端的最大带宽=服务总带宽。例如有线电视的客户端的线路支持100个频道(如果采用数字压缩技术,理论上可以提供500个频道),即使服务商有更大的财力配置更多的发送设备、改成光纤主干,也无法超过此极限。也就是说无法向众多客户提供更多样化、更加个性化的服务。
3)广播禁止允许在Internet宽带网上传输。

10.如何判断主机字节序?

主机字节序又叫做 CPU 字节序,不是由操作系统决定的,而是由 CPU 指令集架构决定的。主机字节序分为两种,大端字节序(Big Endian)和小端字节序(Little Endian)。

考虑一个 16 位整数,它由 2 个字节组成。内存中存储这两个字节有两种方法:一种是将低序字节存储在低地址中,高字节存储在高地址中,这称为小端字节序。另一种是将高序字节存储在低地址中,低序字节存储在高地址中,这称为大端字节序。

判断方法
假设我们的32位整数0x12345678是从起始位置为0x00的地址开始存放,则:

0x00  0x01    0x02    0x03
78    56      34      12  (小端)
12    34      56      78  (大端)

因此,我们可以这样判断:

int main() {  int i = 0x12345678;  if(*((char*)&i) == 0x12) printf("大端");  else printf("小端");return 0;
}

11.网络字节序是大端还是小端?

网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序是大端字节序。

12.ARM 架构的字节序是什么?

ARM 公司的 ARM 架构处理器大端和小端都支持,默认是小端模式。

Intel、AMD x86 架构的 CPU 是小端模式。

IBM 公司 PowerPC 架构的 CPU 是大端字节序,Motorola 处理器也是大端字节序。

13.Linux 内核的同步方式?

首先问了我线程的同步方式有哪些,我回答的是 Linux 下线程的同步方式。Linux 下提供了多种方式来处理线程同步,最常用的是互斥量、条件变量和信号量,还有不常用的读写锁,自旋锁和屏障

然后面试官又问我Linux内核的同步方式,没有研究过内核,懵逼。首先解释一下几个与同步有关的术语。

临界区(Critical Region):
指的是访问临界资源的程序片段,而这个程序片段不能并发执行,必须被原子地的执行。

竞争状态:
多个任务(进程或线程)同时访问同一临界区。

同步(synchronization):
避免竞争状态,使多个任务有序互斥的访问临界区。

查询网络资源,了解到Linux内核大概有如下几种同步方式:中断屏蔽、原子操作、自旋锁和信号量。

(1)中断屏蔽。
在单CPU范围内避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。由于linux内核的进程调度等操作都依赖中断来实现,发生内核抢占时进程之间的并发也就得以避免了。内核抢占(kernel preemption)指的是若内核具有抢占性,一个在内核态运行的进程,当且仅当在执行内核函数期间被另外一个进程取代。Linux 2.6引入了内核抢占机制。

中断屏蔽的使用方法:

local_irq_disable()//屏蔽中断
// 临界区
local_irq_enable()//开中断

特点:
由于 Linux 系统的异步IO,进程调度等很多重要操作都依赖于中断,在屏蔽中断期间所有的中断都无法得到处理,因此长时间的屏蔽是很危险的,有可能造成数据丢失甚至系统崩溃,这就要求在屏蔽中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。

中断屏蔽只能禁止本CPU内的中断,因此,并不能解决多CPU引发的竞态,所以单独使用中断屏蔽并不是一个值得推荐的避免竞态的方法,它一般和自旋锁配合使用。

(2)原子操作。
原子操作指的是在执行过程中不会被中断的操作。原子原本指的是不可分割的微粒,所以原子操作也就是不能够被分割的指令。

原子操作是不可分割的,在执行完毕不会被任何其它任务或事件中断。在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_settest_and_clear等指令用于临界资源互斥的原因。但是,在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。

下面看一个本应该是原子操作,结果出现了错误。
我们的程序逻辑经常遇到这样的操作序列:
1.读一个位于 memory 中的变量的值到寄存器中;
2.修改该变量的值(也就是修改寄存器中的值);
3.将寄存器中的数值写回 memory 中的变量值。

如果这个操作序列是串行化的操作(在一个线程中串行执行),那么一切 OK,然而,世界总是不能如你所愿。在多CPU体系结构中,运行在两个CPU上的两个内核控制路径同时并行执行上面操作序列,有可能发生下面的场景:

CPU1上的操作 CPU2上的操作
读操作
读操作
修改 修改
写操作
写操作

多个 CPU 和 Memory Chip 是通过总线互联的,在任意时刻,只能有一个总线 master 设备(例如 CPU、DMA Controller)访问该Slave设备(在这个场景中,slave设备是 RAM chip)。因此,来自两个 CPU 上的读memory操作被串行化执行,分别获得了同样的旧值。完成修改后,两个CPU都想进行写操作,把修改的值写回到memory。但是,硬件arbiter的限制使得 CPU 的写回必须是串行化的,因此CPU1首先获得了访问权,进行写回动作,随后,CPU2 完成写回动作。在这种情况下,CPU1 的对 memory 的修改被 CPU2 的操作覆盖了,因此执行结果是错误的。

不仅是多 CPU,在单 CPU 上也会由于有多个内核控制路径的交错而导致上面描述的错误。一个具体的例子如下:

系统调用的控制路径 中断handler控制路径
读操作
读操作
修改
写操作
修改
写操作

系统调用的控制路径上,完成读操作后,硬件触发中断,开始执行中断 handler。这种场景下,中断 handler 控制路径的写回的操作被系统调用控制路径上的写回覆盖了,结果也是错误的。

如何解决上面的问题呢?在 Linux 内核提出了原子操作,原子操作又分为原子整数操作和原子位操作,下面我们来看看这两个操作用法。

原子整数操作。
内核提供了一个特殊的类型 atomic_t,具体定义如下:

typedef struct { int counter;
}atomic_t;

相应的对该类型变量的操作提供了一系列 API,因在一系列API使用利用了指令集的特性和锁来保证对atomic_t类型变量的操作是原子操作。

原子位操作。
除了原子整数操作外,内核还提供了一组针对位这一级数据进行操作的函数,位操作函数是对普通的内在地址进行操作的,它的参数是一个指针和一个位号。由于是对普通的指针进程操作,所以没有像atomic_t这样的类型约束。

unsigned long word = 0;
set_bit(0,&word);
clear_bit(0,&word);
change_bit(0,&word);//翻转第0位的值

原子整数操作和原子位操作的相应的API介绍可以参考Linux内核的原子操作。

(3)自旋锁(spin lock)。
Linux内核中最常见的锁是自旋锁,自旋锁最多只能被一个可执行线程持有,如果一个执行线程试图获得一个被争用(已经被持有)的自旋锁,那么该线程就会一直进行忙循环—旋转—等待锁重新可用,要是锁未被争用,请求锁的执行线程便能立刻得到它,继续执行,在任意时刻,自旋锁都可以防止多个执行线程同时进入临界区。注意同一个锁可以用在多个位置,对于给定数据的所有访问都可以得到保护和同步。

自旋锁在Linux内核同步和线程同步的区别。
这里自旋锁与Linux用户态线程同步使用的自旋锁实现原理是相同的,但在使用上有一点区别。Linux线程同步使用的自旋锁供应用程序使用,在用户态使用且使用次数比较少,接口也不相同。内核同步使用的自旋锁在内核使用的比较多,且只能工作在内核态。

(4)信号量(semaphore)。
Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它绝不可能在内核之外使用,它是一种睡眠锁。如果有一个任务想要获得已经被占用的信号量时,信号量会将其放入一个等待队列,然后让其睡眠。当持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并让其获得信号量。

信号量与自旋锁的区别:
(1)自旋锁让一个任务(进程或线程)旋转,使CPU处于忙等状态,此时不能去执行其它的代码,但信号量使任务睡眠,此时CPU处于闲等状态,可以去执行其它的代码。
(2)由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况;相反,自旋锁一般短时间被持有,如果使用信号量就不太适宜了,因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁占用的全部时间表还要长;
(3)信号量允许有多个持有者,而自旋锁在任何时候只能允许一个持有者。

14.fork 和 pthread 在创建子进程和线程时最终会调用哪个系统 API?

fork,vfork,clone,都是系统调用,三者不存在谁调用谁的关系。但三者最终都会调用do_fork。
pthread_create是对clone的封装,最终会调用clone。

clone系统调用就是一个创建轻量级进程的系统调用:

int clone(int (*fn)(void * arg), void *stack, int flags, void * arg);

所以 fork 和 pthread 在创建子进程和线程时最终会调用 do_fork。但面试官给我的回答是clone。可能问题问的有点问题,或者问题里面没有问 fork 最终调用的哪一个API。

15.C++ 构造函数为什么不能有返回值?

在 C++ 设计时规定构造函数和析构函数均不能有返回值,连 void 也不行,函数体内也不能使用 return。那为什么要这样设计呢?

构造函数不需要用户调用,在创建一个对象时会自动执行构造函数。当然也可以直接调用构造函数。构造函数返回的就是这个类的对象this指针,这是不能改变的,默认的。所以构造函数不能指定返回值。参考如下代码:

class Test {int i;
public:Test(int i);
};Test::Test(int a) {i=a;
}

在 VS2012 对应的汇编代码是:

class Test {int i;
public:Test(int i);
};Test::Test(int a){
003DA9C1  mov         ebp,esp
003DA9C3  sub         esp,0CCh
003DA9C9  push        ebx
003DA9CA  push        esi
003DA9CB  push        edi
003DA9CC  push        ecx
003DA9CD  lea         edi,[ebp-0CCh]
003DA9D3  mov         ecx,33h
003DA9D8  mov         eax,0CCCCCCCCh
003DA9DD  rep stos    dword ptr es:[edi]
003DA9DF  pop         ecx
003DA9E0  mov         dword ptr [this],ecx  i=a;
003DA9E3  mov         eax,dword ptr [this]
003DA9E6  mov         ecx,dword ptr [a]
003DA9E9  mov         dword ptr [eax],ecx
}
003DA9EB  mov         eax,dword ptr [this]   //看这里,将this赋给eax作为构造函数的返回值
003DA9EE  pop         edi
003DA9EF  pop         esi
003DA9F0  pop         ebx
003DA9F1  mov         esp,ebp
003DA9F3  pop         ebp
003DA9F4  ret         4

下面直接调用构造函数,来验证构造函数返回的是类对象的this指针。直接调用构造函数的方法是调用placement new(),关于什么事placement new和operator new可以参考我的另一篇博文:C++中的定位放置new(placement new)。

#include <stdint.h>#include <iostream>
using namespace std;class Test {
public:int i;Test(int a){i=a;}
};int main(){void* mem=new uint8_t[sizeof(Test)];Test* pT=NULL;pT=new (mem) Test(8); //直接调用构造函数,返回对象this指针cout<<mem<<endl;        //输出:005092D0cout<<pT<<endl;         //输出:005092D0cout<<pT->i<<endl;       //输出:8

析构函数为什么也不能有返回值呢?
析构函数不带任何参数,也不能有返回值,为什么要这样设计析构函数呢?

析构函数不带任何参数是因为析构函数仅仅只是负责对类指针成员指向的空间进行释放,不需要有任何参数。

析构函数同构造函数一样,不需要用户调用,而是在销毁对象时自动执行。但我们也可以直接调用析构函数,但这样做容易导致对内存空间释放两次,使程序出错。鉴于析构函数总是由编译器来生成调用析构函数的代码,以确保它们被执行,如果析构函数有返回值,要么编译器必须知道如何处理返回值,要么就只能由客户程序员自己来显式的调用构造函数与析构函数,这样一来,安全性就被破坏了。所以析构函数同构造函数一样,不能为之指定返回值。

16.C++ 构造函数无返回值,如何判断对象是否构造成功?

两个办法。
(1)在 C++ 构造函数中抛出异常,但要注意资源泄漏问题,因为 C++ 拒绝为没有完成构造函数的对象调用析构函数;
(2)向 C++ 构造函数多传递一个标志参数,通过该参数来判断对象是否构造成功。

17.不用循环打印 1 到 N?

方法一:递归。

void print1ton(int n) {   if(n>1) {print2n(n-1);  }printf("%d\n", n);
}

该方法简洁明了,适合在面试的适合手写出来。

方法二:类对象的引用计数。

#include <iostream>
using namespace std;  class A {
static int counter;
public:A(){cout <<counter++ <<endl;  }
};  int A::counter = 1;  int main(){#define N 10A array[N];return 0;
}

18.C++ list 容器实现的是单链表还是双链表?

双链表。

19.QT 中信号与槽的简介?

因为简历中写到了关于QT的项目,所以被问到了这个问题。

QT 是一个跨平台的 C++ GUI 应用构架,它提供了丰富的窗口部件集,具有面向对象、易于扩展、真正的组件编程等特点。

信号与槽是 QT 自行定义的一种对象通信机制,也是 QT 的核心机制,它独立于标准的C/C++ 语言。

信号:
当对象改变其状态时,信号就由该对象发射 (emit) 出去,而且对象只负责发送信号,它不知道另一端是谁在接收这个信号。这样就做到了真正的信息封装,能确保对象被当作一个真正的软件组件来使用。

当一个信号被发射时,与其相关联的槽将被立刻执行,就象一个正常的函数调用一样。信号-槽机制完全独立于任何GUI事件循环。只有当所有的槽返回以后发射函数(emit)才返回。 如果存在多个槽与某个信号相关联,那么,当这个信号被发射时,这些槽将会一个接一个地 执行,但是它们执行的顺序将会是随机的、不确定的,我们不能人为地指定哪个先执行、哪 个后执行。

信号的声明是在头文件中进行的,QT 的 signals 关键字指出进入了信号声明区,随后即可 声明自己的信号。例如,下面定义了三个信号:

signals: void mySignal();void mySignal(int x);void mySignalParam(int x,int y);

在上面的定义中,signals是QT的关键字,而非C/C++的。接下来的一行void mySignal() 定义了信号mySignal,这个信号没有携带参数;接下来的一行void mySignal(int x)定义 了重名信号mySignal,但是它携带一个整形参数,这有点类似于C++中的虚函数。从形式上 讲信号的声明与普通的C++函数是一样的,但是信号却没有函数体定义,另外,信号的返回 类型都是void,不要指望能从信号返回什么有用信息。

信号由 moc 自动产生,它们不应该在.cpp文件中实现。moc(Meta Object Compiler)是QT的工具,该工具是一个C++预处理程序,它为高层次的事件处理自动生成所需要的附加代码。

槽:
槽是普通的C++成员函数,可以被正常调用,它们唯一的特殊性就是很多信号可以与其相关联。当与其关联的信号被发射时,这个槽就会被调用。

既然槽是普通的成员函数,因此与其它的函数一样,它们也有存取权限。槽的存取权限决定了谁能够与其相关联。同普通的C++成员函数一样,槽函数也分为三种类型,即public slots、private slots和protected slots。

public slots:在这个区内声明的槽意味着任何对象都可将信号与之相连接。这对于组件编程非常有用,你可以创建彼此互不了解的对象,将它们的信号与槽进行连接以便信息能够正确的传递。
protected slots:在这个区内声明的槽意味着当前类及其子类可以将信号与之相连接。这适用于那些槽,它们是类实现的一部分,但是其界面接口却面向外部。
private slots:在这个区内声明的槽意味着只有类自己可以将信号与之相连接。这适用于联系非常紧密的类。
槽也能够声明为虚函数,这也是非常有用的。
槽的声明也是在头文件中进行的。例如,下面声明了三个槽:

public slots:void mySlot();void mySlot(int x);void mySignalParam(int x,int y);

信号与槽的关联:
只有将信号与槽关联在一起,当某个对象发送信号时,信号对应的槽才会被触发执行。通过调用 QObject 对象的 connect 函数来将某个对象的信号与另外一个对象的槽函数相关联,这样当发射者发射信号时,接收者的槽函数将被调用。该函数的申明如下:

bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * member ) [static]

这个函数的作用就是将发射者 sender 对象中的信号 signal 与接收者 receiver 中的member 槽函数联系起来。当指定信号signal时必须使用 QT 的宏 SIGNAL(),当指定槽函数时必须使用宏 SLOT()。如果发射者与接收者属于同一个对象的话,那么在 connect 调用中接收者参数可以省略。
例如,下面定义了两个对象:标签对象label和滚动条对象scroll,并将valueChanged()信号与标签对象的setNum()相关联,另外信号还携带了一个整形参数,这样标签总是显示滚动条所处位置的值。

QLabel *label  = new QLabel;
QScrollBar *scroll = new QScrollBar;
QObject::connect( scroll, SIGNAL(valueChanged(int)), label,  SLOT(setNum(int)) );

信号与槽之间的关系 :
(1)一个信号可以连接多个槽。
当信号发射时,会以不确定的顺序一个接一个的调用各个槽。

(2)多个信号可以连接同一个槽。
即无论是哪一个信号被发射,都会调用这个槽。

(3)信号直接可以相互连接。
发射第一个信号时,也会发射第二个信号。

(4)连接可以被移除。
这种情况用得比较少,因为在对象被删除时,Qt会自动移除与这个对象相关的所有连接。语法如下:

disconnect(sender, SIGNAL(signal), receiver, SLOT(slot));

(5)信号的参数与槽的参数的关系
信号的参数个数必须大于等于槽的参数个数,并且与槽的参数类型要一一对应,超过槽的参数后面的参数会被忽略。需要注意的是,槽的参数不能有缺省值。

小结

整个面试过程被面试官牵着鼻子走,回答的也有点吃力,问题涉及到的知识点纷杂多样,涉及到编程语言,Linux操作系统,计算机网络,算法与数据结构,计算机组成原理等知识。很遗憾,还是折戟沉沙,铩羽而归。尽管如此,还是要感谢 CVTE 给我这次面试的机会,让我认识到了自己的不足。分析了一下失败的原因,主要有两方面,一是,从上一次参加了 CVTE 2016 春季实习校招的面试,再到这一次校招面试,两次面试,CVTE 的校招宣讲、笔试和面试都比其他很多公司要早,给我个人的感觉就是太重宣传,招人甚少,难逃未有招人诚心之嫌,僧多粥少,这也就导致了竞争压力较大。二是,这也是最根本的原因就是个人能力不足,面试没有做到充足的准备。面试是个技巧活,即使工作经验有多么丰富,也不能等同于自己的面试求职能力有多强。面试的问题涉及到的知识点,工作中往往只能用到其中的一部分,长时间不温习,很容易会忘记。所以面试一定要好好准备。

个人的面试的心得吧,面试过程中,求职者应做到:礼貌,谦逊,少说。说的越多,错的就可能越多,很多问题答到点上就行了,要做到应适可而止。

从 9 号面试,到今天 14 号,历时 5 天,终于坚持整理出了面试过程遇到的问题。明天就是猴年中秋了,祝愿在 IT 道路上渐行渐远的网友们节日快乐。仅以此篇博客纪念我第二次 CVTE 被虐之旅,对 CVTE 已累觉不爱。


参考文献

[1] 求一个数组中第k大的数方法
[2] TCP与UDP的区别
[3] 百度百科.TCP
[4] (经典)tcp粘包分析
[5] TCP通讯处理粘包详解
[6] 单播、多播(组播)和广播的区别
[7] 如何判断字节序
[8] 内核抢占
[9] Linux内核的同步机制之四--信号量
[10] fork, vfork, clone,pthread_create,kernel_thread
[11] c++构造函数有返回值吗?
[12] Qt入门之信号与槽机制
[13] QT的信号与槽机制介绍

CVTE 2017 秋季校招一面(C++ 后台)相关推荐

  1. CVTE 2017 秋季校招笔试题回忆(C++后台)

    1.概述 2016.09.06 晚参加了 CVTE C++ 岗的在线笔试.笔试题型分为不定向选择题和编程题,总共 27 题.其中不定项选择题为 25 道,编程题 2 道.其特点是不定项选择题不告诉你是 ...

  2. 华为 2017 秋季校招笔试题(C++ 后台)

    文章目录 1.删除字符串中的指定字符 1.1 问题描述 1.2 问题求解 2.成绩排名 2.1 问题描述 2.2 问题求解 3.字符串变换最小费用 3.1 问题描述 3.2 递归法求解 3.2 动态规 ...

  3. CVTE 2017春季校招技术类面试问题

    我参加了CVTE2017年春季校园招聘,投递的为研发岗,在这里大致回忆一下整个面试流程和自己了解到的面试问题,希望可以对看到比篇文章的童鞋有些许帮助~~ 首先说以下春季校园的整个招聘流程:先是网申,简 ...

  4. 2017秋季校园招聘iOS开发岗位面试题集锦

    笔者参加了2017秋季不少的校招iOS岗位面试,下面我把这些面试题都贴出来和大家共勉: 一.搜狐快站 1.谈谈你做过的项目: 2.项目中最有成就感的部分: 3.倒计时如何实现?(NSTimer,还有其 ...

  5. 2018年秋季校招投递记录

    这个文章用来记录我2018年秋季校招投递记录 8月底抓内推的尾巴,总共在内推期间投了有有大约30家,没一家邀请面试,要么没结果,要么简历挂,要么转笔试.拿给阿里巴巴的学长检查简历,他又说没问题,这个简 ...

  6. 苹果 2017 秋季产品发布会直播预告

    他是谁? 覃超,峰瑞资本技术合伙人,前 Facebook 早期员工.现在在锋瑞资本做互联网早期项目投资,专注于开发者工具.垂直社区.在线教育和消费升级.技术出身,写代码爱好者,潜在创业者,former ...

  7. 2019年秋季校招前端面经

    19年秋季校招已经结束,因为一直在实习,参与笔试面试的公司并不多,错过了很多公司面试.已经拿到百度,腾讯,拼多多等offer,面筋如下,希望对大家能有帮助: 阿里 天猫 8.3 1.垂直水平居中 越多 ...

  8. 【2017秋季校园招聘笔经面经专题汇总】

    [2017秋季校园招聘笔经面经专题汇总] 2017秋招进行时,牛妹特意为大家开放2017秋招面经专栏,同时面经活动也正在进行中,欢迎大家参与~~活动详情戳:http://www.nowcoder.co ...

  9. 2021年秋季校招_软件技术_Java开发工程师_求职心得

    2021年秋季校招求职心得 前言   一晃二十多年过去了,00后也开始面临着就业的难题:究竟是去腾讯好呢,还是去字节好呢...我不知道,但装B的一律打死.   这是人生中一次重要的转折点,标志着我们从 ...

最新文章

  1. Git详解之九 Git内部原理
  2. WinAPI: waveInGetNumDevs - 获取波形输入设备的数目
  3. 第10周项目实践 线索二叉树的建立及遍历
  4. Jackson学习二之集合类对象与JSON互相转化--转载
  5. GVIM中文编码出现乱码的解决方案
  6. 给定数组Arr[n],O(n)时间内找出每个元素左侧所有元素中位置最靠近该元素且大于该元素的元素
  7. pyqt5与matplotlib结合画图 ,绘制动态图形
  8. 怎么new一个指针_19. Go语言里的 make 和 new 有什么区别?
  9. mybatis默认的数据源连接池(PooledDataSource和UnPooledDataSource)
  10. 第14讲:Selenium 的基本使用
  11. 解决Qt5 Creator无法切换输入法(fcitx),Ubuntu中不能使用搜狗输入法录入汉字问题...
  12. 计算机对中断的处理是在用户态下进行的,电大本科生作业系统作业3与答案.doc...
  13. mysql内存数据库性能_Mysql内存表配置及性能测试
  14. 通过Bigtop编译Hadoop组件的rpm包
  15. centos7下安装mysql及测试centos_CentOS7安装mysql8
  16. C语言考研初试学习(预备篇)
  17. 华为OLT快速配置指南
  18. Qt日历控件显示周数为:日一二三四五六
  19. 那些设计出来就不希望别人看懂的C代码——IOCCC国际模糊C代码大赛
  20. 【隧道应用-3】Cobalt Strike正向连接多层内网

热门文章

  1. 开源自动化服务器软件 Jenkins 被曝严重漏洞,可泄露敏感信息
  2. PenMount Touch显示鼠标指针驱动安装
  3. 为你的Web程序加个启动画面
  4. 初学Python之利用map编写姓名格式化输出函数
  5. 15款优雅的 WordPress 电子商务网站主题
  6. windows端口号速查表
  7. 菜鸟必须了解的4种后门技术知识!
  8. Mysql 批量更改数据表和字段的字符集(Mysql UTF-8字符集更改为utf8mb4字符集)
  9. 蓝桥杯 ALGO-145 算法训练 4-1打印下述图形
  10. 蓝桥杯 BEGIN-3 入门训练 圆的面积