面经积累
1.线程池
1.1线程池的原理
简单来说就是线程本身存在开销,我们利用多线程来进行任务处理,单线程也不能滥用,
无止禁的开新线程会给系统产生大量消耗,而线程本来就是可重用的资源,
不需要每次使用时都进行初始化,因此可以采用有限的线程个数处理无限的任务。
什么情况下使用线程池?
1、单个任务处理时间比较短
2、处理任务数量大
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。
每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。
1.2线程池的组成部分
(1)线程池管理器(ThreadPoolManager):用于创建并管理线程池
(2)工作线程(WorkThread): 线程池中线程
(3)任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行。
(4)任务队列:用于存放没有处理的任务。提供一种缓冲机制。
1.3 boost线程池使用
Boost库实现线程池学习及线程实现的异步调用
关于boost::function与boost::bind函数的使用心得
2.纯虚函数
2.1 虚函数
C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。
子类可以重写父类的虚函数实现子类的特殊化。
如下就是一个父类中的虚函数:
class A
{
public:
virtual void out2(string s)
{
cout<<"A(out2):"<
2.2 纯虚函数
C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。
C++中的纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。
C++中的纯虚函数也是一种“运行时多态”。
如下面的类包含纯虚函数,就是“抽象类”:
class A
{
public:
virtual void out1(string s)=0;
virtual void out2(string s)
{
cout<<"A(out2):"<
1、每一个类都有虚表
2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现,如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现,如果派生类有自己的虚函数,那么虚表中就会添加该项。
3、派生类的虚表中虚地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。这就是c++中的多态性,当c++编译器在编译的时候,发现Father类的Say()函数是虚函数,这个时候c++就会采用晚绑定技术,也就是编译时并不确定具体调用的函数,而是在运行时,依据对象的类型来确认调用的是哪一个函数,这种能力就叫做c++的多态性,我们没有在Say()函数前加virtual关键字时,c++编译器就确定了哪个函数被调用,这叫做早期绑定(其中最常见的就是函数重载)。c++的多态性就是通过晚绑定技术来实现的。c++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。
3.协程
3.1 概念
Coroutine是相对于routine(过程)而提出的,它与一般的父程序调用子程序的routine不同之处在于,它允许挂起一个程序执行点,过后再从挂起的地方继续运行,是一种更高级的程序运行顺序控制。为保证程序挂起再恢复,协程有其自己的栈和控制块,记录挂起时的状态。
协程有如下特点:
1.同其他数据类型一样,协程也是第一类(first-class)对象,可以被当参数传递等操作;
下面是boost中协程相关的两种类型:coroutine<>::pull_type和coroutine<>::push_type,pull的意思是从主运行环境可以“拉”数据到协程环境,push的意思是从协程环境将数据“推”到主运行环境中,coroutine<>::pull_type拉数据的方法是get(),
coroutine<>::push_type推数据的方法是operator(),即在协程中,pull_type.get()可以得到外部传入的数据,而push_type()可以将数据传递到外部环境。
其中soucre和sink相当于两种类的变量
2.运行特点是挂起运行,离开协程,过后再进入,恢复运行;
3.具有对称和非对称的转移控制机制
4.挂起前和恢复后本地变量的值是一致的;
5.有stackless和stackful两种类型
3.2 python的协程
python中yield关键字讲解
def foo():
print("starting...")
while True:
res = yield 4
print("res:",res)
g = foo()
print(next(g))
print("*"*20)
print(next(g))
我直接解释代码运行顺序,相当于代码单步调试:
1.程序开始执行以后,因为foo函数中有yield关键字,所以foo函数并不会真的执行,而是先得到一个生成器g(相当于一个对象)
2.直到调用next方法,foo函数正式开始执行,先执行foo函数中的print方法,然后进入while循环
3.程序遇到yield关键字,然后把yield想想成return,return了一个4之后,程序停止,并没有执行赋值给res操作,此时next(g)语句执行完成,所以输出的前两行(第一个是while上面的print的结果,第二个是return出的结果)是执行print(next(g))的结果,
4.程序执行print(""20),输出20个*
5.又开始执行下面的print(next(g)),这个时候和上面那个差不多,不过不同的是,这个时候是从刚才那个next程序停止的地方开始执行的,也就是要执行res的赋值操作,这时候要注意,这个时候赋值操作的右边是没有值的(因为刚才那个是return出去了,并没有给赋值操作的左边传参数),所以这个时候res赋值是None,所以接着下面的输出就是res:None,
6.程序会继续在while里执行,又一次碰到yield,这个时候同样return 出4,然后程序停止,print函数输出的4就是这次return出的4.
python协程讲解
参考网站:https://www.jianshu.com/p/03b1a41fdf8d
3.3 Boost Coroutine
参考网站:https://blog.csdn.net/guxch/article/details/82803769
int main()
{
typedef boost::coroutines2::coroutine coro_t2;
std::cout<< "start corountine" << std::endl;
coro_t2::pull_type source( // constructor enters coroutine-function
[&](coro_t2::push_type& sink){
std::cout<< " sink1" << std::endl;
sink(1); // push {1} back to main-context
std::cout<< " sink2" << std::endl;
sink(2); // push {2} back to main-context
std::cout<< " sink3" << std::endl;
sink(3); // push {3} back to main-context
});
std::cout<< "start while" << std::endl;
while(source) // test if pull-coroutine is valid
{
int ret=source.get(); // pushed data,that is the argument of sink()
std::cout<< "move to coroutine-function "<< ret << std::endl;
source(); // context-switch to coroutine-function
std::cout<< "back from coroutine-function "<< std::endl;
}
return 0;
}
输出为:
start corountine
sink1
start while
move to coroutine-function 1
sink2
back from coroutine-function
move to coroutine-function 2
sink3
back from coroutine-function
move to coroutine-function 3
back from coroutine-function
解析:
source和sink分别属于pull和push的类型的对象,pull是从主程序中到协程中,所以直接就进入source(pull type)了,执行到sink(push type)的时候,进入到主程序可以获取相应的返回值,执行source(pull type)的时候就进入到协程中。
4.线程同步
请参考多线程编程的文件夹
5.c++11的新特性
5.1 lambda表达式
lambda表达式讲解
[capture list] (params list) mutable exception-> return type { function body };
[capture list] (params list) -> return type {function body}; //1
[capture list] (params list) {function body}; //2
[capture list] {function body}; //3
vector v({1,5,2,7,8});
sort(v.begin(), v.end(), [] (const int &a, const int &b) { return a < b; });
for(int i = 0; i < v.size(); ++i)
cout<
5.2 自动类型推导和 decltype
自动类型推导:
auto x = 0; //0 是 int 类型,所以 x 也是 int 类型
auto c = 'a'; //char
auto d = 0.5; //double
auto national_debt = 14400000000000LL;//long long
vector vi;
auto ci=vi.begin();
auto作为函数返回值时,只能用于定义函数,不能用于声明函数。
#pragma once
class test
{
public:
auto testWork(int a, int b); // 声明函数(错误)
// 在引用头文件的调用testWork函数是,编译无法通过。
}
class test
{
public:
auto testWork(int a, int b) // 定义函数(正确)
{
return a+b;
}
// 但如果把实现写在头文件中,可以编译通过,
// 因为编译器可以根据函数实现的返回值确定auto的真实类型。
}
C++11 也提供了从对象或表达式中“俘获”类型的机制,
新的操作符 decltype 可以从一个表达式中“俘获”其结果的类型并“返回”:
decltype使用:
const vector vi;
typedef decltype (vi.begin()) CIT;
CIT another_const_iterator;
5.3 deleted 函数和 defaulted 函数
=default; 指示编译器生成该函数的默认实现。
这有两个好处:一是让程序员轻松了,少敲键盘,二是有更好的性能。
与 defaulted 函数相对的就是 deleted 函数, 实现 non copy-able 防止对象拷贝,
要想禁止拷贝,用 =deleted 声明一下两个关键的成员函数就可以了:
int func()=delete;
//防止对象拷贝的实现
struct NoCopy
{
NoCopy & operator =(const NoCopy &) = delete;
NoCopy(const NoCopy &) = delete;
};
NoCopy a;
NoCopy b(a); //编译错误,拷贝构造函数是 deleted 函数
5.4 nullptr
nullptr 是一个新的 C++ 关键字,它是空指针常量,它是用来替代高风险的 NULL 宏和 0 字面量的。
nullptr 是强类型的,所有跟指针有关的地方都可以用 nullptr,包括函数指针和成员指针:
void f(int); //#1
void f(char *);//#2
//C++03
f(0); //调用的是哪个 f?
//C++11
f(nullptr) //毫无疑问,调用的是 #2
const char *pc=str.c_str(); //data pointers
if (pc != nullptr)
cout << pc << endl;
int (A::*pmf)()=nullptr; //指向成员函数的指针
void (*pmf)()=nullptr; //指向函数的指针
5.5 右值引用
右值引用讲解
有一个可以区分左值和右值的便捷方法:
看能不能对表达式取地址,如果能,则为左值,否则为右值。
关于右值引用的两个应用:
1.移动构造
2.移动赋值
MyString(const MyString& str) // 拷贝构造函数
MyString(MyString&& str) // 移动构造函数
MyString& operator=(const MyString& str) // 拷贝赋值函数 =号重载
MyString& operator=(MyString&& str) // 移动赋值函数 =号重载
移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是const MyString& str,是常量左值引用,而移动构造的参数是MyString&& str,是右值引用,而MyString("hello")是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。
对于一个左值,肯定是调用拷贝构造函数了,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。我觉得它其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是用移动构造函数吧。。。
vector vecStr2;
vecStr2.reserve(1000); //先分配好1000个空间
for(int i=0;i<1000;i++){
MyString tmp("hello");
vecStr2.push_back(std::move(tmp)); //调用的是移动构造函数
}
5.6 智能指针
http://note.youdao.com/noteshare?id=5e68c3b0014fc8345428404216724d99
6.STL的空间配置器(两级分配)
http://note.youdao.com/noteshare?id=3444273eb8024c68ad9c33cde7e2c45c
7.数据库三范式
7.1第一范式
第一范式:当关系模式R的所有属性都不能在分解为更基本的数据单位时,称R是满足第一范式的,简记为1NF。满足第一范式是关系模式规范化的最低要
求,否则,将有很多基本操作在这样的关系模式中实现不了。
7.2第二范式(确保表中的每列都和主键相关)
第二范式:如果关系模式R满足第一范式,并且R得所有非主属性都完全依赖于R的每一个候选关键属性,称R满足第二范式,简记为2NF。
这样就产生一个问题:这个表中是以订单编号和商品编号作为联合主键。这样在该表中商品名称、单位、商品价格等信息不与该表的主键相关,而仅仅是与商品编号相关。所以在这里违反了第二范式的设计原则。
而如果把这个订单信息表进行拆分,把商品信息分离到另一个表中,把订单项目表也分离到另一个表中,就非常完美了。如下所示。
7.3第三范式
第三范式:设R是一个满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖于R的任意一个候选关键字,称R满足第三范式,简记为3NF.
第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。
比如在设计一个订单数据表的时候,可以将客户编号作为一个外键和订单表建立相应的关系。而不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段,上面二范式中的订单信息表中所属单位和联系方式两个字段都和客户有关系,而不是直接依赖于订单编号,这样我们可以单独建立一张表来存放用户的信息。如下面这两个表所示的设计就是一个满足第三范式的数据库表。
8.malloc和alloc的区别
https://www.cnblogs.com/longyi1234/archive/2010/03/22/malloc.html
calloc() 函数会将所分配的内存空间中的每一位都初始化为零,malloc是无法保证申请的空间是没有被使用的,两者都是在堆空间进行申请
函数原型:
void *calloc(size_t n, size_t size)
头文件
#include
#include
功能:
在内存的动态分配区中分配n个长度为size的连续空间,
函数返回一个指向分配起始地址的指针;
如果分配不成功,返回NULL。
函数malloc向系统申请分配指定size个字节的内存空间.
返回类型是 void*类型.void*表示未确定类型的指针.
void* 类型可以强制转换为任何其它类型的指针.
实现原理:
malloc、calloc函数的实质体现在,它有一个将可用的内存连接为一个长长的链表(即所谓的空闲链表)。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块,然后将该内存块一分为二(一块的大小与用户申请的大小一样,另一块就是剩下的字节),接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到链表上,调用free函数 时,它将用户释放的内存块连接到空链上,到最后,空闲链表会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可能满足用户要求的片段了,于是malloc函数请求延时,并开始在空间中翻箱倒柜的检查内存片段,对它们进行整理,并将相邻的小空闲块合成较大的内存块realloc是从堆空间上分配内存,当扩大一块内存空间时,realloc试图直接从现存的数据后面的哪些字节中获得附加的字节,如果能够满足,自然天下太平,那么如果后面的字节不够,问题就来了,那么就使用堆上第一个足够满足要求的自由空间块,现存的数据然后就被拷贝到新的位置上,而老块则放回堆空间,这句话传递的一个很重要的信息就是数据可能被移
9. auto_ptr和shared_ptr的区别
9.1 关于auto_ptr的几种注意事项:
1、auto_ptr不能共享所有权。
2、auto_ptr不能指向数组
3、auto_ptr不能作为容器的成员。
4、不能通过赋值操作来初始化auto_ptr
std::auto_ptr p(new int(42)); // OK
std::auto_ptr p = new int(42); // ERROR,本想通过new int(42)来产生临时对象temp(问题出在这里),再由temp拷贝构造产生p。
// 这是因为auto_ptr 的构造函数被定义为了explicit
std::auto_ptr p = auto_ptr(new int(42)); // Success
5、不要把auto_ptr放入容器
9.2 关于shared_ptr的几种注意事项:
1、shared_ptr是Boost库所提供的一个智能指针的实现,shared_ptr就是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针.
2、shared_ptr比auto_ptr更安全
3、shared_ptr是可以拷贝和赋值的,拷贝行为也是等价的,并且可以被比较,这意味这它可被放入标准库的一般容器(vector,list)和关联容器中(map)。
10.堆排序怎么维护堆,时间复杂度
https://www.jianshu.com/p/1a3484b527f8
11.排序算法的总结
12.进程和线程区别
类似”进程是资源分配的最小单位,线程是CPU调度的最小单位“这样的回答感觉太抽象,都不太容易让人理解。
做个简单的比喻:进程=火车,线程=车厢线程在进程下行进(单纯的车厢无法运行)一个进程可以包含多个线程(一辆火车可以有多个车厢)不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
13.tcp和udp区别
tcp面向连接,udp是无连接;
tcp保证数据有效传输,udp是“尽最大努力进行交付”;
tcp有流量控制、拥塞控制,udp没有;
tcp是面向字节传输,udp面向报文传输;
14.http和https协议的不同和大致原理
15.tcp三次握手和四次挥手的过程,以及设计的原因
16.二叉树的非递归写法
17.多线程同步的方法和如何使用
18.进程之间通信的方式,与socket通信的相同点和不同点
19.linux进程之间通信和windows有何不同点
20.vector何时进行空间搬运
21.为什么vector的新空间为2倍
22.placement new和operator new的不同
z