面试八股题

结构体对齐问题

为什么结构体要内存对齐,对于某些硬件平台,访问int32_t,float,double这些类型的成员时,只能按照4字节对齐访问,也就是成员地址必须是4的整数倍。
对齐规则:

  • 第一个成员在与结构体变量起始地址偏移量为0的地址处
  • 其他成员要对齐到成员自身大小的整数倍的地址处
  • 结构体最大对齐数,指的是所有成员中最大的对齐数
  • 结构体总大小为结构体最大对齐数的整数倍
  • 结构体1嵌套了结构体2的时候,嵌套的结构体2对齐到自己的最大对齐数的整数倍处,结构体1的整体大小就是最大对齐数的整数倍
    在这里插入图片描述
    在这里插入图片描述

指针和引用

指针:指针是一个特殊的变量,它里面存储的的数值为内存里的一个地址,通过*访问内存地址所指向的值

引用:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

区别:

  • 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
  • 指针可以有多级,引用只有一级
  • 指针可以为空,引用不能为NULL且在定义时必须初始化
  • 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
  • sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
  • 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
  • 引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间(具体情况还要具体分析)。
  • 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
  • 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。
  • 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。

静多态和动多态

静多态:在编译阶段就已经绑定了函数地址
动多态:在运行期间绑定,主要是给父类指针传递不同的类型,调用的函数也会不同。

int add(int a, int b){
	return a+b;
}
ddouble add(double a,double b){
	return a+b;
}
class Person{
	virtual void BuyTick(){ cout<<"父类"<<endl;}
};
class Student : public Person{
	virtual void BuyTick(){ cout<<"子类"<<endl;}
};
void TickWindow(Person &p){
	p.BuyTick();
}

int main(){
	//静多态,编译的时候就已经绑定了函数地址
	add(1,2);
	add(3.0,4.0);

	//动多态,只有在调用TickWindow函数时才知道局部变量p指向谁的BuyTick函数
	Person ps;
	Student st;
	TickWindow(ps);
	TickWindow(st)
}

ET和LT

epoll有两种模式ET和LT
水平触发LT:只要缓冲区中有数据就一直通知,默认情况下是LT模式,若一次性没读完,则epoll_wait继续读取
边缘触发ET:epoll_wait只通知一次,之后再有数据才会通知

事件通知机制,例子:

  1. 一个客户端socket对应的fd已经注册到了epoll实例中
  2. 客户端socket发送了2kb的数据
  3. 服务器调用epoll_wait得到通知说fd就绪
  4. 服务端从fd读取了1kb数据
  5. 回到步骤3(再次调用epoll_wait形成循环)
    在这里插入图片描述
    LT模式:当监听到两个文件描述符有事件发生,则将其添加到list_head中,epoll会将list_head中的fd从内核复制到events中,若一次性没读完,则epoll_wait继续通知读取,此时还需复制一次。会产生多次交互,造成性能下降
    ET模式:当监听到两个文件描述符有事件发生,则将其添加到list_head中,epoll会将list_head中的fd从内核复制到events中,epoll_wait通知一次,并将其从list_head中删除fd,此时epoll_wait不会再通知。但还有数据没读完,那就只能等下一次通知了。或者在读取一次后自己手动将fd添加到list_head中,则epoll_wait还会再通知(变成LT模式了),直到读完,再删除list_head中的就绪fd。第二种方法就是while(True)循环读取,读取完成后提示读取完毕,跳出循环。这类情况下需要设置文件描述符为非阻塞,若阻塞情况下会导致一直直阻塞在这等待新的数据到来。

优缺点:
LT会频繁与内核交互,造成性能下降;还会有惊群现象,若不停的通知需要读数据的时候,会唤醒所有线程来竞争任务,这是没必要的。但epoll默认的是LT模式。
ET只能读取一次,若在读取需要手动改变代码,且读取时不需要频繁唤醒线程,也不需要频繁与内核交互,复制fd和通知,大大提高效率。
所以在数据量较大的情况下ET是合适的。反之!

红黑树

特性:

  • 节点是红色或黑色

  • 根是黑色

  • 叶子节点都是黑色,叶子节点指的是最底层的空节点(下图的null节点才是叶子节点,null节点的父节点在红黑树中不将其看作叶子节点)

  • 红色节点的的子节点都是黑色,红色节点的父节点都是黑色,从根节点到叶子节点的所有路径上不能有2个连续的红色节点

  • 从任一节点到叶子节点的所有路径都包含相同数目的黑色节点。
    截图来源于水印@小七mod
    红黑树的插入:叔是红染色,不是红就换,父染黑原祖父染红

  • 当前节点是根节点:根节点改为黑色

  • 当前节点的父节点是黑节点:保持不变

  • 当前节点的父节点是红节点,并且当前节点的叔叔节点是红节点:把父节点和叔叔节点改为黑色,把祖父节点改成红色,把祖父节点作为当前节点,向上判断

  • 当前节点的父节点是红节点,并且当前节点的叔叔节点不是红节点

    • 祖父节点->父节点->当前节点,是一直向右:做左旋操作,再把父节点改为黑色,之前的祖父节点改为红色
    • 祖父节点->父节点->当前节点,是一直向左:做右旋操作,再把父节点改成黑色,之前的祖父节点改成红色
    • 祖父节点->父节点->当前节点,是先向左在向右:对当前节点做左旋操作,在对当前节点做右旋操作,然后把当前节点改为黑色,之前的祖父节点改为红色
    • 祖父节点->父节点->当前节点,是先向右在向左:对当前节点做右旋操作,在对当前节点做左旋操作,然后把当前节点改成黑色,之前的祖父节点改成红色

c++的存储类型

auto:局部变量的声明,但一般不写
static:全局静态变量和局部静态变量,全局静态变量在整个文件中都可以被访问到,局部静态变量不会分配在栈上
		并只会被初始化一次
extern:将c文件编译成二进制文件,第二个文件若想访问第一个文件的变量,则使用extern关键字来声明一下。
register:变量除了放在内存里,还会被放在寄存器里,当需要反复读写时,可以考虑将变量放在寄存器。
		这只是提的一个建议,编译器不一定会将其放在寄存器中

C++的基础变量类型

short(2),int(4),long(8),long long(8),char(1),float(4),double(8),bool(1)
windows下,采用gbk编码:一个汉字占2个字节;linux下采用UTF-8编码:一个汉字占3个字节

C++中不同作用域的变量,哪些在堆上存储,哪些在栈上存储?
堆区:全局变量,静态变量,malloc函数
栈区:函数的参数值,局部
变量存储的位置取决于其声明方式、作用域和生命周期。栈上的变量由系统自动管理其生命周期,而堆上的变量需要手动管理内存的分配和释放变量

数组的底层实现

数组的底层实现是通过一块连续的内存空间来存储元素。数组的每个元素都紧密地排列在内存中,并且可以通过索引来访问每个元素

vector底层数据结构为数组,支持快速随访问
list底层数据结构为双向链表,支持快速增删
deque底层数据结构为一个中央控制器和多个缓冲区
queue,stack底层一般用list和deque实现
set,multiset,map,multisetmap底层都是红黑树。set无序,map有序
hash_set,hash_multiset,hash_map,hash_mutimap底层都是hash表

指针和引用的区别

引用是直接访问一个变量,指针是间接访问
引用是变量的一个别名,本身不单独分配自己的内存空间,指针有自己的内存空间
引用在开始的时候就绑定到了一个内存空间,所以他只能是这个内存空间的名字,
不存在空值的引用,但存在空值的指针

HTTP请求主要用于:

  1. 客户与服务器建立连接
  2. 客户向服务器提出请求
  3. 服务器接受请求,并根据请求返回响应的文件作为应答
  4. 客户与服务器关闭连接

HTTP与TCP的关系

  • TCP是传输层协议,HTTP是应用层协议
  • HTTP要建立在TCP连接基础上的

vector操作

vector<> c;

  • c.size():返回容器可容纳的最多元素个数
  • c.max_size():返回容器c可容纳的最多元素个数
  • c.capacity():返回c在需要重新分配更多存储空间之前能够存储的元素总数
  • c.reserve():告诉容器c应该预留多少个元素的空间
  • c.pop_size():删除vector的尾元素
  • c.clear():清空vector中的所有元素
  • c.erase():(传入单个值)删除单个元素,(传入一个元素区间)删除一个区间内的所有元素
//假设数组的capacity是翻倍增长的
int main(){
    vector<int> vec(2);  //预定义大小为2,默认值为0,此时数组里元素为两个0
    int a = vec.size(); //2        
    int b = vec.capacity(); //2;数组的容量
    vec.push_back(1); 
    //vec.push_back(2);
    //vec.push_back(3);
    //vec.push_back(4);
    //预定义空间大小,若空间大小>实际空间大小,则vec.capacity()的值为预设的空间大小
    vec.reserve(3); 
    int c = vec.size(); //3
    for(int i = 0;i<c;i++){
        cout<<vec[i]<<endl;  //数组内元素为0,0,1
    }
    int d = vec.capacity(); //容量翻倍增长 2*2 = 4
    cout<<a<<" " <<b<<" "<<c<<" "<<d<<endl; //2 2 3 4 
    return 0;
}

值传递和引用传递:
值传递:实参给形参传递一个值,但并不能影响到实参,修改的只是实参的副本:形参
引用传递:在传参数时进行引用传递,相当于给实参起了个别名,即形参,对形参的操作会作用到实参上
值传递会给形参分配空间,而引用传递并不需要给形参开辟额外的空间,只需要给传进来的实参起个别名,所以引用传递的效率会比按值传递的效率高。

TCP和UDP

区别:
TCP是基于连接的,UDP是无连接的
TCP要求的系统资源较多,UDP较少
TCP是流模式,UDP是数据报模式
TCP保证数据正确性,UDP可能丢包
TCP保证数据顺序,UDP不保证
UDP的速度较快
TCP是点到点的,UDP支持一对一,一对多,多对一和多对多的交互

UDP分片:
UDP有接收缓冲区,但没有发送缓冲区,只要有数据就发送,不管对方是否可以正确接收,所以不需要发送缓冲区。

UDP:当套接口接收缓冲区满时,新来的数据报无法进入接收缓冲区,此数据报就丢弃,UDP没有流量控制,快的发送者可以很容易的就淹没慢的接收者;如果再传输过程中,一次传输被分成多个分片,传输中有一个小的分片丢失,那接收端最终会舍弃整个文件,导致传输失败。

UDP丢包原因:

  1. UDP缓冲区满,造成的丢包
  2. UDP缓冲区过小或文件过大,造成的丢包
  3. ARP缓存过期,导致丢包ARP的缓存时间约10分钟,ARP缓存列表没有对方的MAC地址或缓存过期的时候,会发送ARP请求MAC地址,在没有获得MAC地址之前,UDP包会被内核缓存到一个队列中,默认最多缓存3个包,多余的UDP包会被丢弃。
  4. 接收端处理时间过长导致丢包:处理数据花费的时间过长,中间发过来的包可能丢失
  5. 发送的包巨大丢包
  6. 发送包的频率太快

UDP丢包的解决方案:

  1. 从发送端解决-延迟发送
  2. 从接收端解决:数据接收与数据处理相分离
  3. 从接收端解决:修改接收缓存大小使用条件

UDP协议的正确使用场合:

  1. 高通信实时性要求和低持续性要求的场景下
  2. 多点通信的场景下

TCP和UDP的基本区别

TCP的慢启动:

  • 避免拥塞:在网络拥塞的情况下,若发送方突然发送大量数据,可能会导致网络拥塞加剧,丢包率增加,甚至导致网络崩溃,慢启动可以以较慢的速率发送数据,然后逐渐增加发送频率.
  • 控制窗口大小:慢启动会根据网络的拥塞状况逐渐增加滑动窗口大小,从而允许发送方发送更多数据
  • 测试网络情况:慢启动的初衷之一是在开始时发送方并不了解网络的拥塞程度.TCP可以通过观察网络中是否出现数据包丢失来推测网络的负载情况,并相应的调整发送速率.

TCP的流量监控:让发送方发送速率不要太快,要让接收方来得及接收.
发送方根据滑动窗口来发送对应大小的数据,接收方接收之后进行反馈,告诉发送方我还能接收多少数据,然后发送方再根据这个信息进行发送,若中间有部分报文丢失,则再接收方反馈的时候会告诉发送方,我接受到哪里了,发送方会重新发送,若接收方反馈接收能力为0了,那发送方会发送零窗口探测报文,约定接收方必须接收零窗口报文,并反馈自己的接收窗口,由于零窗口报文和反馈报文都有丢失的情况会造成死锁的产生,TCP中会有持续计时器,对报文进行重复发送.

TCP的拥塞控制:
假定条件:
1.数据是单方向传输,另一个方向只传送确认;
2.接收方总是有足够大的缓存空间,因而发送方发送窗口的大小由网络的拥塞程度来决定;
3.以最大报文段MSS的个数为讨论问题的单位,而不是以字节为单位

慢开始

  • 发送方维护一个叫做拥塞窗口cwnd的状态变量,其值取决于网络的拥塞程度,并动态变化
    • 拥塞窗口cwnd的维护原则:只要网络没有出现拥塞,拥塞窗口就再增大一些;但只要网络出现拥塞,拥塞窗口就减少一些
    • 判断出现网络拥塞的依据:没有按时收到应当到达的确认报文(即发生超时重传)
  • 发送方将拥塞窗口作为发送窗口swnd,即swnd = cwnd
  • 维护一个慢开始门限ssthresh状态变量:
    • 当cwnd < ssthresh时,使用慢开始算法
    • 当cwnd > ssthresh时,停止使用慢开始算法而改用拥塞避免算法
    • 当cwnd = ssthresh时,既可以使用慢开始算法,也可以使用拥塞避免算法

若在发送的时候丢失了报文段,则会产生超时重传(重传计时器超时),判断网络可能出现了拥塞,进而执行的操作是:1.将ssthresh值更新为发生拥塞时cwnd值的一半;2.将cwnd值减少为1,并重新开始执行慢开始算法
在这里插入图片描述
初始慢开始门限值ssthresh为16,传输速度按指数增加,到达门限值后,则执行拥塞避免,每次传输值+1,直到出现丢失报文会引起超时重传,此时判断网络发生阻塞(拥塞窗口为24),然后就将门限值ssthresh更新为发生拥塞时的一半,即新的ssthresh值变为12,cwnd值减少为1,并重新开始执行慢开始算法

丢失个别报文,并不是发生了拥塞,但依旧会错误的启动慢开始算法,则导致传输效率低,采用快重传可以让发送方尽早知道发生了个别报文段的丢失.

快重传:使发送方进行重传,而不是等超时重传计时器超时再重传

  • 要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认
  • 即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认
  • 发送方一旦收到3个连续的重复确认,就将响应的报文段立即重传,而不是等待该报文段的超时重传计时器超时再重传.

在这里插入图片描述
快恢复: 一旦收到3个重复确认,就知道现在只是丢失了个别的报文段,于是不启动慢开始算法,而执行快恢复算法.

  • 发送方将慢开始门限ssthresh值和拥塞窗口cwnd值调整为当前窗口的一半,开始执行拥塞避免算法.
  • 也有的快恢复算法实现是把快恢复开始时的拥塞窗口cwnd值再增大一些,即等于新的ssthresh+3(收到3个重复的确认,就表明有3个数据报文已经离开了网络,不再消耗网络资源,则可以适当把拥塞窗口扩大些)
    在这里插入图片描述

TCP的三次握手和四次挥手:

为什么不能是两次握手? 若进行两次握手,此时第一个请求报文段丢失,将会产生超时重传,服务器正确接收到了超时重传的报文后开始通信,等通信结束后,四次挥手关闭连接之后,却收到了第一个丢失的报文,服务器会以为客户端还要重新建立连接,服务器就会确认建立连接,但此时客户端并没有请求连接,造成资源的浪费.(为了防止已失效的连接请求报文段突然又传送到了TCP服务器,因此导致错误)

为什么不能是三次挥手? 客户端发送关闭请求的包,说明自己不再发送数据,服务器反馈之后告诉客户端自己知道你不再发送了,但此时服务器可能还没有接收完数据,等接收完数据客户端再发送关闭连接的请求包,告诉自己接收完了,可以关闭了,然后客户端再反馈回来说关闭连接了,这就是四次挥手.若服务器端在反馈给客户端关闭请求的时候接收完数据的时候,按理是可以同时发送关闭请求的,但一般这种情况很少出现.

2MSL:客户端接收到服务器发送的关闭请求之后会等2MSL的时间才关闭,这是因为防止第四次挥手的包丢掉.若直接关闭,当第四次挥手的包丢失时会造成服务器一直请求重新发送,但是客户端无响应,造成资源浪费.
在这里插入图片描述

OSI七层模型:物数网传会表应
HTTP在应用层、TCP,UDP在传输层、IP在网络协议层、ARP在数据链路层、DNS在应用层

DNS的查询方式

递归解析:局部DNS服务器自己负责向其他DNS服务器进行查询,一般是先向该域名的根域服务器查询,再由根域服务器一级一级向下查询。得到查询结果最后返回局部DNS服务器,再由局部DNS服务器返回给客户端。

迭代解析:局部DNS服务器不是自己向其他DNS服务器进行查询,而是把能解析该域名的其他DNS服务器的IP返回给客户端DNS程序,客户端DNS程序再继续向这些DNS服务器进行查询,直到得到查询结果为止。

设计模式:

  • 工厂模式:用一个工厂类来根据输入的条件实例化不同的类,然后根据类中相应的虚函数得到不同的结果
  • 策略模式:对于用户而言,这些算法都是一样的,但往往根据传入的参数不同或者其他差异,会各自调用算法对应的实现方式。策略相当于简单工厂模式中的产品,上下文类比于简单工厂模式中的工厂
  • 单例模式:保证一个类仅有一个实例,并提供一个访问他的全局访问点。类只能有一个实例化对象,需要定义静态成员函数作为获得该类的实例对象的接口。

虚析构函数:
主要是来解决基类指针指向派生类对象,并用基类指针释放派生类对象.也就是说父类的虚析构函数会调用子类的析构函数(在删除指向子类对象的父类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,从而防止内存泄漏,在删除基类指针的时候,会调用子类的析构函数,再调用基类的虚析构函数)

智能指针

智能指针本质是一个类模板,它可以创建任意类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间.

  • auto_ptr:管理权限转移的思想.原对象拷贝给新对象的时候,原对象就会被设置为nullptr,此时就只有新对象指向一块资源空间.一般禁止使用
  • unique_ptr:直接将拷贝构造函数和赋值重载函数给禁用掉,不让其进行拷贝和赋值.
  • shared_ptr:shared_ptr允许多个智能指针可以指向同一块资源,并能够保证共享的资源只会被释放一次,因此程序不会崩掉.采用引用计数原理来实现多个shared_ptr对象之间共享资源
    • shared_ptr在内部会维护一份引用计数,用来记录该份资源被几个对象共享
    • 当一个shared_ptr对象被销毁时,析构函数内就会将该计数减一
    • 如果引用计数减为0后,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源
    • 如果引用计数不是0,就说明自己还有其他对象在使用,则不能释放该资源,否则其他对象就成为野指针
    • 循环引用:双向链表中,如果想要让创建出来的链表的结点都定义成shared_ptr智能指针,那么也需要将节点内的pre和next都定义成shared_ptr的智能指针.如果定义成普通指针没那么就不能赋值给shared_ptr的智能指针.当两个结点相互引用的时候,就会出现循环引用的现象.解决方法:weak_ptr类型的智能指针,指向shared_ptr,并且不会改变shared_ptr的引用计数,一旦最后一个shared_ptr被销毁时,对象就会被释放.

malloc\free和new\delete的区别:

  1. malloc\free是c语言中的库函数,new\delete是C++中的操作符
  2. new自动计算所需分配内存大小,malloc需要手动计算
  3. new返回的是对象类型的指针,malloc返回的是void*,可以进行类型转换
  4. new分配失败会抛出异常,malloc分配失败返回的是null;
  5. new是在freestore上分配内存,malloc在堆上分配内存 — free store是一个自由存储区,若重写new方法则可以在自定义位置分配内存,若不重写,则默认在堆上分配
  6. delete需要对象类型的指针,free是void*类型的指针
  7. new申请足够的空间,调用构造函数,初始化成员变量
  8. delete调用析构函数,释放空间
  9. malloc分配的是虚拟内存,new是物理内存,malloc是需要通过一个缺页中断来映射到物理内存的.

虚函数和虚函数表,虚函数指针

虚函数表会出现在一个带有虚函数的类中或者从这样的类派生的类,属于类.虚函数表相当于一个数组,其中存放的只有虚函数,可以是继承而来的,也可以是自己本身的;多重继承时可以有多个虚函数表.
虚函数指针是一个指向虚函数表的指针,是属于对象的,只有对象被创建时才会有。类的对象中包含一个虚函数指针,指向这个虚函数表。当我们通过基类的指针或引用调用虚函数时,实际上是通过这个虚函数指针找到虚函数表,然后在表中查找并调用响应的函数,这个过程是在运行时完成的,所以可以实现运行时多态。

静多态和动多态

静多态:通过函数重载或函数模板实现;
动多态:运行时多态,通过虚函数表实现,父类对象的某个接口会随着派生类对象的不同而执行不同的操作。

局部变量和全局变量的区别:

  • 作用域不同:局部变量的作用域在当前函数或者循环,全局变量的作用域在整个程序,函数内部会优先使用局部变量再使用全局变量
  • 内存存储方式不同:全局变量存储在全局数据区中,局部变量存储在栈区
  • 生命周期不同:全局变量的声明周期和主程序一样,随程序的销毁而销毁,局部变量在函数内部或循环内部,随函数的退出或循环退出就不存在了

左值和右值

左值:表示存储在计算机内存的对象,而不是常量或计算的结果.或者说左值是代表一个内存地址值,并且通过这个内存地址就可以对内存进行读写操作,这也就是为什么左值可以被赋值.
右值:当一个符号或者常量在操作符右边的时候,计算机就读取他们的右值,也就是代表的真实值.简单来说,右值相当于数据值,左值相当于地址值,右值指的是引用了一个存储在某个内存地址里的数据.

可以取地址的,有名字的,非临时的就是左值;
不能取地址的,没有名字的,临时的就是右值;

左值引用和右值引用:左值引用的目的是防止函数在进行传参和返回值的时候进行对象拷贝。右值引用是为了移动语义和完美转发

i++和++i:i++是右值,++i是左值
后置++操作中编译器首先会生成一份x值的临时复制,然后才对x递增,最后返回临时复制内容。而前置++,则会直接对x递增后马上返回其自身,所以++i是左值。

右值引用:是一种引用右值且只能引用右值的方法,右值引用则是在类型后添加&&,常量左值引用可以绑定右值

#include <iostream>
class X{
public:
	X(){ cout<<"X ctor"<<endl;}
	X(const X&x){ cout<<"X ctor"<<endl; }
	~X() {cout<<"X dtor"<<endl;}
	void show(){ cout<<"show X"<<endl;}
};
X make_x(){
	X x1;    1 -> 创建x1
	return x1;  2 -> x1赋值给X
}
int main(){
	X &&x2 = make_x();   3 -> 析构make_x()中的X变量
	x2.show();   4 -> show X  执行完析构 5
}

结果:
X ctor             1
X copy ctor        2
X dtor             3
show X             4
X dtor             5

从运行结果可以看出代码发生了两次构造,一次默认构造,一次复制构造。由于x2是一个右值引用,引用的对象是函数make_x返回的临时对象,因此该临时对象的生命周期得到延长,故可以使用x2继续调用show函数而不会发生如何问题。但延长生命周期并不是右值引用的最终目的,其真实目的应该是减少对象复制,提升程序性能。
在这里插入图片描述
左值引用和完美转发

#include <iostream>
#include <string>
template<class T>
void show_type(T t){
	cout<<typeid(t).name()<<endl;
}
template<class T>
void normal_forwarding(T &&t){  //完美转发
	//希望传入左值的时候,show_type收到的也是左值,传入右值时,收到的也是右值
	//为了将转发的左右值属性也转发到目标函数中,使用类型强转
	show_type(static_cast<T &&>(t));   
	//若T被推到为string&时,static_cast<T &&>被推导为static_cast<string&>  遇左则左
	//若T被推到为string时,static_cast<T &&>被推导为static_cast<string&&>  
}
string get_string(){
	return "hi";
}
int main(){
	string s = "hello world";
	normal_forwarding(s);
	//get_string是右值,之所以可以传入是因为normal_forwarding函数中使用了&&右值引用/万能引用 
	normal_forwarding(get_string());  
}

二叉树和二叉搜索树:
二叉搜索树是二叉树的一种,在中序遍历之后的结果为有序的情况下是二叉搜索树。

深拷贝和浅拷贝:
浅拷贝是简单的赋值拷贝操作,深拷贝在堆区重新申请空间,进行拷贝操作,二者不会共享同一块内存空间,彼此之间的修改互不影响。
浅拷贝就是简单的赋值操作,如果两个指针都指向同一个地址,则会出现重复释放问题,所以需要深拷贝来解决该问题,也就是new一个新的地址,将数据复制过来,如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。

移动语义:

进程调度算法:

快排,优化问题

MySQL

锁有那些?
按粒度分类:

  1. 行锁:锁某行数据,锁粒度最小,并发度高
  2. 表锁:锁整张表,锁粒度最大,并发度低
  3. 间隙锁:锁的是一个区间

还可以分为:

  1. 共享锁:读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写
  2. 排他锁:写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写
  3. 乐观锁:并不会真正的锁某行记录,而是通过一个版本号来实现
  4. 悲观锁:行锁,表锁等都是悲观锁(不管别人用不用,反正就上锁)

MySQL慢查询:
执行慢的sql语句(执行事件较长的查询),包括crud,一般是查询,所以称为慢查询
优化

  1. 检查是否走了索引,如果没有则优化sql利用索引
  2. 检查利用的索引,是否是最优索引
  3. 检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
  4. 检查表中数据是否过多,是否应该进行分库分表
  5. 检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源。

数据库索引:
索引是一种用于提高查询效率的数据结构,类似于书籍的目录,可以帮助数据库系统快速的定位和访问数据。
索引的优点:

  • 提高查询速度:索引可以加快数据库的查询速度,通过利用索引,数据库可以快速定位到满足查询条件的数据,不需要扫描整个表。
  • 减少IO操作:索引可以减少磁盘IO操作,因为数据库可以直接通过索引定位到数据所在的磁盘位置,而不需要扫描整个表
  • 加速排序:若查询需要对结果进行排序,索引可以提供有序的数据,从而加快排序操作的速度。

索引的缺点:

  • 占用存储空间:索引需要占用额外的存储空间,特别是在大规模数据表中创建复合索引时,可能会占用较大的存储空间。
  • 增加写操作的开销:当对表进行插入,更新或删除时,索引也需要被更新,这会增加写操作的开销
  • 增加索引维护的成本:当表中的数据发生变化时,索引需要被维护,包括索引的创建,更新和删除操作,这会增加数据库的维护成本。

内连接和外联接:

事务

数据库事务:访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的单位。
回滚点:某些操作完成后,后续的操作有可能成功也有可能失败,不管成功还是失败,在目前成功的位置设置一个回滚点,若后续操作失败,返回到该位置,而不需要返回所有操作。
事务的特性:

  • 原子性:事务是一个不可分割的工作单位,要么全部发生,要么全不发生
  • 一致性:事务前后数据的完整性必须保持一致
  • 持久性:一旦事务被提交,对数据库中数据的改变就是永久的
  • 隔离性:考虑到并发操作,一个用户的事务不能被其他用户的事务所干扰,多个事务之间互相隔离
    • 读脏数据:一个事务读取到了另一个事务尚未提交的数据
    • 不可重复读:两次读取的内容不一样
    • 幻读:两次读取的数据的数量不一致,比如事务一第一次读取了10条数据,第二次读取的之前,事务二有插入了几条满足条件的数据,这时就会多读出来几条。
    • 丢弃修改:两个事务同时对一个值进行修改,导致结果覆盖。

数据库隔离级别:

  • 未提交读:事务一数A为50修改为100,其他事务是看得到的,但是事务一并没有修改,而是回滚了,此时A还是50,但是另一个事务看到A是100
  • 提交读:事务一读取A为50,但是另一个事务修改了A为100,此时事务一再次读取发现A变成100了
  • 重复读:对一个记录读取多次是相同的
  • 可串行化读:串行化和并发情况下读取的数据是一样的

数据库的存储过程:
数据库的存储过程是一种在数据库中存储和执行的一组预定义的SQL语句,可以看作是一段可重复使用的程序代码,用于封装和执行特定的数据库操作和业务逻辑。存储过程通常由一系列SQL语句,流程控制语句,变量定义和参数等组成,他们可以接收输入参数,执行一系列的操作,并返回结果。
将需要多次调用的实现某个特定任务的代码编写成一个过程,将其保存在数据库中,并由SQLServer服务器通过过程名来调用他们,这个过程就叫做存储过程

中断和异常的区别
中断是由外部事件触发的,异常是由程序内部错误触发的。

  • 中断是指来自外部设备或其他程序的异步事件,他会打断当前正在执行的程序,引起操作系统的注意。中断可能是硬件中断或软件中断。当中断发生后,操作系统会中断当前程序的执行,保存当前上下文,并转而处理中断事件。处理完中断后,恢复被中断程序的执行
  • 异常是指程序在执行过程中发生的一些意外或非法的事件,如除零错误,非法访问内存等。异常通常是由程序内部的错误引起的,它会导致程序无法正常继续执行。当异常事件发生时,操作系统会中断当前进程给的执行,保存上下文,并处理异常事件,处理完异常事件后,操作系统可能会终止异常的执行或采取其他措施进行处理。

cookie、session、token区别
三者都是用于识别用户身份的技术,但他们的工作方式和使用场景有所不同:

  • cookie:是服务器发送到用户浏览器并保存在浏览器上的一块数据,主要用于记录用户的一些信息。当每次浏览器向服务器发送请求时,都会自动带上这个cookie数据
  • Session:是在服务器端保存的一个数据结构,用来跟踪用户的状态。这个数据可以保存在集群、数据库、文件中等。用户浏览器的每一次请求,服务器都会根据这个Session来识别用户状态。
  • Token:是服务端生成的一串字符串,作为客户端进行请求的一个凭证。当用户第一次登录后,服务器生成一个Token返回给客户端,以后客户端只需带上这个Token来请求数据,无序再次登录验证。

区别:

  • cookie和Session是服务器用来识别用户的,而Token是无状态的,它不需要在服务器端保存用户状态
  • Cookie数据存放在客户的浏览器上,Session数据放在服务器上。
  • Token设计目的是为了减轻服务器压力,不需要在服务器保存会话信息。Token更适用于移动应用和单页面应用。

Lambda表达式
Lambda表达式与普通函数类似,也有参数列表、返回值类型和函数体,只是他的定义方式更简洁,并且可以在函数内部定义。Lambda表达式一般与一些STL的算法结合使用。
优点:Lambda表达式可以直接看到上下文,让代码更加简洁、清晰、可读性强

int main(){
	vector<int> vec(6);
	grnerate(vec.begin(),vec.end(),[](){ return rand()%10;});  //插入随机数
	//用lambda表达式统计数组中被2整除的元素个数
	//count_if用来计算容器中符合条件的元素有多少个
	int n1 = count_if(vec.begin(),vec.end(),[](int x){ return x%2 == 0;});
	//for_each()用来逐个遍历容器元素,并执行由单参数函数对象所定义的操作
	int n2 = 0;
	for_each(vec.begin(),vec.end(),[&](int x){ n2 += ( x % 2 == 0);});
}

C++11新特性:

  • nullptr代替了NULL
  • 引入了auto和decltype这俩关键字实现了类型推导
  • 基于范围的for循环for(auto &i:res){}
  • 类和结构体中初始化列表
  • Lambda表达式
  • forward_list单项链表
  • 右值引用和move语义
  • 无序容器和正则表达式
  • 成员变量默认初始化
  • 智能指针等

为什么析构函数一般写成虚函数:
由于类的多态,常常会有父类指针指向子类对象,若删除父类指针,则会调用该指针指向的子类析构函数,而子类的析构函数又自动调用父类的析构函数,这样整个子类的对象完全被释放。若不写成虚函数,则删除父类指针时,只会调用父类的析构函数而不调用子类的析构函数,造成子类对象析构不完全,造成内存泄漏。防止只析构父类而不析构子类的状况发生。(子类的析构函数会默认调用父类的析构函数,用于析构该基类子对象)

死锁

死锁是指两个或多个线程互相等待对方数据的过程
死锁产生的四个必要条件:

  1. 互斥条件:进程对所需求的资源具有排他性,若有其他进程请求该资源,请求进程只能等待。
  2. 不剥夺条件:进程在所获得的资源未释放前,不能被其他进程强行夺走,只能自己释放
  3. 请求和保持条件:进程当前所拥有的资源在进程请求其他新资源时,由该进程继续占有
  4. 循环等待条件:存在一种进程资源循环等待链,链中每个进程已获得的资源同时被链中下一个进程锁请求

处理方法:

  • 鸵鸟策略:忽略死锁
  • 死锁检测和死锁恢复:利用抢占恢复、利用回滚恢复、通过杀死进程恢复
  • 死锁预防:
    • 破坏互斥条件
    • 破坏请求和保持条件:所有进程在开始执行前请求所需要的全部资源
    • 破坏不剥夺条件:允许抢占资源
    • 破坏循环请求等待:给资源统一编号,进程只能按编号顺序来请求资源
  • 死锁避免

Linux查看哪个端口被进程占用:

  • lsof:列出当前系统中打开的所有文件,包括网络端口。可以使用lsof命令查看某个端口被哪个进程占用lsof -i
  • netstat:可显示网络连接,路由表,网络接口信息等。可以使用netstat命令查看某个端口被哪个进程占用netstat -tlnp | grep 端口号
  • ss:列出当前系统中打开的套接字信息,包括网络端口。可以使用ss命令查看某个端口被哪个进程占用ss -tlnp | grep 端口号
  • fuser:可以查看某个文件或目录被哪个进程占用。fuser 端口号/tcp
  • ps:列出当前系统中正在运行的进程信息。结合grep来使用ps -ef | grep 进程名

i++和++i断点区别:

  • i++返回原来的值,先用后加;++i返回加一后的值,先加后用;
  • i++先赋值,再自增;++i先自增,后赋值
char a[] = {'a','b','c','d','e'};
char b[] = “abcde”;
cout << sizeof(a)<<endl;  //5
cout<<sizeof(b);  //6  相当于a[6]最后一位保存’\0‘

有符号和无符号数取值范围:
看该字符的大小,由于c/c++中会以\0结束,则需要-1
有符号数:-2^(n-1) ~2^(n-1)-1,这是由于符号需要占一位,所以需要(n-1),而以\0结束则最后需要-1位
无符号数:0~-2^n-1,没有符号故不需要占一位,最后-1也是因为\0

求字符串长度:

char类型求长度:
strlen(参数);  //参数可以填变量名也可以填字符串

sizeof和strlen的区别:
sizeof是运算符,strlen是函数
sizeof操作符的结果类型是size_t,该类型保证能容纳实现所建立的最大对象的字节大小
sizeof可以用类型做参数,strlen只能用char做参数,且必须是以\0结尾的
sizeof在编译时期就计算出来,strlen在运行之后才能计算出来
strlen计算字符串的具体长度,不包括字符串结束符,返回字符个数。sizeof计算声明后所占的内存数,不是实际长度。

string类型求长度:
.size()函数.length()

赋值语义:
赋值语义会先拷贝一个临时变量,再将临时变量拷贝给左边的变量。在函数返回值中也是先产生临时变量再拷贝,,这样每次赋值都会产生一次临时变量,当赋值复杂时,效率会变低。
为解决多余临时变量问题,提出了move语义
move语义:将旧指针的值复制到新指针,并把旧指针的值赋为NULL。如果我们能确定某个值是一个非常量右值,则我们在进行临时对象的拷贝时,可以不用拷贝实际的数据,而只是窃取指向实际数据的指针,并将临时对象的生命周期提高,避免析构。

内存分配策略:

  1. 首次适应算法:每次都从低地址开始查找,找到第一个能满足大小的空闲分区
  2. 最佳适应算法:由于动态分配是一种连续分配方式,为每个进程分配的空间必须是连续的一整片区域,因此,为了保证当前大进程到来时能有连续的大片空间,可以尽可能多的留下大片的空闲区,即优先使用更小的空闲区。,空闲区按照容量递增次序连接,每次分配内存时顺序查找空闲分区链,找到大小能满足要求的第一个空闲分区。缺点:导致每次使用最小的分区进行分配,越来越多的难以利用的校内存块会被保留下来,产生更多的额外碎片。
  3. 最坏适应算法:为了解决最佳适应算法的问题,每次在分配时优先使用最大的连续空闲区。分区按照容量依次递减,每次分配内存时顺序查找满足要求的空闲分区。缺点:每次都选用最大的分区,这会导致较大的连续空间被用完,后续大进程到来,找不到合适的内存。
  4. 邻近适应算法:为解决首次适应算法,每次都需要从头开始查找,增加了查找开销的问题,临近适应算法从查找结束的位置开始检索。空闲分区按递增排成一个循环链表,按照上次结束的位置继续向下查找。这会导致高地址部分被大量使用,并被划分为小分区,导致无大分区可用。

总结:还是首次适应算法最简单,效果最好,

Redis

Redis的五种数据结构
简单动态字符串:就是简单的字符串,获取长度的复杂度为O(1),减少修改长度是所需的内存分配。
链表:Redis链表实现是双端链表,使用list结构,这个结构带有表头结点指针,表尾结点指针,以及链表长度等信息。
字典:底层是哈希表,键值对形式,每个字典带有两个哈希表,一个平时使用,一个在rehash时才使用
哈希表:哈希算法,使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用这个单向链表连接起来。
跳跃表:把列表的某些节点拔高一层,即每两个节点中间有一个节点变成两层,那么第二层的节点只有第一层的一半,查找效率会变高。查找步骤是从头结点的顶层开始的,查找第一个的大雨指定元素的节点时,退回上一个节点,,在下一层继续查找。

Redis的优点:

  • 访问速度快
  • 数据类型丰富,支持string,list,set,sorted set,hash这五种数据结构
  • 支持事务
  • 特性丰富

缓存和Redis的区别

  • 存储方式:缓存存储在内存之中,断电后会挂掉,没有持久化功能,redis部分存储在硬盘上,能保证数据的持久性
  • 数据支持类型,:缓存只支持string这一种类型,Redis有父子的数据类型。
  • 使用底层模型不同
  • 缓存是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路IO复用模型
  • value值不同:redis最大可达512MB;缓存只有1MB。

redis的过期时间:在设置key的时候,会给一个过期时间,指定这个key可以存货的时间,采用定期删除和过期删除。定期删除是是随机时间间隔抽取一些设置了过期时间的key,若过期就删除。为什么要随机呢?若经常便利会造成很大的负荷。惰性删除,在下一次访问时,发现过期了就删除。

Redis为什么这么快?

  1. 纯内存操作
  2. 采用单线程,避免了频繁的上下文切换
  3. 采用了非阻塞IO多路复用机制

缓存雪崩:缓存同一时间大面积的失效,后面的请求会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方法:
事前:进了保证整个集群的高可用性,发现宕机尽快补上,选择合适的内存淘汰策略
事中:通过加锁或者队列来控制都数据库写缓存对的线程数量
事后:利用redis持久化保存的数据尽快恢复缓存

缓存穿透:故意请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
缓存更新:自定义的缓存淘汰:定时删除和惰性删除,定时删除:定时去清理过期的缓存;惰性删除:当有用户请求过来是,在判断这个请求所用到的缓存是否过期,过期就去底层系统得到新数据并更新缓存。

缓存击穿:key是一个超级大热点,不停的扛着大并发,集中对这一点进行访问,当这个key在失效的瞬间,大并发就会穿破缓存,直接请求数据库。

Redis持久化
Redis支持持久化,通过持久化吧内存中的数据同步导硬盘文件来保证数据持久化。当redis重启后通过把硬盘文件重新加载导内存,就能达到恢复数据的目的。

快照持久化:创建快照来获得存储在内存中的数据在某个时间点上的副本。快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本,还可以将快照留在原地一边重启服务器的时候使用。
AOF持久化:开启AOF持久化后每执行一条会更改Redis中数据的命令,Redis就会将该命令写入硬盘中的AOF文件。

AOF重写
Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后服务器用新的AOF文件替换旧的AOF文件,依次来完成AOF文件重写操作。

Redis的并发竞争key问题
redis并发竞争key问题是多个系统同时对一个key进行操作,但最后执行的顺序和我们期望的顺序不同,也导致了结果不同。

如何保证缓存与数据双写时的数据一致性
将读请求和写请求串行化,串到一个内存队列里去
预留缓存模式:

  • 读的时候先读缓存,缓存如果没有的话,就读数据库,然后取出数据库后放入缓存,同时返回响应。
  • 更新的时候,先删除缓存,然后再更新数据库,这也读的时候发现缓存中没有数据而直接去数据库中拿数据,

Redis的主从架构模式
高并发的主要依赖,单主用来写入数据,多从用来查询数据

实现高可用
故障转移,也称为主备切换,在故障时自动检测,并且将某个节点自动切换为主节点,从而实现高可用

Redis烧饼主备切换的数据丢失问题
主备切换时可能会导致数据丢失:

  • 异步复制时,有部分数据还没复制过去,就宕机了,此时这部分数据就丢了
  • 脑裂导致数据丢失,若主机器突然脱离了正常的网络,跟其他不能连接,但实际上还在运行,此时哨兵会认为主机宕机了,此时会切换主机,此时集群中就会有两个主机,也就是所谓的脑裂。若客户端没来得及切换到新的主机,还继续向旧主机写数据,旧的主机恢复时,会被作为其他主机挂到新的主机上,自己的数据会被清空,这就会导致原来写入的数据丢失掉。

解决方法:

  • 数据同步和复制的延迟不能超过10秒
  • 减少异步复制数据的丢失:一旦复制数据的时延太长,就认为主机宕机,那么就拒绝写请求
  • 减少脑裂的数据丢失,如果不能继续给指定数量的从机器发送数据,,而且超过十秒没有收到自己的会应消息,就拒绝客户端的写请求

信号和槽

声明信号使用signals关键字,发送信号使用emit关键字
所有的信号声明都是公有的,没有返回值,返回值都用void,所有的信号都不需要定义,必须直接或间接继承QOBject类,并且开头私有声明包含Q_OBJECT
在一个线程中,信号被emit发出时,会立即执行其槽函数,等槽函数执行完才会执行emit后面的代码,若一个信号连接了多个槽函数,则会等所有的槽函数执行完毕后才执行后面的代码,槽函数的执行顺序是按照连接顺序执行的,不同线程中槽函数的执行顺序是随机的。
信号与槽的多种用法

  • 一个信号可以和多个槽连接
  • 多个信号可以连接到一个槽
  • 一个信号可以连接到另外的一个信号
  • 槽可以被取消连接,使用disconnect函数
  • 使用lambda表达式

connect的第五个参数:

  • Qt::AutoConnection:默认值,使用这个只则连接类型会在信号发送时觉得。如果接收者和发送者在同一线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
  • Qt::DirectConnection:槽函数会在信号发送的时候被直接调用,同步执行。
  • Qt::QueueddConnection:信号发出后,信号会暂时被放到一个消息队列中,需要等到接收对象所属线程的事件循环取得控制权时才取得该信号,然后执行和信号关联等待槽函数。
  • Qt::BlockQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。而且接收者和发送者绝对不能再一个线程,否则会死锁。
  • Qt::UniqueConnection:

获取信号发送者sender()
当多个信号连接一个槽时,需要判断哪个对象发来的,可以调用sender()函数获取对象指针,返回QObject指针。

信号和槽的实际流程

  1. moc查找头文件中的signals,slots,标记出信号和槽
  2. 将信号槽信息存储到类静态变量staticMetaObject中,并且按声明顺序进行存放,建立索引
  3. 当发现有connect连接时,将信号槽的索引信息防到一个map中,彼此配对。
  4. 当调用emit时,调用信号函数,并且传递发送信号的对象指针,元对象指针,信号索引,参数列表到activate函数
  5. 通过activate函数找到在map中所有与信号对应的槽索引。
  6. 根据槽索引找到槽函数,执行槽函数。

static静态变量
静态成员被类的示例所共享,静态变量不能调用非静态的变量和方法
全局静态变量就只在本文件在可以使用,不能用在程序的其他文件里
静态函数也是,仅在本文件中使用
static局部变量只被初始化一次
在考虑到信息之间共享的时候,可以使用静态变量

数据库的左右链接内外链接
内连接inner join:两张表的交集,A、B两张标都有的数据才能查询出来。
外联接outer join:以一个表为基准,和另一个表中联结字段
左外连接left outer join:以左表为基础,根据ON后的条件,将两表连接起来,将左表的所有查询信息列出,右表只列出满足条件的部分。
左连接left join
右外连接right outer join:以右表为基础,根据ON后的条件,将两表连接起来,将右表的所有查询信息列出,左表只列出满足条件的部分。
右连接right join
全连接full join:显示两侧表中所有满足检索条件的行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值