1.google mock?了解的测试理论?
google mock是用来配合google test对C++项目做单元测试的。
测试基础理论:https://blog.csdn.net/sterson/article/details/88913742
白盒测试
通过对程序内部结构分析来寻找问题。白盒测试是把程序看成是装在一个透明的 白盒子里,清楚了解程序结构和处理逻辑过程。检查所有路径及条件是否正确。 白盒测试又称结构测试。
黑盒测试方法
-
等价类划分(最常用)
-
边界值分析法(最常用)
-
因果图分析法
-
错误推测法
-
判定表驱动法
-
正交试验设计法
-
功能图法
等价类划分的办法是把程序输入条件划分成若干部份,从每个部份中选取少数代表数据作为测试数据每一类中的代表数据在测试中等价于这整个类的其他数据。
边界值分析法是通过选择等价类刚好被划分的边界值作为测试数据。
因果图法是一种利用图解法分析输入的各种组合情况,从而设计测试用例的方法,它适合于检查程序输入条件的各种组合情况。
错误推测法是基于经验和直觉推测程序中所有可能存在各种错误。
判定表是把作为条件的所有输入的各种组合值以及对应输出值都罗列出来而形成的表格。它能够将复杂的问题按照各种可能的情况全部列举出来,简明并避免遗漏。
正交试验设计,是指研究多因素多水平的一种试验设计方法。根据正交性从全面试验中挑选出部分有代表性的点进行试验,这些有代表性的点具备均匀分散,齐整可比的特点。
功能图法是一种黑盒、白盒混合用例设计方法,是功能图FD形式化地表示程序的功能说明,并机器地生成功能图的测试用例。
2.C++、golang各自全局变量初始化的顺序?
待查~
3.C++的类如果使用memset初始化,可能存在什么问题?
-
memset 是按字节对内存块进行初始化的函数,用来给某一块内存空间进行赋值的;
-
memset 作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法;
-
成员变量可以是简单的内置类型(
short、int、long、char、float、double
); -
memset函数不能将int数组初始化为0和-1之外的其他值(除非该值高字节和低字节相同):
memset是逐字节进行初始化,比如对整型数进行初始化,int是32位的共四个字节,每个字节设置为n,则
如果n=1,1为00000001 00000001 00000001 00000001,转为十进制数是1+1*2^8+1*2^16+1*2^24=16843009,而不是1;
如果n=0,0为 00000000 00000000 00000000 00000000,转化为十进制为0;
如果n=-1,-1为 11111111 11111111 11111111 11111111(原码的反码的补码),转化后为-1。
-
类中含有C++类型的对象(string, list, set, map等)时,千万不要使用memset进行初始化,因为会破坏对象的内存,可用构造函数来实现;
-
类含有虚函数表时,初始化会破坏虚函数表,后续对虚函数的调用都将出现异常;
-
类含有指针时,初始化时,并不会初始化指针指向对象的值,而会把该指针设置为0;
示例:
#include <iostream>
#include <vector>
using namespace std;
class A {
public:
virtual void test() {
cout << 'A' << endl;
}
};
class B :public A{
public:
vector<string>* ptr;
B() {
ptr = new vector<string>;
}
virtual void test() {
cout << 'B' << endl;
}
~B() {
delete ptr;
}
};
int main(){
B b;
memset(&b, 0, sizeof(b));
A* a = &b;
a->test();
return 0;
}
由于虚函数表指针被置零,调用虚函数时发生崩溃。
https://blog.csdn.net/yz930618/article/details/84660076
4.C++中哈希表的哈希函数怎么做的?
待查~
5.TCP的滑动窗口详解?
滑动机制
-
发送窗口只有收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界。
-
接收窗口只有在前面所有的段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下,窗口不会移动,并不对后续字节确认。以此确保对端会对这些数据重传。
-
遵循快速重传、累计确认、选择确认等规则。
-
发送方发的window size = 8192;就是接收端最多发送8192字节,这个8192一般就是发送方接收缓存的大小。
https://blog.csdn.net/yao5hed/article/details/81046945
快重传机制
由于TCP采用的是累计确认机制,即当接收端收到比期望序号大的报文段时,便会重复发送最近一次确认的报文段的确认信号,我们称之为冗余ACK(duplicate ACK)。
如图所示,报文段1成功接收并被确认ACK 2,接收端的期待序号为2,当报文段2丢失,报文段3失序到来,与接收端的期望不匹配,接收端重复发送冗余ACK 2。
这样,如果在超时重传定时器溢出之前,接收到连续的三个重复冗余ACK(其实是收到4个同样的ACK,第一个是正常的,后三个才是冗余的),发送端便知晓哪个报文段在传输过程中丢失了,于是重发该报文段,不需要等待超时重传定时器溢出,大大提高了效率。这便是快速重传机制。
为什么是3次冗余ACK?
首先要明白一点,即使发送端是按序发送,由于TCP包是封装在IP包内,IP包在传输时乱序,意味着TCP包到达接收端也是乱序的,乱序的话也会造成接收端发送冗余ACK。那发送冗余ACK是由于乱序造成的还是包丢失造成的,这里便需要好好权衡一番,因为把3次冗余ACK作为判定丢失的准则其本身就是估计值。
在没丢失的情况下,有40%的可能出现3次冗余ACK
在乱序的情况下必定是2次冗余ACK
在丢失的情况下,必定出现3次冗余ACK
基于这样的概率,选定3次冗余ACK作为阈值也算是合理的。在实际抓包中,大多数的快速重传都会在大于3次冗余ACK后发生。
https://blog.csdn.net/whgtheone/article/details/80983882
6.HTTP0.9、1.0、1.1、2.0、3.0演变过程?
HTTP/0.9
1990年问世,那时的HTTP并没有作为正式的标准被建立,这时的HTTP其实含有HTTP/1.0之前版本的意思,那时候还有严重设计缺陷,只支持GET方法,很快被HTTP/1.0取代。
并且协议还规定,服务器只能回应HTML格式的字符串,不能回应别的格式,当服务器发送完毕,就关闭TCP连接。
HTTP/1.0
[特点]
- 任何格式的内容都可以发送。这使得互联网不仅可以传输文字,还能传输图像、视频、二进制文件。这为互联网的大发展奠定了基础。
- 除了GET命令,还引入了POST命令和HEAD命令,丰富了浏览器与服务器的互动手段。
- HTTP请求和回应的格式也变了。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。
- 其他的新增功能还包括状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。
[不足]
- HTTP/1.0 版的主要缺点是,每个TCP连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。TCP连接的新建成本很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢(slow start)。所以,HTTP 1.0版本的性能比较差。随着网页加载的外部资源越来越多,这个问题就愈发突出了。
HTTP/1.1
[特点]
- 引入了持久连接(persistent connection),即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive。客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送Connection: close,明确要求服务器关闭TCP连接
- 引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。举例来说,客户端需要请求两个资源。以前的做法是,在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发出B请求。管道机制则是允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。
- 将Content-length字段的作用进行扩充,即声明本次回应的数据长度(一个TCP连接现在可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的)
- 采用分块传输编码,对于一些很耗时的动态操作,服务器需要等到所有操作完成,才能发送数据,显然这样的效率不高。更好的处理方法是,产生一块数据,就发送一块,采用"流模式"(stream)取代"缓存模式"(buffer)
- 1.1版还新增了许多动词方法:PUT、PATCH、HEAD、 OPTIONS、DELETE。另外,客户端请求的头信息新增了Host字段,用来指定服务器的域名
[不足]
- 虽然1.1版允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为"队头堵塞"(Head-of-line blocking)。为了避免这个问题,只有两种方法:一是减少请求数,二是同时多开持久连接。这导致了很多的网页优化技巧,比如合并脚本和样式表、将图片嵌入CSS代码、域名分片(domain sharding)等等。如果HTTP协议设计得更好一些,这些额外的工作是可以避免的。
HTTP/2.0
[特点]
- 二进制传输
HTTP2.0中所有加强性能的核心是二进制传输,在HTTP1.x中,我们是通过文本的方式传输数据。基于文本的方式传输数据存在很多缺陷,文本的表现形式有多样性,因此要做到健壮性考虑的场景必然有很多,但是二进制则不同,只有0和1的组合,因此选择了二进制传输,实现方便且健壮。
在HTTP2.0中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。 - 多路复用
所谓多路复用,即在一个TCP连接中存在多个流,即可以同时发送多个请求,对端可以通过帧中的表示知道该帧属于哪个请求。在客户端,这些帧乱序发送,到对端后再根据每个帧首部的流标识符重新组装。通过该技术,可以避免HTTP旧版本的队头阻塞问题,极大提高传输性能。 - Header压缩
在HTTP1.0中,我们使用文本的形式传输header,在header中携带cookie的话,每次都需要重复传输几百到几千的字节,这着实是一笔不小的开销。
在HTTP2.0中,我们使用了HPACK(HTTP2头部压缩算法)压缩格式对传输的header进行编码,减少了header的大小。并在两端维护了索引表,用于记录出现过的header,后面在传输过程中就可以传输已经记录过的header的键名,对端收到数据后就可以通过键名找到对应的值。 - 服务器Push
在HTTP2.0中,服务端可以在客户端某个请求后,主动推送其他资源。 - 更安全
HTTP2.0使用了tls的拓展ALPN做为协议升级,除此之外,HTTP2.0对tls的安全性做了近一步加强,通过黑名单机制禁用了几百种不再安全的加密算法。
HTTP/3.0
谷歌开发的QUIC协议,利用UDP实现可靠数据传输。
google的 QUIC协议从TCP切换到UDP
机制一:自定义连接机制
一条tcp连接是由四元组标识的,分别是源ip、源端口、目的端口,一旦一个元素发生变化时,就会断开重连,重新连接。在次进行三次握手,导致一定的延时
在TCP是没有办法的,但是基于UDP,就可以在QUIC自己的逻辑里面维护连接的机制,不再以四元组标识,而是以一个64
位的随机数作为ID来标识,而且UDP是无连接的,所以当ip或者端口变化的时候,只要ID不变,就不需要重新建立连接
机制二:自定义重传机制
tcp为了保证可靠性,通过使用序号和应答机制,来解决顺序问题和丢包问题
任何一个序号的包发过去,都要在一定的时间内得到应答,否则一旦超时,就会重发这个序号的包,通过自适应重传算法(通过采样往返时间RTT不断调整)
但是,在TCP里面超时的采样存在不准确的问题。例如发送一个包,序号100,发现没有返回,于是在发送一个100,过一阵返回ACK101.客户端收到了,但是往返的时间是多少,没法计算。是ACK到达的时候减去第一还是第二。
QUIC也有个序列号,是递增的,任何宇哥序列号的包只发送一次,下次就要加1,那样就计算可以准确了
但是有一个问题,就是怎么知道包100和包101发送的是同样的内容呢?quic定义了一个offset概念。QUIC既然是面向连接的,也就像TCP一样,是一个数据流,发送的数据在这个数据流里面有个偏移量offset,可以通过offset查看数据发送到了那里,这样只有这个offset的包没有来,就要重发。如果来了,按照offset拼接,还是能够拼成一个流。
机制三: 无阻塞的多路复用
有了自定义的连接和重传机制,就可以解决上面HTTP2.0的多路复用问题
同HTTP2.0一样,同一条 QUIC连接上可以创建多个stream,来发送多个HTTP请求,但是,QUIC是基于UDP的,一个连接上的多个stream之间没有依赖。这样,假如stream2丢了一个UDP包,后面跟着stream3的一个UDP包,虽然stream2的那个包需要重新传,但是stream3的包无需等待,就可以发给用户。
机制四:自定义流量控制
TCP的流量控制是通过滑动窗口协议。QUIC的流量控制也是通过window_update,来告诉对端它可以接受的字节数。但是QUIC的窗口是适应自己的多路复用机制的,不但在一个连接上控制窗口,还在一个连接中的每个steam控制窗口。
在TCP协议中,接收端的窗口的起始点是下一个要接收并且ACK的包,即便后来的包都到了,放在缓存里面,窗口也不能右移,因为TCP的ACK机制是基于序列号的累计应答,一旦ACK了一个序列号,就说明前面的都到了,所以是要前面的没到,后面的到了也不能ACK,就会导致后面的到了,也有可能超时重传,浪费带宽
QUIC的ACK是基于offset的,每个offset的包来了,进了缓存,就可以应答,应答后就不会重发,中间的空档会等待到来或者重发,而窗口的起始位置为当前收到的最大offset,从这个offset到当前的stream所能容纳的最大缓存,是真正的窗口的大小,显然,那样更加准确。
7.为什么会有大小端?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
https://www.jianshu.com/p/0ebdf779cab5
8.持续高QPS的账号登录,找出重复登录的账号?考虑单节点和分布式
1. 内存够的情况
分段、map、多线程。
- 分段:哈希分桶,根据哈希值对桶数目取模得到对应桶号。
- map:需要计数采用unordered_map去重;不需要计数采用set去重。
- 多线程:将数据进行哈希分桶之后,各桶内map的去重可以采用多线程执行。
2. 内存不够的情况
思路一:bitmap
位图bitmap:每个int数字只用一个比特位来做标记
位图的操作(算法)基本依赖于下面3个元操作:
set_bit(char x, int n); //将x的第n位置1,可以通过x |= (1 << n)来实现
clr_bit(char x, int n); //将x的第n位清0,可以通过x &= ~(1 << n)来实现
get_bit(char x, int n); //取出x的第n位的值,可以通过(x >> n) & 1来实现
比如,要对数字int x = 1848105做标记,就可以调用set_bit(bit_map[x/8], x%8);
除法看做求“组编号”,x/8即是 以8个位为一个小组,分组到编号为idx = x/8
的bit_map元素中,然后在组内偏移lft = x%8
个比特位。
10亿数字(int 32位):10^8 * 32 / 8 = 40亿字节 / 1024 ≈ 400万 KB / 1024 ≈ 4000 MB / 1024 ≈ 4 GB
int 32位所需bitmap大小:2^32 / 8 = 2^29 字节 / 1024 = 2^19 KB / 1024 = 2^9 MB = 512 MB
10亿数字(long long 64位):4 GB * 2 = 8GB
long long 64位所需bitmap大小:2^64 / 2^23 = 2^41 MB / 1024 = 2^31 GB / 1024 = 2^21 TB / 1024 = 2048 PB = 2 EB
https://www.cnblogs.com/zhanghaiba/p/3594559.html
思路二:多路归并排序
问题:如何给100亿个数字排序?
注:100亿个 int 型数字放在文件里面大概有 37.2GB
- 把这个37GB的大文件,用哈希分成1000个小文件,每个小文件平均38MB左右(理想情况),把100亿个数字对1000取模,模出来的结果在0到999之间,每个结果对应一个文件,所以我这里取的哈希函数是 h = x % 1000,哈希函数取得”好”,能使冲突减小,结果分布均匀。
- 按各输入文件中下一个读到的元素的大小构造一个输入流最小堆.
- 从堆顶文件里读一个元素并写入输出文件.
- 同时按读的那个文件的下一个元素的值调整堆.
- 若第3步已到达文件结尾.则从堆中删除该输入流.
- 如果堆中还有元素. 回到第2步.
3. 外存不够的情况
考虑是不是可以进行分布式处理,此时需要增加服务治理措施,比如:服务发现、负载均衡
待查~
9.C、C++编译得到的文件有什么不同?
C程序与C++程序中同样的函数在编译后的obj文件中的symbol是不同的,所以以C方式编译的obj文件与以C++方式编译的obj文件无法成功链接。
待查~
10.C++ 类的默认函数有哪些?“a=b”赋值符做了什么?
如果一个类中什么都没有,简称空类,空类占一个字节的空间。
一个空类中生成6个默认的成员函数:
- 构造函数:函数名和类名形同,无返回值,完成初始化工作
- 析构函数:完成清理工作
- 拷贝构造函数:使用同类对象初始化 创建对象
- 赋值运算符重载函数:主要是把一个对象赋值给另一个对象
- 取地址操作符重载函数:实现对普通对象取地址操作
- const修饰的取地址操作符重载函数:实现对const修饰的对象取地址操作
一个空类,相当于显示调用了这些函数:
class S
{
public:
S(); //构造函数
S(const A& a); //拷贝构造函数
~S(); //析构函数
S& operator=(const S& s); //赋值运算符重载
S* operator &(); //取地址运算符重载
const S* operator &() const; //const修饰的取地址运算符重载
};
https://www.pianshen.com/article/3600388236/
C++中对象之间的相互赋值
C++提供了克隆对象的方法,来实现上述功能。这就是对象的复制机制。
用一个已有的对象快速地复制出多个完全相同的对象。如Box box2(box1);
其作用是用已有的对象box1去克隆出一个新对象box2。
复制构造函数也是构造函数,但它只有一个参数,这个参数是本类的对象(不能是其他类的对象), 而且采用对象的引用的形式(一般约定加const声明,使参数值不能改变,以免在调用此函数时因不慎而使对象值被修改)。此复制构造函数的作用就是将实参对象的各成员值一一赋给新的对象中对应的成员。
如果用户自己未定义复制构造函数,则编译系统会自动提供一个默认的复制构造函数,其作用只是简单地复制类中每个数据成员。C++还提供另一种方便用户的复制形式,用赋值号代替括号,如Box box2=box1; //用box1初始化box2
,其一般形式为
类名 对象名1 = 对象名2;
对象之间的赋值也是通过赋值运算符“=”进行的。本来,赋值运算符“=”只能用来对单个的变量赋值,现在被扩展为两个同类对象之间的赋值,这是通过对赋值运算符的重载实现的。实际这个过程是通过成员复制来完成的,即将一个对象的成员值一一复制给另一对象的对应成员。对象赋值的一般形式为:对象名1 = 对象名2;
类的数据成员中不能包括动态分配的数据,否则在赋值时可能出现严重后果 ,答案如下:
问题本质是,如果某个类的对象a里面有动态申请的数据,当你把a直接复制给同一个类的对象b的时候,a中的动态指针也给了b,这样a,b中的指针指向同一块内存.这样无论a或者b释放内存都会导致另外一个访内违例崩溃.
解决这个问题需要自己重载赋值运算符和拷贝构造函数.如果不想重载,并且也不喜欢出现错误,那么就把这两类函数声明为私有。
浅拷贝——指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。
https://www.cnblogs.com/liushui-sky/p/8004763.html
11.select与epoll的优缺点?
select优点
1)select()的可移植性更好,在某些Unix系统上不支持poll()
2)select() 对于超时值提供了更好的精度:微秒,而poll是毫秒。
select缺点
1) 单个进程可监视的fd数量被限制。
2) 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
3) 对fd进行扫描时是线性扫描。fd剧增后,IO效率较低,因为每次调用都对fd进行线性扫描遍历,所以随着fd的增加会造成遍历速度慢的性能问题
4)select() 函数的超时参数在返回时也是未定义的,考虑到可移植性,每次在超时之后在下一次进入到select之前都需要重新设置超时参数。
epoll
底层实现:
epoll在底层实现了自己的高速缓存区,并且建立了一个红黑树用于存放socket,另外维护了一个链表用来存放准备就绪的事件。
工作过程:
执行epoll_ create时,创建了红黑树和就绪链表,执行epoll_ ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
epoll优点
1)支持一个进程打开大数目的socket描述符(FD)
2)IO效率不随FD数目增加而线性下降
3)使用mmap加速内核与用户空间的消息传递。
总结:
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。
而epoll其实也需要调用 epoll_ wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在 epoll_wait中进入睡眠的进程。
虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的 时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间,这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内 部定义的等待队列),这也能节省不少的开销。
https://blog.csdn.net/u014162133/article/details/84143684
select、poll、epoll适用场景:https://www.jianshu.com/p/dfd940e7fca2
12.Windows下使用epoll的有哪些框架?
Windows的IOCP:https://www.cnblogs.com/xiaobingqianrui/p/9258665.html