C++面经(持续更新)

一. c,c++区别<九大点>

c: 面向过程
c++: 面向对象(封装,继承,多态)
对象:对数据和作用于数据的操作组成的封装实体
类:描叙了一组有相同属性和方法的对象<虚拟>
对象为类的实例化,类为对象的抽象

c: 不支持函数重载
c++: 支持函数重载<_Z + len(函数名) + 函数名 + 参数类型首字母> int add(int,int) -> _Z3addii
原因与对函数名修饰规则有关

c: 函数参数为空表示可传任意参数,不传参数需加void
c++: 函数参数为空表示无法传递参数

c: struct中不能有函数
c++: struct中可以有函数

c:不支持函数参数给定默认值
c++:支持函数参数给定默认值

c: 不支持引用
c++: 支持引用

c: 不支持内联函数
c++: 支持内联函数

c: 采用malloc,free管理堆空间
c++: 采用new, delete管理 <new,delete为malloc,free的封装>

c: 仅有全局,局部两个作用域
c++: 有全局,局部,类,namespace作用域

二. 四种类型转换

1.static_cast: 静态转换,用于基类派生类引用和指针的转换<向上安全,向下不安全>;基本数据类型转换<无动态检查>

2.dynamic_cast:动态转换,拥有动态检查,向下转换更安全

3.const_cast:常量转换,常量转非常量<去常量>

4.reinterpret_cast:重新解释,随意转换,最不安全

三. c++引用概念

1.引用可看做的数据的别名<类似快捷方式>

2.引用必须引用合法空间,定于必须初始化,绑定后不许更换<本质为指针常量>

四. 指针常量,常量指针

1.指针常量:int* const p,指针本身值不可变,始终指向同一地址,定于必须初始化 <顶层指针, const在*号右边>

2.常量指针:const int* p,指向“常量”,可指向其他地址,但不能修改内容 <底层指针, const在*号左边>

... * ...const... 指针常量
...const... * ... 常量指针<从左往右读>

五. 内联函数作用<特点>

1.inline关键字,避免函数调用带来的开销,如果内联函数太复杂,编译器忽略

2.原理:预处理时,调用inline会将语句复制在调用处<还能解决宏定义问题>

六. new 的实现原理,new 和 malloc 的区别

1.new原理:简单类型直接调用operator new(),new()调用malloc函数,如果失败则调用_callnewh(),如果返回0则抛异常bac_alloc;复杂类型(类),先调用new(),然后在分配的内存上调用构造函数

2.两者区别:new为操作符,malloc为函数;new无需确定内存大小,malloc则需要指出大小;分配失败一个抛出异常一个返回NULL;new会调用构造函数,malloc不会;new可以重载,malloc不行;new从自由存储区分配内存,malloc从上动态分配<两者有包含关系,两者区别>;malloc可以用realloc重新分配内存实现扩充,new没有<malloc更加灵活>。

七. 重载,复写,隐藏的区别

1.重载:同一作用域,同名函数的形参不同,构成函数重载,与返回值无关

2.重写:派生类与基类同名、同返回值、同参的虚函数重定义,构成虚函数覆盖<重写>

3.隐藏:不同作用域,同名函数构成隐藏,比如派生类成员函数隐藏与其同名的基类成员函数
隐藏的实质是:在函数查找时,名字查找先于类型检查。如果派生类中成员和基类中的成员同名,就隐藏掉。编译器首先在相应作用域中查找函数,如果找到名字一样的则停止查找。

八. C++ 中智能指针和指针的区别是什么?

1.智能指针是普通指针加上一层封装,本质是一个类模板,可以自动释放所指内存;而普通指针是一种基本类型,需要自己手动释放内存

九. 简述一下 C++ 中的多态

1.多态是同一个事物在不同场景下的多种形态。多态分为静态多态和动态多态。

2.静态多态:编译器在编译期间完成的,根据实参类型来选择调用函数<函数重载,运算符重载,泛型编程等>

3.动态多态:程序在运行时根据基类的引用(指针)指向的实际对象来确定具体调用哪一个虚函数<条件:在继承关系中,有对虚函数的重写,且使用基类引用(指针)指向不同对象>

十.为什么将析构函数设置成虚函数

1.主要是防止内存泄漏,在基类指针指向派生类对象时,如果基类析构函数没有声明为虚函数,则基类指针释放时不会调用派生类析构函数,如果派生类中有对内存的释放,就会造成内存泄漏
<构造函数不能为虚函数,如果构造函数是虚函数,就需要vptr指定,而此时vptr本身还未初始化,构成矛盾>

十一.简述一下堆和栈的区别

1.管理方式:栈是由编译器自动管理,而堆得分配和释放都是程序员控制的

2.空间大小:栈空间小于堆,堆内存几乎没有什么限制,而栈一般有一定大小

3.碎片问题:栈是先进后出的数据结构,内存不存在碎片化,而堆由程序员控制,频繁操作会导致内存空间不连续,产生大量内存碎片

4.生长方向:栈生长方向向下,沿内存地址减小方向增长,堆则是向上,向内存地址增大方向增长

5.分配方式:堆空间都是动态分配,没有静态分配的堆。而栈有静态分配和动态分配,但都是由编译器自动管理的,动态分配可用alloca函数

6.分配效率:栈是机器系统提供的数据结构,有底层支持,因此栈的效率很高。堆是由c/c++函数提供的,机制复杂,效率低。

十二.请你介绍一下死锁,产生的必要条件,产生的原因,怎么预防死锁

1.死锁:两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种相互等待的现象,若无外力作用,它们将无法推进下去,此时系统处于死锁状态。

2.必要条件:互斥,进程对资源占有具有排它性,其他进程请求该资源只能等待,直至释放;请求和保持,进程已经保持至少一个资源而又提出新的请求,而该资源已被其他进程占有,此时请求阻塞,但自身资源无法释放;不抢占:进程占有的资源在未结束前不能被剥夺,只能由自身释放;循环等待:多个进程产生对占有资源请求环链

3.预防:打破四个必要条件的其中一条即可,有著名的银行家算法

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

1.C++11:c++11特性

2.c++14:二进制字面量;Lambda表达式引入泛型;std::make_unique;std::shared_timed_mutex;std::exchange;std::quoted<给字符串加引号> c++14

3.c++17:构造函数模板推导 - 结构化绑定 - 内联变量 - 折叠表达式 - 字符串转换 - std::shared_mutex

4.c++20:允许 Lambda 捕获 [=, this] - 三路比较运算符 - char8_t - 立即函数(consteval) - 协程 - constinit

十四.四个智能指针

1.auto_ptr:最初的c++98的智能指针,拥有最基本的管理内存功能,c++11后被unique_ptr替代<不支持复制,不支持对象数组内存管理>

2.unique_ptr:禁用直接赋值和拷贝,支持对象数组的内存管理。其他和auto_ptr用法一致<release()是取消指针管理,未释放原内存,reset()是释放原指针内存后再重新管理其他内存>

3.shared_ptr:由于reset存在排他性,会释放原内存,导致其他产生空指针,其他智能指针无法使用,因此shared_ptr指针解决多指针共享问题

记录引用特定内存对象的智能指针数量<use_count>,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它。
<使用make_shared 初始化对象,分配内存效率更高(推荐使用)>,其中交叉管理防止“死锁”(类似进程死锁)

4.weak_ptr:当出现循环引用问题时,可以用虚指针代替其中一个shared_ptr,因为其不会增加引用数。由于是辅助shared_ptr出现的类模板,虚指针本身不能直接赋值,只能将shared_ptr的对象赋值给虚指针,甚至连get函数,operator* 和 operator->都没有。不过为了检查虚指针,有expired函数检查是否绑定了智能指针,lock函数用于返回绑定的智能指针。

十五.请你说说左值、右值、左值引用、右值引用、右值引用的使用场景

1.左值:在 C++ 中可以取地址的、有名字的就是左值 int a = 10; // 其中 a 就是左值(lvalue)

2.右值:不能取地址的、没有名字的就是右值 int a = 10; // 其中 10 就是右值(rvalue)

3.左值引用:是对一个左值进行引用的类型,必须立即进行初始化操作.左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。

4.右值引用:用 "&&" 表示。必须立即进行初始化操作,且只能使用右值进行初始化 int&& a = 10;<右值引用可以延长局部变量的生命周期,同时也把右值变成左值,其中10为右值,a绑定后成了左值>

5.右值引用的使用场景:右值引用可以实现移动语义< std::move >和完美转发<std::forword,和模板T &&a 搭配使用可以实现对左右值的完美转移完美转发>。

十六.std::move()

1.std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝

2.它唯一的功能是将一个左值引用<左值>强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。目的是为了节省拷贝带来的时间消耗

十七.override和final

1.override关键字:当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写

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

如果加有override关键字修饰的函数,表示子类必须重载父类的虚函数,可以防止因为名称错误导致重写无效。
如写成: virtual void f00(); // 此函数为新增虚函数,不会报错

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

class Base
{
    virtual void foo();
};
 
class A : public Base
{
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
    void foo() override; // Error: 在A中已经被final了
};
 
class C : B // Error: B is final
{
};

final表示虚函数重写结束,后续子类无法重写。

十八.浅拷贝和深拷贝

1.浅拷贝:
浅拷贝指仅仅进行值的拷贝,对于指针类型变量来说,相当于两个指针指向同一块内存,并未开辟新的空间,若其中一个指针将内存释放,另外一个指针将悬空。

2.深拷贝:
深拷贝不仅拷贝值,还开辟了一块新的空间,这样确保两个指针独立,在拷贝变量中有指针,需要自己实现深拷贝。

十九.public继承,protected继承,private继承

1.public继承:基类的所有public成员和protected成员作为派生类的成员时,都保持原有的状态,而基类的private成员任然是私有的,不能被这个派生类的子类所访问

2.protected继承:基类的所有public成员和protected成员都成为派生类的protected成员,private成员子类无法访问。

3.private继承:基类的所有public成员和protected成员都成为派生类的private成员,派生类的子类无法访问。

二十.大小端存储

1.大端存储:字数据的高字节存储在低地址中 <"高低",正序>

2.小端存储:字数据的低字节存储在低地址中 <"低低",逆序>

3.如何区别?
将int类型转成char,观察是否为正序

int a = 0x1234;
//由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
char c = (char)(a);
if (c == 0x12) //正序
    cout << "big endian" << endl;
else if(c == 0x34) //逆序
    cout << "little endian" << endl;

二十一.volatile、mutable和explicit关键字

1.volatile<多线程开发常用>:volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。
有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。
如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。

2.mutable:mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。
被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。

// first   const function
class person
{
    int m_A;
    mutable int m_B;//特殊变量 在常函数里值也可以被修改
public:
    void add() const//在函数里不可修改this指针指向的值 常量指针
    {
        m_A = 10;//错误  不可修改值,this已经被修饰为常量指针
        m_B = 20;//正确
    }
};

// second   const value
class person
{
public:
    int m_A;
    mutable int m_B;//特殊变量 在常函数里值也可以被修改
};

int main()
{
    const person p = person();//修饰常对象 不可修改类成员的值
    p.m_A = 10;//错误,被修饰了指针常量
    p.m_B = 200;//正确,特殊变量,修饰了mutable
}

3.explicit:用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显式的方式进行类型转换 <防止隐式转化带来错误>

二十二.push_back和emplace_back

1.push_back:
该函数是正常的左值,或者左值引用为<形>参数列表,对于传入的右值,需要调用移动构造函数。

2.emplace_back:

template<typename… Args> void emplace_back(Args&&… args);

代码上看,它的参数列表是C++11可变模板参数,类型是右值引用,对于传值来说,无需移动构造。

3.差异
在传值(传参)中emplace_back效率高于push_back,原因是前者是右值引用,不需要多余的移动构造,而后者需要。
在传对象时,区别不大。
两者区别,代码测试

二十三.函数指针

1.指向函数的地址,结构如下:
int (*pf)(const int&, const int&); //int 返回值, ()内为参数类型,pf为指针名称。

2.函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。
一个具体函数的名字,如果后面不跟调用符号(即括号),则该名字就是该函数的指针(注意:大部分情况下,可以这么认为,但这种说法并不很严格)。

二十四.C++无序容器和map set

1.map
map支持键值的自动排序,底层机制是红黑树,红黑树的查询和维护时间复杂度均为\(O(logn)\),但是空间占用比较大,因为每个节点要保持父节点、孩子节点及颜色的信息

2.set
set与map类似,Set的底层实现通常也是红黑树。Set是一种特殊的Map,只有键没有值。

3.unordered_map
unordered_map是C++ 11新添加的容器,底层机制是哈希表,通过hash函数计算元素位置,其查询时间复杂度为O(1),维护时间与bucket桶所维护的list长度有关,但是建立hash表耗时较大.

4.unordered_set
unordered_set: 与unordered_map类似,unordered_set的底层实现通常也是哈希表。unordered_set是一种特殊的unordered_map,只有键没有值。

5.从底层机制和特点可以看出:map适用于有序数据的应用场景,unordered_map适用于高效查询的应用场景.

二十五.sort()函数

1.sort()源码中采用的是一种叫做IntroSort内省式排序的混合式排序算法

2.首先进行判断排序的元素个数是否大于stl_threshold,stl_threshold是一个常量值是16,小于使用插入排序

3.如果说我们的元素规模大于16,就判断我们的递归深度有没有到达递归深度的限制阈值2*lg(n),未达到就用快排

4.如果超过就使用堆排序

二十六.C++从代码到可执行程序经历了什么?

1.预编译
主要处理源代码文件中的以“#”开头的预编译指令
删除所有的#define,展开所有的宏定义。
处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他 文件。
删除所有的注释,“//”和“/**/”。
保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重 复引用。
添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是 能够显示行号。

对代码进行展开,做编译前的处理。

2.编译
把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应 的汇编代码文件。

3.汇编
将汇编代码转变成机器可以执行的指令(机器码文件)。
汇编转机器码,对照翻译,生成.obj(.o)文件

4.链接
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。 把多个.obj文件链接,生成可执行文件.exe

二十七.友元函数与友元类

1.友元提供了外部访问类内部的私有和保护成员的权力。
友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

2.友元函数
在类的定义中声明所有可以访问它的友元函数(友元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员)

lass A
{
public:
    friend void set_show(int x, A &a);      //该函数是友元函数的声明
private:
    int data;
};

void set_show(int x, A &a)  //友元函数定义,为了访问类A中的成员
{
    a.data = x;
    cout << a.data << endl;
}

3.友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。

class A
{
public:
   friend class C;                         //这是友元类的声明
private:
   int data;
};

class C             //友元类定义,为了访问类A中的成员
{
public:
   void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}
};

4.特点
友元关系不能被继承。
友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

二十八.进程之间的通信方式

详细全文:六大进程通信

1.管道通信(半双工通信)
数据只能单向流动,而且<匿名管道>只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。<有名管道>允许无亲缘关系进程间的通信(mkfifo)。

//shell for Linux
$ command1 | command2

//code for Linux
int pipe (int fd[2]);  //fd[1] 的输出是 fd[0] 的输入

2.套接字(socket)
跨网络与不同主机上的不同进程进行通信,是网络通信的基石。

3.消息队列
消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。

A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程在需要的时候自行去消息队列中读取数据就可以了。同样的,B 进程要给 A 进程发送消息也是如此。如下图:

4.共享内存
两个不同进程的逻辑地址通过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存。

这是最快的一种进程通信方式(建立共享内存区域后无需借助内核)。

5.信号量与PV操作(semophore)
消息队列无需避免冲突,而共享内存机制可能会发生冲突。

为了保证共享内存在任何时刻只有一个进程在访问(互斥),并且使得进程们能够按照某个特定顺序访问共享内存(同步),我们就可以使用进程的同步与互斥机制,常见的比如信号量与 PV 操作。

进程的同步与互斥其实是一种对进程通信的保护机制,并不是用来传输进程之间真正通信的内容的,但是由于它们会传输信号量,所以也被纳入进程通信的范畴,称为低级通信。

6.信号 (signal)
信号是进程通信机制中唯一的异步通信机制,它可以在任何时候发送信号给某个进程。通过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。

二十九.常问排序

PS:排序的稳定指:即在原序列中,A1=A2,且A1在A2之前,而在排序后的序列中,A1仍在A2之前,则称这种排序算法是稳定的;否则称为不稳定的。

1.冒泡排序<稳定>
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大
的数。

//O(n ^ 2)
for (i = 0; i < len - 1; i++)
    for (j = 0; j < len - 1 - i; j++)
           if (arr[j] > arr[j + 1]) //相邻
                 swap(arr[j], arr[j + 1]);

2.快排<不稳定>
还是啊哈算法讲的最清楚:快排理解

void quicklySort(int a[], int l, int r)
{
	//递归结束条件
	if(l > r) return;
	
	//基准态t, i,j两“哨兵”
	int t = a[l], i = l, j = r;   
	
	//不碰头
	while(i != j)  
	{
		//永远是右边比左边先走
		while(i < j && a[j] >= t) j --;
		while(i < j && a[i] <= t) i ++;
		
		if(i < j) swap(a[i], a[j]);
	 } 
	
	//碰头后  
	//基准态归位  swap(a[i], a[l]);
	a[l] = a[i];
	a[i] = t;
	
	//左右递归  i已经归位 
	quicklySort(a, l, i-1);  //[l, i-1]
	quicklySort(a, i+1, r);  //[i+1, r]
	 
}

快排本质:基准态归位,运用分治思想把大于基准态的数移到右边,小于的数移到左边,随后分段处理左右两边直至所有基准态全部归位,即可完成排序。

3.归并排序<稳定>
归并排序思路也是分治思想,先分成若干小部分,在单独递归处理后,合并在一起。为了满足数组有序,我们可以一直二分到子数组只有两个数,在一个个调整顺序,最后左右两边实行最后一次合并到另外一个数组中,完成所有排序。如下图:

int t[10000];

void mergeSort(int a[], int l, int r)
{
	if(l >= r) return; //取等号表示最小分成两份 
	
	int mid = (l + r) / 2;
	
	//分 
	mergeSort(a, l, mid); //[l, mid]
	mergeSort(a, mid+1, r); //[mid+1, r]
	
	//调整大小
	int k = 0, i = l, j = mid + 1;
	//[i, mid] [mid+1, j]
	while(i <= mid && j <= r)
		if(a[i] <= a[j]) t[k++] = a[i++];
		else t[k++] = a[j++];
		
	while(i <= mid) t[k++] = a[i++];
	while(j <= r) t[k++] = a[j++];
	
	//合并
	for(i=l,j=0; i<=r; i++,j++) a[i] = t[j]; 
	
}

4.堆排序<不稳定>
采用二叉排序树,也就是大小根堆来排序,将数组看成完全二叉树后,进行结点移动维护排序树,也就是维护大小根堆。

堆排序其实也是一种选择排序,是一种树形选择排序。

// heapsize 
int n; 

// 将子节点大的交给父节点 
// 同时也将小的交给子节点 
void maxHeap(int a[], int i)  
{
	if(i >= n) return;
	
	int l = 2*i, r = 2*i+1;
	int ans = 0;
	if(l < n && a[l] > a[i]) ans = l;
	else ans = i;
	
	if(r < n && a[r] > a[ans]) ans = r;
	
	if(ans != i)
	{
		swap(a[ans], a[i]);
		maxHeap(a, ans); // 交换后,继续维护子树的根堆 
	}
}

//建堆
void setHeap(int a[])
{
	for(int i=n/2-1; i>=0; i--)
	{
		maxHeap(a, i);
	}
 } 
 
void heapSort(int a[])
{
	setHeap(a);
	for(int i=n-1; i>=1; i--)
	{
		swap(a[0], a[i]);
		n --;
		maxHeap(a, 0);
	}
 } 


5.基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

三十.二叉树四种遍历

1.前序(先序):根左右

2.中序:左根右

3.后序:左右根

4.层序:按树的高度,从根开始往下遍历,用BFS(队列)

三十一.Lambda函数

1.为C++11新增的 匿名函数,用以替换独立函数或者函数对象,简化某些代码书写
具体语法

[捕获列表] (函数参数列表) specifiers exception -> type { function body ;}

// []()->{}

一个匿名函数至少要有[]{},其他均可省略。

2.Lambda表达式的捕获列表可以捕获当前函数作用域的零个或多个变量,变量之间用逗号分隔;
这些变量可以在Lambda表达式中被访问和修改。捕获方式有三种,分别是值捕获引用捕获混合捕获

3.限定符值为mutable,异常说明符值为noexcept

4.每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。

三十二.无序容器

1.unordered_map
unordered_map 容器底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。(键值不重复的哈希表)

2.unordered_multimap
和 unordered_map 唯一的区别在于,该容器允许存储多个键相同的键值对。(普通的哈希表)

3.unordered_set
该容器存储的元素不能重复,且容器内部存储的元素也是无序的。(无序的集合)

4.unordered_multiset
和 unordered_set 唯一的区别在于,该容器允许存储值相同的元素。(这玩意有什么用?

PS:底层哈希表的详解

三十三.进程调度算法

1.先来先服务算法(FIFO)
按照进程到达的先后顺序进行调度,先到的进程就先被调度,然后进程会一直运行。直到线程退出或被阻塞,才会选择下一个进程接着运行。当一个长作业先运行了,那么后面的短作业等待的时间就会很长。

2.最短作业优先调度算法(MIN)
每次调度时选择当前已到达的且运行时间最短的进程。如果一直有短作业到来,那么长作业永远得不到调度(饥饿)。

3.高响应比优先调度算法
每次进行进程调度时,先计算响应比优先级,为响应比最高的进程分配 CPU。

4.最短剩余时间优先算法
按剩余运行时间的顺序进行调度,当一个新的进程到达时,把它所需要的整个运行时间与当前进程的剩余运行时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程,否则新的进程等待。

5.时间片轮转调度算法(TIMETURN)
设置一个时间片,允许进程在该时间片内运行。如果时间片用完,进程还在运行,那么将会把此进程从CP 释放出来,并把 CPU分配给另外一个进程。

6.最高优先级调度算法
为每个进程分配一个优先级,从就绪队列中选择最高优先级的进程进行运行。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

7.多级反馈队列调度算法
设置多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列。

三十四.死锁与银行家算法

1.死锁
在多道程序系统中,进程并发执行,为争夺同一个有限资源,会出现每一个进程都处于锁定状态的情况,且都在等待一个事件的发生(通常是请求另外一个进程释放一些资源)来触发。但是这个事件却包含在另外一个进程中,而这个进程也处于阻塞状态,导致的结果就是整个系统中的进程无法继续推进。

2.死锁产生四大必要条件
「互斥条件」:进程要求所分配的资源,在一段时间内必须仅由一个进程所占有。如果其他的进程请求该资源,则请求进程必须等待该资源的释放。
「不剥夺条件」:进程获得该资源后,只有该进程主动释放资源后,其他的进程才可以竞争该资源,不可以强行被其他进程夺走,该资源属于不可剥夺资源。
「请求并保持条件」:进程已经保持了至少一个资源,但是又需要请求新的资源,该资源又被其他的进程占有,此时请求进程就处于阻塞状态,而且自己已经持有的资源也得不到释放。
「循环等待条件」:存在一个进程资源的循环等待链,链中的每个进程在持有资源的条件下又请求另外一个进程所持有的资源。

3.银行家算法
银行家算法是由迪杰斯特拉发明出来解决银行贷款问题,后续被用来解决死锁问题。
本质上是让OS通过检查目前系统是否处于安全状态,来寻找一个安全序列,确保每一个进程在有限的资源下都能满足需求。
如果说能找到一个安全序列,就说明系统是安全的,不会造成死锁。

详细链接

三十五.信号量

1.信号量(Semaphore)
信号量是一种比互斥锁更强大的同步工具,它可以提供更高级的方法来同步并发进程(由Dijkstra1965提出)

For English: A semaphore S is an integer variable that, apart from initialization(除了初始化以外), is accessed only through two standard atomic operations: P and V.

2.PV
P: wait() operation

// S is semaphore
P(s){
  while(s <= 0)
    doing nothing;  //busy waiting
  s --;
}

V:signal() operation

V(s){
  s++;
}

3.应用与种类
binary semaphore 二进制信号量: 信号量的值只能为0或1,通常初始化为1,用于实现互斥锁功能

semaphore mutex = 1;
process pi{
  P(mutex); // lock
  critical section //临界区(共享数据区)
  V(mutex); // unlock
}

counting semaphore 一般信号量:信号量取值可以为任意数值,用于控制并发进程对共享资源的访问。

//信号量的值类似于允许通过的最大数量,也可以理解成共享资源的个数,控制对其访问的最大数量
//PV必须成对出现,维护信号量不变
semaphore road = 2;
process Cari{
  P(road);
   pass the fork
   in the road
  v(road);
}

如下图

4.经典问题
生产-消费者问题

苹果橘子问题

三十六.局部性原理

1.时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再次被访问。
(因为程序中存在大量的循环)

2.空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。
(因为很多数据在内存中都是连续存放的,并且程序的指令也是顺序地在内存中存放的)

三十七.各种树

1.平衡排序树

2.红黑树

3.前缀树(字典树)

4.B(-)树

5.B+树

三十八.topK问题

topk时间复杂度以及方法

//力扣 第K大
class Solution {
public:
    int partition(vector<int>& a, int l, int r)
    {
        int pos = l;
        while(l < r)
        {
            while(l < r && a[r] >= a[pos]) r --;
            while(l < r && a[l] <= a[pos]) l ++;
            if(l < r) swap(a[l], a[r]);
        }
        swap(a[pos], a[l]);
        return l;
    }

    int topK(vector<int> & nums, int l, int r, int k)
    {
        int pos = partition(nums, l , r);
        if(pos == k) return nums[pos];
        if(pos > k) return topK(nums, l, pos-1, k);
        else return topK(nums, pos+1, r, k);
    }

    int findKthLargest(vector<int>& nums, int k) {
        return topK(nums, 0, nums.size() - 1, nums.size() - k); //nums.size() - k 使第K大变成具体数组下标
    }
};

理论上最优解BFprt

bfPrt代码

三十九.memcpy函数实现(深信服手撕)

memcpy相对于strcpy来说,它是一个个字节拷贝,size表示需要拷贝的字节数,用char控制单字节,由于dest后续会改变,需要预先保存原来地址ret,再返回出去。

void* _memcpy(void* dest, void* src, int size) //size为需要拷贝的字节数 
{
	void* ret = dest;
	if(dest != NULL && src != NULL) //assert(dest && src)
	{
		while(size --)
		{
			*(char*)dest = *(char*)src;
			dest = (char*)dest + 1;
			src = (char*)src + 1;
		}
	}
	return ret;
}

补:strcpy

char* strcpy(char* dest,const char* src)
{
	char* ret = dest;
	if(dest != NULL && src != NULL) //assert(dest && src)
	{
		while(*src != '\0')
		{
			*dest++ = *src++;
		}
	}
	return ret;
 } 

四十.私有IP

1.在现在的网络中,IP地址分为公网IP地址和私有IP地址。
公网IP是在Internet使用的IP地址,而私有IP地址则是在局域网中使用的IP地址。
私有IP地址是一段保留的IP地址。只使用在局域网中,无法在Internet上使用。
当私有网络内的主机要与位于公网上的主机进行通讯时必须经过地址转换,将其私有地址转换为合法公网地址才能对外访问。
NAT-Network Address Translation 网络地址转换

2.范围
A类 10.0.0.0 --10.255.255.255
B类 172.16.0.0–172.31.255.255
C类 192.168.0.0–192.168.255.255

posted @ 2023-03-02 19:56  gonghw403  阅读(345)  评论(0编辑  收藏  举报