面经问题整理
-
-
memcpy的实现
/*按照4个字节拷贝*/
void *memcpy(void *dst, const void *src, size_t num) {
assert((dst != NULL) && (src != NULL));
int wordnum = num / 4;
int slice = num % 4;
int *pintsrc = (int *)src;
int *pindst = (int *)dst;
while(wordnum--) *pintdst++ = *pindsrc++;
while(slice--) *((char *)pintdst++) = *((char *)pintsrc++);
return dst;
} -
ping原理
在同一个广播域中,我们的host1想要去访问host2,如果host1的mac表上有host2的mac地址,如果有的话,host1会向host2发送ICMP报文。如果没有,host1就会向外部发送arp广播包,交换机在收到这个apr广播包之后,会查看自己的mac表,如果有的话,通过arp报文返回。如果没有交换机就会向所有的端口发送arp广播,其他端口的主机上接收到这个arp报文段之后,如果目标不是自己,就丢弃。一直到host2的主机接受了报文之后,就会响应host2的mac的mac地址是多少,然后交换机经过学习之后,通过arp报文的形式返回给host1 。host1收到报文之后,会发送icmp报文,host1的报文段中icmp为echo request,host2的响应报文段中icmp为echo answer
在不同的广播域中,首先通过arp报文确定网关的mac地址,网关收到arp报文之后,通过arp报文找到host2的mac地址。确定之后,host1发送icmp报文到网关,然后网关将源mac替换为自己的mac,然后向host2转发,host2收到报文之后,就会返回icmp报文
-
ICMP报文
Internet control message protocol,网络消息控制协议,分为两部分,ICMP差错报告报文,ICMP询问报文。
为什么存在ICMP报文,因为IP协议是不可靠的,没有差错机制,而ICMP就可以弥补这个情况
-
webserver项目
reactor的优势
-
单reactor单线程模型
reactor线程负责多路分离套接字,accept新连接,然后将请求分派到handler中
缺点:
-
一旦reactor线程意外中断或则进入死循环,会造成整个系统通信模块不可用
-
当reactor线程负载过重,会导致大量消息挤压和处理超时,有系统瓶颈
-
-
单reactor多线程模型
在事件处理方面采用了多线程
优点:
-
降低了reactor的性能开销
缺点:
-
reactor负责承担所有事件的监听和响应,可能会存在性能问题
-
-
主从reactor‘多线程模型
优势:
-
响应快
-
可扩展性好
-
编程简单,避免了多线程或者多进程的切换
-
-
-
三七互娱笔试题
-
通常操作系统提供的用户接口包括 命令接口,程序接口和图形接口
-
RSA,SHA,DES,BASE64哪个不可逆?
RSA是非对称加密,DES是对称加密,BASE64为简单的编码解码,SHA为hash算法,不可逆
-
设某棵二叉树有360个节点,则该二叉树的最小高度是(包括根节点)?
深度为h的二叉树,最多含有2^h -1 个节点,所以h最小取9
-
在linux中,有一个文件夹里面含有若干文件,通常用哪个命令获取这个文件的大小? B
-
ls -h 只有加l的时候才会显示
-
du -sh du显示当前目录及子目录的磁盘占用情况
-
df -h df显示整个文件系统的使用情况
-
fdish -h
-
-
冯。诺伊曼机工作的基本方式的特点是?
按地址访问并且顺序执行指令
-
缺省的linux系统中,从后台启动进程,因该在命令的结尾添加哪个符号?
&
-
dup和dup2函数
dup函数返回当前可用文件描述符的最小值,dup2可以使用第二个参数指定的新的描述符的数值,如果第二个参数已经打开,。就先将其关闭,如果两个参数相同返回第二个参数,并且不关闭。 dup返回的新的文件描述符和dup2第二个参数共享同一个文件表项
-
手写泛型max函数
template <typename T>
void Swap(T &a, T &b) {
T c = a;
a = b;
b = c;
}为什么函数模板能够执行不同的类型参数?
编译器对函数模板进行了两次编译,第一次是检查语法是否正确,第二次是找到调用函数的代码,然后通过调用代码来判断参数类型
-
线程有哪些状态?
运行态,挂起态,阻塞态,终止态 【挂起态和阻塞态的区别,阻塞态是等待某个事件的发生,放弃CPU的使用权。挂起态是等待获取CPU的资源,只要有CPU就能够继续往下运行。而阻塞态即使获得了CPU,而等待的事件没有发生,也不会继续往下运行】
-
TCP/IP在协议安全缺陷主要表现在那几方面?
TCP协议的安全问题,三次握手来建立一条连接。但是这个过程是有漏洞的,假设发送方为A,接收方为B,第一次A向B发送了一个SYN包,第二次的时候B会回复一个ACK+SYN包,这个时候C监听B的报文,然后给B发送RST包,然后C伪装成A向B发送SYN包建立新的连接
-
UDP中使用bind和connect的作用
-
首先,UDP的connect和TCP的connect有本质的区别,TCP中调用connect会引起三次握手,而UDP中调用connect仅仅是把对端的IP和PORT记录下来
-
UDP中多次调用connect有两个作用,指定一个新的IP&PORT连接,断开和之前的IP&PORT连接
-
UDP中使用connect可以提高效率,普通的UDP发送两个报文,内核做了如下操作 建立连接,发送报文,断开连接,建立连接,发送报文,断开连接。而采用connect方式的UDP发送两个报文内核处理如下:建立连接 发送报文 发送报文 断开连接
-
在高并发服务的时候更加稳定,如果client A想要和serverB,C进行UDP的通信,如果通过非connect的方式,我们让a和B交替通信,A和B通信 IPa:PORTa <=> IPb:PORTb, IPa:PORTa' <=> IPc:PORTc 如果PORTa和PORTa'相同,那么就有可能出现A等待B的报文,却收了C的报文,导致收报错误,解决方法就是采用connect的通信方式,分别创建两个UDP,然后分别connect到B和C,这样就不会造成上述的问题了
-
-
socket编程中write,read和send,recv之间的区别?
我们创建好tcp连接之后,就可以将得到的fd当作文件描述符来使用。
write的返回值大于0,表示写入了部分或者全部的数据,可以通过while循环来实现数据的完整写入。如果返回的值小于0,代表出现了错误。如果错误为EINTR,代表写的时候出现了中断错误,如果是EPIPE,表示网络出现了问题,可能是对方已经关闭连接
read读取的时候和write类似,只不过判断错误的类型不同,小于0表示出现了错误。如果错误为EINTR代表中断,如果是ECONNECT表示网络连接出现了问题
recv和send函数提供了和read,write差不多的功能,只不过提供了第四个参数来控制读写操作。
MSG_DONTROUTE 不查表,告诉目标主机在本地网络中,一般用于网络诊断和路由程序中
MSG_OOB 接收或者发送带外数据
MSG_PEEK 查看数据,并且不从系统缓冲区移走数据。从系统缓冲区中读取数据,而不清除系统缓冲区的内容,一般有多个进程读写的时候可以使用这个标志
MSG_WAITALL 等待所有的数据,recv会一直阻塞,一直等到条件满足或者发生错误。当读到指定的字节,返回值为len,正常返回。当读到了文件的结尾, 函数正常返回,返回值小于len
-
socket中close和shutdown的区别
https://www.jianshu.com/p/eecab8d50697
shutdown是一种优雅的单方向或者双方向关闭socket的方法,而close是立即双方强制关闭socket并且释放相关资源
如果多个进程共享同一个socket,shutdown会影响所有的线程,而close只影响本线程
下面从两个方面来进行解释:
-
server端调用shutdown
server调用了shutdown之后,此前发出的异步的send/recv都不会立即返回,当所有发送的包被client确认之后,不管是关闭发送还是关闭接收,server会发送FIN到client,然后开始四次挥手断开连接
-
server端调用close
根据参数的设置不同,调用close会出现不同的两种情况
第一,当l_onoff为0,l_linger为0的时候,server调用close会向客户端发送RST报文段,然后丢弃缓冲区中未读取的数据并且关闭socket以及释放相关的资源
第二,当l_onoff非0,l_linger非0的时候,server调用close之后,会向客户端发送一个FIN报文段,在收到client的ack之后,server进入了FIN_WAIT2状态,如果在l_linger的时间内还没有完成四次挥手,就会强制关闭
client端如何知道已经接收到了RST报文?
server发送RST报文之后,并不等待从client端收到任何ack应答,直接关闭socket,而client收到RST之后,也不会产生任何响应
在阻塞模式下,内核无法主动通知应用层出现错误,只有当应用层主动调用read或者write这样的IO系统调用之后,内核才会用通知的方式来告诉应用层对方已经发送了RST报文
在非阻塞模式下,select或者epoll会返回sockfd可读,应用层在进行读取操作的时候,read会会报RST错误
client收到RST之后如何处理?
client收到RST报文之后,如果继续调用read函数,会返回RST错误,在已经产生RST错误的前提下,如果再次调用write,会产生EPIPE错误,然后内核会向进程发送SIGPIPE信号,在未处理信号SIGPIPE的前提下,信号的处理函数会将进程终止
-
-
-
do{...}while(0)的妙用
-
帮助定义复杂的宏来避免错误
这样在展开的时候,就会变成
if(a > 0)
foo1();
foo2();这样就会出现错误了,无论a > 0,foo2都会执行
那如果将foo1和foo2包起来呢
# define func() {foo1();foo2();}
代码编译的时候,就会改成{...};
这样在展开的时候,就会变成
if(a > 0) { foo1(); foo2(); };
这样后面就会多一个分号,可能不会报错,但是要尽量避免
而如果用
# define func() do{foo1();foo2();}while(0)
就能够让分号正确的使用,符合语法
-
定义单一的函数块来完成复杂的操作
在do{...}while(0);中,可以定义变量并且不用考虑变量名会和函数中的变量起冲突
-
避免由宏定义引起的警告,空宏在编译的时候可能会引起warning,为了避免这个,可以使用do{...}while(0);来定义空宏
-
保证只执行一次,并且可以用break跳出循环,可以用来替代goto,并且代码可读性变高
-
-
c++11了解多少?
-
右值引用 std::move()
-
auto for循环
-
std::lambda,是一个匿名函数
auto func1 = [](int i) {return i + 4;}
-
std::bind实现函数组合,可以绑定普通函数,lambda表达式,成员函数,成员变量,模板函数等
-
std::function 通过std::function中各种可调用的实体(普通函数,lambda表达式,函数指针,函数对象等),形成一个新的可调用的std::function对象
-
initializer_list initial lizer
std::vector v = {1, 2, 3, 4};
-
thread 线程的创建
-
互斥量
-
atomic 原子操作
简单的应用,对Int,char,bool等进行原子性封装,在多线程并发的时候,对std::atomic对象的访问不会造成竞争-冒险,可以实现数据结构的无锁设计
#include <thread> #incldue <atomic> #include <iostream> using namespace std; atomic_long total(0); void click(int i) { cout << " thread" << i << endl; for(i = 0; i < 100; i++) { total += 1; } } int main() { std::thread threads[10]; for(int i = 0; i < 10; i++) { threads[i] = std::thread(click, i); } for(auto &t : threads) { t.join(); } cout << "value: " << total << endl; return 0; }
高阶用法
store 原子写操作 load 原子读操作 exchange 允许两个值进行交换,并且保证整个过程是原子的 compare_exchange_weak 当前值和期望值相同时,修改当前值为设定值,返回true;当前值和期望值不相等的时候,将期望值修改成当前值,返回false;weak允许假失败,当期望值和当前值比较的时候,可能用 = 判断相同,但是用整个函数判断却有可能失败, true和3都表示真,但是compare_exchange_weak会返回虚假的false,但是比比strong性能好,在一些循环算法中weak还可以接受 compare__exchange_strong 当前值和期望值不相等的时候,将期望值修改成当前值,返回false,当前值和期望值相等的时候,将当前值改成期望值
-
override和final关键词
override关键词强制检查虚函数,确认派生类中的成员函数覆盖基类中的虚成员函数(可能函数名相同,但是参数不同)。final关键词禁止虚函数的继续重写
-
delete和default关键词
delete,禁止编译器默认生成的函数,可以用在模板特例化中,通过delete来过滤一些特定的类型参数;
default,当我们重载了c++默认生成的函数之后,c++将不再生成这些函数的默认形式,但是c++允许通过=default来要求编译器生成一个默认函数
-
正则表达式
-
-
移动语义和完美转发介绍一下?
移动语义,首先说一下可拷贝和可移动,可拷贝就是有的类是可以复制的,可以调用拷贝构造函数。有的类对象是独一无二的,是不可复制的,但是可以把资源所有权交给新的对象,这个称为可移动的,这样的话,在一些对象的构造时可以直接获取到已经存在的资源而不需要通过拷贝,申请新的内存,这样移动就可以避免拷贝,从而提高性能
https://github.com/changkun/modern-cpp-tutorial/blob/master/book/zh-cn/03-runtime.md
右值可以细分为两种,纯右值和将亡值,将亡值就是即将被销毁,却能够被移动的值,函数在返回的时候,产生一个将亡值,被对象的移动构造引用,从而延长了生命周期,并且将其中的指针拿出来保存到obj中,然后将将亡值的指针设置为null,这样就能够比避免无意义的构造拷贝
完美转发,是为了让我们在传递参数的时候,保持原来的参数类型(左引用保持左引用,右引用保持右引用)。std::forward
-
协程
和线程一样共享堆,可以悬停和恢复的函数
-
stl vector push_back的复杂度,扩容机制? 为什么要二倍扩容?
vector在push_back以成倍增长在均摊后可以达到常数的复杂度,相对于O(n)的复杂度更好
-
阻塞信号集,未决信号集
阻塞信号能决定未决信号集,当阻塞信号集中该信号成功从1变成0的时候,这个时候未决信号集也变成0
-
htonl和inet_pton的区别
都是对IP地址的转换,只不过第二个比第一个多了一个指定缓冲区的大小,还有返回的结果,htonl返回整型,inet_pton返回字符串
-
全局变量,静态变量初始化时间
全局变量的初始化时间,既有编译的时候,也有在运行的时候。
static initialization当用常量开对变量进行初始化的时候,是在程序加载的过程中完成的,分为zero initialization和constant initialization,一个是初始化为0存储在bss段,另一个存储在data段
而需要经过函数调用才能完成初始化的全局变量,是静态初始化和动态初始化的结合,状态的时候先给变量分配地址空间,进行zero initlization,然后程序运行的时候在调用函数进行dynamic initialization
全局静态变量,在main函数执行之前的静态初始化过程中分配内存并且初始化,局部静态变量在第一次使用的时候进行分配内存并且初始化
-
mysql索引
如果没有索引这个东西, 一旦我们运行一个sql语句,就很有可能造成一个全表的搜索,效率太低了。为了提高效率,我们开始采用数据结构,首先来对比hashmap,插入,修改,删除,查询的复杂度为O(1),而树结构只能达到logn级别的,那为什么索引的底层实现不用hashmap而是使用树呢,是因为如果我们要进行范围查找,以及比较大小的时候,hashmap只能是进行全表的搜索,而Tree依然能够保证logn的高效率。在树中,二叉树和b/b+树相比,在数据非常大的时候,二叉树会变得很高,而b/b+树能够完美的利用局部性原理(时间上,当前要执行的指令和下一条执行的指令,当前数据的访问和下一次访问都是在一个较短的时间内;空间上,当前指令和相邻的指令,当前访问的数据集和临近的几个数据集都存放在一个较小的区域上)
b+树只有叶子节点存储数据,非叶子节点存储索引,所以和b树相比,每一个非节点能存储的索引变多,以及不需要进行中序遍历就能够访问范围内的数,这样效率会更加提高
-
sleep和wait的区别?
sleep调用的时候会让线程暂停指定的时间,但是监控依然存在,对象锁也不会释放。wait调用会放弃对象锁,等待调用notify唤醒之后才能再次获取对象锁进入运行状态
-
对于管程的理解
-
概念
管程可以看作一个软件模块,它是将共享的变量和对于这个共享变量的操作封装起来,形成一个具有一定接口的功能模块, 进程可以调用管程来实现进程级别的并发控制
进程只能互斥的使用管程,当一个进程使用管程的时候,另一个进程必须等待
在管程入口等待的队列称为入口等待队列,由于进程可能会执行唤醒操作,因此可能有多个等待使用管程的队列,这样的队列称为紧急队列,优先级高于等待队列
-
特征
模块化,抽象的数据类型,信息隐藏,使用的互斥性
-
过程
enter过程
一个进程进入管程之前要提出申请,一般由管程提供一个外部过程
leave过程
当一个进程离开管程的时候,如果紧急队列不为空,那么它就必须负责唤醒紧急队列中的一个进程
条件变量c
是一个指针,如果motfull表示不满,如果缓冲区已经满了,那么将要在缓冲区进行写入的进程就要等待notfull
wait(c)
表示进入管程的进程申请使用c资源,如果此时这个资源可用,进程使用。如果不可用,进程阻塞,放入紧急队列中
signal(c)
表示进入管程的进程使用的某种资源要释放,此时进程会唤醒等待这种资源而进入紧急队列的第一个进程
-
-
二叉树任意两个节点之间的最长路径的长度
假设根节点为node,max_len(node) = max_len(node->right) + max_len(left) + 2
max(node->right) = max(node->right->right, node->right->left) + 1
-
为什么MTU的大小普遍都是1500 ?
一个标准的以太网数据帧是1518,头信息有14字节,尾部校验和FCS占了4字节
考虑到传输效率和传输时间而折中的一个值,如果某一个节点的MTU和其他节点不同,还需要进行拆包重组
-
TCP拥塞控制和流量控制的区别?
拥塞控制是为了防止过多的数据注入到网络中,导致网络超出负荷;流量控制是为了解决发送方和接收方接受速度不一致而导致数据丢失问题,采用滑动窗口的形式来解决问题
-
TCP拥塞控制的缺点
-
网络资源分配的不公平导致加重拥塞,在拥塞发生的时候,有拥塞控制反应机制的TCP数据流会按照拥塞控制进入拥塞避免阶段,主动减少发送进入网络的数据的数量。而UDP没有拥塞控制,这样会导致遵守拥塞控制的TCP得到的网络资源越来越少,没有遵守拥塞控制 的UDP得到的资源却越来越多
-
一些TCP连接之间也存在公平性问题,比如一些TCP在拥塞前使用了大窗口,或者RTT比较小,数据包比其他TCP大,这样也会占据带宽
-
-
GET和POST的安全问题
-
POST更加安全一点,因为GET的传输方式是在URL中显示参数,容易泄露信息。post相对于get来说更加安全一点,把数据隐藏在了请求体中
-
HTTP协议中提到了GET是安全的方法,因为GET不会改变服务器中的数据,所以不会产生副作用
-
只要正确使用的话,没有多大问题。对于非保密的信息通过get来传输,而对于保密的信息来通过post传输
-
-
查看最近的日志用什么命令?
tail -f 文件名
-
设计模式
单例模式,分为饿汉模式和饱汉模式,饿汉模式就是在类在加载的时候进行实例化
饿汉模式
class Node { private static Node node; static { node = new Node(); } private Node(){ } public: static Node getnode() { return node; } }
饱汉模式
class Node { private static Node node; private Node () { } public static Node node () { if(node == null) { node = new Node(); } return node; } }
-
哈希避免冲突的算法?
-
线性探测法,如果鼬冲突的话,往后找,找到第一个非空的位置放进去,缺点:删除比较麻烦,只能标记该位置已经被删除;容易产生堆聚现象
-
线性补偿探测法,将增量从1变成了q,q和m互斥
-
随机探测,每一个关键字的步长是随机生成的,可以避免或者减少堆聚
-
拉链法,通过链表来进行存储
-
平方探测法
-
-
快排的时间复杂度和空间复杂度。为什么快排不稳定
快排的时间复杂度:最优情况下(时间复杂度 nlogn,空间复杂度nlogn),最差情况下(时间复杂度n^2,空间复杂度 n)
-
堆排序的时间复杂度和空间复杂度?
时间复杂度,建堆的复杂度 O(n),重建堆的复杂度O(logn) nlogn,空间复杂度O(1)
-
vector里面size和capacity的区别?
size指的是当前有多少值,capacity是指定最少要多少元素才会使其容量重新分配
-
c++程序的内存格局
栈,堆,自由存储区,全局/静态存储区,常量存储区
-
一个 c++源文件从文本到可执行文件经历的过程
预处理,编译,汇编,链接
i s o exe
预处理:将define删除,将宏定义展开;处理一些条件编译指令,ifndef,ifdef,endif等;处理#include预编译指令。过滤掉注释,添加行号和文件名标识;保留#pragma指令
编译:将预处理的文件进行一系列的词法分析,语法分析,语义分析,以及优化后产生的汇编文件代码文件
汇编:将汇编文件翻译成机器语言,生成目标文件
链接:将每个源代码独立的编译,然后按照他们的要求组装起来,主要解决代码之间的依赖问题
处理方式分为静态链接和动态链接。
-
前++和后++如何重载?
class Complex { public: Complex(int t) { a = t; } friend Complex& operator ++ (Complex &c); friend const Complex operator++(Complex &c, int); private: int a; } Complex& operator++(Complex t) { t.a++; return t; } const Complex operator++(Complex &c, int) { Complex tmp(c.a); c.a++; return tmp; }
-
哪些运算符可以重载?
不可重载的运算符, :: . ?:
-
计算机网络的七层结构
应用层,表示层,会话层,传输层,网络层,数据链路层,物理层
-
二叉树的最大路径和
int cal(TreeNode *root, int& m) { if(!root) { return 0; } int left = cal(root->left, m); int right = cal(root->right, m); m = max(m, left + right + root->val); m = max(m, max(left + root->val, right + root->val)); m = max(m, root->val); left += root->val; right += root->val; int tmp = max(left, right); tmp = max(tmp, root->val); return tmp; }
-
能不能同时用static和const修饰类的成员函数?
不能,c++编译器在实现const的成员函数得时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*,但是当一个成员为static的时候,这个函数是没有this指针的,也就是说会发生冲突
-
七层模型,每一层的作用
应用层:为进程提供网络接口
表示层:数据格式的变化,数据加密和解密,数据压缩和恢复
会话层:组织同步的两个会话用户之间的对话,并且管理数据的交换
传输层:向用户提供可靠的端到端的服务,透明的传输报文
网络层:路由寻址
数据链路层:在通信实体上建立数据链路交换,传送以帧为单位的数据,通过差错控制,流量控制方法,将有差错的物理线路变成无差错的数据链路
物理层:利用物理传输介质为数据链路层提供物理连接
-
二分
int search(int l, int r, int val) { while(l < r) { int mid = l + r >> 1; if(check(mid)) { ans = mid; l = mid; } else { r = mid - 1; } } }
-
linux查找文件中含有某行字符串的行数
find filename | xargs cat | grep . "hello" | wc -l
-
http 1.0, http 1.1, http 2.0的区别
http 1.0和http1.1相比
-
带宽优化和网络连接的使用,在http 1.0中,存在一些浪费的情况,比如客户端只是需要一个对象的一部分,而服务器却将整个对象送过来,并不支持断电重续功能。http 1.1在请求头中引入了range,允许只请求资源的某个部分,返回码是206.
-
长连接
-
错误通知管理,http 1.1中,409代表请求的资源和当前的状态发生冲突。410代表服务器上的某个资源被永久性删除
-
host头处理,在http 1.0中认为每台服务器都绑定一个唯一的IP,因此在URL中并没有传递主机名。而随着技术的发展,一个服务器可以存在多个虚拟主机,并且共享一个IP地址,所以http1.1的请求消息支持host头域
http1.x的优化
-
降低延迟,多路复用
-
请求优先级
-
header压缩
-
基于https的加密传输
-
服务器端推送
http2.0和http1.x相比
-
2.0可以多个请求在一个连接上并行执行
-
-
24点游戏
#include <bits/stdc++.h> using namespace std; # define eps 1e-5 bool judge(vector<double> sto) { if(sto.size() == 1) { return fabs(sto[0] - 24) <= eps; } int len = sto.size(); for(int i = 0; i < len; i++) { for(int j = i + 1; j < len; j++) { vector<int> vis(len, 0); vis[i] = vis[j] = 1; vector<double> tmp; for(int k = 0; k < len; k++) { if(vis[k])continue; tmp.push_back(sto[k]); } double oper; // 加法 oper = sto[i] + sto[j]; tmp.push_back(oper); if(judge(tmp)) { return true; } tmp.pop_back(); // 减法 oper = sto[i] - sto[j] > 0 ? sto[i] - sto[j] : sto[j] - sto[i]; tmp.push_back(oper); if(judge(tmp)) { return true; } tmp.pop_back(); // 乘法 oper = sto[i] * sto[j]; tmp.push_back(oper); if(judge(tmp)) { return true; } tmp.pop_back(); // 除法 //if(sto[i] == 0 || sto[j] == 0) continue; if(sto[i] != 0) { oper = sto[j] / sto[i]; tmp.push_back(oper); if(judge(tmp)) { return true; } tmp.pop_back(); } if(sto[j] != 0) { oper = sto[i] / sto[j]; tmp.push_back(oper); if(judge(tmp)) { return true; } tmp.pop_back(); } } } return false; } int main() { int i, tmp, a, b, c, d; while(cin >> a >> b >> c >> d) { vector<double> sto; double cal = static_cast<double>(a); sto.push_back(cal); cal = static_cast<double>(b); sto.push_back(cal); cal = static_cast<double>(c); sto.push_back(cal); cal = static_cast<double>(d); sto.push_back(cal); cout << (judge(sto) == 1 ? "true" : "false") << endl; } return 0; }
-
对抽象类和接口的理解
抽象类是只能定义类型,不能生成对象的类。抽象类中定义的纯虚函数,一方面是为了给派生类提供虚函数,另一方面定义为纯虚函数就是为了不定义函数体
接口时一个特殊的抽象类,成员函数全部是public,并且都是纯虚函数
-
mysql主从复制
-
redis的持久化,有哪些方式,原理是什么?
https://blog.csdn.net/qq_17312239/article/details/81009522
redis的持久化就是将数据放到断电后不会丢失的设备中
数据库在进行持久化的时候都做了哪些事情?
-
客户端向服务器端发送写操作,此时数据保存在客户端内存中
-
服务器端收到写请求,此时数据保存在服务器端的内存中
-
服务器端调用write往硬盘上写,此时数据保存在系统内存的缓冲区中
-
操作系统将缓冲区中的数据写入到磁盘控制器中,此时数据在磁盘缓冲中
-
磁盘控制器将数据写入到磁盘的物理介质中,此时数据才真正落到磁盘上
当数据库系统系统发生故障的时候,这个时候系统的内核还是完好的,只要执行完write之后,是没有问题的,剩下的让OS来处理
当系统发生断电的时候,上面五步都会失效,只有当数据在完成第五部之后,才能保证断电后数据不丢失
广泛的来说,对于数据损害有三种处理方式
-
进行数据整体的备份
-
进行数据命令的备份
-
数据库不对九的数据进行修改,只是通过追加得方法来完成写操作
RDB
在指定的时间间隔内将内存中的数据集快照写入到磁盘中
有三种处理方式,一个是在指定的时间内,如果有超过一个key被修改,就发起快照保存;还有一个方法是,在指定的时间内,如果超过x个key被修改,就自动做快照;还有一个方法就是调用slave通知redis做一次快照持久化,但是因为redis是一个主线程处理所有的请求,这种方法会阻塞所有的client请求,不推荐使用
处理过程:
-
redis调用fork
-
父进程继续处理client的请求,子进程负责将内存中的内容写入到临时文件中,根据写时复制机制,父子进程会供向物理页面,当父进程处理写请求的时候,OS会为父进程要修改的页面创建副本,子进程fork整个数据库的一个快照
-
当子进程将快照写入到临时文件完毕之后,用临时文件替换掉原来的快照,之后子进程退出
快照持久化是将内存中的数据完整的写入到磁盘,并不只是更新脏数据
优势:
-
redis数据库只会包含一个文件,备份的时候就非常方便
-
在恢复大型数据的时候比aof快
-
在保存rdb文件的时候,唯一要做的就是fork一个进程,然后子进程去处理,独立性比较好
劣势:
-
rdb进行备份的时间间隔比较大,如果要尽量避免服务器故障的时候丢失数据,尽量不要使用rdb
-
当数据集比较大的时候,fork可能会非常耗时
AOF
将收到的每一个写命令通过write写入到文件中
有多种处理方式,比如每次收到写命令就立即强制写入,或者每秒强制写入
但是通过记录命令的方式,也会带来别的问题,比如调用incr test 100次,文件就必须保存所有的命令,但是事实上有很多是冗余的,其实只需要一个命令就够了
为了解决这个问题,redis提供了bgrewriteaof命令,大体意思就是redis将通过快照的形式将内存中的数据写入到临时文件中,然后替换掉原来的文件。
处理过程:
-
调用fork
-
子进程根据内存中的快照,往临时文件中写入重建数据库状态的命令
-
父进程继续处理client请求,除了将写命令写入到原来的aof中,还把写命令缓存起来
-
当子进程把快照内容通过命令的方式写入到临时文件中后,子进程通知父进程,然后将缓存的命令写入到临时文件中
-
父进程用临时文件替换掉老的aof文件
优势:
-
使用aof使得redis更加的耐用,数据防止丢失方面比rdb好
-
redis可以在aof文件体积比较大的时候,自动对aof重写
-
日志非常容易读懂,有序的保存了对数据库执行的写入操作
劣势:
-
对于相同的数据集,aof的体积大于rdb
-
-
mysql组合索引在使用的时候有顺序的区别吗?
没有区别,sql执行优化器会对语句进行优化,但是sql优化是有一定开销的,所以,最好按照标准的方法写
-
最左匹配原则
mysql创建复合索引的规则是对复合索引的最左边进行排序,在第一个字段的基础上,然后再对下一个进行排序
所以,第一个字段是绝对有序的,但是第二个字段就是无序的了
-
如何设计100000QPS的系统
从多个方面来考虑:
-
数据库方面,redis + mysql,读多还是写多,建立索引,语句优化
-
分布式方面 master-slave,cluster,sential
-
服务器方面,一致性hash
-
-
TCP的被动关闭端为什么不需要类似TIME_WAIT状态?
time_wait的作用是服务端要确定客户端收到最后一个ack,ack有一个编号,假设被动端有数据丢失,第二次挥手,客户端返回的ack,服务器没有收到,然后服务器还是出去fin_wait_1状态,然后客户端处于close_wait状态。然后第三次挥手,客户端向服务器端发送fin,然后没有收到,因为没有收到服务器返回的ack,所以客户端会一直发送,然后有一个突然发送成功了,此时服务器端可以理解成迅速道道fin_wait_2,然后立刻进入TIME_WAIT状态
-
设计模式的原则
总原则:开闭原则(对扩展开放,对修改关闭)在程序需要扩展的时候,不去修改原有的代码,而是扩展原有代码
-
单一职责原则:不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的原则
-
里氏替换原则:任何基类出现过的地方,子类一定可以出现,只有当派生类可以替换掉基类,软件单位的功能不受到影响的时候,基类才能够真正被复用,而派生类也能够在基类的基础上增加新的行动
-
依赖倒转原则:面向接口变成,依赖于抽象而不依赖于具体。写代码用到具体类的时候,不与具体类交互,而是与具体类的上层接口交互
-
接口隔离原则:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分,使用多个隔离的接口
-
最少知道原则:一个类对自己依赖的类知道的越少越好,也就是说被依赖的类多么复杂,都应该讲逻辑封装在方法内部,通过public方法提供给外部,这样当依赖的类变化的时候,才能最小的影响该类
-
合成复用原则:尽量首先使用合成/聚合的方法,而不是使用继承
-
-
几种常见的设计模式
-
单例模式
作用:保证一个类只有一个实例,并且提供一个访问它的全局访问点,使得系统中只有唯一的一个对象实例
应用:常用于管理资源,比如日志,线程池
/* 单例模式的作用,就是整个系统中只有一个对象 我们需要考虑两个部分,一个是只有一个对象 为了达到这个目的,我们可以将这个对象变成静态变量,这样只需要申请一次即可 还有一个部分是,阻止外部构造,外部拷贝构造,外部赋值构造函数 将外部构造函数,外部拷贝构造函数,外部赋值构造函数设置为privatee 细分的话,还有懒汉模式和饿汉模式,饿汉模式就是上述的情况,在类进行定义的时候就进行实例化,它是线程安全的。懒汉模式,就是对象的定义是在第一次要使用的时候才进行定义,它是线程不安全的,可以通过加锁的形式来保证线程安全 */ // 饿汉模式 #include <bits/stdc++.h> using namespace std; class Singleton { public: static Singleton* GetSingleton(){ return &singleton; } private: static Singleton singleton; Singleton() { cout << "create\n"; } Singleton(Singleton const& singleton); Singleton operator = (Singleton const& singleton); ~Singleton(){}; }; Singleton Singleton::singleton; int main() { cout << "catch\n"; Singleton* t = Singleton::GetSingleton(); return 0; } // 懒汉模式,通过加互斥锁的形式来保证线程安全 //不加锁 #include <bits/stdc++.h> using namespace std; class Singleton { public: static Singleton& GetSingleton() { static Singleton singleton; return singleton; } void test() { cout << 1 << endl; } void Del() { if(singleton == NULL) { return ; } delete singleton; } private: Singleton(){} Singleton(Singleton const &singleton); Singleton& operator=(Singleton const &singleton); }; int main() { Singleton& a = Singleton::GetSingleton(); a.test(); return 0; } // 加锁 #include <bits/stdc++.h> using namespace std; class Singleton { public: static pthread_mutex_t mutex; static Singleton* GetSingleton() { if(singleton == NULL) { pthread_mutex_lock(&mutex); singleton = new Singleton(); pthread_mutex_unlock(&mutex); } return singleton; } void Del() { if(singleton == NULL) { return ; } delete singleton; } protected: Singleton(){ cout << "create the Singleton\n"; pthread_mutex_init(&mutex, NULL); } private: static Singleton* singleton; Singleton(Singleton const &singleton); Singleton operator = (Singleton const &singleton); ~Singleton() { cout << "destroy the Singleton\n"; } }; Singleton* Singleton::singleton = NULL; pthread_mutex_t Singleton::mutex; int main() { cout << "get the singleton\n"; Singleton* t = Singleton::GetSingleton(); return 0; } // 做一个懒汉模式和饿汉模式对比吧 // 饿汉模式是在这个程序运行之前就已经创建好了。懒汉模式是在这个程序运行的时候,需要这个对象才会进行创建(加锁/不加锁) // 还有一个需要注意的地方就是要注意对象销毁的销毁,如果是从栈上申请的,直接析构函数就可以,如果是从堆上申请的,就是需要调用delete。单独弄一个函数就行
-
工厂模式
主要是封装对象的创建,分离对象的创建和操作过程,用于批量的管理对象的创建过程,便于程序的维护和扩展
-
简单工厂模式
对于不同的产品的创建定义一个工厂类,将产品的类型作为参数传入到工厂的创建函数,根据类型分支选择不同的产品构造函数
#include <bits/stdc++.h> using namespace std; typedef enum ProductTypeTag { TypeA, TypeB, TypeC }PRODUCTTYPE; class Product { public: virtual void show() = 0; }; class ProductA : public Product { public: void show() { cout << "I am ProductA" << endl; } }; class ProductB : public Product { public: void show() { cout << "I ams ProductB" << endl; } }; class ProductC : public Product { public: void show() { cout << "I ams ProductC" << endl; } }; class Factory { public: Product* CreateProduct(PRODUCTTYPE type) { switch (type) { case TypeA: return new ProductA(); case TypeB: return new ProductB(); case TypeC: return new ProductC(); default: return NULL; } } }; int main() { Factory productCreator; Product *productA = productCreator.CreateProduct(TypeA); Product *productB = productCreator.CreateProduct(TypeB); productA->show(); return 0; }
-
工厂方法模式
#include <bits/stdc++.h> using namespace std; typedef enum ProductTypeTag { TypeA, TypeB, TypeC }PRODUCTTYPE; class Product { public: virtual void show() = 0; }; class ProductA : public Product { public: void show() { cout << "I am ProductA" << endl; } }; class ProductB : public Product { public: void show() { cout << "I ams ProductB" << endl; } }; class ProductC : public Product { public: void show() { cout << "I ams ProductC" << endl; } }; class Factory { public: virtual Product* createProduct() = 0; }; class FactoryA : public Factory { public: Product* createProduct() { return new ProductA(); } }; class FactoryB : public Factory { public: Product* createProduct() { return new ProductB(); } }; class FactoryC : public Factory { public: Product* createProduct() { return new ProductC(); } }; int main() { Factory* factoryA = new FactoryA(); Product* productA = factoryA->createProduct(); return 0; }
工厂方法模式,就是在简单工厂模式的基础上,将简单工厂模式中的工厂变成一个基类,然后对于每一个产品有一个工厂,方便后期种类的扩展
-
抽象工厂模式
#include <bits/stdc++.h> using namespace std; class ProductA { public: virtual void show() = 0; }; class ProductA1 : public ProductA { public: void show(); }; class ProductA2 : public ProductA { public: void show(); }; class ProductB { public: virtual void show() = 0; }; class ProductB1 : public ProductB { public: void show() { cout << "I ams ProductB1" << endl; } }; class Factory { public: virtual ProductA* createProduct() = 0; virtual ProductB *createProduct() = 0; }; class FactoryA : public Factory { public: Product* createProduct() { return new ProductA(); } }; class Factory1 : public Factory { public: productA *createPeoductA() { return new ProductA1(); } productB *createProductB() { return new ProductB!(); } class Factory2 : public Factory { public: productA *createProductA() { return new ProductA2(); } productB *createProductB() { return new ProductB2(); } }; int main() { return 0; }
工厂方法模式适用于结构类型单一的场合,抽象工厂方法适用于产品结构种类多的场合,可以让低端工厂生产不同类型的低端产品,高端工厂生产不同种类的高端产品
-
-
-
socket通信原理
TCP/IP是有一个四层结构的,从上到下是 应用层,传输层,网络层,链路层。而socket是在应用层和传输层的一个抽象层,相当于为应用进程提供了一个接口。把复杂度TCP/IP协议族放在socket接口的后面,让socket去组织数据 然后再就是讨论到在网络之中,进程如何进程通信了。在网络中,**协议 + IP + 端口号**能唯一的标识一个进程。有了这三个因素,我们就可以在网络中定位一个进程了 然后服务器端的函数调用顺序是 socekt,bind,listen,accept。socket的作用是创建一个文件描述符,确定协议族AF_INET,socket类型(SOCK_STREAM, SOCK_DGRAM)。bind函数的作用是绑定本地套接字,为客户端提供服务。listen的作用是设置一个监听上限,这个上限指的是排队队列的大小。accept函数的作用就是等待客户端的连接了 对于客户端的函数的调用顺序是socket,bind,connect。sonnect的作用就是和服务器端建立连接。 当我们输入一个url之后,会先通过三次握手建立连接,然后再就是客户端向服务器端发送一个请求报文,然后服务器端会返回一个响应报文。然后客户端再进行数据的分析,之后就是通过四次挥手断开连接。
-
写入时复制的思想
如果有多个调用者要求相同的资源,他们呢会获取相同的指针指向相同的资源,直到某一个调用者要修改资源的内容的时候,系统会复制一份副本给这个调用者,然后其他的调用者所见到的资源还会保持不变
-
协程
轻量级线程,调度由用户来控制。拥有自己的寄存器上下文和栈。 允许我们写着同步代码的逻辑,然后做着异步的事情
-
一根绳子分三段成三角形的概率
通过画图的方式来解决,首先把三段绳子算出来,x, y, a - x - y。然后任意两边之和大于第三边,列出三个式子, 然后根据线来画图
-
报文的传输时间计算思路
首先是速率,就是单位时间内传输的数据量。然后再就是时延,就是指的数据从一个网络节点跳到另一个节点所花费的时间。这个时间包括,节点处理时延(验证是否出错,确定如何转发分组),排队时延,传输时延
-
有5 10 25 三种硬币,给定一个数字N,有多少种组合情况
int cal(int n) { int dp[maxn]; memset(dp, 0, sizeof(dp)); dp[0] = 1; int f[3] = {5, 10, 25}; for(int i = 0; i < 3; i++) { for(int j = f[i]; j <= n; j++) { dp[j] = dp[j] + dp[j - f[i]]; } } return dp[j]; }
所有的组合情况
-
25匹马赛跑,共有五个赛道,最少赛多少次就可以找出前三名?
首先分成五组进行赛跑,然后获得每一组的排名,A1, B1, C1, D1, E1。然后每组后两名淘汰掉,现在还有15个马,然后五组第一名再进行一次,然后D,E全部被淘汰。然后还剩下9个马,然后将B3,C2,C3淘汰掉,然后A2, A3, B1,B2,C1再比一次。选出前两名
一共是5 + 1 + 1 = 7
-
查询一个表 name字段中出现次数最多的top3
select name, count(*) as count from 表名 group by name order by count desc limit 3;
-
栈和队列的区别
-
队列是先进先出,栈是先进后出
-
操作的对象也不同,栈操作的是栈顶,而队列的话,可以在队尾入队,队头出队
-
遍历数据的速度不同,如果说想要从头进行遍历的话,栈需要遍历整个栈最后才能取出来,并且还需要额外的空间进行存储。如果是队列的话,可以从头部开始遍历
-
-
操作系统中常见的几种进程调度算法
-
先来先去
-
短作业优先
-
优先级调度算法
-
轮转调度算法
-
多级反馈队列
-
-
c++内联函数
https://www.cnblogs.com/chenwx-jay/p/inline_cpp.html
是c++的一种特殊函数,可以像函数一样被调用,但是在调用的时候,并不像函数的调用机制一样,没有函数参数的压栈,减少了调用的开销。然后内联函数是通过直接将函数插入到调用处来实现的
优点:
-
和函数调用相比,开销小
-
编译器在调用一个内联函数的时候,会首先检查参数类型,如果正确,然后再进行一些列的相关检查 宏定义不会检查参数类型,安全隐患比较大
-
inline函数可以组为一个类的成员函数,和类的普通成员函数作用相同,可以访问一个类的私有成员和保护成员
缺点:
-
具有一定的局限性,内联函数的函数体体积一般不能太大,如果体积过大的话,编译器会放弃内联的方式,而是采用不用的方式调用函数
-
-
什么函数不能声明为虚函数?
-
普通函数,只能被重载
-
构造函数
-
静态成员函数,对于每一个类只有一份代码
-
内联函数,在编译的时候展开
-
友元函数
-
-
linux socket非阻塞connect方法
如何设置是阻塞模式,connect会阻塞到连接成功或者连接超时(75 S到几分钟),如果是非阻塞的话,调用connect之后,函数就会立即返回,如果errno返回EINPROGRESS,就代表三次握手仍在继续,这个时候可以调用select检测非阻塞connect是否完成。如果多次返回0,代表超时。如果大于0,说明检测到了可读或者可写的套接字描述符。
如果select返回的是 > 0 的值,说明检测到可读或者可写的套接字文件描述符。
当建立连接成功的时候,socket 描述符变成了可写
socket 描述符变成既可读又可写,有两种可能性,一种是服务器端发送过来数据。另一种是连接出错。可以通过getsockopt来进行判断,如果获得的errno是0,就代表第一种情况。如果是ETIMEOUT,ECONNREFUSED等就代表建立连接出现错误
另外一种方式是再次调用connect,如果返回的errno是EISCONN,就代表已经连接
-
创建socket
-
设置套接字为非阻塞
fcntl
-
connect
-
调用select,通过FD_ISSET检查是否可写
-
-
socket可读,可写的条件
-
接收低水位,让select返回可读时套接字接收缓冲区所需的数据量,默认值为1
-
发送低水位,让select返回可写时套接字发送缓冲区所需的可用空间大小,默认值是2048
socket可读的条件:
-
socket接收缓冲区的数据大于等于这个socket接收缓冲区的接收低水位
-
这个连接的读这一半关闭,这样的socket不会阻塞,并且返回0
-
socket是一个用于监听的socket,并且已经完成的连接数非0
-
有一个socket有异常错误条件等待处理
socket可写的条件:
-
socket发送缓冲区的数据大于这个socket的发送缓冲区的低水位
-
这个连接的写这一半关闭
-
有一个socket异常错误条件等待处理
-
-
EPOLL_ONESHOT
这个EPOLL_ONESHOT的作用就是监听到一次事件之后,将这个事件对应的文件描述从监听集合中移除,读完之后再次把对应的fd添加到监听集合中。避免出现多个线程处理同一个读事件
-
new的底层实现
先是分配内存,然后再调用对象的构造函数。分配内存底层是通过malloc实现的
malloc的实现过程:
-
获取分配区的锁,防止多线程冲突
-
计算出实际需要分配的内存大小
-
如果是小于64Byte的话,就会去fast bins上去找,如果有合适的大小的话,分配结束,否则继续往下走
-
如果是小于512Byte的话,就会去small bins上去找,如果有合适的大小的话,分配结束,否则继续往下走
-
然后ptmallic会遍历fast bins中的chunk,相邻的chunk进行合并,然后连接到unosrted bins中,然后遍历unsorted bins。如果只有一个合适的,并且大于等待分配的chunk的大小,会进行切割。然后将剩余的chunk扔回到unsorted bins中,如果有正好合适的,就将这个chunk从unosrted bins中删除。然后会对unsorted bins中的chunk进行整理,输入small bins中的放回到small bins,输入large bins中的放回到large bins中。然后还没有找到的话,会去tio chunk中寻找
-
如果还没有的话,主分配区会调用sbrk增加top trunk的大小,非主分配区,直接调用mmap来进行分配
-
-
volatile关键字
使得一个变量在多个线程中可见,但是并不能保证多个线程产生的竞态条件
-
redis和mysql的应用场景
redis是基于内存的,读写速度快,也可以做持久化。但是内存空间有限,当数据量超过内存空间的时候,需要进行扩充内存,内存的成本比较高;
mysql是基于磁盘的,读写速度没有redis那么快,但是不受空间容量限制,性价比高
-
redis数据类型和应用场景
-
string,是二进制安全的,也就是说可以包含任何数据
-
hash,存储,读取
-
list,构建消息队列,查看最新消息
-
set,一堆不可重复的组合,求交集,并集,差集
-
zset,排行榜
-
心动网络
-
redis的数据在硬盘上是如何进行存储的?
rdb和aof
-
lambda表达式的参数如何进行传递
-
vector如何设置扩容机制,reverse,resize函数,如果存储的是类的话,会不会调用类的构造函数
-
mysql什么时候不走索引
-
static_cast如果是强行转换unique_ptr的话,会不会成功
-
inline函数是在什么时候进行编译的