前言
面试总结第三波,关于后台开发面试相关基础知识,数据结构、算法、linux操作系统、计算机网络、C++、数据库进行分类总结。
后端面试总结
目录
1、数据结构
链表和数组的区别
内存分布:链表是无连续的内存空间,通过指针来实现链式存储;数组是一段连续的内存空间,一般大小需提前知道。
增:链表:无序链表:O(1),有序链表:O(n);数组:无序数组:内存充足O(1),内存不充足O(O(n)+1)=O(n),有序数组:O(n) 查找+移动
删:链表:O(O(n)+1)=O(n) 找到+删除;数组:末尾 O(1),非末尾O(1)+O(n) =O(n) 删除+移动
查:链表:O(n);数组:O(1)
树的先序、中序、后序遍历
波兰序列:先序遍历
逆波兰序列:后序遍历
表达式:8+(3-1)*5
先序遍历:+ 8 * - 3 1 5
中序遍历:8 + 3 - 1 * 5
后序遍历:8 3 1 - 5 * +
栈:也可以模拟整个过程
双链表的增删查
结构图:
插入:
删除:
图
深度优先搜索
广度优先搜索
树
平衡二叉树:操作时间复杂度logn,维持平衡比较复杂
二叉排序树:中序遍历为有序序列
红黑树:自平衡二叉查找树,从根节点到叶子节点最长长度不多于最短长度的两倍
B树:多路搜索树
B+树:特殊的B树,数据都在叶子节点,根节点一般用于索引,常用于文件的索引结构
排序
堆排:O(nlogn)、不稳定、n大时较好
希尔:O(nlogn)、不稳定、n小时较好
快排:平均O(nlogn)、不稳定、n大时较好
冒泡:O(
)、稳定、n小时较好
稳定排序:冒泡、插入、归并
不稳定排序:快排、希尔排序、选择排序、堆排序
2、算法
典型的算法案例
- 存储10亿个INT型qq号,但所给的内存只有1G:一个int占4个字节,所以一共需要4G内存,使用哈希+位图,每个qq用一位进行存储,一个int可以存储32个了。
- 在1亿数中找出前1000个大的数,内存只有4KB:堆排序
- 找出第K大的数:快排
int find_k(vector<int> &q,int k,int left,int right){ int l=left-1,r=right+1,x=q[l+r>>1]; while(l<r){ do l++; while(q[l]<x); do r--; while(q[r]>x); if(l<r) swap(q[l],q[r]); else { if(r==k-1) return q[r]; else if(r>k-1) return find_k(q,k,left,r-1); else return find_k(q,k,r+1,right); } } }
- 大整数乘法:利用分治思想
3、linux
指令
netstat:显示与IP、TCP、UDP、ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况
tcpdump:对网络上的数据包进行捕获,输出信息:系统时间 来源主机.端口>目的主机.端口 数据包参数
ipcs:提供关于一些进程间通信方式的信息,包括共享内存、消息队列、信号。
ipcrm:移除一个消息对象,共享内存、信号集,同时将与ipc对象相关链的数据也一起移除。
top:相当于windows下的资源管理器,能够动态实时的显示系统中进程的资源占用情况。包括进程信息、CPU信息、内存信息。top -Hp pid查看某个进程的线程信息。
awk:强大的文本分析工具,对数据分析并生成报告,使用方法:awk '{pattern+action}'{filenames} 。
sed:实现对文件的增删查改,sed [ ] '{ }' {filename} p 、d、 =(打印匹配行的行号)、-n(只要需要的输出)、-e(允许多项编辑)、-i(修改文件内容)
ps aux --sort -rss:对进程按照内存使用情况进行排序
ps aux --sort=-pcpu | head -10:按CPU使用比排序
共享内存
实现原理:通过将不同的虚拟内存映射到同一块物理内存,实现共享;
共享内存段被映射到进程空间后,存在于进程空间的什么位置:栈和堆之间的共享段
共享内存段最大限制是:32M
ELF文件(可执行可链接文件)
源代码经过编译器编译后生成的文件叫做目标文件,而目标文件经过编译器链接后得到的就是可执行文件。动态链接库.so和静态链接库.a也是以可执行文件格式存储。
基本结构:ELF Header、程序头、节点表、ELF Sections
注:段与节的区别,段是程序执行的必要组成,当多个目标文件链接成一个可执行文件时,会将相同权限的节合并到一个段中。相比而言,节的粒度更小。
.bss节:存在于data段中,占用空间不超过4字节,仅表示这个节本身的空间,由于.bss节未保存实际的数据,所以节类型为SHT_NOBITS。
分类:1)可重定向文件:保存着代码和适当的数据,例如:目标文件或静态库文件,即后缀为.o和.a的文件
2)可执行文件:保存着用来执行的程序,例如:bash,gcc等
3)共享目标文件:共享库,用来连接编辑器和动态链接器链接,例如:后缀为.so的文件
进程间通信
管道:pipe(无名管道)有血缘关系(父子进程)、fifo(有名管道)无血缘关系
消息队列:内核中有一个消息队列,可以向队列添加消息,也可以向队列获取消息,消息的链表,存放在内核中并由消息队列标识符表示。消息队列提供了一个从一个进程向另一个进程发送数据块的方法,每个数据块都可以被认为是有一个类型,接收者接收的数据块可以有不同的类型。每个消息的最大长度是有上限的。内核为每个IPC对象维护了一个数据结构struct ipc_perm,用于标识消息队列,让进程知道当前操作的是哪个消息队列。msgget、msgctl、msgsnd、msgrcv
信号量:ctrl C,kill
内存映射:mmap,最快
socket:用于两个不同的主机间
makefile文件编写
SrcFiles=$(wildcard *.c) ObjFiles=$(patsubst %.c,%,$(SrcFiles)) all:$(ObjFiles) %:%.c gcc -o $@ $^ .PHONY:clean clean: -rm -f $(ObjFiles)
gdb调试
编写makefile文件时,对于gcc后面加上-g
多线程调试:1)先在进入子线程前,加上sleep,防止attach后,不知道运行到哪里了,确保在子进程在睡眠状态结束之前,attach上子进程;2)先ps -aux|grep xxx,查出子进程id;3)attach id;4)stop、break、continue、step等
如何定位内存泄漏?
可用一个工具,VLD(Visual Leak Detector),可安装作为VS的一个插件。
动态链接和静态链接的区别
静态链接:#pragma comment(lib,“test.lib”),静态链接的时候,载入代码就会把程序会用到的动态代码或动态代码的地址确定下来;
动态链接:使用这种方法的程序并不在一开始就完成动态链接,而是直到真正调用动态库代码时,载入程序才计算(被调用的那部分)动态代码的逻辑地址。所以这种方法使程序初始化时间较短,但运行期间的性能比不上静态链接的程序。
动态库与静态库
动态库:多个程序可以使用同一个动态库,启动多个应用程序的时候,只需要将动态库加载到内存一次即可
静态库:代码的装载速度快,执行速度也快,因为编译时只需要把那部分链接进来,应用程序相对较大,而且多个应用程序使用的话,会被装载多次,浪费内存。
找不到头文件:编译时使用-I来指定头文件路径
找不到库文件:把库文件放入系统的库文件目录下,如/lib;或者把库文件所在的目录加入到对应的环境变量中
32位系统一个进程最多多少堆内存
理论上为:4G-1G=3G,如果报错,会出现std::bad_alloc
栈内存一般只有几M
进程/线程/协程
进程:系统分配资源的基本单位,每一个进程创建出来,都会分配三种基本的内存资源,分别是代码段、数据段和堆栈段。栈空间是子任务(线程、协程)独立存放自己的数据地方,比如:函数调用、参数、返回值和局部变量。这样一来,子任务之间就可以独立运行,而且还可以共享堆空间中的变量数据。
线程:CPU调度的基本单位,操作系统不仅仅维持一个进程表,而且还会维持一个线程表,这样操作系统就可以把线程作为调度单位。线程是在进程内创建,可以共享进程的资源,所以,线程自身独立的资源依赖就会少很多,因为只需要为每个线程分配独立的栈空间。而线程的栈空间是固定大小的,如果程序比较复杂,或者里面的数据量大,为了不出现“栈空间不足”的错误,就必须把栈空间设置的足够大才行。
协程:是可以在应用态协作的程序,它的调度不是操作系统处理,而是应用系统自己来调度处理,也称为轻量级线程。协程作为应用系统内调度的子任务单元,当然也是会共享进程的各种资源,除了自己的栈空间(函数调用、参数、返回值、局部变量)。
而协程和线程主要区别有两个,1)最大的就是调度方式,线程是操作系统调度,协程是应用系统自己调度;2)协程的栈空间是可以动态调整的,这样空间利用率就可以更高,一个任务需要2K空间就分配2K空间,一个任务需要20M空间就分配20M,而不用担心栈空间不够或者空间浪费。
协程的优势:1)协程可以更好的利用CPU,不用把CPU浪费在线程调度和上下文切换上;2)协程可以可以更好的利用内存,不用全部分配一个偏大的空间,只需要分配需要的对应空间即可。
队列:应用中维持数据的一个队列,很多时候会是一个数组或者链表。队列里保存的也不是一个子任务,而只是一个数据,具体这个数据拿出来之后要启动什么子任务,这个队列是不关心的。
队列只是一个缓冲带,把更多的独立数据先临时保持住,应用系统有多大的能力消化吸收就从里面用多快的速度进行处理。
多线程模式是为每个网络请求创建一个线程来处理这个请求,当请求执行结束,再销毁这个线程。于是,当网络的请求量高的时候,意味着反复的为这些请求创建和销毁线程,这个开销就变得比较大,效率也就下降了。
在多进程模式下,进程是复用的,不会反复的创建和销毁,所以就没有之前多线程模式那样大的资源浪费了。当多进程内存开销大,系统调度开销大,所以也就意味着并发量相对就会较小。
线程池:可以复用线程,很好的避免了线程频繁创建和销毁所带来的损耗。但线程栈空间是固定的,在一些个别请求中,数据量很大时,也可能会不得已要设置较大的栈空间,这样一来,内存浪费也是比较严重了。
多进程和多线程的区别
多进程:使用fork,子进程会复制父进程的task_struct结构体,并为子进程的堆栈分配物理页。理论上,子进程应该完整地复制父进程的堆、栈以及数据空间,但实际上是进行写时复制。
有各自的对立运行地址空间,互不干扰,可靠性高,进程的创建、销毁、切换复杂,速度慢,占用内存大,通信复杂,但同步简单,适用于多核多机分布,CPU密集型。
多线程:线程就是把一个进程分为很多片,每一个片都可以是一个独立的流程,进程是一个拷贝的流程,而线程没有拷贝这些额外的开销。
共享同一个进程的地址空间,但线程间会相互影响,一个意外终止会导致同一个进程的其他线程也终止,可靠性弱,线程的创建、销毁、切换简单,速度快,通信简单,同步复杂,适用于单机多核分布式,I/O密集型。
哪些东西是线程私有的?
1)线程id;2)堆栈;3)寄存器值,以便线程切换时得以恢复;4)错误返回码error;5)信号屏蔽码;6)线程的优先级
写一个c程序辨别系统是64位还是32位
判断一个指针的size,如果是4则是32位,如果是8则是64位
写一个c程序辨别系统是大端还是小端节序
用union,如果是小端:则从高字节向低字节读,如果是大端:则从低字节向高字节读
常见信号
SIGHUP 终端挂起,终止进程
SIGINT 键盘中断 CTRL+C
SIGQUIT CTRL+D 终止进程
SIGKILL 强制退出
SIGALRM 定时器超时
同步机制,什么是死锁,如何避免死锁
互斥锁:如果要进入一段临界区需要多个mutex锁,那么就很容易导致死锁。解决:申请锁的时候按照固定顺序,或者及时释放不需要的互斥锁就可以。
读写锁:读时可进入临界区
自旋锁:适用于临界区短,线程需要等待的时间也短,即便轮询浪费CPU资源,也浪费不了多少,还省了context切换的开销。
条件变量:在合适的时候唤醒正在等待的线程,必须和互斥锁联合起来用
信号量:wait和post
死锁:两个进程或两个以上进程在执行过程中,因争夺资源而造成的相互等待的现象。
死锁发生四个条件:1)互斥条件;2)请求和保持条件——资源一次性分配;3)不可剥夺条件——当进程新的资源未得到满足时,释放已占有的资源;4)环路等待条件——系统给每个资源分配一个序号,每个进程按编号递增请求资源,释放则相反。
异步机制
信号:进程也不知道信号到底什么时候到达
epoll:一种高效处理IO的异步通信机制
epoll水平触发(LT)和边沿触发(ET)
水平触发:只要缓冲区有数据就会一直触发
边沿触发:只有在缓冲区增加数据的那一刻才会触发
在设置边沿触发时,因为每次发送消息只会触发一次(不管缓冲区是否还留有数据),所以必须把数据一次性读取出来,否则会影响下一次消息。需要用while(recv())这里会发生阻塞,所以需要设置非阻塞状态。
exit()和_exit()的区别
exit():要调用终止处理程序和清楚IO缓存后再退出
_exit():立即终止程序
如何实现守护进程
1)fork();2)父进程退出;3)子进程当会长setsid;4)切换当前目录到home目录;5)设置掩码权限umask(0);6)关闭文件描述符;7)执行核心程序
内存管理机制
有物理内存和虚拟内存,采用分页管理,每个页的大小是4KB,进程运行时,先分配虚拟页表,而不把数据和代码加载到物理内存。
任务调度机制
经过两个过程:选择算法+上下文切换
任务调度可分为:主动调度和被动调度(被强占)
系统调用和标准库函数
很多库函数是对系统调用的一个封装
系统调用:需要在用户空间和内核空间之间切换,开销较大
标准库函数:可以利用缓冲区,等往缓冲区写完了,再系统调用一次性把数据写入到硬件媒介
系统如何将一个信号通知到进程
进程有一个链表的数据结构,维护一个未决信号链表,信号在进程中注册,其实就是把该信号加入到这个未决信号链表中。进程处理信号的时机就是从内核态即将返回用户态的时候。
执行用户自定义的信号处理函数的方法很巧妙,把该函数的地址放在用户栈栈顶,进程从内核态返回到用户态的时候,先弹出信号处理函数地址,于是就去执行信号处理函数了,然后再弹出,才是返回进入内核时的状态。
被屏蔽的信号,取消屏蔽后还会被检查
linux中/etc和/var目录
/var:包含系统一般运行时要改变的数据,通常这些数据所在的目录的大小是要经常变化或扩充的。
/var/lib:存放系统正常运行时要改变的文件。
/etc:包含各种系统配置文件。
线程的五大状态
新建状态——>就绪状态——>运行状态——>阻塞状态、死亡状态
阻塞状态——>就绪状态
4、计算机网络
tcp和udp区别
tcp:面向连接的、可靠的、面向字节流、数据传输慢、仅有两方进行彼此通信
udp:无连接、不可靠、面向报文、数据传输快、可以一对多通信,所以适用于广播和组播
tcp头
最长60字节=固定20字节+可变长的可选信息40字节
源端口、目的端口、32位序列号、32位确认号、6个标志位(SYN、ACK、FIN)、16位滑动窗口大小
connect
connect在进行三次握手,如果失败情况,需要等待75s的超时时间,即出现阻塞,设置套接字为非阻塞状态:setBlockOpt(fd,false);,然后使用select或者poll等机制来检测套接字一定时间,如果在超时时间内不可写,则认为connect失败。
如果select返回可读,结果只读到0字节,什么情况?
如果在一个描述符碰到了文件尾端,则select会认为该描述符是可读的。然后调用read,它返回0,这是指示到达文件尾端方法。
tcp选项
kind=2:最大报文段长度(MSS)选项
kind=3:窗口扩大因子选项
kind=4:选择性确认(SACK)选项
kind=8:时间戳选项
socket的读和写
读:1)接收缓冲区中已接收的数据字节数大于等于阈值时,这时不阻塞,返回大于0,可用SO_RCVLOWAT来设置此阈值
2)接收到对方发过来的FIN的TCP连接时,这时不阻塞, 返回0(即文件结束符,FIN包体长度为0字节)
3)socket是一个用于监听的socket,并且已经完成的连接数为非0。这样的socket处于可读状态,是因为socket收到了对方的connect请求,执行了三次握手的第一步,对方发送SYN请求过来,使监听socket处于可读状态。
4)有一个socket有异常错误条件待处理,不会阻塞,返回一个错误-1
写:1)socket发送缓冲区中可用空间字节数大于等于socket发送缓冲区阈值。
2)连接的写这一半关闭,对于这样的socket的写操作将会产生信号SIGPIPE
3)有一个socket异常错误条件待处理
TCP三次握手/4次挥手
三次握手:客户端向服务器端发起连接请求,让SYN标志位置1,并填入32位的随机序号,一起发送到服务器端;服务器端同样让SYN标志位置1,并填入32位随机序号,和让ACK标志位也置1,并把客户端发送过来的随机序号+1作为确认序号,一起发送到客户端;客户端让ACK标志位置1,把服务端发送过来的随机序号+1作为确认序号,发送到服务端。
客户端:connect()——>SYN_SENT——>ESTABLISHED
服务端:listen()——>SYN_RECVD——>ESTABLISHED
四次挥手:任意一端都可以发起断开请求,假设客户端向服务端发起断开请求,客户端让FIN标志位置1,把最后一次收到服务端发过来的确认号作为序号,和让ACK标志位置1,把收到的数据作为确认号,一起发送到服务端;服务端先发送ACK确认;再同样发送FIN;客户端发送ACK确认
客户端:FIN_WAIT_1——>FIN_WAIT_2——>TIME_WAIT——>CLOSED
服务端:CLOSE_WAIT——>LAST_ACK——>CLOSED
TIME_WAIT:会等待2MSL(报文最大生存时间),等待服务端收到最后的ACK,对方如果没收到,就会超时重传FIN;可以使本次连接中迟缓的时间所产生的报文都从网络中消失,这样可以使下一个新的连接中不会出现这种旧的连接请求报文。
TIME_WAIT状态如何避免
首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。
从输入URL到显示页面,后台发生了什么?
1)在浏览器输入网址
2)浏览器查询域名的IP地址:DNS查找:1.浏览器缓存;2.系统缓存;3.路由器缓存;4.ISP DNS缓存;5.递归搜索
3)浏览器给web服务器发送一个HTTP协议
4)负载均衡:当一台服务器无法支持大量的用户访问时,将用户分摊到两个或多个服务器上的方法叫负载均衡。Nginx是一款面向性能设计的HTTP服务器。web服务器受到请求,产生响应,并将网页发送给Nginx负载均衡服务器。Nginx负载均衡服务器将网页传递给filters链处理,之后发回给我们的浏览器。
4)服务器给浏览器发送HTML响应
5)浏览器开始显示HTML
6)浏览器发送获取嵌入在HTML中的对象,比如图片
DNS解析过程
1)用户向ISP-DNS发起DNS递归查询,必须返回域名-IP映射关系;2)ISP-DNS查本地cache;3)向根-DNS发起迭代DNS查询;4)返回通用顶级域名;5)继续迭代返回IP
ARP地址解析协议
IP地址与物理地址之间的转化。
获取目的端口的MAC地址(在一个以太网中)步骤:1)发送ARP请求的以太网数据帧给以太网上的每个主机,即广播(以太网源地址全填1)。ARP请求帧包含了目的主机的IP地址;2)目的主机收到了该ARP请求后,会发送一个ARP应答,里面包含了目的主机的MAC地址。
拥塞控制
避免更多的数据包流入到网络中,引起路由器和数据链路过载。
慢启动、拥塞避免、快重传、快速恢复
ssthresh(慢开始门限)、cwnd(拥塞窗口)
ICMP协议
网络层协议,可以传递差错报文以及其他信息
ping程序:发送ICMP回显请求给主机,等待主机返回回显应答,来测试另一台主机是否可达。可根据往返时间来确定主机离我们多远,应答和请求之间进行通过序列号字段来实现,从0开始,每次加1,通过ping打印序列号来确定分组是否有丢失、失效或者重复。
ping获取目的主机的地址:通过IP记录路由选项,ping提供了一个-R选项来记录路由功能,最后复制到ICMP回显应答中。
cookie和session
http请求是无状态的, 也就是说即使第一次和服务器连接后并且登陆成功后,第二次请求服务器依然不能知道当前请求是哪个用户。第一次登陆后,服务器返回一些数据(cookie)给浏览器,然后浏览器保存在本地,当该用户发送第二次请求时,就会自动把上次请求存储的cookie发送给服务器。一般不会超过4KB。
session:存储在服务器,更安全,不易窃取,会占用服务器资源
cookie:存储在本地浏览器
http协议
1)http协议与TCP/IP协议的关系
http的长连接和短连接本质上是TCP长连接和短连接。http属于应用层协议,在传输层使用tcp协议,在网络层使用ip协议。ip协议主要解决网络路由和寻址问题,tcp协议主要解决如何在ip层之上可靠地传递数据包,使得网络上接收端收到发送端发出的所有包,并且顺序与发送顺序一致。TCP协议是可靠的、面向连接的。
2)http协议是无状态的
协议对事务处理没有记忆能力,服务器不知道客户端是什么状态。也就是说,打开一个服务器上的网页和上一次打开这个服务器上的网页之间没有任何联系。
3)什么是长连接、短连接?
http/1.0中默认使用短连接,也就是说,客户端和服务器每次进行一次http操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的web页中包含有其他的web资源,每遇到这样一个web资源,浏览器就会重新建立一个http会话。所以并发量大,但每个用户无需频繁操作情况下用短连接好。
而从http/1.1起,默认使用长连接,用以保持连续性。使用长连接的http协议,会在响应头加入这行代码:Connection:keep-alive,在使用长连接的情况下,当一个网页打开完成后,客户端与服务器之间用于传输http数据的tcp连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。keep-alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件中设定这个时间。实现长连接需要客户端与服务端都支持长连接。多用于操作频繁,点对点的通讯,而且连接数不能太多情况。
惊群问题
惊群是指当多个进程/线程在等待同一个资源时,每当资源可用时,所有的进程/线程都来竞争资源的现象,但最终只可能有一个进程/线程对该事件进行处理,其他进程/线程会在失败后重新休眠,这种性能浪费就是惊群。惊群造成的结果是系统对用户进程/线程频繁的做无效的调度、上下文切换,系统性能大打折扣。
在linux2.6版本以后,内核已经解决了accept()函数“惊群”问题,大概处理的方式就是,当内核接收到一个客户连接后,只会唤醒等待队列上的第一个进程或线程。
在实际中,大都使用select、poll或epoll机制,此时,服务器不是阻塞在accept,而是阻塞在select、poll或epoll_wait,这种情况下的“惊群”仍然需要考虑。fork多个子进程,在有事件发生时调用epoll_wait开始accept连接,只有一个进程接收到连接,其他没有,说明没有发生惊群现象,这是因为只会唤醒等待队列上的第一个进程或线程。但这个只是部分解决,比如:在epoll_wait后调用sleep一次,就会出现惊群问题,解决:可使用mutex互斥锁解决这个问题,每个子进程在epoll_wait之前先去申请锁,申请到则继续处理,获取不到则等待,并设置了一个负载均衡的算法(当某一个子进程的任务量达到总设置量的7/8时,则不会再尝试去申请锁)来均衡各个进程的任务量。
TCP的RST报文
RST:用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到RST位时候,通常发生了某些错误。发送RST包关闭连接时,不必等缓冲区的包都发出去,直接就丢弃缓冲区中的包,发送RST;接收端收到RST包后,也不必发送ACK包来确认。Socket.close()表示我不在发送也不接受数据了。
产生RST报文的几种场景
1)connect一个不存在的端口;
2)向一个已经关掉的连接send数据;
3)向一个已经崩溃的对端发送数据(连接之前已经被建立);
4)close(fd)时,直接丢弃接收缓冲区未读取的数据,并给对方发送一个RST,这个是由SO_LINGER选项来控制;
5)a重启,收到b的保活探针,a发RST通知b。
MSL、TTL和RTT
MSL:Maximum Segment Lifetime报文最大生存时间
TTL:time to live生存时间,这个生存时间是由源主机设置初始值但不是存的具体时间。
RTT:round-trip time客户端到服务器往返所花时间
5、C++
new与malloc的区别
1)申请的内存所在位置:new是在自由存储区上为对象动态分配内存空间,那么自由存储区是否是堆?取决于operator new的实现细节,自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。还有定位new可以不为对象分配内存,只是简单返回指针实参;malloc函数从堆上分配内存。
2)返回类型安全性:new操作符返回的是对象类型的指针,类型严格与对象匹配,无需进行类型转换,故new是符合类型安全性的操作符;malloc内存分配成功返回void*,需要强制类型转换将void*指针转换成我们需要的类型。
3)内存分配失败时的返回值:new会抛出bad_alloc异常,它不会返回NULL,所以需要使用异常机制(try...catch);malloc会返回NULL。
4)是否需要指定内存大小:new不需要指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指定所需内存地大小。
5)是否调用构造函数/析构函数:new会;malloc不会。
6)对数组的处理:new对数组的支持体现在它会分别调用构造函数初始化每一个数组元素,释放对象时为每个对象调用析构函数;malloc它并不知道你在这块内存上要放的是数组还是啥的别的东西,反正它就给你一块原始的内存,在给你内存的地址就完事,所以如果动态分配一个数组的内存,还需要我们手动自定数组的大小。
7)new与malloc是否可以相互调用:operator new/operator delete的实现可以基于malloc;而malloc的实现不可以去调用new。
8)是否可以被重载:operator new/operator delete可以被重载;而malloc/free并不允许重载。
9)已分配内存得扩充:new没有配套设施来扩充内存;malloc分配内存后,如果使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。
10)客户处理内存分配不足:operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new_handler,new_handler是一个指针类型,指向一个没有参数没有返回值得函数,即错误处理函数,客户需要调用set_new_handler(new_handler p);而malloc,客户并不能够去编程决定内存不足以分配时要干什么,只能看着malloc返回NULL。
使用引用减少拷贝构造函数使用次数
sizeof
sizeof(空类),答案是1B,而不是0B,我们在声明该实例的时候,必须给实例在内存中分配一定的空间,否则无法使用该实例,由于空类型不含任何信息,故而所占内存大小由编译器决定。
切记:一旦类中有其他的占用空间成员,则这1个字节就不在计算之内。
static变量不会占用类的大小,因为它存储在静态存储区。
若d由b,c派生而来,b的大小是1,c的大小是4,则d的大小是8而不是5,这是因为为了提高实例在内存中的存取效率,类的大小往往被调整到系统的整数倍,并采取就近的法则,是哪个最近的倍数,就是该类的大小,所以类d的大小为8个字节。
虚继承
虚继承是解决C++中多重继承问题的一种手段,从不同途径继承来的同一个基类,会在子类中存在多份拷贝。这将存在两个问题:1)浪费空间;2)存在二义性。
虚继承底层原理与编译器相关,一般通过虚基类指针vbptr,该指针指向了一个虚基类表,虚表中记录了虚基类与直接继承类的偏移地址,通过偏移地址,这样就可以找到虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类的两份拷贝,节省了存储空间。
模板特例化与实例化
模板特例化:如果我们想对特定的数据类型执行不同的代码(而不是通用模板),这种情况下就可以使用模板特例化。
函数模板特例化:当特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。使用关键字template后跟一个空尖括号<>,即template <>,以指出我们正在特例化一个模板。
template<> void fun(int a){}
类模板特例化
template<> //对int型特例化 class Test<int>{};
实例化:使用类时后面加<>指明具体类型。
栈溢出几种情况
1)局部数组过大。当函数内部的数组过大时,有可能导致栈溢出;
2)递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出;
3)指针或数据越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。
模板与多态的使用场景
模板:STL标准库中的容器就是模板类
多态:设计模式
STL容器的线程安全
(1)线程安全的情况:1)多个读者是安全的。多线程可能同时读取一个容器的内容,这将正确地执行;2)对不同容器的多个写入者是安全的。多线程可以同时写不同的容器。
(2)线程不安全的情况:1)在对同一个容器进行多线程的读写、写操作时;2)在每次调用容器的成员函数期间都要锁定该容器;3)在每个容器返回的迭代器(例如通过调用begin或end)的生存期之内都要锁定该容器;4)在每个在容器上调用的算法执行期间锁定该容器。
C/C++中volatile关键字
1)为什么用volatile
volatile关键字和const对应,用来修饰变量,用它声明的类型变量表示可以被某些编译器未知的因素更改。比如,操作系统、硬件或者其他线程。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明语法:int volatile vInt;当要求使用volatile声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
2)多线程下的volatile
防止优化编译器把变量从内存装入CPU寄存器中,volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。
STL sort函数实现详解
除了对普通的快速排序进行优化,它还结合了插入排序和堆排序。根据不同的数量级以及不同情况,能自动选用合适的排序方法。当数据量较大时采用快速排序,分段递归。一旦分段后的数据小于某个阈值,为避免递归调用带来过大的额外负荷,便会改用插入排序。而如果递归层次过深,有出现最坏情况的倾向,还会改用堆排序。
6、数据库
基本操作
为表中的字段添加索引:alter table 表名 add index(字段)
多列索引:最左前缀匹配规则(a,b,c)相当于创建了(a)单列索引,(a,b)组合索引以及(a,b,c)组合索引。
增加一列:alter table 表名 add column 列名 varchar(20) not null after 列名;
删除一列:alter table 表名 drop column 列名;
插入一行:insert into 表名 values( , , ,);
更新数据:update 表名 set a=b where c=' ';
删除一行:delete from 表名 where a=' ';
建表:
create table tb1( id int not null, title varchar(100) not null, );
删除表:drop table db;
加锁:共享锁:select * from db where ... lock in share mode; 排他锁:select ... for update;
一条SQL语句执行得很慢的原因有哪些?
(1)大多数情况下是正常的,只是偶尔会出现很慢的情况。
1)数据库在同步数据到磁盘的时候。当我们要插入或更新一条数据时,数据库会在内存中把对应字段的数据更新,但是更新后,这些更新的字段并不会马上同步持久化到磁盘中,而是把这些更新的记录写入到redo log日志中,等到空闲的时候,在通过redo log里日记把最新的数据同步到磁盘中。不过,redo log里的容量是有限的,如果数据库一直很忙,更新又很频繁,这个时候redo log很快会被写满,这个时候就没办法等到空闲的时候再把数据同步到磁盘的,只能暂停其他操作,全身心来把数据同步到磁盘中,而这个时候,就会导致我们平时正常的SQL语句突然执行的很慢。
2)拿不到锁:我们要执行这条语句,刚好这条语句涉及到表,别人在用,并且加锁了,我们拿不到锁,只能慢慢等待别人释放锁了。或者,表没有加锁,但要使用到的某一个行被加锁了。如果要判断是否真的在等待锁,我们可以用show processlist这个命令来查看当前的状态。
(2)在数据量不变的情况下,这条SQL语句一直一来都执行的很慢。
1)没有用到索引:你要查询的字段没有用索引,那么只能走全表扫描了,导致这条查询语句很慢。
2)字段有索引,但没有用索引:select * from t where c-1=1000;
3)函数操作导致没有用上索引
4)数据库自己选错索引:主键索引存放的值是整行字段的数据,而非主键索引上存放的是主键字段的值。所以在选择时,会进行判断是走索引的行数少?还是直接扫描全表行数少?通过索引的区分度来预测判断要扫描的行数,我们把区分度称为基数,即区分度越高,基数越大。通过采样的方式来预测索引的基数。误测基数很小,然后误以为索引的基数很小。
B+树索引和hash索引的区别
1)如果是等值索引,那么哈希索引明显有绝对的优势,因为只需要一次算法即可找到相应的键值,当然前提是键值都是唯一的,如果键值不唯一,就需要先找到该键所在的位置,然后再根据链表往后扫描,直到找到相应的数据。
2)若是范围查找,这时候hash索引就不行了,因为原先有序的键值,经过哈希算法后,有可能变成了不连续的,就没办法利用索引完成范围查询检索了。
3)hash索引也没办法利用索引完成排序,以及like 'xxx%'这样的部分模糊查询
4)hash索引也不支持多列联合索引的最左匹配规则
5)B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在所谓的哈希碰撞问题。
聚集索引和非聚集索引
聚集索引:就是对这堆记录进行堆划分,即主要描述的是物理上的存储。即不同的书放到不同的房间。可以帮助把很大的范围,迅速减小范围。但是要查找该记录,就要从这个小范围中scan了。
非聚集索引:通过索引表查询记录所在的位置,然后通过位置去取要找的记录。把一个很大的范围,转换成一个小的地图。你需要在这个小地图中找到你要寻找的信息的位置,然后通过这个位置,再去找你所需要的记录。
悲观锁与乐观锁
悲观锁:先获取锁,再进行业务操作。1)在对任何记录进行修改之前,先尝试为该记录加上排他锁;2)如果加锁失败,说明记录正在被修改,那么当前查询可能要等待或抛出异常;3)如果成功加锁,则就可以对记录做修改,事务完成后就会解锁;4)期间如果有其他对记录做修改或加排他锁,都会等待我们解锁或直接抛出异常。
特点:1)为数据处理的安全提供了保证;2)效率上,由于处理加锁的机制会让数据库产生额外开销,增加产生死锁机会;3)在只读型事务中由于不会产生冲突,也没必要使用锁,这样就会增加系统负载,降低并行性。
乐观锁:假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据,在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务修改数据,如果有则回滚正在提交的事务。
特点:乐观并发控制相信事务之间的数据竞争概率是较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
MySql主从复制原理
至少需要两台数据库服务器,其中一台为Master库,另一台为Slave库,MySql主从数据同步是一个异步复制过程,要实现复制首先需要在master上开启bin-log日志功能,bin-log日志用于记录在Master库中执行的增、删、修改、更新操作的sql语句,整个过程需要开启3个线程,分别是Master开启IO线程,Slave开启IO线程和SQL线程,Master的IO线程把bin-log发送给slave的IO线程,除了bin-log内容外,还有最新的bin-log文件名以及在bin-log中的下一个指定更新位置点。到Slave的中继日志relay-log日志,用sql线程去检测,若检测到更新,则去Slave数据库上执行一遍sql语句。
使用explain优化sql
对于复杂、效率低的sql语句,我们通常是使用explain sql来分析sql语句,这个语句可以打印处语句的执行过程,这样方便我们分析,进行优化。possible_keys在试用了哪个索引在该表中查找行。
SQL连接(内连接、外连接、交叉连接)
内连接:等值连接和不等连接 =、>、<、<>、>=、<=、!>、!<
左连接:select * from db1 left join db2 on db1.id=db2.id; 左连接显示左表全部行,和右表与左表相同的行。
右连接:right join ... on ... 显示右表全部行,和左表与右表相同的行。
全连接:full join ... on ... 返回左表和右表中所有行,当某一行在另一表中没有匹配行,则另一表中的列返回空值。
交叉连接,也称笛卡尔积:select * from db1 cross join db2; 返回结果的行数等于两个表行数的乘积。
char/varchar/nvarchar
char:定长的,不足时补充空值,超过时截取超出的字符
varchar:可变长度,n必须是一个介于1和8000之间的数值,存储大小为输入数据的字节实际长度,而不是n个字节
nvarchar:可变长度,n必须是一个介于1和4000之间,存储大小是所输入字符个数的两倍。