计算机基础面经积累---持续更新

1、gcc,g++,gdb常用命令
首先了解gcc,g++的区别。要先知道我们写的源代码是如何被编译器运行的。大概有四个阶段:
预处理:处理宏定义等宏命令,删除空格等,生成后缀为“.i”的文件  
编译:将预处理后的文件转换成汇编语言,生成后缀为“.s”的文件
汇编:由汇编生成的文件翻译为二进制目标文,生成后缀为“.o”的文件
连接:多个目标文件(二进制)结合库函数等综合成的能直接独立执行的执行文件——生成后缀为“.out”的文件(exe文件)。
gcc无法进行库文件的连接;而g++则能完整编译出可执行文件。前三个阶段g++也是调用gcc实现的。
简单来说gcc是C语言编译器,g++是C++语言编译器。但是事实上,二者都可以编译c或cpp文件。区别在于,对于 .c和.cpp文件,gcc分别当做c和cpp文件编译,g++则统一当做cpp文件编译。
gdb调试是一个功能强大的命令调试程序,也就是一个debug的工具。
安装gdb: sudo apt install gdb
生成可执行代码:gcc test.c -g -o test或者g++ -g test.cpp -o test(注意添加-g参数,才可以调试)
进入gdb调试:gdb ./test

**list:**查看源代码,默认显示10行,可以修改。后面可以加文件名:行号,显示指定文件的以那一行为中心的附近的代码。这个很重要,因为我们需要观察源代码来了解程序逻辑,知道在哪里设置断点等。

插入断点:b 行号(或者函数入口处) 显示断点:info break 删除断点:delete 断点号
知道如何使用断点很重要,这样我们才知道程序中变量的值是如何变化的。
watch 可以用来监视一个变量或者一段内存,当这个变量或者该内存处的值发生变化时,GDB 就会中断下来。被监视的某个变量或者某个内存地址会产生一个 watch point(观察点)。这用于我们想要观察一个变量是否改变,如果一句句地调试太慢了。
关于调试还有**setp[n]和next[n]**两个但不调试命令,二者都代表每隔n行就自动断点,但是前者遇到函数会进入函数体执行,后者则会把函数执行完出来,整个函数体当成是一行。
知道了断点之后,我们可以用run命令运行程序,在断点处会中断,我们进行查看相应的变量的操作,再用continue或者go继续向下执行。

接下来很重要的一部分就是如何查看变量的值、地址、寄存器的值等操作。
查看变量的值**:print/p**. p 变量名,就可以查看变量的值。p/a按十六进制显示,p/c按字符显示等。也可以用display来显示变量的值,display在每次断点都会自动显示,不需要每次都p了,更方便。

查看内存地址的值:x/ n代表往后显示几个地址的内容,f代表显示的格式,比如十六进制十进制浮点数等,如果址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。u代表指定的字节作为一个值,默认是4个字节。

查看寄存器的值**:info registers**查看所有寄存器的情况。也可以使用print命令来访问寄存器的情况,只需要在寄存器名字前加一个$符号就可以了。如:p $eip。寄存器中可能存放了下一条指令的地址、函数返回地址、堆栈地址等,很重要。

**set args [arguments]:**重新指定被调试程序的命令行参数。show args显示被调试程序的命令行参数。这对于程序参数的相关调试有用。
GDB调试多线程看面试场景题积累汇总那一篇文章。

2、http和https
HTTP,超文本传输协议,规定了浏览器和服务器之间数据传输的规则。HTTP 是应用层协议,它以 TCP(传输层)作为底层协议,默认端口为 80。
http的通信过程:服务器在80端口等待客户的请求;接着浏览器发起到服务器的TCP连接,服务器接受TCP连接,然后浏览器和服务器交换http消息,最后关闭TCP连接。
可以看出,http是基于tcp的。是可靠的,请求相应一一对应。
另外
HTTP协议是无状态协议:无状态指的是客户端发送HTTP请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息。这种特性有优点也有缺点,缺点是无法共享数据。无状态的,因此无法做连续多个步骤的操作。例如:加入购物出,结算,支付。每次都需要验证身份信息,但是无状态所以无法连续。解决办法就是利用会话技术(Cookie、Session)。优点就是速度快。
HTTPS 协议(Hyper Text Transfer Protocol Secure),是 HTTP 的加强安全版本。HTTPS 是基于 HTTP 的,也是用 TCP 作为底层协议,并额外使用 SSL/TLS 协议用作加密和安全认证。可以理解成HTTPS = HTTP + SSL/TLS。默认端口号是 443。SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。
SSL/TLS的核心要素是非对称加密。非对称加密采用两个密钥——一个公钥,一个私钥。在通信时,私钥仅由解密者保存,公钥由任何一个想与解密者通信的发送者(加密者)所知。
常用的非对称加密算法:RSA。
常见的对称加密算法有:DES、3DES和AES
在实际通信过程中,计算的代价较高,效率太低,因此,SSL/TLS 实际对消息的加密使用的是对称加密。

https的主要步骤:1、客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。2、Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端/。3、客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。4、客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。5、Web服务器利用自己的私钥解密出会话密钥。6、Web服务器利用会话密钥加密与客户端之间的通信。
结合了对称加密和非对称加密的方式。
数字签名实现完整性。数字证书CA实现身份认证。加密实现密文传输。
常见的状态码:
100-199 用于指定客户端应相应的某些动作。
200-299 用于表示请求成功。
300-399 用于已经移动的文件并且常被包含在定位头信息中指定新的地址信息。
400-499 用于指出客户端的错误。
500-599 用于支持服务器错误。

如果服务器收到头信息中带有100-continue的请求,这是指客户端询问是否可以在后续的请求中发送附件。
200 OK:客户端请求成功。
300表示被请求的文档可以在多个地方找到,并将在返回的文档中列出来。如果服务器有首选设置,首选项将会被列于定位响应头信息中。
301 状态是指所请求的文档在别的地方;文档新的URL会在定位响应头信息中给出。浏览器会自动连接到新的URL。
403 Forbidden:指的是服务器端有能力处理该请求,但是拒绝授权访问。404 Not Found:请求资源不存在,比如资源被删除了,或用户输入了错误的URL。
408 (Request Timeout/请求超时)
500 Internal Server Error:服务器发生不可预期的错误,一般是代码的BUG所导致的。500 (SC_INTERNAL_SERVER_ERROR) 是常用的“服务器错误”状态。该状态经常由CGI程序引起
502 (Bad Gateway/错误的网关)

3、get post put delete等区别
这些都是http定义的与服务器进行交互的不同方法。
从数据库角度来说,get相当于查询,从服务器查询相关数据; post是增加服务器的资源;delete是删除资源;put是修改服务器的资源。
还有其余的,head用来查询头部信息,不返回实体信息;trace用来回环诊断,回显服务器收到的请求,因为请求传递过程中会经过防火墙等,trace查看请求中途是否被更改;options允许客户端查看服务器性能;

由于put是幂等的,也就是如果两次请求相同,那么第二次会覆盖第一次,所以put用来更新资源;而post是非幂等的,不会覆盖,所以用来添加资源。

重点是get和post的区别和联系:
1.一个请求数据,一个上传数据;
2、安全性:get因为参数会放在url中,,get请求会保存在浏览器历史,可保存为书签,所以隐私性,安全性较差,post的数据在body中。
3、数据长度:由于get的请求放在url中,虽然http对url长度没有限制,但是服务器了浏览器有限制,一般在1024字节以内。因为长url解析消耗性能太大,并且容易收到恶意构造长url的攻击。post由于数据放在body中,所以没有限制。示例如下:

GET /updateInfo?name=Javanx&age=25 HTTP/1.1
Host: localhost

POST /updateInfo HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
name=Javanx&age=25

GET 和 POST 只是 HTTP 协议中两种请求方式(异曲同工),而 HTTP 协议是基于 TCP/IP 的应用层协议,无论 GET 还是 POST,用的都是同一个传输层协议,所以在传输上,没有区别。
但如果不按规范来也是可以的,可以在 URL 上写参数,然后方法使用 POST;也可以在 Body 写参数,然后方法使用 GET。当然,这需要服务端支持。
GET 方法参数写法是固定的吗?
在约定中,我们的参数是写在 ? 后面,用 & 分割。
我们知道,解析报文的过程是通过获取 TCP 数据,用正则等工具从数据中获取 Header 和 Body,从而提取参数。
POST 方法比 GET 方法安全?
有人说POST 比 GET 安全,因为数据在地址栏上不可见。

然而,从传输的角度来说,他们都是不安全的,因为 HTTP 在网络上是明文传输的,只要在网络节点上捉包,就能完整地获取数据报文。

要想安全传输,就只有加密,也就是 HTTPS。
POST 方法会产生两个 TCP 数据包?
有些文章中提到,post 会将 header 和 body 分开发送,先发送 header,服务端返回 100 状态码再发送 body。
HTTP 协议中没有明确说明 POST 会产生两个 TCP 数据包,而且实际测试(Chrome)发现,header 和 body 不会分开发送。
所以,header 和 body 分开发送是部分浏览器或框架的请求方法,不属于 post 必然行为。

4、TCP粘包问题
TCP粘包的本质原因是TCP是基于字节流的协议(为了可靠传输,即前后是有顺序的,和效率,每次都要确认序列号,发送读取的次数不一致,有缓冲区的),这说明数据之间是没有边界的,从TCP首部也能看出来,没有数据部分长度的字段。而UDP是不会产生粘包现象的,它发送的是独立的数据报,UDP首部有数据报长度字段,可以知道读取多少,发送读取的次数一致。
表面原因:
1、发送方采用Nagle算法就可能产生粘包:Nagle是拥塞控制算法,当ftp每次敲击一个字符,就会传一个,此时数据只有1字节,但是TCP/ip首部却达到40字节,浪费资源,造成拥塞。所以nagle算法做了两件事:一是延迟发送小数据,等到上一个确认才发送,二是收集多个小分组,在一个确认到来时一起发送。
2、接收方:如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。

解决方法:对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。
接收方没有办法来处理粘包现象,只能将问题交给应用层来处理,解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了;发送端将每个数据包封装为固定长度(不够的可以通过补0填充);可以在数据包之间设置边界,如添加特殊符号。

5、死锁问题
死锁就是,两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。比如很多进程需要以独占方式占用资源,这样进程间互相等待,无限期陷入僵持。
原因有:资源不足、资源分配不当、进程执行顺序不当等。
死锁的四个必要条件,即只有这四个条件成立,才可能发生死锁,不是肯定的。
互斥:一个资源只能同时被一个进程使用;
占有等待:进程资源得不到满足等待时,不释放已有的资源
不可剥夺:进程资源不能被别的进程抢占;
循环等待:每个进程都在等待链中等待下一个进程所持有的资源

死锁解决办法:
死锁防止:破坏四个必要条件之一即可。比如采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行。实现简单,但是严重的减低了资源利用率。剥夺调度能够防止死锁,但是只适用于内存和处理器资源。给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
总结,死锁防止方法能够防止发生死锁,但必然会降低系统并发性,导致低效的资源利用率。

死锁避免:典型的就是银行家算法。本质就是,每次分配的时候,保证系统处于安全状态,如果这次分配导致不安全状态,则不分配。安全状态就是可以找到一个执行序列,满足每个进程对资源的最大需求,顺利完成执行序列。
银行家算法就是有最大需求矩阵,已分配矩阵,还需要矩阵,可用资源矩阵。分配资源时,若分配大于所需要或分配大于可用资源,不分配;若分配后导致进入不安全状态,不分配。

死锁检测:当且仅当资源分配图不可完全简化,就是死锁。
死锁解除:抢占资源、终止进程。

6、浏览器从输入 URL 开始到页面显示内容,中间发生了什么?
1.进行DNS解析。如果有缓存(浏览器,操作系统,本地DNS服务器)直接找到对应的IP地址,否则,会分别向根域名服务器,顶级域名服务器…直到找到对应的,然后本地DNS服务器将这个IP送回发送端。其中客户机和本地DNS服务器之间的是递归查询,本地DNS服务器和其他DNS服务器之间的查询是迭代查询。
2、TCP三次握手建立连接;
3.开始发送 HTTP 请求报文。请求行包含请求方法、URL、协议版本;请求头包含请求的附加信息,由关键字/值对组成(Host,表示主机名,虚拟主机;Connection,HTTP/1.1 增加的,使用 keepalive,即持久连接,一个连接可以发多个请求;User-Agent,请求发出者,兼容性以及定制化需求。)请求体,可以承载多个请求参数的数据
4、如果有数据一切正常(状态码200),当浏览器拿到服务器的数据(html)之后,开始渲染页面同时获取HTML页面中图片、音频、视频、CSS、JS,在这期间获取到JS文件之后,会直接执行JS代码,阻塞浏览器渲染,因为渲染引擎和JS引擎互斥,不能同时工作,所以通常把Script标签放在body标签的底部。渲染过程就是先将HTML转换成dom树,再将CSS样式转换成stylesheet,根据dom树和stylesheet创建布局树,对布局树进行分层,为每个图层生成绘制列表,再将图层分成图块,紧接着光栅化将图块转换成位图,最后合成绘制生成页面。
5/当数据传送完毕,需要断开 tcp 连接,此时发起 tcp 四次挥手

7、进程间通信方式
1.管道:
匿名管道:两个特点:信息单向传输,只能在父子兄弟进程之间使用(因为没有显式的管道文件,只能fork复制父进程的fd)。
例子:ps auxf | grep mysql 这个就是打印出所有进程信息,通过管道送入右边,再获取其中mysql进程。
命名管道FIFO:可以在不相关的进程间传递信息。使用方法:先需要通过mkfifo命令来创建,并且指定管道名字:mkfifo myPipe
echo “hello” > myPipe // 将数据写进管道 cat < myPipe // 读取管道里的数据。可以看出,管道这种通信方式效率低, 不适合进程间频繁地交换数据。当然,它的好处就是简单。

2、消息队列:
A进程要给B进程发送消息,A进程把数据放在对应的消息队列后就可以正常返回了,B进程需要的时候再去读取数据就可以了。效率更高。
消息队列是内核中消息链表,发送消息是消息体,固定大小的存储块,所以克服了字节流效率低的特点。消息队列生命周期内核持续,不是随进程的。
缺点:消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理 另一进程 读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程

3、共享内存
消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去, 大大提高了进程间通信的速度。
缺点是:多人写的话,会产生覆盖问题,读没问题。

4、信号量(按理说不严格属于进程通信)
为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。控制信号量的方式有两种原子操作:一个是P操作,这个操作会把信号量减去1,另一个是V操作,这个操作会把信号量加上1。P操作是用在进入共享资源之前,V操作是用在离开共享资源之后,这两个操作是必须成对出现的。初始为0是同步信号量,初始为1是互斥信号量。

5、信号
信号一般用于一些异常情况下的进程间通信,是一种异步通信,它的数据结构一般就是一个数字。信号来源主要是键盘或者命令(比如kill)。crtrl+C就是产生 SIGINT 信号,表示终止该进程;资源管理器结束进程;kill -9 1050,表示给PID为1050的进程发送SIGKILL 信号,用来立即结束该进程

6、socket
socket就是常见的网络编程。跨网络与不同主机上的进程之间通信,就需要Socket通信。

8、程序、进程、线程、协程
进程是程序在某个数据集合上的一次运行活动,也是操作系统进行资源分配和保护的基本单位
所以程序是静态的,进程是动态的,有生命周期的。
进程的组成:PCB(进程描述符,进程控制管理信息比如阻塞挂起等,资源分配信息,CPU寄存器相关信息)、数据段、程序段。
进程三种基本状态:阻塞(等待某一事件,比如IO完成)、运行、就绪(缺少CPU,比如CPU时间用完)

线程:一个进程可以有很多线程,是轻量级的进程,不同的线程共享进程的资源(内存,IO等),没有自己的地址空间,但有自己的堆栈和局部变量。线程是处理器调度的基本单位,而进程是资源分配的基本单位。
比如浏览器是一个进程,其中的HTTP 请求线程、事件响应线程、渲染线程等等,所以线程并发程度比进程高,同时由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,需要较大的时空开销,而线程相对开销小从属于不同进程的线程间切换,它是会导致进程切换的,所以开销也大

缺点:一个线程崩溃会影响其他线程,所以健壮性不够

协程:协程是一个用户态的轻量级线程。线程是同步的,协程是异步的。
进程线程的痛点是涉及到线程阻塞状态和可运行状态之间的切换,同步锁,上下文切换。比如JDBC,数据库是最大性能瓶颈,因为是同步阻塞的,线程占用的CPU一直在空转。
协程切换:当出现IO阻塞的时候,协程调度切换时,将数据流立刻yield掉(主动让出),将寄存器上下文和栈保存到其它地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,且其他协程可以继续运行,不会使整个线程阻塞住;
第二是可以不加锁的访问全局变量,所以上下文的切换非常快,因为协程都是属于一个线程的,不存在同时写变量冲突。

临界资源:一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如打印机等。访问临界资源的那段代码称为临界区

9.智能指针问题
智能指针是一个,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。
使用new和delete运算符进行动态内存的管理虽然可以提高程序的效率,但是也非常容易出问题:忘记释放内存,造成内存泄漏,在尚有指针引用内存的情况下就将其释放,产生引用非法内存的指针,程序发生异常后进入catch忘记释放内存。智能指针是借用RAII技术对普通指针进行封装。RAII技术,也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。使用存储在栈上的局部对象(类)来封装资源的分配和初始化,在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理,可以保证正确的初始化和资源释放。 局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
C++11版本之后提供的智能指针包含在头文件中,分别是auto_ptr、shared_ptr、unique_ptr、weak_ptr
智能指针代码实现: 用两个类来实现智能指针的功能,一个是引用计数类,另一个则是指针类。

//  引用计数器类  用于存储指向同一对象的指针数
template<typename T>
class Counter
{
private:
	//  数据成员
	T *ptr;    //  对象指针
	int cnt;   //  引用计数器
 
	//  友元类声明
	template<typename T>
	friend class SmartPtr;
 
	//  成员函数
	//  构造函数
	Counter(T *p)   //  p为指向动态分配对象的指针
	{
		ptr = p;
		cnt = 1;
	}
	//  析构函数
	~Counter()
	{
		delete ptr;
	}
};
//  智能指针类  
template<typename T>
class SmartPtr
{
private:
	//  数据成员
	Counter<T> *ptr_cnt;  //  
 
public:
 
	//  成员函数
	//  普通构造函数  初始化计数类
	SmartPtr(T *p)
	{
		ptr_cnt = new Counter<T>(p);
	}
	//  拷贝构造函数 计数器加1
	SmartPtr(const SmartPtr &other)
	{
		ptr_cnt = other.ptr_cnt;
		ptr_cnt->cnt++;
	}
	//  赋值运算符重载函数
	SmartPtr &operator=(const SmartPtr &rhs)
	{
		ptr_cnt = rhs->ptr_cnt;
		rhs.ptr_cnt->cnt++; 增加右操作数的计数器
		ptr_cnt->cnt--; 左操作数计数器减1
		if (ptr_cnt->cnt == 0)
			delete ptr_cnt;
		return *this;
	}
	//  解引用运算符重载函数
	T &operator*()
	{
		return *(ptr_cnt->cnt);
	}
 
	//  析构函数
	~SmartPtr()
	{
		ptr_cnt->cnt--;
		if (ptr_cnt->cnt == 0)
			delete ptr_cnt;
		else
			cout << "还有" << ptr_cnt->cnt << "个指针指向基础对象" << endl;
	}
};

shared_ptr:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。
shared_ptr初始化:


std::shared_ptr<T> sp; //空shared_ptr,可以指向类型为T的对象
 
std::shared_ptr<int> sp(new int(5)); //指定类型,传入指针通过构造函数初始化
 
std::shared_ptr<int> sp = std::make_shared<int>(5); //使用make_shared函数初始化
 
//智能指针是一个模板类,不能将一个原始指针直接赋值给一个智能指针,因为一个是类,一个是指针
std::shared_ptr<int> sp = new int(1); //error

unique_ptr unique_ptr唯一拥有其所指的对象,在同一时刻只能有一个unique_ptr指向给定对象。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。
初始化:没有make_shared函数,只能通过new传入指针。

std::unique_ptr<T> up; //空unique_ptr,可以指向类型为T的对象,up会使用delete来释放它的指针
 
std::unique_ptr<int> up(new int(5)); //绑定动态对象

nique_ptr没有copy构造函数,不支持普通的拷贝和赋值操作;但却提供了一种移动机制来将指针的所有权从一个unique_ptr转移给另一个unique_ptr(使用std::move函数,也可以调用release或reset)

std::unique_ptr<int> upMove = std::move(up); //转移所有权

std::unique_ptr<int> up1(new int(5));
std::unique_ptr<int> up2(up1.release()); //up2被初始化为up1原来保存的指针,且up1置为空
std::unique_ptr<int> up3(new int(6));
up2.reset(up3.release()); //reset释放了up2原来指向的内存,指向up3原来保存的指针,且将up3置为空

unique_ptr适用范围比较广泛,它可返回函数内动态申请资源的所有权;可在容器中保存指针;支持动态数组的管理。

weak_ptr是一种弱引用指针,它是伴随shared_ptr而来的,不具有普通指针的行为 ,模板类中没有重载 * 和 -> 运算符,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。。它主要是解决了shared_ptr引用计数的问题:在循环引用时会导致内存泄漏的问题。
weak_ptr指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。
use_count() 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
expired() 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)
lock() 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针.
循环引用:A调用B,B调用A,这样初始化时A,B的计数为1,赋值时又加1,析构减1,最后还是1,资源没有释放。

智能指针线程安全吗?
结论:同一个shared_ptr被多线程读是安全的;同一个shared_ptr被多线程写不安全;共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的。
原因,shared_ptr其实由指向对象的指针和计数器组成,计数器加减操作是原子操作,所以这部分是线程安全的,但是指向对象的指针不是线程安全的。比如智能指针的赋值拷贝,首先拷贝指向对象的指针,再使引用次数加减操作,虽然引用次数加减是原子操作,但是指针拷贝和引用次数两步操作 并不是原子操作,线程不安全,需要手动加锁解锁。

10. 二叉搜索树、平衡二叉树、红黑树、B树、B+树
二叉搜索树:为了使查找的平均时间复杂度为logn,性质是左子树节点小于根,右子树节点大于根,同时左右子树也是二叉搜索树,是一个递归定义。特点是中序遍历的话是递增序列。
但是,如果本身是一个递增序列,那么构造的二叉搜索树就退化成链表了,查找复杂度变成O(n)了,所以才有平衡二叉搜索树。
AVL的特点是左右子树的高度差不超过1,左右子树也满足。把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间(为了保持平衡,插入删除元素都要调整二叉树)。
因此,才有了红黑树。
红黑树一些特点:节点是红色或黑色,根节点是黑色;红色节点的孩子是黑色节点;从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
虽然这么构造深意我也不理解,但是是为了从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的,但不是AVL限制那么严格。这么做原因是换来了保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。AVL调整旋转次数就不可预计了。
红黑树虽然减少了树的高度,但是当数据量很大时,树高度还是很高,不适合io级别的操作,更适合内存级别的应用。比如STL中哈希表unordered_map的底层实现就是红黑树,可以实现查找插入删除的复杂度都是logn级别。

那么,适用于io操作的是B树和B+树,适用于数据库的索引
B树就是一个多路的二叉搜索树,B树每个节点有键、数据、指针三部分。每个节点有m-1个元素和2-m个孩子节点,这样就大大减小树的高度,从而减小io操作次数。
B+树是B树的变种,B+树的非叶子结点只保存指针和键,数据保存在叶子结点。B+树在叶子结点添加了指向相邻叶子结点的指针。B+树由于非叶子节点没有数据域,所以能够携带更多的键,所以B+树的层数少,看起来更矮胖一点。那么查询时,B+树所进行的I/O次数更少;由于B+树在叶子结点增加了指向相邻叶子结点的指针,当进行区间查询时,只要沿着指针读取就可以,天然具备排序功能。而B树的索引字段大小相邻近的结点可能隔得很远。

11.深拷贝和浅拷贝
浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化;深拷贝是将对象及值复制过来,指向两个独立的地方,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝。
进一步思考,在对对象进行浅拷贝时,如果两个对象都析构函数的话,就会导致多次析构造成崩溃。
同时,在传参时,值传递就是深拷贝,比如vector a作为形参,那么函数里的a变化,不会导致函数外面的a变化,就会发现你的a没变化,因为函数退出时,局部的a已经销毁了。而且深拷贝复制对象时还浪费时间空间,开销很大。如果传vector& a,就是浅拷贝了,只复制了地址。

12.友元问题
由于类实现了封装,变量一般是私有的,需要通过成员函数访问。但是有一些函数需要频繁访问数据成员,就需要频繁的使用成员函数调用,效率较低,因为调用成本,类型检查等事项)
所以友元函数是放弃了一些封装性,提高效率,使其可以直接访问类的私有成员。
它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,放在私有共有都可以。而且一个函数可以是不同类的友元函数,只要在不同类声明就可以。
friend 类型 函数名(形式参数);
看起来像一个特权函数。

友元类: 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
同样,在类外定义一个类B。然后在类A内声明friend class B;就可以了。B的所有成员函数就可以直接访问A的私有或保护成员了。

注意:友元关系不能被继承。若C继承了B,那么C并不是A的友元类。友元关系是单向的,也不能传递。我想这些限制都是为了最大限度保持封装性,毕竟友元是特殊情况才声明的,权利不能滥用。

13、计算机系统开机的过程
第一阶段:BIOS,基本输入输出系统,控制硬件的一段代码,存放在ROM中,无法更改的,断电不消失。其中主要包含了自检程序、系统自动装载程序、IO驱动程序中断服务等。
1.1 BIOS程序首先检查,计算机硬件能否满足运行的基本条件,这叫做**”硬件自检”;**
1.2 **启动顺序:BIOS知道下一阶段的启动程序”具体存放在哪一个设备

第二阶段:主引导记录
BIOS按照”启动顺序”,把控制权转交给排在第一位的储存设备(U盘,硬盘等),然后读取启动设备的主引导记录,存放在最前面的512字节。它主要包含分区表,它的主要作用是,告诉计算机到硬盘的哪一个位置去找操作系统

第三阶段:**硬盘启动,**计算机的控制权就要转交给硬盘的某个分区

第四阶段,操作系统
控制权转交给操作系统后,操作系统的内核首先被载入内存。以Linux系统为例,内核加载成功后,第一个运行的程序是/sbin/init。它根据配置文件产生init进程。这是Linux启动后的第一个进程,pid进程编号为1,其他进程都是它的后代。
然后,i
nit线程加载系统的各个模块
,比如窗口程序和网络程序,直至执行/bin/login程序,跳出登录界面,等待用户输入用户名和密码。

14. cookie,session区别
http是无连接的,这次请求和上次请求没有关系。但是如果需要数据共享,追踪用户行为(购物车)就需要知道用户信息。
cookie是一种存储在浏览器的字段信息,只有4kb,以键值对形式存在。一般访问流程:
用户http请求;服务器在响应头加一个set-cookie选项,里面包含cookie。之后用户再发送http请求时就会把cookie信息加在请求部分里面,服务器就会进行相应的业务响应。
session和cookie看起来很像。但是我们要分清session概念和session实现。session就是一个会话,相当于给服务器和浏览器建立一个连接。那么session的实现就是利用cookie为载体的,服务器会为每个浏览器产生一个独一的session id来标识连接,之后每次请求只要把id放在请求头部就可以知道浏览器是谁了。而一些用户信息上下文信息都是存储在服务器中,请求时只需要复制id就可以。而这个id往往就是通过cookie实现的。也就是说cookie是实现session id传递的载体之一。如果浏览器禁用cookie的话,session id也可以通过url重写实现。
所以综合来说,cookie很具体,就是一种服务器生成,存储在浏览器上的小字段,实现鉴权的具体方式。而session是一种宽泛的会话技术,通过session id标识,为服务器浏览器建立连接。
区别总结:
1、cookie存储在浏览器中,所以安全性较差,容易被劫持伪造;session只有id在浏览器中,具体信息存储在服务器中,安全性好一点,但是会增加服务器的负担,而且多个服务器的话负载均衡后很有可能找不到了。比如自动保存的密码等。
2、cookie只有4kb大小,只能存ASCII字符,而session存储信息不限,类型也很多。

15. 谈一谈http长连接、短连接、超时
明确一个概念,http是无状态的应用层协议,没有长短连接一说,请求响应和下一次没有关系的。这里的长连接实际上是指TCP连接。
短连接就是这一次请求响应结束后,下一次请求响应就要重新建立TCP连接。Connection:close就是短连接
长连接就是TCP连接不关闭,下一次请求响应就不用再三次握手四次挥手了,减少资源消耗,比如一次请求 HTML,可能还需要请求后续的 JS/CSS/图片等。http1.0默认是短连接,1.1默认是长连接。
一般长连接就是在请求头设置:Connection:keep-alive 和Keep-Alive: timeout=60。有timeout说明60s后连接失效,没有的话说明连接一直有效。
对于点对点的,交互频繁的。比如数据库就是长连接。对于web网页这种,用户很多,一般用短连接。不然的话会有成千上万的连接同时保存在服务器端。
长连接还有一个额外的问题,就是队头阻塞。在请求应答过程中,如果出现任何状况,剩下所有的工作都会被阻塞在那次请求应答之后。
解决办法:现代浏览器会针对单个域名开启6个连接,通过各个连接分别发送请求。它实现了某种程度上的并行,但治标不治本。
HTTP/2是完全多路复用的,而非有序并阻塞的。http2在一个连接上处理多个请求,实现流式传输,将请求分片,每个请求都有自己的id标识,到达服务器后再组装起来。这样就实现多路复用,提高效率。

超时的情况:请求超时,比如现在网络超级不好,当客户端发起一个请求,通信层开始请求与服务器建立连接(包括在重试),如果在5S之内还没有连接到服务器,那么认为超时。
响应超时一个不正确的url,将会响应404。或者服务器响应速度太慢,连接已经关闭。

  1. Linux系统的目录结构及目录的主要功能
    我们可以用ls /命令来查看linux的目录。ls就是显示目录下的文件名。/就是根目录,显示根目录下的文件,目录也是文件。
    在这里插入图片描述

bin: cd bin;ls就可以看到bin目录下的文件。普通用户可以使用的必须的命令的存放目录。比如ls,pwd等等。是二进制文件。
sbin:超级root用户可以使用的系统管理的必须的命令的存放目录。普通用户无法执行。普通用户用$标识,root用#标识。
boot: 引导程序,内核等存放的目录。
dev :是 Device 的缩写, 该目录下存放的是 Linux 的外部设备,在 Linux 中访问设备的方式和访问文件的方式是相同的。设备文件可以使用mknod命令来创建。想要Linux系统支持某个设备,只要:相应的硬件设备,支持硬件的驱动模块,以及相应的设备文件。
/home:用户的主目录,在 Linux 中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的,保存了用户自己的配置文件,定制文件,文档,数据等。
/lost+found:看名字就知道是系统崩溃非法关机等文恢复的位置。一般是空的。
/mnt:系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在 /mnt/ 上,然后进入该目录就可以查看光驱里的内容了。
/proc: 是 Processes(进程) 的缩写,/proc 是一种伪文件系统,存储的是当前内核运行状态的一系列特殊文件,这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息
这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令,使别人无法ping你的机器。
/tmp:tmp 是 temporary(临时) 的缩写这个目录是用来存放一些临时文件的。实际上是内存中的,当关机重启后tmp清空了。
/var: 是 variable(变量) 的缩写,这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件、缓冲文件等。
/etc: 是 Etcetera(等等) 的缩写,这个目录用来存放所有的系统管理所需要的配置文件。例如,要配置系统开机的时候启动那些程序,配置某个程序启动的时候显示什么样的风格等等。通常这些配置文件都集中存放在/etc目录中,所以想要配置什么东西的话,可以在/etc下面寻找我们可能需要修改的文件。
/lib:lib 是 Library(库) 的缩写这个目录里存放着系统最基本的动态连接共享库,其作用类似于 Windows 里的 DLL 文件(system32目录)。几乎所有的应用程序都需要用到这些共享库。按理说,这里存放的文件应该是/bin目录下程序所需要的库文件的存放地。
/media:linux 系统会自动识别一些设备,例如U盘、光驱等等,当识别后,Linux 会把识别的设备挂载到这个目录下。比如插入U盘会在里面建一个disk目录,就能通过disk来访问u盘。
/opt:opt 是 optional(可选) 的缩写,这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。
/usr这个目录中包含了命令库文件和在通常操作中不会修改的文件。这个目录对于系统来说也是一个非常重要的目录,其地位类似Windows上面的”Program Files”目录
/usr/local这个目录存放的内容,一般都是我们后来自己安装的软件的默认路径
/usr/bin一般存放的只是对用户和系统来说“不是必需的”程序(二进制文件)。
/usr/sbin一般存放用于系统管理的系统管理的不是必需的程序(二进制文件)。

16. XSS攻击是什么?如何避免?
XSS攻击全称跨站脚本攻击(前端注入),注入攻击的本质,是把用户输入的数据当做前端代码执行(比如SQL注入)
。XSS拼接的是网页的HTML代码,一般而言我们是可以拼接出合适的HTML代码去执行恶意的JS语句。在渲染DOM树的时候,执行了不可预期的JS脚本,从而发生了安全问题。(信息泄露、未授权操作、弹窗关不掉等)
常见的 XSS 攻击有三种:反射型XSS攻击、DOM-based 型XXS攻击以及存储型XSS攻击。
反射型 XSS 一般是攻击者通过特定手法(如电子邮件),诱使用户去访问一个包含恶意代码的 URL,当受害者点击这些专门设计的链接的时候,恶意代码会直接在受害者主机上的浏览器执行。反射型XSS通常出现在网站的搜索栏、用户登录口等地方,常用来窃取客户端 Cookies 或进行钓鱼欺骗
例子:张三做了一个超链接发给阿伟,超链接地址为:http://www.xxx.com?content= 当阿伟点击这个链接的时候(假设他已经登录xxx.com),浏览器就会直接打开bbb.com,并且把xxx.com中的cookie信息发送到bbb.com。而bbb.com就是张三的非法网站,此时他已经得到了cookie信息。

存储型XSS也叫持久型XSS,主要将XSS代码提交存储在服务器端(数据库,内存,文件系统等),下次请求目标页面时不用再提交XSS代码。当目标用户访问该页面获取数据时,XSS代码会从服务器解析之后加载出来,返回到浏览器做正常的HTML和JS解析执行,XSS攻击就发生了。存储型 XSS 一般出现在网站留言、评论、博客日志等交互处,恶意脚本存储到客户端或者服务端的数据库中。
例子:张三在网站发布了文章,其中包含恶意代码:。
这样,只要你打开文章,就会丢失cookie信息,危害更大。

基于 DOM 的 XSS 攻击是指通过恶意脚本修改页面的 DOM 结构,是纯粹发生前端的攻击。属于前端 JavaScript 自身的安全漏洞。常见于类似JSON转换、翻译等工具区。

防御:1.对用户向服务器提交的信息(URL、关键字、HTTP头、POST数据等)进行检查,仅接受规定长度、适当格式、预期内容,其余的一律过滤
2.对输入内容的特定字符进行编码,例如表示 html标记的 < > 等符号。
3. 对重要的 cookie设置 httpOnly, 防止客户端通过document.cookie读取 cookie.
4. 确定接收到的内容被规范化,仅包含最小最安全的tag(不含JavaScript),去掉对任何远程内容的引用(尤其是样式表和JavaScript)

17.Cmake怎么用?为什么要用?
为什么要CMAKE工具呢?
我们如果直接把可执行代码给别人运行,可能由于平台不一样无法运行,怎么办呢?就要用make工具,实际上make工具有很多种,而生成的makefile文件形式也千差万别,所以,CMAKE出现,允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件。所以编译流程是先写 CMakeList.txt ,再cmake 路径生成makefile文件,再make一下生成可执行文件、动态库静态库等文件。实现写一次就可以运行在任何系统上。
CMakeList.txt 主要内容有

cmake_minimum_required(VERSION 3.0)  //最小版本
project(sylar) //工程名字

set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_FLAGS "$ENV{CXXFLAGS} -rdynamic -O3 -fPIC -ggdb -std=c++11 -Wall -Wno-deprecated -Werror -Wno-unused-function -Wno-builtin-macro-redefined -Wno-deprecated-declarations") #C++标准。比如-fPIC代表生成动态库,-O3优化级别,-ggdb可以用gdb调试

set(LIB_SRC
      sylar/log.cc
     ) # 设置要编译的源文件

add_library(sylar SHARED ${LIB_SRC})  #编译成动态库 把log源文件编译成动态库,这样就可以给test的main函数用
#add_library(sylar_static STATIC ${LIB_SRC})
#SET_TARGET_PROPERTIES (sylar_static PROPERTIES OUTPUT_NAME "sylar")

add_executable(test tests/test.cc)  #编译成可执行文件,将test源文件编译成可执行文件
add_dependencies(test sylar)



SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) #设置可执行文件的存储路径,一般就在bin目录下
SET(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib) #设置动态库文件的存储路径,一般在lib文件下

总结一下,就是设置要编译的源文件、要生成的动态库静态库,可执行文件,设置生成文件的存储位置等。

18.堆和栈的区别
首先,从内存管理角度看。在C++中,内存分为几个区:内核区域、栈、堆、全局/静态存储区(初始化和未初始化)、常量存储区。堆和栈相向生长。一般堆空间大一点,栈空间小
堆的内存分配是程序员控制的(malloc,new),申请时,系统遍历一个记录空闲区域地址的链表,找到第一个符合条件的,所以分配不连续,会产生碎片;栈是系统自动分配的,是连续的。
栈一般存储返回地址、局部变量、参数等。堆的话程序员自己控制。
形象一点,栈就是饭店点菜,不用烧不用洗碗,高效方便但是自由度小;堆就是自己烧,麻烦效率低还有可能内存泄漏,但是自由度大。

19. 请你说说 C++11、C++14、C++17、C++20 都有什么新特性

C++11:
auto自动类型推倒(在以前的版本中,auto 关键字用来指明变量的存储类型,它和 static 关键字是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,一般我们也不写,这使得 auto 关键字的存在变得非常鸡肋。)
auto 的一个典型应用场景是用来定义 stl 的迭代器。不同容器的迭代器有不同的类型,在定义迭代器时必须指明。而迭代器的类型有时候比较复杂,书写起来很麻烦;auto 用于泛型编程,不希望指明具体类型的时候,比如泛型编程中。
还有一个decltype和auto很像,也是自动类型推倒。
decltype(exp) varname [= value] 括号代表可省略,区别是,它根据exp表达式来推倒类型,表达式可以是简单的也可以是函数等。因为auto必须初始化,但decltype不用。auto不能用于类的非静态成员变量(也是因为没初始化),但decltype可以。

final关键字,使得类不能被继承,函数不能被重写 override提示虚函数要重写
lambda表达式 匿名函数,简单地理解就是没有名称的函数
使用using定义别名(替代typedef)。typedef被重定义的类型并不是一个新的类型,仅仅只是原有的类型取了一个新的名字
tuple tuple 最大的特点是:实例化的对象可以存储任意数量、任意类型的数据。
C++11 long long超长整形
C++11 shared_ptr、unique_ptr、weak_ptr智能指针 具体看前面

C++11右值引用右值引用只不过是一种新的 C++ 语法,真正理解起来有难度的是基于右值引用引申出的 2 种 C++ 编程技巧,分别为移动语义和完美转发
在 C++ 或者 C 语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。
1 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。举个例子:
int a = 5; 5 = a; //错误,5 不能为左值.
C++ 中的左值也可以当做右值使用,例如:
int b = 10; // b 是一个左值
a = b; // a、b 都是左值,只不过将 b 可以当做右值使用

2 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。a 和 b 是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量 5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值。
简单说,左值就是变量,右值就是字面常量。

引用,使用 “&” 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子:
int num = 10;
int &b = num; //正确
int &c = 10; //错误
为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。
int && a = 10;
a = 100;
cout << a << endl; (但是这种常量右值引用是没有意义的,引用一个不可修改的常量,因为完全可以交给常量左值引用完成)
非常量右值引用才可以实现移动语义和完美转发
移动语义
解决的问题:当拷贝对象时,如果对象成员有指针的话,是深拷贝的方式(浅拷贝的话,多次析构的问题),深拷贝会将指针指向的内存资源一起拷贝一份,如果临时对象中的指针成员申请了大量的堆空间,那么效率很低。
所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
实现方式:手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式(引用就是浅拷贝),同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。
我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。

完美转发:完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)
总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值(不然的话只看成左值);其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。
之前的版本,const 左值引用既可以接收左值,也可以接收右值,但考虑到其 const 属性,除非被调用函数的参数也是 const 属性,否则将无法直接传递。

std::future
我们想要从线程中返回异步任务结果(即任务A的执⾏必须依赖于任务B的返回值),一般需要依靠全局变量;从安全角度看,有些不妥;为此C++11提供了std::future类模板,future对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。
简单地说,std::future 可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段。
std::future 通常由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)
Provider 可以是函数或者类
std::async 函数,本文后面会介绍 std::async() 函数。
std::promise::get_future,get_future 为 promise 类的成员函数
std::packaged_task::get_future,此时 get_future为 packaged_task 的成员函数
std::future 一般由 std::async, std::promise::get_future, std::packaged_task::get_future 创建,不过也提供了构造函数。td::future 的拷贝构造函数是被禁用的,只提供了默认的构造函数和 move 构造函数(移动构造函数,利用右值作为参数)
std::future::valid()
检查当前的 std::future 对象是否有效,即释放与某个共享状态相关联。一个有效的 std::future 对象只能通过 std::async(), std::future::get_future 或者 std::packaged_task::get_future 来初始化。另外由 std::future 默认构造函数创建的 std::future 对象是无效(invalid)的,当然通过 std::future 的 move 赋值后该 std::future 对象也可以变为 valid。
std::future::wait()
等待与当前std::future 对象相关联的共享状态的标志变为 ready.
如果共享状态的标志不是 ready(此时 Provider 没有在共享状态上设置值(或者异常)),调用该函数会被阻塞当前线程,直到共享状态的标志变为 ready。一旦共享状态的标志变为 ready,wait() 函数返回,当前线程被解除阻塞,但是 wait() 并不读取共享状态的值或者异常。(这就是和std::future::get()区别 )
std::future::wait_for() 可以设置一个时间段 rel_time
std::shared_future 与 std::future 类似,但是 std::shared_future 可以拷贝、多个 std::shared_future 可以共享某个共享状态的最终结果
参数是⼀个future,⽤这个future等待⼀个int型的产品:std::future& fut
⼦线程中使⽤get()⽅法等待⼀个未来的future,返回⼀个result。⽣产者使⽤async⽅法做⽣产⼯作并返回⼀个future,消费者使⽤future中的get()⽅法可以获取产品。

C++14,17 就不详细介绍了。 C++20多了协程。

20 计算机存储系统层次
计算机基础面经积累---持续更新_第1张图片

介绍几种存储介质
ram,随机存取,特点是断电后数据消失。分为sram和dram,前者更快,用双稳态电路,造价高,后者电容存储电荷表示0,1,电容会放电,所以需要刷新。
rom,只读存储,断电不消失,做外存。后来各种升级,flash等。
寄存器最快,用的ram或者dff等,不太了解;
高速缓存基本用sram,主存一般用dram。高速缓存的存在是解决CPU速度和内存速度的速度差异问题。利用时空局部性。而磁盘缓存是减少内存访问IO的次数,提高效率。
先到一级缓存中找,找不到再到二级缓存中找,如果还找不到就只有到内存中找了,再不行再到辅存找。

21. 程序装入和链接几种方式
预处理,编译,链接,装入,运行。
绝对装入:只适合单道程序环境。编译后产生绝对的物理地址;
可重定位装入:程序的地址以0开始计算,根据内存起始地址,真实物理地址为内存起始地址加程序中的地址;
动态重定位:可重定位装入不允许程序运行时在内存中移动位置(有点像多态的意思),实际上程序运行时会移动,比如对换技术。所以这种装入内存时依旧是逻辑地址,真正执行时再转换成物理地址,需要重定位寄存器的支持。

静态链接:在装入之前就变成一个完整的模块,已经可以执行了。需要修改相对地址,外部调用符号改成相对地址。
装入时动态链接:外部模块是分开单独存储的。在装入时,发生一个外部模块的调用,才链接。优点是方便修改更新,不用重新打开模块。而且容易实现模块的共享,不然程序很大占空间。

运行时动态链接:执行时才链接,不仅加快装入过程,还节省内存空间。比如错误处理模块,不出错时就不进入内存。

22 内存分配方式
主要分为连续分配和离散分配
连续分配分为:单一连续,固定分区,动态分区,动态可重定位分区。缺点是产生碎片,紧凑时需要代价;
离散分配(虽然也会有内部碎片):分页,分段,段页结合(最好的)

动态分区分配有一个空闲分区表和空闲链表。分配算法有首次适应、最佳适应、哈希等。动态可重定位分区就是多了一个紧凑的功能,收拾碎片。

页面大小一般在1KB-8KB。太小导致一个进程占用页面太多,页表太大,占用内存,页面换进换出效率低。太大导致业内碎片正大。

这种地址为 :页号+页内偏移地址。
地址转换的过程:硬件完成。页表在内存中**,每个进程都有**,存的页号到块号的映射。页表寄存器存页表始址和页表长度。先判断页号是否超过页表长度。若没有,则利用页表寄存器,通过页表始址+页表项长度*页号找到页表项,得到块号,物理地址=块号+页内地址。
可见,CPU存取一个数据需要两次访问内存,第一次访问页表合成物理地址,第二次访问这个物理地址。比较慢。所以,出现了快表。相当于放在高速缓存中的页表,无则添加,有则直接利用快表给出块号。也是利用了程序的时空局部性。

由于现代计算机的逻辑地址空间很大(2的32到64次方),这样页表就很大,甚至可以到2MB。可以利用两级页表。这样就变成外层页号+外层页内地址+页内地址。其实是一种时间换空间的方法。

而段式管理不是为了内存利用率,而是为了方便用户编程。方便共享。信号保护等。

23 介绍虚拟内存
通过对换技术,允许部分程序放入内存就可执行程序,执行中,不需要的换出,需要的换入。过程对用户是透明的,所以用户感觉内存很大,虚拟内存一般受可寻址的大小和外存大小限制。虚拟内存可提高系统资源利用率,方便用户编程

由于利用局部性原理,所以CPU利用率和内存利用率都更高;
虚拟内存的页表结构
计算机基础面经积累---持续更新_第2张图片
当访问一个不在内存的逻辑地址时,产生缺页中断/缺段中断;OS将阻塞该进程,启动磁盘I/O,如果内存有空闲,装入该页,没有空闲启用置换算法,装入所需的页/段后,将阻塞进程置为就绪态。
虚拟地址和物理地址转换过程前面已经说过了。

虚拟内存管理考虑的算法问题。
读取策略:某一页何时调入内存(请求调入/预调入)。预调入:考虑到启动磁盘的寻道和延迟开销,在调入所需页的同时,也调入若干可能马上访问的页面(通常是所需页的后面几页)
置换策略:
OPT最佳置换:淘汰那些永不再使用或者下次访问距当前时间最长的页面最佳,但不可实现,用于衡量其它置换算法性能。因为不知道未来的情况。
LRU:最近最久未使用算法,淘汰在最近一段时间内最近未使用的一页。以过去预测将来。实现时注意时间戳。
FIFO:先入先出算法。淘汰驻留内存时间最久的一页。Belady异常现象:通常,帧越多则缺页次数越少,但FIFO算法中,有时**,帧越多反而缺页率越高**
简单时钟算法 某进程的所有页面(或整个内存的页框)排成一循环缓冲链;某页被装入或访问时,其使用位U置1 置换时顺序查找循环链,U=0,置换该页,U=1,将U置0,替换指针前移,下次置换时从替换处开始寻找。

抖动:
当系统并发度 ( 多道程序度 ) 过高时,缺页频繁,用 于调页的时间比进程实际运行的时间还多, CPU 利用率急剧下降,此时发生了 抖动
解决:抖动时,挂起 一 些进程,释放它们的帧
预防:L=S 准则:调整并发度,使得 产生缺页的平均时间 ( 即缺页中断之间的平均时间 ) 等于处理 一 次缺页中断的平均时间 ,此时 CPU 利用率最高。

23 assert用法
#include “assert.h” void assert( int expression );
assert 的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向 stderr 打印一条出错信息,然后通过调用 abort 来终止程序运行。
使用 assert 的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。
用法:在函数开始处检验传入参数的合法性
每个assert只检验一个条件

24 前置++和后置++区别和效率
最初始的就是,前置是先自增,再赋值;后置是先赋值再自增。更深入一点的区别是:
++a表示取a的地址,增加它的内容,然后把值放在寄存器中;
a++表示取a的地址,把它的值装入寄存器,然后增加内存中的a的值;
int a=0;
b=++a; //b=1
b=a++; //b=0;
再深入:
对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符.,理由是 前置自增 (++i) 通常要比后置自增 (i++) 效率更高
后置++编译器会生成一个临时空间进行存储,最后返回的也是临时空间的值,前置++不会生成临时对象,直接在原对象上++。

a++是线程安全的吗?
不是,从汇编角度看,他不是原子操作,因为要先找到a的地址,再把它装入寄存器,再把地址中的值加1.一共三个步骤,如果多线程的话,可能线程A还没来得及加1到内存,线程2就开始读,就是脏读。

int a=b是线程安全的吗?
也不是,因为对应两个语句,将b的值放入寄存器,再把寄存器的值放到a的内存。如果执行一半就被另一个线程占用CPU,就会出问题。

解决办法, 设置std::atomic类型。std::atomic value; value=99. 这样就可以避免在多线程编程时加锁,不但麻烦,而且效率还低。 atomic类型变量有一个特点,不可拷贝只能赋值。因为源码里拷贝构造函数delete了。(为什么这么做?可能是在没有原子指令的平台上,需要通过互斥体即不能复制提供原子性,所以。拷贝赋值时不能保证原子性)

25 C++三大特性
封装、继承、多态
封装就是把数据和代码组合在一起形成类,避免不确定访问和外界干扰。类可以设置访问限制,把公共的数据和方法用public访问,私有的private.
在类的内部(定义类的代码内部),⽆论成员被声明为 public、protected 还是 private,都是可以互相访问的,
没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访
问 private、protected 属性的成员。
⽆论共有继承、私有和保护继承,私有成员不能被“派⽣类”访问,基类中的共有和保护成员能被“派⽣类”访问。对于私
有和保护继承,基类中的所有成员不能被“派⽣类对象”访问。

继承:它可以使⽤现有类的所有功能,并在⽆需新编写原来的类的情况下对这些功能进⾏扩展。代码复用。
(理解虚拟继承和多重继承的问题)

多态:即向不同对象发送同⼀消息,不同的对象在接收时会产⽣不同的⾏为(重载实现编译时多态,虚函数实现运⾏时多态)
主要使用虚函数,用于当父类对象指针指向不同子类对象,表现的是虚函数的不同的行为。
基类是⼀个抽象对象——⼈,那学⽣、运动员也是⼈,⽽使⽤这个抽象对象既可以表示学⽣、也可以表示运动员。
虚函数依赖虚函数表⼯作,表来保存虚函数地址,当我们⽤基类指针指向派⽣类时,虚表指针指向派⽣类的虚函数

26 类模板和模板特化
类模板
template
class A {}; // 类模板是能接受任意类型,A后面不需要(不能)任何处理
模板偏特化(局部特化) 可以接受任意指针类型
// A
template
class A {}; // 类模板A的偏特化版本,在A后指出特化的范围

指定接受int类型
template<>
class A {} // 类模板A的全特化版本(已经是类模板的一个实例了),在A后直接指出明确类型int

全特化优先级最高。

27 强制类型转换的几种方法
static_cast:没有运⾏时类型检查来保证转换的安全性进⾏上⾏转换(基类指针=派生类指针)是安全的
进⾏下⾏转换(把基类的指针或引⽤转换为派⽣类表示),由于没有动态类型检查,所以是不安全的。
使⽤:

  1. ⽤于基本数据类型之间的转换,如把int转换成char。
  2. 把任何类型的表达式转换成void类型

dynamic_cast:主要用于“安全地向下转型”。在进⾏下⾏转换时,dynamic_cast具有类型检查(信息在虚函数中)的功能,⽐static_cast更安全。
转换后必须是类的指针、引⽤或者void*,基类要有虚函数,因为dynamic_cast需要从类的虚函数表表中获得类类型信息。
dynamic本身只能⽤于存在虚函数的⽗⼦关系的强制类型转换;对于指针,转换失败则返回nullptr,对于引⽤,转
换失败会抛出异常。

reinterpret_cast
可以将整型转换为指针,也可以把指针转换为数组;可以在不同类型的指针间进行任意转换,其实就是重新解释了内存取值,指针是一样的,但是指针的内容怎么解释(取几个字节),这个转换了一下。错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。它的使用价值:用来辅助哈希函数,把void当成Int,对整数的操作显然要对地址操作更方便。
const_cast 常量指针转换为⾮常量指针,并且仍然指向原来的对象。常量引⽤被转换为⾮常量引⽤,并且仍然指向原来的对
象。去掉类型的const或volatile属性。
去掉const属性:const_cast> (&num),常用,因为不能把一个const变量直接赋给一个非const变量,必须要转换。
但是,如果把常量A的指针改成非常量指针,然后赋给一个非常量B,修改B值,常量A的值并没有变。但是它们地址是相同的。很奇怪。如果我们不想修改const变量的值,那我们又为什么要去const呢?原因是,我们可能调用了一个参数不是const的函数,而我们要传进去的实际参数确实const的,但是我们知道这个函数是不会对参数做修改的。于是我们就需要使用const_cast去除const限定,以便函数能够接受这个实际参数。

28 std::function实现回调机制的了解
什么是回调函数:如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
回调:解耦导致:高效、灵活现代C++的封装性导致模块之间一定的独立性。但是模块之间又需要互相协作,所以有了回调
直观一点:假设A是核心团队(A提供一个funA函数供B调用,它可以看成库函数,还有一个sell函数必须等funA返回结果才可以执行),B是另一个团队。
如果funA需要很长时间才能执行完成,如果不用回调,那么,A必须等B执行完才能执行接下来的sell(),浪费时间。
回调的话,就是funA(sell),sell回调函数虽然是A定义的,但是是B调用的,调用完直接把结果返回给A,A不用等。这就是高效
**灵活:**如果直接把sell函数写到funA函数里面不就万事大吉了吗,但是,funA不知道到底后来要做什么,只有调用者自己知道。所以如果不同人调用,只需要传入不同的函数指针就可以了。

思考:为什么用?
但是把函数指针应用于回调函数就体现了一种解决问题的策略,一种设计系统的思想。在解释这种思想前我想先说明一下,回调函数固然能解决一部分系统架构问题但是绝不能再系统内到处都是,如果你发现你的系统内到处都是回调函数,那么你一定要重构你的系统。回调函数本身是一种破坏系统结构的设计思路
就是说回调函数的本质就是“只有我们B才知道做些什么,但是我们并不清楚什么时候去做这些,只有其它模块A才知道,因此我们必须把我们知道的封装成回调函数告诉其它模块”

函数指针可以是普通函数,也可是类的静态函数。
也可是类的非静态函数。

为什么要用?
在C语言的时代,我们可以使用函数指针来把一个函数作为参数传递,这样我们就可以实现回调函数的机制。到了C++11以后在标准库里引入了std::function模板类,这个模板概括了函数指针的概念
std::function plusFunc;
它可以表示任何一个返回值为int,形参列表为int a, int b这样的函数指针。
int puls(int a, int b)

{
return a + b;
}
// 函数名就代表着该函数的地址,也就是指向该函数的指针

plusFunc = plus;

29. epoll使用
IO多路复用是什么?
先介绍三种IO模型:阻塞IO,非阻塞IO,IO多路复用。
阻塞IO:所有套接口都是阻塞的。比如read调用必须等到缓冲区有内容才返回,不然一直阻塞在这里;
非阻塞IO:当我们把一个套接口设置为非阻塞时,就是在告诉内核,当请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。不会一直阻塞。
多路复用:此模型用到select poll epoll,这两个函数也会使进程阻塞,select先阻塞,有活动套接字才返回,但是和阻塞I/O不同的是,这两个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写(就是监听多个socket)。select被调用后,进程会被阻塞,内核监视所有select负责的socket,当有任何一个socket的数据准备好了,select就会返回套接字可读,我们就可以调用recvfrom处理数据。
正因为阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用。

为什么epoll最好?
select: 1 由于只知道有几个fd准备好了,所以需要采用轮询的方式找到准备好的fd。所以会随着连接数的增加,性能变低。 2 调用时需要将fdset用用户态拷贝到内核态,遍历时再拷贝回用户空间,判断哪个fd就绪了。 3 fdset只能监视1024个。

poll:主要用链表代替fdset结构,监视数量不受限制了。新增水平触发,这次没处理,下次继续通知。

epoll:空间换时间。没有 fd 个数限制,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过 epoll_ctl 注册 fd,一旦 fd 就绪就会通过 callback 回调机制来激活对应 fd,进行相关的 io 操作。
三个函数:
int epoll_create(int size);创建一个 epoll 的句柄(红黑树结构),参数 size 并非限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。它就会占用一个 fd 值,在 linux 中查看/proc/进程id/fd/,能够看到这个 fd,所以 epoll 使用完后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
事件注册函数,将需要监听的事件和需要监听的 fd 交给 epoll 对象。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。如果返回–1,则表示出现错误,需要检查 errno错误码判断错误类型。

要深刻理解epoll,首先得了解epoll的三大关键要素:mmap、红黑树、链表。
epoll是通过内核与用户空间mmap同一块内存实现的。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到epoll监听的句柄,效率高。
红黑树将存储epoll所监听的套接字。上面mmap出来的内存如何保存epoll所监听的套接字,必然也得有一套数据结构,epoll在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度O(logN)。
当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,这个回调函数其实就所把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。成功时返回就绪的文件描述符的个数

关于ET、LT两种工作模式:默认情况下,epoll采用 LT模式工作,这时可以处理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以将一个事件改为 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。
当一个新的事件到来时,ET模式下当然可以从 epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字没有新的事件再次到来时,在 ET模式下是无法再次从 epoll_wait调用中获取这个事件的;而 LT模式则相反,只要一个事件对应的套接字缓冲区还有数据,就总能从 epoll_wait中获取这个事件。因此,在 LT模式下开发基于 epoll的应用要简单一些,不太容易出错,而在 ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。默认情况下,Nginx是通过 ET模式使用 epoll的。

30 QPS,TPS,吞吐量
TPS就是每秒钟处理的事务数。用户一次请求到收到服务器响应。比如双十一一秒完成多少订单。
QPS:每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。QPS主要针对查询服务性能指标,不能描述增删改等操作,不建议用QPS描述系统整体性能。所以在最小并发数和最大并发数之间,一定有一个最合适的并发数值,在该并发数下,QPS能够达到最大。但是,这个并发并非是一个最佳的并发,因为当QPS到达最大时的并发,可能已经造成用户的等待时间变得超过了其最优值,所以对于一个系统,其最佳的并发数,一定需要结合QPS,用户的等待时间来综合确定。
假设一个查询功能需要调用N个查询接口
则QPS = N * TPS
吞吐量是指系统在单位时间内处理请求的数量。表现了一个系统的承压能力。

31 为什么有了epoll,非阻塞IO还需要reactor
技术角度,没有反应堆也能达到很高的并发,但是从编程角度,不行。即,某一瞬间,服务器共有10万个并发连接,此时,一次IO复用接口的调用返回了100个活跃的连接等待处理。先根据这100个连接找出其对应的对象,这并不难,epoll的返回连接数据结构里就有这样的指针可以用。接着,循环的处理每一个连接,找出这个对象此刻的上下文状态,再使用read、write这样的网络IO获取此次的操作内容,结合上下文状态查询此时应当选择哪个业务方法处理,调用相应方法完成操作后,若请求结束,则删除对象及其上下文。我们的主程序需要关注各种不同类型的请求,在不同状态下,对于不同的请求命令选择不同的业务处理方法。这会导致随着请求类型的增加,请求状态的增加,请求命令的增加,主程序复杂度快速膨胀,导致维护越来越困难。
反应堆模式可以在软件工程层面,将事件驱动框架分离出具体业务,将不同类型请求之间用OO的思想分离。通常,反应堆不仅使用IO复用处理网络事件驱动,还会实现定时器来处理时间事件的驱动(请求的超时处理或者定时任务的处理)
https://blog.csdn.net/fedorafrog/article/details/113849305具体内容看这篇文章

32. linux交换分区是什么?
是磁盘上的一块区域,可以是一个分区或一个文件,或者是他们组合。当系统物理内存吃紧时,Linux会将内存中不常访问的数据保存到swap上,这样系统就有更多的物理内存为各个进程服务。
比如:一些软件只在启动时需要很大内存,运行时不需要,就可以将这些弄到交换分区;
比如一些休眠功能,就是把内存中的数据保存到 swap 分区上,等下次系统启动的时候,再将数据加载到内存中,这样可以加快系统的启动速度。
比如:在某些情况下,物理内存有限,但又想运行耗内存的程序怎么办?这时可以通过配置足够的 swap 空间来达到目标,虽然慢一点,但至少可以运行。

33.linux普通用户和超级用户
普通用户显示符号位**$**
超级用户显示符号位**#**
普通用户进入超级用户:
输入su root,回车,再输入登陆密码
超级用户切换到普通用户:
su zx (zx是我的普通用户名)

34. vim编辑器使用
1、输入vim 文件名就可以进入文件,不存在的话会新建一个文件;
2、刚进入时是命令模式,vim编辑器会将按键解释成命令,键入i可进入插入模式(a,o也可以进入插入模式)可以编辑代码了,返回命令模式用esc键。
3、x:删除当前光标所在位置的字符 dd:删除当前光标所在行 y复制,p粘贴 可使用/(斜线键)来查找文本
4、命令模式下输入冒号:就进入底行模式。wq 保存并退出vim(最常用)q! 强制退出vim(不保存)set nu 显示行号

切换用户主目录 cd ~ 一般用户的主目录就是home/zx,切换父目录 cd … cd 绝对路径 .代表当前目录

查看当前路径:pwd

如何查看文件内容:cat 文件名 vim 文件名

ls -a显示所有文件包含隐藏文件 ls -l显示文件及其属性(rwx,拥有者,时间戳等)

Linux的touch命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件。
touch 文件名 ——创建一个文件
mkdir 目录名 —— 创建一个目录
mkdir -p 目录名1/目录名2/目录名3 ——创建多层级目录
cp 原路径 目标路径 一般复制目录用cp -r 目录名 位置保证目录所有文件都被复制了。
mv 原路径 目标路径

ps 命令是最常用的监控进程的命令,通过此命令可以查看系统中所有运行进程的详细信息。(用户,PID,CPU和内存使用率,运行时间,状态等)
ps aux --sort -%mem按照内存使用率降序排列进程。
还可以通过一个管道给grep查看是否存在特定的进程。

rm 指令 :移除【删除】文件或目录 -r :递归删除整个文件夹
-f : 强制删除不提示 rm -rf要慎用

rmdir只能删除空文件夹

文件查看 head -n tail -n cat ; 长文件用less两边翻页 more可往下翻页

tail命令是实时显示日志文件的最常用解决方案 加上-f参数实时监控日志

ln -s target source 建立软连接,可以用ls -l查看这个文件夹的属性。
A 是 B 的硬链接(A 和 B 都是文件名),则 A 的目录项中的 inode 节点号与 B 的目录项中的 inode 节点号相同,即一个 inode 节点对应两个不同的文件名,两个文件名指向同一个文件,A 和 B 对文件系统来说是完全平等的。删除其中任何一个都不会影响另外一个的访问。
硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。
ln oldfile newfile建立硬链接。

echo命令将输入回显到屏幕上 -e参数表示转义字符会处理比如/n换行。
[root@localhost ~]# read name
Michael Zhang
[root@localhost ~]# echo “My name is $name”
My name is Michael Zhang

通配符
*表示匹配任意长度的任意字符 ?表示任意的一个字符 []表示括号内的匹配。

wc命令的功能为统计指定文件中的字节数、字数、行数, 并将统计结果显示输出。wc -lcw 文件名
-c, --bytes: 统计字节数。
-1,–lines: 统计行数。
-w,–words: 统计字数。

grep命令一般用来筛选数据,用来筛选我们需要的数据
grep [参数] [过滤的规则] [路径] 参数 -i : 忽略大小写 -n : 显示过滤出来的文本在文件内的行号 -v反向查找等等
标准输出 | grep [参数] [过滤规则] 用到了管道
grep -n “root” /etc/passwd 要求过滤出/etc/passwd中包含的root的行及其行号

linux进程几种状态:用ps top可以查看。S栏就是状态的意思。S 列可以看到 R、D、S、I 、Z 几个状态
R 是 Running,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。
D是Disk Sleep 的缩写,也就是不可中断状态睡眠,一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。获得资源后才能运行。
Z 是 Zombie 的缩写,它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源
S 是 InterrupTIble Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。
:I 是 Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上

如何让一个任务在后台执行:&加在一个命令的最后,可以把这个命令放在后台执行
jobs用于查看当前终端后台运行的任务
ctrl+Z也可以将一个正在前台执行的命令暂停,并且放到后台(不过在后台是暂停stop的)
fg命令 将后台中的命令调至前台继续运行 如果后台中有多个命令,可以先用jobs查看jobnun,然后用 fg %jobnum 将选中的命令调出。
bg命令 将一个在后台暂停的命令,变成在后台继续执行,比如crtl+Z之后的那个进程。
kill命令:结束进程 通过jobs命令查看jobnum,然后执行 kill %jobnum 或者通过ps命令查看进程号PID,然后执行 kill %PID 如果前台进程的话直接crtl+C就可以终止了。

linux搜索文件最强大命令:find
find 指定目录 指定条件 指定动作
比如使用find命令搜索在根目录下的所有interfaces文件所在位置,命令格式为”find / -name ‘interfaces’
还有别的locate whereis等。

查看使用过的命令 history
history 5查看近5条命令
删除序号为 534 的历史命令 history -d 534
history -c清空历史

命令who的功能较简单,仅显示用户登录名、终端标志、和登录日期和时间 w显示更多用户正在执行的程序、占用CPU时间、系统的运行时间和平均负载
命名who am i 最简单,仅显示当前用户正使用的终端和登录时间,例如:
[francis@localhost ~]$ who am i
francis pts/2 2010-04-19 10:29

df 是检查Linux安装程序上可用分区空间的最常用的命令之一。可以使用“df -TH”以直观易读的格式打印分区类型和分区大小。此命令将显示每个部分的总可用空间、已用空间和可用空间。
du查看目录大小,df查看磁盘使用情况。du是通过搜索文件来计算每个文件的大小然后累加,du能看到的文件只是一些当前存在 。文件被删除不是立马就消失了,当所有程序都不用时,才会根据OS的规则释放掉已经删除的文件。

查看Linux服务器网络连接的方法:1、在Linux服务器终端可使用ifconfig命令显示所有网络接口的详细情况;2、在Linux服务器终端可使用ping命令检查网络上某台主机是否正常工作;3、在Linux服务器终端可使用netstat命令显示网络连接、路由表和网络接口的相关信息。
环境变量
env命令可以显示当前操作系统所有的环境变量 使用 echo 命令查看单个环境变量,例如:echo $PATH

$ export TEST="Test..."  # 增加一个环境变量 TEST
$ env|grep TEST  # 此命令有输入,证明环境变量 TEST 已经存在了
TEST=Test...
unset  TEST  # 删除环境变量 TEST
$ env|grep TEST  # 此命令没有输出,证明环境变量 TEST 已经删除

按照变量的生存周期划分,Linux 变量可分为两类:
永久的:需要修改配置文件,变量永久生效。
临时的:使用 export 命令声明即可,变量在关闭 shell 时失效。
在 Linux 中设置环境变量有三种方法:
所有用户永久添加环境变量: vi /etc/profile,在 /etc/profile 文件中添加变量。source /etc/profile # 激活后,环境变量才可永久生效
当前用户永久添加环境变量: vi ~/.bash_profile,在用户目录下的 ~/.bash_profile 文件中添加变量。
临时添加环境变量 PATH: 可通过 export 命令,如运行命令 export PATH=/usr/local/cuda/lib64:$PATH,将 /usr/local/cuda/lib64 目录临时添加到环境变量中。查看是否已经设置好,可用命令 export 查看。

36. C++中this指针的理解
this是一个指向对象的const指针,只能用在类的内部,只有在对象被创建的时候才会给this指针赋值,不能认为赋值修改。
比如this->name=name 若成员变量和形参同名就可以一目了然,比如返回对象本身可以return *this;
实际上,它就是成员函数的一个形参,是隐式的,编译器添加的。在调用成员函数时,就会把对象的地址作为实参传递给this。由于C++中类的布局,成员变量是在类内部的,但是成员函数是放在单独的空间的和普通函数一样(深入理解C++对象模型),所以编译是要在成员函数加入一个额外的tihs,关联成员函数和成员变量。

37.同源策略和跨域问题
同源策略是浏览器安全功能,阻止一个域的js脚本和另一个域的内容进行交互,用于隔离潜在恶意文件,防止恶意网站窃取用户数据(不同源的网页不能共享cookie,获取dom,ajax请求)
当协议(http,https)域名(cc.com)端口号有一个不同就是跨域;
几乎任何时候安全性和便捷性都是负相关的,所以浏览器做了平衡,比如img,script,style等允许跨域引用,但是引用并不能读取资源的内容。

38.对虚拟内存的理解
单片机是没有操作系统的,所以每次写完代码,都需要借助工具把程序烧录进去,这样程序才能跑起来。并且单片机的 CPU 是直接操作内存的**「物理地址」。要想在内存中同时运行两个程序是不可能的。如果第一个程序在 2000 的位置写入一个新的值,将会擦掉第二个程序存放在相同位置上的所有内容,所以同时运行两个程序是根本行不通的,这两个程序会立刻崩溃。
如何解决?
我们可以把进程所使用的地址「隔离」开来,即让操作系统为
每个进程分配独立的一套「虚拟地址」,人人都有,大家自己玩自己的地址就行(虚拟地址),互不干涉。但是有个前提每个进程都不能访问物理地址,至于虚拟地址最终怎么落到物理内存里,对进程来说是透明的,操作系统已经把这些都安排的明明白白了。操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。主要有两种方式,分别是内存分段和内存分页
分段管理
程序是由若干个逻辑分段组成的,如可由
代码分段、数据分段、堆段、栈段**组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。
分段机制下的虚拟地址由两部分组成,段选择因子和段内偏移量。
段选择因子里面有段号,可以通过它寻找段表中对应的段基址,然后加上偏移量就是物理地址了。
缺点:
内存碎片和内存交换效率低的问题。内存分段管理可以做到段根据实际需求分配内存,所以有多少需求就分配多大的段,所以不会出现内部内存碎片。但是会出现外部碎片,很多小块的内存分散,无法被利用。解决「外部内存碎片」的问题就是内存交换。这个内存交换空间,在 Linux 系统里,也就是我们常看到的 Swap 空间,这块空间是从硬盘划分出来的,用于内存与硬盘的空间交换。因为硬盘的访问速度要比内存慢太多了,每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。所以,如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿。(swap分区的作用就是在物理内存不够时,将一些不用的程序移到swap分区中,要用时再进入内存,有可能不是原来的物理内存位置了,因为有内存交换紧凑碎片)

分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB。会出现页内碎片。
虚拟地址与物理地址之间通过页表来映射,CPU将地址解释为页号和偏移量,页表保存页号和物理块号的映射。
缺点:内部碎片;因为操作系统是可以同时运行非常多的进程的,那这不就意味着页表会非常的庞大。在 32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有 4MB 的内存来存储页表。
这 4MB 大小的页表,看起来也不是很大。但是要知道每个进程都是有自己的虚拟地址空间的,也就说都有自己的页表。
那么,100 个进程的话,就需要 400MB 的内存来存储页表,这是非常大的内存了,更别说 64 位的环境了。(可以用多级页表解决)
多级页表对导致映射过程变复杂,时间开销大。所以有TLB快表。把最常访问的几个页表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer)。有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。
linux虚拟内存空间分布
计算机基础面经积累---持续更新_第3张图片
文件映射段,包括动态库、共享内存等。
代码段下面还有一段内存空间的(灰色部分),这一块区域是「保留区」,之所以要有保留区这是因为在大多数的系统里,我们认为比较小数值的地址不是一个合法地址,例如,我们通常在 C 的代码里会将无效的指针赋值为 NULL。因此,这里会出现一段不可访问的内存保留区,防止程序因为出现 bug,导致读或写了一些小内存地址的数据,而使得程序跑飞。
总结虚拟内存作用:
1、最主要的就是为了实现多进程。由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的,解决了多进程之间地址冲突的问题。
2、在多进程时,内存空间可能不够用,就需要虚拟内存的换入换出技术,swap。将硬盘空间当成内存,让用户感觉内存很大。
3、段中有优先级等,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性

39. 在4GB物理内存的机器上申请8GB内存空间会怎样
在 32 位操作系统,因为进程理论上最大能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。
在 64位 位操作系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:
如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;

40 如何避免预读失效和缓存污染的问题?
传统的 LRU 算法存在这两个问题:
「预读失效」导致缓存命中率下降(操作系统读磁盘时会多读一些没用的数据)
「缓存污染」导致缓存命中率下降(批量读数据可能会把热点数据挤出去)
Redis 的缓存淘汰算法则是通过实现 LFU 算法来避免「缓存污染」而导致缓存命中率下降的问题(Redis 没有预读机制)。
MySQL 和 Linux 操作系统是通过改进 LRU 算法来避免「预读失效和缓存污染」而导致缓存命中率下降的问题。

预读失效:由于空间局部性,如果想要0-4KB的数据,很可能把后面的8-16KB都读到缓存中,如果使用传统的 LRU 算法,就会把「预读页」放到 LRU 链表头部,而当内存空间不够的时候,还需要把末尾的页淘汰掉。
如果这些「预读页」如果一直不会被访问到,就会出现一个很奇怪的问题,不会被访问的预读页却占用了 LRU 链表前排的位置,而末尾淘汰的页,可能是热点数据,这样就大大降低了缓存命中率 。
我们不能因为害怕预读失效,而将预读机制去掉,大部分情况下,空间局部性原理还是成立的。

要避免预读失效带来影响,最好就是让预读页停留在内存里的时间要尽可能的短,让真正被访问的页才移动到 LRU 链表的头部,从而保证真正被读取的热数据留在内存里的时间尽可能长。
Linux 操作系统实现两个了 LRU 链表:活跃 LRU 链表(active_list)和非活跃 LRU 链表(inactive_list);
MySQL 的 Innodb 存储引擎是在一个 LRU 链表上划分来 2 个区域:young 区域 和 old 区域。
这两个改进方式,设计思想都是类似的,都是将数据分为了冷数据和热数据,然后分别进行 LRU 算法。不再像传统的 LRU 算法那样,所有数据都只用一个 LRU 算法管理。
有了这两个 LRU 链表后,预读页就只需要加入到 inactive list 区域的头部,当页被真正访问的时候,才将页插入 active list 的头部。如果预读的页一直没有被访问,就会从 inactive list 移除,这样就不会影响 active list 中的热点数据。

但是如果还是使用「只要数据被访问一次,就将数据加入到活跃 LRU 链表头部(或者 young 区域)」这种方式的话,那么还存在缓存污染的问题。
当我们在批量读取数据的时候,由于数据被访问了一次,这些大量数据都会被加入到「活跃 LRU 链表」里,然后之前缓存在活跃 LRU 链表(或者 young 区域)里的热点数据全部都被淘汰了,如果这些大量的数据在很长一段时间都不会被访问的话,那么整个活跃 LRU 链表(或者 young 区域)就被污染了。

当某一个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 I/O,MySQL 性能就会急剧下降。

前面的 LRU 算法只要数据被访问一次,就将数据加入活跃 LRU 链表(或者 young 区域),**这种 LRU 算法进入活跃 LRU 链表的门槛太低了!**只要我们提高进入到活跃 LRU 链表(或者 young 区域)的门槛,就能有效地保证活跃 LRU 链表(或者 young 区域)里的热点数据不会被轻易替换掉。Linux 操作系统:在内存页被访问第二次的时候,才将页从 inactive list 升级到 active list 里。MySQL Innodb:在内存页被访问第二次的时候,并不会马上将该页从 old 区域升级到 young 区域,因为还要进行停留在 old 区域的时间判断:

41.count(1),count( ),count(字段),select(1),select ( )效率比较
首先说一下select(1),select ( )。
select(1)主要用于查询表中是否有符合条件的记录(比如select 1 from seckill where id = 1001;),select 1一般用来当作条件使用,比如**exists( select 1 from 表名)**等;select 1配合count()、sum()函数。select 1的效率比select 列名和select
快,因为不用查字典表。(为什么这么说,参数是 1,不是字段,所以不需要读取记录中的字段值。参数 1 很明显并不是 NULL,因此 server 层每从 InnoDB 读取到一条记录,就将 count 变量加 1。)
而select * from … 是返回所有行的所有列,性能很差,不推荐用。性能差的原因有:1 不需要的列会增加数据传输时间和网络开销(需要解析更多的对象、字段、权限、属性等相关内容,一些大的文本字段开销很大传输)2.大字段还会增加IO操作;3 失去MySQL优化器
“覆盖索引”策略优化的可能性。如果用户使用select *,获取了不需要的数据,则首先通过辅助索引过滤数据,然后再通过聚集索引获取所有的列,这就多了一次b+树查询,速度必然会慢很多。而聚集索引很可能数据在磁盘(外存)中(取决于buffer pool**的大小和命中率),这种情况下,一个是内存读,一个是磁盘读,速度差异就很显著了,几乎是数量级的差异。
(覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。)

count(1),count( *),count(字段)
计算机基础面经积累---持续更新_第4张图片
在通过 count 函数统计有多少个记录时,MySQL 的 server 层会维护一个名叫 count 的变量。

server 层会循环向 InnoDB 读取一条记录,如果 count 函数指定的参数不为 NULL,那么就会将变量 count 加 1,直到符合查询的全部记录被读完,就退出循环。最后将 count 变量的值发送给客户端。
count(1),count( *),InnoDB 循环遍历聚簇索引(主键索引)或二级索引,将读取到的记录返回给 server 层,但是不会读取记录中的任何字段的值,所以效率高一点。
count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。
使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。

使用 MyISAM 引擎时,执行 count 函数只需要 O(1 )复杂度,这是因为每张 MyISAM 的数据表都有一个 meta 信息有存储了row_count值,由表级锁保证一致性,所以直接读取 row_count 值就是 count 函数的执行结果。
而 InnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的,所以无法像 MyISAM一样,只维护一个 row_count 变量。

如何优化 count(*)
就可以使用 show table status 或者 explain 命令来表进行估算,不会真正查表;
或者每新插入一个记录就将计数表中的计数字段 + 1。也就是说,在新增和删除操作时,我们需要额外维护这个计数表。

static关键字的作用
最主要的是隐藏和对象共享某一数据功能。
作用在局部变量,使其存储在静态区(包括全局,静态),和程序生命一样长;作用在全局变量主要是其他文件不能引用它,防止不同文件重名的干扰;作用在成员变量,成为类的一部分,公共部分比如总数,只能在类外初始化不能在构造函数初始化。作用在成员函数主要是为了访问静态成员变量,只能访问它,因为没有this指针。

静态链接和动态链接
程序的几个步骤就是预处理,编译,汇编,链接,装入,运行。链接主要完成符号解析和重定位功能,如果在编译汇编时就完成链接,就是静态链接,如果这一步只添加参数信息,推迟到运行时链接,就是动态链接。

举个例子 test.c main.c源文件,静态链接方法
gcc -c test.c main.c -o test.o main.o
ar -rv  test.lib test.o main.o 
gcc  test.lib  -o test.exe
动态链接方法
gcc  test.c -shared -o test.dll
gcc main.c test.dll -o test.exe

静态链接使每个进程都包含完整的库代码副本,比较占空间,且一个库修改就要重新编译整个代码。优点就是发布方便,可独立运行,不需要动态库依赖,前期编译速度快一点。动态链接相反,耦合度小,适合大项目。

变量声明和定义
变量声明不分配内存,可声明多次(extern int a),定义分配内存只有一次(int a,int a=1)

#ifdef 标识符
程序1
#else
程序2
#endif
条件编译

#idndef aaa
#define aaa 2
##endif 解决双重define嵌套问题

strlen和sizeof 区别
sizeof()是运算符,其值在编译时 就已经计算好了,参数可以是数组、指针、类型、对象、函数等。它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。strlen(…)是**函数,*要在运行时 才能计算。参数必须是字符型指针(char)。当数组名作为参数传入时,实际上数组就退化成指针了。
它的功能是:返回字符串的长度。
char p[] = “hello”;首先p的结果大家应该不会错吧? p=hello,strlen=5,sizeof=6 字符串hello的最后隐藏着一个\0故sizeof计算所占字节数是6

const和volatile的区别
volatile关键字是一个类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。之所以优化是因为访问寄存器要比访问内存单元快得多。但是优化之后容易出现问题,例如现在要直接对内存地址单元的内容修改,如果继续使用未经过valatile声明的变量,则读到的值有可能是寄存器中未经过修改的值,但本意是要读发生变化后的数值,所以会出现意想不到的错误。而经valatile声明的变量,每次访问该变量时都会从内存单元中重新读取
const只是不允许程序中的代码改变某一变量,其在编译期发挥作用,它并没有实际地禁止某段内存的写特性。我只要一听到被面试者说:“const意味着常数”,我就知道我正在和一个业余者打交道。其实只要能说出const意味着**“只读”**就可以了
const int *a; 修饰的是int,说明指针可以修改,但是a是一个常量不能修改.
int * const a;修饰的是a指针,不能修改指针方向,但是可以修改指向的int的值
int const * a const; 都不行
下面是volatile变量的几个例子:
(1) 并行设备的硬件寄存器(如:状态寄存器)
(2) 一个中断服务子程序中会访问到的非自动变量
(2) 多线程应用中被几个任务共享的变量
一个参数既可以是const还可以是volatile吗?解释为什么。可以,比如只读的状态寄存器

比如要往某一地址送两指令:
int *ip =…; //设备地址
*ip = 1; //第一个指令
*ip = 2; //第二个指令
以上程序compiler可能做优化而成:
int *ip = …;
*ip = 2;
结果第一个指令丢失。如果用volatile, compiler就不允许做任何的优化,从而保证程序的原意

C++中类成员的访问权限****三种继承方式
private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问

C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

一般成员变量放在私有部分,隐藏起来不让外部看到,成员函数一般是公有的,因为你定义成保护或私有,类外是无法访问这个函数的,没什么意义。成员函数一般是给外人使用,操作成员变量。
保护权限和私有权限在继承时会有区别,比如父亲的车可以作为保护成员,可以让儿子也使用,而父亲的私房钱应作为私有成员,儿子不能使用。
继承方式: 公共继承、保护继承、私有继承
继承语法:class 子类 : 继承方式 父类 默认的继承方式(如果缺省,默认为private继承)
父类私有的部分不管是哪种继承方式,子类都无法访问(除非父类在public或protected中定义了访问私有内容的函数你才有可能访问到)
保护继承的特点是基类的所有公有成员和保护成员都作为派生类的保护成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然为私有的。

友元:类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend. 函数的定义在类外。

class Box
{
   double width;
public:
   double length;
   friend void printWidth( Box box );
   void setWidth( double wid );
};

子类对父类成员的访问权限跟如何继承没有任何关系,“子类可以访问父类的public和protected成员,不可以访问父类的private成员”——这句话对任何一种继承都是成立的。
(2)继承修饰符影响着谁可以知道“继承”这件事。public继承大家都知道,有点像“法定继承人”,因此,任何代码都可以把子类的引用(或指针)直接转换为父类。也因为这个原因,public继承常用来表达设计中所谓的“is-a”关系。private继承则有点像“私生子”,除了子类自己,没有人知道这层关系,也因此,除了子类自己的代码之外,没有其它人知道自己还有个父亲,于是也就没有其它人可以做相应的类型转换。为此,私有继承常用于表达非“is-a”的关系,这种情况下子类只是借用父类的某些实现细节。protected继承则有点特殊,外界同样不知道这层关系,但家族内部的子孙们可以知道,有点像“自家知道就行了,不许外扬”的意思,于是子孙们是可以做这种向上转型,其它代码则不可以。因为这种特殊性,protected继承在实际中用得很少。
(3)还需要补充一点,由于“继承关系”的可见性受到了影响,那么继承来的财产的可见性也必然受到影响。比如一个成员变量或成员函数,在父类中本来是public的,被某个子类protected继承之后,对子类来讲,这个成员就相当于protected成员了——继承是继承到了,但权限变了。具体的规则教材上都会讲的。

**函数声明(函数原型)、函数调用、函数定义:**函数声明一般放在头文件中,没有函数体的,规定了参数和返回类型,参数名可以忽略,不开辟空间,目的是告诉编译器这是一个函数,在函数调用时编译器帮你检查函数写没写对。函数定义有函数体。函数调用不用写返回类型和参数类型。
多态的实现有哪几种?
多态分为静态多态和动态多态。其中,静态多态是通过重载和模板技术实现的,在编译期间确定;动态多态是通过虚函数和继承关系实现的,执行动态绑定,在运行期间确定。

重载和重写的区别:

是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

#include
using namespace std;
class A
{
	void fun() {};
	void fun(int i) {};
	void fun(int i, int j) {};
	void fun(double i){};
};

重写(覆写)
是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

#include

using namespace std;

class A
{
public:
	virtual	void fun()
	{
		cout << "A";
	}
};
class B :public A
{
public:
	virtual void fun()
	{
		cout << "B";
	}
};
int main(void)
{
	A* a = new B();  基类的指针指向派生类的对象,指向的是派生类中基类的部分。
	所以只能操作派生类中从基类中继承过来的数据和基类自身的数据。C++的多态性可以解决基类指针不能操作派生类的数据成员的问题。
	a->fun();//输出B  
}

为什么需要基类的指针指向派生类的对象,不用派生类自己的指针指向自己的对象?
因为可以用基类指针指向其不同派生类对象,所以才能实现多态和虚函数更为重要的是,当我们想要用某个数据结构去存储不同对象时,比如我们想实现一个所有动物对象“走”得数组,遍历数组时向我们展示出不同动物对象走的情况,该怎么做呢?因为我们的数组只能实现同一类型数据存储,而我们又想展示出不同类型对象的“走”,所以很显然我们只需在数组中存储基类指针,然后把数组中的每个基类指针与相应的类对象进行动态绑定即可(书面来说就是接口重用,提高代码可重用性,维护性扩充性)

vector<Animal*> walk;
	walk.emplace_back(new Bird());
	walk.emplace_back(new Reptile());
	walk.emplace_back(new Human());
	for (const auto &c : walk)
		c->walk();

动态绑定是如何实现的?
就是问动态多态怎么实现?当编译器发现类中有虚函数时,会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中并且在对象中增加一个指针vptr,用于指向类的虚函数表。当派生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定。
实现的必要条件就是:必须要有继承,有虚函数,有基类指针指向派生类对象
虚函数表是针对类的,类的所有对象共享这个类的虚函数表,因为每个对象内部都保存一个指向该类虚函数表的指针vptr,每个对象的vptr的存放地址都不同,但都指向同一虚函数表。在gcc编译器的实现中虚函数表vtable存放在可执行文件的只读数据段.rodata中。

纯虚函数有什么作用?如何实现?
定义纯虚函数是为了实现一个接口,起到规范的作用,想要继承这个类就必须覆盖该函数。所有人用同一个接口。
实现方式是在虚函数声明的结尾加上= 0即可。 virtual void fun()=0;

C++禁止使用拷贝构造函数和赋值运算符方法
要么将拷贝构造函数和赋值运算符声明为private并不实现。或者在后面加=delete

如果类A是一个空类,那么sizeof(A)的值为多少?
sizeof(A)的值为1,因为编译器需要区分这个空类的不同实例,分配一个字节,可以使这个空类的不同实例拥有独一无二的地址。

关于类的大小和类的对象的大小关系?
Class A; A obj;
那么sizeof(A)==sizeof(obj) 那么sizeof(A)的大小和成员的大小总和是什么关系呢,很简单,一个对象的大小大于等于所有非静态成员大小的总和
为什么是大于等于而不是正好相等呢?
C++对象模型本身 对于具有虚函数的类型来说,常见的方法是建立一个虚函数入口表,这个表可为相同类型的对象共享,因此对象中需要有一个指向虚函数表的指针;
编译器优化 因为对于大多数CPU来说,CPU字长的整数倍操作起来更快,因此对于这些成员加起来如果不够这个整数倍,有可能编译器会插入多余的内容凑足这个整数倍;

原则:
1、构造函数不能为虚函数;
2、析构函数需要是虚函数;

析构函数为什么要是虚函数: 防止内存泄露
因为我们可以用基类的指针指向派生类对象,我们希望调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。但是,如果析构函数不被声明成虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,只会调用基类析构函数,而不调用派生类析构函数,这样就会导致基类指针指向的派生类对象析构不完全。

构造函数不能为虚函数:不能也没必要
不能:虚函数有一个指向虚函数表的指针,这个虚函数表存储在对象的内存空间中,而构造函数就是实例化对象的,对象都没有,哪来的虚函数表。虚函数的调用依赖于虚函数表,而指向虚函数表的指针vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数。
没必要:虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的。*

内存泄漏的场景有哪些?
1、没有成对使用new/delete ,malloc/free, new []/delete [] 比如在构造函数new申请内存,但是析构函数没有释放内存。
2、基类析构函数不是虚函数,导致派生类析构时不调用。为了实现动态绑定,基类指针指向派生类对象,如果析构函数不是虚函数,那么在对象销毁时,就会调用基类的析构函数,只能销毁派生类对象中的部分数据。
3. 如果类中有从堆中动态分配的指针变量,则类中必须定义拷贝构造函数。为什么呢?因为默认的是位拷贝(浅拷贝)。导致两个对象指向同一个内存地址,两次释放就会出问题。

拷贝构造函数注意点 A(const A &a);
拷贝构造函数必须是当前类的引用
如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头**,陷入死循环**。
只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。
拷贝构造函数是const 引用
拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。
另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。
没有返回类型

Student::Student(const Student &stu){
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
    cout<<"Copy constructor was called."<<endl;
}
**//重载=运算符
Student & Student::operator=(const Student &stu){
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
    cout<<"operator=() was called."<<endl;
   
    return *this;
}**

重载赋值运算符是返回引用。 赋值运算符写的时候还要注意就是要先删除之前动态申请的空间,重新申请。还要判断是不是本身,是本身直接返回*this 。

拷贝构造函数的应用场景:
将其它对象作为实参 A a(b);
Student stu4 = stu1; //在创建对象的同时赋值
stu5=display(stu5); //函数形参,返回值调用
(返回值优化(Return value optimization,缩写为RVO)是C++的一项编译优化技术。它最大的好处是在于: 可以省略函数返回过程中复制构造函数的多余调用,解决 “C++ 中长久以来为人们所诟病的临时对象的效率问题”。)
Student stu5; //调用普通构造函数Student()
stu5 = stu1; //调用operator=()

内存分配方式有几种?
1、栈分配,比如函数的局部变量,函数结束自动销毁,分配效率很高,但是栈容量有限。
2、堆分配(new,malloc)释放需要程序员自己控制;
3.自由存储区分配,和堆比较像,但不是一个空间,比定位new运算符就是指定一个地址分配,这个地址属于自由存储区。
4、常量存储区,无法修改;
5.全局、静态存储区,初始化和未初始化BSS,C++已经不区分初始化和未初始化了,自动初始化为0。

如何构造一个类,使得只能在堆上或只能在栈上分配内存?
//A a;//栈上创建
A* p = new A;//堆上创建
在C++中,创建类的对象有两种方法,一种是静态建立,A a; 另一种是动态建立,调用new 操作符。
只能在堆上分配内存:将析构函数声明为private;
编译器管理了对象的整个生命周期,编译器为对象分配空间的时候,只要是非静态的函数都会检查,包括析构函数,但是此时析构函数不可访问,编译器无法调用类的析构函数来释放内存,那么编译器将无法在栈上为对象分配内存。

只能在栈上生成对象:将new和delete重载为private。

private:
    void* operator new(size_t)
    {};
    void operator delete(void*)
    {};

如何让类只能创建一个对象?
单例模式。 构造函数,拷贝赋值都私有,创建一个静态的成员,静态的函数返回这个成员。

class singleton
{
public:
 static singleton* getInstance()
 {
  return &_single;
 }
private:
 //构造函数私有
 singleton(){};
 //防拷贝
 singleton(const singleton& s) = delete;
 singleton& operator=(const singleton& s) = delete;
 static singleton _single;
};

//静态成员的初始化
singleton singleton::_single;
// 在程序入口之前就完成单例对象的初始化

结构体内存对齐规则
一、结构体对齐规则首先要看有没有用**#pragma pack宏声明**,这个宏可以改变对齐规则,有宏定义的情况下结构体的自身宽度就是宏上规定的数值大小,所有内存都按照这个宽度去布局(这样说其实不太严谨,后面会提到),#pragma pack 参数只能是 ‘1’, ‘2’, ‘4’, ‘8’, or ‘16’。不过如果最大的类型都没有pack定义的大,就根据最大的类型优化。
二、在没有#pragma pack这个宏的声明下,遵循下面三个原则:
1、第一个成员的首地址为0.
2、每个成员的首地址是自身大小的整数倍
3、结构体的总大小,为其成员中所含最大类型的整数倍。

OSI七层模型其功能简介
物理层:规定了一系列的物理、电气、接口标准,传输的是比特流,主要实现0,1-电平-0,1的数模转换);
数据链路层:封装成帧,差错检测,透明传输,介质访问控制(MAC子层)。以及还有重要的MAC寻址作用。它的主要功能是如何在不可靠的物理线路上进行数据的可靠传递。帧是用来移动数据的结构包,它不仅包括原始数据,还包括发送方和接收方的物理地址以及检错和控制信息。
网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。
传输层:最后一公里,端口号,IP是尽力传输,不确保消息的可靠性。TCP,UDP
会话层:会话层管理主机之间的会话进程,即负责建立、管理、终止进程之间的会话。
表示层:多种数据格式之间的转换,表示层的数据转换包括数据的加密、压缩、格式转换等
应用层:提供各种各样的应用层协议,为用户与网络之间提供一个打交道的接口。

数据链路层的具体介绍:
MAC地址48比特,任何一个网卡的MAC地址是唯一的。共享介质型网络主要有以太网,FDDI,用一个信道发送接收信息,基本上是半双工。必须要有介质访问控制方式:争用方式和令牌传递方式

CSMA:先到先得,确认没有其他设备发送数据,立即发数据;接收端如果MAC地址不匹配就丢弃;
忙的话可以一直监听或等一段时间再监听。
CSMA/CD:载波监听多路访问/冲突检测:改进版,发送后一直在监听,如果冲突了,停止发送,延迟重发。
令牌环:不会有冲突,每个点有平等获得令牌的机会,网络拥堵不会造成性能下降。但是在网络空闲时利用率比较低。改进有:令牌追加,多个令牌等。

非共享介质:ATM。全双工。每个点都直连交换机,不会冲突。缺点就是布线成本很高,以及交换机故障,所有相连的点都无法通信。

以太网交换机就是由多个端口的网桥,根据目标MAC地址,决定从哪个接口发送数据。交换机自学的原理:如果转发表中没有该目标MAC的接口信息,执行洪泛操作,那么目标主机会回复,这样就知道了。

环路检测技术:生成树:每个网桥定期交换数据信息,判断哪些端口使用和不使用,一旦故障,就利用不使用的端口通信。 源路由法.

VLAN:虚拟局域网:不用修改实际路线,只需修改网络结构。实际上就是交换机按照端口区分了网段,减少的广播的流量和安全性。

以太网的帧格式:目标MAC地址,源MAC地址,协议,数据,FCS检验序列。

TCP,UDP区别和应用场景
1、TCP是面向连接的运输层协议;所谓面向连接就是双方传输数据之前,必须先建立一条通道,例如三次握手就是建议通道的一个过程,而四次挥手则是结束销毁通道的一个其中过程。UDP是无连接的传输层协议;
2、TCP提供可靠的传输服务。传送的数据无差错、不丢失、不重复、按序到达;UDP使用尽最大努力交付,不保证可靠交付;/
3、每一条TCP连接只能有两个端点(即两个套接字),只能是点对点的;UDP支持一对一 一对多 多对多的交互通信;/
4、UDP是面向报文的,对应用层交下来的报文,不合并,不拆分,保留原报文的边界;TCP是面向字节流的,即应用和TCP交互是一次一个数据块,但是TCP把它看成无结构的字节流,不关心一次收到多少数据,TCP根据网络拥塞情况来判断是否需要将数据块拆分,或者等待积累足够多的数据再一起发。正是由于这个特点,灵活发送但是TCP会产生粘包的问题。从TCP首部也能看出来,没有数据部分长度的字段。而UDP是不会产生粘包现象的,它发送的是独立的数据报,UDP首部有数据报长度字段,可以知道读取多少,发送读取的次数一致。

HTTP、HTTPS、FTP、TELNET、SMTP(简单邮件传输协议)协议基于可靠的TCP协议。DNS、DHCP、TFTP、SNMP(简单网络管理协议)、RIP基于不可靠的UDP协议

如何解决粘包问题?
表面原因:
1、发送方采用Nagle算法就可能产生粘包:Nagle是拥塞控制算法,当ftp每次敲击一个字符,就会传一个,此时数据只有1字节,但是TCP/ip首部却达到40字节,浪费资源,造成拥塞。所以nagle算法做了两件事:一是延迟发送小数据,等到上一个确认才发送,二是收集多个小分组,在一个确认到来时一起发送。关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。
2、接收方:如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
解决方法:对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用
TCP_NODELAY选项来关闭算法

接收方没有办法来处理粘包现象,只能将问题交给应用层来处理,解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了;发送端将每个数据包封装为固定长度(不够的可以通过补0填充);可以在数据包之间设置边界,如添加特殊符号。
(网络编程如何解决这个问题,当连续发送数据的时候,他们时常会认识tcp会丢包。其实不然,因为当他们使用的缓冲区足够大时,他们有可能会一次接收到两个甚至更多的数据包,而很多人往往会忽视这一点,只解析检查了第一个数据包,而已经接收的其他数据包却被忽略了。这个点可以具体编程实现,作为项目的一个点 https://www.cnblogs.com/wiessharling/p/4230878.html

TCP如何实现可靠传输的?
首先确保传输的信道是可靠的,通过三个握手四次挥手。
确认应答与序列号:保证了不遗漏不乱序不重复。
计算机基础面经积累---持续更新_第5张图片
超时重传:发送方在发送完数据后等待一个时间,时间到达没有接收到ACK报文,那么对刚才发送的数据进行重新发送。时间一开始是500ms,超时一次就是2*500ms,指数退避。到达一个阈值关闭了连接。

流量控制:TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。在TCP协议的报头信息当中,有一个16位字段的窗口大小。在介绍这个窗口大小时我们知道,窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大,证明接收端接收缓冲区的剩余空间越大,网络的吞吐量越大。接收端会在确认应答发送ACK报文时,将自己的即时窗口大小填入,并跟随ACK报文一起发送过去。而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。

拥塞控制:慢启动(拥塞窗口指数增长);拥塞避免(设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长)。拥塞之后阈值减半,窗口为1,开始新一轮的。
(扩展知识:基于丢包的控制并不好,随着带宽增大和无线链路的使用,丢包的可能性变大,而加性增乘性减的方式会导致带宽剧烈震荡,吞吐量不高;另外缓存大小增加,延迟高。最佳的应该是在刚出现拥塞就控制,丢包就是连缓冲区都满了,太迟了。比如BBR就是基于瓶颈链路带宽和往返传播时延,二者乘积为最佳控制点,网络充满包,之后将会有数据被缓存在链路的缓冲区中,再到后面就会丢包)

三次握手四次挥手考点总结
1、第一次握手:客户端给服务器发送一个 SYN 报文。2、第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。3、第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文服务器收到 ACK 报文之后,三次握手建立完成。
这样不好,太简单。
刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后
1、第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_Send 状态。
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 established 状态。
服务端收到ACK后也处于 established 状态,连接建立。
三次握手作用
1、需要三次握手才能确认双方的接收与发送能力是否正常。
2、指定自己的初始化序列号,为后面的可靠传送做准备。三次握手的一个重要功能是客户端和服务端交换ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果ISN是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。ISN主要为了防止历史报文被下一个相同四元组的连接接收(主要方面);
为什么两次握手不行?最后一次ACK没有行不行
防止
失效的连接请求报文段被服务端接收
,从而产生错误。如果网络拥塞,客户端发送的连接请求迟迟到不了服务端,客户端便超时重发请求,如果服务端正确接收并确认应答,双方便开始通信,通信结束后释放连接。此时,如果那个失效的连接请求抵达了服务端,由于只有两次握手,服务端收到请求就会进入ESTABLISHED状态,等待发送数据或主动发送数据。但此时的客户端早已进入CLOSED状态,服务端将会一直等待下去,这样浪费服务端连接资源

什么是半连接队列?
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。
SYN-ACK 重传次数的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,一定次数后从半连接队列删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s,

三次握手可以携带数据吗?
其实第三次握手的时候,是可以携带数据的。对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。第三次握手作用就是确保客户端没有失效。
假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。

四次挥手
计算机基础面经积累---持续更新_第6张图片

假如是客户端先发起关闭请求,则:
第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态
第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态
服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
TIME_WAIT要确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 FIN 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功就是 ACK 报文

为什么TIME-WAIT状态必须等待2MSL的时间
MSL指一个片段在网络中最大的存活时间
2MSL当然是2倍 这个存活时间了。
虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

TIME-WAIT状态过多会造成什么问题?
高并发短连接的TCP服务器上,当服务器处理完请求后立刻按照主动正常关闭连接。这个场景下,会出现大量socket处于TIMEWAIT状态。如果客户端的并发量持续很高,此时部分客户端就会显示连接不上。一个进程最大可以同时打开的文件描述符是有上限的,ulimit命令可以查到。

time_wait状态如何避免
调整服务器内核参数:tcp_max_tw_buckets 参数,当 TIME_WAIT 的连接数量超过该参数时,新关闭的连接就不再经历 TIME_WAIT 而直接关闭:tcp_tw_reuse 参数,允许将 TIME_WAIT sockets 重新用于新的 TCP 连接,复用连接。但需要注意,该参数是只用于客户端
首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。
客户端,调整短链接为长链接,HTTP 请求的头部,connection 设置为 keep-alive,保持存活一段时间;服务器端允许 time_wait 状态的 socket 被重用缩减 time_wait 时间,设置为 1 MSL(即,2 mins)
查看状态为 TIME_WAIT 的 TCP 连接
netstat -tan |grep TIME_WAIT

三次挥手可以吗?(第二次第三次可以合并吗?)
服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文, 但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序:
如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,
粗暴关闭 vs 优雅关闭
close 函数,同时 socket 关闭发送方向和读取方向,也就是 socket 不再有发送和接收数据的能力。会直接发送FIN包,再收到消息会回复RST,没有四次挥手了
如果有多进程/多线程共享同一个 socket,如果有一个进程调用了 close 关闭只是让 socket 引用计数 -1,并不会导致 socket 不可用,同时也不会发出 FIN 报文,其他进程还是可以正常读写该 socket,直到引用计数变为 0,才会发出 FIN 报文。

shutdown 函数,可以指定 socket 只关闭发送方向而不关闭读取方向,也就是 socket 不再有发送数据的能力,但是还是具有接收数据的能力。如果有多进程/多线程共享同一个 socket,shutdown 则不管引用计数,直接使得该 socket 不可用,然后发出 FIN 报文,如果有别的进程企图使用该 socket,将会受到影响。
什么情况会出现三次挥手?
当被动关闭方(上图的服务端)在 TCP 挥手过程中,「 没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。

DDos攻击
DDos的前身 DoS (DenialofService)攻击,其含义是拒绝服务攻击,这种攻击行为使网站服务器充斥大量的要求回复的信息,消耗网络带宽或系统资源,导致网络或系统不胜负荷而停止提供正常的网络服务。而DDoS分布式拒绝服务,则主要利用 Internet上现有机器及系统的漏洞,攻占大量联网主机,使其成为攻击者的代理。当被控制的机器达到一定数量后,攻击者通过发送指令操纵这些攻击机同时向目标主机或网络发起DoS攻击,大量消耗其网络带和系统资源,导致该网络或系统瘫痪或停止提供正常的网络服务。
SYN/ACK Flood攻击:攻击者通常会采用虚拟ip,所以也就意味着服务器永远不可能接收到最终的确认包。这种情况下当服务器未接收到最终ACK数据包的时候,服务端一般会重试(再次发送SYN+ACK给客户端)并等待一段时间后丢弃这个未完成的连接。么服务器端将为了维护一个非常大的半连接列表而消耗非常多的资源。从而造成服务器的崩溃,即使你的服务器系统资源够强大,服务端也将忙于处理攻击者伪造的TCP连接请求而无暇理睬客户的正常请求。
由于源IP都是伪造的故追踪起来比较困难,缺点是实施起来有一定难度,需要高带宽的僵尸主机支持,少量的这种攻击会导致主机服务器无法访问,但却可以Ping的通,在服务器上用 Netstat-na命令会观察到存在大量的 SYN RECEIVED状态,大量的这种攻击会导致Ping失败

TCP全连接攻击
这种攻击是为了绕过常规防火墙的检查而设计的,一般情况下,常规防火墙大多具备过滤 TearDrop、Land等DOS攻击的能力,但对于正常的TCP连接是放过的,殊不知很多网络服务程序(如:IIS、 Apache等Web服务器)能接受的TCP连接数是有限的,一旦有大量的TCP连接,即便是正常的,也会导致网站访问非常缓慢甚至无法访问,TCP全连接攻击就是通过许多僵尸主机不断地与受害服务器建立大量的TCP连接,直到服务器的内存等资源被耗尽面被拖跨,从而造成拒绝服务,这种攻击的特点是可绕过一般防火墙的防护而达到攻击目的,缺点是需要找很多僵尸主机,并且由于僵尸主机的IP是暴露的,因此此种DDOs攻击方容易被追踪。

TCP刷 Script脚本攻击
这种攻击主要是针对存在ASP、JSP、PHP、CGI等脚本程序,并调用 MSSQL Server、My SQL Server、 Oracle等数据库的网站系统而设计的,特征是和服务器建立正常的TCP连接,不**断的向脚本程序提交查询、列表等大量耗费数据库资源的调用,**典型的以小博大的攻击方法。一般来说,提交一个GET或POST指令对客户端的耗费和带宽的占用是几乎可以忽略的,而服务器为处理此请求却可能要从上万条记录中去查出某个记录,这种处理过程对资源的耗费是很大的,常见的数据库服务器很少能支持数百个查询指令同时执行,而这对于客户端来说却是轻而易举的,因此攻击者只需通过 Proxy代理向主机服务器大量递交查询指令,只需数分钟就会把服务器资源消耗掉而导致拒绝服务,常见的现象就是网站慢如蜗牛、ASP程序失效、PHP连接数据库失败、数据库主程序占用CPU偏高。这种攻击的特点是可以完全绕过普通的防火墙防护,轻松找一些Poxy代理就可实施攻击,缺点是对付只有静态页面的网站效果会大打折扣,并且有些代理会暴露DDOS攻击者的IP地址。

应付方法:网络设备,服务器硬件升级,让攻击成本变高;把网站做成静态页面,在需要调用数据库的脚本中拒绝使用代理的访问

乐观锁、悲观锁的概念实现及应用场景
什么是悲观锁?使用场景是什么?
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
像 Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
悲观锁通常多用于写多比较多的情况下(多写场景),避免频繁失败和重试影响性能。

什么是乐观锁?使用场景是什么?
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
在 Java 中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。
版本号:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
CAS 涉及到三个操作数:
V :要更新的变量值(Var)
E :预期值(Expected)
N :拟写入的新值(New)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。

乐观锁的问题:
ABA问题:一开始是1,中间改成6,后来有改成1,这时是可以更新的,但是确实被修改过
循环开销大:CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。可以用pause指令来优化,不用循环。
只能保证一个共享变量的原子操作

MTU和MSS分别是什么?
MTU = MSS + TCP Header + IP Header.
MTU:maximum transmission unit,最大传输单元,由硬件规定,如以太网的MTU为1500字节。这是以太网接口对IP层的约束,如果IP层有<=1500 byte 需要发送,只需要一个IP包就可以完成发送任务;如果IP层有> 1500 byte 数据需要发送,需要分片才能完成发送,这些分片有一个共同点,即IP Header ID相同。

MSS:Maximum Segment Size ,TCP提交给IP层最大分段大小,不包含TCP Header和 TCP Option,只包含TCP Payload ,MSS是TCP用来限制application层最大的发送字节数。如果底层物理接口MTU= 1500 byte,则 MSS = 1500- 20(IP Header) -20 (TCP Header) = 1460 byte,如果application 有2000 byte发送,需要两个segment才可以完成发送,第一个TCP segment = 1460,第二个TCP segment = 540。
路径MTU发现
数据链路不同,MTU则相异。经过分片之后的IP数据在被重组的时候只能由目标主机进行。路由器虽然做分片但不会进行重组。
分片机制也有它的不足:
路由器的处理负荷加重
随着人们对网络安全的要求提高路由器需要做的其他处理也越来越多
在分片处理中一旦某个分片丢失则会造成整个IP数据报作废
所谓路径MTU是指从发送端主机到接收端主机之间不需要分片时最大MTU的大小,即路径中存在的所有数据链路中最小的MTU。而路径MTU发现从发送主机按照路径MTU的大小将数据报分片后进行发送。进行路径MTU发现就可以避免在中途的路由器上进行分片处理,也可以在TCP中发送更大的包。
(怎么发现的,发送时IP首部的分片标志位设置为不分片。路由器丢包。由ICMP通知下一次MTU的大小。如此反复,直到数据报被发送到目标主机为止没有再收到任何ICMP,就认为最后一次ICMP所通知的MTU即是一个合适的MTU值。)

说一下MAC,TCP,IP,UDP头部的一些字段
MAC头部有源MAC地址(6字节,48位,如:08:00:20:0A:8C:6D就是一个MAC地址。IP地址是32位,4字节),目标MAC地址,上层协议类型2字节。中间是数据。尾部还有一个CRC校验4字节。

TCP首部:
计算机基础面经积累---持续更新_第7张图片
序号用来标识从TCP源端向TCP目标端发送的数据字节流,它表示在这个报文段中的第一个数据字节。
确认序号只有ACK标志为1时,确认号字段才有效。它包含目标端所期望收到源端的下一个数据字节。TCP规定, 在连接建立后所有传达的报文段都必须把ACK = 1
首部长度没有任何选项字段的TCP头部长度为20字节;最多可以有60字节的TCP头部。
窗口大小此字段用来进行流量控制
校验和对整个TCP报文段,即TCP头部和TCP数据进行校验和计算,并由目标端进行验证。

UDP首部
计算机基础面经积累---持续更新_第8张图片
UDP长度是包含UDP头部和UDP数据的总长度字节。(这一点可以看出TCP只有首部长度字段,基于字节流的,不知道报文商都,没有边界。而UDP是可以通过这个总长度识别报文边界)
校验和是可选项,TCP是必须的
UDP相比TCP简单多了,没有序号确认号,窗口,ACK等关键字。

IP头部
计算机基础面经积累---持续更新_第9张图片
版本号:IPV4,IPV6
首部长度一般也是20字节。
服务类型:时延,吞吐量,可靠性,花费等。一些动态路由协议会根据这个进行决策,比如OSPF
标识用来唯一地标识主机发送的每一份数据报。通常每发一份报文,它的值会加1。
标志位字段:占3比特。标志一份数据报是否要求分段
段偏移字段:占13比特。如果一份数据报要求分段的话,此字段指明该段偏移距原始数据报开始的位置。
TTL:每经过一个路由器,其值减1,直到0时该数据报被丢弃。
协议:”指明IP层所封装的上层协议类型
校验和:只对头部进行校验,数据部分不算,这点和TCP,UDP不一样,说明是不可靠的协议。

说一下常用的应用层协议
计算机基础面经积累---持续更新_第10张图片
struct可以定义函数吗?
在C++中,class和struct是同样的东西
区别仅仅在于class中的成员函数和变量如果不指定访问类型的话,缺省是private的,而struct中的成员函数和变量如果不知定访问类型,缺省是public的而已
其他的都是一样的了,所以struct可以有任何函数(构造、析构也包括在内)

explict关键字作用
主要用在构造函数上,而且是只有一个参数的构造函数(或者其他参数有默认值)。声明这个后,造函数不能在隐式转换中使用,只能显示调用,去构造一个类对象。
举个例子,A(int i,int j=2);这是一个只有一个参数的构造函数
A a=12;这个声明是可以的。编译器会自动执行复制构造函数。A tmp(12), a(tmp)

指针和引用区别
指针是一个存储对象地址的变量,指针本身也占用存储空间。引用是对象的一个别名,也可以看做是对象的地址。
引用必须声明的时候初始化,并且不能改变了,指针可以为空,也可以指向别的变量。
在作为函数参数时,指针作为参数实际上也是值传递,需要把实参拷贝给形参,实参形参独立的。如果在函数内指针指向别的地方,实参并不会改变。指针指向地址修改内容,实参会变因为实际上修改的是同一地址。而引用没有拷贝实参的过程。所以尽量用引用别用指针,特别是对于类对象,引用是标准形式,对于递归(栈空间有限),用指针可能会栈溢出。

你觉得堆快一点还是栈快一点?(可以推广堆和栈的区别,malloc,new实现区别和原理)
毫无疑问是栈快一点。
因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。

区别以下指针类型?
int *p[10]:表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
int (*p)[10] 数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
int *p(int) 是函数声明,函数名是p,参数是int类型的,返回值是int * 类型的。
int (*p)(int) 函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的

strlen和sizeof区别?
sizeof是计算对象占据的内存字节数,strlen是字符数组的长度
sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数
sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是’\0’的字符串。
因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小

常量指针和指针常量区别?
指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const 和 const int,都是一个常量,可以写作int const *p或const int *p。

常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能在改变了,即不能中途改变指向,如int *const p。

a和&a有什么区别?
假设数组int a[10],int *p=a;
那么a,p的区别:
首先说一下共同点:都可以通过偏移量来访问数组元素,a是数组首元素的地址,因此p就指向了数组的第一个元素a[0]。
所以 *(p+1)=a[1] *(a+1)=a[1]

不同点:a是一个常量指针,不能修改数组大小定以后无法修改,p是变量指针,可以修改;
在sizeof时,p大小是4字节,一个指针变量的大小,而a大小是40字节,数组的大小;在取地址后加1,&a+1,增加了40字节,而&p+1增加了4字节。也就是说当sizeof,&时,数组名不再是一个指向一个元素的常量指针。
另外,当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了

final和override关键字
当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写,以下方法都可以:

class A
{
virtual void foo();
}
class B : public A
{
void foo(); //OK
virtual void foo(); // OK
void foo() override; //OK
}

如果不使用override,当你手一抖,将**foo()写成了f00()**会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:

final
当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。

初始化和赋值的区别
对于简单类型来说,初始化和赋值没什么区别
对于类和复杂数据类型来说,这两者的区别就大了,举例如下:比如重载赋值运算符后,拷贝和赋值不一样的,
A a(b);
A a; a=b;

extern"C"的用法
为了能够正确的在C++代码中调用C语言的代码:在程序中加上extern "C"后,相当于告诉编译器这部分代码是C语言写的,因此要按照C语言进行编译,而不是C++;(除函数重载外,extern “C”不影响C++其他特性)。

C++比C出道晚,但是增加了很多优秀的功能,函数重载就是其中之一。由于C++需要支持重载,单纯的函数名无法区分出具体的函数,所以在编译阶段就需要将形参列表作为附加项增加到函数符号中.C和C++编译期间后得到的函数符号不同,所以C++代码和C代码不能互相调用。

//xx.h 
extern int add(...)

//xx.c
int add(){
    
}

//xx.cpp   C++调用C函数
extern "C" { 
    #include "xx.h"
}
//xx.h
extern "C"{
    int add();
}
//xx.cpp
int add(){    
}
//xx.c
extern int add();  C调用C++函数

你可能感兴趣的:(服务器,java,linux)