【面经】八股面经

目录

操作系统

计算机网络

数据库

数据结构与算法

C++

Golang

项目细节和场景设计

操作系统

1.线程独有的资源是什么

  • 栈空间,CPU的使用权

2.进程和线程的区别

  • 一个进程里包含多个线程
  • 进程是操作系统分配资源的基本单位,线程是操作系统执行任务的基本单位
  • 进程的通信方式是:
    • 共享内存:这块内存由多个进程共享
    • 管道:半双工通道,是单向的
    • 消息队列:消息的链表,有标识符标记存在内核里面
    • 套接字:socket,可实现不同机器、不同进程之间的通信
    • 信号:通知进程,系统中发生了某种预先规定好的事件
  • 线程的通信方式:
    • 互斥量(即锁)
    • 临界区:通过对多线程的串行化来访问公共资源,速度快
    • 信号量:可视作有buffer的互斥锁
    • 事件:针对具有后继任务需要操作的的事件设计,用通知的方式来保持多线程同步

3.同一进程下的线程共享哪些资源

  • 数据空间
  • 静态存储区

4.进程切换开销大是因为进程得去切换环境,例如全局变量、打开的文件

5.进程间通信有什么方式,详细展开说怎么通信

  • 共享内存
  • 管道
  • 信号
  • 消息队列
  • socket套接字

6.共享内存是并发安全的吗,如何加锁

7.管道两端可以互传数据吗

  • 不可以,管道是半双工的,即是单向的

8.浏览器是一个多进程服务

  • 每开一个标签页,其实就是多开一个进程
  • 设计的目的是避免某一页面出问题影响到其他页面

9.用户态和内核态

  • Linux进程的虚拟内存分为用户空间(前3GB)和内核空间(最后1GB)
  • 常规操作默认用用户态,使用到内核空间的必须要切换为内核态操作
  • 从用户态切换到内核态需要进行中断操作,处理完后还需要恢复操作

10.虚拟内存

  • 虚拟内存简单来说就是把外存当作内存来使用,便于缓解物理内存压力的不足
  • 实现内存抽象,每个进程都以为自己拥有全部内存,操作虚拟地址的操作都会被转换为操作物理地址

11.多线程访问同一资源

  • 需要上锁

12.epoll和select

  • epoll
    • 文件描述符FD上限是最大可打开文件数,参考值20w
    • 水平触发和边缘触发(只会通知一次)
    • 通知进程时会直接返回就绪事件,不用去轮询(底层原理是内核里维护一个红黑树和双向链表)
    • 只在注册新事件的时候整体拷贝一遍,开销小
    • 只能在linux上使用,使用逻辑比select复杂
  • select
    • FD上限是1024个,可以在linux内核中修改
    • 水平触发,如果程序没有完成I/O操作,那么这个FD还是会被select作为通知
    • 通知进程时只会告知有事件发生,具体地址还需要进程去轮询
    • 每次改变内核中的fd集合时,都需要整体拷贝一遍,开销巨大

13.在4G物理内存的机器上,申请8G内存

  • 32位机器上,申请失败
    • 虚拟地址空间:内核占1G,用户占3G,实际最大只能申请3G
  • 64位机器上,申请成功,但是实际读写该内存超过时会触发OOM
    • 虚拟地址空间:内核128T,用户128T

14.零拷贝

  • 计算机在执行IO操作时,CPU不需要从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间
  • 把数据从磁盘缓冲区搬运到内核缓冲区这个工作交给DMA控制器去做,CPU可以去处理其他事务
    • DMA处理完成后,CPU再去把数据从内核拷到用户空间
    • 现在的每个I/O设备基本都有自己的DMA控制器
  • kafka就是利用了零拷贝,I/O吞吐量增大,处理海量数据效率才能这么高

计算机网络

1.TCP和IP

  • TCP在传输层,IP在网络层
  • TCP里包含IP包

2.TCP报文头里包含以下内容

  • 源端口号
  • 目的端口号
  • 序列号:占4字节,表示当前报文的序列号,用于保证传输的有序性
  • 校验和
  • 窗口:占2字节,表示接收方所能处理的数据量
  • 标识位:ACK、SYN、FIN等

3.GET和POST请求的区别

  • GET请求
    • 参数通过url明文传递,只能用ASCII码,有长度限制,这个长度是浏览器限制的,实际上http别未有这种限制
    • 浏览器会主动缓存(包括传的参数)
    • 按RFC规范来是只读操作,是安全幂等的(幂等指的是多次执行相同操作,结果都是相同的)
  • POST请求
    • 参数放在request body里,有多种编码方式(get只能url编码)
    • 不会被浏览器主动缓存
    • 未指定资源的存放位置,由服务器决定
    • 按RFC规范来说是新增操作,不是安全幂等的(实际上开发者可以不遵循规范,让get也去修改服务器数据)
  • PUT请求(其他和POST相似)
    • 参数放在request body里
    • 指定了资源的存放位置
    • 按RFC规范来说是更新操作,是安全幂等的

4.跨域问题是什么,怎么解决跨域问题的,不同服务之间调用会有跨域问题么

  • 同源政策保证该网站的资源只能由同源请求执行(域名和端口名一样),是为了防止别的网站对该网站进行攻击,是浏览器采取的安全策略
  • 实现方法是新增一组HTTP首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源
  • 对那些可能对服务器数据产生副作用的HTTP请求方法(特别是GET以外的HTTP请求),浏览器必须首先使用OPTIONS方法发起一个预检请求,从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的HTTP请求
  • 不同服务如果部署在不同域名端口上,也会有跨域问题

5.说一下知道的状态码,200是什么,重定向是什么,它的过程是什么,如果重定向的话那么网页是如何获取客户端信息的呢?

  • 状态码
    • 1开头表示信息
    • 2开头表示成功(200-请求成功)
    • 3开头表示发生了重定向(301-网页被永久转移到其他URL)
    • 4开头表示客户端错误(404-请求的网页不存在)
    • 5开头表示服务端错误(500-内部服务器错误)
  • 重定向是浏览器发送请求到s1之后,s1需要访问s2,但并未在服务器内直接访问,而是由服务器自动向浏览器发送一个响应,浏览器再自动提交一个新的请求,这个请求就是对s2的请求
  • s2获取客户端信息还通过第二次浏览器发起的请求获得的
  • 对s2的请求是无法获得之前对s1提交的参数的,所以重定向也用来保证表单不重复提交

6.TCP和UDP区别

  • TCP面向连接提供可靠服务;UDP是无连接的,尽最大努力交付,但不保证可靠交付,UDP 具有较好的实时性
  • TCP连接是1对1,UDP无连接可以1对多
  • TCP首部开销20字节,UDP首部开销8字节
  • TCP把数据看成字节流,UDP把数据看成报文包

7.为什么TCP更可靠

  • 校验和
  • 序列号
  • 超时重传机制
  • 确认应答ACK机制
  • 流量控制
  • 拥塞控制

8.TCP三次握手,如果改成两次握手会怎么样,那如果我就按两次握手算是建立连接了,那客户端向服务端发数据会怎么样

  • 1.会产生历史连接,例如client发了一个seq1的连接请求,然后client重启后重新发了一个seq2连接请求,此时client想要的结果是建立seq2的连接忽略seq1的连接,如果是两次握手的话会存在两个连接,其中历史连接会浪费资源
  • 2.没有同步双方的初始序列号,比如client可能收不到serve的初始序列号,导致后面的传输序列号乱序,不能保证可靠性
  • 3.产生冗余连接浪费资源,比如client发出的某个连接请求经网络延迟很久以后才到serve端,这时serve端回一个SYN+ACK后就默认连接建立的话,client此时并不需要建立连接直接忽略掉,那么serve端会一直等待数据传输导致资源浪费

9.HTTP是基于TCP的吗,HTTPS七次握手,七次握手中什么时候是对称加密什么时候是非对称加密,最后握手结束之后采用什么加密

10.将url输入到浏览器中会发生什么?

  • 域名解析
  • RAP解析将ip地址解析为物理地址
  • TCP三次握手
  • 服务端返回响应
  • 浏览器将响应渲染成页面

11.单点登录:一次登录后,访问其他页面就不再需要登录

  • 用token或者共享cookie实现

12.http版本区别

  • http1.0:无状态无连接
  • http1.1:长链接默认把Connection设为:keep-alive、增加host域、支持请求管道化
  • http2.0:二进制分帧、头部压缩、服务器推送

13.cookie、session和token

  • cookie存在客户端,本地易被篡改
  • session存在服务端,服务器要为每个用户保留session信息,连接用户过多会造成服务器内存压力过大
  • token:客户端后续每次请求中,浏览器会把token作为http header发送给服务器,使用jwt(json web token)加密token是现在主流形式
    • 这种方式的特点就是客户端的token中自己保留有大量信息,服务器没有存储这些信息,而只负责验证,不必进行数据库查询,执行效率大大提高。

14.https的七次握手

  • 正常的三次握手
  • c->s:TLS版本号+加密套件列表+希望采取的TLS选项
  • s->c:加密套件+TLS选项+自己证书+公钥
  • c->s:自己证书+对称秘钥(用公钥加密)
  • s->c:FINISH

15.实际抓包中,会发现不是四次挥手而是三次挥手

  • 因为在某些特定情形下C端不会处在持续传数据的情况
  • 所以第二次挥手和第三次挥手合并了
  • 这样可以极大地减少断开连接的开销,增加速度

16.close_wait和time_wait

  • close_wait是接受端第一次收到FIN报文后,进入的状态,传输数据完成后就结束
  • time_wait是发起端在等待接受端结束close_wait后收到FIN报文,进入的状态,等待2MSL时间之后就结束
    • 2MSL默认4分钟

17.http请求和响应报文格式

  • 第一行
    • 请求是请求行:方法+url+版本,例如 GET /index/htm HTTP/1.1
    • 响应是响应行:版本+状态码+短语,例如 HTTP/1.1 404 Not Found
  • 中间有n行首部行:字段名+值,例如host:www.baidu.com(表示请求资源存在哪个域名上)
  • 最后有个请求体(post、put这类常用)

18.https如何保证服务端的证书是可信的

  • 客户端将证书的公钥、持有者信息、有效时间等打包,然后进行Hash计算得到一个值h1
  • 客户端对证书的签名用公钥进行解密,得到h2
  • 比较h1和h2是否相等就可以验证证书是否合法了

19.https出现的性能下降

  • TLS额外的四次握手(RSA需要2RTT)
    • 非对称加密算法从RSA改成ECDHE,这样只需要1RTT
      • 该算法由于支持False Start抢跑,客户端可以在TLS协议的第3次握手后,第4次握手前,发送加密的应用数据
    • 升级到TLS1.3,也可以优化成1RTT
      • 1.3客户端在第一次握手的时候就传支持的椭圆曲线,以及这些椭圆曲线对应的公钥,这样服务端收到后,选定一个参数并带上服务端这边的公钥。经过1个 RTT,双方手上已经有生成会话密钥的材料
    • 服务器应该选用ECDSA证书而不是RSA证书,这样可以减少密钥长度
    • 采用会话重用技术
  • 握手后的对称加密传输
    • 主流的AES等算法已进行优化,且CPU厂商会针对其做硬件上的优化,这方面开销已经极大减少

20.TCP拥塞控制

  • 发送方维护一个拥塞窗口作为发送的大小
    • 出现拥塞的话要减少这个窗口,否则就增大
  • 有一个慢开始门限,低于用慢开始算法,高于用拥塞避免算法
    • 慢开始:1,2,4,8….翻倍
    • 拥塞避免:1,2,3,4,5,6….+1

数据库

1.mysql的引擎有三种

  • InnoDB:mysql默认引擎,用于事务,默认行锁,适合大规模数据,一张表的qps根据实验应该是8000左右
  • MyISAM:不支持事务,适合小规模,默认表锁,mysql之前默认的老引擎
  • Memory:数据放到内存中

2.b+树

  • b+树的深度浅,每次读一个节点只要一次IO,减少了IO次数
  • b+树的IO次数少,b+树的节点可以存多个关键字,并且单个节点默认存16k与磁盘页对齐,这样一次IO就可以传输一个完整节点
  • b+支持范围查询和排序操作
  • 具体实现:
    • 非叶子节点存的是每个子树的最小主键id,这样就可以根据要查的主键id去判断往哪边的子树往下走
    • 叶子节点按主键id有序存放数据,走到某叶子节点后用二分查找来定位到具体的位置
    • 插入需要旋转平衡开销,因为如果索引节点和数据节点某个已满的话需要拆分,如果两个都满了,还需要再往上加一层索引

3.跳表

  • 跳表的深度深,同等高度下存的数据要比b+树少
  • 跳表可能出现跨页IO
  • 跳表只支持单个关键字的查找
  • 具体实现
    • 插入不需要旋转平衡开销,直接在底层节点插入即可,而是否需要把这个数据也加到索引里,依靠随机函数即可
      • 因为理论上为了达到二分的效果,每一层的结点数需要是下一层结点数的二分之一,所以底层新增一个节点时,有0.5的概率需要加到第二层索引,有0.5*0.5的概率加到第三层索引

4.索引选型场景问题

  • mysql用b+树实现索引而不是跳表的原因
    • b+树的IO次数少,一是因为节点大小和磁盘页大小对齐一次IO可以传输一个节点,二是b+树深度浅
    • Innodb引擎出现的时候,跳表还没得到广泛应用
  • redis用跳表实现有序集合zset而不是b+树的原因
    • redis在内存中运行,所以层高,IO开销大就不再是劣势
    • 跳表的插入效率高,因为不用像b+树一样需要去旋转平衡
  • 跳表写性能好:插入不需要去旋转平衡,b+树读性能好:层数低

5.Innodb的next-key lock机制解决幻读

  • 里面包含间隙锁和记录锁,间隙锁会锁上记录之间的间隙
  • 比如对5到10行的记录上next-key lock就是由(5, 10)之间的间隙锁和加在10上的记录锁组成,左开右闭(5,10],即5不加锁,10要加锁

6.数据库的锁底层是怎么实现的,类似间隙锁,xx锁这种

  • InnoDB并没有采用系统提供的锁机制,而是自己封装了操作系统提供的基本mutex互斥量和event信号量来实现锁
  • InnoDB行锁是通过给索引上的索引项加锁来实现的,而不是给表的行记录加锁,通过索引检索数据才使用行级锁,否则将使用表锁

7.Redis是串行但是仍然快的原因

  • Redis是单线程的,减去了CPU切换不同线程的开销,并且Redis是在内存里运行的
  • Redis的多路复用模型是用epoll实现的

8.Redis是基于键值对的非关系数据库,值的数据类型可以是

  • string
  • hash
  • list
  • set
  • zset(有序集合)
  • Bitmaps(位图)

9.跳表的原理是二分查找,所以快。查询、插入、删除的平均复杂度都是logn

  1. redis的哪种数据结构是有序的
  • zset

11.mysql的MVCC

  • 目的:为了实现事务读数据的时候不需要加锁
  • 实现:InnoDB中的MVCC实现是通过undolog日志实现的
    • 执行更新操作时,会把更新前的数据放入undolog里面,并通过隐藏的回滚指针roll_pointer指向
    • 其他的事务,如果需要查询该记录,就会去undolog里面找这个记录的最新历史版本
  • MVCC最大的好处是读不用加锁,实现读写不冲突,极大地增加了并发性同时保证了事务的隔离性

12.Innodb的隔离级别

  • 读未提交
  • 读提交RC:每次读都会更新快照,所有不能解决不可重复读和幻读问题
  • 可重复读RR:只在第一次读的时候生成快照,然后后面只有事务内更新了数据才会去更新快照
    • 快照读情况下,根据MVCC机制,用版本号、undolog的方法,可以解决幻读问题
    • 当前读情况下,手动加record lock(记录锁)和gap lock(间隙锁),即next-key-lock机制,可以解决幻读问题
      • 当前读的情况下只用MVCC不能解决幻读,要加间隙锁才行
  • 串行

13.InnoDB的行锁算法有以下3种

  • 记录锁record-lock:直接在记录上加锁
  • 间隙锁gap-lock:锁定一个范围,但不包括记录本身
  • next-key-lock:锁定一个范围和记录本身,解决幻读

14.聚簇索引和非聚簇索引,Innodb默认的是聚簇索引

  • 聚簇索引将索引和数据放一块,b+树叶子节点存的是:主键id+数据,查询快+更新代价大
  • 非聚簇索引将索引和数据分开放,b+树叶子节点存的是:主键id+数据地址(还需要去磁盘里找),查询慢+更新代价小

15.缓存雪崩,缓存击穿和缓存穿透

  • 缓存穿透:用户每次查的数据都不在缓存里面,都得去底层数据库查,导致缓存机制形同虚设
  • 缓存雪崩:同一时间大量的缓存数据过期,导致该时间的请求底层全打到数据库上
  • 缓存击穿:缓存里的某个key一直扛着大并发请求,这个key一旦过期,就会出现缓存击穿

16.mysql的最左匹配

  • 例子,t1,t2,t3建立了一个联合索引,那么实际上是建立了(t1,t2)、(t1,t2,t3)这两个索引
  • MySQL8.0之前一定是遵循最左前缀匹配的,即WHERE条件有t1==xx或者t1==xx and t2==xx时才能用到这个索引来加速检索
  • MySQL8.0之后出现了索引跳跃扫描技术,假如只查了t2==xx and t3==xx也能去用到联合索引,因为实际上底层会去执行
    1
    2
    3
    4
    xxx WHERE t1 == 1 and t2 == xx and t3 == xx;
    xxx WHERE t1 == 2 and t2 == xx and t3 == xx;
    .....
    直到扫描完t1字段所有的唯一值,最后将结果合并返回

17.redis是k-v结构,怎么实现复杂查询

  • 一般可以使用set的交并集来实现多表联查需求,但是这样其实就抛弃了redis的优势,一般还是不建议在redis里面进行复杂查询,应该存放直接能使用的值

18.mysql的主从复制
主从复制的基本原理:slave会从master读取binlog来进行数据同步

  • master将改变记录到二进制日志binlog,这些记录过程叫做二进制日志事件binlog events
  • slave将master的binlog events拷贝到中继日志relay log
  • slave重做中继日志中的事件,将改变应用到自己的数据库中。MySQL的复制是异步且串行化的

19.Redis实现数据持久化的方法

  • RDB:把当前的数据生成快照,保存到硬盘里,有手动触发和自动触发两种
  • AOF:用日志记录每次执行的写命令,重启时重新执行一遍AOF里的记录

20.数据库慢查询的原因

  • 没有用到索引
  • 内存不足
  • 查询的数据量过大

21.解决慢查询用explain检查sql是否有问题,关注以下字段

  • type:用来表示该sql底层是怎么扫描的,
    • all全表扫描
    • index全索引扫描
    • null表示不用去表里查可以直接得到结果
  • possible_keys:可能用到的索引列
  • key:实际用到的索引列
  • Extra:底层用的是哪种检索方式
    • Using where用where来处理结果
    • Using index condition表示用索引

22.乐观锁和悲观锁

  • 乐观锁:默认不发生并发冲突,只在提交时检查版本号
    • 有可能会出现两个事务读同一行后再写入的未知结果
  • 悲观锁:默认发送并发冲突,需要关闭mysql的自动提交,并在事务中用select…for update用排他锁锁住数据,其他事务需要在该事务提交后才能去写这条数据
    • 效率比较低
  • 并发冲突少时选择乐观锁,并发冲突多时选择悲观锁
    • 因为乐观锁出现冲突只能往上抛出,然后重试,而悲观锁可以避免冲突的发生

23.mysql如何管理脏页

  • 用buffer pool来做缓存加速
  • 更新数据的时候会把buffer pool里面的页更新,然后把该页标记为脏页
  • 后台线程再将脏页写入到磁盘里面
    • 用一个flush链表来存所有脏页,后台线程遍历这个链表就可以把脏页写入磁盘了
    • 触发:redo log或者buffer pool满了,数据库空闲或者关闭
    • 数据库性能波动,可能就是后台正在写脏页导致的

24.MySQL是如何处理全局搜索导致的LRU污染

  • 新增一个old区域
  • 当某页第一次被访问时,直接放入old区域
  • 后续访问的时间 - 第一次访问时间 > 一定时间,才能将该页放入LRU的热点young区域
    • 原理是:每次从页中取一条记录,即认为访问了一次该页。所以在全表扫描时,InnoDb会认为对该页短时间进行了多次访问
  • 并且新增了,只有young区域后1/4的页被访问才将其移到首部

25.一级索引和二级索引

  • 一级索引:叶子节点存的是数据,主键索引都是一级索引
  • 二级索引:叶子节点存的是主键,找到叶子节点后再用主键去访问对应数据

26.怎么加索引,什么时候要去加索引

  • alter table 表名 add 索引类型 列名
  • alter table 表名 drop xx

27.为什么myisam查询比innodb快

  • innodb寻址要映射到块,再到行,myisam记录的直接是文件的myisam,定位比innodb要快
  • innodb需要维护MVCC一致,需要额外开销
  • innodb要缓存数据块,所以其中还有块在缓存中换进换出的开销

28.Hive是怎么实现大数据查询的,mysql为什么不行

  • Hive用MapReduce去分布式查询
  • Hive是将结构化的文件映射成一张表,这个文件是在HDFS上的

数据结构与算法

1.哈希表的负载因子

  • 负载因子=已保存节点空间/哈希表空间,一般设为0.75

2.set和array的区别

  • 唯一性。set的底层是红黑树,但是保证唯一性用的是哈希表
    • 保证唯一性的底层实现是,先判断key得到的hashcode有一样的么,如果有,那么再调equals()去检查

3.如果发生哈希冲突应该怎么处理,哈希表的时间复杂度是多少
- 链表法,插入、查询、判断key值开销:unordered_map是O(1),map是O(logn)

4.二叉树插入元素的时间复杂度

  • O(logn),插入其实开销就和查找一样

5.动态数组删除元素的时间复杂度

  • O(n),因为无论是开辟新数组还是移动其他成员的位置,复杂度都是n

6.unordered和普通的数据结构有什么区别,比如unordered_map和map

  • 底层:unordered_map底层是hash_map,map底层是红黑树
    • hash_map是由vector和链表构成,每个vector的位置连一个链表,声明的时候就会自动初始化一个vector去占用空间(质数,不同编译器用不同的质数比如53)
  • 顺序:unordered_map是插入顺序,map是按key从小到大排列的
  • 插入、查询、判断key值开销:unordered_map是O(1),map是O(logn)

7.红黑树的特性

  • 每个节点或者是黑色,或者是红色。
  • 根节点是黑色
  • 每个叶子节点是黑色(注意:红黑树叶子节点是NULL)
  • 不能出现连续的黑色节点
  • 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

8.红黑树与AVL树比较,都是BST(二叉搜索树)

  • AVL是严格的平衡二叉搜索树,执行插入或删除,往往需要开销较大的旋转来保持平衡,但是高度较低
  • 红黑树是一种弱平衡二叉搜索树,只需确保没有一条路径比其他路径长两倍即可,所以旋转次数少,但是高度较高
  • 查询较多的情况用AVL树,
  • 插入和删除场景较多的情况用红黑树,map和set

9.vector与list比较

  • vector底层是数组,list底层是双向链表
  • vector支持下标访问,list不支持
  • vector是顺序内存,list不是
  • vector扩容才会申请内存,list每次插入都需要申请内存

10.dijkstra和spfa区别

  • dijkstra用堆,spfa用优先队列
  • dijkstra不能处理负权值,spfa可以处理
  • spfa的最坏复杂度和dijkstra相同,但是spfa可能会被卡

11.deque

  • 底层由多段等长的连续空间组成
  • 用一个数组map存放指向这些连续空间地址的指针
    • 和unordered_map底层类似,一个存指针指内存空间,一个存链表

12.希尔排序就是插入排序的改进版,也是不稳定的

  • 先划分子序列,注意这里划分的子序列是交错的
    • 按增量的方式进行子序列划分,将整个序列中下标值相差固定值的所有元素划分到同一个子序列中。比如,整个序列有9个元素,而增量为3,那么第0、3、6个元素属于第一个子序列,第1、4、7个元素属于第二个子序列,第2、5、8个元素属于最后一个子序列
  • 然后对每个子序列进行插入排序
  • 然后再按一个新的较小的增量进行划分,再重复以上步骤
  • 最后得到完整的排序

13.sort底层是快排

  • 元素少的话会去用插入排序
  • 用快排是因为快排在比较随机的分布上,表现较好

C++

1.C++的多态

  • 多态就是一个接口,多种实现方法,即动态绑定
  • 通过虚函数实现

2.什么是虚函数,原理是什么,怎么样使用虚函数

  • 类中存有一个虚函数表,实例化的对象拥有一个虚函数指针用来指向虚函数表中的某个位置
  • 虚函数在运行时通过虚函数表确定,是动态的;内联函数在编译时候进行代码嵌入,是静态的,所以如果把虚函数声明为内联,编译器仍然会当做普通虚函数处理
  • 虚函数的缺点是:一是虚函数表和虚函数指针会增大开销,二是在一个CPUcache里面,有了虚函数以后,编译器不会做一些很强大的优化
  • 对于继承自父类的子类,应该将父类析构函数设为虚函数

3.const关键字的作用和一般使用情景

  • 定义不会改变的常量,使编译器保护这些常量,防止代码无意的修改

4.static关键字的作用和一般使用情景

  • 类里的static函数,里面就只能调用static成员
  • static成员在类未进行第一次实例化时就已经占据了空间

5.了解构造函数和析构函数吗?它们俩可以是虚函数吗

  • 构造函数不能是虚函数,析构函数可以是虚函数
  • 一般析构函数设为虚函数是便于子类重载,避免使用父类指针指向子类对象时,去调用了父类的析构函数导致部分子类成员没能释放,造成内存泄漏

6.incline函数和宏的区别是什么

  • incline在编译时展开,宏在预编译时展开;
  • incline内联函数有类型检测、语法判断等功能,而宏没有,因为宏只是做简单的文本替换,这也导致要注意括号问题

7.说一下你了解的vector和map实现方式和功能

  • vector,动态数组,有扩容机制
  • unordered_map的底层是用vector+链表实现的

8.字节对齐

  • 原理:
    • 因为尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的。它一般会以2字节,4字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度
    • 假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作
    • 有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。
  • 对齐的准则:
    • 结构体变量的首地址能够被其对齐字节数大小所整除
    • 结构体每个成员相对结构体首地址的偏移都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足
    • 结构体的总大小为结构体对齐字节数大小的整数倍,如不满足,最后填充字节以满足

9.单例模式

  • 饿汉单例模式:系统启动的时候就利用static机制实例化了类
    1
    2
    3
    4
    5
    6
    Class A{
    static A single;
    static A* getClass(){
    return &single;
    }
    };
  • 懒汉单例模式:真正用到这个类时才去实例化
    1
    2
    3
    4
    5
    6
    7
    Class A{
    static A* single;
    static A* getClass(){
    if(single == nullptr) single = new A();
    return single;
    }
    };

10.i++其实分3步执行

  • 读取i的值,增加i的值,回写i的新值
  • 这3步每一步都是原子操作,但是组合在一起就不一定是原子操作了,所以i++线程不安全,++i线程安全
  • ++i比i++效率高得原因是,前者只用返回i+1的结果,而后者既要先保存i的结果还要保存i+1的结果

11.static关键字的作用

  • 静态函数(在函数返回类型前加static):作用域为声明语句所在的整个文件
  • 全局静态变量(在全局变量前加static):作用域为声明语句所在的整个文件
  • 局部静态变量(在局部变量前加static):作用域仍为局部
  • 总结:
    • 类的静态成员和静态函数:这个类的所有对象都共用一个,注意静态函数只能引用类中的静态成员
    • static与extern相反,static意味着该变量、函数在被声明的文件之外是不可见的,除非引入声明的文件作为头文件,但是这样的话会每一个引入该头文件的文件都会定义一个自己的变量造成内存浪费,所以一般用extern在头文件中声明才是正确的方式
    • 静态变量都放在内存的静态存储区内,在程序运行期间一直存在不会释放,所有在函数内定义的局部静态变量,会在第一次运行到该语句时就创建,第二次运行到该定义语句会自动跳过,直到程序终止才释放

12.堆和栈的区别

  • 堆会产生内存碎片,栈不会
  • 堆只能动态分配需要程序员手动分配和释放;栈由系统管理静态分配,也可以用alloca()动态分配,但都是由系统释放
  • 堆内存地址由低到高;栈内存地址相反
  • 堆为虚拟内存大小;栈默认为1M(linux下为10M)
  • 堆的分配效率低;栈的分配效率高,因为由系统调用,会有硬件层面的支持

13.new与malloc区别

  • new是C++用于内存分配的运算符,malloc是C用于内存分配的库函数
  • new会自动计算要多少字节的内存,malloc需要用户指定字节数
  • new会返回指定类型的指针,malloc返回的统一是void *,需要用户自己再转一次
  • new用delete释放,malloc用free释放

14.#define和typedefine

  • define在编译前预处理做字符串替换,typedefine在编译的时候才处理,相当于起别名

15.shared_ptr

  • shared_ptr和局部变量一样,离开作用域时会自己销毁
  • 每当一个shared_ptr被拷贝或当成值赋给其他变量时,内置计数器都会自动+1,当指向该对象的一个shared_ptr被销毁时,shared_ptr类的析构函数会对计数器-1,为0时就去销毁这个对象(调用这个对象的析构函数)来释放动态内存
  • weak_ptr指针不能直接访问对象的方法,要先将其转化成shared_ptr指针再访问
    • weak_ptr:用于解决shared_ptr互相指向形成循环,内存一直无法释放导致泄漏,它不会增加对象的引用计数

16.指针和引用的区别

  • 指针占4字节空间,引用只是一个别名不占空间
  • 指针可初始化为nullptr,引用必须初始化为一个已存在对象
  • 引用易引起内存泄漏,因为指向的对象可能在栈里被回收了

17.C++的编译过程分几个阶段

  • 预编译->编译->汇编->链接->执行
  • Linux下用C++编程的步骤
    • 用vim编写.cpp文件
    • 用g++编译得到.out文件
    • 用./xx.out执行

18.C++在linux下怎么断点调试

  • 使用gdb,对某行打上断点
  • 然后在生成二进制程序的时候, 加上 -g 选项

Golang

1.GMP模型中,如果本地队列已经没有g了,全局队列也为空会休息吗

  • 不会,因为还会去隔壁的本地队列里面拿g

2.channel的底层,如何用它来实现并发,如果channel满了会怎么办

  • 底层用hchan结构体实现,一个ringbuffer,一个发送队列,一个接受队列

3.Go语言的特点

  • 直接编译成二进制,没有虚拟化损失
  • 代码跨平台,但是编译出来的产物是不跨平台的
  • runtime直接编译进可执行文件里

4.Go中的扇入、扇出

  • 扇入:多个通道合并到一个通道,select关键字聚合多条通道分别服务
  • 扇出:一个通道分散到多个通道,用go关键字起多个协程

Linux

1.Linux下查找文件用什么命令

  • find “xx”表示在当前目录及其子目录查找xx

场景设计

1.秒杀系统最重要的是异步、削峰,通过拦截器、分布式架构和消息队列实现

2.缓存数据一致性如何保证

  • 一般而言都是先更新数据库,再去缓存里删老数据
  • 两种方案
    • 双写策略:修改完数据库后,马上再用set方法将新数据写入到缓存中
      • 缺点:写入数据库后,再写入到缓存中这一步可能会由于网络原因延迟,导致脏数据
        • 例如改了数据库a改成1然后再改成2,此时网络波动改成1的写缓存操作在改成2的写缓存操作之后才执行,这样就不一致了
      • 解决办法:设置过期时间,这样一段时间后到数据库里取更新缓存的数据仍然可以得到正确数据
    • 先写后删策略:修改完数据库后,马上把缓存中的数据去删掉
      • 缺点:删除因为网络波动延迟和删除操作失败仍然可能有脏数据
      • 解决办法:设置过期时间,MQ异步处理保证执行删除
    • 延迟双删策略:先删缓存,再写数据库,延迟n秒后再去删缓存
      • 缺点:这n秒里还是会有脏数据,因此仍然不是强一致性的
      • 解决办法:设置过期时间,MQ异步处理保证执行删除
  • 根据系统设计的思想:
    • 放入缓存的数据本就不应是实时性的、一致性要求很高的,所以缓存数据的时候要加上过期时间保证每天拿到最新的即可。
    • 遇到实时性、一致性要求高的数据,就应该直接查数据库,即时速度慢一些

3.rabbitmq如何保证消息不丢失,如果发送了多次相同的消息要怎么处理

  • 消息丢失有三种
    • 生产者发给mq途中丢失:开启confirm机制给每个消息分配一个id,生成者发送消息后mq收到返回ACK,没收到返回nack
    • mq因为断电等情况丢失:设置消息持久化和镜像集群
    • mq发给消费者途中丢失:开启AKC应答机制,即消费者收到后给mq回一个ACK
      • kafka的ack有以下三种策略:
      • ACK = 0 时 发送一次 不论下游是否接收
      • ACK = 1 时,等待下游接收成功即可
      • ACK = -1 时 ,需等待下游将消息同步给消息队列

4.如果让你设计一个应用层的协议你会考虑什么,怎么去识别包的长度,如果包里本身包含这个特殊字符怎么办
- 用socket封装实现,基于TCP去实现,考虑首部包含资源的位置、请求的方式、协议版本
- 1.首部加一个len字段 2.用特殊字符做分割,换一个少用的字符或者加转义符/

5.rabbitmq和kafka的优缺点

  • kafka有ack机制可以保证消息的顺序处理
  • rabbitmq有消息存活时间和延迟/预定消息
  • rabbitmq消息一旦消费成功就会删除,反之处理失败则放回;但kafka会一直保留消息,根据超时时间来删除,可以反复消费消息
  • kafka使用分区的结构,在横向扩展上更有优势