面经
大纲
简历
c++ socket开源库 :Asio
socket
服务器向客户端发送消息:websocket方式
WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
服务器端函数:
(1)socket创建一个套接字
(2)bind绑定ip和port
(3)listen使套接字变为可以被动链接
(4)accept等待客户端的链接
(5)write/read接收发送数据
(6)close关闭连接
客户端函数:
(1)创建一个socket,用函数socket()
(2)bind绑定ip和port
(3)连接服务器,用函数connect()
(4)收发数据,用函数send()和recv(),或read()和write()
(5)close关闭连接
本地进程间通信
- 消息传递(管道、消息队列、FIFO)
- 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
- 共享内存(匿名的和具名的,eg:channel)
- 远程过程调用(RPC)
Socket通信的数据传输方式,常用的有两种
- SOCK_STREAM:表示面向连接的数据传输方式。
- SOCK_DGRAM:表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,效率比 SOCK_STREAM 高。
socket阻塞与非阻塞
阻塞调用是指在调用结果返回前,当前线程会被挂起,调用线程只有在得到调用结果之后才会返回。
非阻塞调用是指在不能立刻得到结果之前,该调用不会阻塞当前线程
socket()函数创建套接字时,默认的套接字都是阻塞的
tcp连接中,突然拔掉网线,在很短的时间内插上,tcp连接不会中断
epoll
多路复用技术的Epoll,其核心结构是红黑树 + 双向链表。
epoll是 linux内核为处理⼤批量文件描述符 ⽽作了改进的poll,是Linux下多路复⽤ 接⼝select/poll的增强版本,它能显著提⾼程序在⼤量并发连接 中只有少量活跃的情况下的系统cpu利⽤率。
epoll之所以高效,是因为epoll将用户关心的文件描述符放到内核里的一个事件列表中,而不是像select/poll每次调用都需要重复传入文件描述符集或事件集(大量拷贝开销),比如一个事件发生,epoll无需遍历整个被监听的描述符集,而只需要遍历哪些被内核IO事件异步唤醒而加入就绪队列的描述符集合即可
epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。
ET模式(边缘触发)只有数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致epoll_wait返回;
LT 模式(水平触发,默认)只要有数据都会触发,缓冲区剩余未读尽的数据会导致epoll_wait返回。
epoll反应堆模型的流程:
epoll_create(); // 创建监听红黑树
epoll_ctl(); //向epoll对象的红黑树添加事件,返回0为标识成功,-1为失败
epoll_wait(); // 判断双向链表是否为空,空则阻塞。
首先通过epoll_create()系统调用在内核中创建一个eventpoll类型的句柄,其中包括红黑树根节点和双向链表头节点。然后通过epoll_ctl()系统调用,向epoll对象的红黑树结构中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。最后通过epoll_wait()系统调用判断双向链表是否为空,如果为空则阻塞。当文件描述符状态改变,fd上的回调函数被调用,该函数将fd加入到双向链表中,此时epoll_wait函数被唤醒,返回就绪好的事件。
参数传递都是字符串
说说select、poll、epoll的区别
select所能监控的描述符数量是有限的,默认是1024个,poll和epoll对监控的描述符数量没有限制;select和poll采用的是轮旋遍历,会随着监控的描述符的增多,性能也会随之降低,而epoll监控采用的是异步阻塞,不会随着描述符的增多导致性能下降;select的跨平台移植比poll和epoll好;select和poll每次监控调用返回后需要程序员遍历判断哪些描述符是否就绪,而epoll会直接向程序员返回就绪的文件描述符,可以直接对描述符进行操作;epoll支持水平触发和边缘触发
数据库
redis
redis是单线程的,数据结构简单,使用多路复用IO模型,非阻塞IO
Redis底层数据结构有六种
- 简单动态字符串
- 链表
- 字典
- 跳跃表
- 整数集合
- 压缩列表
- 快速列表
redis的优点和缺点
优点:
- 读写性能优异
- 支持数据持久化,支持AOF和RDB两种持久化方式;
- 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性操作
- 数据结构丰富
- 支持主从复制
缺点:
- 数据库容量收到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上;
- Redis不具备自动容错和恢复功能,主机从机宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复;
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性;
- Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。 ####.redis中的string类型和c++string类型有什么区别 因为c++的string类在初始化时并不会对字符串进行预分配,所以会导致在一定的情况下,string类的分配操 作会比redis中string的分配操作更多。
Redis里面的String用的什么数据结构
SDS (Simple Dynamic String,简单动态字符串)是 Redis 底层所使用的字符串表示, 几乎所有的 Redis 模块中都用了 SDS。
Redis分布式锁咋实现的?
分布式锁作用:在面临多个服务去访问一个公共资源时,是要保证服务层面的 同步安全性
可以使用SETNX命令,即如果key不存在,才会设置它的值,否则什么也不做。两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。
Redis跳跃表
Redis的跳跃表实现由 zskiplist和 zskiplistnode两个结构组成,其中 zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistnode则用于表示跳跃表节点;
项目中已经使用redis存储库存了,为什么redis减库存之后还要操作mysql减库存
Mysql相比于redis持久性更强。将库存放到缓存,利用redis的incrby特性来扣减库存,解决了超扣和性能问题
noSQL
NoSQL是非关系型数据库,NoSQL = Not Only SQL。
什么是NoSQL数据库?在哪些情况下使用和不使用NoSQL数据库?
关系型数据库采用的结构化的数据,NoSQL采用的是键值对的方式存储数据。两者是不同的数据存储结构。
在考虑数据库的成熟度;支持;分析和商业智能;管理及专业性等问题时,应优先考虑关系型数据库。 在处理非结构化/半结构化的大数据时;在水平方向上进行扩展时;随时应对动态增加的数据项时可以优先考虑使用NoSQL数据库。
关系型数据库与非关系型数据库的区别:数据存储结构的不同。
C++
拷⻉构造函数
一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象
拷⻉构造函数调⽤时机
- 以值的⽅式传⼊函数参数
- 以值的⽅式返回参数
- ⼀个对象对另⼀个对象进⾏初始化的时候
不写拷⻉构造函数的时候,C++会⽣成⼀个默认拷⻉构造函数 ⽣成的构造函数是浅拷⻉
浅拷⻉和深拷⻉
浅拷⻉:只是指针的拷⻉,两个指针指的是相同的地址
深拷⻉:是值的拷⻉,新⽣成⼀块地址并指向它,地址中的值相同
浅拷⻉的问题:
- 改变⼀个值,另⼀个也会变
- 析构的时候会析构两次
面向对象的四个基本特性:
多态,继承,封装,抽象
封装:将抽象得到的数据和行为相结合形成一个有机的整体
绑定:确定操作的具体对象的过程
C++ 多态是什么?怎么实现的?
多态是在不同继承关系的类对象,去调同⼀函数,产⽣了不同的⾏为。
C++ 多态意味着调⽤成员函数时,会根据调⽤函数的对象的类型来执⾏不同的函数。
静态多态:编译期间的多态,通过重载和模板实现
动态多态:在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法,通过继承和虚基类实现
1.通过基类类型的引⽤或者指针调⽤虚函数
2.必须是虚函数
虚函数,纯虚函数
虚函数:在基类中冠以关键字virtual的成员函数。它提供了一种接口界面,允许在派生类中对基类的虚函数重新定义。基类指针或引⽤⼦类,会调⽤⼦类重写的函数。
纯虚函数:没有函数体的虚函数。=0,强制重写
什么场景需要用到纯虚函数?
提供一个接口的时候使用纯虚函数
虚函数的实现与虚表
每一个包含了虚函数的类都包含一个虚表,无论虚函数是继承而来或是本身。
- 相比于普通类而言,虚函数的类中会多一个vptr指针,该指针即指向一个虚函数表,虚表中存放了函数的实际地址。虚表在编译阶段构造。
- 同一个类的所有对象使用同一个虚表。当一个类被创建时,会自动设置一个指向类的虚表。
构造函数可以是虚函数吗?析构函数可以是虚函数吗
- 构造函数不可以是虚函数
- 析构函数常常是虚函数
基类的析构函数可以调用虚函数吗?基类的构造函数可以调用虚函数吗?
可以,但是最好不要在构造和析构函数中调用
虚析构函数的作用
虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的.
接口类,抽象类
有纯虚函数的类为接口类(抽象类)
抽象类不能被实例化
类的继承方式:
公有继承,私有继承(默认),保护继承
构造函数和析构函数的调用顺序
假设存在类继承,组合对象的情况
- 调用基类构造函数,调用顺序按照它们被继承时声明的顺序
- 对派生类新增成员初始化,初始化顺序按照它们在类中声明的顺序
- 执行派生类的构造函数体中的内容
而析构函数的执行次序和构造函数正好严格相反
代码重用的方法
1,类继承 2.模板 3.组合对象
重载和重写
重载:同一范围中声明几个功能类似的同名函数,但这些同名函数的形参必须不同
重写:指子类重新定义父类虚函数的方法。
1.范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。
2.参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。
3.virtual的区别:重写的基类函数必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。
c++派生新类的过程:
吸收基类成员,改造基类成员,添加新成员
派生类的成员分为
从基类继承的成员,自己定义的新成员
引用
引用就是某个目标变量的别名,对引用的操作与对变量直接操作效果完全相同
将“引用”作为函数参数有哪些特点:
1)传递引用给函数与传递指针的效果是一样的。
2)使用引用传递函数的参数,在内存中并没有产生实参的副本。
3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调用函数中同样要给形参分配存储单元,且需要重复使用“*指针变量名”的形式进行运算
常引用:
既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变。
const 1)定义常变量 2)修饰成员函数,表示不可以修改成员变量的值
引用与指针的区别
1) 引用必须被初始化,指针不必
2) 引用初始化以后不能被改变,指针可以改变所指的对象
3) 不存在指向空值的引用,但是存在指向空值的指针
数组与指针的区别
- 数组要么在静态存储区被创建,要么在栈上被创建。指针可以随时指向任意类型的内存块。
- sizeof数组是求数组中全部元素占内存空间的大小;而sizeof指针求的是指针的大小,不是4个字节就是8个字节
- 当使用数组作为函数参数时会退化为指针
堆与栈的区别
栈是由高地址向低地址扩展,而堆是由低地址向高地址扩展
1) 栈由系统自动分配,而堆是人为申请开辟
2) 栈获得的空间小,而堆获得的空间大
3) 栈由系统自动分配,速度较快,堆一般速度较慢
4) 栈和堆存储的内容不同
5) 栈是连续的空间,堆是不连续的空间
数组、list、map、vector区别
① vector 底层数组
(连续的空间存储,可以使用[]操作符)快速的访问随机的元素,快速的在末尾插入元素,但是在序列中间岁间的插入,删除元素要慢,而且如果一开始分配的空间不够的话,有一个重新分配更大空间,然后拷贝的性能开销。
② deque 底层双向队列
(小片的连续,小片间用链表相连,实际上内部有一个map的指针,因为知道类型,所以还是可以使用[],只是速度没有vector快)快速的访问随机的元素,快速的在开始和末尾插入元素,随机的插入,删除元素要慢,空间的重新分配要比vector快,重新分配空间后,原有的元素不需要拷贝。对deque的排序操作,可将deque先复制到vector,排序后在复制回deque。
③ list 底层双向链表
(每个元素间用链表相连)访问随机元素不如vector快,随机的插入元素比vector快,对每个元素分配空间,所以不存在空间不够,重新分配的情况
④ set 底层红黑树
内部元素唯一,用一棵平衡树结构来存储,因此遍历的时候就排序了,查找也比较快的哦。
⑤ map 底层红黑树
一对一的映射的结合,key不能重复。
⑥ stack 底层deque作为容器
适配器,必须结合其他的容器使用,stl中默认的内部容器是deque。先进后出,只有一个出口,不允许遍历。
⑦ queue 底层deque作为容器
是受限制的deque,内部容器一般使用list较简单。先进先出,不允许遍历。
选择顺序容器类型的一些准则 (vector\deque\list)
1.如果我们需要随机访问一个容器则vector要比list好得多 。
2.如果我们已知要存储元素的个数则vector 又是一个比list好的选择。
3.如果我们需要的不只是在容器两端插入和删除元素则list显然要比vector好 。
4.除非我们需要在容器首部插入和删除元素否则vector要比deque好。
5.如果只在容易的首部和尾部插入数据元素,则选择deque。
6.如果只需要在读取输入时在容器的中间位置插入元素,然后需要随机访问元素,则可考虑输入时将元素读入到一个List容器,接着对此容器重新拍学,使其适合顺序访问,然后将排序后的list容器复制到一个vector容器中。
map和unordered_map有什么区别?分别在什么场景下使用?
map: map内部实现了一个红黑树,时复O(logn),适用于有顺序要求的
unordered_map内部实现了一个哈希表,时复O(1),对应查找问题
vector中resize和reserve的区别是什么?
reserve只修改capacity大小,不修改size大小,resize既修改capacity大小,也修改size大小。
链表和数组的区别
1、链表是链式存储结构,数组是顺序存储结构
2、链表通过指针连接元素与元素,而数组则是把所有元素按顺序进行存储
3、链表的插入和删除元素比较简单,不需要移动元素,且较为容易实现长度的扩充,但是查询元素比较困难,数组是查询比较快,但是删除和增加会比较麻烦。
static
1.隐藏:当同时编译多个⽂件时,所有未加static前缀的全局变量和函数都具有全局可⻅性。如果加了static,就会对其它源⽂件隐藏。
2.保持变量内容的持久:存储在静态数据区的变量会在程序刚开始运⾏时就完成初始化,也是唯⼀的⼀次初始化。如果作为static局部变量在函数内定义,它的⽣存期为 整个源程序,但是其作⽤域仍与⾃动变量相同
3.默认初始化为0
4.在类中,static 可以⽤来修饰静态数据成员和静态成员⽅法。
静态数据成员可以实现多个对象之间的数据共享
静态成员函数和静态数据成员⼀样,他们都属于类的静态成员,它仅能访问类的静态数据和静态成员函数。
extern 关键字
extern可以置于变量或者函数前,以标示变量或者函数的定义在别的⽂件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外extern也可⽤来进⾏链接指定。
inline函数
在c/c++中,为了解决⼀些频繁调⽤的⼩函数⼤量消耗栈空间(栈内存)的问题,特别的引⼊了inline修饰符,表示为内联函数。 内联是以代码膨胀(复制)为代价,仅仅省去了函数调⽤的开销,从⽽提⾼函数的执⾏效率。
malloc和new的区别
new/delete是c++关键字,需要编译器支持,
malloc/free是库函数,需要头文件支持
(1)new分配内存空间无需指定分配内存大小,malloc需要;
(2)new返回类型指针,类型安全,malloc返回void*,再强制转换成所需要的类型;
(3)new是从自由存储区获得内存,malloc从堆中获取内存;
(4)对于类对象,new会调用构造函数和析构函数,malloc不会(核心)。
malloc的内存可以用delete释放吗?new出来的内存用free释放呢?
都不可以
new[]和delete[]一定要配对使用吗?为什么
- 当类型为int, float等内置类型时,new、delete、new[]、delete[]不需要配对使用;
- 当是自定义类型时,new、delete和new[]、delete[]才需要配对使用。
c++ 的类型转换
定义了四种转换
- const_cast 即将const转换为非const(也可以反过来),const_cast只能用于指针或引用
- static_cast ⽆条件转换,静态类型转换,可以实现C++中内置基本数据类型之间的相互转换,enum、struct、 int、char、float等
- dynamic_cast 有条件转换,动态类型转换,用于将基类的指针或引用安全地转换成派生类的指针或引用(转换失败返回NULL)
- reinterpret_cast 仅重新解释类型,但没有进⾏⼆进制的转换
内存对⻬问题
成员变量在类中的内存存储并不⼀定是连续的,为了更快的读取,是内存对⻬的。(数据的首地址的值是某个数k(通常它为4或8)的倍数)
内存管理的注意事项
-
栈区:由编译器⾃动分配释放,存放为函数运⾏的局部变量,函数参数,返回数据,返回地址等。操作⽅式与数据结构中的类似。
-
堆区:⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配⽅式类似于链表
-
全局数据区:也叫做静态区,存放全局变量,静态数据。程序结束后由系统释放
-
⽂字常量区:可以理解为常量区,常量字符串存放这⾥。程序结束后由系统释放
-
程序代码区:存放函数体的⼆进制代码。但是代码段中也分为代码段和数据段。
1).new了之后delete
2)⼦类析构函数要重写,⽗类析构函数要是虚函数
内存泄漏,定位和处理
- 内存泄漏在使⽤完毕后未释放
- 专⻔⽤来检测内存泄漏的⼯具
(1) 内存分配未成功,却使⽤了它。
(2) 内存分配虽然成功,但是尚未初始化就引⽤它。
(3) 内存分配成功并且已经初始化,但操作越过了内存的边界。
(4) 忘记了释放内存,造成内存泄露。
(5) 释放了内存却继续使⽤它。
避免内存泄漏
智能指针或RAII思想
智能指针
- auto_ptr、unique_ptr、shared_ptr、weak_ptr
- 智能指针主要⽤于管理在堆上分配的内存,它将普通的指针封装为⼀个栈对象。当栈对象的⽣存周期结束后,会在析构函数中释放掉申请的内存,从⽽防⽌内存泄漏。
(1)shared_ptr ,多个共享指针可以指向相同的对象,采用了引用计数的机制,当最后一个引用销毁时,释放内存空间;
(2)unique_ptr,保证同一时间段内只有一个智能指针能指向该对象(可通过move操作来传递unique_ptr);
(3)weak_ptr,用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
请你回答智能指针存在内存泄漏的情况
当两个对象互相使用一个shared_ptr成员变量指向对方时,会造成循环引用,使引用计数器失效,从而造成内存泄漏。为了解决这个问题,引入了weak_ptr弱指针,weak_ptr不会修改引用计数器的值,也不会对对象的内存进行管理
C++中四种智能指针的实现原理
auto_ptr、unique_ptr和shared_ptr都是通过RAII的思想来实现的
了解RAII吗?介绍一下?
RAII利用了C++语言局部对象自动销毁的特性来控制资源的生命周期
整个RAII过程
- a.设计一个类封装资源
- b.在构造函数中初始化
- c.在析构函数中执行销毁操作
- d.使用时声明一个该对象的类
struct和class有什么区别?
1.默认的继承访问权。class默认的是private,strcut默认的是public。
2.默认访问权限:struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
3.“class”这个关键字还用于定义模板参数,就像“typename”。但关建字“struct”不用于定义模板参数
如果虚函数是有效的,那为什么不把所有函数设为虚函数?
虚函数会引入虚表和虚指针的开销
c++ 类的大小怎么计算?
- (一)类内部的成员变量:
- 普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。
- static修饰的静态变量:不占用内容,原因是编译器将其放在全局变量区。
- (二)类内部的成员函数:
- 普通函数:不占用内存。
- 虚函数:要占用4个字节,用来指定虚函数的虚拟函数表的入口地址。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系的
volatile关键字的作用
跟编译器优化有关,告诉编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的备份。
了解左值引用和右值引用吗?
- 左值,就是在内存有确定存储地址、有变量名,表达式结束依然存在的值
- 右值,就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值
- 左值引用,就是绑定到左值的引用,通过&来获得左值引用。
- 右值引用,就是绑定到右值的引用,通过&&来获得右值引用。
区别
- (1)左值引用绑定到有确定存储空间以及变量名的对象上,表达式结束后对象依然存在;右值引用绑定到要求转换的表达式、字面常量、返回右值的表达式等临时对象上,赋值表达式结束后就对象就会被销毁。
- (2)左值引用后可以利用别名修改左值对象;右值引用绑定的值不能修改。
引入右值引用的原因
- 替代需要销毁对象的拷贝,提高效率
- 移动含有不能共享资源的类对象
enum 和 enum class有什么区别
enum 称为不限定范围的枚举, enum class 称之为限定范围的枚举。
stl平时都使用什么标准库?可以简单介绍一些嘛?
容器 deque、list、vector、map
迭代器 代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。
string的常用函数
- a)string s; //生成一个空字符串s
- b) string s(str) //拷贝构造函数 生成str的复制品
- c) string s(str,stridx) //将字符串str内"始于位置stridx"的部分当作字符串的初值
- d) string s(str,stridx,strlen) //将字符串str内"始于stridx且长度顶多strlen"的部分作为字符串的初值
- e) string s(cstr) //将C字符串作为s的初值
动态链接库和静态链接库的区别,exe是静态链接还是动态链接
1.时期:
- 静态库在编译时连接,在链接时拷贝
- 动态库在运行时连接
2.资源
- 静态库在每次使用时将全部连接进可执行程序,浪费资源。
- 动态库在使用时访问动态库中函数,节省资源。
3.更新升级
- 静态库更新,则每个使用该静态库的程序都需要更新,不易于更新升级
- 动态库仅更新自身,易于更新升级
4.包含其他库
- 静态链接库不能再包含其他动态链接库
- 动态链接库可以包含其他动态链接库
exe是静态,dll是动态
数据结构
常见排序
快排空间复杂度O(logn)