当前位置:网站首页>454-百度面经1

454-百度面经1

2022-07-06 17:59:00 liufeng2023

0、项目

项目:

  • 代码传到github上去!
  • 不要过多的去描述项目的业务!
  • 描述项目的设计,重写的开源库,有什么样的好处!高并发,缓存,解耦网络层、业务层、db层的强耦合!用到了微服务、消息队列,redis就是将业务层和db层解耦了!

介绍项目:

  • 项目背景;为什么做!(聊天服务器,想根据某个业务场景,设计一个高并发的业务的聊天服务器!)写完服务器测试一下性能,支持同时在线的10000人的在线聊天!再提升,怎么做,加集群!还有种方式将它的服务拆分成非常小的服务,做成基于分布式的架构,基于微服务的模型!
  • 聊天服务器项目的架构设计,网络层、服务层、数据传输采用的是Json协议,网络使用的是muduo库,基于epoll+多线程的高并发模型!

面试官说:你写的项目没有必要啊?

  • 介绍为什么写这个项目!
  • 每一次写一个项目都有收获!把前面…知识在项目上得到了实践锻炼,真正的了解了多线程开发,线程通信,在项目中得应用,和之前学习知识点后做的demo是完全不一样的;智能指针…真正的感受到了他的好处!

项目中遇到了哪些问题?

  • 设计的问题,分析性能原因,引入了redis,也是问题!
  • 碰见死锁!
  • 碰见段错误,挂掉了!
  • 测试服务器的瓶颈的时候,fd的上限的设置,怎么解决的?
  • 项目的稳定性,安全性!

1、如何保证发送数据不丢且不重(项目相关)

IT技能的每一个描述点,能从项目的什么地方引入进去!

项目如何保证一件事情是否完成?三种情况;

  • 协议本身帮你做了;
  • 基于协议实现的第三方框架帮你做了;
  • 自己写的应用,自己做的!

项目中怎样实现高并发的?

  • muduo库本身就已经实现了高并发了,将muduo库中的基于Rector模型IO复用 + 线程池说一下

TCP保证发送数据不丢且不重?

  • 协议本身就保证了;
  • 不丢:超时重传机制;
  • 不重:序号和确认应答机制;

应用保证发送数据不丢且不重?

  • 也是类似于TCP的重传机制、序号和确认应答机制;
  • 将TCP在传输层协议上的一个机制,在应用层上再来一遍!

面试的时候聊到TCP协议和UDP协议,面试官经常给你一种场景让你设计;

  • TCP基于连接的,是流式协议;UDP是无连接的,是用户数据包协议,通信双方不管能不能收到,发送方只管发,接收方尽量收!
  • TCP有安全的保障机制,超时重传、乱序重排、滑动窗口、流量控制、拥塞控制!在协议上保证数据可靠性传输!UDP不可靠的!
  • TCP的ACK响应也是会占用网络带宽的!(现在带宽很好,光纤入户,可以不考虑! )
  • 现在设计服务器类的程序,都是采用TCP协议!
  • TCP虽然耗费了一点点的多余的流量,但是换来了非常可靠的传输!UDP不行!
  • 服务器一定首先要保证高可用高可靠

TCP的三次握手和四次挥手!

2、线程池的线程为什么要动态扩容? & 进程池(nginx)/线程池(muduo)/内存池/连接池(数据库)

2.1、进程池(nginx)/线程池(muduo)/内存池/连接池(数据库)

  • 不用池的话开销太大了,临时生成以及销毁代价都大!使用池;
  • 需要用资源,从池里面拿,用完了也不释放,再返回池中;
  • 进程和线程,在Linux环境下,创建进程线程时,底层用的也是克隆,内核中用哪个的还是fork!
  • 项目中碰到小块内存的频繁开辟和释放时,malloc和free的效率是比较低的!使用内存池来提高小块内存的使用效率!
  • 连接池也是一样,不断的TCP三次握手和四次挥手,效率降低,资源消耗大!

2.2、线程池的线程为什么要动态扩容?

不进行动态扩容的话,线程池就是死的,数量固定好不好?客观的看!

不进行动态扩容:

  • muduo库的线程池没有动态扩容!采用Reactor模型 + epoll + 线程池,线程池中线程数量:IO数量 + 工作线程数量 和CPU的核数相同,这样才能最大利用CPU进行并行的处理!

进行动态扩容:

  • 所有的池都是有一个初始的数量的!
  • 如果所有的线程都被占用,此时由耗时的任务,需要扩容产生新的线程!
  • 线程的上下文切换开销很大;所以要线程池;业务并发量无法预估的情况下;可以保证系统资源消耗小,性能得到保证;
  • 当在一定时间内,有部分线程一直处于空闲状态的话,就将多余的线程释放掉!

线程开销:

  • 32位linux下,4G虚拟内存, 3G用户, 每个进程能pthread_create 能创建380左右个线程 ,3G / 8M = 380+(8M为线程栈的大小)
  • 线程的创建开销和进程的创建开销一样,都是很大的;
  • 线程太多,线程本身占用的内存比较大,业务可以使用的内存就少了;
  • 使用ulimit -s查看线程栈的大小,1000-2000线程的话,内存会被占一半了!
  • 线程的上下文切换;
  • 造成服务器锯齿状负载;(1000-2000线程在阻塞等待时,当有事件到达时,那一瞬间,系统的cpu和内存消耗是非常大的,很容易造成服务器崩溃!所有我们不允许创建很多线程!)

3、假设与数据库交互出现问题,线程在和数据库交互时产生阻塞,线程不能继续执行了,会产生什么问题?

  • 聊天服务器采用的是Reactor模型,一个IO线程复杂监听新连接,剩下的线程属于工作线程!一个线程在epoll上监听了很多个客户端的套接字,接收到一个用户的请求之后,就会开始做业务!做业务的过程中会涉及到数据库的curd;
  • 如果一个工作线程阻塞了,导致注册在这个工作线程上的epoll上的所有的套接字后面发生的读写事件,当前线程无法处理了!

4、3中的问题怎么解决?

gdb调试,大秦坑王,有专门的调试文章!

解决方法:

  • 使用连接池,当经过一定时间(设置为1-2s)无响应,会返回错误!这样和数据库交互的时候就不会产生阻塞!
  • 工作线程不阻塞,工作这个线程上的其他clientfd就可以处理事件了!

5、C++中引用和指针的概念和区别?

从swap函数引入,形参想要改变实参的值,使用值传递不行;

C语言中使用指针,C++中使用引用!

在windows直接断点调试,发现指针和引用的汇编代码是完全一样的!

在Linux下,直接-g,使用gdb调试,disassemble查看反汇编代码!

在查看汇编指令的时候,定义一个引用,实际上也是要在栈上,
开辟4个字节的内存,将它要引用的变量内存的地址放到4个字节中;

通过引用变量来引用它所指向的变量的时候,实际上汇编上也是从之前底层开辟的那4个字节的内存的地址,自动做一个解引用的操作!
当通过指针解引用的时候,实际上是从指针的 4个字节取出指向的内存的地址,去访问所指向的内存;

可以看出在汇编的层面上是100%的相似! 即汇编层面是一样的,在语言层面是不同的!

C++中开发时尽量使用引用:

  • 实际上引用起来和指针一样,但是引用必须初始化,所有引用相对指针安全一些;
  • 但是指针也不能说不好,C和C++最大的一个特点,通过指针可以在内存上漫游,通过指针的++和–可以在内存上偏移;引用不能偏移,只要使用引用,就会自动偏移了!
  • 指针给我们的扩展性较大,没有解引用指针,指针可以p+1,p+2进行偏移!
  • 引用没有办法做内存上的偏移,直接进行的解引用了!

6、地址传递和引用传递区别?

7、实际应用中传递一个很大的数组,不想去改变它,但是还不想让他拷贝,怎么做?

挖坑题!

  • 传递数组根本不会发生拷贝,传递数组名永远传递的是数组起始地址,传递的是指针;
  • 因为只传递了数组的地址,我们在使用的时候,需要传入数组的长度传入!
  • 直接传入数组的地址,就像当于指针传递,在函数的内部是可以修改数组的内容;如果不想修改,在数组参数前面加上const!可以读取数组内容,不可以修改数组内容!
  • 再延伸,数组=》stl容器!stl容器就是我们的准备好的!

vecto容器和数组的区别:

  • 容器不用自己管理内存,不够了可以扩容…
  • 当传递一个容器的时候,实参到形参是一个拷贝构造的过程!如果函数中传递一个容器,性能就会大大下降,有拷贝构造的过程;这时应该通过引用进行传递!引用也是可以修改容器的,因此需要通过const常引用!

8、const A * p 和 A* const p的区别?

const修饰的是指针本身还是指针指向;

const A * p

  • const修饰p,表示 p不能被赋值,p可以被赋值;

A* const p

  • const修饰的是p,表示p不可以被赋值,*p可以被赋值;

仅仅是上面这样简单的回答吗?

怎么发散?

  • 实践的角度进行发散,表示我们用过!

实践角度进行发散:

  • const A * p主要用在调用函数时, 实参是通过指针传递给形参,在形参中,我们只想访问实参,不想修改实参!所以形参的指针,在最前面加上const!可以读* p。不可以修改* p;
  • A* const p,即和this指针相关,p不可以被修改,不能给this指针赋值,this指针永远指向调用方法的对象的!可以通过this指针可以修改指向对象的值(*p可以赋值),但是不可以修改this的指向(p不可以修改)!

const修饰的额值都是常量,常量都是不能被修改吗?
const int a = 10

如果直接a = 20,这是绝对不可以的!

怎么改?

int *p = (int *) &a;
*p = 20;

通过汇编修改:
在这里插入图片描述

这样就已经被修改了!

为什么又可以修改了?

  • const修饰的变量不能被修改只是在编译阶段,语法级别上;
  • 实际上,在汇编上,并不是说const直接将内存的属性给改了,从原来的可读可写变为只读不写,不可能的!
  • const只是立足于语法层面,编译阶段,编译器能够检测出对一个const修饰的常量赋值了!但是经过编译以后,就到了汇编指令上,这个时候就没有const了, const只是在语法层面不能修改!
  • C语言中,const是常变量;C++语言中,const是常量!

9、成员函数后加const是什么意思?会有什么样的限制?

从实践的角度去回答:

  • 定义一个const常对象去调用普通方法时,我发现是调用不了的!

为什么调用不了?

  • C++用一个对象调用一个方法,汇编之后,实际的调用过程还是一个C函数的调用!只是 将调用对象的地址当成实参传递给函数;所有在成员方法编译的时候,都会加一个形参this来接收对象的地址,但是this是一个指向非const对象的const指针,不能接收const对象;

所有当常对象调用成员方法的时候,成员方法的后面需要加const,影响了this指针,让this指变成指向const对象的const指针,这样形参this就可以接收对象了!常对象就可以调用常成员方法了!

限制:

  • 在常方法中,只能访问对象的成员变量,不可以改变!
  • mutable修饰的变量是依旧可以修改的!

this指针

  • 每个对象拥有一个this指针,通过this指针来访问自己的地址。
  • this指针并不是对象的一部分,this指针所占的内存大小是不会反应在sizeof操作符上的。
  • this指针只能在成员函数中使用,全局函数、静态函数都不能使用this指针;
  • 在普通成员函数中,this是一个指向非const对象的const指针(如类类型为Student,那么this就是Student *const类型的指针);
  • 在const成员函数中,this指针是一个指向const对象的const指针(如类类型为Student,那么this就是const Student * const类型的指针)
  • this指针在成员函数开始执行前构造,在成员函数执行结束后销毁。

const对象与const成员函数

  • 在成员函数参数列表后面加上const修饰,表示函数内this指针是一个指向常量对象的常量指针,不能修改成员变量。
  • 一个const成员函数如果以引用的形式返回*this,那么它的返回类型是常量引用;
  • 对于const对象或者const对象的引用和指针,对象内的成员变量是不能修改的,因此只能调用const成员函数,不会修改成员变量;
  • 对于非const对象,既可以调用const成员函数,也可以调用非const成员函数。

常量成员函数特点

  • 常量成员函数不能改变调用它的对象的内容。
  • 常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
  • 不能在一个常量对象上调用普通成员函数。
  • 如果常量成员函数在类外部定义,也必须在参数列表后面加上const关键字。
  • const成员函数中可以修改添加了mutable关键字的成员变量的值。
  • 静态成员函数与对象无关,因此不能被声明为const。

10、类中const成员如何初始化?

定义必须初始化的成员变量,必须放在初始化列表中(const变量、引用成员变量)

11、vector如何管理内存?

  • 从vector的底层说起,它是一个动态扩容的数组;

  • 容器依赖空间配置器allocator来进行管理内存,空间配置器中有4个方法非常重要!

  • vector只负责对象的构造、析构,内存管理都是靠allocator空间配置器来管理的。

  • 在Linux下是0 - 1 - 2 - 4 - 8 2倍扩容的!vs2017,2019下是1.5倍扩容;(从实践的角度,编写代码,不断的插入元素,每次打印vector的容量以及实际大小,探究扩容机制。capacity返回vector的容量,sizeof是返回vector中实际元素的大小)

  • 小结下,这就是vector如何管理内存的;

  • 话锋一转!还可以介绍一下vector的其他重要的方法;(vector还有很多其他有用的方法,比如说…)

  • 所有C++ STL容器在进行管理底层对象内存的时候,不可能直接用new和delete,为什么不能用,上课的时候说过!

  • 所有容器都必须依赖allocator空间配置器,里面有4个方法:construct、destory、allocate、deallocate;

  • construct、destory负责对象的构造和析构;allocate、deallocate负责内存的开辟和释放,就是在vector管理内存的时候,一定要将对象new的对象的构造和内存开辟分隔开,将delete的对象的析构和内存的释放分隔开!(原因:初始化一个vector的时候,如果用new的话,不仅仅会开辟一个内存,还会构造一些没有用的对象,不符合我们的逻辑,所有底层的内存开辟是靠allocate来的,allocate和deallocate都是用的默认C库的malloc和free)

  • 如果应用层使用vector比较频繁的话,小块内存使用比较频繁,可以使用内存池来替换allocate和deallocate中使用的malloc和free;

  • 再扩展,在C++11以后提供的带右值引用的拷贝构造和赋值重载!很大程度的提高了vector拷贝和构造的性能!

12、vector什么时候会发生迭代器失效?(从编程习惯考虑如何避免)

  • 在自己实践的过程中,有没有接触过迭代器的失效?
  • 当我们用vector解决问题的时候,有可能实际到遍历vector,即读取时,迭代器不会发生失效;
  • 在进行insert和 erase的时候,迭代器就会失效,insert和erase中接收的都是迭代器,不仅仅是vector,相关的容器的方法,一般接收的都是迭代器,我后面再说!
  • 迭代器的底层实际上是对一个指针的封装,拿vector来说,vector的迭代器的底层是一个指针,vector的迭代器的指针指向的是vector底层的一个数组;
  • 迭代器通过该运算符重载,以用相同的方式 遍历不同的容器! 因为不同的容器的遍历方式都被封装在迭代器的++,- - 运算符中;
  • insert可能回到值vector容器的扩容,内存位置都变了,迭代器的底层是一个指针,指向一个地址,扩容之后,迭代器肯定就失效了!
  • 怎么处理?insert和erase返回值会返回新位置的迭代器,需要将迭代器的循环变量更新下;
  • erase删除元素之后,后面所有的元素都有向前挪,erase从首元素到当前删除元素之间的迭代器还是有效的!但是当前位置到末尾元素迭代器都会发生变化,需要更新!

现在开始扩展:

  • 迭代器对于容器时非常重要的!
  • 每一种容器都有字节的迭代器,因为每一种容器的底层数据结构都是不一样的;
  • 但是实际上写代码的时候,发现迭代器遍历容器的代码都是一模一样的,这是怎么做到的?因为迭代器提供++、- -运算符重载函数,虽然不同容器,不同数据结构元素的迭代方式不一样,但是不同数据结构的不同迭代方式都被封装在迭代器++、- -运算符重载函数中,所以用迭代器操纵不同容器,代码是一模一样的!
  • 不仅容器方法接收的增加删除用的迭代器,泛型算法用的也是迭代器!

13、用拷贝构造给一个复杂vector赋值不太好,如何解决?

  • 意思就是vector里面装的是大对象,vector的拷贝构造会造成很多大对象的拷贝构造;
  • 赋值会先临时构造右边的对象,根据右边的资源,给左边的对象开辟内存,再给它的内存上构造很多对象,再将右边临时对象析构,将内存析构掉!效率是很低的!
  • 视情况采用std::move调用vector的右值引用拷贝构造和赋值重载(右值引用又称为移动语义,不会按照右边的尺寸给左边开辟内存,构造对象,而是直接将右边的资源全部拿过来!)
  • vector除了拷贝构造和赋值,其他方法也都提供了带右值引用的方法!

14、map/multimap区别

关联容器 + 底层数据结构 + 常用的方法融合到一块!

map/multimap的底层都是红黑树,说完这个可以说数据结构的的哈希表和红黑树!

延伸到哈希表,可以说哈希冲突,解决方案!

说到红黑树,可以将BST和AVL都说一下,这些都属于二叉搜索树,只是各自有各自的特点!

如果只说 map的key是可以重复的,multimap的key是不可以重复读的是不行的!

回答问题尽量从实践的角度出发

思路:

  • 什么时候会用到map,做什么的时候会用到map;从实践的角度切到map;
  • 然后再切到map/multimap的区别上!
  • 实践 + 理论!

map和multimap都称为映射表,主要处理数据有关联关系的键值对的场景! 比如说用一个学生的学号对应一个学生成绩,通过快速查找学号来得到学生的数据!

map和multimap主要的区别是key值是否能重复!value是可以重复的!

接着介绍底层的数据结构红黑树 !所以map增删查的时间复杂度都是O(log2n),效率非常快的!

红黑树一般运用在对于key有排序性要求的场景下!

再延伸,就说,实际上大部分场景对key的有序性是没有要求的,我一般在看一些开源库的时候,muduo网络库的时候,都是用的是unordered_map 和 unordered_set!点到为止!

再延伸,Linux的虚拟内存管理用的就是红黑树,效率是非常高的!早期的Linux的内存管理用的是AVL,最后Linux3.6版本之后又改成了红黑树!因为红黑树相对于AVL树,它的优势是…

这样我们就延伸到了AVL树,是一个平衡树,它是一个基于内存的!

接着说,还有基于IO操作的平衡树,B树和B+树…这是数据库索引常用的,它的好处是…

问题的核心详细说出来,相关联的内容点到为止,等待面试官提问!


顺序容器vector、list、deque

  • 尤其是vector和list,映射与基础数据的数组和链表;
  • 说到容器,将其迭代器、泛型算法都说说!

15、智能指针

真正工作时,使用裸指针的情况是非常少的!

说起它的时候,还是要从实践的角度说起,要他有什么用?

  • 目的: 自动释放资源(对象资源,文件资源),就是担心有些资源用完了没有被释放,就会产生资源泄漏!
  • 智能指针利用栈上对象出作用域自动析构特点,将资源的释放代码写到智能指针的析构函数中!

面试时这样介绍:

  • 在C语言中,动态分配堆内存用的是malloc+free;在C++中用的是new+delete;
  • 但是手动开辟,可能会 free和delete忘记写了,或者其他文件资源忘记关闭了!
  • 为了自动解决这些问题,使用智能指针,利用的栈上对象出作用域自动析构特点;

能指针从大的方向上分有两类,不带引用计数的和带引用计数的!

  • 不带引用计数:C++标准库之前提供的auto_ptr,介绍一下;auto_ptr问题:会自动交出管理对象的所有权!所以auto_ptr在拷贝构造和赋值之后,原来的auto_ptr就不能再使用了,这是坑,他没有任何防范措施,理论上是可以使用的,但是一旦使用就会出错!auto_ptr不能用在容器中,容器的拷贝构造会涉及到每一个元素的拷贝构造和赋值,最后还是涉及到auto_ptr的拷贝构造和赋值!
  • 不带引用计数的还有unique_ptr,我们推荐的,unique_ptr去掉了左值的拷贝构造和赋值重载,公布出了右值的拷贝构造和赋值重载,好处是…
  • 带引用计数:如果想要多个智能指针管理一个资源,使用引用计数,shared_ptr,底层使用CAS操作来保证引用计数加减的原子操作,可以直接使用在多线程环境下!

经典问题:有了shared_ptr为什么还要weak_ptr,强弱智能指针?

  • 从实践出发,如果只使用shared_ptr会出现循环引用问题,介绍自己实践的例子,怎么解决?

还介绍了shared_ptr和weak_ptr在解决多线程访问共享C++对象的线程安全的问题(课上讲的)

还有enable_shared_from_this和shared_from_this获取当前对象的一个智能指针;

  • shared_from_this:在当前对象的成员函数中如何获取当前对象的一个shared_ptr指针;
  • 不能通过当前对象的this指针直接穿件一个shared_ptr,会导致两个shared_ptr都认为只有自己持有了这个资源,最后释放资源的时候,会释放2次,出现问题!(大秦坑王blog)

继续延伸:make_shared

作用:为shared_ptr填坑;

shared_ptr的操作分为2步:

  • 构造shared_ptr会给它传入一个new的内存,shared_ptr底层还会new一个控制资源的控制块,也相当于一个内存块,上面记录了资源的地址 和资源的引用计数;
  • 现在假设管理的外部资源new成功了,底层的控制块new失败了,怎么办?存shared_ptr本身就没有创建起来,最后资源就不会帮我们自动释放,智能指针本身的创建有问题;
  • C++11以后给我们提供了make_shared,获取一个指向资源的shared_ptr,非常安全的!

C++14以后提供了make_unique,补不带引用计数的unique_ptr的坑的!

16、设计:10万个正整数,取值范围0到100w,互不重复,给定一个数字判断是否在这些数中出现过?

面试参考的问题:

  • 1、大数据求topK的问题;

  • 2、查重的问题;(真没有思路,哈希表!增删查的时间复杂度都是O(1))

哈希表的缺点:

  • 空间换时间,占用内存大!
  • 常见的链式哈希表,发生哈希冲突的元素放在一个桶中,一个节点表示一个元素,链表串起来,一个节点包含 该节点元素 + 下一个节点的地址!假设存储的是int类型,10万个元素就需要40万个字节 + 下一个节点的地址40万个字节,总共80万个子节;

查重优化的话:
1、位图法

  • 一个位记录一个数字,1个字节8个位,1个字节可以记录8个数字;
  • 位图法非常节省内存,只能判断数字是否出现过;
  • 是用数组来记录的,必须先知道,10万个整数最大的位是多少;需要根据最大的数组来定义位图数组的大小!

2、布隆过滤器;

  • 存在一定误判;
  • 布隆过滤器说 数字不在,那么数字一定不在;布隆过滤器说数字在,数字不一定在
  • 我们需要提前说明,这个问题允不允许一定数量的误判!
  • 布隆过滤器常用于redis的缓存中,比如在缓存中,命中key的话,允许一定的误判,提高身体缓存的效率!

17、两个有序数组取交集?代码层面优化?n个数组取交集?

1、哈希表;

  • 一个数组元素先构建哈希表,另外一个数组的元素在这个哈希表中查;
  • 问题:题目中的有序数组没有用到,不是最优的!
  • 时间复杂度: O(m)xO(1)

2、二分查找;

  • 遍历第一个数组的每一个元素,在第二个数组中进行二分查找;
  • 时间复杂度: O(mlogn)
  • 问题:还是没有将两个数组都有序全部用上,只用了一个数组是有序的!

3、双指针;

  • 时间复杂度: O(m) + O(n) = O(m + n)

总结对比:

  • 两个数组的个数相近(使用双指针); 比如都是10000个,二分查找时间O(10000*13)= O(130000),双指针O(20000);
  • 两个数组的个数相差很大(使用二分法); 一个10个,一个20000个;双指针O(10 + 20000),二分查找O(14*10);

n个数组取交集?

  • 1、划分成两两数组,一直做下去!
  • 2、n个指针进行处理!
原网站

版权声明
本文为[liufeng2023]所创,转载请带上原文链接,感谢
https://blog.csdn.net/Edward_LF/article/details/125577243