概述
一、计算机基础
1.讲一下OSI七层协议
- 应用层
位于第七层,作用是为用户的应用进程提供网络通信服务。
提供的常用协议有:HTTP(超文本传输协议,底层TCP,默认端口80)、HTTPS(443)、FTP(文本传输协议、TCP、23)、TFTP(简单文本传输协议、21、UDP) - 表示层
处理用户信息的表示问题:比如数据的编码,压缩和解压缩,数据的加密和解密、格式转换等。(LPP轻量级表示协议、XDP外部数据表示协议、会话服务协议) - 会话层
是用户应用程序和网络之间的接口,主要任务是:向两个实体的表示层提供建立和使用连接的方法。比如:会话管理、会话流量控制、寻址。(包含SQL结构化查询语言、网络文件系统 NFS、远程过程调用RPC 、DNS协议、SMTP协议)NDS(域名解析服务、53、服务器间进行域传输TCP、客户端查询UDP) - 传输层
主要是为两台主机进程之间的通信提供服务。(TCP、UDP) - 网络层
将网络地址翻译成对应的物理地址,并通过路由选择算法为分组通过通信子网选择最适当的路
径。(IP、ICMP、ARP、AKP等) - 数据链路层
接收来自物理层的位流形式的数据,并封装成帧,传送到上一层(PDN/PPP等) - 物理层
利用传输介质为数据链路层提供物理连接,实现比特流的透明传输,如网线;网卡标准。(IEEE 802.1A,IEEE 802.2到IEEE 802.11)
2.讲一下TCP/IP参考模型
应用层、传输层、网际互连层、网络访问层
3.网络层常见协议?(IP、ICMP、RIP、IGMP)
- IP(网际协议)
IP协议不但定义了数据传输时的基本单元和格式,还定义了数据报的递交方法和路由选择 - ICMP(Internet控制报文协议)
ICMP就是一个“错误侦测与回报机制”,其目的就是让我们能够检测网路的连线状况﹐也能确保连线的准确性,是ping和traceroute的工作协议 - RIP (路由信息协议)
使用“跳数”(即metric)来衡量到达目标地址的路由距离 - IGMP (Internet组管理协议)
用于实现组播、广播等通信
4. HTTP头部
通用头:是客户端和服务器都可以使用的头部,可以在客户端、服务器和其他应用程序之间提供一些非常有用的通用功能,如Date头部。
请求头:是请求报文特有的,它们为服务器提供了一些额外信息,比如客户端希望接收什么类型的数据,如Accept头部。
响应头:便于客户端提供信息,比如,客服端在与哪种类型的服务器进行交互,如Server头部。
实体头:指的是用于应对实体主体部分的头部,比如,可以用实体头部来说明实体主体部分的数据类型,如Content-Type头部。
5. HTTP状态码
HTTP 状态码由三个十进制数字组成,第一个数字定义了状态码的类型,后两个并没有起到分类的作用。HTTP 状态码共有 5 种类型:
- 1XX 指示信息–表示请求正在处理
- 2XX 成功–表示请求已被成功处理完毕
- 3XX 重定向–要完成的请求需要进行附加操作
- 4XX 客户端错误–请求有语法错误或者请求无法实现,服务器无法处理请求
- 5XX 服务器端错误–服务器处理请求出现错误
200 响应成功
302 跳转,重定向
403 拒绝访问 404 找不到资源
501 服务器不支持当前请求所需要的某个功能
6.GET和POST区别(http请求方法)
GET 请求指定的页面信息,并返回实体主体。
POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。
- get是获取数据,post是修改数据,而且 post比get更安全,
因为 get t把请求的数据放在url上, 以?分割URL和传输数据,参数之间以&相连,所以get不太安全。参数直接暴露在 URL中,可能会存在安全问题,因此往往用于获取资源信息。
而 post 参数放在 HTTP的包体内(requrest body )中,并且参数不会被保留,相比get 方法,post 方法更安全,主要用于修改服务器上的资源。
- get 请求只支持 URL 编码,post 请求支持多种编码格式。
- get 只支持 ASCII 字符格式的参数,而 post 方法没有限制。
- GET请求会被浏览器主动缓存,而POST不会,除非手动设置。
- get 提交的数据大小有限制(针对浏览器),最大是2k,而 post 方法提交的数据没限制
- GET产生一个TCP数据包,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
- POST产生两个TCP数据包,浏览器先发送header,服务器响应100 状态码,浏览器再发送data,服务器响应200 ok(返回数据)。
7 URI和URL的区别(应用层)
URL(统一资源定位符),就是平时上网输入的网址,它标识了一个互联网资源,并指定对其进行操作或获取该资源的方法;
URI(统一资源标识符)是一种抽象概念,URL是URI的子集
简而言之就是,只要能唯一标识资源的就是URI,在URI基础上给出其资源的访问方式的就是URL。
8.在浏览器输入一个URL(http请求)
- 在发送http请求前,需要域名解析(DNS解析),获取对应的IP
浏览器先查看浏览器缓存-操作系统缓存-路由器缓存,如果缓存中有,会直接在屏幕中显示页面内容。若没有,则跳到第三步操作。
(1)浏览器缓存:,先在浏览器找之前有没有缓存过的域名所对应的ip地址,如果有,就调用这个 IP 地址映射,解析完成。
(2)操作系统缓存:(查找硬盘的hosts文件)使系统调用操作系统,获取操作系统的记录(保存最近的DNS查询缓存);
(3) 路由器缓存:向DNS服务器发送DNS请求,查询本地DNS服务器,这其中用的是UDP的协议。在一个子网内采用ARP地址解析协议进行ARP查询,如果不在一个子网那就需要对默认网关进行DNS查询。如果缓存中有此条记录,就可以直接返回结果。
(5)如果没有,本地DNS服务器还要向DNS根服务器进行查询。 - 拿到解析的IP地址及端口号(http 80 端口,https 443 端口),调用系统库函数Socket,向服务器发起tcp连接,与浏览器建立tcp三次握手。
- 客户端向服务器发送 HTTP 请求报文。
- 服务器端经过物理层→数据链路层→网络层→传输层→应用层,解析请求报文,发送 HTTP 响应报文。
- 关闭连接,TCP 四次挥手。
- 客户端解析 HTTP 响应报文,浏览器开始显示 HTML
- 分析页面中的超链接,显示在当前页面,重复以上过程直至没有超链接需要发送,完成页面的全部显示。
(1)应用层:客户端发送 HTTP 请求报文。
(2)传输层:(加入源端口、目的端口)建立连接。实际发送数据之前,三次握手客户端和 服务器建立起一个 TCP 连接。
(3)网络层:(加入 IP 头)路由寻址。
(4)数据链路层:(加入 frame 头)传输数据。
(5)物理层:物理传输 bit。
9.HTTP和HTTPS的区别
1.HTTP协议以明文方式发送内容,数据未加密,安全性较差;而HTTPS数据传输过程是加密的。
2.HTTP和HTTPS使用的连接方式不一样,用的端口也不一样,HTTP使用80端口,HTTPS使用443端口。
3.HTTPS协议需要到数字机构申请证书,要钱。
4.HTTP页面响应比HTTPS快,主要HTTP使用三次握手建立连接,而HTTPS除了三次握手,还需要经历一个SSL协商过程。
10. HTTPS加密(SSL是怎么保证安全的)
HTTPS 采用对称加密和非对称加密相结合的方式。
使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。
- 对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。
优点:运算速度快 缺点:无法安全地将密钥传输给通信方 - 非对称密钥加密,,加密(公开密钥)和解密(私有密钥)使用不同的密钥。
优点:可以更安全地将公开密钥传输给通信发送方;缺点:运算速度慢。
使用SSL(安全套阶层)、TLS(安全层传输协议)。SSL 利用数据加密、身份验证和消息完整性验证机制,为网络上数据的传输提供安全性保证。
(1)客户端向服务器端发起SSL连接请求;
(2)服务器把公钥发送给客户端,并且服务器端保存着唯一的私钥
(3)客户端用公钥对双方通信的对称秘钥进行加密,并发送给服务器端
(4)服务器利用自己唯一的私钥对客户端发来的对称秘钥进行解密,
(5)进行数据传输,服务器和客户端双方用公有的相同的对称秘钥对数据进行加密解密,可以保证在数据收发过程中的安全,即是第三方获得数据包,也无法对其进行加密,解密和篡改。
11.Cookie是什么?
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,
HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务,HTTP/1.1 引 入 Cookie 来保存状态信息,通过在请求和响应报文中写入cookie信息来控制客户端的状态。
可以用在哪些方面:
- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
- 个性化设置(如用户自定义设置、主题等)
- 浏览器行为跟踪(如跟踪分析用户行为等)
12. Socket(TCP/IP)
- socket
是对TCP/IP协议的封装。socket是应用层与TCP/IP协议族通信的一组调用接口(TCP/IP网络的API函数),是基于TCP协议的,所以通常情况下 Socket 连接就是 TCP 连接,因此 Socket 连接一旦建立,通信双方即可开始相互发送数据内容. - HTTP
连接使用的是**“请求—响应**”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。
13.http 1.0 和1.1区别
- 长连接:
http1.0 中默认使用短连接,服务器和客户端每进行一次 http 操作,就建立一次连接,任务结束就终端连接,
http1.1 :默认使用长连接,用以保持连接特性,当一个网页打开完成后, 服务器和客户端之间用于传输 http 数据的 tcp 连接不会关闭,客户端再次访问这个服务器时, 会继续使用这一条已经建立好的连接。 - 错误状态响应码 :
在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。 - 缓存处理 :
在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准
HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。 - 带宽优化及网络连接的使用 :
HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1支持只发送header信息(不带任何body信息),如果服务器认为客户端有权限请求服务器,则返回100,客户端接收到100才开始把请求body发送到服务器;如果返回401,客户端就可以不用发送请求body了节约了带宽。
14.讲一下三次握手(传输层)
三次握手是TCP连接的建立过程,刚开始,客户端进入close状态,服务端处于listen状态
- 第一次握手:建立连接时,客户端发送 SYN 包到服务器(标志位SYN=1,初始序号为Seq = X),并进入SYN-SENT 状态, 等待服务器确认;SYN:同步序列编号。
- 第二次握手:服务器收到 SYN 包,必须确认客户的 SYN,(返回ACK=X+1),同时自己也发送一个 SYN 包(syn=1,Seq = y),即一起发了 SYN+ACK 包,此时服务器进入 SYN-RECV 状态;
- 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=y+1),此包发送完毕,客户端和服务器进入 ESTABLISHED(TCP 连接成功)状态,完成三次握手。
为什么三次
如果只进行两次握手,服务端不知道客户端能否接收消息,同时也不知道自已的消息发出去了没有,三次握手已经保证了TCP连接的需求,所以就不用第四次握手了
15.讲一下四次挥手(传输层)
在挥手之前主动释放连接的客户端结束 ESTABLISHED 阶段,随后开始四次挥手:
- 第一次挥手
客户端向服务器发送一段 FIN 报文 (FIN=1,序号seq=u),表明其想要释放 TCP 连接,随后 客户端进入FIN-WAIT-1 阶段,并且停止发送通信数据。 - 第二次挥手
(1)服务端收到 FIN 报文之后,会发送 ACK 报文 (ACK=1 , 确认号 ack=u+1 , 序 号 seq=v),表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
(2) 客户端收到服务器发送过来的报文后,确认服务器已经收到连接释放的请求,随后客户结束 FIN-WAIT-1 阶段,进入FIN-WAIT-2 阶段 - 第三次挥手
服务器端在发出ACK 确认报文后,会将遗留的待传数据传送给客户端,待传输完成后即经过 CLOSE-WAIT 阶段,便做好了释放服务器端到客户端的连接准备,再次向客户端发出一段连接释放报文段 (FIN=1,ACK=1,序号seq=w,确认号ack=u+1),随后服务器端结束 CLOSE-WAIT 阶段,进入 LAST-ACK 阶段。并且停止向客户端发送数据。
4.第四次挥手
(1) 客户端收到 FIN 报文之后,再发送一个 ACK 报文作为应答(ACK=1,seq=u+1,ack=w+1),并结束 FIN-WAIT-2 阶段,进入 TIME-WAIT 阶段。
随后客户端开始在 TIME-WAIT 阶段 等待 2 MSL 。
(2)服务器端收到从客户端发出的 TCP 报文之后结束 LAST-ACK 阶段,进入 CLOSED 阶段。由此正式确认关闭服务器端到客户端方向上的连接。
客户端等待完 2 MSL 之后,结束 TIME-WAIT 阶段,进入 CLOSED 阶段,由此完成「四次挥手」。
为什么四次
在连接中,服务器的 ack 和 syn 包是同时发送的,
而在断开连接的时候,服务器向客户端发 送的 ack 和 fin 包是分两次发送的,因为服务器收到客户端发送的 fin 包时,可能还有数据要传 送,所以先发送 ack,等数据传输结束后再发送 fin 断开这边的连接。
CLOSE-WAIT 和 TIME-WAIT 的状态和意义
- CLOSE-WAIT 状态就是为了保证服务器在关闭连接之前将待发送的数据发送完成。
- TIME-WAIT 发生在第四次挥手,当客户端发送 ACK 确认报文后进入该状态,若取消该状态,即客户端在收到 FIN 报文后立即关闭连接,此时服务端相应的端口并没有关闭,若客户端在相同的端口立即建立新的连接,则有可能接收到上一次连接中残留的数据包,可能会导致不可预料的异常出现。
- 除此之外,假设客户端最后一次发送的 ACK 包在传输的时候丢失了,由于 TCP 协议的超时重传机制,服务端将重发 FIN 报文,若客户端并没有维持 TIME-WAIT 状态而直接关闭的话,当收到服务端重新发送的 FIN 包时,客户端就会用 RST 包来响应服务端,这将会使得对方误认为是有错误发生。
2MSL等待理由
MSL指最长报文段寿命,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
- 保证客户端发送的最后一个ACK报文段能够到达服务端。
- 防止“已失效的连接请求报文段”出现在本连接中。
16.TCP、UDP区别(传输层)
传输控制协议和用户数据报协议区别
面向连接的TCP协议保证了数据的传输可靠性,面向无连接的UDP协议能够实现数据表简单、快速地传输。
TCP 是面向连接的,UDP 是面向无连接的
UDP程序结构较简单,传输效率更高
TCP 是面向字节流的,UDP 是基于数据报的
TCP保证数据正确性,UDP 可能丢包
TCP 保证数据顺序,UDP 不保证
17.TCP是如何保证可靠的
- 数据分块:应用数据被分割成 TCP 认为最适合发送的数据块。
- 序列号和确认应答:TCP 给发送的每一个包进行编号,在传输的过程中,每次接收方收到数据后,都会发送 ACK 报文,告诉发送方成功接收了哪些数据以及下一次的数据从哪里开始发。
- 校验和:TCP 将保持它首部和数据部分的检验和,如果收到报文段的检验和有差错,TCP 将丢弃这个报文段并且不确认收到此报文段。
- 流量控制:发送方发送的数据量不能超过接收端缓冲区的大小。当接收方来不及处理发送方的数据,会提示发送方降低发送的速率,防止产生丢包。
- 拥塞协议:当网络某个节点发生拥塞时,减少数据的发送。
- ARQ协议:为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
- 超时重传:当 TCP 发出一个报文段后,它启动一个定时器,等待目的端确认收到这个报文段。如果超过某个时间还没有收到确认,将重发这个报文段。
- TCP 的顺序问题,丢包问题,流量控制都是通过滑动窗口来解决的拥塞控制时通过拥塞窗口来解决的
滑动窗口
TCP 建立连接时,各端分配一个缓冲区用来存储接受的数据,并将缓冲区的尺寸发送给另一 端,接收方发送的确认消息中包含了自己剩余的缓冲区尺寸,剩余缓冲区空间的数量叫做窗口, 所谓滑动窗口,就是接收端可以根据自己的状况通告窗口大小,从而控制发送端的接收,进行流 量控制
18.TCP拥塞控制(慢启动、拥塞避免、拥塞发生。快速回复)
拥塞控制到现在主要是四个算法:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。
-
慢热启动算法 – Slow Start
所谓慢启动,也就是TCP连接刚建立,一点一点地提速,试探一下网络的承受能力,以免直接扰乱了网络通道的秩序。
慢启动算法:
(1) 连接建好的开始先初始化拥塞窗口cwnd大小为1,表明可以传一个MSS大小的数据。
(2) 每当收到一个ACK,cwnd大小加一,呈线性上升。
(3)每当过了一个往返延迟时间RTT(Round-Trip Time),cwnd大小直接翻倍,乘以2,呈指数让升。
(4) 还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法” -
拥塞避免算法 – Congestion Avoidance
当拥塞窗口大小 >= 慢启动阈值 ssthresh后,就进入拥塞避免算法。
算法如下:
(1) 收到一个ACK,则cwnd = cwnd + 1 / cwnd
(2)每当过了一个往返延迟时间RTT,cwnd大小加一。
(3)过了慢启动阈值后,拥塞避免算法可以避免窗口增长过快导致窗口拥塞,而是缓慢的增加调整到网络的最佳值。 -
拥塞发生状态时的算法
TCP拥塞控制默认认为网络丢包是由于网络拥塞导致的,所以一般的TCP拥塞控制算法以丢包为网络进入拥塞状态的信号。
(1)对于丢包有两种判定方式,一种是超时重传RTO超时,另一个是收到三个重复确认ACK。
(2)超时重传是TCP协议保证数据可靠性的一个重要机制,其原理是在发送一个数据以后就开启一个计时器,在一定时间内如果没有得到发送数据报的ACK报文,那么就重新发送数据,直到发送成功为止。
(3)但是如果发送端接收到3个以上的重复ACK,TCP就意识到数据发生丢失,需要重传。这个机制不需要等到重传定时器超时 -
快速恢复算法 – Fast Recovery
在进入快速恢复之前,拥塞窗口cwnd和慢启动阈值ssthresh已经被更改为原有cwnd的一半。
19. PING(网络层)
- ping命令通常用来作为网络可用性的检查。ping命令可以对一个网络地址发送测试数据包,看该网络地址是否有响应并统计响应时间,以此测试网络。
- 两台电脑连起来后 ping 不通,你觉得可能存在哪些问题?
· 首先看网络是否连接正常,检查网卡驱动是否正确安装。
局域网设置问题,检查 IP 地址是否设置正确。
· 看是否被防火墙阻拦(有些设置中防火墙会对 ICMP 报文进行过滤),如果是的话,尝试关闭防火墙 。
· 看是否被第三方软件拦截。
· 两台设备间的网络延迟是否过大(例如路由设置不合理),导致 ICMP 报文无法在规定的时间内收到。
20.光猫、路由器和交换机的区别
- 交换机:是一种用于光/电信号转发的网络设备,它利用主机的物理地址(MAC 地址)确定数据转发的目的地址,它工作在数据链路层。
- 路由器:路由器接入光猫,发射wifi信号,实现网络共享,路由器通过数据包中的目的 IP 地址识别不同的网络从而确定数据转发的目的地址,网络号是唯一的。路由器根据路由选择协议和路由表信息从而确定数据的转发路径,直到到达目的网络,它工作于网络层。
- 光猫:负责的是将通过光纤传送的数字信号和模拟信号之间进行转换,在发送端通过调制将数字信号转换为模拟信号,而在接收端通过解调再将模拟信号转换为数字信号,通俗的说就是数字信号与模拟信号的“翻译员”。
21.IPV6和IPV4的区别
IP地址数量不同。
IPv4的地址是32位,采用A、B、C三类编址方式
而IPv6的地址是128位的。IPv6具有更大的地址空间,
- A类
地址范围从1.0.0.1——127.255.255.254 ,
子网掩码为255.0.0.0,适用大型网络,A类网络地址数量较少,有126个网络 - B类
地址范围从128.0.0.1——191.255.255.254,
子网掩码为255.255.0.0,适用中型网络,B类网络地址数量适中,有16384个网络 - C类
地址范围从192.0.0.1——223.255.255.254,
子网掩码为255.255.255.0,适用小型网络,C类网络地址数量较多,有209万余个网络
22.IP 协议(网络层)
即互联网协议,是支持网间互联的数据报协议。该协议工作在网络层,
主要目的:为了提高网络的可扩展性,和传输层 TCP 相比,IP 协议提供一种无连接/不可靠、尽力而为的数据包传输服务,其与TCP协议(传输控制协议)一起构成TCP/IP 协议族的核心。
IP 协议主要有以下几个作用:
- 寻址和路由:在IP 数据报中会携带源 IP 地址和目的 IP 地址,来标识该数据报的源主机和目的主机。IP 数据报在传输过程中,每个中间节点(IP 网关、路由器)只根据网络地址进行转发,如果中间节点是路由器,则路由器会根据路由表选择合适的路径。IP 协议根据路由选择协议提供的路由信息对 IP 数据报进行转发,直至抵达目的主机。
- 分段与重组:IP 数据包在传输过程中可能会经过不同的网络,在不同的网络中数据报的最大长度限制是不同的,IP 协议通过给每个 IP 数据包分配一个标识符以及分段与组装的相关信息,使得数据包在不同的网络中能够传输,被分段后的 IP 数据报可以独立地在网络中进行转发,在到达目的主机后由目的主机完成重组工作,恢复出原来的 IP 数据包。
23.MAC地址和IP地址(数据链路层)
- MAC 地址是数据链路层和物理层使用的地址,是写在网卡上的物理地址。用来定义网络设备的位置。
- IP 地址是网络层和以上各层使用的地址,是一种逻辑地址。IP 地址用来区别网络上的计算机。
二、C++基础
1.C++ 程序编译过程
编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。
- 编译预处理:处理以 # 开头的指令;
- 编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码;
- 汇编:将汇编代码 .s 翻译成机器指令 .o 文件;
- 链接:汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp 文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe 文件。
静态连接和动态连接
- 静态链接:将程序调用的库一起打包到可执行文件中,这样执行时就不需要调用别的库了(浪费空间、更新困难,但是执行的时候运行速度快)
- 动态链接:代码被放到动态链接库或共享对象的某个目标文件中,是在程序执行时才载入引用的库,因此方便更新(节省内存、更新方便,但是每次执行都需要连接,存在一定的性能损失)
2.面向对象三大特性
面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。
- 封装:
将具体的实现过程和数据封装成一个类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如将公共的数据或方法使用public修饰,而不希望被访问的数据或
方法采用private修饰。 - 继承:
指可以让某个类型的对象获得另一个类型的对象的属性的方法。子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。 - 多态:
多态就是不同继承类的对象,对同一消息做出不同的响应,同一个函数,在调用父类对象和子类对象的时候会产生不同的行为。(重载实现编译时多态,虚函数实现运行时多态)。
3.多态如何实现,为什么用多态
- 实现方法:
在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。 - 实现过程:
(1)编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址
(2)编译器会在每个对象的前四个字节中保存一个虚表指针,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数;
(3)在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表
(4)当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面
(5)这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性。 - 虚表:虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表
虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针 - 多态的种类和表现形式?
静态多态(编译器多态):重载
动态多态(运行时多态):多态
为什么用多态:
1.代码重用: 封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);
2. 提高程序的可复用性:应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。
3. 提高可扩充性和可维护性: 派生类的功能可以被基类的方法或引用变量所调用,可以解决项目中紧偶合的问题。
4. 【 耦合度讲的是模块与模块之间,代码与代码之间的关联度,通过对系统的分析把他分解成一个一个子模块,子模块提供稳定的接口,达到降低系统耦合度的目的,模块与模块之间尽量使用模块接口访问,而不是随意引用其他模块的成员变量。】
4.重载、重写、隐藏的区别
- 重载:是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
- 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
- 重写(覆盖):是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。
- 重写和重载的区别:
范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。
参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。
virtual 关键字:重载的函数可以有也可没有,重写的函数基类中必须有 virtual关键字的修饰, - 隐藏和重写,重载的区别:
范围区别:隐藏与重载范围不同,隐藏发生在不同类中。
参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。
5.面向对象几个原则
设计目标:开闭原则、里氏代换原则、迪米特原则
设计方法:单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则
- 单一职责
系统中的每个对象应该只有一个单独的职责,所有对象关注的应该是自身职责的完成。
基本思想:高内聚、低耦合 - 开闭原则
一个对象对扩展开发,对修改关闭。
基本思想:对类的改动是通过增加代码进行的,而不是修改现有的代码。 - 里氏替换原则
在任意父类出现的地方,都可以使用子类来替代。 - 依赖注入原则
要依赖于抽象,不要依赖于具体实现。
基本思想:在开发中尽量的面向接口编程。 - 接口分离原则
不要去使用一些不需要使用的功能。
基本思想:一个接口不要提供太多的行为。 - 迪米特原则
一个对象应该对其它的对象应该尽可能少的了解。
基本思想:降低耦合。 - 组合/聚合复用原则
基本思想:在复用对象的时候,要优先考虑组合,而不是继承。因为父类的任何改变都可能直接影响子类的行为。
6.虚函数和纯虚函数
- 虚函数:被 virtual 关键字修饰的成员函数,就是虚函数,可以直接使用;必须实现,否则编译器报错;
- 纯虚函数:在类中声明时,加上 =0,必须在派生类中实现
含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;
继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。 - 虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。
- 虚函数作用:
实现动态绑定,可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时, 基类指针调用其虚成员函数,则会调用其真正指向对象的成员函数, 而不是基类中定义的成员函数。 若不是虚函数,则不管基类指针指向的哪个派生类对象,调用时都 会调用基类中定义的那个函数。
7.虚函数的实现机制
- 实现机制:虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。
- 虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。
- 虚函数表相关知识点:
虚函数表存放的内容:类的虚函数的地址。
虚函数表建立的时间:编译阶段
虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
8.析构函数和构造函数
-
析构函数为什么是虚函数
因为基类的指针可以指向派生类对象,如果不是虚函数的话,删除基类的指针, 只会调用基类的析构函数而不会调用派生类的,这样派生类就不能完全被析构, 会造成内存泄漏。 -
构造函数不能是虚函数
(1)没法创建对象:创建对象的时候要知道对象的类型,而虚函数是在运行的时候动态确定类型的,而构造函数调用的时候,对象还没有被创造,所以不知道类型,也就没办法创建对象。
(2) 没有虚函数表:虚函数调用的时候需要虚函数表,虚函数表放在这个对象的内存空间中。而对象还没有创建的时候,也就没有虚函数表,也就无法调用虚函数。
(3)没有意义:虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数没有实际意义。
构造函数析构函数调用的顺序
(1) 类对象的初始化顺序:基类构造函数–>派生类成员变量的构造函数–>自身构造函数
(2) 析构函数 先调用派生类的,再调用成员类对象的,再调用基类的。
9.怎么初始化一个类的成员和顺序
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。
- 一般变量可以在初始化列表里或者构造函数里初始化,不能直接初始化或者类外初始化
- 静态成员变量static必须在类外初始化
- 常量const必须在初始化列表里初始化
- 静态常量必须只能在定义的时候初始化(定义时直接初始化)
一个派生类构造函数的执行顺序如下:
① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
③ 类类型的成员对象的构造函数(按照初始化顺序)
④ 派生类自己的构造函数。
10.什么情况下必须用初始化列表
- 成员类型是没有默认构造函数的类。
若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。 - const 成员或引用类型的成员。
因为 const 对象或引用类型只能初始化,不能对他们赋值。
对于普通数据成员而言,其值的设定可以放在初始化阶段或者普通计算阶段完成。
对于 const类型和&引用类型数据成员,其初始化必须在初始化阶段完成。若通过普通计算阶段来初始化该值,编译器会报错:该变量未初始化。 - 作用
① 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;
② list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;
11.深copy浅copy
- 浅拷贝是增加了一个指针,指向原来已经存在的内存。
- 而深拷贝是增加了一个指针,并新开辟了一块空间让指针指向这块新开辟的空间。浅拷贝在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误。
12.内存分配
在C++中内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
- 堆:用于程序的内存动态分配
- 栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 自由存储区:自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。
- 全局/静态存储区:这块内存是在程序编译的时候就已经分配好的,在程序整个运行期间都存在。例如全局变量,静态变量。
- 常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量(const),不允许修改。
动态内存分配
在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。
动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分 配的大小就是程序要求的大小。
13.堆和栈的区别
- 申请方式:栈是系统自动分配,堆是程序员主动申请
- 栈是内存中连续的一块空间,向低地址扩展,最大容量是系统预定好的,堆在内存中的空间,向高地址扩展,是不连续的。
- 申请效率:栈申请效率高,但是程序员无法控制,堆效率低。
- 存放内容:栈中存放局部变量,函数的参数;堆中存放的内容由程序员控制。
- 堆是指程序运行是申请的动态内存,而栈只是指一种使用堆的方法(即先进后出。
- 栈是先进后出的,但是于堆而言却没有这个特性,两者都是存放临时数据的地方。
14.数组和指针的区别
数组:数组是用于储存多个相同类型数据的集合。
指针:指针相当于一个变量,它存放的是其它变量在内存中的地址
区别:
- 赋值:同类型指针变量可以相互赋值;数组不行
- 存储方式:数组在内存中是连续存放的;指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
- 求 sizeof:数组所占存储空间的内存;在 32 位平台下,无论指针的类型是什么,sizeof(指针名)都是 4,在 64下都是 8。
- 初始化方式不同。
- 传参方式:数组传参时,会退化为指针;将整个数组拷贝一份传入函数时,将数组名看做常量指针,传数组首元素的地址。
一级指针传参可以接受的参数类型:(1)整形指针 (2)整型变量地址 (3)一维整型数组数组名;
函数参数部分是二级指针时(1)二级指针变量(2)一级指针变量地
3)一维指针数组的数组名
15.引用和指针的区别
- 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
- 指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)
- 指针在内存中占有存储内存空间,引用相当于变量的别名,在内存中会占4字节内存
- 指针可以为空,但是引用必须绑定对象。(是否可为空)
- 指针可以有多级,但是引用只能一级。(是否能为多级)
引用在创建的时候必须初始化,只有在调用虚函数时,才能实现动态绑定
常量指针和指针常量的区别
- 常量指针:
常量指针本质上是个指针,只不过这个指针指向的对象是常量。
特点:const 的位置在指针声明运算符的左侧。只要 const 位于的左侧,无论它在类型名的左边或右边,都表示指向常量的指针。 - 指针常量:
指针常量的本质上是个常量,只不过这个常量的值是一个指针。
特点:const 位于指针声明操作符右侧,表明该对象本身是一个常量,* 左侧表示该指针指向的类型。
16.C++中的四种强制类型转换
- Reinterpret_cast 可以用于类型之间的强制转换
reinterpret_cast (expression)
改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型。 - Const_cast 可以将常量转换成非常量
const_cast<type_id> (expression)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
用法如下:
常量指针被转化成非常量的指针,并且仍然指向原来的对象
常量引用被转换成非常量的引用,并且仍然指向原来的对象
const_cast一般用于修改底指针。如const char *p形式 - Static_cast 主要是派生类的指针或引用和基类之间的转换
static_cast < type-id > (expression)
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
3.1 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换
【1】进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
【2】 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的
3.2 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
3.3 把空指针转换成目标类型的空指针
3.4 把任何类型的表达式转换成void类型
注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。dynamic_cast 进行下行转换。 - Dynamic_cast (主要用于类层次间的上行转换和下行转换)
有类型检查 基类行派生类转换比较安全,派生类向基类不安全。
该运算符把expression转换成type-id类型的对象。type-id 必须是类的指针、类的引用或者void*
如果 type-id 是类指针类型,那么expression也必须是一个指针,如果 type-id 是一个引用,那么expression 也必须是一个引用
dynamic_cast运算符可以在执行期决定真正的类型,也就是说expression必须是多态类型。
如果下行转换是安全的(也就说,如果基类指针或者引用确实指向一个派生类对象)这个运算符会传回适当转型过的指针。
如果下行转换不安全,这个运算符会传回空指针(也就是说,基类指针或者引用没有指向一个派生类对象)
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的
在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全
17.malloc/free和new/delete的区别
- malloc、free 是库函数,而new、delete 是关键字。
- new 申请空间时,编译器会根据类型自行计算空间;malloc 在申请空间时,需要确定所申请空间的大小。
- new 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,是类型安全的操作符;malloc 申请空间时,返回的是 void* 类型,需要进行强制类型的转换,转换为对象类型的指针。
- new 分配失败时,会抛出 bad_alloc 异常,malloc 分配失败时返回空指针。
- 对于自定义的类型,new 首先调用 operator new() 函数申请空间,然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete() 释放空间。malloc、free 无法进行自定义类型的对象的构造和析构。
- new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)
18.struct和class的区别
- 默认访问控制不同:struct是public的,而class是private的
- 在继承关系,struct默认是public的,而class是private,默认的防控属性取决于子类而不是基类
- class可用于定义模板参数,就像typename。但是strcut不用。
- 比起C语言来,struct可以拥有静态成员、成员数据可进行初始化、拥有函数、也可以继承、甚至多态也支持
19.define宏定义和const的区别
- 编译阶段
define是在编译的预处理阶段起作用,
const是在编译、运行的时候起作用 - 安全性
define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般可以用大括号包含住全部的内容,要不然很容易出错
const常量有数据类型,编译器可以对其进行类型安全检查 - 内存占用
(1)define在内存中会产生多份相同的备份。 const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表
(2)宏替换发生在编译阶段之前,属于文本插入替换;const作用发生于编译过程中。
(3)define不检查类型;const会检查数据类型。
(4)define定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间
20.const的作用
- 不考虑类的情况
(1)const常量在定义时必须初始化,之后无法更改
(2)const形参可以接收const和非const类型的实参 - 考虑类的情况
(1)const成员变量:
不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
(2)const成员函数:
const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值
21.static的作用
- static 定义静态变量,静态函数。
- 不考虑类的情况
(1)隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
(2)默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
(3)静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用 - 考虑类的情况
(1)static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
(2)static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问
22.全局变量、局部变量、静态全局变量、静态局部变量的区别
C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
- 从作用域看:
全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用 extern 关键字再次声明这个全局变量。
静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被 static 关键字修饰过的变量具有文件作用域 。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。 - 从分配内存空间看:
静态存储区:全局变量,静态局部变量,静态全局变量。
栈:局部变量。
说明:
静态变量和栈变量(存储在栈中的变量)、堆变量(存储在堆中的变量)的区别:静态变量会被放在程序的静态数据存储区(.data 段,中(静态变量会自动初始化),这样可以在下一次调用的时候还可以保持原来的赋值。而栈变量或堆变量不能保证在下一次调用的时候依然保持原来的值。
静态变量和全局变量的区别:静态变量用 static 告知编译器,自己仅仅在变量的作用范围内可见。
23.inline 作用与内连函数
- inline 是一个关键字,可以用于定义内联函数。
- 内联函数,像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是直接在调用点处展开,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。
- 使用方法:
类内定义成员函数默认是内联函数
类外定义成员函数,若想定义为内联函数,需用inline关键字声明
4.宏定义(define)和内联函数(inline)的区别
4.1 内联函数是在编译时展开,而宏在编译预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
4.2 内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。而宏定义编写较为复杂,常需要增加一些括号来避免歧义。
4.3 宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。
24.main函数之前执行什么
- 在调用main函数之前,会先进行初始化栈,堆,打开标准输入,输出,错误流,把参数压栈。还有一些全局变量、对象和静态变量、对象的空间分配和赋初值。
- 在调用main函数之后,要销毁堆内存,关闭标准输入,输出,错误流。
25. 内存溢出
内存溢出(OutOfMemory)是指程序申请内存时,没有足够的内存供申请者使用。
- 内存溢出的原因:
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据。
2.集合类中有对象的引用,使用完后为清空,使得不能回收。
3.代码中存在死循环或循环产生过多重复的对象实体。
26.什么是内存泄漏,怎么防止和检测
- 内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存,造成了内存的浪费。一般是:调用了malloc/new等内存申请的操作,但缺少了对应的free/delete,
- 防止:
(1)计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
(2)一定要将基类的析构函数声明为虚函数
(3)对象数组的释放一定要用delete []
(4)有new就有delete,有malloc就有free,保证它们一定成对出现
(5)智能指针:智能指针是 C++ 中已经对内存泄漏封装好了一个工具,可以直接拿来使用。 - 后果:
只发生一次小的内存泄漏可能不被注意,但泄漏大量内存的程序将会出现各种证照:性能下降到内存逐渐用完,导致另一个程序失败; - 检测:
(1) mtrace :检测一些内存分配和泄漏的失败等.(linux下)
使用方法:程序开始时调用mtace()函数。
mtrace 会将内存情况记录下来存在.log 文件中,存放结果可由环境变量malloc_trace 设定。
#gcc -o test test.c -g ;
#./test ;
#mtrace ./test malloc.log 会显示多少行出现问题,内存没释放。
#include <stdlib.h>
#include <mcheck.h>
int main(void) {
mtrace(); /* 开始记录内存分配和释放 */
int* a = NULL;
a = malloc(sizeof(int)); /* 分配内存并将其分配给指针 */
if (a == NULL) {
return 1; /* error */
}
free(a); /*我们释放分配的内存,这样就不会有泄漏*/
muntrace();
return 0;
}
(2) valgrind :(linux)
Valgrind包括如下一些工具:
Memcheck。这是valgrind应用最广泛的工具,一个重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。这也是本文将重点介绍的部分。
Callgrind。它主要用来检查程序中函数调用过程中出现的问题。
Cachegrind。它主要用来检查程序中缓存使用出现的问题。
Helgrind。它主要用来检查多线程程序中出现的竞争问题。
Massif。它主要用来检查程序中堆栈使用中出现的问题。
Extension。可以利用core提供的功能,自己编写特定的内存调试工具
6. Memcheck
$ valgrind --tool=memcheck ./val 命令,而我们想使用的工具是通过’-tool’选项来指定的. 上面的‘a.out’指的是我们想使用memcheck运行的可执行文件.
27.智能指针如何解决内存泄露问题
- shared_ptr共享的智能指针
std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。
可以通过构造函数、std_make_shared辅助函数和reset方法来初始化shared_ptr: - unique_ptr 独占的智能指针
unique_ptr不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另外一个unique_ptr。
unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。
如果希望只有一个智能指针管理资源或管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。 - weak_ptr弱引用的智能指针
weak_ptr不会使shared_ptr引用计数加一,它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命周期,更像是shared_ptr的一个助手。
28.段错误产生的原因
- 原因主要有:
解引用空指针
访问不可访问的内存空间(如内核空间)
访问不存在的内存地址
试图写一个只读内存空间(如代码段)
栈溢出(函数递归调用)
使用未初始化的指针(定义时没有初始化或者已经回收) - 避免段错误
定义指针后初始化
数组下标是否越界
在堆上分配空间是否足够(内存限制)
变量处理时格式控制是否合理
29.智能指针有几种?实现原理,可能出现的问题
- 共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。
- 独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用 move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。
- 如果希望只有一个智能指针管理资源或管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr
- 弱指针(weak_ptr):指向 share_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。weak_ptr不会使shared_ptr引用计数加一,它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命周期,更像是shared_ptr的一个助手。
30.内存对齐
- 内存对齐:
编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中 - 内存对齐的原则:
1.1 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
2.2 结构体每个成员相对于结构体首地址的偏移量 (offset) 都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);
3.3 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节 (trailing padding)。 - 进行内存对齐的原因:(主要是硬件设备方面的问题)
3.1 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
3.2 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
3.3 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
3.4 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignment trap);
3.5 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。 - 内存对齐的优点:
4.1 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
4.2 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。
31.memcpy 函数(用于内存复制)
void *memcpy(void *destin, void *source, unsigned n);
作用是:以source指向的地址为起点,将连续的n个字节数据,复制到以destin指向的地址为起点的内存中。
函数有三个参数,第一个是目标地址,第二个是源地址,第三个是数据长度。
char a[10] = "abcdefgh";
unsigned n = 2;
void * p = memcpy(a+3, a, n);
32.strcpy()(src 所指向的字符串复制到 dest)
char *strcpy(char *dest, const char *src)
把 src 所指向的字符串复制到 dest。
缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。
33.菱形继承
B和C从A中继承,而D多重继承于B,C。那就意味着D中会有A中的两个拷贝。因为成员函数不体现在类的内存大小上,所以实际上可以看到的情况是D的内存分布中含有2组A的成员变量。
菱形继承带来了二义性,还会有有数据冗余浪费内存空间。
- 解决方法:
虚拟继承 - 虚拟继承和普通继承的区别:
对象中多了4个字节
为子类中的构造函数中填充一个指针
对象模型和普通的对象模型不一样,是颠倒过来的。 - 如何解决:
在对象的前面添加4字节,用来存放虚基表指针,这个指针指向虚基表,表中存放的是偏移量,分别是子类对像对于自己的偏移量,和派生类对于基类的偏移量。
通过偏移量即可访问到数据,所以只用保存一份数据即可,解决了二义性的问题。
三、数据结构和算法
1.数组、链表、二叉排序增删改查的时间复杂度
数据结构 | 插入 | 删 | 改 | 查 | 优点 | 缺点 |
---|---|---|---|---|---|---|
数组 | O(1) | O(n) | O(n) | O(n) | 插入效率高,查找速度快 | 空间利用率不高、数组空间大小固定、内存空间要求高 |
有序数组 | O(n) | O(n) | O(logn) | O(logn) | ||
无序链表 | O(1) | O(n) | O(n) | O(n) | 插入元素速度快、内存利用率高、可以动态拓展 | 随机访问效率低 |
有序序链表 | O(n) | O(n) | O(n) | O(n) | ||
二叉树 | O(logn)-O(n) | O(logn)-O(n) | O(logn)-O(n) O(logn)- | O(n) | 查找、插入、删除都快、树保持平衡 | 算法复杂 |
2.哈希表及其原理
Hash 表即散列表,是通过关键字(key)根据哈希算法计算出应该存储地址的位置。其最突出的优点是查找和插入删除是O(1),最坏的就是hash值全都映射在同一个地址上,这样哈希表就会退化成链表。
实现原理:
- 把 Key 通过哈希函数转换成一个整型数字,然后将这份数字对数组长度进行取余,取余结果就当作数组的下标,将value 存储在以该数字为下标的数组空间里。
- 当使用哈希表进行查询的时候,就是再次使用哈希函数将 key 转换为对应的数组下标,并定位到该空间获取 value;
常见的哈希算法
哈希表的组成取决于哈希算法,也就是哈希函数的构成,下面列举几种常见的哈希算法。
- 直接定址法
取关键字或关键字的某个线性函数值为散列地址。
即 f(key) = key 或 f(key) = a*key + b,其中a和b为常数。 - 除留余数法
取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。
即 f(key) = key % p, p < m。这是最为常见的一种哈希算法。 - 数字分析法
当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。
仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。 - 平方取中法
先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
随机分布的关键字,得到的散列地址也是随机分布的。 - 随机数法
选择一个随机函数,把关键字的随机函数值作为它的哈希值。
通常当关键字的长度不等时用这种方法。
哈希hash冲突
哈希冲突是指哈希函数算出来的地址被别的元素占用了
key1 ≠ key2 , f(key1) = f(key2)
一般来说,哈希冲突是无法避免的,如果要完全避免的话,也就是一个值就有一个索引,这样一来,空间就会增大,甚至内存溢出。
解决办法:
- 线性探测
使用hash函数计算出的位置如果已经有元素占用了,则向后依次寻找,找到表尾则回到表头,直到找到一个空位 - 开链
每个表格维护一个链表list,如果hash函数计算出的格子相同,则按顺序存在这个list中 - 再散列
发生冲突时使用另一种hash函数再计算一个地址,直到不冲突 - 二次探测
使用hash函数计算出的位置如果已经有元素占用了,按照 1 2 1^2 12、 2 2 2^2 22、 3 2 3^2 32…的步长依次寻找,如果步长是随机数序列,则称之为伪随机探测 - 公共溢出区
一旦hash函数计算的结果相同,就放入公共溢出区
3.常用数据结构
有链表、栈、队列、树
- 链表
是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点组成,这些节点不必在内存中相连。每个节点由 数据部分 Data 和链部分 Next,Next 指向下一个节点,这样当添加或者删除时,只需要改变相关 节点的 Next 的指向,效率很高
4.二叉查找树、红黑树
二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”;
平衡二叉树(AVL树)在符合二叉查找树(左子树的键值小于根的键值,右子树的键值大于根的键值)的条件下,还满足任何节点的两个子树的高度最大差为1;
二叉查找树(中序遍历,时间O(n))
在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都要大于这个节点的值。
- 查找
首先取根节点,如果它等于要查找的数据,则直接返回,如果小于要查找的数据,则在右子树中继续查找,如果大于要查找的数据,则在左子树中继续查找,也就是二分查找的思想,这样一直递归。 - 插入
首先还是从根节点开始,然后依次它与节点的关系。如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点的数据小,也是类似的操作。 - 删除
如果要删除的节点没有子节点,只需要将父节点中,指向要删除节点的指针置为NULL,
如果要删除的节点只有一个子节点(只有左子节点或者右子节点),只需要删除父节点中,指向要删除的指针,让它指向要删除的节点的子节点就可以了。
如果要删除的节点上有两个子节点,要稍微复杂一点。首先找到这个节点的右子树中最小的节点,把它替换到要删除的节点,然后再删除这个最小节点。因为最小节点肯定没有左子节点。
红黑树
红黑树是一个近似平衡的二叉树,
7. 定义:
具有二叉查找树的特点;根节点是黑色的
每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据
任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的
每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点
5.STL常用容器
C++ STL从广义来讲包括了三类:算法,容器和迭代器。
算法包括排序,复制等常用算法,以及不同容器特定的算法。
容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。
迭代器就是在不暴露容器内部结构的情况下对容器的遍历。
顺序容器:
3. vector
是一种动态数组,具有连续的存储空间,支持快速随机访问。但在插入和删除操作方面,效率比较慢。
底层:
底层结构为数组,由于数组的特点,vector也具有以下特性:
1)、O(1)时间的快速访问;
2)、顺序存储,所以插入到非尾结点位置所需时间复杂度为O(n),删除也一样;
3)、扩容规则:
当我们新建一个vector的时候,会首先分配给他一片连续的内存空间,如std::vector vec,当通过push_back向其中增加元素时,如果初始分配空间已满,就会引起vector扩容,其扩容规则在gcc下以2倍方式完成:
首先重新申请一个2倍大的内存空间;
然后将原空间的内容拷贝过来;
最后将原空间内容进行释放,将内存交还给操作系统;
4. deque
和 vector 类似,支持快速随机访问。二者最大的区别在于,vector 只能在末端插入 数据,而 deque 支持双端插入数据。deque 空间的重新分配要比 vector 快,重新分配空间后,原有的元素是不需要拷贝的。
底层:
底层数据结构为一个中央控制器(map)和多个缓冲区,支持首位(中间不能)快速增删,也支持也随访问,deque 的内存空间分布是小片的连续,小片间用链表相连。中控器(map保存着一组指针,每个指针指向一段数据空间的起始位置,通过中控器可以找到所有的数据空间。如果中控器的数据空间满了,会重新申请一块更大的空间,并将中控器的所有指针拷贝到新空间中。
1.start迭代器:绑定到第一个有效的map结点和该结点对应的缓冲区。
2.finish迭代器:绑定到最后一个有效的map结点和该结点对应的缓冲区。
5. list
是一个双向链表,它的内存空间可以不连续,通过指针来进行数据的访问,导致其随机存储非常低效;但 list 可以很好地支持任意地方的插入和删除,只需移动相应的指针即可
底层:双向链表
关联容器:
- map && multimap
是一种关联性容器,该容器用唯一的关键字来映射相应的值,即具有 key-value 功能。map 内部自建一棵红黑树(一种自平衡二叉树),这棵树具有数据自动排序的功能,内部数据都是有序的。
map与multimap的区别在于,multimap允许关键字重复,而map不允许重复。
底层:
根据红黑树的原理,map与multimap可以实现O(lgn)的查找,插入和删除 - unordered_map 与unordered_multimap
无序排序,低层是哈希表,因此其查找时间复杂度理论上达到了O(n) - set & multiset
是一种关联性容器,set系与map系的区别在于map中存储的是,而set可以理解为关键字即值,即只保存关键字的容器。
低层:
底层使用红黑树实现,插入删除操作时仅仅移动指针即可,不涉及内存的移动和拷贝,所以效率比较高。set 中的元素都是唯一的,而且默认情 况下会对元素进行升序排列。所以在 set 中,要改变元素值必须先删除旧元素,再插入新元素。不提供直接存取元素的任何操作函数, 只能通过迭代器进行间接存取 - queue
是一个队列,实现先进先出功能,queue 不是标准的 STL 容器,却以标准的 STL 容器为基础。(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)
底层:
queue 是在 deque 的基础上封装的。 - stack
实现先进后出的功能,和 queue 一样,也是内部封装了 deque。 - priority_queue:
底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现。
5. 迭代器失效
- vector迭代器失效
(1)当执行erase方法时,指向删除节点的迭代器全部失效,指向删除节点之后的全部迭代器也失效
(2)当进行push_back()方法时,end操作返回的迭代器肯定失效。
(3)当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效。
(4)当插入(push_back)一个元素后,如果空间未重新分配,指向插入位置之前的元素的迭代器仍然有效,但指向插入位置之后元素的迭代器全部失效。 - deque迭代器
(1)对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用都会失效,但是如果在首尾位置添加元素,迭代器会失效,但是指针和引用不会失效
(2)如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器全部失效
(3)在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。 - map
对于map,当进行erase操作后,只会使当前迭代器失效,不会造成其他迭代器失效,这是因为map底层实现是由红黑树实现的,所以当删除一个元素时,会进行二叉树的调整,但每个节点在内存中的地址是没有改变的,改变的只是他们之间的指针指向。
6.为什么要有迭代器,不是有指针吗?
Iterator(迭代器)模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的 方法)访问聚合对象中的各个元素。迭代器不是指针,是类模板,表现的像指针,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
四、操作系统
1.讲一下进程和线程的区别
进程是程序的一次执行过程
线程是进程的一部分,是CPU调度和分派的基本单位,一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
区别
- 每个程序都有独立的代码和数据空间,程序之间的切换会有比较大开销;线程相当于轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己的独立运行栈和程序计数器,线程之间切换的开销小。
- 如果一个进程里面有多个线程,则是有多个线程之间共同完成的。
- 同一进程的线程共享本进程的地址空间和资源,进程之间的地址空间和资源相互独立。
- 每个进程都有程序的出入口,但是线程不能独立执行。
2.线程的哪些资源共享,那些资源不共享
- 堆 由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的
- 全局变量 它是与具体某一函数无关的,所以也与特定线程无关;因此也是共享的
- 静态变量 虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的
- 文件等公用资源 这个是共享的,使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。
独享的资源有 - 栈 栈是独享的
- 寄存器 因为电脑的寄存器是物理的,每个线程去取值难道不一样吗?其实线程里存放的是副本,包括程序计数器PC
3.进程间通信
目的:
数据传输:一个进程需要将它的数据发送给另一个进程。
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
- 管道:
一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。
(1)无名管道(内存文件):是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程之间使用。进程的亲缘关系通常是指父子进程关系。
(2)有名管道(FIFO文件,借助文件系统):也是半双工的通信方式,但是允许在没有亲缘关系的进程之间使用,先进先出的通信方式。 - 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。
- 消息队列:消息队列是有消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号:传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 套接字socket:适用于不同机器间进程通信,在本地也可作为两个进程通信的方式。
- 信号:用于通知接收进程某个事件已经发生,比如按下ctrl + C就是信号。
- 信号量:是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,实 现进程、线程的对临界区的同步及互斥访问。
4.线程间通信
线程间的同步方式包括互斥锁、信号量、条件变量、读写锁:
5.了解的锁机制?(线程同步)
互斥锁:mutex,保证在任何时刻,都只有一个线程访问该资源,当获取锁操作失败时,线程进入阻塞,等待锁释放。
读写锁:rwlock,分为读锁和写锁,处于读操作时,可以运行多个线程同时读。但写时同一时刻只能有一个线程获得写锁。
互斥锁和读写锁的区别:
(a)读写锁区分读锁和写锁,而互斥锁不区分
(b)互斥锁同一时间只允许一个线程访问,无论读写;读写锁同一时间只允许一个线程写,但可以多个线程同时读。
3. 自旋锁:spinlock,在任何时刻只能有一个线程访问资源。但获取锁操作失败时,不会进入睡眠,而是原地自旋,直到锁被释放。这样节省了线程从睡眠到被唤醒的时间消耗,提高效率。
4. 条件锁:就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态。一旦条件满足了,即可唤醒该线程(常和互斥锁配合使用)
5. 信号量:计数器,允许多个线程同时访问同一个资源。
6.讲一下死锁
如果一组进程中的每个进程都在等待一个事件,而这个事件只能由该组的另一个进程触发,这种情况会导致死锁。可以理解为:死锁就是两个线程同时占用两个资源,但又在彼此等待对方释放锁。比如两只羊过独木桥。进程比作羊,资源比作桥。若两只羊互不相让,争着过桥,就产生死锁。
产生条件:
1.互斥条件:进程对所需求的资源具有排他性,若有其他进程请求该资源,请求进程只能等待。
2.不剥夺条件:进程在所获得的资源未释放前,不能被其他进程强行夺走,只能自己释放。
3.请求和保持条件:进程当前所拥有的资源在进程请求其他新资源时,由该进程继续占有。
4.循环等待条件:存在一种进程资源循环等待链,链中每个进程已获得的资源同时被链中下一个进程所 请求。
数据库死锁:
常见的解决死锁的方法
1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
7.虚拟内存
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存 (一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换
8.大文件传输
- 基于socket
由于socket本身缓冲区的限制,大概一次只能发送4K左右的数据,所以在传输大数据时客户端就需要进行分包,在目的地重新组包。 - 使用现有的通讯中间件
- 基于共享文件、ftp(文件传输协议)、scp等
ftp:使用 TCP 传输
scp
用于在Linux下进行远程拷贝文件的命令,scp传输是加密的
scp 被复制目标 复制存储的目录
五、linux
1.内核与系统组成
Linux内核主要由五个子系统组成:进程调度,内存管理,虚拟文件系统,网络接口,进程间通信。
Linux系统一般有4个主要部分:内核、shell、文件系统和应用程序。
2. linux用户态和内核态
用户程序运行在用户态,操作系统运行在内核态.
- 用户态:
当进程在执行用户自己的代码时(应用程序),则称其处于用户态,这时cpu 访问资源有限,运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。 - 内核态:
当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核状态,这时cpu可以访问计算机的任何资源。
特权指令,只能内核态使用:I/O指令、置终端屏蔽指令、停机、修改程序状态字、清内存、建存储保护、设置时钟指令 - 用户态的应用程序可以通过三种方式来访问内核态的资源:
1)系统调用
2)库函数
3)Shell脚本 - 从用户态到内核态切换可以通过三种方式:
(1)系统调用:
用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,
(2)异常:
当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。
(3)外设中断(硬中断):当外围设备完成用户的请求操作后,会像CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序
系统调用与普通函数调用的区别
- 系统调用:
1.使用INT和IRET指令,内核和应用程序使用的是不同的堆栈,因此存在堆栈的切换,从用户态切换到内核态,从而可以使用特权指令操控设备。
2.依赖于内核,不保证移植性
3.在用户空间和内核上下文环境间切换,开销较大
4.是操作系统的一个入口点 - 普通函数调用:
1.使用CALL和RET指令,调用时没有堆栈切换
2.平台移植性好
3.属于过程调用,调用开销较小
4.一个普通功能函数的调用
中断处理流程
请求中断→响应中断→关闭中断→保留断点→中断源识别→保护现场→中断服务子程序→恢复现场→中断返回。
3.I/O多路复用(select、poll和epoll)
I/O复用指的是允许计算机执行或者阻塞在一组数据流上,直到某个到达唤醒阻塞的进程,此时的I/O信道不仅仅是通过一个数据流,而是一组,所以是复用。
阻塞和非阻塞:拿I/O为例子,
如果是阻塞模型,那么程序一直会等到有数据来的时候才会继续向下执行,否则会一直等待数据的到来;
如果是非阻塞模型,如果有数据,那么直接读取数据向下执行,没有数据也会继续向下执行,不过此时可能会进行一些其他的操作,比如Linux中设置一些错误的比特位等。
select、poll和epoll这三个函数是Linux系统中I/O多路复用的系统调用函数。
- select (水平触发LT)
(1)使用select 可以在一个线程内同时处理多个socket的IO请求。用户可以注 册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的
(2)select运行机制
select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或 设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时由内核根据IO状态修改fd_set的内容(通过轮询所有的文件描述符来检查是否有事件发生),由此来通知执行了select()的进程哪一Socket或文件可读。
每个select都要处理一个fd_set结构。
fd_set简单地理解为一个长度是1024的比特位,每个比特位表示一个需要处理的FD,如果是1,那么表示这个FD有需要处理的I/O事件,否则没有
(3)select机制的缺点
=每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
=每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
=为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小限制为1024
(4)优点:
可移植性好;
连接数少并且连接都十分活跃的情况下,效率也不错。
- poll(水平触发LT)
调用过程和select类似,时间复杂度:O(n),其和select不同的地方:采用链表的方式替换原有fd_set数据结构,而使其没有连接数的限制。所以poll的文件描述符没有最大数量的限制,但是依然采用轮询遍历的方式检查是否有事件发生。 - epoll,时间复杂度:O(1)
(1)运行机制
epoll是基于事件驱动的I/O方式,相对于select来说, epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件 描述符的事件存放到内核的一个事件表中,这时候epoll_wait将会接收到消息,并且将数据拷贝到用户空间,这样在用户空间和内核空间的 copy只需一 次
具体是通过红黑树和就绪链表实现的,红黑树存储所有的文件描述符,就绪链表存储有事件发生的文件描述符;
(2)步骤:
第一步:epoll_create()系统调用。建立一个epoll对象,此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。
第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
第三步:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。
(3)优点
接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离
数据轻量拷贝:只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频 繁(而 select / poll 都是每次循环都要进行拷贝),开销变的很小
事件回调机制:避免使用遍历,而是使用回调函数的方法,将就绪的文件描述符加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪,这和操作时间复杂度是O(1),即使文件描述符很多,效率也不会受到影响。
没有数量限制:文件描述符无上限
表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。 - epoll的两种工作方式:
1.水平触发(LT)2.边缘触发(ET)
LT模式:若就绪的事件一次没有处理完要做的事件,就会一直去处理。即就会将没有处理完的事件继续放回到就绪队列之中(即那个内核中的链表),一直进行处理。
ET模式:就绪的事件只能处理一次,若没有处理完会在下次的其它事件就绪时再进行处理。而若以后再也没有就绪的事件,那么剩余的那部分数据也会随之而丢失。
由此可见:ET模式的效率比LT模式的效率要高很多。只是如果使用ET模式,就要保证每次进行数据处理时,要将其处理完,不能造成数据丢失,这样对编写代码的人要求就比较高。
epoll读到一半又有新事件来了怎么办?
避免在主进程epoll再次监听到同一个可读事件,可以把对应的描述符设置为EPOLL_ONESHOT,效果是监听到一次事件后就将对应的描述符从监听集合中移除,也就不会再被追踪到。读完之后可以再把对应的描述符重新手动加上。
4.Sockrt
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
发送接收方式
1、异步
报文发送和接收是分开的,相互独立的,互不影响。这种方式又分两种情况:
(1)异步双工:接收和发送在同一个程序中,由两个不同的子进程分别负责发送和接收
(2)异步单工:接收和发送是用两个不同的程序来完成。
2、同步
报文发送和接收是同步进行,既报文发送后等待接收返回报文。
同步方式一般需要考虑超时问题,即报文发出去后不能无限等待,需要设定超时时间,超过该时间发送方不再等待读返回报文,直接通知超时返回。
在长连接中一般是没有条件能够判断读写什么时候结束,所以必须要加长度报文头。读函数先是读取报文头的长度,再根据这个长度去读相应长度的报文。
5.调试程序(gdb命令)
GDB调试:gdb调试的是可执行文件,在gcc编译时加入 -g ,告诉gcc在编译时加入调试信息,这样gdb才能调试这个被编译的文件
gcc -g tesst.c -o test
GDB命令格式:
- quit:退出gdb,结束调试
- list:查看程序源代码
(1) list 5,10:显示5到10行的代码
(2)list test.c:5, 10: 显示源文件5到10行的代码,在调试多个文件时使用
(3) list get_sum: 显示get_sum函数周围的代码
(4)list test,c get_sum: 显示源文件get_sum函数周围的代码,在调试多个文件时使用 - reverse-search:字符串用来从当前行向前查找第一个匹配的字符串
- run:程序开始执行
- help list/all:查看帮助信息
- 条件断点:break if 条件 以条件表达式设置断点
- break:设置断点
(1)break 7:在第七行设置断点
(2)break get_sum:以函数名设置断点
(3)break 行号或者函数名 if 条件:以条件表达式设置断点 - watch 条件表达式:条件表达式发生改变时程序就会停下来
- next:继续执行下一条语句 ,会把函数当作一条语句执行
- step:继续执行下一条语句,会跟踪进入函数,一次一条的执行函数内的代码
- 多进程下如何调试:用set follow-fork-mode child 调试子进程
或者set follow-fork-mode parent 调试父进程
6.查看IP、GPU、显卡
- 查看ip:输入ifconfig -a,然后回车
- 查看显卡信息: lspci | grep -i vga
- 查看GPU使用情况:nvidia-smi
7.查看cpu、内存、磁盘(IO)使用率(top命令)
- 查看内存:free (总内存、使用、空闲)
- free -g # 以 G 为单位显示内存使用状况
- 查看磁盘使用率:df
- io状态查询:iostat -d -k 2
参数 -d 表示,显示设备(磁盘)使用状态;-k某些使用block为单位的列强制使用Kilobytes为单位;2表示,数据显示每隔2秒刷新一次。
8.查看进程状态(PS命令)
用来查看当前运行的进程状态,一次性查看,如果需要动态连续结果使用 top
- 进程的状态
运行(正在运行或在运行队列中等待)
中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号)
不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生)
僵死(进程已终止, 但进程描述符存在, 直到父进程调用wait4()系统调用后释放)
停止(进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运行) - 工具标识进程的5种状态码:
D 不可中断 uninterruptible sleep (usually IO)
R 运行 runnable (on run queue)
S 中断 sleeping
T 停止 traced or stopped
Z 僵死 a defunct (”zombie”) process - 命令参数:
-A 显示所有进程
-a 显示现行终端机下的所有程序,包括其他用户的程序
c 显示进程真实名称
e 显示环境变量
f 显示进程间的关系
r 显示当前终端运行的进程
-aux 显示所有包含其它使用的进程
1.显示当前所有进程环境变量及进程间关系
ps -ef
2.显示当前所有进程
ps -A
3.与grep联用查找某进程
ps -aux | grep apache
4.找出与 cron 与 syslog 这两个服务有关的 PID 号码
ps aux | grep ‘(cron|syslog)’
9.grep命令(文本搜索)
强大的文本搜索命令
//参数
-A n --after-context显示匹配字符后n行
-B n --before-context显示匹配字符前n行
-C n --context 显示匹配字符前后n行
-c --count 计算符合样式的列数
-i 忽略大小写
-l 只列出文件内容符合指定的样式的文件名称
-f 从文件中读取关键词
-n 显示匹配内容的所在文件中行数
-R 递归查找文件夹
//grep 的规则表达式:
^ #锚定行的开始 如:’^grep’匹配所有以grep开头的行。
$ #锚定行的结束,如:‘grep$‘匹配所有以grep结尾的行。
. 匹配一个非换行符的字符 如:‘gr.p’匹配gr后接一个任意字符,然后是p。
[] #匹配一个指定范围内的字符,如’[Gg]rep’匹配Grep和grep。
[^] #匹配一个不在指定范围内的字符,如:’[^A-FH-Z]rep’匹配不包含A-R和T-Z的一个字母开头,紧跟rep的行。
(…) #标记匹配字符,如’(love)’,love被标记为1。
< #锚定单词的开始,如:’<grep’匹配包含以grep开头的单词的行。
> #锚定单词的结束,如’grep>'匹配包含以grep结尾的单词的行。
x{m} #重复字符x,m次,如:'0{5}'匹配包含5个o的行。
x{m,} #重复字符x,至少m次,如:'o{5,}'匹配至少有5个o的行。
x{m,n} #重复字符x,至少m次,不多于n次,如:'o{5,10}'匹配5–10个o的行。
w #匹配文字和数字字符,也就是[A-Za-z0-9],如:'Gw*p’匹配以G后跟零个或多个文字或数字字符,然后是p。
W #w的反置形式,匹配一个或多个非单词字符,如点号句号等。
b #单词锁定符,如: 'bgrepb’只匹配grep。
//查找指定进程
ps -ef | grep svn
//查找指定进程个数
ps -ef | grep svn -c
//从文件中读取关键词
cat test1.txt | grep -f key.log
//显示包含 ed 或者 at 字符的内容行
grep -E ‘ed|at’ test.txt
10.搜索文件(find)
find [path][options][expression]path
11.linux更改用户权限
chmod +/-rwx 文件名|目录名
六、数据库
1.平衡二叉树、B树和B+树是什么,区别
- 二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”;
- 平衡二叉树(AVL树)在符合二叉查找树(左子树的键值小于根的键值,右子树的键值大于根的键值)的条件下,还满足任何节点的两个子树的高度最大差为1;
- B树:就是为了存储设备或者磁盘设计的一种平衡查找树
1)树中的每个节点最多有m个孩子
2)除了根节点和叶子节点外,其他节点最少含有m/2(取上限)个孩子
3)若根节点不是叶子节点,则根节点最少含有两个孩子
4)所以叶子节点都在同一层,叶子节点不包含任何关键字信息
B树的插入
1)若B树中已存在需要插入的键值时,用新的键值替换旧值;
2)若B树中不存在这个值,则在叶子节点进行插入操作; - B+树
B树的一种变形,它把数据都存储在叶子节点,内部只存关键字(其中叶子节点的最小值作为索引)和孩子指针,简化了内部节点。
B+树插入
B+树插入:
1)若为空树直接插入
2)对于叶子结点:根据key找到叶子结点,对叶子结点进行插入操作。插入后如果当前叶子结点的key值数b不大于m-1,则插入结束
3)对于索引结点:如果当前结点的key个数小于等于m-1,插入结束。
3.区别
为什么B+树比B树更适合做系统的数据库索引和文件索引
1)B+树的磁盘读写代价更低
因为B+树内部结点没有指向关键字具体信息的指针,内部结点相对B树小
2)B+树的查询更加稳定
因为非终端结点并不是指向文件内容的结点,仅仅是作为叶子结点的关键字索引,因此所有的关键字查询都会走一条从根节点到叶子结点的路径。即s所有关键字查询的长度是一样的,查询效率稳定。
既然Hash比B+树更快,为什么MySQL用B+树来存储索引呢?
MySQL中存储索引用到的数据结构是B+树,B+树的查询时间跟树的高度有关,是log(n),如果用hash存储,那么查询时间是O(1)。
采用Hash来存储确实要更快,但是采用B+树来存储索引的原因主要有以下两点:
一、从内存角度上说,数据库中的索引一般是在磁盘上,数据量大的情况可能无法一次性装入内存,B+树的设计可以允许数据分批加载。
二、从业务场景上说,如果只选择一个数据那确实是hash更快,但是数据库中经常会选中多条,这时候由于B+树索引有序,并且又有链表相连,它的查询效率比hash就快很多了
2.数据库并发带来的问题(脏读、幻读、丢弃更改、不可重复读)
- 脏读:读脏数据指在不同的事务下,当前事务可以读到另外事务未提交的数据。例如:T1修改一个数据但未提交,T2随后读取这个数据。如果T1撤销了这次修改,那么T2读取的数据是脏数据。
- 幻读:幻读本质上也属于不可重复读的情况,T1读取某个范围的数据,T2在这个范围内插入新的数据,T1再次读取这个范围的数据,此时读取的结果和第一次读取的结果不同。
- 丢弃修改:指一个事务的更新操作被另外一个事务的更新操作替换。例如:T1和 T2两个事务都对一个数据进行修改,T1先修改并提交生效,T2随后修改,T2的修改覆盖了T1的修改。
- 不可重复读:在这一事务还未结束前,另一事务也访问了该同一数据集合并做了修改,由于第二个事务的修改,第一次事务的两次读取的数据可能不一致。
3.隔离级别
- 未提交读:事务中的修改,即使没有提交,对其它事务也是可见的。(可能导致脏读、幻读和不可重复读)
- 提交读:一个事务只能读取已经提交的事务所做的修改。(可以阻止脏读)
- 可重复读:保证在同一个事务中多次读取同一数据的结果是一样的。(可以阻止脏读、不可重复度)
- 可串行化:强制事务串行执行,这样多个事务互不干扰,不会出现并发一致性问题。该隔离级别需要加锁加粗样式实现,因为要使用加锁机制保证同一时间只有一个事务执行,也就是保证事务串行执行。(可以阻止脏读、幻读和不可重复读)
4.什么是索引,作用、优点,有哪些
- 索引是数据库管理系统中一个排序的数据结构索引就相当于目录,实现通常使用B树及其变种B+树。
- 优点
可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
缺点
创建索引和维护索引要耗费时间,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
索引需要占物理空间 - 主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。
SELECT 查询列表 7
FROM 表 1 1
【连接类型】 JOIN 表 2 3
ON 连接条件 2
WHERE 筛选条件 4
GROUP BY 分组列表 5
HAVING 分组后的筛选条件 6
ORDER BY 排序的字段 8
LIMIT 起始的条目索引,条目数; 9
5.数据库分类
- 网络数据库
网络数据库是指把数据库技术引入到计算机网络系统中,借助于网络技术将存储于数据库中的大量信息及时发布出去,而计算机网络借助于成熟的数据库技术对网络中的各种数据进行有效管理,并实现用户与网络中的数据库进行实时动态数据交互。 - 层级数据库
层次结构模型实质上是一种有根节点的定向有序树(在数学中“树”被定义为一个无回的连通图)。 - 关系数据库
关系数据库,是建立在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数据库中的数据
6.悲观锁、乐观锁
- 悲观锁:就是一种悲观心态的锁,每次访问数据时都会锁定数据
- 乐观锁:一种乐观心态的锁,每次访问数据时并不锁定数据,期待数据并没作修改,如果数据没被修改则作具体的业务。
- 适用场景:
1.响应速度:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁
2.冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率,如果冲突频率大,乐观锁会需要多次重试才能成功,代价比较大
3.重试代价:如果重试代价大,建议采用悲观锁
7.请你说一下数据库事务、主键与外键的区别?
数据库的事务:事务即用户定义的一个数据库操作序列,这些操作要么全做要全不做,是一 个不可分割的工作单位,它具有四个特性,ACID,原子性,一致性,隔离性,持续性
主键和外键的区别:
- 主键是能确定一条记录的唯一标识,比如,一条记录包括身份正号,姓名,年龄。 身份证号是唯一能确定你这个人的,其他都可能有重复,所以,身份证号是主键。
- 外键用于与另一张表的关联。是能确定另一张表记录的字段,用于保持数据的一致性。
8.数据库如何优化
- 调整数据结构的设计,对于经常访问的数据库表建立索引
- 调整 SQL 语句, 可以使用一些语句优化器、行锁管理器(来调整优化 SQL 语句。 减少数据访问
比如:不要使用BY RAND()命令,尽量避免SELECT *命令 - 调整服务器内存分配。是在运行过程中优化配置的,数据库管理员可以根据数据库运行状况调整数据库系统全局区(SGA 区)的数据缓冲区、日志缓冲区和共享池的大小;还可以调整程序全局区(PGA 区)的大小。
- 调整硬盘I/O,DBA 可以将组成同一个表空间的数据文件放在不同的硬盘上,做到硬 盘之间I/O负载均衡。
9.索引失效
- 如果条件中有or,即使其中有条件带索引也不会使用 (这也是为什么尽量少用or的原因)
- like查询是以%开头
- 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
- 对于多列索引,不是使用的第一部分,则不会使用索引
10.联合索引
联合索引是什么
对多个字段同时建立的索引(有顺序,ABC,ACB是完全不同的两种联合索引。)
为什么要使用联合索引
- 减少开销。
建一个联合索引(col1,col2,col3),实际相当于建了(col1),(col1,col2),(col1,col2,col3)三个索引。每多一个索引,都会增加写操作的开销和磁盘空间的开销。对于大量数据的表,使用联合索引会大大的减少开销! - 覆盖索引
在真正的实际应用中,覆盖索引是主要的提升性能的优化手段之一。 - 效率高。
索引列越多,通过索引筛选出的数据越少。有1000W条数据的表,有如下sql:select from table where col1=1 and col2=2 and col3=3,假设假设每个条件可以筛选出10%的数据,如果只有单值索引,那么通过该索引能筛选出1000W10%=100w条数据,然后再回表从100w条数据中找到符合col2=2 and col3= 3的数据,然后再排序,再分页;如果是联合索引,通过索引筛选出1000w10% 10% *10%=1w,效率提升可想而知
11.SQL中有哪些索引
- 普通索引:仅加速查询
- 唯一索引:加速查询 + 列值唯一(可以有null)
- 主键索引:加速查询 + 列值唯一(不可以有null)+ 表中只有一个
- 组合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
- 全文索引:对文本的内容进行分词,进行搜索
- 索引合并:使用多个单列索引组合搜索
- 覆盖索引:select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖
- 聚簇索引:表数据是和主键一起存储的,主键索引的叶结点存储行数据(包含了主键值),二级索引的叶结点存储行的主键值。使用的是B+树作为索引的存储结构,非叶子节点都是索引关键字,但非叶子节点中的关键字中不存储对应记录的具体内容或内容地址。叶子节点上的数据是主键与具体记录(数据内容)
七、设计模式
常见C++设计模式面试题和场景题
设计模式有 6 大设计原则:
单一职责原则:就一个类而言,应该仅有一个引起它变化的原因。
开放封闭原则:软件实体可以扩展,但是不可修改。即面对需求,对程序的改动可以通过增加代码来完成,但是不能改动现有的代码。
里氏代换原则:一个软件实体如果使用的是一个基类,那么一定适用于其派生类。即在软件中,把基类替换成派生类,程序的行为没有变化。
依赖倒转原则:抽象不应该依赖细节,细节应该依赖抽象。即针对接口编程,不要对实现编程。
迪米特原则:如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某个方法的话,可以通过第三个类转发这个调用。
接口隔离原则:每个接口中不存在派生类用不到却必须实现的方法,如果不然,就要将接口拆分,使用多个隔离的接口。
设计模式分为三类:
创造型模式:单例模式、工厂模式、建造者模式、原型模式
结构型模式:适配器模式、桥接模式、外观模式、组合模式、装饰模式、享元模式、代理模式
行为型模式:责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式
1.单例模式
单例模式:单例模式只允许创建一个活动的对象(实例),提供了对唯一实例的受控访问。
单例实现原理:将能够创建对象的函数都设置为private,通过静态成员返回一个实例。有两种方式,一个是懒汉式,一个是饿汉式。懒汉式需要考虑加锁。
懒汉模式:直到第一次用到类的实例时才去实例化。存在的问题:每次判断实例对象是否为空,都要被锁定,如果是多线程的话,就会造成大量线程阻塞。
饿汉模式:类定义的时候就实例化。
应用场景:
- 表示文件系统的类,一个操作系统一定是只有一个文件系统,因此文件系统的类的实例有且仅有一个。
- 打印机打印程序的实例,一台计算机可以连接好几台打印机,但是计算机上的打印程序只有一个,就可以通过单例模式来避免两个打印作业同时输出到打印机。
实现方式:
单例模式可以通过全局或者静态变量的形式实现,这样比较简单,但是这样会影响封装性,难以保证别的代码不会对全局变量造成影响。
- 默认的构造函数、拷贝构造函数、赋值构造函数声明为私有的,这样禁止在类的外部创建该对象;
- 全局访问点也要定义成 静态类型的成员函数,没有参数,返回该类的指针类型。因为使用实例化对象的时候是通过类直接调用该函数,并不是先创建一个该类的对象,通过对象调用。
八、其他问题
1.为什么SSD不能当做内存用?
- 速度差异
现在ssd的读写速度可以达到三、四GB/s,当前内存基本上能到20GB - 30GB 左右。即使SSD的速度很快,但和内存相比还是有一个数量级的差异。 - 访问内存与访问硬盘的区别
内存的寻址粒度是byte级别的,也就是说每个字节都有它的内存地址,CPU可以直接通过这个地址获取到相应的内容。
但对于SSD来说就不是这样了,SSD是以块的粒度来管理数据的,
CPU没有办法直接访问文件中某个特定的字节。正是因为CPU无法直接按照字节粒度去访问SSD,因此CPU无法脱离内存直接在SSD中运行你写的程序。
2.对上百万数量的手机号码进行排序怎么做?
可以使用快排或者归并,时间复杂度为O(nlogn)。因为数据量很大,先要把一部分数据读入到数组内,我们可以先排序一部分数据,然后把这部分数据写入文件中,再接着排一部分数据,又写入另一个文件中。然后就可以合并。两个文件没办法同时读入内存,那我们就每一百行每一百行读,然后两个数组进行归并,成功归并的元素立马写入新的文件中,这
3. 从百万数据中去掉重复的数据
分组读入,,将百万数据分成k组,写入k个文件中,对每个文件分别去重,设置k个文件,然后将所有数据哈希映射至文件中,通过哈希映射,可以保证相同的数据只会写入固定的文件中。最后再对每个文件进行去重。就完成整体数据去重了。
4.给定一个数组,求取第k大的值。
使用堆,
或者快排,基准元素的下标我们知道了,基准元素是第几大我们就清楚了啊!如果基准元素的下标为Index,时间O(N),空间O(1)
while (index == k){
if (index > k)
抛弃index右边所有的元素,对左边的元素继续排序
else if (index < k)
抛弃index左边所有的元素,对右边的元素继续排序
}
20. 某一地区普遍反映卡顿
1、数据库问题,压力过大,如慢查询过多导致,某个查询吃完了内存,数据库并发量大 都有可能
2、web服务器CPU高,很多线程被锁,很多线程在等待IO或者其他服务响应
3、网络宽带原因
21.访问网址慢
- 本地网络原因,比如网络宽度被占用
- 网站服务器原因,可以通过 ping 命令查看链接到服务器的时间和丢包等情况(一个速度好的机房,首先丢包率不能超过 1%,其次 ping 值要小,最后是 ping 值要稳定,如最大和最小差值过大说明路由不稳定。
- 空间不稳定原因,表现为网速时快时慢,找网络空间商,有些地方慢有些地方快可能是网络线路问题,比如电信用户访问联通服务器。
- 网址本身设计原因,比如大尺寸图片和flash过多
22.视频直播卡顿
最后
以上就是幽默鱼为你收集整理的软开面试-C++一、计算机基础二、C++基础三、数据结构和算法四、操作系统五、linux六、数据库七、设计模式八、其他问题的全部内容,希望文章能够帮你解决软开面试-C++一、计算机基础二、C++基础三、数据结构和算法四、操作系统五、linux六、数据库七、设计模式八、其他问题所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复