C++后端校招学习路线(学完必拿后端offer)

C++后端校招准备以及个人补充面试常考题

文章目录


前言

秋招面试结束了,我的专业是机械工程,有幸也能拿到互联网后端的offer,在此总结一下C++后端学习路线以及一些面经,当然前提是要系统学习相关知识。


提示:以下是个人经验,可供参考

一、学习方式

做C++后端必须会的知识点有如下几块:C++基础、操作系统、计算机网络、数据库、算法与数据结构、设计模式。
操作系统、计算机网络、数据库都要看书看视频、数据库还要自己练习一下,然后自己也要做相关的项目。当然面试不是全考,在此总结一下学习的途径。
1.面试基础入门(牛客网基础)
牛客网
2.提升版(拓跋阿秀面经,强烈推荐)
阿秀面经
还可以看看微信部门老哥总结的面经
腾讯面经
3.算法
其实算法刷力扣剑指offer和TOP100就够了,还要熟悉一下牛客网ACM模式的输入输出。

二、个人面经总结

但是有些问过但是不会的,个人在此总结一下。

1.什么是内核态和用户态,陷入内核态的方式?什么是中断?

内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。

用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。

为什么要有用户态和内核态?

由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 – 用户态和内核态。

用户态切换到内核态的3种方式

a. 系统调用

这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。

b. 异常

当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

c. 外围设备的中断

当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

什么是中断?

中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。

硬件中断(Hardware Interrupt)

  • 可屏蔽中断(maskable interrupt)。硬件中断的一类,可通过在中断屏蔽寄存器中设定位掩码来关闭。
  • 非可屏蔽中断(non-maskable interrupt,NMI)。硬件中断的一类,无法通过在中断屏蔽寄存器中设定位掩码来关闭。典型例子是时钟中断(一个硬件时钟以恒定频率—如50Hz—发出的中断)。
  • 处理器间中断(interprocessor interrupt)。一种特殊的硬件中断。由处理器发出,被其它处理器接收。仅见于多处理器系统,以便于处理器间通信或同步。
  • 伪中断(spurious interrupt)。一类不希望被产生的硬件中断。发生的原因有很多种,如中断线路上电气信号异常,或是中断请求设备本身有问题。

软件中断(Software Interrupt)

  • 软件中断。是一条CPU指令,用以自陷一个中断。由于软中断指令通常要运行一个切换CPU至内核态(Kernel Mode/Ring 0)的子例程,它常被用作实现系统调用(System call)。

2.如何解决缓存雪崩、缓存穿透?

缓存雪崩

缓存雪崩:由于缓存层承载着大量请求,有效保护了存储层,但是如果缓存层由于某些原因不能提供服务,于是所有的请求到达存储层,存储层的调用量会暴增,造成存储层级联宕机的情况。预防和解决缓存雪崩问题可以从以下几方面入手

(1)保证缓存层服务的高可用性,比如一主多从,Redis Sentine机制。

(2)依赖隔离组件为后端限流并降级,比如netflix的hystrix。关于限流、降级以及hystrix的技术设计可参考以下链接。

(3)项目资源隔离。避免某个项目的bug,影响了整个系统架构,有问题也局限在项目内部。

缓存穿透

缓存穿透指查询一个根本不存在的数据,缓存层和存储层都不命中。一般的处理逻辑是如果存储层都不命中的话,缓存层就没有对应的数据。但在高并发场景中大量的缓存穿透,请求直接落到存储层,稍微不慎后端系统就会被压垮。所以对于缓存穿透我们有以下方案来优化

1、缓存空对象

第一种方案就是缓存一个空对象。对于存储层都没有命中请求,我们默认返回一个业务上的对象。这样就可以抵挡大量重复的没有意义的请求,起到了保护后端的作用。不过这个方案还是不能应对大量高并发且不相同的缓存穿透,如果有人之前摸清楚了你业务有效范围,一瞬间发起大量不相同的请求,你第一次查询还是会穿透到DB。另外这个方案的一种缺点就是:每一次不同的缓存穿透,缓存一个空对象。大量不同的穿透,缓存大量空对象。内存被大量白白占用,使真正有效的数据不能被缓存起来。

所以对于这种方案需要做到:第一,做好业务过滤。比如我们确定业务ID的范围是[a, b],只要不属于[a,b]的,系统直接返回,直接不走查询。第二,给缓存的空对象设置一个较短的过期时间,在内存空间不足时可以被有效快速清除。

2、布隆过滤器

布隆过滤器是一种结合hash函数与bitmap的一种数据结构。相关定义如下:

布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

关于布隆过滤器的原理与实现网上有很多介绍,大家百度/GOOGLE一下便可。

布隆过滤器可以有效的判别元素是否集合中,比如上面的业务ID,并且即使是上亿的数据布隆过滤器也能运用得很好。所以对于一些历史数据的查询布隆过滤器是极佳的防穿透的选择。对于实时数据,则需要在业务数据时主动更新布隆过滤器,这里会增加开发维护更新的成本,与主动更新缓存逻辑一样需要处理各种异常结果。

综上所述,其实我觉得布隆过滤器和缓存空对象是完全可以结合起来的。具体做法是布隆过滤器用本地缓存实现,因为内存占用极低,不命中时再走redis/memcache这种远程缓存查询。

3.如何解决电商系统的常见问题?

什么是负载均衡高可用

nginx作为负载均衡器,所有请求都到了nginx,可见nginx处于非常重点的位置,如果nginx服务器宕机后端web服务将无法提供服务,影响严重。
为了屏蔽负载均衡服务器的宕机,需要建立一个备份机。主服务器和备份机上都运行高可用(High Availability)监控程序,通过传送诸如“I am alive”这样的信息来监控对方的运行状况。当备份机不能在一定的时间内收到这样的信息时,它就接管主服务器的服务IP并继续提供负载均衡服务;当备份管理器又从主管理器收到“I am alive”这样的信息时,它就释放服务IP地址,这样的主服务器就开始再次提供负载均衡服务。

什么是sso系统

单点登录是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
登录的处理流程:
1、登录页面提交用户名密码。
2、登录成功后生成token。Token相当于原来的jsessionid,字符串,可以使用uuid。
3、把用户信息保存到redis。Key就是token,value就是TbUser对象转换成json。
4、使用String类型保存Session信息。可以使用“前缀:token”为key
5、设置key的过期时间。模拟Session的过期时间。一般半个小时。
6、把token写入cookie中。

如何判断是否登录

1.从cookie中取token
2.取不到未登录
3.取到token,到redis中查询token是否过期
4.如果过期,为登录状态
5.没有过期,登录状态

浏览器跨域问题

跨域是指从一个域名的网页去请求另一个域名的资源。浏览器出于安全的考虑,不允许不同源的请求
JSONP解决AJAX跨域问题:
JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。
它的基本思想是,网页通过添加一个

海量数据的存储问题

如今随着互联网的发展,数据的量级也是呈指数的增长,从GB到TB到PB。对数据的各种操作也是愈加的困难,传统的关系性数据库已经无法满足快速查询与插入数据的需求。这个时候NoSQL的出现暂时解决了这一危机。它通过降低数据的安全性,减少对事务的支持,减少对复杂查询的支持,来获取性能上的提升。
但是,在有些场合NoSQL一些折衷是无法满足使用场景的,就比如有些使用场景是绝对要有事务与安全指标的。这个时候NoSQL肯定是无法满足的,所以还是需要使用关系性数据库。如果使用关系型数据库解决海量存储的问题呢?此时就需要做数据库集群,为了提高查询性能将一个数据库的数据分散到不同的数据库中存储。

什么是数据库分片

简单来说,就是指通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库上面,以达到分散单台设备负载的效果。
数据的切分(Sharding)根据其切分规则的类型,可以分为两种切分模式。
1.一种是按照不同的表来切分到不同的数据库(主机)之上,这种切可以称之为数据的垂直切分
2.另外一种则是根据表中的数据的逻辑关系,将同一个表中的数据按照某种条件拆分到多台数据库上面,这种切分称之为数据的水平切分。

电商活动倒计时方案(秒杀方案):

1、确定一个基准时间。可以使用一个sql语句从数据库中取出一个当前时间。SELECT NOW();
2、活动开始的时间是固定的。
3、使用活动开始时间-基准时间可以计算出一个秒为单位的数值。
4、在redis中设置一个key(活动开始标识)。设置key的过期时间为第三步计算出来的时间。
5、展示页面的时候取出key的有效时间。Ttl命令。使用js倒计时。
6、一旦活动开始的key失效,说明活动开始。
7、需要在活动的逻辑中,先判断活动是否开始。
秒杀方案:
8、把商品的数量放到redis中。
9、秒杀时使用decr命令对商品数量减一。如果不是负数说明抢到。
10、一旦返回数值变为0说明商品已售完。
由于宜立方商城是基于SOA的架构,表现层和服务层是不同的工程。所以要实现商品列表查询需要两个系统之间进行通信。

如何实现远程通信?

1、Webservice:效率不高基于soap协议。项目中不推荐使用。
2、使用restful形式的服务:http+json。很多项目中应用。如果服务太多,服务之间调用关系混乱,需要治疗服务。
3、使用dubbo。使用rpc协议进行远程调用,直接使用socket通信。传输效率高,并且可以统计出系统之间的调用关系、调用次数。

电商项目中是如何解决高并发和高可用的?

1.页面静态化
2.fastDFS图片服务器
3.数据缓存服务器
4.数据库集群、库表散列(数据库的各种优化、数据库的拆分)
5.负载均衡

什么是负载均衡

当一台服务器的单位时间内的访问量越大时,服务器压力就越大,大到超过自身承受能力时,服务器就会崩溃。为了避免服务器崩溃,让用户有更好的体验,我们通过负载均衡的方式来分担服务器压力。
我们可以建立很多很多服务器,组成一个服务器集群,当用户访问网站时,先访问一个中间服务器,在让这个中间服务器在服务器集群中选择一个压力较小的服务器,然后将该访问请求引入该服务器。如此以来,用户的每次访问,都会保证服务器集群中的每个服务器压力趋于平衡,分担了服务器压力,避免了服务器崩溃的情况。
负载均衡是用反向代理的原理实现的。

redis为什么可以做缓存?项目中使用redis的目的是什么?redis什么时候使用?

1)Redis是key-value形式的nosql数据库。可以快速的定位到所查找的key,并把其中的value取出来。并且redis的所有的数据都是放到内存中,存取的速度非常快,一般都是用来做缓存使用。
2)项目中使用redis一般都是作为缓存来使用的,缓存的目的就是为了减轻数据库的压力提高存取的效率。
3)在互联网项目中只要是涉及高并发或者是存在大量读数据的情况下都可以使用redis作为缓存。当然redis提供丰富的数据类型,除了缓存还可以根据实际的业务场景来决定redis的作用。例如使用redis保存用户的购物车信息、生成订单号、访问量计数器、任务队列、排行榜等。
redis支持五种数据类型存储:1.字符串2.散列3.列表4.集合5.有序集合(问深一点可能会问道底层数据结构,以及每种数据结构常用情景)这里也可以列举一些:

应用场景

计数器
数据统计的需求非常普遍,通过原子递增保持计数。例如,点赞数、收藏数、分享数等。
排行榜
排行榜按照得分进行排序,例如,展示 近、 热、点击率 高、活跃度 高等等条件的top list。
用于存储时间戳
类似排行榜,使用redis的zset用于存储时间戳,时间会不断变化。例如,按照用户关注用户的 新动态列表。记录用户判定信息
记录用户判定信息的需求也非常普遍,可以知道一个用户是否进行了某个操作。例如,用户是否点赞、用户是否收藏、用户是否分享等。
社交列表
社交属性相关的列表信息,例如,用户点赞列表、用户收藏列表、用户关注列表等。
缓存
缓存一些热点数据,例如,PC版本文件更新内容、资讯标签和分类信息、生日祝福寿星列表。
队列
Redis能作为一个很好的消息队列来使用,通过list的lpop及lpush接口进行队列的写入和消费,本身性能较好能解决大部分问题。但是,不提倡使用,更加建议使用rabbitmq等服务,作为消息中间件。
会话缓存 使用Redis进行会话缓存。例如,将web session存放在Redis中。
业务使用方式
String(字符串): 应用数, 资讯数等, (避免了select count(*) from …)
Hash(哈希表): 用户粉丝列表, 用户点赞列表, 用户收藏列表, 用户关注列表等。
List(列表):消息队列, push/sub提醒。
SortedSet(有序集合):热门列表, 新动态列表, TopN, 自动排序。

4.什么是右值引用?std::move

右值引用是C++11中最重要的新特性之一,它解决了C++中大量的历史遗留问题,使C++标准库的实现在多种场景下消除了不必要的额外开销(如std::vector, std::string),也使得另外一些标准库(如std::unique_ptr, std::function)成为可能。

右值引用的意义通常解释为两大作用:移动语义和完美转发。

C++中的std::move,它的作用是无论你传给它的是左值还是右值,通过std::move之后都变成了右值。

std::forward被称为完美转发,它的作用是保持原来的属性不变。啥意思呢?通俗的讲就是,如果原来的值是左值,经std::forward处理后该值还是左值;如果原来的值是右值,经std::forward处理后它还是右值。

10.11不会的

为什么要用第三方证书?

SSL证书的主要功能是:

1、对网站信息进行加密,提高安全防护能力
在未安装SSL证书的情况下,用户访问服务器的过程是明文传输,不进行加密处理,传递的任何信息都可能被第三方获取,包括信用卡号、用户名和密码以及其他敏感信息,会对个人的财产安全以及网站形象造成很大损害。加装SSL证书后,可实现对网站数据的加密传输,有效防止用户信息被黑客监听、窃取和篡改。

2、SSL证书可提供身份验证,防止钓鱼网站
除信息加密外,SSL证书还具有身份验证功能。SSL证书是由可信任的CA机构颁发,申请证书时会严格核实申请人信息,核实通过才会颁发证书,有效防止第三方通过伪装目标网站欺骗用户的行为,保障用户将信息发送到正确的服务器,不用担心信息的泄露。

3、SSL证书可增加网站的可信任度
安装SSL证书的网站在Web浏览器的地址栏可显示绿色小锁图标和绿色地址栏,EV 级别的SSL证书还能显示企业/组织名称。用户看到这些信息和标志,可以确认所访问的网站是目标网站而不是仿冒的钓鱼网站,有效提升客户的信任度。

4、提升网站的搜索排名
目前Google、百度、360等主流搜索引擎表示会优先收录以HTTPS开头的网站,并赋予网站更高的权重,有效提高网站关键词的排名,增加网站流量和权重,提升品牌的形象和曝光。

5、防止流量劫持
普通的http网站非常容易遭受网络攻击,尤其流量劫持,会强制用户访问其他网站,从而造成网站流量损失,而安装了可信任的SSL证书,你的网站就能有效地避免流量劫持此类的问题。

SSL的会话过程
SSL会话主要分为三步:
1.客户端向服务器端索要并验正证书;
2.双方协商生成“会话密钥”;对成密钥
3.双方采用“会话密钥”进行加密通信;

SSL证书工作过程?

当我们将SSL证书安装在网站服务器之后,用户使用浏览器访问我们的网站时,SSL证书就会触发SSL/TLS协议,此类协议会将服务器与浏览器之间(或服务器与服务器之间)传输的信息进行加密。

1、首先,创建TCP连接之后,SSL证书会开始工作,发起“SSL握手”。

2、服务器将其SSL证书连同若干说明一起发送给用户客户端,这些说明包括了SSL/TLS证书的版本以及所使用的加密方法。

3、客户端会检查网站的SSL证书是否有效,然后选择服务器和SSL证书均能支持的最高级别加密,并使用这些加密方式开启安全会话。不同加密方法合集具有不同的加密强度,它们统称为密码套件。

4、为确保所有传输信息的完整性和真实性,SSL/TLS协议还包含了采用信息验证代码(MAC)的身份验证流程。

weak_ptr实现原理

constructor构造函数,初始化的时候默认引用计数为0

在使用普通指针初始化两个shared_ptr的时候,两个shared_ptr的引用计数都为1

在进行拷贝构造的时候,使用shared_ptr去初始化另一个shared_ptr的时候引用计数会+1

destructor析构函数,析构的时候在指针不为空且引用计数为0的时候释放空间

operator = 当使用一个shared_ptr给另一个shared_ptr赋值的时候这里需要注意

  1. 由于指针指向发生变化,原来的_ptr指针的引用计数要–,且当达到了0的时候要注意回收原来指针的空间
  2. _ptr又指向了新的_ptr,则新的_ptr指针的引用计数要++

operator* 解引用运算符,直接返回底层指针的引用,即共享的地址空间内容

operator ->指针运算符

weak_ptr为什么能解决循环引用?

利用weak_ptr就可以解决循环引用的问题,因为weak_ptr是弱引用型指针,是用来监视shared_ptr的,不会使引用计数增加,但是它不管理shared_ptr内部的指针,也就是数据指针,它是用来监视shared_ptr生命周期的,通过它的构造不增加引用计数,析构不减少引用计数这一点,从而解决了循环引用的问题。

垃圾回收(garbage collection)机制除了 use_count还有什么?

1、引用计数算法

引用技术算法是唯一一种不用用到根集概念的GC算法。其基本思路是为每个对象加一个计数器,计数器记录的是所有指向该对象的引用数量。每次有一个新的引用指向这个对象时,计数器加一;反之,如果指向该对象的引用被置空或指向其它对象,则计数器减一。当计数器的值为0时,则自动删除这个对象。

缺点二是多个线程同时对引用计数进行增减时,引用计数的值可能会产生不一致的问题,必须使用并发控制机制解决这一问题,也是一个不小的开销。

2、 Mark & Sweep 算法

这个算法也称为标记清除算法,为McCarthy独创。它也是目前公认的最有效的GC方案。Mark&Sweep垃圾收集器由标记阶段和回收阶段组成,标记阶段标记出根节点所有可达的对节点,清除阶段释放每个未被标记的已分配块。典型地,块头部中空闲的低位中的一位用来表示这个块是否已经被标记了。通过Mark&Sweep算法动态申请内存时,先按需分配内存,当内存不足以分配时,从寄存器或者程序栈上的引用出发,遍历上述的有向可达图并作标记(标记阶段),然后再遍历一次内存空间,把所有没有标记的对象释放(清除阶段)。因此在收集垃圾时需要中断正常程序,在程序涉及内存大、对象多的时候中断过程可能有点长。当然,收集器也可以作为一个独立线程不断地定时更新可达图和回收垃圾。该算法不像引用计数可对内存进行即时回收,但是它解决了引用计数的循环引用问题,因此有的语言把引用计数算法搭配Mark & Sweep 算法构成GC机制。

3、 节点复制算法

Mark & Sweep算法的缺点是在分配大量对象时,且对象大都需要回收时,回收中断过程可能消耗很大。而节点复制算法则刚好相反,当需要回收的对象越多时,它的开销很小,而当大部分对象都不需要回收时,其开销反而很大。
算法的基本思路是这样的:从根节点开始,被引用的对象都会被复制到一个新的存储区域中,而剩下的对象则是不再被引用的,即为垃圾,留在原来的存储区域。释放内存时,直接把原来的存储区域释放掉,继续维护新的存储区域即可。

分代回收

以上三种基本算法各有各的优缺点,也各自有许多改进的方案。通过对这三种方式的融合,出现了一些更加高级的方式。而高级GC技术中最重要的一种为分代回收。它的基本思路是这样的:程序中存在大量的这样的对象,它们被分配出来之后很快就会被释放,但如果一个对象分配后相当长的一段时间内都没有被回收,那么极有可能它的生命周期很长,尝试收集它是无用功。为了让GC变得更高效,我们应该对刚诞生不久的对象进行重点扫描,这样就可以回收大部分的垃圾。为了达到这个目的,我们需要依据对象的”年龄“进行分代,刚刚生成不久的对象划分为新生代,而存在时间长的对象划分为老生代,根据实现方式的不同,可以划分为多个代。

shared_ptr内部为什么是线程安全的,对对象的读写为什么不是线程安全的?

operator = 当使用一个shared_ptr给另一个shared_ptr赋值的时候这里需要注意

  1. 由于指针指向发生变化,原来的_ptr指针的引用计数要–,且当达到了0的时候要注意回收原来指针的空间
  2. _ptr又指向了新的_ptr,则新的_ptr指针的引用计数要++

(shared_ptr)的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。

B继承A,C继承A,那么有几个虚函数表;

A virtual

一个类只能有一个虚函数表。

ClassB继承与ClassA,其虚函数表是在ClassA虚函数表的基础上有所改动的,变化的仅仅是在子类中重写的虚函数。如果子类没有重写任何父类虚函数,那么子类的虚函数表和父类的虚函数表在内容上是一致的

处理器如何实现原子操作

(1) 使用总线锁保证原子性

如果多个处理器同时对共享变量进行读写操作,那么共享变量就会被多个处理器同时进行操作,这样读写操作就不是原子的,操作完之后共享变量的值会和期望的不一致.

所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出次信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存.

在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的。

(2) 使用缓存锁保证原子性

通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。

所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效。

但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

vector使用会出现什么问题?

是因为vector与普通的vector模板的实现,不是一回事。这是一个被STL底层实现上做过特殊处理的实现,与普通的vector这种生成的容器不同。

它的底层使用的是bit来实现,而bool的存储是一个字节(8个bit),bit是一个位。所以FunBool(bool&)要求传的是一个字节,而实际上v_bool[0]返回的引用是一个bit,类型不同,所以报错。
解决办法:可以使用char去替代解决这个问题。

偏特化的一个例子!!!

内存泄漏怎么排查

VC下内存泄漏的检测方法

用MFC开发的应用程序,在DEBUG版模式下编译后,都会自动加入内存泄漏的检测代码。在程序结束后,如果发生了内存泄漏,在Debug窗口中会显示出所有发生泄漏的内存块的信息,以下两行显示了一块被泄漏的内存块的信息:

E:\TestMemLeak\TestDlg.cpp(70) : {59} normal block at 0x00881710, 200 bytes long.

Data: 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70

C++初始化列表

除了性能问题之外,有些时候合初始化列表是不可或缺的,以下几种情况时必须使用初始化列表
1.常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
2.引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面

3.没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化

成员变量的顺序
成员是按照他们在类中出现的顺序进行初始化的,而不是按照他们在初始化列表出现的顺序初始化的,看代码:

class foo
{
public:
int i ;int j ;
foo(int x):j(x), i(j){} // i值未定义
};
这里i的值是未定义的因为虽然j在初始化列表里面出现在i前面,但是i先于j定义,所以先初始化i,而i由j初始化,此时j尚未初始化,所以导致i的值未定义。一个好的习惯是,按照成员定义的顺序进行初始化。

网络IO模型

Unix 有五种 I/O 模型:

  • 阻塞IO(bloking IO)
  • 非阻塞IO(non-blocking IO)
  • 多路复用IO(multiplexing IO)
  • 信号驱动式IO(signal-driven IO)
  • 异步IO(asynchronous IO)
1.阻塞式 IO

应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。

应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的 CPU 利用率效率会比较高。

下图中,recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。

2.非阻塞式 IO

应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。

由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率是比较低的。

3.多路复用IO

由于阻塞式IO通过轮询得到的只是一个IO任务是否完成,而可能有多个任务在同时进行,因此就想到了能否轮询多个IO任务的状态,只要有任何一个任务完成,就去处理它。这就是所谓的IO多路复用。LINUX下具体的实现方式就是select、poll、epoll。

这种机制可以让单个进程具有处理多个 IO 事件的能力。又被称为 Event Driven IO,即事件驱动 IO。

最实际的应用场景就是web服务器响应连接的方式,IO 复用可支持更多的连接,同时不需要进程线程创建和切换的开销,系统开销更小。

4.信号驱动 IO

应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。

相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。

5.异步 IO

应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。

异步 IO 与信号驱动 IO 的区别在于,异步 IO 的信号是通知应用进程 IO 完成,而信号驱动 IO 的信号是通知应用进程可以开始 IO。

五大 IO 模型比较

前四种 I/O 模型的主要区别在于第一个阶段,而第二个阶段是一样的:将数据从内核复制到应用进程过程中,应用进程会被阻塞。

怎么创建一个只能从堆/栈上生成的类?

一.定义一个只能在堆上生成的类
分析:我们都知道一个对象是既可以在栈上生成也可以在堆上new出来,要想生成一个只能在堆上生成的类,也就是说,局部对象,静态对象,全局对象都不能生成,那么我们可以将构造函数写成私有或者保护(类外不可以直接访问的),通过new来构造出一个堆上的对象;
1.将构造函数设为私有+将new操作符封装在类的公有函数中;

2.将析构函数设为私有+将delete封装在类的公有成员函数中

3.方法二的优化:支持继承 还好C++提供了第三种访问控制,protected。将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。
另一个问题是,类的使用很不方便,使用new建立对象,却使用destory函数释放对象,而不是使用delete。(使用delete会报错,因为delete对象的指针,会调用对象的析构函数,而析构函数类外不可访问)这种使用方式比较怪异。为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。代码如下,类似于单例模式:
class A
{
protected :
A(){}
~A(){}
public :
static A* create()
{
return new A();
}
void destory()
{
delete this ;
}
};
————————————————
版权声明:本文为CSDN博主「gogogo_sky」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/gogogo_sky/article/details/74725935

二.定义一个只能在栈上生成的类
想法一:要想只在栈上定义对象,就不能使用new,也不能定义全局和静态变量,我们可以通过以下的方式实现:将构造函数设为私有或者保护,然后写一个公有方法去获取对象,但是不能使用new这个操作符。

想法二:只能在栈上开辟对象,也就是说不能在堆上开辟了。产生堆对象的唯一方法就是使用new,而禁止使用new也就是不能在堆上产生对象了。由于new执行时会调用operator new, 而operator new是可重载的, 所以将operator new和operator delete重载为私有即可。
实现代码如下:

内核态转用户态

通常情况下,内核是不会调用用户层的代码,要想实现这逆向的转移,一般做法是在用户进程的核心栈(tss->esp0)压入用户态的SS,ESP,EFLAGS,CS,EIP,伪装成用户进程是通过陷阱门进入核心态,之后通过iret返回用户态。

const在编译器中怎么实现

对于这段程序,g++是把constant放到了常量区,也就是经常有人问起的保存char *s="abc"的地方。
这块内存是不允许修改的,所以试图赋值的时候就会segment fault
楼主可以试试输出addr的值,然后看看const int constant和int constant的情况下addr的值有什么变化,还可以再输出一个char *s="abc"的地址和addr对比一下。

有的时候,编译器有可能把const int做优化,直接替换成立即数而不给分配内存。不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高,同时,这也是它取代预定义语句的重要基础。

static在编译器中怎么实现?

static的实现是通过将这个变量初始化在全局变量区实现的,但与全局变量不同,它具有可见性(也可以说隐藏性)

atomic +1怎么实现:

lock前缀实现原子性的两种方式:

  • 锁bus:性能消耗大,在intel 486处理器上用此种方式实现
  • 锁cache:在现代处理器上使用此种方式,但是在无法锁定cache的时候(如果锁驻留在不可缓存的内存中,或者如果锁超出了划分cache line 的cache boundy),仍然会去锁定总线。
BUS LOCK

英特尔处理器提供LOCK#信号,该信号在某些关键内存操作期间会自动断言,以锁定系统总线或等效链接。在断言该输出信号时,来自其他处理器或总线代理的控制总线的请求将被阻止。此度量标准度量总线周期的比率,在此期间,在总线上声明LOCK#信号。当由于不可缓存的内存,跨越两条缓存行的锁定操作以及来自不可缓存的页表的页面遍历而导致存在锁定的内存访问时,将发出LOCK#信号。

在这里,锁定进入操作由总线上的一条消息组成,上面写着“好,每个人都退出总线一段时间”(出于我们的目的,这意味着“停止执行内存操作”)。然后,发送该消息的内核需要等待所有其他内核完成正在执行的内存操作,然后它们将确认锁定。只有在其他所有内核都已确认之后,尝试锁定操作的内核才能继续进行。最终,一旦锁定被释放,它再次需要向总线上的每个人发送一条消息,说:“一切都清楚了,您现在就可以继续在总线上发出请求了”。

CACHE LOCK

cache lock 要比bus lock 复杂很多,这里涉及到内核cache同步,还有 memory barrierscache lineshared memory等概念,后续会持续更新。

epoll优缺点

epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

优点:

1)支持一个进程打开大数目的socket描述符(FD)

select最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024/2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

2)IO效率不随FD数目增加而线性下降

传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在Linux内核。

3)使用mmap加速内核与用户空间的消息传递。

这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核与用户空间mmap同一块内存实现的。

©著作权归作者所有:来自51CTO博客作者LOVEMERIGHT的原创作品,如需转载,请注明出处,否则将追究法律责任
select、poll与epoll的优缺点
https://blog.51cto.com/lovemeright/1831920

缺点:

\1. 相对select来说, epoll的跨平台性不够用 只能工作在linux下, 而select可以在windows linux apple上使用, 还有手机端android iOS之类的都可以. android虽然是linux的内核 但早期版本同样不支持epoll的.
\2. 相对select来说 还是用起来还是复杂了一些, 不过和IOCP比起来 增加了一点点的复杂度却基本上达到了IOCP的并发量和性能, 而复杂度远远小于IOCP.
\3. 相对IOCP来说 对多核/多线程的支持不够好, 性能也因此在性能要求比较苛刻的情况下不如IOCP

进程切换VS线程切换

进程切换分两步

1.切换页目录以使用新的地址空间
2.切换内核栈和硬件上下文。

对于linux来说,线程和进程的最大区别就在于地址空间。
对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。 所以明显是进程切换代价大

线程上下文切换和进程上下文切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

进程切换要保存的信息有哪些?

1、进程当前暂存信息

2、下一指令地址信息

3、进程状态信息

4、过程和系统调用参数及调用地址信息

进程上下文切换

1.保存被切换进程的正文部分至有关存储区

2.操作系统进程中有关调度和资源分配程序执行

3.将被选中进程的原来被保存的正文部分从有关存储区中取出,并送至有关寄存器与堆栈中,激活被选中进程执行。

共享内存常见场景

1、进程间通讯——生产者消费者模式

一个现场负责产生数据,一个现场负责处理数据,那么我们可以用主线程作为生产者,从键盘获取数据写入共享缓冲区,另一个线程作为消费者,从共享缓冲区读取数据,并打印。当然,这里要解决互斥的问题

2、父子进程间通讯

由fork()产生的子进程和父进程不共享内存区,所以父子进程间的通讯也可以共享内存,以POSAX共享内存为例:父进程启动后使用MAP_SHARED建立内存映射,并返回指针ptr。fork()结束后,子进程也会有指针ptr的拷贝,并指向同一个文件映射。这样父子进程便共享了ptr指向的内存区

3、进程间共享——只读模式

经常碰到一种场景,进程需要加载一份配置文件,可能这个文件有100K大,那如果这台机器上多个进程都要加载这份配置文件时,比如有200个进程,那内存开销合计为20M,但如果文件更多或者进程数更多时,这种对内存的消耗就是一种严重的浪费。比较好的解决办法是,由一个进程负责把配置文件加载到共享内存中,然后所有需要这份配置的进程只要使用这个共享内存即可。

共享内存分类

1、POSIX共享内存对象

特点:

  • 访问速度非常快。
  • 进程重启共享内存中数据不会丢失,即持续性

2、POSIX内存映射文件

特点:

  • 访问速度比内存区对象慢。
  • 持续性同上

3、SYSTEM共享内存

特点:

  • 访问速度非常快
  • 持续性同上

总结

以上就是今天要讲的内容,本文仅仅简单介绍了学习途径和个人面试一些不会的题目,觉得有帮助可以点赞收藏关注。

猜你喜欢

转载自blog.csdn.net/DexiangPeng/article/details/121175310