面经
面试总要总结吧
自我介绍
- 面试官您好,我是,毕业于XXX大学计算机科学与技术专业。
- 在校期间,我独立完成了项目…,除了本校课程….
- 在做项目期间遇到问题能谷歌/stackoverflow,查看相关手册,独立思考解决问题,且能够利用一些工具提高开发效率。
- 我对计算机系统方向感兴趣。
- 除此之外,喜欢阅读原版技术书籍,文学类书籍,热爱体育运动,热爱与他人交流讨论问题。希望贵公司可以给我一个机会。
计网/网络编程
- socket编程
- 服务器端
- socket。创建监听描述符
- bind。绑定IP地址和端口号,ip地址和端口号可以确定一台计算机上的一个进程
- listen。第二个参数为内核监听队列的最大长度,超过这个长度服务器将不受理新的客户连接(backlog真正取min(backlog, /proc/sys/net/core/somaxconn));backlog只表示完全连接和半连接的上限
- accept。等待客户端建立,建立成功则返回连接描述符(用于通信)
- send/recv
- close。主动关闭一方进入FIN_WAIT2
- 客户端
- socket
- connect
- send/recv
- close
- 服务器端
- TCP,UDP的区别
- TCP是面向连接的、可靠的(确认应答、超时重传、拥塞控制)、有序的(序列号);UDP是无连接的、不可靠的、无序的
- TCP开销比UDP大,TCP头部需要20个字节,UDP头部只需要8个字节
- TCP头部
- UDP头部
- TCP有拥塞控制,UDP没有拥塞控制
- TCP通过字节流传输,UDP中每一个包都是单独的
- TCP提供可靠的服务,适用于可靠性要求高的场景;UDP传输效率高,适用于高速传输和实时性要求的场景
- TCP仅支持一对一连接
- TCP是如何保证可靠的?
- 确认应答机制
- 超时重传
- 序号(一个字节对应一个序号)
- UDP什么不可靠?
- 不保证消息交付:不确认、无超时重传
- 不保证交付顺序:不设置序列号,不重排,不会发生队首阻塞
- 不跟踪连接状态:不必建立连接或重启状态机
- 不需要拥塞控制:不设置客户端或网路反馈机制
- UDP什么时候会出现丢包的现象
- UDP报文错误。系统会将错误报文直接丢弃
- UDP接收缓冲区不足。接收报文速率过快,UDP报文超过缓冲区或MTU的大小,
- 防火墙
- epoll,select,poll的区别?
- select
- 每次调用select,都需要把fd集合从用户空间拷贝到内核空间(不论这些文件描述符是否就绪),fd很多时开销就很大
- 每次调用select都需在内核遍历传递进来的所有fd, 线性扫描,即轮询是否有就绪的文件描述符, 效率低, 查询时间为O(n)
- select支持的文件描述符数量太少(默认最大支持1024个,可以通过ulimit -n修改)
- fds集合不能重用
- poll
- 相对于select
- 无最大连接数限制,基于链表存储
- 水平触发,报告fd后没有被处理,则下次poll时会再次报告该fd
- 解决了select第三第四个缺点
- 相对于select
- epoll
- 水平触发(LT)和边缘触发(ET)
- 边缘触发若存在就绪的文件描述符只提示一次,直到下次再有数据流入之前都不会再提示,无论fd中是否还有数据可读。所以ET模式下,read一个fd时,一定要把它的buffer读完,即直到read返回值小于请求值或遇到EAGAIN错误。
- 采用回调机制(使用
epoll_ctl
往内核事件表中注册),即调用read/write处理I/O就绪事件,有信号发生则执行相应的处理函数,激活fd。 - 若用LT,若系统中有大量无需读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会降低效率
- 对比select和poll省略了把fd集合从用户空间拷贝到内核空间(而是通过直接在内核空间中创建),而且从内核空间拷贝到用户空间的fd集合是就绪fd集合(拷贝开销较小),时间复杂度是O(1)
- 水平触发(LT)和边缘触发(ET)
- 三者对比:
- 表面上看epoll性能远优于poll和select,但在连接数少且都十分活跃的情况下,select/poll性能可能比epoll好。
- select
- 红黑树说一下?
- 特化的平衡二叉树
- 查找、插入、删除的时间复杂度都是O(logn)
- 性质
- 节点是红色或者黑色的
- 根节点是黑色
- 所有叶子都是黑色
- 每个红色结点的两个子结点都是黑色(因此路径上不能有两个相邻的红色节点)
- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点
- https加密过程?
- 采用对称加密+非对称加密+CA
- 对称加密:”和日常中的钥匙差不多”
- 非对称加密:简单来说就是有两把钥匙,一把公钥,一把私钥。使用私钥加密的内容用公钥才能解开,同理,使用公钥加密的内容使用私钥才能解开
- 非对称加密+对称加密
- 某网站拥有用于非对称加密的公钥A和私钥A’
- 浏览器首先向网站服务器发送请求,服务器发回公钥A给浏览器
- 浏览器随机生成一个对称加密的密钥X,用公钥A加密后发给服务器
- 服务器用私钥A’解密并得到密钥X
- 这样双方都有密钥X了,且其他人都不知道,之后双方所有数据都通过密钥X加密解密即可
- 中间人攻击
- 劫持服务器端发送给客户端的公钥,并自己伪造公钥发送给客户端,客户端将密钥X使用中间人的公钥加密(客户端以为是服务器端的公钥),然后发出去中间人用私钥进行解密获得密钥X,然后中间人再用服务器端的公钥再次加密X,发送给服务器端。此时中间人已经获得了密钥x,可以获取服务器端和客户端的之间通信的信息
- 数字证书,证明浏览器收到的公钥一定是服务器端的公钥,浏览器进行验证。
- 将公钥放入数字证书中,由服务器端颁发给客户端
- HTTP常见请求方法和区别?
- GET
- 本质上用来请求服务器上的资源,资源通过一组HTTP头部和呈现数据返回给客户端。GET请求中,永远不会包含呈现数据。即GET请求只用来向服务器获取资源,而GET请求本身并不应该携带任何呈现数据
- 应用场景:
- 登录时GET获取服务器数据库用户名和密码进行验证
- 下载文本、图片、音频时等获取服务器资源
- POST
- 用于将实体提交到指定的资源。数据被包含在POST请求体中。POST 请求可能会导致新的资源的建立或已有资源的修改。
- 应用场景
- 提交用户注册信息
- 提交修改的用户信息
- HEAD。
- 和GET方法一样,只是不返回报文的主体部分(返回的响应没有具体内容)。用于确定URI的有效性及资源更新的日期时间等
- 应用场景:
- 向服务器获取某些易过期或丢失的大型文件时,可以用HEAD方法查询是否存在
- GET和POST的区别见
- GET用来从服务器中的指定资源请求数据,POST将实体提交到服务器中的指定资源中
- GET是幂等的(没有副作用);POST是不幂等的(不能随意多次执行)
- 浏览器可以对GET请求的数据缓存;POST请求不能被浏览器缓存(能缓存意味者比如提交一份订单不会向服务器发送请求)
- GET可收藏为书签; POST不可收藏为书签(因为是不幂等的,比如点一个书签就下一个单是很恐怖的事)
- POST更安全(POST用HTTP请求body携带数据,GET用url携带数据)
- GET
- TCP粘包了解吗?解决方法?UDP为什么不存在粘包?
- 文章
- 通俗的来讲”我客户端调用了两次send,怎么服务器端一个recv就都读出来了?!”
- TCP采用Nagle算法,合并相连的小的数据包,再一次性发送,来达到提升网络传输效率的目的。但是接收方并不知道发送包合并数据包,所以就导致接收方不能还原原始的数据包
- 解决方法:
- 禁用nagle算法(
TCP_NODELAY
选项),只能解决发送方的问题,粘包还可能因为接收方,而且TCP传输效率降低了 - 使用消息边界:发送方在每个数据包的结尾添加一个消息边界标记,接收方可以根据消息边界标记来区分每个数据包,从而避免粘包现象的出现。
- 固定长度
- 禁用nagle算法(
- UDP不是面向流的,发送的每个数据包都是独立的
- TCP三次握手?
- 主要是为了确认双方接收能力和发送能力是否正常
- 序列号x是随机数(考虑到网路安全)
- 为什么TCP握手不是两次?
- 第一次握手确定客户端的发送能力和服务器端的接收能力,第二次握手确认服务器端的发送能力,如果没有第三次握手,不能确定客户端的接收能力是否正常
- TCP四次挥手?
- 为什么TCP挥手不是三次?
- 服务器端在收到第一次挥手时可以将ACK和FIN一并发送出去,但是服务器并不会立刻关闭socket(使用close系统调用),只有等到服务器把所有的报文发送完了(TCP的半关闭特性,这样服务器还能发送,客户端还能接收),才能发送FIN报文,因此是四次挥手
- time_wait为什么需要2MSL?
- 确保被动关闭TCP链接的一端能收到第四次挥手的ACK
- 避免上一次TCP连接的数据包影响到下一次的TCP连接(若服务端没有收到ACK,会重发第三次挥手的FIN包,此时客户端新建了一个到服务端的TCP连接,并且客户端使用的还是之前的端口号,那么网络延迟到达的FIN包就会被这个新的TCP连接接收到,这不是客户端希望接收到的数据)
- 给定一个网址,访问过程是怎么样的?
- 浏览器向DNS服务器请求解析该URL中的域名所定义的IP地址
- 解析出IP地址后,根据该IP地址和默认端口号80,和服务器建立TCP连接
- 浏览器发出读取文件(URL中域名后面部分对应的文件)的HTTP请求,该请求报文作为TCP三次握手的第三个报文的数据发送给服务器
- 服务器对浏览器请求作出响应,并把对应的HTML文本发送给浏览器
- 释放TCP连接
- 浏览器解析该HTML文本并显示内容
- 网络层有哪些协议?
- IP、ARP、ICMP、IGMP
- TCP第二次握手丢包处理?第三次?
- 客户端第一次握手发送的SYN包、以及服务器端第二次握手的发送的SYN、ACK包会发生重传。有重传最大次数的限制
- 第三次握手丢包,第二次握手SYN、ACK包会重传,如果达到最大次数限制还未收到ACK,那么就是一个半连接的状态
- 每一次重传RTO(重传超时时间)就会重新计算
- 网络I/O五层模型?
- 阻塞I/O。执行I/O线程会被阻塞(比如socket缓冲区无数据但调用read去读)
- 非阻塞I/O。上述情况执行I/O线程不会被阻塞,返回EAGAIN或EWOULDBLOCK
- I/O复用。在单进程/单线程中一次可以检测多个客户端的事件
- 信号驱动I/O(在多线程中不好处理)。
- 使用linux相关的API(sigaction),将信号处理程序的注册到服务器上,当读/写事件就绪时调用处理程序(将信号写入管道中),然后在事件循环中解析该信号
- 异步I/O
- close文件描述符后,epoll会不会把它自动从监听集合中删除呢?
- 如果fd的引用计数是1,close之后会自动删除
- 广播和多播的区别?
- 广播(客户端绑定本地IP和端口,需要设置一下socket属性)只能用于局域网;多播即可用于局域网也可用于广域网
- 多播中客户端需要加入多播组才能接收到多播
- I/O多路复用是同步I/O还是异步的?
- 同步
- 常见HTTP状态码?
- 1XX信息
- 2XX成功
200 OK
。成功建立连接
- 3XX重定向
- 4XX客户端错误
400 Bad Request
。请求报文中存在错误403 Forbidden
。请求被拒绝,和资源文件的访问权限相关404 Not Found
。未找到服务器上的资源
- 5XX服务器错误
500 Internal Server Error
。服务器正在执行请求时发生错误
- TCP首部和UDP首部?
- TCP首部20个字节,UDP首部8个字节
- TCP首部
- 源/目的端口号
- 序号。用于对字节流进行编号(以字节为单位)
- 确认号
- 数据偏移(指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度,以4字节为单位),标志位,窗口大小(告知接收方,发送窗口的大小)
- 校验和,紧急指针
- UDP首部
- 源/目的端口号
- 校验和
- 报文长度,UDP首部的长度和数据部分的长度之和
- 说一下cookie和session?
- cookie。因为HTTP是无状态协议,HTTP/1.1引入
Cookie
来保存状态信息- cookie是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务器两个请求是否来自同一浏览器,会带来额外的性能开销
- 用途
- 会话状态管理(如用户登录状态,购物车,游戏分数或其他需要记录的信息)
- 个性化设置(如用户自定义设置、主题等)
- 浏览器行为跟踪(如跟踪分析用户行为等)
- session。存储在服务器端,存储在服务器端的信息更在安全。
- 使用session维护用户登录状态的过程
- 用户进行登录时,浏览器发出HTTP请求报文,用户提交包含用户名和密码表单
- 服务器端验证该用户名密码,并返回一个session ID(作为redis的key),放在响应报文的Set-Cookie字段中
- 客户端在收到响应报文时将该cookie值存入浏览器中
- 客户端之后对同一个服务器进行请求时会包含该cookie值,服务器收到提取session id,然后从redis中取出用户信息,继续处理业务
- cookie。因为HTTP是无状态协议,HTTP/1.1引入
- HTTP状态机?
- 解析请求行:
GET /index.html HTTP/1.1
- 解析请求头:
connection: close
- 解析请求体
- 解析结束后生成:状态行,响应头,响应体
- 解析请求行:
- 客户端拔掉网线会如何?
- 客户端不会重传
- 解决方法:使用心跳包,双方定期发送,如果一段时间收不到心跳包就直接断开
- 客户端主动kill掉进程会如何?
- 客户端会给服务器端发送fin报文
- epoll的底层实现?
- time_wait?大量出现的原因?怎么处理?
- time_wait是主动断开连接的一方,在发送第四次挥手报文的状态
- 大量http短连接的存在
- connection: keep-alive,改变为长连接
- close_wait?大量出现的原因?怎么处理?
- 被动关闭的一方在发送第二次挥手的ack和第三次挥手fin中间间隔的状态(这种状态大量出现会影响服务器的性能,如:fd数量达到服务器的上限)
- 通常是服务器出现异常后未关闭连接
close()
, 或者是close_wait的配置时间过长 - top查看cpu利用率,netstate查看网络情况(
netstat -ap | grep <PID>
)
- reuseaddr?
- 设置
SO_REUSEADDR
选项,可以复用端口,不必等待2MSL时间(time_wait状态下两端的端口都不能使用)
- 设置
- TCP半关闭状态
- 比如被动关闭的一方处于close_wait状态,因为TCP是全双工的连接,主动关闭连接的一方已经关闭,此时TCP连接处于半关闭状态(既能关闭读方向也能关闭写方向,通过伯克利套接字
shutdown
)
- 比如被动关闭的一方处于close_wait状态,因为TCP是全双工的连接,主动关闭连接的一方已经关闭,此时TCP连接处于半关闭状态(既能关闭读方向也能关闭写方向,通过伯克利套接字
- 流量控制和拥塞控制?
- 对比:
- 拥塞控制是一个全局性的过程,涉及到所有的主机、路由器等
- 流量控制指的是点对点通信的控制,所要做的就是抑制发送端发送数据的速率,以便使接收端来得及接收
- 流量控制:为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收。
- 接收窗口(rwnd):接收方根据自己的接收缓存的大小,动态地调整发送方的发送窗口大小
- 拥塞窗口(cwnd):发送方根据其堆当前网络拥塞估计而确定的窗口值
- 发送窗口=min{cwnd, rwnd}
- 拥塞控制
- 慢启动(cwnd由1(即一个最大报文段长度MSS)开始指数级增加),拥塞避免(cwnd达到阈值后线性增加)。如果出现网络拥塞,则cwnd又重新变为1,然后阈值设为出现拥塞时cwnd的一半。
- 快重传(三次冗余ACK(收到接收方发送的三个ACK), 因为接收方每收到一个失序的报文段后就立即发出重复确认),直接重传对方尚未收到的报文
- 快恢复。遇到网络拥塞直接将cwnd置为和阈值一样的值
- 快重传和快恢复属于慢启动和拥塞避免的改进
- 对比:
- 视频会议为什么用UDP协议呢?
- 不需要可靠性,一些卡顿是可以接收到
- UDP的速度是TCP比不了的
- webbench压测工具的原理?
- fork出多个子进程,每个子进程都循环做web访问测试。子进程把访问的结果通过pipe告诉父进程,父进程做最终的统计结果
- udp如何实现可靠?
- 参照QUIC
- 介绍一下Http1.1和http2
- TLS/SSL在哪一个阶段发挥它的作用?
- ssl和tsl握手过程?
- http的队头阻塞?
- 多线程accept有什么问题?惊群效应?如何解决?
- 服务端网页如果传输大文件,用什么解决?
- post能否把请求写入到url中?
- AB两台主机通信时如何检测是否有数据传输?
- GET为什么比POST多了一个TCP?
- 选择重传?
- http2.0和http1.x的区别?
- 如何改进udp使其变得可靠?
计组/OS
- 线程切换用到的硬件有什么?
- 寄存器组
- 程序计数器(PC)
- 常见的系统调用有哪些?
- 文件操作
- creat, read, write, open, close, link, unlink, chmod等
- 过程控制
- fork, wait, exit, exec
- 文件操作
- 了解线程池吗?
- 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。这避免了在处理短时间任务时创建与销毁线程的代价
- 半同步半异步模式(采用Reactor模式作为事件处理模式):分为同步层(用于处理客户逻辑)、队列层、异步层(用于处理I/O事件)三层。同步层的主线程(异步线程,不会出现阻塞)处理工作任务并存入请求队列,工作线程(同步线程)从工作队列取出任务执行,取不到任务的工作线程进入挂起状态
- 缺点
- 主线程往请求队列中添加任务,或者工作线程从请求队列中取任务都需要对请求队列加锁保护,从而白白耗费cpu时间
- 如果客户数量较多,工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢,如果通过添加工作线程来解决这一问题,工作线程的切换也将耗费大量CPU时间
- 缺点
- 进程间通信方式?
- 信号
- 管道。 半双工通信方式(通常指无名管道PIPE)
- 消息队列。用一个链表来存储操作系统内核中的消息,并且使用“消息队列标识符”来标识消息队列。
- 共享内存。共享内存使用mmap,不同进程的虚拟地址映射到同一处物理地址进行通信
- 套接字
- 信号量。信号值为0下调用sem_wait()则会阻塞直到信号值大于0或者是信号中断处理的调用
- 堆和栈有什么区别?
- 申请方式:栈由系统自动分配释放(如函数调用时存放函数的参数值,局部变量的值等);堆需要程序员自己申请,并指明大小
- 申请效率:栈由系统自动分配,申请效率高;堆内存分配效率比栈低,容易产生内存碎片。
- 增长方向:栈由高地址向低地址方向增长;堆由低地址向高地址方向增长
- 堆中空闲空间使用空闲链表来管理,动态分配内存时空闲内存的管理:首次适配(不需要排序)、最佳适配(空闲分区按容量递增次序排序)、下次适配(在首次适配的基础上,每次分配内存时,从上次查找结束的位置开始查找)、最差适配(空闲分区按容量递减次序排序)
- 由进程的虚拟地址空间布局可知,在多线程环境中,共享的变量应该存储在堆上,而不是栈(每个线程都有自己的栈)
- 静态库与动态库(共享库)?
- 静态库是在编译时链接到目标程序中,而动态库则是在运行时动态链接到目标程序中。
- 静态库文件一旦被编译到目标程序中,就不能再进行更新和维护,如果需要更新或修复库文件中的某个bug,就需要重新编译整个程序;而动态库文件可以独立更新和维护,不会影响到目标程序,只需要替换动态库文件即可。
- 动态库并不会链接到目标程序中,而是在程序运行时才被载入。不同的应用程序如果调用相同的库,那么共享内存(可实现进程间资源的共享)中只需要有一份实例
- 静态库浪费空间和资源,包括对程序更新、部署和发布都需要重新编译(因此衍生出了动态库)
- 死锁?
- 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象
- 自旋锁和互斥锁?
- 加锁失败的处理
- 互斥锁加锁失败后会进行线程切换
- 自旋锁(通过CPU提供的CAS(compare and swap), TAS原子操作)加锁失败后,线程会忙等待,直到它拿到锁
- 如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁(因为线程互斥锁线程上下文切换的开销)
- 加锁失败的处理
- 悲观锁和乐观锁(加场景)?
- 悲观锁,认为多线程同时修改共享资源的概率比较高,因此访问共享资源前总会加锁
- 乐观锁(无锁编程,并没有加锁),反之,会先修改完共享资源后再验证这段时间内是否发生冲突,如果没有则操作完成,有则放弃本次操作。
- 乐观锁重试的成本非常高,因此只有在锁成本高且冲突概率低的场景时,才考虑使用乐观锁,如:多人在线文档
- 进程和线程?区别?
- 进程是正在执行的程序实实例,执行程序时,内核会将程序代码载入虚拟内存中,并为程序变量分配空间,建立进程数据结构(记录如进程ID、用户ID、组ID以及终止状态等)。进程虚拟地址空间逻辑上划分为,栈区、堆区、数据区(初始化的全局和静态变量、未初始化的全局和静态变量(BSS))、文本区
- 线程共享同一数据区和堆,每个线程都有属于自己的栈(硬件多线程有自己的PC和寄存器组),可以通过共享的全局变量进行通信,线程也能利用IPC的方式进行通信,多线程应用能从多处理器硬件的并行处理中得到性能的提升
- 区别:
- 进程是资源分配的最小单位,线程是CPU调度的最小单位(引入线程之后的cpu)
- 一个线程只能属于一个进程,而一个进程可以有多个线程
- 进程在执行过程中拥有独立的内存空间,而多个线程共享进程的内存空间
- 进程上下文切换的开销大,线程切换的开销小
- 进程间不会相互影响,而线程为了保证同步需要加锁
- 线程间的同步方式?
- 互斥锁、自旋锁、读写锁、条件变量
- 僵尸进程和孤儿进程?
- 孤儿进程是指,父进程已经退出了,子进程还在运行,那么这些子进程将被init进程(1号进程,0号进程为idle进程)所收养,并由init进程对它们完成状态收集工作
- 僵尸进程是指,子进程退出了,但父进程并未调用waitpid/wait收集子进程状态信息,那么子进程的进程描述符仍然在系统中(如果有大量的僵尸进程,因为系统所能使用的进程号是有限的,就可能因为没有进程号而导致创建不了新的进程)
- 死锁及避免?
- 死锁预防,破坏四个必要条件之一:
- 请求并保持,互斥条件,不可剥夺,循环等待
- 破坏
- 请求并保持:一次性申请在整个运行过程中需要的全部资源
- 互斥条件:无锁编程
- 循环等待:按顺序申请资源
- 不可剥夺:如果某一个进程进一步的请求资源被拒绝,则释放该进程的资源
- 死锁避免,银行家算法
- 死锁预防,破坏四个必要条件之一:
- 如何检验死锁?
- 死锁的检测算法通过检测有向图是否存在环来实现,从一个结点出发进行DFS,如果存在环,则出现死锁
- 什么是mmap?
- 是一种内存映射文件的方法,将一个文件映射到虚拟地址空间,实现这样的映射关系后,进程就可以采用指针的方式读写这一段内存,对文件的操作不再需要用read、write等系统调用。
- mmap由操作系统负责管理
- 条件变量和信号量的区别?
- 条件变量可以一次唤醒所有等待者(通过pthread_cond_broadcast唤醒所有阻塞的线程, pthread_cond_signal保证至少唤醒一条,效率较高);而信号量不行
- 信号量可以指明有效资源的数量(是有值的),而条件变量没有
- 信号量使用忙等待;条件变量使用阻塞等待
- CAS
compare and swap
(传入三个参数),通过检查内存位置中的值是否等于旧值,若未发生变化,则将新值更新到内存位置中。需要搭配Volatile使用(使编译器不再优化对该变量的ld和st)
- 内存管理的方式?
- 页式管理
- 段式管理
- 段页式管理
- 字节对齐的好处?
- 字节对齐能够更好地对指令进行译码,同时避免访问一个数据多次访存(比如获取一个8bit数据,主存宽度为8,如果字节不对齐,取数据需要访问两次存储器)
- 为什么
pthread_create
第三个传入的参数为成员函数必须得是静态的?- 执行的函数返回值必须为
(void*)
, 且参数必须是void*
- 例子
1
2
3void* func(); // 必须返回的是指针
pthread_t p1;
pthread_ctate(&p1, NULL, func, NULL); // 线程执行函数func - 首先在C++的类中,普通成员函数不能作为pthread_create的线程函数,如果要作为pthread_create中的线程函数,如果要是成员函数,必须是static(普通函数都可以)!
- 普通成员函数的参数会隐式存在一个默认的底层const的this参数,这就和pthread_create的第四个参数(参数列表)不匹配,编译器会报错。因为传入的是静态成员函数,若想使用成员变量,传入一个this指针即可(因为静态成员函数没有this指针)
- 执行的函数返回值必须为
- 进程挂起态和阻塞态的区别?
- 挂起是主动的(这里指的操作系统主动不活跃的进程或线程挂起);阻塞是被动的(程序员可以进行IO操作)
- 中断和异常的区别?
- 中断一般由外部引起(如外部设备的中断);异常一般由内部引起一般会重新执行处于异常的指令(如缺页异常)
- 中断会进行中断优先级的比较;异常没有
- 虚拟内存的优势?
- 程序的保护和共享(每个进程有自己的虚拟地址空间,进程间通过共享内存通信)
- 内存管理的便利,引入页式管理后,可以减少外部碎片
- 使得一个进程所占用的物理空间超过主存的空间(有一部分在磁盘的交换区)
- 可以管理每个页的访问权限(进程虚拟地址空间内核数据的部分是和物理地址空间一一映射的,不需要进行VA到PA的转换)
- 为什么要引入Cache?
- 充分利用局部性(局部性原理:时间局部性和空间局部性)
- CPU和主存的速度相差三个数量级,指令和数据都需要到主存中去取(太慢)
- 说一下进程的状态?
- xv6中:使用、未使用、运行、就绪、阻塞、僵尸
- 进程调度算法?
- 周转时间=作业完成时间-作业提交时间,即为等待时间,在就绪队列中排队,在处理机上运行,输入/输出花费的时间总和
- 先来先服务算法
- 短进程优先算法
- 时间片轮转算法
- 多级反馈队列算法
- 高响应比优先调度算法,即考虑作业等待时间又考虑作业运行时间
- 线程间的同步手段?
- 互斥量(mutex)
- 读写锁
- 条件变量
- 信号量
- 协程?
- 用户态线程
- 轻量(栈比线程小)
- 在同一个线程的协程是串行执行的
- 对称协程(go),非对称协程(libco,同一个线程的子协程不能直接切换,而是要通过主协程)
- 遍历数组为什么比遍历链表普遍要快?
- 主要是Cache的局部性原理,链表元素虚拟地址空间是不一定连续的,但是数组元素一定是连续的
DB
- B+树和B树的区别?
- B树每个节点在内存中占用一个页面
- 树有多高就需要检索多少次(访问二叉树每个节点就会发生一次I/O),索引存在于磁盘中
- B树中键不能重复存储
- B+树范围查找更高效快捷
- B+树只有叶子节点存储数据指针(这样内部节点就可存储更多的key,使得B+树相对B树来说更矮);B树叶子节点和内部节点都存储数据指针,因此节点数量相同,B+的高度比B树更低
- mysql中语句查询的顺序
- sql查询语句执行顺序
- SELECT、DISTINCT、FROM、JOIN、ON、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT
- 关键字执行顺序
- FROM、WHERE、GROUP BY、HAVING、SELECT、DISTINCT
- sql查询语句执行顺序
- mysql左连接和内连接?
- 内连接,返回两个表交集的部分
- 左连接,返回两个表交集的部分以及左表的部分,左表的记录将会全部显示出来,右表记录不足的属性置为NULL
- 什么是事务?
- 指数据库中一组相关的操作,它们被视为一个单独的工作单元,要么全部执行成功,要么全部回滚。
- 事务通常用于保证数据库的完整性和一致性
- 满足ACID特性的一组操作
- 数据库的ACID?
- 保证事务的正确可靠:
- Atomicity(原子性)。事务是为不可分割的最小单元
- Consistency(一致性)。DB在事务执行前后都保持一致性状态
- Isolation(隔离性)。一个事务再最终提交之前,对其他事务是不可见的
- Durability(持久性)。一旦事务提交就会永远保存到数据库中,即使发生系统崩溃,事务执行的结果也不能丢失
- 只有满足一致性,事务的执行结果才是正确的。在无并发的情况下,保证原子性就保证了一致性;在有并发的情况下,保证原子性和隔离性才能保证一致性。事务满足持久化是为了应对系统崩溃的情况
- 保证事务的正确可靠:
- 并发一致性的问题(主要原因是破坏了事务的隔离性)
- 丢失修改。一个事务的更新操作被另一个事务的更新操作所替代
- 脏读。事务T1的修改了一个数据,但并未提交,此时事务T2读取到T1修改后的数据,但此时T1撤销了这次修改,T2所读的就是脏数据
- 不可重复读。事务T1先读取一次数据,之后事务T2修改数据,事务T1再次读取到数据,此时T1读取到的两次数据的结果不同
- 幻读。类似于不可重复读,事务T1读取一段范围的数据,事务T2在这个范围内插入了新的数据,事务T1两次读取的数据不一致
- 封锁类型
- 读写锁。X锁和S锁,X锁和X锁互斥。对A加了S锁之后可以其他事务能对数据对象A再加S锁
- 互斥锁,又称为写锁,X锁
- 共享锁,又称为读锁,S锁
- 意向锁。存在表级锁和行级锁的情况下,事务想要对表加X锁,需要先检测是否有其他事务对表或者表中的任意一行加了锁,那么就需要对表的每一行都检测一次,非常耗时。因此引入表锁IX/IS,用来表示一个事务想要在表中的某个数据行上加X锁或者S锁
- 一个事务在获得某个数据行对象的S锁之前,必须要获得表的IS锁或更强的锁
- 一个事务在获得某个数据行对象的X锁之前,必须要获得表的IX锁
- 读写锁。X锁和S锁,X锁和X锁互斥。对A加了S锁之后可以其他事务能对数据对象A再加S锁
- 封锁协议
- 三级封锁协议
- 一级封锁协议。事务要修改数据时必须加X锁,直到事务结束。解决了丢失修改
- 二级封锁协议。在一级的基础上,事务要读取数据时必须加S锁,读取完马上释放S锁。解决了脏读
- 三级封锁协议。在二级的基础上,事务读完数据不释放S锁,直到事务结束。解决了不可重复读
- 两段锁协议
- 加锁和解锁分为两个阶段运行
- 三级封锁协议
- 隔离级别。并发控制可以通过封锁实现,但是封锁操作需要用户自己控制,相当复杂,因此DBMS提供了事务的隔离级别
- 未提交读。事务中的修改,即使没有提交,对其他事务也是可见的
- 提交读。事务中的修改,只有提交了,其他事务才可见
- 可重复读。保证同一个事务中多次读取同一数据的结果是一样的
- 可串行化。强制事务串行执行,多个事务互不干扰,需要加锁实现
- (Multi-Version Concurrency Control, MVCC),是mysql的InnoDB存储引擎实现隔离级别,实际场景中读操作往往多于写操作,为了避免读写锁引入的不必要的加锁操作。
- 写操作(DELETE、INSERT、UPDATE)更新最新的版本快照,读操作去读旧版本快照,没有互斥关系(类似于copyonwrite)
- 为了避免脏读和不可重复读,MVCC规定事务进行读取操作时,只能读取已经提交的快照(当然一个事务可以读取自身未提交的快照)
- 范式。解决四种异常(冗余数据、修改异常、删除异常、插入异常)
- 1NF。属性不可再分,否则需要再分解
- 2NF。非主属性完全依赖于主键,否则需要再分解
- 3NF。非主属性不依赖于其他非主属性,否则需要再分解
- 聚簇索引和非聚簇索引?
- 聚簇索引叶子节点存储的是数据;非聚簇索引叶子节点存储的是指向数据的指针
- 聚簇索引插入和更新插入开销大(因为聚簇索引的数据和索引,存储在同一物理位置上,因此会导致物理位置的变化),会导致数据的碎片化;非聚簇索引开销小
- 聚簇索引范围查询和排序操作的性能高;非聚簇索引不如聚簇索引
- 存储过程?视图?
- edis有哪些数据结构?
- Redis持久化机制?
- 缓存和数据库是如何实现同步的?
- 分布式和集群了解吗?
- 写了redo log,但事务还没提交,系统突然崩溃了怎么样?
- 索引的原理和种类?
- B+树搜索复杂度
- 哈希索引和B+树的区别
- Mysql的join用法?原理?Union?
- redo log如何实现?
- order by的实现?
- 索引覆盖?回表?
- 数据库为什么用b+树?
- SQL注入?
算法
- 稳定排序有哪些?不稳定排序有哪些?
- 稳定性:经过排序算法后,原先两个元素的相对位置不变,就称它是稳定的
- 稳定排序:直接插入排序、冒泡排序、归并排序、基数排序
- 不稳定排序:简单选择排序、希尔排序、快速排序、堆排序
- 快速排序的时间复杂度为什么是nlogn?
- 如果分区间的pivot恰好为中点,递归对两个子区间sort复杂度logn,分为n个子区间
- 二分查找时间复杂度为什么是
O(logn)
?- 第一次迭代之后剩余数组长度为$\frac{n}{2}$
- 第二次迭代之后剩余数组长度为$\frac{n}{2^2}$
- 第k次迭代之后剩余数组长度为1,即$\frac{n}{2^k}=1$
- 故时间复杂度为k,即$k=\log_{2}{n}$
- 哈希表的原理?如何实现?处理冲突的方法?
- 定义存储桶,底层可以用数组或者链表来实现
- 设计哈希函数
- 常见处理冲突的方法(多个键被映射到同一个槽上的处理方法)
- 开放定址法
- 线性探测
- 平方探测
- 再散列
- 拉链法
- 如果该插槽中不存在相同的键,则将该键值对添加到链表的末尾;若存在相同的键则直接更新值。
- 开放定址法
- 什么是动态规划?
- 通过把原问题分解为相对简单的子问题来解决复杂问题
- KMP算法的原理,next数组怎么计算?
C/C++
- 什么是内联函数?
- 内联函数编译时展开,省去了函数调用的开销(如将实参、局部变量、返回地址等压入栈中, 返回时还要弹栈)
- 说一下多态?
- C++支持两种多态性:编译时多态(通过函数重载,泛型编程实现),运行时多态(通过虚函数实现。常见通过派生类对象指针或引用, 赋给基类对象指针或引用)。
- 静态多态是指在编译期间就可以确定函数的地址,而动态多态需要等到运行时才能知道函数的地址
- malloc和new的区别?
- operator new是运算符(其中调用malloc),malloc是标准库函数
- new申请的堆大小由系统自动判断,malloc需要传递申请大小的参数
- new返回相应类型的指针,malloc返回void*,需要强制转换
- 若new申请不到内存则会抛出异常,malloc返回NULL
- new中会调用malloc
1
2
3
4
5
6
7
8
9
10
11
12// no inline, required by [replacement.functions]/3
void* operator new(std::size_t sz)
{
std::printf("1) new(size_t), size = %zu\n", sz);
if (sz == 0)
++sz; // avoid std::malloc(0) which may return nullptr on success
if (void *ptr = std::malloc(sz))
return ptr;
throw std::bad_alloc{}; // required by [new.delete.single]/3
}
- C++为什么构造函数不能是虚函数?
- 因为虚函数的地址是通过虚函数表来查找的,虚函数表由实例化对象中的vptr指向,实例化对象需要构造函数完成初始化,但此时vptr还未初始化
- 虚函数表在哪?虚函数指针vptr在哪?
- 虚函数表在只读数据区;vptr一般在对象内存分布的第一个位置,对象的地址就是虚函数指针vptr的地址
- C++纯虚函数?
- 没有函数体,只有声明
- 函数声明的结尾加上=0,告诉编译器这是纯虚函数
- 含纯虚函数的类称为抽象类,之所以抽象,因为它无法实例化。
- const限定的变量可以修改吗?怎么修改?
mutable
只能用来修饰类的数据成员,该成员变量可以在const成员函数内进行修改- 如果是顶层const修饰的变量则不能修改
- 如果是指针变量,且用底层const修饰,那么该指针指向的值不能修改,但是指针值可以修改
const int* const p; // 靠右的是顶层const,靠左的是底层const
- 如何用C语言实现面向对象?
- 面向对象三大特性封装、继承、多态,见文章
- 封装。可以将结构体的定义在
.c
文件中,.h
中仅作结构体的声明, 这样其他文件就没办法直接访问结构体的具体内容。若想访问成员变量需要提供访问这些变量的方法,如1
2
3
4char *animalGetName(Animal this)
{
return this->name;
} - 继承。了解过C++内存模型之后很容易能知道,只需要定义”父类”作为”派生类”结构体成员(必须在第一个位置),这样转换为父类指针时,子类结构体前面部分就是父类结构体了
- 多态。定义一个结构体作为虚函数表,在基类中增加虚表指针。虚表在构造函数中初始化,在析构函数中销毁。
- 用过函数指针吗?
- 本质上是一个指针,指向函数。通常用于函数回调的应用场景
- C++深拷贝和浅拷贝?
- 若没有定义拷贝构造函数,编译器执行的是默认拷贝构造函数(浅拷贝)。如果类成员中有指针成员变量,对指针拷贝后会出现两个指针变量指向同一个内存单元,会出现两次析构,造成内存泄露,此时要采用深拷贝(即自己定义拷贝构造函数, 使拷贝后的对象指针成员有自己的内存空间)
- 为什么在继承情况下析构函数要为虚函数?
- 如果不是虚函数则会造成内存泄露,比如多态,delete父类指针后,仅调用父类的析构函数,子类析构函数并未被调用。但如果析构函数是虚函数,则会先调用子类析构函数再调用父类虚构函数(析构函数调用的顺序)
- C语言sizeof和strlen的区别?
- sizeof是一元运算符,以字节为单位,用来求指定变量或变量类型所占内存空间的大小(单位为字节),其值在编译时期就计算好了,因此只能算出静态的大小
- strlen是库函数,用来求字符串的长度,它回去找字符串结尾的’\0’结束符(返回值不包括\0);如果找不到,返回值会是一个不确定的值
- 构造函数的顺序和析构函数的顺序原因?
- 个人感觉是因为对象模型
- 空类有哪些函数?空类的大小?
- 有六个:
- 默认构造函数
- 默认拷贝构造函数
- 默认拷贝赋值运算符
- 默认析构函数
- 取地址运算符
- 取地址运算符const
- 编译器默认分配1 byte空间,编译器是支持空类实例化对象的,因此对象必须要被分配内存空间才有意义
- 有六个:
- RAII机制?
- 资源获取即初始化,利用C++构造的对象最终会被析构函数销毁的原则,进行资源的释放,避免内存泄露的风险
- 四种类型转换
- const_cast, 消除底层const
- static_cast, 只要不含底层const都可以使用,
- dynamic_cast, 运行时进行类型转换的安全检查,用于基类和派生类之间的转换
- reinterpreter_cast, 进行位模式上的强制转换
- vector在push_back的时候容量满了怎么办?
- capacity会变为原来的两倍。申请一块新内存,拷贝数据,释放原内存
- vector中resize和reserve的区别?
- resize是改变容器大小,且创建对象,如果改变之后的大小小于当前容器大小,则erase,否则insert
- reserve指定容器预留的空间(改变可用空间的大小),并未构造对象
- map和unordered_map的区别?
- map的底层是红黑树,存储的键是有序的。缺点:空间的开销
- unordered_map底层是哈希表,存储的键是无序的。缺点:哈希表的建立费时
- C++常用设计模式?
- TODO
- 工厂模式
- 抽象工厂定了用于创建不同产品的接口,具体的工厂类重写该接口实现其相应功能
- 单例模式
- 懒汉式
- 为了保证线程安全,懒汉式锁使用双重检测,减少不必要线程同步的开销(即可以先查看对象是否已经初始化再加锁),因为如果每个线程都要获取锁来检查对象是否已经被初始化,会导致性能下降,是一种常见的优化方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 懒汉式
class Singleton {
public:
static Singleton& Getinstance() {
// 双重检测优化
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex_);
if (instance == nullptr) {
instance = new Singleton();
}
}
return *instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
// 确保只通过Getinstance()获取实例销毁
~Singleton() {}
static std::mutex mutex_;
static Singleton* instance;
};
// 类外初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;
int main() {
// 在使用时才创建实例
Singleton& instance = Singleton::Getinstance();
return 0;
}
- 为了保证线程安全,懒汉式锁使用双重检测,减少不必要线程同步的开销(即可以先查看对象是否已经初始化再加锁),因为如果每个线程都要获取锁来检查对象是否已经被初始化,会导致性能下降,是一种常见的优化方式
- 饿汉式,不需要考虑线程安全的问题(因为在程序启动时就被创建,只会执行一次),缺点是增加程序的启动时间和内存消耗
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 饿汉式
class Singleton {
public:
static Singleton& Getinstance() {
return *instance;
}
// 禁止拷贝构造函数和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
~Singleton() {}
static Singleton* instance;
};
// 类外初始化静态成员变量,在main函数启动前就创建,不存在多线程环境下的竞争问题
Singleton* Singleton::instance = new Singleton();
- 懒汉式
- 观察者模式
- 要有一个订阅者列表,添加订阅者和删除订阅者的方法。当事件发生时,遍历订阅者列表通知订阅者(调用其对象的特定通知方法)
- 装饰器模式
- 代理模式
- 策略模式,简化了单元测试,新建一个类来与工厂类的父类聚合
- 原型模式
- 模板模式
- 对象的什么函数不能被声明为虚函数?
- 非成员函数
- 静态成员函数。该类的所有对象都共享这份代码,不能被继承,没有动态绑定的必要性
- 内联成员函数。内联函数在编译期展开,virtual函数体现的是运行时机制
- 构造函数(上面有说过)
- 友元函数。本质上是因为C++不支持友元的继承
- 指针和引用的区别?
- 指针是存储地址的变量,引用是变量的别名
- 引用定义时必须初始化,而指针可以不必初始化
- 指针可以改变指向的存储单元,而引用初始化之后就不能改变了
- 指针的自增标识指向下一个地址单元,而引用的自增表示引用变量值的增减
- 哪几种情况必须用到初始化列表?
- const成员变量,不能赋值
- 引用成员变量,不能赋值
- 类成员没有默认构造函数的类类型
- 如果类存在继承关系,派生类必须在其初始化列表中调用直接基类的构造函数
- struct和class的区别?
- struct类成员默认是public,class是private(作用域限定)
- struct默认是public继承,而class是private
- 常见C++四大内存分区(const变量的位置取决于编译器)
- 代码区
- 静态/全局数据区,又分为已初始化/未初始化。const修饰的全局变量
- 栈区。const修饰的局部变量
- 堆区
- 迭代器失效的情况?解决方案?
- 在迭代时调用erase()、insert()、容器扩容(原来容器的迭代器失效)等
- 数组型数据容器(如vector、deque等,因为是连续分配的内存,所有元素的迭代器都会失效, 都向前/后移动了一个位置)
- 链表型数据容器(如list,因为是链式存储,插入和删除不会对其他迭代器造成影响)
- 树型数据容器(如map、set,删除、插入元素不会对其他元素造成影响,因此只是当前被删除的迭代器失效)
- 解决方法:
- erase会返回当前删除的迭代器的下一个迭代器
1
2
3
4
5
6
7
8
9
10
11
12// 在这里想把大于2的元素都删除
for(auto it=q.begin();it!=q.end();)
{
if(*it>2)
{
it=q.erase(it); // 这里会返回指向下一个元素的迭代器,因此不需要再自加了
}
else
{
it++;
}
}
- erase会返回当前删除的迭代器的下一个迭代器
- ++i和i++是原子操作吗?线程安全?
- 不是,++i先自加再赋值,i++先赋值再自加
- 如果i是局部变量,那就是线程安全的(每个线程有自己的栈)
- 宏和inline的区别
- 宏
- 没有类型检测,不安全,如:
1
2
- 没有类型检测,不安全,如:
- inline时将函数展开,减少函数调用的开销,编译器会进行安全检查
- 宏
- C++重写和重载的区别?
- 重写
- 被重写的函数必须是virtual的(C++对象模型虚函数表)
- 函数名称、参数列表、参数个数、返回值类型都一致
- 访问修饰符(private等)可以不同
- 重载
- 在一个类中(相同作用域)
- 函数名称相同
- 参数个数、参数类型、不同
- 重写
- emplace_back vs. push_back?
- push_back会先创建一个元素然后再将这个元素拷贝/移动到容器的尾部(push_back会优先调用移动构造,如果没有才调用拷贝构造);而emplace_back直接在容器尾部直接构造,省去了拷贝/移动的过程。
- 预处理阶段做的事情
- 使用
gcc -E
,生成.i
文件,将注释删去,将将头文件和宏进行展开和替换
- 使用
- include防范?
- 为了避免重复声明
- 静态成员变量/函数的意义?
- 使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存
- 编译器会在编译一个普通成员函数时,隐式地加上一个this指针。静态成员函数中没有this指针,因此只能访问静态成员变量
- 静态成员变量/函数声明时要加
static
,定义时不需要,编译器会去找声明 - 静态成员不能在类内初始化,只能在类外初始化,
类型 类名::变量名 = 值
- 虚函数和纯虚函数的区别?
- 纯虚函数当前抽象基类的子类必须对该纯虚函数进行覆写;虚函数可以不需要
- 纯虚函数没有函数体;虚函数有函数体
- 纯虚函数在声明结尾加上=0
1
2
3
4// 虚函数声明
virtual void func();
// 纯虚函数声明
virtual void func()=0;
- 构造函数和析构函数有返回值吗?
- 它们都没有返回值
- 命名空间的作用?
- 处理常见的同名冲突,实际上是由程序设计者命名的内存区域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开
- C++中this指针什么情况下必须要用?
- 重载赋值运算符通常返回
*this
- 把自己作为函数实参。
- 重载赋值运算符通常返回
- #define INT 5是什么类型?
- 预处理阶段不会进行类型检查(编译器做的事),因此它没有类型
- NULL和nullptr的区别
1
2
3
4
5
6- 在C++中使用
nullptr(std::nullptr_t)
能隐式转换为其他类型的指针,但C++中不能将void*
指针隐式转换为其他类型(反过来是可以的) - 避免出现重载时的二义性,比如一个函数的参数为
void*
,另一个函数的参数为int
,这时候编译器就会报错 - 模板的类型推导会将
NULL
推导为long int
- 在C++中使用
- C++模板类和普通类的区别?
- 普通类都是在头文件中声明,在源文件中是实现;而模板类必须都放在头文件中
- 模板类和普通类都可以在类体中定义,但模板类在类体外定义要用函数模板
- 函数默认参数?
- 在声明时指定,定义时不指定
- 典型的移动构造函数和移动复制运算符?
- 移动构造函数(本质上就是浅拷贝)。这里没有深拷贝,只是移动了资源。
1
2
3
4
5
6
7
8Holder(Holder&& other) // <-- rvalue reference in input
{
m_data = other.m_data; // (1)
m_size = other.m_size;
// (2) 将右值引用数据设置为某个有效的状态,防止它在临时对象死亡时被意外删除(调用析构函数),(浅拷贝的危险性),右值是将亡值
other.m_data = nullptr;
other.m_size = 0;
} - 移动赋值运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14Holder& operator=(Holder&& other) // <-- rvalue reference in input
{
if (this == &other) return *this;
delete[] m_data; // (1) 清理现有资源
m_data = other.m_data; // (2) 传入数据
m_size = other.m_size;
other.m_data = nullptr; // (3) 防止临时对象死亡时被意外删除
other.m_size = 0;
return *this;
}
- 移动构造函数(本质上就是浅拷贝)。这里没有深拷贝,只是移动了资源。
- vector::clear()会释放内存吗?
- 不保证vector的内存被释放,且capacity()的大小并未改变,改变的是size()大小
- new和make_shared初始化智能指针的区别?
- new会分配两个不连续的内存块(一块分配给new,一块分配给智能指针对象),而make_shared会分配一段连续的内存给这两个内存块。前者会造成碎片化
1
2
3std::shared_ptr<int> ptr = std::make_shared<int>(1);
std::shared_ptr<int> ptr(new int(1));
// 两者取值是一样的
- new会分配两个不连续的内存块(一块分配给new,一块分配给智能指针对象),而make_shared会分配一段连续的内存给这两个内存块。前者会造成碎片化
- shared_ptr底层实现?
- 引用计数成员变量用指针变量存储(指针对应的智能指针对象共享同一个引用计数)
- 在析构函数中判断引用计数的值来决定是否要
delete
成员变量ptr
(拷贝过程为浅拷贝)
- unique_ptr和shared_ptr的性能差异?
- 内存泄漏?
- 内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放
- 种类
- 堆内存泄漏
- 资源泄露:比如有限的socket资源
- 避免
- 避免在堆上分配
- 手动释放
- 使用STL
- 智能指针
- RAII
- weak_ptr解决shared_ptr循环引用问题
- 必须在程序员能预见会出现循环引用的情况下才能使用
- 访问一个NULL会发生什么?
- 段错误
- join()和detach()?
- join()主线程会阻塞等待被调用线程执行结束后,然后主线程回收被调用线程的资源
- detach(),主线程继续运行(不被阻塞),被调用线程驻留在后台运行,当主线程结束时,库负责清理与被调用西安城相关的资源
- C++lambda表达式(匿名函数)
1
2
3
4
5
6
7int i = 0;
// 返回类型为int,x,y为传入的参数,若想使用外部变量,要在[]种捕获外部变量
// []不捕获任何外部变量
// [this]显式捕获this指针
// [=]以值的形式捕获所有外部变量
// [&]以引用的形式捕获所有外部变量
[&,i](int x, int y) -> int {return i;} // [&,i]表示i变量以值的形式捕获,其他外部变量以引用的形式捕获 - C++模板特化
- 匹配顺序:全特化类 > 偏特化类 > 主版本模板类
- 全特化模板形参列表没有参数
<>
,类名后显式指定所有模板实参 - 偏特化模板形参列表有参数但不是全部,类名后指定了部分模板实参,以及模板形参
1
2
3
4
5
6
7
8
9
10
11
12// 全特化
template<>
LexicalCast<int, int> {};
// 偏特化
// 主模板类
template<class T>
LexicalCast<int, T> {};
// 主模板类
template<class T, class F>
LexicalCast {};
- 指针为什么在释放之后要置为空?
- 避免出现悬空指针(dangling pointer)的问题,即指针指向的对象被销毁了,指针变量仍然指向这个位置
- 减少智能指针的引用计数
- std::function内存管理?
- std::function用内部可能使用智能指针来管理,如果可调用对象是一个函数对象或lambda表达式,就是new运算符在堆上动态分配一个函数对象;如果可调用对象是一个函数指针,就直接将其存储在函数指针成员中。
- C++内存模型?
- 右值引用的底层实现?
Objective-C
- OC当中进行内存管理引用计数有什么缺点
- strong和weak指针
- 怎么用promise发送多个请求
- 常见的性能优化放啊?
- 解释一下重绘,怎么避免?
- runtime?
- OC的类是什么?
- IOS中常见的内存泄漏及其解决办法?
- NSProxy解决内存泄漏具体怎么做?
- NSProxy不是NSObject的子类吗?
- mask属性如何实现蒙层的?为什么不推荐这么做?
- 离屏渲染细说
- iOS的持久存储
- OC和Swift的重载
- 如何获取设备信息?
- UIKit类要在哪个线程上使用?
- 下载一个巨大的图片,各个步骤详细说
- NSOpertion如何实现线程依赖的
- iOS的内存泄漏
- autorealeasepool?
- OC对象的创建和销毁?
- UItableview的reuse原理?
- UItableview如果要删除某个cell,你的动画会怎么设计和实现?底层删除逻辑也说说
- 数据源操作细说?
- 怎么判定两个cell相同?
- reuse队列长度怎么调整?属性名是什么?
- cell进入reuse池,然后重新被激活了,计时器逻辑怎么处理?
- MVC说一说?
- 单向数据流怎么说?Model和View如果要实现通信怎么做?
- MVVM怎么做的?
- UI一般在什么线程更新?
- OC的消息转发机制?
- 优先级反转?
- CALayer的三个树,然后渲染树是什么,CALayer是几维坐标?
- 离屏渲染是什么,怎么避免?
- kvo的原理。三种调用方式_property, self.property, kvc的形式,哪些会触发kvo?
- weak和assign区别?
- 自己设计检测性能?
- block中局部变量修改这个点,__block关键字?
设计模式
- UML类图
- 泛化关系(空心箭头)(is a)
- 聚合关系(空心菱形+箭头)(has a)
- 组合关系(实心菱形+箭头)(contains a),比聚合更强。A需要知道B的生命周期,A可能需要负责生成和释放B。如人的驱干和手脚的关 系
- 依赖关系(虚线箭头)(use a)。表示对象A用到了对象B,没有实际的关系。如
驾驶员
依赖汽车
- 依赖关系具体表现形式为B为A的构造器或方法中的局部变量、方法或构造器的参数、方法的返回值,或者A调用B的静态方法。
- 一些设计原则
- 单一职责原则
- 开放-封闭式原则
智力题
- 25匹马,5个跑道,用最少的比赛次数找出前3匹马?
- 有一个3L量杯和5L量杯,如何量出四升的水?
- 有两条绳子,质量不均匀,烧断一条绳子的时间是1小时,如何测量15分钟?
- 有 9 个球,其中 8 个球质量相同,有 1 个球比较重。要求用 2 次天平,找出比较重的那个球?
- 有 20 瓶药丸,其中 19 瓶药丸质量相同为 1 克,剩下一瓶药丸质量为 1.1 克。瓶子中有无数个药丸。要求用一次天平找出药丸质量 1.1 克的药瓶。
- 一栋楼有 100 层,在第 N 层或者更高扔鸡蛋会破,而第 N 层往下则不会。给 2 个鸡蛋,求 N,要求最差的情况下扔鸡蛋的次数最少。
- 村子里有红眼病,可以互相看到但不会互相告诉,当发现自己是红眼病之后会自杀,当外人告诉后第三天有人自杀,问村子里有几个人是红眼病
场景题
- 40亿个数中找一个数是否存在
反问环节
反问技术面试官
- 您可以给我一些学习上的建议吗?
- 您已经这么多候选人,您觉得我相对于这个岗位还有哪些差距需要改善呢?
- 您希望我们校招应届生身上具有什么特质呢?
- 请问贵公司所用到的技术栈?
HR面
- 进入职场之后的规划?
- 贵公司是体系完善。自己希望能够与公司一起发展,也希望公司能在未来的培训中给予一定的指导,我也会努力让自己的目标与公司团队的目标相匹配。
- 未来规划?
- 我准备在技术领域有所作为,短期的职业规划是,如果有幸能够进入贵公司,我会先把我相对于应聘岗位的短板给补齐,向身边的同事、前辈请教经验。因为我在学习过程中总是会夹带思考一些问题,也热爱和周围的同学和老师交流问题。
- 对工作地点有什么要求?
- 没有要求。如果有机会,我更希望能去总部就职,可能会让我进步的更快
- 期望工资?
- 基于我的能力我期望的薪资是
- 因为我对计算机领域非常感兴趣,只要能够从事到我感兴趣的领域就可以了,所以对工资没有硬性要求。
- 如何看待加班?
- 我会调配自己的时间,积极配合,因为所从事的行业领域是我一直以来都热爱的,所以加班过程中我肯定也能收获很多!
- 我理想中加班是效率比较高的加班,如果是无效没有目的的加班就没必要
- 团队遇到分歧怎么办?
- 在分歧中寻找共识,通过共识化解分歧
- 想想各自的出发点
- 承认冲突并找到解决方案
- 为什么拒绝之前的offer?
- 交叉行业
- 技术栈不符
- 薪资不满意
- 自己的三个优势?
- 有计划
- 学习能力强
- 抗压能力强
- 自己最大的优点和缺点?
- 缺点:公共场合的公开表达
- 自己需要改进的地方?
- 团队中如何分工?
- 过往中对你帮助很大的事?
- 毕业的旅游
- 为什么想加入我们公司?
- 公司的声誉和价值观
- 官司的文化和氛围
- 公司的业务和发展前景
- 公司的培训和职业发展机会
- 因为我的专业及项目经历与贵司的这个岗位很匹配
- 学校生活中遇到的困难?
- 有一些idea没有合适的人可以交流讨论
- 反问?
- 请看下面
反问HR
- 公司的公司氛围、团队建设是怎样子的?
- 这个岗位出差、加班多吗?
- 新人有培训吗?
- 公司的晋升机制是什么样子的呢?
- 公司有餐补、房补、交通补助之类的吗?
- 当面试官问你的薪资要求时,你可以先问一下公司的薪酬体系
- 您认为考核这个岗位员工的最重要指标有哪些?
- 您觉得这个团队的氛围怎么样?