C++面试梳理
C++基础
四种类型转换
static_cast:用于良性转换,一般不会导致意外发生,风险很低。常用于基本类型转换到 void,转换父类指针到子类不安全;
const_cast:一般用于去掉const属性以及volatile,但是如果原来他就是常量去掉之后千万不要修改;比如你手里有一个常量指针引用,但是函数接口是非常量指针,可能需要转换一下;成员函数声明为const,你想用this去执行一个函数,也需要用const_cast;
dynamic_cast:用于在类的继承层次之间进行类型转换,它既允许向上转型(Upcasting),也允许向下转型(Downcasting)。向上转型是无条件的,不会进行任何检测,所以都能成功;向下转型的前提必须是安全的,要借助 RTTI 进行检测,所有只有一部分能成功;
reinterpret_cast:非常简单粗暴,所以风险很高,一般用于跨类型的转换,如int到void*,用于序列化网络包数据等。
C++的内存管理
在C++中,内存被分成五个区:栈、堆、自由存储区、静态存储区、常量区
栈:存放函数的参数和局部变量,编译器自动分配和释放
堆:new关键字动态分配的内存,由程序员手动进行释放,否则程序结束后,由操作系统自动进行回收
自由存储区:由malloc分配的内存,和堆十分相似,由对应的free进行释放
全局/静态存储区:存放全局变量和静态变量
常量区:存放常量,不允许被修改
extern“C”作用
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。也就是C和C++代码的编译逻辑是不一样的,并且C中的全局变量也需要被其他引用的文件识别。
static 的作用域
某一个文件中定义的静态的变量,则它的作用域是整个文件。与之相对的的是extern!
在.h文件中定义了一个静态的全局变量x,不同文件为每个包含该头文件的cpp都创建一个全局变量,但他们都是独立的,只在该cpp文件共享该变量。所以一般定义static全局变量时,都把它放在cpp文件中而不是头文件,从而避免多个源文件共享,从而造成不必要的信息污染。
enum枚举类型和#define宏定义的区别
- 宏定义没有类型检查和安全检查,所以会导致边际效应,出现不可预知的错误;enum在编译阶段进行类型检查,但是只能进行整型的定义;
- 宏定义仅仅是替换和展开,并不进行内存的分配;enum常量存储在内存数据段的静态存储区;
- define是在预处理阶段对所定义的常量进行替换展开;enum是程序运行时起作用;
- 枚举常量具有类型,但宏没有类型;
栈溢出的原因以及解决方法
- 函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈;
- 局部变量体积太大。解决办法大致说来也有两种: 增加栈内存的数目;增加栈内存方法如下,在vc6种依次选择Project->Setting->Link,在Category中选择output,在Reserve中输入16进制的栈内存大小如:0x10000000;使用堆内存;具体实现由很多种方法可以直接把数组定义改成指针,然后动态申请内存;也可以把局部变量变成全局变量,一个偷懒的办法是直接在定义前边加个static,呵呵,直接变成静态变量(实质就是全局变量)
什么是内存泄漏?面对内存泄漏和指针越界,有哪些方法
动态分配内存所开辟的空间,在使用完毕后未释放,导致一直占据该内存,即为内存泄漏。
避免内存泄漏方法:
- 谁开辟谁释放
- 使用智能指针
- 指针指向新地址时参考规则1
- new和delete、new[]和delete[]要对应
C++中const的作用以及和宏的区别
const修饰的内容不可改变, 定义常量只是一种使用方式,还有const数据成员,const参数, const返回值, const成员函数等, 被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
const相对#define可以做类型校验,并且分配内存。
既然C++中有更好的const为什么还要使用宏
宏的作用发生在预编译,const是再编译阶段,所以const无法代替宏作为卫哨来防止文件的重复包含。
对多态的理解
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
多态是指一个接口多种实现,这是面向对象编程的核心,多态分为以下两种:
静态多态:也就是编译时多态,主要实现方式为重载、模板
动态多态:也就是运行时多态,主要实现方式为虚函数 + 继承
浅拷贝和深拷贝的区别
深拷贝和浅拷贝区别是,在有指针的情况下,浅拷贝只是增加了一个指针指向已经存在的内存,而深拷贝就是增加一个指针并且申请一个新的内存,使这个增加的指针指向这个新的内存,采用深拷贝的情况下,释放内存的时候就不会出现在浅拷贝时重复释放同一内存的错误。
C++ 模板
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector <int> 或 vector <string>。
STL由什么组成
由容器,迭代器,仿函数,算法,分配器,适配器构成。分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配置器用来套接适配仿函数。
STL迭代器的实现原理
迭代器(iterator)是一种抽象的设计理念,通过迭代器可以在不了解容器内部原理的情况下遍历容器。除此之外,STL中迭代器一个最重要的作用就是作为容器(vector,list等)与STL算法的粘结剂,只要容器提供迭代器的接口,同一套算法代码可以利用在完全不同的容器中,这是抽象思想的经典应用。
C++中的容器了解多少
一个容器是特定类型对象的集合,在C++标准库中包含了大部分常见的容器。STL 是“Standard Template Library”的缩写,中文译为“标准模板库”。STL 是 C++ 标准库的一部分,不用单独安装。TSL核心包括3个组件。容器(containers),算法(algorithms),迭代器(iterators)。除此外还有仿函数,内存配置器和配接器。
- 序列式容器(Sequence containers):此为可序群集,其中每个元素均有固定位置—取决于插入时机和地点,和元素值无关。如果你以追加方式对一个群集插入六个元素,它们的排列次序将和插入次序一致。STL提供了三个序列式容器:向量(vector)、双端队列(deque)、列表(list),此外你也可以把 string 和 array 当做一种序列式容器。
- 关联式容器(Associative containers):此为已序群集,元素位置取决于特定的排序准则以及元素值,和插入次序无关。如果你将六个元素置入这样的群集中,它们的位置取决于元素值,和插入次序无关。STL提供了四个关联式容器:集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)。
对于容器,主要的操作有:容器的建立、插入元素、删除元素、查询、遍历、计算元素个数、检查元素是否为空、输出容器包含的内容。
指针和引用的区别?
指针是一个实体,需要分配内存空间。引用只是变量的别名,不需要分配内存空间。
引用在定义的时候必须进行初始化,并且不能够改变。指针在定义的时候不一定要初始化,并且指向的空间可变。(注:不能有引用的值不能为NULL)
有多级指针,但是没有多级引用,只能有一级引用。
指针和引用的自增运算结果不一样。(指针是指向下一个空间,引用时引用的变量值加1)
sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小。
引用访问一个变量是直接访问,而指针访问一个变量是间接访问。
使用指针前最好做类型检查,防止野指针的出现;
引用底层是通过指针实现的;
作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。
new/delete 与 malloc/free的区别
malloc与free是C语言的标准库函数, new/delete是C++的运算符, 他们都可以用来申请和释放内存, malloc和free不在编译器控制权限之内, 无法调用构造函数和析构函数;而new/delete是由C++编译器控制的,可以在分配内存之外调用构造函数和析构函数。
new和malloc的区别
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持(stdlib);
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
new和delete的实现原理, delete是如何知道释放内存的大小的
new简单类型直接调用operator new分配内存;而对于复杂结构,先调用operator new分配内存,然后在分配的内存上调用构造函数;对于简单类型,new[]计算好大小后调用operator new;对于复杂数据结构,new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;
new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;
编译器运行相应的构造函数以构造这些对象,并为其传入初始值;
对象被分配了空间并构造完成,返回一个指向该对象的指针。
delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。
需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。
解释下封装、继承和多态
封装:
封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
继承:
继承主要实现重用代码,节省开发时间。子类可以继承父类的一些东西。
多态:
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。
计算机网络
TCP 和 UDP 的区别
TCP:传输控制协议,全称 Transmission Control Protocol ,是面向连接、可靠的、基于字节流的传输层协议。
UDP:支持无连接的一个传输协议,全称用户数据报协议(User Datagram Protocol)。UDP为应用程序提供了一种无需建立连接就可发送封装的数据包的方法。它不提供复杂的机制,只是利用IP来提供面向无连接的一种通信协议。
- TCP是面向连接的,通过三次握手建立连接,四次挥手解除连接;而UDP是面向无连接的,它发送数据是不需要建立连接的,这样大大的提高了它的传输效率,但是不能确保数据是否完整的传输。
- TCP是一种可靠的通信方式,TCP通过超时重传、确认应答、拥塞控制等机制来确保数据无差错、不丢包、不重复且有序;而UDP由于是无连接的,它会以最大的传输效率进行数据的传输,但不能保证数据传输的可靠交付,所以就会出现数据的丢失、重复等问题。
- TCP首部开销大占20个字节,而UDP的首部才占8个字节,开销小
- TCP协议提供可靠的、面向连接的传输服务,一般用于文件的传输、邮件的发送以及远程设备的控制;而UDP无需建立连接,传输效率高,不需要接收任何确认回复,可以用于即时的通信,例如QQ或WeChat的语言、视频通话以及抖音、斗鱼等平台的直播
- TCP因需要建立连接所以消耗资源大、而UDP不需要建立连接所以消耗资源小
- 每一条TCP连接只能是点到点的;而UDP不建立连接,所以可以支持一对一,一对多,多对一和多对多的交互通信,也就是可以同时接受多个人的包。
OSI七层模型的简单介绍
网络模型不是一开始就有的,在网络刚发展时,网络协议是由各互联网公司自己定义的,各家的协议也是不能互通的。这样大大的阻碍了互联网的发展,为了解决这个问题,国际标准化组织 1984 提出的模型标准,简称 OSI(Open Systems Interconnection Model)。具体如下图:
OSI七层模型每一层都有自己的作用,从上到下的作用依次为:
- 应用层(Application) :提供网络与用户应用软件之间的接口服务
- 表示层(Presentation) :提供格式化的表示和转换数据服务,如加密和压缩
- 会话层(Session) 提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制
- 传输层(Transimission):提供建立、维护和取消传输连接功能,负责可靠地传输数据(PC)
- 网络层(Network): 处理网络间路由,确保数据及时传送(路由器)
- 数据链路层(DataLink): 负责无错传输数据,确认帧、发错重传等(交换机)
- 物理层(Physics) :提供机械、电气、功能和过程特性(网卡、网线、双绞线、同轴电缆、中继器)
七层中应用层、表示层和会话层由软件控制,传输层、网络层和数据链路层由操作系统控制,物理层有物理设备控制。
TCP/IP参考模型及协议
模型:TCP/IP 模型是由 OSI 模型演化而来,TCP/IP 模型将 OSI 模型由七层简化为五层(一开始为四层),应用层、表示层、会话层统一为应用层。
<img src="https://cdn.jsdelivr.net/gh/xzMhehe/StaticFile_CDN/static/img/20210718152150.png" style="zoom:50%;" />
协议:TCP/IP协议被称为传输控制协议/互联网协议,又称网络通讯协议(Transmission Control Protocol)。是由网络层的IP协议和传输层的TCP协议组成,是一个很大的协议集合。
物理层和数据链路层
没有定义任何特定协议,支持所有的标准和专用的协议
。网络层
定义了网络互联
也就是IP协议
,主要包括IP
、ARP
、RARP
、ICMP
、IGMP
。传输层
定义了TCP
和UDP
(User Datagram Protocol),我们会后面重点介绍一下TCP协议。应用层
定义了HTTP
(超文本传输协议)、FTP
(文件传输协议)、DNS
(域名系统)等协议。
搜索baidu,会用到计算机网络中的什么层?每层是干什么的
浏览器中输入URL浏览器要将URL解析为IP地址,解析域名就要用到DNS协议,首先主机会查询DNS的缓存,如果没有就给本地DNS发送查询请求。DNS查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的DNS服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。
得到IP地址后,浏览器就要与服务器建立一个http连接。因此要用到http协议,http协议报文格式上面已经提到。http生成一个get请求报文,将该报文传给TCP层处理,所以还会用到TCP协议。如果采用https还会使用https协议先对http数据进行加密。TCP层如果有需要先将HTTP数据包分片,分片依据路径MTU和MSS。TCP的数据包然后会发送给IP层,用到IP协议。IP层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如PPP,SLIP),以太网协议需要直到目的IP地址的物理地址,有需要ARP协议。
其中:
1、DNS协议,http协议,https协议属于应用层
应用层是体系结构中的最高层。应用层确定进程之间通信的性质以满足用户的需要。这里的进程就是指正在运行的程序。应用层不仅要提供应用进程所需要的信息交换和远地操作,而且还要作为互相作用的应用进程的用户代理,来完成一些为进行语义上有意义的信息交换所必须的功能。应用层直接为用户的应用进程提供服务。
2、TCP/UDP属于传输层
传输层的任务就是负责主机中两个进程之间的通信。因特网的传输层可使用两种不同协议:即面向连接的传输控制协议TCP,和无连接的用户数据报协议UDP。面向连接的服务能够提供可靠的交付,但无连接服务则不保证提供可靠的交付,它只是“尽最大努力交付”。这两种服务方式都很有用,备有其优缺点。在分组交换网内的各个交换结点机都没有传输层。
3、IP协议,ARP协议属于网络层
网络层负责为分组交换网上的不同主机提供通信。在发送数据时,网络层将运输层产生的报文段或用户数据报封装成分组或包进行传送。在TCP/IP体系中,分组也叫作IP数据报,或简称为数据报。网络层的另一个任务就是要选择合适的路由,使源主机运输层所传下来的分组能够交付到目的主机。
4、数据链路层
当发送数据时,数据链路层的任务是将在网络层交下来的IP数据报组装成帧,在两个相邻结点间的链路上传送以帧为单位的数据。每一帧包括数据和必要的控制信息(如同步信息、地址信息、差错控制、以及流量控制信息等)。控制信息使接收端能够知道—个帧从哪个比特开始和到哪个比特结束。控制信息还使接收端能够检测到所收到的帧中有无差错。
5、物理层
物理层的任务就是透明地传送比特流。在物理层上所传数据的单位是比特。传递信息所利用的一些物理媒体,如双绞线、同轴电缆、光缆等,并不在物理层之内而是在物理层的下面。因此也有人把物理媒体当做第0层。
请你说一说TCP拥塞控制?以及达到什么情况的时候开始减慢增长的速度?
拥塞控制是防止过多的数据注入网络,使得网络中的路由器或者链路过载。流量控制是点对点的通信量控制,而拥塞控制是全局的网络流量整体性的控制。发送双方都有一个拥塞窗口——cwnd。
1、慢开始
最开始发送方的拥塞窗口为1,由小到大逐渐增大发送窗口和拥塞窗口。每经过一个传输轮次,拥塞窗口cwnd加倍。当cwnd超过慢开始门限,则使用拥塞避免算法,避免cwnd增长过大。
2、拥塞避免
每经过一个往返时间RTT,cwnd就增长1。
在慢开始和拥塞避免的过程中,一旦发现网络拥塞,就把慢开始门限设为当前值的一半,并且重新设置cwnd为1,重新慢启动。(乘法减小,加法增大)
3、快重传
接收方每次收到一个失序的报文段后就立即发出重复确认,发送方只要连续收到三个重复确认就立即重传(尽早重传未被确认的报文段)。
4、快恢复
当发送方连续收到了三个重复确认,就乘法减半(慢开始门限减半),将当前的cwnd设置为慢开始门限,并且采用拥塞避免算法(连续收到了三个重复请求,说明当前网络可能没有拥塞)。
采用快恢复算法时,慢开始只在建立连接和网络超时才使用。
达到什么情况的时候开始减慢增长的速度?
采用慢开始和拥塞避免算法的时候
- 一旦cwnd>慢开始门限,就采用拥塞避免算法,减慢增长速度
- 一旦出现丢包的情况,就重新进行慢开始,减慢增长速度
采用快恢复和快重传算法的时候
- 一旦cwnd>慢开始门限,就采用拥塞避免算法,减慢增长速度
- 一旦发送方连续收到了三个重复确认,就采用拥塞避免算法,减慢增长速度
为什么连接的时候是三次握手
刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_Send 状态。
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 established 状态。
服务器收到 ACK 报文之后,也处于 established 状态,此时,双方以建立起了链接。
三次握手的作用
三次握手的作用也是有好多的,多记住几个,保证不亏。例如:
1、确认双方的接受能力、发送能力是否正常。
2、指定自己的初始化序列号,为后面的可靠传送做准备。
关闭的时候却是四次挥手
刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:
第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。
第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态。
服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
TCP ,UDP协议属于哪一层
传输层。
HTTP协议属于哪一层?IP协议属于哪一层
应用层;网络层。
操作系统
进程和线程的区别
- 进程是资源分配的最小单位,而线程是 CPU 调度的最小单位;
- 创建进程或撤销进程,系统都要为之分配或回收资源,操作系统开销远大于创建或撤销线程时的开销;
- 不同进程地址空间相互独立,同一进程内的线程共享同一地址空间。一个进程的线程在另一个进程内是不可见的;
- 进程间不会相互影响,而一个线程挂掉将可能导致整个进程挂掉;
并发和并行有什么区别
- 并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
- 并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。
进程间通信方式有哪些?
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC 的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams 等。其中 Socket 和 Streams 支持不同主机上的两个进程 IPC。
管道
- 它是半双工的,具有固定的读端和写端;
- 它只能用于父子进程或者兄弟进程之间的进程的通信;
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
命名管道
- FIFO 可以在无关的进程之间交换数据,与无名管道不同;
- FIFO 有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
消息队列
- 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符 ID 来标识;
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
信号量
- 信号量(semaphore)是一个计数器。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据;
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存;
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作;
- 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;
- 支持信号量组。
共享内存
- 共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区;
- 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
什么是死锁?死锁产生的条件?
死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 如下图所示:如果此时有一个线程 A,已经持有了锁 A,但是试图获取锁 B,线程 B 持有锁 B,而试图获取锁 A,这种情况下就会产生死锁。
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何解决线程死锁?
如果发生了死锁,那么只要破坏死锁 4 个必要条件之一中的任何一个,死锁问题就能被解决。
如果想要打破互斥条件,需要允许进程同时访问某些资源,这种方法受制于实际场景,不太容易实现条件;
打破不可抢占条件,这样需要允许进程强行从占有者那里夺取某些资源,或者简单一点理解,占有资源的进程不能再申请占有其他资源,必须释放手上的资源之后才能发起申请,这个其实也很难找到适用场景;
进程在运行前申请得到所有的资源,否则该进程不能进入准备执行状态。这个方法看似有点用处,但是它的缺点是可能导致资源利用率和进程并发性降低;
避免出现资源申请环路,即对资源事先分类编号,按号分配。这种方式可以有效提高资源的利用率和系统吞吐量,但是增加了系统开销,增大了进程对资源的占用时间。
如果我们在死锁检查时发现了死锁情况,那么就要努力消除死锁,使系统从死锁状态中恢复过来。消除死锁的几种方式:
最简单、最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程;
撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素;
进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的
算法
快速排序的实现原理
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
// // Created by mxz on 2022/12/19. // #include<iostream> using namespace std; /** * 严蔚敏《数据结构》标准分割函数 * * @param A * @param low * @param high * @return */ int Paritition1(int A[], int low, int high) { int pivot = A[low]; while (low < high) { while (low < high && A[high] >= pivot) { --high; } A[low] = A[high]; while (low < high && A[low] <= pivot) { ++low; } A[high] = A[low]; } A[low] = pivot; return low; } void QuickSort(int A[], int low, int high) //快排母函数 { if (low < high) { int pivot = Paritition1(A, low, high); QuickSort(A, low, pivot - 1); QuickSort(A, pivot + 1, high); } } int main() { int A[] = {2, 5, 1, 3, 4, 10}; QuickSort(A, 0, sizeof(A) / sizeof(A[0])); for (int num: A) { cout << num << " "; } return 0; }
四种类型转换
static_cast:用于良性转换,一般不会导致意外发生,风险很低。常用于基本类型转换到 void,转换父类指针到子类不安全;
const_cast:一般用于去掉const属性以及volatile,但是如果原来他就是常量去掉之后千万不要修改;比如你手里有一个常量指针引用,但是函数接口是非常量指针,可能需要转换一下;成员函数声明为const,你想用this去执行一个函数,也需要用const_cast;
dynamic_cast:用于在类的继承层次之间进行类型转换,它既允许向上转型(Upcasting),也允许向下转型(Downcasting)。向上转型是无条件的,不会进行任何检测,所以都能成功;向下转型的前提必须是安全的,要借助 RTTI 进行检测,所有只有一部分能成功;
reinterpret_cast:非常简单粗暴,所以风险很高,一般用于跨类型的转换,如int到void*,用于序列化网络包数据等。
C++的内存管理
在C++中,内存被分成五个区:栈、堆、自由存储区、静态存储区、常量区
栈:存放函数的参数和局部变量,编译器自动分配和释放
堆:new关键字动态分配的内存,由程序员手动进行释放,否则程序结束后,由操作系统自动进行回收
自由存储区:由malloc分配的内存,和堆十分相似,由对应的free进行释放
全局/静态存储区:存放全局变量和静态变量
常量区:存放常量,不允许被修改
extern“C”作用
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。也就是C和C++代码的编译逻辑是不一样的,并且C中的全局变量也需要被其他引用的文件识别。
static 的作用域
某一个文件中定义的静态的变量,则它的作用域是整个文件。与之相对的的是extern!
在.h文件中定义了一个静态的全局变量x,不同文件为每个包含该头文件的cpp都创建一个全局变量,但他们都是独立的,只在该cpp文件共享该变量。所以一般定义static全局变量时,都把它放在cpp文件中而不是头文件,从而避免多个源文件共享,从而造成不必要的信息污染。
enum枚举类型和#define宏定义的区别
- 宏定义没有类型检查和安全检查,所以会导致边际效应,出现不可预知的错误;enum在编译阶段进行类型检查,但是只能进行整型的定义;
- 宏定义仅仅是替换和展开,并不进行内存的分配;enum常量存储在内存数据段的静态存储区;
- define是在预处理阶段对所定义的常量进行替换展开;enum是程序运行时起作用;
- 枚举常量具有类型,但宏没有类型;
栈溢出的原因以及解决方法
- 函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈;
- 局部变量体积太大。解决办法大致说来也有两种:增加栈内存的数目;增加栈内存方法如下,在vc6种依次选择Project->Setting->Link,在Category中选择output,在Reserve中输入16进制的栈内存大小如:0x10000000;使用堆内存;具体实现由很多种方法可以直接把数组定义改成指针,然后动态申请内存;也可以把局部变量变成全局变量,一个偷懒的办法是直接在定义前边加个static,呵呵,直接变成静态变量(实质就是全局变量)
什么是内存泄漏?面对内存泄漏和指针越界,有哪些方法
动态分配内存所开辟的空间,在使用完毕后未释放,导致一直占据该内存,即为内存泄漏。
避免内存泄漏方法:
- 谁开辟谁释放
- 使用智能指针
- 指针指向新地址时参考规则1
- new和delete、new[]和delete[]要对应
C++中const的作用以及和宏的区别
const修饰的内容不可改变, 定义常量只是一种使用方式,还有const数据成员,const参数, const返回值, const成员函数等, 被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
const相对#define可以做类型校验,并且分配内存。
既然C++中有更好的const为什么还要使用宏
宏的作用发生在预编译,const是再编译阶段,所以const无法代替宏作为卫哨来防止文件的重复包含。
对多态的理解
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。 C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
多态是指一个接口多种实现,这是面向对象编程的核心,多态分为以下两种:
静态多态:也就是编译时多态,主要实现方式为重载、模板
动态多态:也就是运行时多态,主要实现方式为虚函数 + 继承
浅拷贝和深拷贝的区别
深拷贝和浅拷贝区别是,在有指针的情况下,浅拷贝只是增加了一个指针指向已经存在的内存,而深拷贝就是增加一个指针并且申请一个新的内存, 使这个增加的指针指向这个新的内存,采用深拷贝的情况下,释放内存的时候就不会出现在浅拷贝时重复释放同一内存的错误。
C++ 模板
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。 模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。 每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector <int> 或 vector <string>。
STL由什么组成
由容器,迭代器,仿函数,算法,分配器,适配器构成。分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配置器用来套接适配仿函数。
STL迭代器的实现原理
迭代器(iterator)是一种抽象的设计理念,通过迭代器可以在不了解容器内部原理的情况下遍历容器。除此之外,STL中迭代器一个最重要的作用就是作为容器(vector,list等)与STL算法的粘结剂,只要容器提供迭代器的接口,同一套算法代码可以利用在完全不同的容器中,这是抽象思想的经典应用。
C++中的容器了解多少
一个容器是特定类型对象的集合,在C++标准库中包含了大部分常见的容器。STL 是“Standard Template Library”的缩写,中文译为“标准模板库”。STL 是 C++ 标准库的一部分,不用单独安装。TSL核心包括3个组件。容器(containers),算法(algorithms),迭代器(iterators)。除此外还有仿函数,内存配置器和配接器。
- 序列式容器(Sequence containers):此为可序群集,其中每个元素均有固定位置—取决于插入时机和地点,和元素值无关。如果你以追加方式对一个群集插入六个元素,它们的排列次序将和插入次序一致。STL提供了三个序列式容器:向量(vector)、双端队列(deque)、列表(list),此外你也可以把 string 和 array 当做一种序列式容器。
- 关联式容器(Associative containers):此为已序群集,元素位置取决于特定的排序准则以及元素值,和插入次序无关。如果你将六个元素置入这样的群集中,它们的位置取决于元素值,和插入次序无关。STL提供了四个关联式容器:集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)。
对于容器,主要的操作有:容器的建立、插入元素、删除元素、查询、遍历、计算元素个数、检查元素是否为空、输出容器包含的内容。
指针和引用的区别?
指针是一个实体,需要分配内存空间。引用只是变量的别名,不需要分配内存空间。
引用在定义的时候必须进行初始化,并且不能够改变。指针在定义的时候不一定要初始化,并且指向的空间可变。(注:不能有引用的值不能为NULL)
有多级指针,但是没有多级引用,只能有一级引用。
指针和引用的自增运算结果不一样。(指针是指向下一个空间,引用时引用的变量值加1)
sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小。
引用访问一个变量是直接访问,而指针访问一个变量是间接访问。
使用指针前最好做类型检查,防止野指针的出现;
引用底层是通过指针实现的;
作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。
new/delete 与 malloc/free的区别
malloc与free是C语言的标准库函数, new/delete是C++的运算符, 他们都可以用来申请和释放内存, malloc和free不在编译器控制权限之内, 无法调用构造函数和析构函数;而new/delete是由C++编译器控制的,可以在分配内存之外调用构造函数和析构函数。
new和malloc的区别
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持(stdlib);
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
new和delete的实现原理, delete是如何知道释放内存的大小的
new简单类型直接调用operator new分配内存;而对于复杂结构,先调用operator new分配内存,然后在分配的内存上调用构造函数;对于简单类型,new[]计算好大小后调用operator new;对于复杂数据结构,new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;
new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;
编译器运行相应的构造函数以构造这些对象,并为其传入初始值;
对象被分配了空间并构造完成,返回一个指向该对象的指针。
delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。
需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。
解释下封装、继承和多态
封装:
封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
继承:
继承主要实现重用代码,节省开发时间。子类可以继承父类的一些东西。
多态:
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。
计算机网络
TCP 和 UDP 的区别
TCP:传输控制协议,全称 Transmission Control Protocol ,是面向连接、可靠的、基于字节流的传输层协议。
UDP:支持无连接的一个传输协议,全称用户数据报协议(User Datagram Protocol)。UDP为应用程序提供了一种无需建立连接就可发送封装的数据包的方法。它不提供复杂的机制,只是利用IP来提供面向无连接的一种通信协议。
- TCP是面向连接的,通过三次握手建立连接,四次挥手解除连接;而UDP是面向无连接的,它发送数据是不需要建立连接的,这样大大的提高了它的传输效率,但是不能确保数据是否完整的传输。
- TCP是一种可靠的通信方式,TCP通过超时重传、确认应答、拥塞控制等机制来确保数据无差错、不丢包、不重复且有序;而UDP由于是无连接的,它会以最大的传输效率进行数据的传输,但不能保证数据传输的可靠交付,所以就会出现数据的丢失、重复等问题。
- TCP首部开销大占20个字节,而UDP的首部才占8个字节,开销小
- TCP协议提供可靠的、面向连接的传输服务,一般用于文件的传输、邮件的发送以及远程设备的控制;而UDP无需建立连接,传输效率高,不需要接收任何确认回复,可以用于即时的通信,例如QQ或WeChat的语言、视频通话以及抖音、斗鱼等平台的直播
- TCP因需要建立连接所以消耗资源大、而UDP不需要建立连接所以消耗资源小
- 每一条TCP连接只能是点到点的;而UDP不建立连接,所以可以支持一对一,一对多,多对一和多对多的交互通信,也就是可以同时接受多个人的包。
OSI七层模型的简单介绍
网络模型不是一开始就有的,在网络刚发展时,网络协议是由各互联网公司自己定义的,各家的协议也是不能互通的。这样大大的阻碍了互联网的发展,为了解决这个问题,国际标准化组织 1984 提出的模型标准,简称 OSI(Open Systems Interconnection Model)。具体如下图:
OSI七层模型每一层都有自己的作用,从上到下的作用依次为:
- 应用层(Application) :提供网络与用户应用软件之间的接口服务
- 表示层(Presentation) :提供格式化的表示和转换数据服务,如加密和压缩
- 会话层(Session) 提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制
- 传输层(Transimission):提供建立、维护和取消传输连接功能,负责可靠地传输数据(PC)
- 网络层(Network): 处理网络间路由,确保数据及时传送(路由器)
- 数据链路层(DataLink): 负责无错传输数据,确认帧、发错重传等(交换机)
- 物理层(Physics) :提供机械、电气、功能和过程特性(网卡、网线、双绞线、同轴电缆、中继器)
七层中应用层、表示层和会话层由软件控制,传输层、网络层和数据链路层由操作系统控制,物理层有物理设备控制。
TCP/IP参考模型及协议
模型:TCP/IP 模型是由 OSI 模型演化而来,TCP/IP 模型将 OSI 模型由七层简化为五层(一开始为四层),应用层、表示层、会话层统一为应用层。
协议:TCP/IP协议被称为传输控制协议/互联网协议,又称网络通讯协议(Transmission Control Protocol)。是由网络层的IP协议和传输层的TCP协议组成,是一个很大的协议集合。
物理层和数据链路层没有定义任何特定协议,支持所有的标准和专用的协议。 网络层定义了网络互联也就是IP协议,主要包括IP、ARP、RARP、ICMP、IGMP。 传输层定义了TCP和UDP(User Datagram Protocol),我们会后面重点介绍一下TCP协议。 应用层定义了HTTP(超文本传输协议)、FTP(文件传输协议)、DNS(域名系统)等协议。
搜索baidu,会用到计算机网络中的什么层?每层是干什么的
浏览器中输入URL 浏览器要将URL解析为IP地址,解析域名就要用到DNS协议,首先主机会查询DNS的缓存,如果没有就给本地DNS发送查询请求。DNS查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的DNS服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。
得到IP地址后,浏览器就要与服务器建立一个http连接。因此要用到http协议,http协议报文格式上面已经提到。http生成一个get请求报文,将该报文传给TCP层处理,所以还会用到TCP协议。如果采用https还会使用https协议先对http数据进行加密。TCP层如果有需要先将HTTP数据包分片,分片依据路径MTU和MSS。TCP的数据包然后会发送给IP层,用到IP协议。IP层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如PPP,SLIP),以太网协议需要直到目的IP地址的物理地址,有需要ARP协议。
其中:
1、DNS协议,http协议,https协议属于应用层
应用层是体系结构中的最高层。应用层确定进程之间通信的性质以满足用户的需要。这里的进程就是指正在运行的程序。应用层不仅要提供应用进程所需要的信息交换和远地操作,而且还要作为互相作用的应用进程的用户代理,来完成一些为进行语义上有意义的信息交换所必须的功能。应用层直接为用户的应用进程提供服务。
2、TCP/UDP属于传输层
传输层的任务就是负责主机中两个进程之间的通信。因特网的传输层可使用两种不同协议:即面向连接的传输控制协议TCP,和无连接的用户数据报协议UDP。面向连接的服务能够提供可靠的交付,但无连接服务则不保证提供可靠的交付,它只是“尽最大努力交付”。这两种服务方式都很有用,备有其优缺点。在分组交换网内的各个交换结点机都没有传输层。
3、IP协议,ARP协议属于网络层
网络层负责为分组交换网上的不同主机提供通信。在发送数据时,网络层将运输层产生的报文段或用户数据报封装成分组或包进行传送。在TCP/IP体系中,分组也叫作IP数据报,或简称为数据报。网络层的另一个任务就是要选择合适的路由,使源主机运输层所传下来的分组能够交付到目的主机。
4、数据链路层
当发送数据时,数据链路层的任务是将在网络层交下来的IP数据报组装成帧,在两个相邻结点间的链路上传送以帧为单位的数据。每一帧包括数据和必要的控制信息(如同步信息、地址信息、差错控制、以及流量控制信息等)。控制信息使接收端能够知道—个帧从哪个比特开始和到哪个比特结束。控制信息还使接收端能够检测到所收到的帧中有无差错。
5、物理层
物理层的任务就是透明地传送比特流。在物理层上所传数据的单位是比特。传递信息所利用的一些物理媒体,如双绞线、同轴电缆、光缆等,并不在物理层之内而是在物理层的下面。因此也有人把物理媒体当做第0层。
请你说一说TCP拥塞控制?以及达到什么情况的时候开始减慢增长的速度?
拥塞控制是防止过多的数据注入网络,使得网络中的路由器或者链路过载。流量控制是点对点的通信量控制,而拥塞控制是全局的网络流量整体性的控制。发送双方都有一个拥塞窗口——cwnd。
1、慢开始
最开始发送方的拥塞窗口为1,由小到大逐渐增大发送窗口和拥塞窗口。每经过一个传输轮次,拥塞窗口cwnd加倍。当cwnd超过慢开始门限,则使用拥塞避免算法,避免cwnd增长过大。
2、拥塞避免
每经过一个往返时间RTT,cwnd就增长1。
在慢开始和拥塞避免的过程中,一旦发现网络拥塞,就把慢开始门限设为当前值的一半,并且重新设置cwnd为1,重新慢启动。(乘法减小,加法增大)
3、快重传
接收方每次收到一个失序的报文段后就立即发出重复确认,发送方只要连续收到三个重复确认就立即重传(尽早重传未被确认的报文段)。
4、快恢复
当发送方连续收到了三个重复确认,就乘法减半(慢开始门限减半),将当前的cwnd设置为慢开始门限,并且采用拥塞避免算法(连续收到了三个重复请求,说明当前网络可能没有拥塞)。
采用快恢复算法时,慢开始只在建立连接和网络超时才使用。
达到什么情况的时候开始减慢增长的速度?
采用慢开始和拥塞避免算法的时候
- 一旦cwnd>慢开始门限,就采用拥塞避免算法,减慢增长速度
- 一旦出现丢包的情况,就重新进行慢开始,减慢增长速度
采用快恢复和快重传算法的时候
- 一旦cwnd>慢开始门限,就采用拥塞避免算法,减慢增长速度
- 一旦发送方连续收到了三个重复确认,就采用拥塞避免算法,减慢增长速度
为什么连接的时候是三次握手
刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_Send 状态。
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 established 状态。
服务器收到 ACK 报文之后,也处于 established 状态,此时,双方以建立起了链接。
三次握手的作用
三次握手的作用也是有好多的,多记住几个,保证不亏。例如:
1、确认双方的接受能力、发送能力是否正常。
2、指定自己的初始化序列号,为后面的可靠传送做准备。
关闭的时候却是四次挥手
刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:
第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。
第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态。
服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
TCP ,UDP协议属于哪一层
传输层。
HTTP协议属于哪一层?IP协议属于哪一层
应用层;网络层。
操作系统
进程和线程的区别
- 进程是资源分配的最小单位,而线程是 CPU 调度的最小单位;
- 创建进程或撤销进程,系统都要为之分配或回收资源,操作系统开销远大于创建或撤销线程时的开销;
- 不同进程地址空间相互独立,同一进程内的线程共享同一地址空间。一个进程的线程在另一个进程内是不可见的;
- 进程间不会相互影响,而一个线程挂掉将可能导致整个进程挂掉;
并发和并行有什么区别
- 并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
- 并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。
进程间通信方式有哪些?
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC 的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams 等。其中 Socket 和 Streams 支持不同主机上的两个进程 IPC。
管道
- 它是半双工的,具有固定的读端和写端;
- 它只能用于父子进程或者兄弟进程之间的进程的通信;
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
命名管道
- FIFO 可以在无关的进程之间交换数据,与无名管道不同;
- FIFO 有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
消息队列
- 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符 ID 来标识;
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
信号量
- 信号量(semaphore)是一个计数器。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据;
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存;
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作;
- 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;
- 支持信号量组。
共享内存
- 共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区;
- 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
什么是死锁?死锁产生的条件?
死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 如下图所示:如果此时有一个线程 A,已经持有了锁 A,但是试图获取锁 B,线程 B 持有锁 B,而试图获取锁 A,这种情况下就会产生死锁。
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何解决线程死锁?
如果发生了死锁,那么只要破坏死锁 4 个必要条件之一中的任何一个,死锁问题就能被解决。
如果想要打破互斥条件,需要允许进程同时访问某些资源,这种方法受制于实际场景,不太容易实现条件;
打破不可抢占条件,这样需要允许进程强行从占有者那里夺取某些资源,或者简单一点理解,占有资源的进程不能再申请占有其他资源,必须释放手上的资源之后才能发起申请,这个其实也很难找到适用场景;
进程在运行前申请得到所有的资源,否则该进程不能进入准备执行状态。这个方法看似有点用处,但是它的缺点是可能导致资源利用率和进程并发性降低;
避免出现资源申请环路,即对资源事先分类编号,按号分配。这种方式可以有效提高资源的利用率和系统吞吐量,但是增加了系统开销,增大了进程对资源的占用时间。
如果我们在死锁检查时发现了死锁情况,那么就要努力消除死锁,使系统从死锁状态中恢复过来。消除死锁的几种方式:
最简单、最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程;
撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素;
进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的
算法
快速排序的实现原理
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
//// Created by mxz on 2022/12/19.//#include<iostream>using namespace std;/** * 严蔚敏《数据结构》标准分割函数 * * @param A * @param low * @param high * @return */int Paritition1(int A[], int low, int high) { int pivot = A[low]; while (low < high) { while (low < high && A[high] >= pivot) { --high; } A[low] = A[high]; while (low < high && A[low] <= pivot) { ++low; } A[high] = A[low]; } A[low] = pivot; return low;}void QuickSort(int A[], int low, int high) //快排母函数{ if (low < high) { int pivot = Paritition1(A, low, high); QuickSort(A, low, pivot - 1); QuickSort(A, pivot + 1, high); }}int main() { int A[] = {2, 5, 1, 3, 4, 10}; QuickSort(A, 0, sizeof(A) / sizeof(A[0])); for (int num: A) { cout << num << " "; } return 0;}