C++工程师十万字笔记

在这里插入图片描述

目录

前言

这篇文章是自己在准备面试过程中看的八股文,然后,不断的扩充,也同时补充好自己的基础知识。希望这篇文章能帮助到大家。目前这篇文章是接近12万字,可能会包含一些链接啥的,比较长,估计十万左右,差不多,但会依然持续的填充。这篇文章还是比较全的,关于各个科目的都有,后面会有一篇专门是C++方面的知识的。
坚持下去很难,但不坚持永远都无法获得成功~
在这里插入图片描述

正文

经验

  1. 简化内存管理单元
  2. 编译器总是要为函数的每个参数制作临时副本。
  3. 我们可以用函数返回值来传递动态内存。
  4. 指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
  5. 指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
  6. void f() { int* p=new int[5]; }->在栈内存中存放了一个指向一块堆内存的指针p–>delete []p
  7. new[]操作符中的个数参数是数组的大小加上额外的存储对象数目的一些字节
  8. 上下文是一个非常宽泛的概念,大概就是与当前工作相关的周围环境。进程的物理实体与支持进程执行的物理环境合称为进程上下文。上文: 把已执行的 进程指令和数据在 相关寄存器与堆栈中的内容称为上文。正文: 把正在执行的 进程指令和数据 在 相关寄存器与堆栈中 的内容称为正文。下文: 把待执行的 进程指令和数据 在 相关寄存器与堆栈中 的内容称为下文。
  9. assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行。
  10. 函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
  11. 当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
  12. 编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p。所以,你若是用该副本去申请动态内存,那么原来的正本是没有改变的。
  13. 可以用函数返回值来传递动态内存。
  14. 指针要先进行delete然后进行置NULL.
  15. malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
  16. new/delete必须配对使用,malloc/free也一样.
  17. new内置了sizeof、类型转换和类型安全检查功能.
  18. 在用delete释放对象数组时,留意不要丢了符号‘[]’。
  19. 注意无论发生什么,临界区都会借助于语言的机制保证释放。
  20. 一个Strong Pointer会在许多地方和我们这个SmartPointer相似–它在超出它的作用域后会清除他所指向的对象。资源传递会以强指针赋值的形式进行。也可以有Weak Pointer存在,它们用来访问对象而不需要所有对象–比如可赋值的引用。
  21. 常说的内存泄漏是指堆内存的泄漏.
  22. 外挂式的内存泄漏检测工具:BoundsChecker
  23. 使用Performance Monitor检测内存泄漏。
  24. 指针指向非法的内存地址,那么这个指针就是悬挂指针,也叫野指针。 意为无法正常使用的指针。
  25. 以使用static对象替代nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
  26. 栈对象会在什么会自动释放了?第一,在其生命期结束的时候;第二,在其所在的函数发生异常的时候。
  27. 如果一个类不打算作为基类,通常采用的方案就是将其析构函数声明为private。
  28. 为了限制栈对象,却不限制继承,我们可以将析构函数声明为protected
  29. 内存声明后,一定要尽快对指针进行初始化。
  30. 函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
  31. 使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
  32. 不能对数组名进行直接复制与比较。
  33. 注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。如下示例中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。

面试题

1. 请说出二分查找的实现思路及时空复杂度

答:首先使用二分查找算法的前提是数组是有序数组,接下来,讲一下具体的算法思路:

  1. 对于该有序数组a,我们可以定义两个变量,一个叫Left,其值为0,一个叫Right,为数组的长度-1。接下来,我们可以确定该有序数组的中间者为[left+right]。
  2. 接下来,判断a[left+right]是否等于所要查找的值,若大于所要查找的值,则将left指针指向mid+1。若小于所要查找的值,则将right指针指向mid-1。所等于则返回当前Mid值。
  3. 接下来,不断的循环执行该过程。若当right指针指向的位置为left+1的话,则该查找过程结束。表示未在该数组找到该值。

二分查找的时间复杂度为O(logn) -->N*(1/2)^x = 1—>x = O(LOG(N))

2. 构造函数可以是虚函数吗?

答: 不能,因为我们在构造一个对象的时候,必须要知道这个对象的类型,而虚函数的特性就是在运行期间确定实际的类型的。并且,虚函数的执行是依赖于虚函数表的,而在构造对象期间,虚函数表还未初始化。注意:这个虚函数表是存放在全局/静态数据区的(全局静态变量,未初始化的放在BSS段,初始化的放在Data段。
参考1

2.1. C++类有继承时,析构函数必须为虚函数

若不是虚函数,则派生类调用析构函数释放内存时,有可能调用的是基类的析构函数,导致派生类所申请的内存未正确释放。

2.2 虚函数与纯虚函数的差别
  1. 纯虚函数只有定义,没有实现;而虚函数不仅有定义,还有实现。
  2. 包含纯虚函数的类不能定义其对象,而包含虚函数的可以。
2.3 纯虚函数实现原理

虚函数的原理采用虚函数表。
类中含有纯虚函数时,其虚函数表不完全,有个空位。
即“纯虚函数在类的虚函数表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。”
所以纯虚函数不能实例化。

3. C++ 四种强制类型转换

隐式转换:说白了就是在转换时不给系统提示具体的显示模型,让其自动转换,但是要记住一条编译器一般只支持自下而上的类型转换,例如int 转 float。
显示转换:就是我们在c语言课程中学的,强制转换,是我们可以直接对其赋值的。

3.1 const_cast
  1. 常量指针被转换为非常量的指针,并且仍然指向原来的对象。
  2. 常量应用被转化为非常量的应用,并且仍然指向原来的对象。
3.2 static_cast
  1. 用于类层次结构中父类和子类指针或引用的转换。
  2. 用于基本数据类型之间的转换。
  3. c++ 任何隐式转换都是使用static_cast.
3.3 dynamic_cast

关于这个,我目前只了解说在子类转成父类的强制类型转换时可以使用。但其实父类转子类也是可以的。并且dynamic_cast要求操作数必须是多态类型。比如在将整形转为浮点型时。
(1)其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。
(2)不能用于内置的基本数据类型的强制转换。
(3)dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。
(4)使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。
B中需要检测有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。
这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见<Inside c++ object model>)中,
只有定义了虚函数的类才有虚函数表。

3.4 reinterpre_cast
  1. 用在任意的指针之间的转换。
  2. 引用之间的转换。
  3. 指针和足够大的int之间的转换。
  4. 整数到指针之间的一个转换。

4. vector怎么删除重复的元素

假如是一个数组,存在多个重复的元素。我们可以先用sort进行排序,然后用vector的erase(unique)删除掉重复的元素。unique返回的是重复元素的开始位置。
注意

  1. 算法unique能够移除重复的元素。每当在[first, last]内遇到有重复的元素群,它便移除该元素群中第一个以后的所有元素。
  2. unique只移除相邻的重复元素,如果你想要移除所有(包括不相邻的)重复元素,必须先将序列排序,使所有重复元素都相邻。
  3. unique会返回一个迭代器指向新区间的尾端,新区间之内不包含相邻的重复元素。
  4. 事实上unique并不改变元素个数,有一些残余数据会留下来,可以用erase函数去除。
    sort->unique->erase
4.1 vector的底层原理?删除一个元素底层会做什么事情?

vector所拥有的变量如下:

template <class T, class Alloc = alloc>class vector
{
protected:
	iterator start;
	// 目前使用的空间头    
	iterator finish;
	// 目前使用的空间尾     i
	terator end_of_storage; // 目前可用的空间尾
}

vector采用的数据结构很简单,就是连续线性空间。以上定义中迭代器start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。end_of_storage存在的原因是为了降低空间配置的成本,vector实际分配空间大小的时候会比客户端需求的大一些,以便于未来的可能的扩展。
运用start,finish,end_of_storage三个迭代器,便可轻易地提供首尾标示、大小、容量、空容器判断、注标([])运算子、最前端元素值、最后端元素值…等机能:
在这里插入图片描述

4.2 vector 扩容的本质

重新申请,移动元素,释放空间
需经历的步骤如下:

  1. 完全弃用现有的内存,重新申请更大的内存空间。
  2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中。
  3. 最后将旧的内存空间释放。
  4. 不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。
4.2 深拷贝和浅拷贝

浅拷贝:实体相同,值拷贝
深拷贝:开辟空间,内容拷贝。
浅拷贝:称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。
深拷贝:拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。

6. LRU

简介
LRU的全称为:Least Recently Used。最近最久未使用算法,它是页面置换算法的一种。
LRU 算法根据数据的历史访问记录来进行淘汰数据,其核心思想是「如果数据最近被访问过,那么将来被访问的几率也更高」。实现的方式还是很难的,如果不会就只能作罢了。
思路
map存储数据,实现查找效率为O(1),双向链表实现算法逻辑。
算法逻辑

  1. 新数据会插入到链表的头部
  2. 当缓存数据被访问时,将该缓存数据移到链表的头部。
  3. 当新数据插入时,达到缓存上限了,将尾部数据删除掉,新数据放在头部。

code

#include <iostream>
#include <map>
using namespace std;
struct LinkNode
{
	int m_key;
	int m_value;
	ListNode* pre;
	ListNode* next;
	ListNode(int key, int value)
	{
		m_key = key;
		m_value = value;
		pre = NULL;
		next = NULL;
	}
}//*  LRU缓存实现类  双向链表。
class LRUCache
{
public:
	LRUCache(int size)
	{
		m_capacity = size;
		phead = NULL;
		pTail = NULL;
	}
	~LRUCache()
	{
		map<int, ListNode*>::iterator it = mp.begin();
		for (; it != mp.end();)
		{
			delete it->second;
			it->second = NULL;
			mp.erase(it++);
		}
		delete phead;
		phead = NULL;
		delete pTail;
		pTail = NULL;
	}//** 这里只是移除,并不删除节点	
	void Remove(ListNode* pNode) {
		// 如果是头节点		
		if (pNode->pPre == NULL)
		{
			pHead = pNode->pNext;
			pHead->pPre = NULL;
		}
		// 如果是尾节点		
		if (pNode->pNext == NULL)
		{
			pTail = pNode->pPre;
			pTail->pNext = NULL;
		}
		else
		{
			pNode->pPre->pNext = pNode->pNext;
			pNode->pNext->pPre = pNode->pPre;
		}
	}
	//  将节点放到头部,最近用过的数据要放在队头。
	void SetHead(ListNode* pNode)
	{
		pNode->pNext = pHead;
		pNode->pPre = NULL;
		if (pHead == NULL)
		{
			pHead = pNode;
		}
		else
		{
			pHead->pPre = pNode;
			pHead = pNode;
		}
		if (pTail == NULL)
		{
			pTail = pHead;
		}
	}
	// * 插入数据,如果存在就只更新数据	
	int Set(int key, int value)
	{
		map<int, ListNode*>::iterator it = mp.find(key);
		if (it != mp.end())
		{
			ListNode* Node = it->second;
			Node->m_value = value;
			Remove(Node);
			SetHead(Node);
		}
		else
		{
			ListNode* NewNode = new ListNode(key, value);
			if (mp.size() >= m_capacity)
			{
				map<int, ListNode*>::iterator it = mp.find(pTail->m_key);
				//从链表移除			
				Remove(pTail);
				//删除指针指向的内存
				delete it->second;
				//删除map元素		
				mp.erase(it);
			}
			//放到头部	
			SetHead(NewNode);
			mp[key] = NewNode;
		}
	}
	//获取缓存里的数据
	int Get(int key)
	{
		map<int, ListNode*>::iterator it = mp.find(key);
		if (it != mp.end())
		{
			ListNode* Node = it->second;
			Remove(Node);
			SetHead(Node);
			return Node->m_value;
		}
		else
		{
			return -1;       //这里不太好,有可能取得值也为-1	
		}
	}
	int GetSize()
	{
		return mp.size();
	}
private:
	int m_capacity;    //缓存容量
	ListNode* pHead;   //头节点
	ListNode* pTail; //尾节点
	map<int, ListNode*>  mp;   //mp用来存数据,达到find为o(1)级别。
};
7.1 静态多态和动态多态是如何实现的?

多态分为静态多态性和动态多态性,其中静态多态性是通过重载实现的,在程序编译时系统就可以确定调用哪个函数,因此也称为编译时的多态性,动态多态性则是通过虚函数实现的,在程序运行时才能决定操作的对象,因此也被称为运行时的多态性。
静态多态
静态多态有两种方式:

  1. 函数重载: 包括普通函数的重载和成员函数的重载。重载函数的关键是函数参数列表——也称函数特征标。包括:函数的参数数目和类型,以及参数的排列顺序。所以,重载函数与返回值,参数名无关。
  2. (泛型编程)函数模板的使用.函数模板是通用的函数描述,也就是说,使用泛型来定义函数,其中泛型可用具体的类型(int 、double等)替换。通过将类型作为参数,传递给模板,可使编译器生成该类型的函数。
    code
// 交换两个值,但是不清楚是int 还是 double,如果不使用模板,则要写两份代码
// 使用函数模板,将类型作为参数传递
template<class T>
class Swa(T a,T b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
};

参考1
动态多态

#include 
<iostream>
using namespace std;
class Base
{
public:
	virtual void Print() = 0;
	virtual ~Base();
}
class child_1 :public Base {
public:
	void Print()
	{
		cout << "--->child1";
	}
}class child_2 :public Base
{
public:
	void Print()
	{
		cout << "---> child2";
	}
}
int main()
{
	Base* p = new Child_1();
	p->Print();
	delete p;
	p = new Child_2();
	p->Print();
	p = NULL;
	return 0;
}

动态多态是如何实现的?
程序运行到动态绑定时,通过基类的指针所指向的对象类型,通过vtpr找到虚函数表,然后调用相应的方法,即可实现多态。
编译器如何判断一个函数是否是虚函数?
该函数的最前面有virtual进行声明。就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写

7.1_1 为什么C语言中没有重载呢?

编译器在编译期间创建的一个字符串,用来指明函数的定义或原型。C和C++程序的函数在内部使用不同的名字修饰方式。
C编译器的函数名修饰规则:
对于__stdcall调用约定,编译器和链接器会在输出函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其参数的字节数,例如_functionname@number。__cdecl调用约定仅在输出函数名前加上一个下划线前缀,例如_functionname。__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个**“@”符号和其参数的字节数**,例如@functionname@number。
C++编译器的函数名修饰规则:
C++的函数名修饰规则有些复杂,但是信息更充分,通过分析修饰名不仅能够知道函数的调用方式,返回值类型,参数个数甚至参数类型。不管 __cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和 按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”,对于__fastcall方式则是“@@YI”。参数表的拼写代号如下所示:

7.2 多态性指的是什么?(什么是多态)

答:
说法一:多态性指的是相同对象在接收不同消息或不同对象接收相同消息产生不同的实现动作。多态支持运行时多态和编译时多态。编译时多态是由子类的重载所实现的,而运行时多态是由虚函数实现的。多态的目的是为了代码模块化和接口重用。
说法二:(运行时多态)在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
说法三:多态按字面来理解就是多种形态。当类之前存在层次结构,并且类之间是通过继承关联的,就会用到多态。C++多态意味着在调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

7.3 使用多态的过程中应注意什么点?
  1. 注意构造函数中不能使用虚函数。因为我们在创建一个对象时是需要知道该对象的类型的,而虚函数的特性就是在运行时才能确定对象的类型。
  2. 析构函数则必须用多态,否则在调用析构函数时,没办法根据调用对象的类型调用相应的析构函数,有写对象的内存有可能不会被释放。
7.4. 讲一下对多态的理解

概念
首先,多态的实现方式分为两种:重载,重写。
重载是在相同作用域内,函数名相同,形参个数或类型不同的实现方式。这是一种静态多态,即其作用的函数是在编译时就确定了的。

重写是在不同的作用域内,即要求一个在父类,而一个在子类,这两个函数的函数名,以及形参个数,类型都必须相同。这是一种动态多态,只有当程序运行的时候,才能确定是哪个函数。
还有一种是重定义也顺便讲一下。
重定义是指在不同的作用域内,即一个在父类,一个在子类中,只要不构成重写,则为重定义。换种说法是,子类中重定义的函数在父类也有一个同名的函数,只不过,父类的该函数并不是虚函数。

7.5 C++虚函数的实现原理(底层结构)

每一个含有虚函数的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的指针。
实现原理

  1. 当类中存在虚函数时,编译器会再类中自动生成一个虚函数表。
  2. 虚函数表是一个存储类成员函数指针的数据结构。
  3. 虚函数表由表一起自动生成和维护。
  4. virtual修饰的成员函数会被编译器放入虚函数表中。
  5. 存在虚函数时,编译器会为对象自动生成一个指向虚函数表的指针(通常称为vptr)

具体的一个实现过程

  1. 父类的fun()是个虚函数,所以编译器会跟父类对象自动添加了一个vptr指针,来指向父类的虚函数表,这个虚函数表存放了父类的fun()函数的指针。
  2. 子类的fun()函数是重写了父类的,所以无论写不写virtual,编译器都会给子类对象添加一个virtual和vptr指针,指向子类的虚函数表,这个虚函数表存放了子类fun()函数的函数指针。
  3. 指向p->fun()时,编译器会检测到fun()是一个虚函数,所以会在运行时,动态的根据指向的对象,找到这个对象的vptr,然后找到这个对象的虚函数表,最后调用虚函数表里对应的函数,从而实现多态
7.6 虚函数的使用场景
  1. 在有继承关系的类中,析构函数最好使用虚函数。
  2. 虚函数的作用是在一个类函数的调用无法在编译时刻确定,必须在运行时刻确定时,应该声明其为虚函数。
7.7. 哪些C++成员函数不能用多态
  1. 什么样的函数不能被声明为虚函数?
  1. 普通函数不属于成员函数,是不能被继承的。普通函数只能被重载,不能被重写。因此声明为虚函数没有意义。因为编译器会在编译时绑定函数。而多态体是在运行时绑定,通常通过基类指针子类对象实现绑定。
  2. 友元函数不属于类的成员函数,不能被继承,对于没有继承特性的函数没有虚函数的说法。
  3. 构造函数:a.构造函数时用来初始化对象的,例如,子类可以继承基类的构造函数,那么子类对象得到构造将使用基类的构造函数,而基类构造函数并不知道子类有什么成员,所以不能使用继承。b.另一个角度是,多态是通过基类指针指向子类对象来实现多态的,在对象构造之前并没有对象产生,因此无法使用多态,这是矛盾的。
  4. 内联成员函数:该函数的作用是为了在代码中直接展开,减少函数调用的代价。也就是内联函数时在编译时展开的。在编译的时候,将这个内联函数的代码副本放置在每个调用该函数的地方。而多态,是在运行时绑定的,所以显然,相互违背。
  5. 静态成员函数:该函数理论上是可继承的。但静态成员函数是在编译时确定的,无法动态绑定。不支持多态。

8. linux系统里,一个被打开的文件可以被另一个进程删除吗?

答:答案是会的。因为我们知道,在linux系统中,一个文件被彻底删除的标志是,不存在任何link了。而其实每个文件都有两个link计数器,一个是i_count,一个是i_nlink 。i_count的意义是打开该文件的进程的数量,而i_nlink是创建硬链接的个数。换句话说就是,当该文件被某个进程引用的时候,i_count就会增加;当创建该文件的硬链接时,i_nlink就会增加。所以,执行了删除操作,只是删除了该文件的硬链接。而i_count并不为0。所以说,i_nlink是删除一个文件的充分条件,而i_count是删除一个文件的必要条件。

9. 一个10M大小的buffer里存满了数据,现在要把这个buffer里的数据尽量发出去,可以允许部分丢包,问是用TCP好还是UDP好?为什么?

使用 UDP 可以保证速度, 没有重传机制, 没有阻塞机制, 保证最快速度。
使用 TCP 可以保证尽量不丢包, 丢包会重传, 在网络环境差的情况下推荐使用。可靠传输,流量控制,拥塞控制。

9.1 UDP无法保证顺序,如何解决这个问题?

UDP丢包的情况之一:

  1. 服务器端的socket接收端存满了,因为udp没有流量控制,因此发送速度比接收速度要快,很容易出现这种情况,方法之一是将接收缓存加大。
    若要对丢包及乱序做处理
    要实现文件的可靠传输,就必须在上层对数据丢包和乱序作特殊处理,必须要有要有丢包重发机制和超时机制
    常见的可靠传输算法有QUIC协议,模拟TCP协议,重发请求(ARQ)协议,它又可分为连续ARQ协议、选择重发ARQ协议、滑动窗口协议等等。
    方法
    方法一:重新设计一下协议,增加接收确认超时重发。(推荐) 基于 UDP 的 QUIC 协议 可以实现类似 TCP 的可靠性传输
    方法二:在接收方,将通信和处理分开,增加个应用缓冲区;如果有需要增加接收socket的系统缓冲区。(本方法不能从根本解决问题,只能改善)

10.一个完整的HTTP请求会涉及到哪些协议?(未完)

在这里插入图片描述
TCP的三次握手:
在这里插入图片描述
位码即tcp标志位,有6种标示:SYN(synchronous建立联机) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)Sequence number(顺序号码) Acknowledge number(确认号码)

在这里插入图片描述
这里首部长度最大值是15,但其单位是4个字节。所以,表示TCP首部长度最大是60个字节。
TCP/IP四层结构
在这里插入图片描述

答:

10.1 TCP四次挥手,最后一次不要行不行?

因为TCP是全双工通信的

  1. 第一次挥手 因此当主动方发送断开连接的请求(即FIN报文)给被动方时,仅仅代表主动方不会再发送数据报文了,但主动方仍可以接收数据报文。
  2. 第二次挥手 被动方此时有可能还有相应的数据报文需要发送,因此需要先发送ACK报文,告知主动方“我知道你想断开连接的请求了”。这样主动方便不会因为没有收到应答而继续发送断开连接的请求(即FIN报文)。
  3. 第三次挥手 被动方在处理完数据报文后,便发送给主动方FIN报文,告知主动方“我也发完了,可以结束了”;这样可以保证数据通信正常可靠地完成。发送完FIN报文后,被动方进入LAST_ACK阶段(超时等待)。
  4. 第四挥手 如果主动方及时发送ACK报文进行连接中断的确认,(保证,第三次挥手之前发送的数据都已经被主动方给接收好了。)这时被动方就直接释放连接,进入可用状态

不能不要
最后一次握手是客户端发送ACK给服务端,确认客户端已接收到服务端请求断开连接的请求。若只有三次握手,那么 服务端,再发送完自己要断开连接的请求后,无法知道,是否客户端已接收到该信息,四次挥手的目的是在链接断开时保证传输数据的完整性。
关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了

所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

在这里插入图片描述

10.2 三次握手的意义

三次握手的目的是同步连接双方的序列号和确认号并交换 TCP 窗口大小信息

10.3 IP数据报

在这里插入图片描述

10.4 UDP如何实现可靠性传输?
  1. 首先,不同场景对可靠的需求是不一样的,有这么三个点:
  1. 尽力可靠:通信的接收方要求发送方的数据尽量完整到达,但业务本身的数据是可以允许缺失的。例如:音视频数据。

  2. 无序可靠:通信的接收方要求发送方的数据必须完整到达,但可以不管到达的先后顺序.例如:文件传输、白板书写、图形实时绘制数据、日志型追加数据。

  3. 有序可靠:通信接收方要求发送方的数据必须按顺序完整到达。

  1. 为什么要用UDP做可靠保证?
    在保证通信的时延和质量的条件下尽量降低成本。

  2. RUDP主要解决的问题?

  1. 端到端连通性问题:一般终端直接和终端通信都会涉及到 NAT 穿越,TCP 在 NAT 穿越实现非常困难,相对来说 UDP 穿越 NAT 却简单很多.

  2. 弱网环境传输问题。

  3. 带宽竞争问题:客户端数据上传需要突破本身 TCP 公平性的限制来达到高速低延时和稳定,也就是说要用特殊的流控算法来压榨客户端上传带宽。

  4. 传输路径优化问题: 在一些对延时要求很高的场景下,会用应用层 relay 的方式来做传输路由优化,也就是动态智能选路,这时双方采用 RUDP 方式来传输,中间的延迟进行 relay 选路优化延时。

  5. 资源优化问题:某些场景为了避免 TCP 的三次握手和四次挥手的过程,会采用 RUDP 来优化资源的占用率和响应时间,提高系统的并发能力,例如 QUIC.

4, RUDP如何保证可靠性?

  1. 重传模式:RUDP 的重传是发送端通过接收端 ACK 的丢包信息反馈来进行数据重传,发送端会根据场景来设计自己的重传方式,重传方式分为三类:定时重传、请求重传和 FEC 选择重传。

A. 定时重传:发送端如果在发出数据包(T1)时刻,在一个RTO(Retransmission TimeOut即重传超时时间)之后,还未收到这个数据包的ACK消息,那么发送端就重传这个数据包。

B.请求重传: 接收端在发送ACK的时候携带自己丢失报文的信息反馈,发送端在接收到ACK信息时根据丢包反馈进行报文重传。

C. FEC 选择重传:FEC(Forward Error Correction)是一种前向纠错技术,一般通过 XOR 类似的算法来实现,也有多层的 EC 算法和 raptor 涌泉码技术,其实是一个解方程的过程。
在发送方发送报文的时候,会根据 FEC 方式把几个报文进行 FEC 分组,通过 XOR 的方式得到若干个冗余包,然后一起发往接收端,如果接收端发现丢包但能通过 FEC 分组算法还原,就不向发送端请求重传,如果分组内包是不能进行 FEC 恢复的,就向发送端请求原始的数据包。
FEC 分组方式适合解决要求延时敏感且随机丢包的传输场景,在一个带宽不是很充裕的传输条件下,FEC 会增加多余的包,可能会使得网络更加不好。FEC 方式不仅可以配合请求重传模式,也可以配合定时重传模式。

2)窗口与拥塞控制
RUDP使用一个收发的滑动窗口来配合对应的拥塞算法做流量控制,若涉及到可靠有序的RUDP,接收端就要做窗口排序和缓冲,如果是无序可靠或尽力可靠的场景,接收端就不做窗口缓冲。

  1. 经典拥塞算法
    A. 慢启动

B. 拥塞避免

C. 拥塞处理

D. 快速恢复。
参考

11.介绍以下进程中的几种通讯方式(进程的通信方式)

  1. 管道:这是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用,一般这种血缘关系是指父子进程。
  2. 有名管道这也是一种半双工的通信方式,但这个可以允许非血缘关系的进程使用。
  3. 信号量:信号量是一种计数器,它可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程在访问某共享资源的时候,其他进程也访问该资源。
  1. 一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
  2. 另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
  3. P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的
  1. 消息队列:消息队列是由消息组成的链表,存放在内核中,并由消息标识符标识。消息克服了信号传递信息少,管道只能传输无格式字节流以及缓冲区受限的问题。一般用在耗时,且不需要即使回复信息的这种情况下。消息队列的经典实用场景是异步,削峰,解耦。
  2. 信号:信号是一种比较复杂的通讯方式,用于通知该进程某一事件的发生。信号是进程间通信机制中唯一的异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。
  3. 共享内存:映射一段能被多个进程所访问的内存。它往往和信号量进行配合使用。
  4. 套接字(Socket)通信:也是进程中的一种通讯方式。
11.1 消息队列的作用:异步,削峰,解耦

消息队列的缺点:一是通信不及时,二是附件也有大小限制,三是消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销。
消息队列主要解决的是:异步消息、流量削峰、应用耦合。

  1. 应用解耦:将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统不需要做任何修改。
  2. 异步消息:将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度。
  3. 流量削峰:系统慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。
11.2 线程间的同步

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
① 锁机制:
互斥锁、条件变量、读写锁和自旋锁。

  1. 互斥锁互斥锁确保同一时间只能有一个线程访问共享资源。当锁被占用时试图对其加锁的线程都进入阻塞状态(释放CPU资源使其由运行状态进入等待状态)。当锁释放时哪个等待线程能获得该锁取决于内核的调度。这个相当于是有智慧的等待。

  2. 读写锁读写锁当以写模式加锁而处于写状态时任何试图加锁的线程(不论是读或写)都阻塞,当以读状态模式加锁而处于读状态时“读”线程不阻塞,“写”线程阻塞。读模式共享,写模式互斥

  3. 条件变量:条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。因为生产者生产出来的物品是临界资源,即所有进程和线程都可以使用的公共资源,则在一个时刻仅允许一个消费者去取。这时便使用互斥量去保护临界资源。
    a. 条件变量是利用线程间共享的全局变量进行同步的一种机制。
    b. 主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。
    为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
    c. 使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的。
    d. 线程的阻塞是通过成员函数wait()wait_for()wait_until函数实现的。wait 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词。wait_for 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,或者超时返回。以上两个类型的wait函数都在会阻塞时,自动释放锁权限,即调用unique_lock的成员函数unlock(),以便其他线程能有机会获得锁。这就是条件变量只能和unique_lock一起使用的原因,否则当前线程一直占有锁,线程被阻塞。
    解决虚假唤醒

while (!(xxx条件) )
{
    //虚假唤醒发生,由于while循环,再次检查条件是否满足,
    //否则继续等待,解决虚假唤醒
    wait();  
}
//其他代码
....

  1. 自旋锁:自旋锁上锁受阻时线程不阻塞而是在循环中轮询查看能否获得该锁,没有线程的切换因而没有切换开销,不过对CPU的霸占会导致CPU资源的浪费。 所以自旋锁适用于并行结构(多个处理器)或者适用于锁被持有时间短而不希望在线程切换产生开销的情况。(相当于是一种苦苦等待的情况。)
    ② 信号量机制(Semaphore)
    包括无名线程信号量和命名线程信号量。线程的信号和进程的信号量类似,使用线程的信号量可以高效地完成基于线程的资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量就增加;公共资源减少的时候,信号量就减少;只有当信号量的值大于0的时候,才能访问信号量所代表的公共资源。
    ③ 信号机制(Signal)
    类似进程间的信号处理。
    ④ violate全局变量-共享内存
    关于violate可以参考博文:多线程并发之volatile的底层实现原理
    ⑤ wait/notify
    阻塞/唤醒,关于这个参考博文:Thread入门与线程方法详解及多线程安全
    参考博文:
    线程间的通信与进程间通信方式
    进程间通信(IPC)介绍
11.2_1 线程间的通信方式

通讯有两种方式:

  1. 使用全局变量进行通信

由于属于同一个进程的各个线程共享操作系统分配该进程的资源,故解决线程间通信最简单的一种方法是使用全局变量。对于标准类型的全局变量,我们建议使用volatile 修饰符,它告诉编译器无需对该变量作任何的优化,即无需将它放到一个寄存器中,并且该值可被外部改变。如果线程间所需传递的信息较复杂,我们可以定义一个结构,通过传递指向该结构的指针进行传递信息。

  1. 使用自定义消息

我们可以在一个线程的执行函数中向另一个线程发送自定义的消息来达到通信的目的。一个线程向另外一个线程发送消息是通过操作系统实现的。利用Windows操作系统的消息驱动机制,当一个线程发出一条消息时,操作系统首先接收到该消息,然后把该消息转发给目标线程,接收消息的线程必须已经建立了消息循环。

11.2_2 多线程的好处
  1. 提高应用程序的响应速度。当使用某些耗时的操作时,若我们使用多线程技术,则可以将该操作放入某个线程中进行执行。界面就可以响应了。
  2. 使多CPU的系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程会置于不同的CPU中进行运行。
  3. 改善程序结构。一个即长又复杂的进程可以拆分成不同的线程。这样的程序会更加便于修改。
11.3进程的调度方式有哪些?

均可使用在作业调度和进程调度。

  1. 先来先服务调度算法。
  2. 短作业优先调度。
  3. 高优先权优先调度。
  4. 高响应比优先调度。

响应比=作业周转时间/作业处理时间=(作业处理时间+作业等待时间)/作业处理时间=1+(作业等待时间/作业处理时间)

11.4 Linux中“|”是无名管道还是有名管道?
  1. 无名管道只能用于公共祖先的两个进程间的通信或存在父子关系的进程,原因是自己创建的管道在别的进程中并不可见。
  2. 有名管道可用于同一系统中的任意两个进程间的通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。

无名管道
无名管道创建完成后,等同于操作文件。
无名管道的读端被视作一个文件,写端也被视作一个文件。
创建用pipe,操作用read、write、close。

① 无名管道通信是单向的,有固定的读端和写端。
② 数据被进程从管道读出后,管道中的数据就不存在了。
③ 进程读取空管道时,进程会阻塞。
④ 进程往满的管道写入数据时,进程会阻塞。
⑤ 管道容量最大为64KB,可以通过宏PIPE_BUFFERS进行设置。

$ ps auxf | grep mysql

上面命令行里的「|」竖线就是一个管道,它的功能是将前一个命令(ps auxf)的输出,作为后一个命令(grep mysql)的输入,从这功能描述,可以看出管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行。
我们得知上面这种管道是没有名字,所以「|」表示的管道称为匿名管道,用完了就销毁。
管道分为无名管道(pipe)和有名管道(FIFO)两种。
所以,你有可能会疑惑,那为啥能够进程父子进程间的通信呢?两个描述符不是都在同一个进程吗?
方案:我们可以使用 fork 创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个「 fd[0] 与 fd[1]」,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。
在这里插入图片描述
但一般为了防止混乱,为了避免这种情况,通常的做法是:

  1. 父进程关闭读取的 fd[0],只保留写入的 fd[1];
  2. 子进程关闭写入的 fd[1],只保留读取的 fd[0];
    在这里插入图片描述
    所以说如果需要双向通信,则应该创建两个管道。
    有名管道
    管道还有另外一个类型是命名管道,也被叫做 FIFO,因为数据是先进先出的传输方式。
    在使用命名管道前,先需要通过 mkfifo 命令来创建,并且指定管道名字:
$ mkfifo myPipe

myPipe 就是这个管道的名称,基于 Linux 一切皆文件的理念,所以管道也是以文件的方式存在,我们可以用 ls 看一下,这个文件的类型是 p,也就是 pipe(管道) 的意思

有名管道又叫FIFO文件,它的操作与文件操作类似,需要创建、打开和关闭等操作。
创建用mkfifo,删除用unlink,读写用read、write,打开关闭用open、close。
FIFO文件的操作与普通文件的操作差异如下:
① 读取FIFO文件的进程只能以“RDONLY”方式打开FIFO文件。
② 写FIFO文件的进程只能以“WRONLY”方式打开FIFO文件。
③ FIFO文件里面的内容被读取后,就消失了。但是普通文件里面的内容读取后还存在。

总结:管道这种通信方式效率低,不适合进程间频繁地交换数据。

  1. https://www.cnblogs.com/xiaolincoding/p/13402297.html
11.5 共享内存同时写该怎么办?

可通过信号量的PV操作实现这一保护。在临界区前实现P,在临界区后实现V。

通过信号量的PV操作实现这一保护。
在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间,并且有一个页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,他们指向的这块内存即共享内存。

  1. 进程控制块(PCB):为了描述和控制进程的运行,系统为每个进程定义了一个 数据结构——进程控制块(PCB)。 它是进程重要的组成部分,它记录了操作系统所需的、用于描述进程的当前状态和控制进程的全部信息。 操作系统就是根据进程的PCB来感知进程的存在,并依此对进程进行管理和控制。 PCB是进程存在的唯一标识。
  2. PCB包括如下四个方面的信息:
  1. 进程标识信息:a:内部标识符:由操作系统赋予每个进程一个唯一的标识符,它通常为一个进程的序号。b:外部标识符:由创建者产生,是由字母和数字组成的字符串,为用户访问该进程方便。
  2. 处理机状态:由寄存器内的信息组成。进程运行时的许多信息均存放在处理机的各种寄存器中。其中PSW(程序状态寄存器)相当重要。
  3. 进程调度信息。
  4. 进程控制信息。
11.6 共享内存的优缺点

标答:
好处:
第一个是效率高,共享内存只需要拷贝两次数据,而管道,消息队列则需要拷贝四次。拷贝的四次是:从输入文件到用户空间,从用户空间到内核空间,从内核空间到用户空间,从用户空间到输出文件。
第二个是对进程的要求低,不像管道那样需要进程有一定的父子关系才可以进行传输,共享内存可用于任意两个进程之间的传输。

缺点:
第一个就是没有提供同步机制,只能借助其他手段,比如互斥量进行同步。
第二个是不适用与网络通信,一般都是在同一台主机的多个线程。

优点

  1. 采用共享内存通信的一个好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

  2. 不像匿名管道那样要求通信的进程有一定的父子关系,可用于任意两个进程之间通信。(要求低

缺点

  1. 共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。(未同步

  2. 利用内存缓冲区直接交换信息,内存的实体存在于计算机中,只能同一个计算机系统中的诸多进程共享,不方便网络通信。(不方便网络通信

11.7 为什么有了互斥锁了还需要条件变量

标答:首先,我们知道互斥锁的特性是对共享资源进行阻塞,在访问完成后,释放互斥量上的锁。但如果对互斥量加锁后,任何其他试图对该共享量进行加锁的线程都会阻塞,直到该锁解开。如果释放互斥锁时,有多个线程阻塞,所有对该共享量加锁的线程都会变成可运行状态。第一个变身成功的,会对共享量再次加锁,其他线程就依然被阻塞。所以,条件变量的作用,就是可以某些特定条件下,才允许某些线程进行加锁,其他线程,不满足这个条件的,就不能加锁。

在这里插入图片描述

11.8 共享内存的使用场景

与其他通信机制,如信号量,配合使用,来实现进程间的同步与通信。

11.9 共享内存的使用原理
  1. A,B进程共享同一块进程空间的意思:同一块物理内存被映射到进程的A,B各自的进程地址空间。

  2. 会保持共享内存的映射,直到通信结束。

  3. 一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。

  4. Linux共享内存的获取:shmget()
    int shmget(key_t key,size_t size,int shmflg);

task_struct ->地址空间->页表->物理地址 <-页表<-地址空间<- task_struct

12. 做UDP,怎么找对方的IP和端口?

UDP会通过广播的方式将自己IP与端口号发送出去,另一方的UDP收到数据后,便知道了对方的信息了。

13. 关键字const的含义、作用与优点

只可读,节省空间,只是给出了对应的内存地址,并不像#define一样给出的是立即数。
答:
含义

  1. 首先,一个变量如果被const进行修饰的话,就认为该变量只可进行访问,不可进行修改。
    作用
  2. 可以用来定义常量,修饰函数参数,修饰函数返回值,防止被意外修改。
  3. 可以节省空间, 避免不必要的内存分配。const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
13.1 const如何修改
  1. 方法一:在const前面加上volatile.
  2. 方法二:通过创建一个指针,通过显式类型转换,获取a变量的地址。

14. 海量数据处理面试题

14.1Top(K)一般处理方式

1、直接全部排序(只适用于内存够的情况)

当数据量较小的情况下,内存中可以容纳所有数据。则最简单也是最容易想到的方法是将数据全部排序,然后取排序后的数据中的前K个。

这种方法对数据量比较敏感,当数据量较大的情况下,内存不能完全容纳全部数据,这种方法便不适应了。即使内存能够满足要求,该方法将全部数据都排序了,而题目只要求找出top K个数据,所以该方法并不十分高效,不建议使用。

2、快速排序的变形 (只使用于内存够的情况)

这是一个基于快速排序的变形,因为第一种方法中说到将所有元素都排序并不十分高效,只需要找出前K个最大的就行。

这种方法类似于快速排序,首先选择一个划分元,将比这个划分元大的元素放到它的前面,比划分元小的元素放到它的后面,此时完成了一趟排序。如果此时这个划分元的序号index刚好等于K,那么这个划分元以及它左边的数,刚好就是前K个最大的元素;如果index > K,那么前K大的数据在index的左边,那么就继续递归的从index-1个数中进行一趟排序;如果index < K,那么再从划分元的右边继续进行排序,直到找到序号index刚好等于K为止。再将前K个数进行排序后,返回Top K个元素。这种方法就避免了对除了Top K个元素以外的数据进行排序所带来的不必要的开销。

3、最小堆法

这是一种局部淘汰法。先读取前K个数,建立一个最小堆。然后将剩余的所有数字依次与最小堆的堆顶进行比较,如果小于或等于堆顶数据,则继续比较下一个;否则,删除堆顶元素,并将新数据插入堆中,重新调整最小堆。当遍历完全部数据后,最小堆中的数据即为最大的K个数。

4、分治法

将全部数据分成N份,前提是每份的数据都可以读到内存中进行处理,找到每份数据中最大的K个数。此时剩下NK个数据,如果内存不能容纳NK个数据,则再继续分治处理,分成M份,找出每份数据中最大的K个数,如果M*K个数仍然不能读到内存中,则继续分治处理。直到剩余的数可以读入内存中,那么可以对这些数使用快速排序的变形或者归并排序进行处理。

5、Hash法

如果这些数据中有很多重复的数据,可以先通过hash法,把重复的数去掉。这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间。处理后的数据如果能够读入内存,则可以直接排序;否则可以使用分治法或者最小堆法来处理数据。

14.2海量日志处理,提取出日访问百度次数最多的IP地址

算法思想:分而治之+hash
首先,将IP地址%1024。将这些ip地址分别放入1024个小文件中。这样每个小文件就包含有4MB个 IP地址,

14.3 如何从 100 亿 URL 中找出相同的 URL?

题目
1.

  1. 50 0000 0000 64B = 564GB = 320GB
  2. 遍历文件a。执行hash(URL) % 1000。根据计算结果把遍历到的 URL 存储到 a0, a1, a2, …, a999。
  3. 遍历文件b。执行hash(URL) % 1000。根据计算结果把遍历到的 URL 存储到b0, b1, b2, …, b999。
  4. 这样所有相同的URL都一定在对应的小文件里面。比如a0->b0
  5. 接下来遍历ai。把 URL 存储到一个 HashSet 集合中。然后遍历 bi 中每个 URL,看在 HashSet 集合中是否存在,若存在,说明这就是共同的 URL,可以把这个 URL 保存到一个单独的文件中。

方法总结

  1. 分而治之,进行哈希取余;
  2. 对每个子文件进行 HashSet 统计。

15. 解释一下HashMap这个数据结构

  1. 首先,hashMap是一个保存(key,value)键值对的一个数据结构,每个键值对叫一个Entry。这些Entry分散存储在一个数组中,这个数组就是HashMap的主干。
  2. 新来的Entry节点若和原来的冲突的话,则使用头插法将其插入Entry主节点后。之所以使用头插法是因为发明者认为后面插入的节点更容易会被使用。
  3. HashMap的默认初始长度是16,并且每次扩展或手动初始化时,长度必须是2的整数次幂。注意为啥是16呢,是为了符合均匀分布的原则,若长度为其他数字,你会发现,在与改数字做与运算的时候,有些index很容易得到,有些index则较难得到。这就违背了Hash算法均匀分布的原则。
  4. index = HashCode(Key) & (Length - 1),从value->index
  5. 扩容后,要重新遍历原来的Hash数组,从而让该hash数组均衡分布到新的hash数组之中。

16. 五层模型

分层的目的
分层的目的是利用层次结构可以把开放系统的信息交换问题分解到一系列容易控制的软硬件模块-层中,而各层可以根据需要独立进行修改或扩充功能,同时,有利于个不同制造厂家的设备互连
OSI七层模型:物理层(比特),数据链路层,网络层,传输层,会话层,表示层,应用层
TCP/IP五层模型:物理层,数据链路层,网络层,传输层,应用层。

OSI七层模型
在这里插入图片描述

1.应用层:应用层是网络体系中最高的一层,也是唯一面向用户的一层,也可视为为用户提供常用的应用程序,每个网络应用都对应着不同的协议。HTTP、TFTP, FTP, SMTP,。

  1. 表示层:主要负责数据格式的转换,确保一个系统的应用层发送的消息可以被另一个系统的应用层读取,编码转换,数据解析,管理数据的解密和加密,同时也对应用层的协议进行翻译。Telnet, Rlogin, SNMP, Gopher。
  2. 会话层:负责网络中两节点的建立,在数据传输中维护计算机网络中两台计算机之间的通信连接,并决定何时终止通信。SMTP, DNS
    4.传输层:是整个网络关键的部分,是实现两个用户进程间端到端的可靠通信,处理数据包的错误等传输问题。是向下通信服务最高层,向上用户功能最底层。即向网络层提供服务,向会话层提供独立于网络层的传送服务和可靠的透明数据传输。TCP的数据的单位被称为段, UDP的单位被称为数据报。
  3. 网络层:进行逻辑地址寻址,实现不同网络之间的路径选择,IP就在网络层。IP, ICMP, ARP, RARP, AKP, UUCP。数据的单位是数据包
  4. 数据链路层:物理地址(MAC地址),网络设备的唯一身份标识。建立逻辑连接、进行硬件地址寻址,相邻的两个设备间的互相通信。FDDI, Ethernet, Arpanet, PDN, SLIP, PPP,STP。HDLC,SDLC,帧中继。数据的单位是帧。
    7.七层模型中的最底层,主要是物理介质传输媒介(网线或者是无线),在不同设备中传输比特,将0/1信号与电信号或者光信号互相转化。IEEE 802.1A, IEEE 802.2到IEEE 802。数据的单位是比特。

17. TCP黏包

原因

  1. TCP为减少网络中报文段的数量,使用Nagle算法,只有上一个分组得到确认时,下一个分组才会发送,收集了多个小分组,在确认到来时一起发送。Nagle造成发送方可能会出现黏包问题。
  2. TCP收到数据包后,有可能不会马上处理,而是将接受到的数据包保存在缓存之中。
    解决的方法
  3. 首先是关闭Nagle算法。
  4. 格式化数据。具有定好的开头和结尾。
    UDP会不会产生黏包问题呢?
  5. TCP为了保证可靠传输并减少额外的开销,每次发包都要验证,采用了基于流的传输,基于流的传输并不认为消息是一条一条的,是无保护消息边界的。
  6. UDP则是面向消息传输的,是有保护消息边界的,接收方一次只接收一条独立的消息,所以不存在黏包问题。
17.1 TCP与UDP的区别?

1、基于连接与无连接;
2、对系统资源的要求(TCP较多,UDP少);
3、UDP程序结构较简单;
4、流模式与数据报模式 ;
5、TCP保证数据正确性,UDP可能丢包;
6、TCP保证数据顺序,UDP不保证。

17.1_1 UDP的一些基本知识
  1. 在这里插入图片描述
    UDP基础知识

  2. UDP的基本字段?
    在这里插入图片描述

  3. UDP最长可以传输多少个字节?
    UDP数据报的长度是指包括报头和数据部分在内的总字节数,其中报头长度固定,数据部分可变。 数据报的最大长度根据操作环境的不同而各异。 从理论上说,包含报头在内的数据报的最大长度为65535字节(64K)。

17.2 TCP如何保证可靠性?
  1. 序列号和确认应答信号
  2. 超时重发控制
  3. 滑动窗口管理
  4. 流量控制
  5. 拥塞控制

在这里插入图片描述
在这里插入图片描述

18.TCP/IP协议族

在这里插入图片描述

该协议族涉及应用层,传输层,网络层,数据访问层。
应用层
1.HTTP
2.TFTP(简单文件传输协议)
3.Telnet
4.DNS(将域名转为IP地址)

网络层
1.IP:通过IP地址,保证了联网设备的唯一性,实现了网络通信的面向无连接和不可靠的传输功能.
2.ICMP:a. 确认IP包是否成功到达目标地址。b. 通知在发送过程中IP包被丢弃的原因.
3.ARP(地址解析协议)根据IP地址获得物理地址(mac地址)
4.RARP

18.1 IP协议详解
18.2 ARP

19. 请说一下C/C++ 中指针和引用的区别?

名字,sizeof,使用,初始化,引用,const,多级

  1. 名字的含义:指针指向的是一个具体的空间,而引用只是一个别名。
  2. sizeof的含义:sizeof§大小依据系统不同,会有所不同,32位的系统,指针的大小为4.而引用的sizeof的大小一般是被引用对象的大小。
  3. 初始化:指针可以被初始化为NULL,而引用只能被初始化为一个已有对象的引用。
  4. 使用:作为参数传递时,指针需要解引用才可以对对象进行操作,而直接对引用的修改都会更改引用所指向的对象。
  5. 可以有const指针,但是没有const引用。
  6. 指针在使用过程中,可以指向其他对象,而引用只能是一个对象的引用,不能被改变。
  7. 指针可以有多级指针,引用只能有一个引用。
19.1 引用的底层原理

引用即别名,它并非对象,相反地,它只是一个已经存在的对象所起的另外一个名字。
引用在C++中是通过一个指针常量(就是const修饰的指针是常量)来实现的,即&b=a实际上等价于int* const b=&a,而编译器会把&b编译为:&(*b),那么得到的自然就是a的地址,所以我们会看到&a、&b得到的地址是一样的。但是一定要注意,&b并不是b的地址。

参考

20.Http详解

20.0 Http密码学基础
  1. 密码学基础
  1. 明文:指的是未被加密过的原始数据

  2. 密文:明文被某种加密算法加密后,会变成密文,从而确保原始数据的安全。

  3. 秘钥:秘钥是一种参数,在明文转密文或密文转明文的算法中输入的参数。 秘钥分为对称秘钥与非对称秘钥,分别应用在对称机密和非对称加密上。

对称加密算法的加密过程如下:明文 + 加密算法 + 私钥 => 密文
解密过程如下:密文 + 解密算法 + 私钥 => 明文

非对称加密算法的过程如下:明文 + 加密算法 + 公钥 => 密文, 密文 + 解密算法 + 私钥 => 明文
被私钥加密过的密文只能被公钥解密,过程如下:
明文 + 加密算法 + 私钥 => 密文, 密文 + 解密算法 + 公钥 => 明文

4)对称与非对称加密算法的优缺点:
对称加密的优点:对称加密的特点是算法公开、加密和解密速度快,适合于对大数据量进行加密。
对称加密的缺点:对称加密的缺点是密钥安全管理困难
应用对称加密的:DES、3DES、TDEA、Blowfish、RC5和IDEA

非对称加密的优点:秘钥管理相对简单。
非对称加密的缺点:加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
应用非对称加密的:RSA、Elgamal、Rabin、D-H、ECC(椭圆曲线加密算法)等

  1. SSL:Secure Sockets Layer:安全套接层协议
    TLS:Transport Layer Security:安全传输层协议
20.1 HTTPS加密过程详解

在这里插入图片描述

定义:
HTTPS在传统的HTTP和TCP之间加了一层用于加密解密的SSL/TLS层(安全套接层Secure Sockets Layer/安全传输层Transport Layer Security)层。使用HTTPS必须要有一套自己的数字证书(包含公钥和私钥)。
HTTPS解决的问题:窃听,篡改,冒充
基本都是明文传输带来的弊端

  1. 窃听风险:信息加密传输:第三方无法窃听;(混合加密)信息加密
  2. 篡改风险:一旦被篡改,通信双方会立刻发现;(摘要 算法)校验机制
  3. 冒充风险:防止身份被冒充。(服务器公钥放到数字证书之中)身份证书
    HTTPS加密过程:
  4. 客户端请求服务器获取证书公钥
  5. 客户端(SSL/TLS)解析证书(无效会弹出警告)
  6. 生成随机值
  7. 用公钥加密随机值生成密钥
  8. 客户端将密钥发送给服务器
  9. 服务端用私钥解密秘钥得到随机值
  10. 将信息和随机值混合在一起进行对称加密
  11. 将加密的内容发送给客户端
  12. 客户端用密钥解密信息
    在这里插入图片描述
    Https还有什么安全性问题?
20.2 为什么 HTTPS 需要 7 次握手以及 9 倍时延

TCP 协议 — 通信双方通过三次握手建立 TCP 连接4;
TLS 协议 — 通信双方通过四次握手建立 TLS 连接5;
HTTP 协议 — 客户端向服务端发送请求,服务端发回响应;

20.3 三次握手哪一次可以传输数据

TCP标准规定,第三次握手的报文,可以携带数据。因为此时客户端已经处于established状态了呀。

20.4 Http与Https的区别?

超文本传输协议http协议一般用于浏览器与网站服务器之间传递信息。http以明文的方式发送数据,不提供任何方式的加密,若攻击者获取到该信息,则可直接进行解密,并读懂其中的信息。因为,http不适合于传输一些敏感信息,比如:信号卡号,密码等敏感信息。
为解决这种问题,就引入了https,安全套接字层超文本传输协议,为了传输数据的安全,https在http的基础上加入了SSL协议.SSL协议依靠证书来验证服务器的身份,并为浏览器和服务器之间的传输加密。

20.5 https公钥存在哪?

HTTPS 中服务器公钥存放在服务器上,浏览器请求时由服务器返回。 验证证书合法性时候使用的是签发者公钥、和服务器公钥是不同的。 验证签发者合法性的使用的是其上一级签发者公钥,直到根证书,内置在系统中。

20.6 http 2.0主要有哪些特性?

http1.1 = http1.0+长连接+支持管道网络传输
http2.x= http1.1+ssl/tls+头部压缩算法+二进制格式+数据流传输方式+多路复用(并发多路请求或回复)

  1. 大幅提高web性能,减少网络延迟
  2. 在应用层和传输层之间增加了一个二进制分帧层
  3. 使用了多路复用。基于二进制分帧层,Http2.0可以在共享TCP链接的基础上同时发送请求和响应,Http消息被分解为独立的帧。但不破坏他原来的语义,交错发出去,在另一端根据流标识符和首部将他们重新组装起来。
  4. 设置请求优先级。

http2.0存在的问题:
HTTP/2 主要的问题在于,多个 HTTP 请求在复⽤⼀个 TCP 连接,下层的 TCP 协议是不知道有多少个 HTTP 请求的。所以⼀旦发⽣了丢包现象,就会触发 TCP 的᯿传机制,这样在⼀个 TCP 连接中的所有的 HTTP 请求都必须等待这个丢了的包被重传回来。

20.7 为什么需要http?

HTTP连接 = 以HTTP协议为通信协议的TCP连接。
TCP/IP协议可以两个进程通过三次握手建立稳定的通信信道,发送字节流;而HTTP协议建立在TCP/IP协议之上,也就是说TCP/IP协议可以让两个程序说话,而HTTP协议定义了说话的规则。
我们在传输数据时,可以只使用TCP/IP协议进行传输,但是这样没有应用层的参与,会导致两端无法识别数据内容,这样传输的数据也就没有意义了。因此如果想让传输的数据有意义,那么就必须要用到应用层的协议(HTTP、FTP、TELNET等),也可以自己定义应用层的协议。
WEB使用HTTP协议作为应用层的协议,以封装HTTP文本信息,然后使用TCP/IP作为传输层的协议将它发到网络上。

20.8 http状态码?301、302、403、404具体含义?

在这里插入图片描述

302:临时重定向
403:服务器拒绝请求,
500:服务器内部错误
200:请求成功
404:页面无法找到
502:服务器网关错误

  1. https://blog.csdn.net/a6864657/article/details/80934213
20.9 http获取消息的几种形式?

1.HEAD 向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。 
2.GET 向特定的资源发出请求。注意:GET方法不应当被用于产生“副作用”的操作中,例如在web app.中。其中一个原因是GET可能会被网络蜘蛛等随意访问。 
3.POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。 
4.PUT 向指定资源位置上传其最新内容。 
5.DELETE 请求服务器删除Request-URI所标识的资源。 
6.TRACE 回显服务器收到的请求,主要用于测试或诊断。 
7.CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
8.OPTIONS 返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送’*'的请求来测试服务器的功能性。

20.10 Http1.1如何优化?

在这里插入图片描述

20.11 Http的消息长什么样?

客户端请求消息
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求体四个部分组成,下图给出了请求报文的一般格式。
在这里插入图片描述
实际的报文如下
在这里插入图片描述
①是请求方法,GET和POST是最经常使用的请求方法,此外还有DELETE,HEAD,OPTIONS,PUT,TARCE.不够,目前的大部分浏览器只支持GET和POST。
②为请求对应的URL地址,它和报文头的Host属性组成完整的请求URL,③是协议名称及版本号。

④是HTTP的报文头,报文头包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。

⑤是报文体,它将一个页面表单中的组件值通过param1=value1&param2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.html? param1=value1&param2=value2”的方式传递请求参数。
服务器响应消息
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

20.11_1.HTTP请求格式,请求头、请求行、请求体都有什么东西

参考1

20.12 SSL/TLS

在这里插入图片描述

TLS/SSL握手的过程
在这里插入图片描述

1.“Client Hello”:客户端发起握手请求,消息包含了客户端所支持的TLS版本和密码组合以供服务器进行选择,还有一个“Client random”字符串。
2. “Server Hello”:服务端发送Server Hello消息对客户端进行回应。消息包含了数字证书,服务器选择的密码组合,“Server Random”字符串。
3. “验证”:客户端对服务端发来的证书进行验证,确保对方的合法身份。
a. 检查数字签名
b. 验证证书链
c. 验证证书的有效状态
d. 检查证书的回撤状态
4. “premaster secret"字符串:客户端向服务器发送另一个随机字符串"premaster secret (预主密钥)”,这个字符串是经过服务器的公钥加密过的,只有对应的私钥才能解密。
5.使用私钥:服务器使用私钥解密"premaster secret"。
6. 生成共享密钥:客户端和服务器均使用 client random,server random 和 premaster secret,并通过相同的算法生成相同的共享密钥 KEY。
7. 客户端就绪:客户端发送经过共享密钥 KEY加密过的"finished"信号。
8. 服务器就绪:服务器发送经过共享密钥 KEY加密过的"finished"信号。
9. 达成安全通信:握手完成,双方使用对称加密进行安全通信。

几个概念,阐述一下

  1. CA:由于不能像非对称加密那么喽去直接发送公钥。服务器将公钥注册在受信任的第三方,然后服务器将公钥连同证书发给客户端,私钥则有服务器自己保存确保安全。

1.参考1

20.12 Https的性能优化

在这里插入图片描述

20.13 https整个握手交互的过程总共花了多少rtt?
  1. 参考1
20.14 https还有什么安全问题?

在这里插入图片描述

20.15 一次HTTP请求,程序一般经历了哪几个步骤?

1)解析域名 -> 2)发起TCP三次握手,建立连接 -> 3)基于TCP发起HTTP请求 -> 4)服务器响应HTTP请求,并返回数据 -> 5)客户端解析返回数据

20.16 session和cookie有什么区别?

朋友:1)存储位置不同,cookie是保存在客户端的数据;session的数据存放在服务器上

朋友:2)存储容量不同,单个cookie保存的数据小,一个站点最多保存20个Cookie;对于session来说并没有上限

朋友:3)存储方式不同,cookie中只能保管ASCII字符串;session中能够存储任何类型的数据

朋友:4)隐私策略不同,cookie对客户端是可见的;session存储在服务器上,对客户端是透明的

朋友:5)有效期上不同,cookie可以长期有效存在;session依赖于名为JSESSIONID的cookie,过期时间默认为-1,只需关闭窗口该session就会失效

朋友:6)跨域支持上不同,cookie支持跨域名访问;session不支持跨域名访问

21. 你知道什么树?

21.1完全二叉树

完全二叉树是一种特殊的二叉树,满足以下要求:
所有叶子节点都出现在 k 或者 k-1 层,而且从 1 到 k-1 层必须达到最大节点数;
第 k 层可以不是满的,但是第 k 层的所有节点必须集中在最左边。
需要注意的是不要把完全二叉树和“满二叉树”搞混了,完全二叉树不要求所有树都有左右子树,但它要求:
任何一个节点不能只有右子树没有左子树
叶子节点出现在最后一层或者倒数第二层,不能再往上
用一张图对比下“完全二叉树”和“满二叉树”:
在这里插入图片描述
当我们用数组实现一个完全二叉树时,叶子节点可以按从上到下、从左到右的顺序依次添加到数组中,然后知道一个节点的位置,就可以轻松地算出它的父节点、孩子节点的位置。
以上面图中完全二叉树为例,标号为 2 的节点,它在数组中的位置也是 2,它的父节点就是 (k/2 = 1),它的孩子节点分别是 (2k=4) 和 (2k+1=5),别的节点也是类似。
完全二叉树使用场景:
根据前面的学习,我们了解到完全二叉树的特点是:“叶子节点的位置比较规律”。因此在对数据进行排序或者查找时可以用到它,比如堆排序就使用了它,后面学到了再详细介绍。

21.2二叉查找树(二叉排序树)

二叉树的提出其实主要就是为了提高查找效率,比如我们常用的 HashMap 在处理哈希冲突严重时,拉链过长导致查找效率降低,就引入了红黑树。

我们知道,二分查找可以缩短查找的时间,但是它要求 查找的数据必须是有序的。每次查找、操作时都要维护一个有序的数据集,于是有了二叉查找树这个概念。

二叉查找树(又叫二叉排序树),它是具有下列性质的二叉树:
若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
左、右子树也分别为二叉排序树。
在这里插入图片描述
也就是说,二叉查找树中,左子树都比节点小,右子树都比节点大,递归定义。
根据二叉排序树这个特点我们可以知道:二叉排序树的中序遍历一定是从小到大的。
比如上图,中序遍历结果是:
1 3 4 6 7 8 10 13 14

二叉排序树(二叉查找树)的性能
在最好的情况下,二叉排序树的查找效率比较高,是 O(logn),其访问性能近似于折半查找;

但最差时候会是 O(n),所谓O(n)的含义是算法的运行时间与输入规模呈比例,若输出规模为N,花费时间为T,则若输出规模为2N,则花费时间为2T.比如插入的元素是有序的,生成的二叉排序树就是一个链表,这种情况下,需要遍历全部元素才行(见下图 b)。
在这里插入图片描述
如果我们可以保证二叉排序树不出现上面提到的极端情况(插入的元素是有序的,导致变成一个链表),就可以保证很高的效率了。

但这在插入有序的元素时不太好控制,按二叉排序树的定义,我们无法判断当前的树是否需要调整。

因此就要用到平衡二叉树(AVL 树)了。

21.3平衡二叉树

注:平衡二叉树的性质是左右子树的高度差不大于1。二叉搜索树的

平衡二叉树的提出就是为了保证树不至于太倾斜,尽量保证两边平衡。因此它的定义如下:
平衡二叉树要么是一棵空树
要么保证左右子树的高度之差不大于 1
子树也必须是一颗平衡二叉树
也就是说,树的两个子树的高度差别不会太大。
那我们接着看前面的极端情况的二叉排序树,现在用它来构造一棵平衡二叉树。
以 12 为根节点,当添加 24 为它的右子树后,根节点的左右子树高度差为 1,这时还算平衡,这时再添加一个元素 28:
在这里插入图片描述
这时根节点 12 觉得不平衡了,我左孩子一个都没有,右边都有俩了,超过了之前说的最大为 1,不行,给我调整!
于是我们就需要调整当前的树结构,让它进行旋转。
因为最后一个节点加到了右子树的右子树,就要想办法给右子树的左子树加点料,因此需要逆时针旋转,将 24 变成根节点,12 右旋成 24 的左子树,就变成了这样(有点丑哈哈):
在这里插入图片描述
依次类推,平衡二叉树在添加和删除时需要进行旋转保持整个树的平衡,内部做了这么复杂的工作后,我们在使用它时,插入、查找的时间复杂度都是 O(logn),性能已经相当好了。

21.4 avl树是什么结构,让我说一下怎么插入,插入后怎么旋转。

AVL树是平衡二叉树。
满足以下性质的平衡二叉树:

1、左右子树都是AVL树
2、左右子树的高度之差的绝对值不超过1

bf = 右子树的个数-左子树的个数
若bf<0—>右旋转
若bf>0---->左旋转

22. C语言中的strlen与sizeof的区别

sizeof与strlen是有着本质的区别,sizeof是求数据类型所占的空间大小,而strlen是求字符串的长度,字符串以/0结尾。

24. Redis

24.1 Redis了解吗?说一下有多少种类型

答:共有5种。String ,list,set,zset,hash。
**String:**这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。
list:使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。
set:因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
hash:这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
sorted set:sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。

25. C++ 友元是什么

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend,如下所示
优点
通常对于普通函数来说,要访问类的保护成员是不可能的,如果想这么做那么必须把类的成员都声明成为public(共用的),然而这做带来的问题遍是任何外部函数都可以毫无约束的访问它操作它,c++利用friend修饰符,可以让一些你设定的函数能够对这些保护数据进行操作,避免把类成员全部设置成public,最大限度的保护数据成员的安全。

缺点
友元能够使得普通函数直接访问类的保护数据,避免了类成员函数的频繁调用,可以节约处理器开销,提高程序的效率,但所矛盾的是,即使是最大限度大保护,同样也破坏了类的封装特性,这即是友元的缺点,在现在cpu速度越来越快的今天我们并不推荐使用它,但它作为c++一个必要的知识点,一个完整的组成部分,我们还是需要讨论一下的。 在类里声明一个普通数学,在前面加上friend修饰,那么这个函数就成了该类的友元,可以访问该类的一切成员。

26. LRU是什么,怎么实现的LRU,到具体细节,代码题也要会。

LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

27. 问我编译型语言和解释型语言的区别

  1. 用编译型语言写的程序执行之前,需要一个专门的编译过程,通过编译系统(不仅仅只是通过编译器,编译器只是编译系统的一部分)把高级语言翻译成机器语言(具体翻译过程可以参看下图),把源高级程序编译成为机器语言文件,比如windows下的exe文件。以后就可以直接运行而不需要编译了,因为翻译只做了一次,运行时不需要翻译,所以编译型语言的程序执行效率高,但也不能一概而论

  2. 解释则不同,解释型语言编写的程序不需要编译。解释型语言在运行的时候才翻译,比如VB语言,在执行的时候,专门有一个解释器能够将VB语言翻译成机器语言,每个语句都是执行的时候才翻译。这样解释型语言每执行一次就要翻译一次,效率比较低。
    在这里插入图片描述

28. C++编译器有哪些,区别在哪,只知道gcc和g++和他们的区别

编译器有哪些
GCC
Clang
MSVC
G++
gcc
实际上,只要是 GCC 支持编译的程序代码,都可以使用 gcc 命令完成编译。可以这样理解,gcc 是 GCC 编译器的通用编译指令,因为根据程序文件的后缀名,gcc 指令可以自行判断出当前程序所用编程语言的类别。
xxx.c:默认以编译 C 语言程序的方式编译此文件;
xxx.cpp:默认以编译 C++ 程序的方式编译此文件。
xxx.m:默认以编译 Objective-C 程序的方式编译此文件;
xxx.go:默认以编译 Go 语言程序的方式编译此文件;
当然,gcc 指令也为用户提供了“手动指定代表编译方式”的接口,即使用 -x 选项。例如,gcc -xc xxx 表示以编译 C 语言代码的方式编译 xxx 文件;而 gcc -xc++ xxx 则表示以编译 C++ 代码的方式编译 xxx 文件。有关 -x 选项的用法,后续会给出具体样例。
g++
但如果使用 g++ 指令,则无论目标文件的后缀名是什么,该指令都一律按照编译 C++ 代码的方式编译该文件。也就是说,对于 .c 文件来说,gcc 指令以 C 语言代码对待,而 g++ 指令会以 C++ 代码对待。但对于 .cpp 文件来说,gcc 和 g++ 都会以 C++ 代码的方式编译。

除此之外对于编译执行 C++ 程序,使用 gcc 和 g++ 也是有区别的。要知道,很多 C++ 程序都会调用某些标准库中现有的函数或者类对象,而单纯的 gcc 命令是无法自动链接这些标准库文件的。

29. 各种排序

插入排序:对于一个带排序数组来说,其初始有序数组元素个数为1,然后从第二个元素,插入到有序数组中。对于每一次插入操作,从后往前遍历当前有序数组,如果当前元素大于要插入的元素,则后移一位;如果当前元素小于或等于要插入的元素,则将要插入的元素插入到当前元素的下一位中。

希尔排序:先将整个待排序记录分割成若干子序列,然后分别进行直接插入排序,待整个序列中的记录基本有序时,在对全体记录进行一次直接插入排序。其子序列的构成不是简单的逐段分割,而是将每隔某个增量的记录组成一个子序列。希尔排序时间复杂度与增量序列的选取有关,其最后一个值必须为1.

归并排序:该算法采用分治法;对于包含m个元素的待排序序列,将其看成m个长度为1的子序列。然后两两合归并,得到n/2个长度为2或者1的有序子序列;然后再两两归并,直到得到1个长度为m的有序序列。

冒泡排序:对于包含n个元素的带排序数组,重复遍历数组,首先比较第一个和第二个元素,若为逆序,则交换元素位置;然后比较第二个和第三个元素,重复上述过程。每次遍历会把当前前n-i个元素中的最大的元素移到n-i位置。遍历n次,完成排序。

快速排序:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

选择排序:每次循环,选择当前无序数组中最小的那个元素,然后将其与无序数组的第一个元素交换位置,从而使有序数组元素加1,无序数组元素减1.初始时无序数组为空。

堆排序:堆排序是一种选择排序,利用堆这种数据结构来完成选择。其算法思想是将带排序数据构造一个最大堆(升序)/最小堆(降序),然后将堆顶元素与待排序数组的最后一个元素交换位置,此时末尾元素就是最大/最小的值。然后将剩余n-1个元素重新构造成最大堆/最小堆。

各个排序的时间复杂度、空间复杂度及稳定性如下:
在这里插入图片描述
(注意快排的空间复杂度是O(logN)~O(N),平均是O(logN),也就是每次都划分在中点上时,递归的深度)

29.1 排序算法集合

1、冒泡排序:
从数组中第一个数开始,依次遍历数组中的每一个数,通过相邻比较交换,每一轮循环下来找出剩余未排序数的中的最大数并“冒泡”至数列的顶端。
稳定性:稳定
平均时间复杂度:O(n ^ 2)
code

class Solution {
public:
	vector<int> sortArray(vector<int>& nums)//冒泡排序 	
	{
		int n = nums.size();
		for (int i = 0; i < n; i++)
		{
			bool flag = false;
			for (int j = n - 1; j > i; j--)
			{
				if (nums[j] > nums[j - 1])//不断的比较相邻的两个数,如果比较到i的位置,都还没有出现过一次交换,那么证明已经该数据已经是有序的了,不用再比较了 		
				{
					swap(nums[j], nums[j - 1]);
					flag = true;
				}
			}
			if (flag == false)
				return nums;
		}
	}
}

2、插入排序:
从待排序的n个记录中的第二个记录开始,依次与前面的记录比较并寻找插入的位置,每次外循环结束后,将当前的数插入到合适的位置。
稳定性:稳定
平均时间复杂度:O(n ^ 2)
code

class Solution {
public:	vector<int> sortArray(vector<int>& nums) {
	int n = nums.size();
	for (int i = 1; i < n; i++)
	{
		int cur = nums[i];//这到哪一位,就能将其排到最适合的位置,从而结束这个排序 		
		int index = i - 1;
		while (index >= 0 && cur < nums[index])
		{
			nums[index + 1] = nums[index];
			index--;
		}
	}
	nums[index + 1] = cur;
}
	  return nums;
}; /* 1 4 2 3 5cur = 2;index = 1;num[2] = num[1](4) 1 2 4 3 51 2 3 4 5 */

3、希尔排序(缩小增量排序):
希尔排序法是对相邻指定距离(称为增量)的元素进行比较,并不断把增量缩小至1,完成排序。
希尔排序开始时增量较大,分组较多,每组的记录数目较少,故在各组内采用直接插入排序较快,后来增量di逐渐缩小,分组数减少,各组的记录数增多,但由于已经按di−1分组排序,文件叫接近于有序状态,所以新的一趟排序过程较快。因此希尔 排序在效率上比直接插入排序有较大的改进。

在直接插入排序的基础上,将直接插入排序中的1全部改变成增量d即可,因为希尔排序最后一轮的增量d就为1。
稳定性:不稳定
平均时间复杂度:希尔排序算法的时间复杂度分析比较复杂,实际所需的时间取决于各次排序时增量的个数和增量的取值。时间复杂度在O(n ^ 1.3)到O(n ^ 2)之间。
code

public:	vector<int> sortArray(vector<int>& nums) {
	int n = nums.size();
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//先划定一个范围 			
		for (int i = gap; i < n; i++)
		{
			int cur = nums[i];
			int index = i - gap;
			while (index >= 0 && cur < nums[index])//这个操作可以把nums中把gap作为间隔的地方全部都排好序 	
			{
				nums[index + gap] = nums[index];
				index = index - gap;
			}
			nums[index + gap] = cur;
		}
	}
	return nums;
}

4、选择排序:
从所有记录中选出最小的一个数据元素与第一个位置的记录交换;然后在剩下的记录当中再找最小的与第二个位置的记录交换,循环到只剩下最后一个数据元素为止。
稳定性:不稳定
平均时间复杂度:O(n ^ 2)
5、快速排序
1)从待排序的n个记录中任意选取一个记录(通常选取第一个记录)为分区标准;
2)把所有小于该排序列的记录移动到左边,把所有大于该排序码的记录移动到右边,中间放所选记录,称之为第一趟排序;
3)然后对前后两个子序列分别重复上述过程,直到所有记录都排好序。
稳定性:不稳定
平均时间复杂度:O(nlogn)
6、堆排序:
堆:
1、完全二叉树或者是近似完全二叉树。
2、大顶堆:父节点不小于子节点键值,小顶堆:父节点不大于子节点键值。左右孩子没有大小的顺序。
堆排序在选择排序的基础上提出的,步骤:
1、建立堆
2、删除堆顶元素,同时交换堆顶元素和最后一个元素,再重新调整堆结构,直至全部删除堆中元素。
稳定性:不稳定
平均时间复杂度:O(nlogn)

核心思想:
将输入分为已排序的和未排序的区域。它通过提取未排序的区域内最大的元素 并将其移到已排序的区域来迭代缩小未排序的区域。其实一种非稳定排序.

堆排序的基本思路:

  1. 将无序序列构建成一个堆,根据升序需求选择大顶堆还是小顶堆。
  2. 将堆顶元素与末尾元素进行交换,将最大元素沉到数组末端。
  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整,交换的操作。

代码:
code

#include <stdio.h>
#include <iostream>
#include <vector>
#include <time.h>
#include <Windows.h>
using namespace std;
//建堆是核心 
void Heap_build(int a[],int root,int len)
{
	int lchild = root*2+1;
	if(lchild<len)
	{
		int flag = lchild;//flag保存的是根节点的左右节点最大值的下标 
		int rchild = lchild+1;//根节点的右子节点下标
		if(rchild<len)
		{
			if(a[rchild]>a[flag])//找出左右子节点的最大值 
			{
				flag = rchild; 
			}	
		} 
		if(a[root]<a[flag])
		{
			swap(a[root],a[flag]);
			Heap_build(a,flag,len); //不断更改根节点的位置。 
		 } 
		
	}
 } 

void Heap_sort(int a[],int len)
{
	for(int i = len/2;i>=0;i--)//第一遍建堆 
	{
		Heap_build(a,i,len); 
	} 
	for(int j = len-1;j>=0;j--)
	{
		swap(a[0],a[j]);//交换那两个元素 
		Heap_build(a,0,j); //继续堆排序,不过,排除已经建好堆的数组 
	}
 } 
int main()
{
	clock_t Start_time = clock();
	int a[10] = {12,45,748,12,56,3,89,4,48,2};
	Heap_sort(a,10);
	for(int j = 0;j<10;j++)
	{
		cout<<a[j]<<" ";
	 } 
	clock_t end_time = clock();
	cout<<endl;
	cout<<"Total_Running Time"<<static_cast<double>(end_time-Start_time)/CLOCKS_PER_SEC*1000<<" ms"<<endl;
	return 0;
 } 

7、归并排序:

采用分治思想,现将序列分为一个个子序列,对子序列进行排序合并,直至整个序列有序。
稳定性:稳定
平均时间复杂度:O(nlogn)
code

void Merge(int arr[], int low, int mid, int high)
{	//low为第一有序区的第一个元素,i指向第i个元素,mid指向第一有序区的最后一个元素
	int i = low, j = mid + 1, k = 0;//mid+1为第二有序区的第一个元素,j指向第二有序区的第二个元素	
	while (i <= mid && j <= high)
	{
		if (arr[i] <= arr[j])
			temp[k++] = arr[i++];
		else
			temp[k++] = arr[j++];
	}
	while (i <= mid)//若比较完后,第一个有序区仍有剩余,则直接复制到temp数组中 	
	{
		temp[k++] = arr[i++];
	}
	while (j <= high)
		temp[k++] = arr[j++];
	for (i = low, k = 0; i <= high; i++, k++)
		arr[i] = temp[k];
	delete[] temp;
}

8、计数排序:
思想:如果比元素x小的元素个数有n个,则元素x排序后位置为n+1。
步骤:
1)找出待排序的数组中最大的元素;
2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
稳定性:稳定
时间复杂度:O(n+k),k是待排序数的范围。

9、桶排序:
步骤:
1)设置一个定量的数组当作空桶子; 常见的排序算法及其复杂度:
2)寻访序列,并且把记录一个一个放到对应的桶子去;
3)对每个不是空的桶子进行排序。
4)从不是空的桶子里把项目再放回原来的序列中。
时间复杂度:O(n+C) ,C为桶内排序时间。

29.1 快排(算一下快排复杂度,计算过程)

**快排的思想:**首先,快排是选出一个基准元素,一般选择第一个元素。然后,通过一趟排序,将数组分为两个部分,一部分比该元素小,一部分比该元素大。然后,再按此方法对这两部分分别进行递归。
快速排序算法的时间复杂度为O(nlogn)
code

int once_quick_sort(vector<int>& data, int left, int right)
{
	int key = data[left];
	while (left < right)
	{
		while (left < right && key <= data[right])
		{
			right--;
		}
		if (left < right)
		{
			data[left] = data[right]; 
            left++;
		}
		while (left < right && key > data[left])
		{
		 	left++;
		}
		if (left < right)
		{
			data[right] = data[left]; 
            right--;
		}
	}
	data[left] = key;
	return left;
}
int quick_sort(vector<int>& data, int left, int right)
{
	if (left >= right)
	{
		return 1;
	}
	int middle = 0;
	middle = once_quick_sort(data, left, right);
	quick_sort(data, left, middle - 1);
	quick_sort(data, middle + 1, right);
}

快排的优化方式

  1. 选择基准的方式
    若每次划分都能划分成等长的两个序列,那么分治效率将会达到最大。下面介绍三种基准的选取方式
    a. 取序列的第一个或最后一个
    这种效率很不好,若数组原本就是有序的,则会导致算法退化成为O(n^2)
    b. 随机选择基准
    可以达到O(Nlogn)
    c. 最佳的是选取序列的中间的值,但这很难得到,所以我们一般选取头,尾,中三数 的中值作为枢纽元。
  2. 其他优化方式:
    a. 待排序序列的长度分割到一定大小后,使用插入排序.
    b. 在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割.
    c. 优化递归操作:快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化。

最好的方式是:三数取中+插排+聚集相等元素,它和STL中的Sort函数效率差不多

尾递归
尾递归和一般的递归不同在对内存的占用,普通递归创建stack累积而后计算收缩,尾递归只会占用恒量的内存(和迭代一样)。
code

def recsum(x):
  if x == 1:
    return x
  else:
    return x + recsum(x - 1)

尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。尾调用不一定出现在函数尾部,只要是最后一步操作即可。

29.2 堆排(算一下复杂度)

堆排的思想:利用堆进行排序的思想,将待排序的序列构成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点,再将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构建成一个大顶堆,这样就是得到这n个元素序列的次小值,如此反复进行,便能得到一个有序序列。

堆排序的时间复杂度为O(n*log(n)), 非稳定排序,原地排序(空间复杂度O(1))。
在这里插入图片描述

code

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
void adjust(int arr[],int len,int index)
{
	int left = 2*index+1;
	int right = 2*index+2;
	int maxIdx = index;
	if(left<len&&arr[left]>arr[maxIdx])
		maxIdx = left;
	if(right<len&&arr[right]>arr[maxIdx])
		maxIdx = right;
	if(maxIdx!=index)
	{
		swap(arr[maxIdx],arr[index]);
		adjust(arr,len,maxIdx);
	}
}


void heapSort(int arr[],int size)
{
	for(int i = size/2;i>=0;i++)
		adjust(arr,size,i);
	for() 
}

int main()
{
	int array[8] = {8,1,14,3,21,5,7,10};
	heapSort(array,8);
	for(auto it:array)
	{
		cout<<it<<endl;
	}
	return 0;
}

30. Time_wait的作用(close_wait是什么情况出现。)

原因1
为了保证客户端发送的最后一个ack报文段能够安全,准确的到达服务端。因为这ack报文是可能会丢失的,然后服务器就会超时重传第三次挥手的fin报文,然后客户端再重传一次第四次挥手的ack报文。
若没有这2msl的Time_Wait,客户端直接关闭的话,就会出现客户端接收不到服务端发送来的fin信息。(实际上,应该是客户端接收到一个非法的报文,返回一个RST的数据报,表明拒绝此次通信,然后双方就产生异常,而不是收不到。)那么服务器就不能按照正常状态进行close状态。那么就会耗费服务器的资源。

原因2
在第四次挥手后,2msl的时间足以让本次连接的所有报文段都从网络中消息了,这样下一次新的连接中,肯定就不会出现旧连接的报文段了。防止已经失效的连接请求报文段再次出现在本次连接之中。
若没有这个2msl的话,客户端直接结束,有可能之前有个迷失的syn信号在网络中,然后下一次连接又马上开始,此时syn信号到达对面,从而出现问题。

使用 TCP 协议通信的双方会在关闭连接时触发 TIME_WAIT 状态,关闭连接的操作其实是告诉通信的另一方自己没有需要发送的数据,但是它仍然保持了接收对方数据的能力。
在这里插入图片描述

  1. 当客户端没有待发送的数据时,它会向服务端发送 FIN 消息,发送消息后会进入 FIN_WAIT_1 状态;
  2. 服务端接收到客户端的 FIN 消息后,会进入 CLOSE_WAIT 状态并向客户端发送 ACK 消息,客户端接收到 ACK 消息时会进入 FIN_WAIT_2 状态;
  3. 当服务端没有待发送的数据时,服务端会向客户端发送 FIN 消息;
  4. 客户端接收到 FIN 消息后,会进入 TIME_WAIT 状态并向服务端发送 ACK 消息,服务端收到后会进入 CLOSED 状态;
    客户端等待两个最大数据段生命周期(Maximum segment lifetime,MSL)2的时间后也会进入 CLOSED 状态;

Time_Wait:是在客户端回复服务端的ACK消息后的一个等待过程。等待服务端将
CLOST_WAIT:是在服务端还有未发送的数据时,在等待完成数据的发送和处理。需要经过这一阶段。

30. 1.TIME_WAIT状态,时间是多少?

2MSL(MSL:报文在网络中的最大生存时间)
为什么是2MSL?
TIME_WAIT 等待 2 倍的 MSL,⽐较合理的解释是: ⽹络中可能存在来⾃发送⽅的数据包,当这些发送⽅的数据包被接收⽅处理后⼜会向对⽅发送响应,所以⼀来⼀回需要等待 2 倍的时间。
Time_Wait状态的主要目的有两个

  1. 优雅的关闭TCP连接,也就是被动关闭的一端收到它自己发出的FIN报文的ACK确认报文。
  2. 处理延迟的重复报文
    这段文字说明了TIME_WAIT状态持续2MSL的时间可以让一个TCP连接的两端发出的报文都从网络中消失,从而保证下一个使用了相同四元组的tcp连接不会被上一个连接的报文所干扰。防止一些旧连接的数据包以为网络延迟,重新发送过来,为后面的TCP的连接造成影响。

在 Linux 系统⾥ 2MSL 默认是 60 秒,那么⼀个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时
间为固定的 60 秒。

31. 二叉树前序后序中序遍历

32. 红黑树

其实平衡二叉树最大的作用就是查找,AVL树的查找、插入和删除在平均和最坏情况下都是O(logn)。AVL树的效率就是高在这个地方。如果在AVL树中插入或删除节点后,使得高度之差大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理, 那么创建一颗平衡二叉树的成本其实不小. 这个时候就有人开始思考,并且提出了红黑树的理论,那么红黑树到底比AVL树好在哪里?

32.1 红黑树与AVL树的比较:
  1. AVL树的时间复杂度虽然优于红黑树,但是对于现在的计算机,cpu太快,可以忽略性能差异
  2. 红黑树的插入删除比AVL树更便于控制操作
  3. 红黑树整体性能略优于AVL树(红黑树旋转情况少于AVL树)
  4. AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树不是高度平衡的,算是一种折中,插入最多两次旋转,删除最多三次旋转。
  5. 红黑树插入,删除,查找的时间复杂度都为 O(logn)
32.2 红黑树特性。

红黑树是一棵二叉搜索树,它在每个节点增加了一个存储位,用来记录节点的颜色,可以是红色,也可以是黑色。通过任意一条从根到叶子简单路径上颜色的约束。红黑树可以保证最长路径不超过最短路径的两倍。近似平衡。
具体性质如下:

  1. 每个节点颜色不是黑色就是红色。
  2. 根节点是黑色。
  3. 叶节点也是黑色。
  4. 若某节点是红色,那其子节点一定是黑色。
  5. 对于每个节点,从该节点到其后代叶节点的简单路径上。均包含相同数目的黑色节点。这个属性很重要这个,保证了上面的最长路径不超过最短路径的两倍的这个特性。
    在这里插入图片描述
32.3 红黑树查找速度。

红黑树是一种二叉查找树。可以在O(log n)时间内做查找,插入和删除。这里的n 是树中元素的数目。恢复红黑属性需要少量(O(log n))的颜色变更(这在实践中是非常快速的)并且不超过三次树旋转(对于插入是两次)。这允许插入和删除保持为 O(log n) 次,

32.4 红黑树的插入

红黑树插入节点过程大致分析:
RBTree为二叉搜索树,我们按照二叉搜索树的方法对其进行节点插入
RBTree有颜色约束性质,因此我们在插入新节点之后要进行颜色调整
具体步骤如下:
1.根节点为NULL,直接插入新节点并将其颜色置为黑色

  1. 根节点不为NULL,找到要插入新节点的位置
    3.插入新节点
  2. 判断新插入节点对全树颜色的影响,更新调整颜色

由于插入黑色节点的成本太高,所以,我们选择节点的颜色为红色。但若上一个节点也是红色的话,我们就需要依据情况进行调整 了。

32.5 红黑树与B+树的比较?

B树是为了提高磁盘或外部存储设备查找效率而设计的一种多路平衡二叉树。B+树为B树的变形结构,用于大多数数据库或文件系统的存储而设计的。
B树的定义
B树也称B-树,它是一颗多路平衡查找树。我们描述一颗B树时需要指定它的阶数,阶数表示了一个结点最多有多少个孩子结点,一般用字母m表示阶数。当m取2时,就是我们常见的二叉搜索树。
一棵m阶的二叉树的定义如下:

a. 每个节点最多有m-1个关键字。
b. 根节点最少可以只有一个关键字。
c. 非根节点至少有math.ceil(m/2)-1个关键字。
d. 每个节点的关键字都按照从小到大的顺序排列,每个关键字的左子树都小于它。而右子树的所有关键字都大于它。
e. 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。
在这里插入图片描述

B+树的定义
所谓B+树,就是关键字的个数比孩子节点个数小1.
在这里插入图片描述
B+树的其他要求
a. B+树包含两种类型的节点:内部节点(索引节点)和叶子节点。根节点本身即可以是内部节点,也可以是叶子节点。根节点的关键字的个数最少可以只有一个。
b. B+树与B树的不同点在于内部节点不保存数据,只用于索引,所有数据都保存在叶子节点中。
c. m阶B+树表示了内部结点最多有m-1个关键字。阶数m同时限制了叶子节点最多存储m-1个记录。
d. 内部结点中的key都要按照从小到大的顺序排列,对于内部结点中的一个key,左树中所有的key都小于它,右树结点的key都大于它。
e.每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。

B树的缺点
1.每个节点中既要存索引信息,又要存其对应的数据,如果数据很大,那么当树的体量很大时,每次读到内存中的树的信息就会不太够。
2.B树遍历整个树的过程和二叉树本质上是一样的,B树相对二叉树虽然提高了磁盘IO性能,但并没有解决遍历元素效率低下的问题。

B树和B+树的区别

  1. B+树中只有叶子节点会带有指向记录的指针,而B树则所有节点都带有,在内部节点出现的索引项不会再出现在叶子节点中。
  2. B+树中所有叶子节点都是通过指针连接在一起,而B树不会。

B+树的优点

  1. 非叶子节点不会带上指向记录的指针,这样,一个块中可以容纳更多的索引项,一是可以降低树的高度。二是一个内部节点可以定位更多的叶子节点。B+树的磁盘读写代价更低。
  2. 叶子节点之间通过指针来连接,范围扫描将十分简单,而对于B树来说,则需要在叶子节点和内部节点不停的往返移动。具体的来讲,如何想扫描一次所有数据,对于b+树来说,可以从因为他们的叶子结点是连在一起的,所以可以横向的遍历过去。而对于b-树来说,就这能中序遍历了。B+树的数据信息遍历更加方便
  3. B+树的查询效率更加稳定。

B树相对于红黑树的区别
B树相对于红黑树的差别在大规模 数据存储的时候,红黑树往往由于深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况。
因为我们知道磁盘IO的代价主要花费在查找所需的柱面上,树的深度过大会造成磁盘IO频繁读写。所以,根据磁盘查找存取的次数往往由树的高度所决定,所以,只要我们通过某种较好的树结构减少树的高度,B树可以有多个子女,从几十到上千,降低树的高度

红黑树 和 b+树的用途有什么区别?
红黑树多用在内部排序,即全放在内存中的,STL的map和set的内部实现就是红黑树。

B+树多用于外存上时,B+也被变成一个磁盘友好的数据结构。

B树参考1

33. 什么导致了粘包?

发送端原因:由于使用Nagle算法,就多个小的数据包合成一个,一起发送,导致客户端接收到信息无法区分哪些数据包是客户端自己发送的。
接收端原因:接收到的数据存放在缓存中,若消息没有被及时取走,则会出现一次取出多个数据包,造成粘包的现象。

粘包问题主要还是因为接收方不知道接收的消息的界限,不知道一次性需要提取多少个字节造成的。
发送端原因: 由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(tcp在实现的时候,为了解决大量小报文场景下包头比负载大,导致传输性价比太低的问题,专门设计的。)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包.
• 接收端原因: 原因一:服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象。原因二:由于tcp是面向流的协议,不会按照应用开发者的期望保持send输入数据的边界,导致接收侧有可能一下子收到多个应用层报文,需要应用开发者自己分开,有些人觉得这样不合理(那你为啥不用udp),起了个名叫“粘包”。
什么时候需要处理粘包现象
(1)如果是短连接情况,无需考虑粘包情况
(2)如果发送方发送的多个分组本来就是同一个数据的不同部分,比如一个很大的文件被分成多个分组发送,这时,当然不需要处理粘包的现象;例如文件传输,只管发送就行,全盘接收存储即可;
(3)但如果多个分组本毫不相干,甚至是并列的关系,我们就一定要处理粘包问题了。比如,我当时- 要接收的每个分组都是一个有固定格式的商品信息,如果不处理粘包问题,每个读进来的分组我只会处理最前边的那个商品,后边的就会被丢弃。这显然不是我要的结果。基本为长连接。
如何处理粘包?

  1. 靠设计一个带包头的应用层报文结构就能解决。包头定长,以特定标志开头,里带着负载长度,这样接收侧只要以定长尝试读取包头,再按照包头里的负载长度读取负载就行了,多出来的数据都留在缓冲区里即可。
  2. 设置TCP_NODELAY就能屏蔽Nagle算法
  3. 解决方案:发送方和接收方都规定固定大小的缓冲区,也就是发送和接收都使用固定大小的byte[]数组长度,当字符长度不够时,用空字符补充。

该方案缺点:大大增加了网络传输的负担。

  1. 解决方案:在 TCP 协议的基础上封装一层数据请求协议,既将数据包封装成数据头(存储数据正文大小)+ 数据正文的形式,这样在服务端就可以知道每个数据包的具体长度了,知道了发送数据的具体边界之后,就可以解决半包和粘包的问题了;

解决方案:编码较大也不够优雅。

  1. 解决方案:以特殊的字符结尾,比如以“\n”结尾,这样我们就知道结束字符,从而避免了半包和粘包问题(推荐解决方案)

34.进程和线程的区别

在这里插入图片描述
区别
进程是资源分配的最小单位,线程是CPU调度的最小单位。
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

**包含关系:**如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

**内存分配:**同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

**影响关系:**一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

**执行过程:**每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

34.1 什么是进程和线程?

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。就比如说,我们开发的一个单体项目,运行它,就会产生一个进程。

线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。在这里强调一点就是:计算机中的线程和应用程序中的线程不是同一个概念。

线程运行的本质其实就是函数的执行,函数的执行总会有一个源头,而这个源头就是所谓的入口函数,CPU从入口函数到开始执行从而形成一个执行流,只不过我们认为的给执行流起一个名字。
总之一句话描述就是:进程是资源分配的最小单位,线程是程序执行的最小单位。

34.2 同一进程的多线程共享哪些资源?

在这里插入图片描述

  1. 可共享的资源
    表达一:
  1. 进程的堆空间。
  2. 全局变量
  3. 静态变量
  4. 文件等公用资源

表达二:

  1. 栈区:由于栈区是没有添加任何保护机制的,一个线程的栈区堆其他线程是可见的,也就是我们可以修改任何一个线程的栈区。

  2. 堆区:malloc或是new出来的数据,只要知道地址,线程都可以进行访问。

  3. 代码区:任何一个函数都可以放到线程中进行运行。

  4. 数据区:全局变量,静态变量

  5. 独享的资源

  1. 线程的栈
  2. 线程ID
  3. 程序计数器
  4. 线程错误返回码
  5. 线程优先级
  6. 函数运行时使用的寄存器
34.3 悲观锁和乐观锁

总有刁民想害朕
更新数据加锁

悲观锁,顾名思义它是悲观的。讲得通俗点就是,认为自己在使用数据的时候,一定有别的线程来修改数据,因此在获取数据的时候先加锁,确保数据不会被线程修改。形象理解就是总觉得有刁民想害朕
悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。

而乐观锁就比较乐观了,认为在使用数据时,不会有别的线程来修改数据,就不会加锁,只是在更新数据的时候去判断之前有没有别的线程来更新了数据。
乐观锁的实现方式主要有两种:CAS(Compare and Swap)机制和版本号机制。
CAS机制
1.需要读写的内存位置(V)
2.进行比较的预期值(A)

  1. 拟写入的新值(B)
    竞争激烈程度
    当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
    当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。
34.4进程切换的过程

在这里插入图片描述

34.5 一个进程最多可以有多少个线程?
  1. 在win32的情况下:
    一个进程可用虚拟空间是2G(可修改为1+3,3G的可用虚拟空间),默认情况下,线程的栈的大小是1MB,所以,理论上可以建立2048个线程。

  2. 在win64的情况下:
    对于 64 位 Windows 上的 64 位进程,虚拟地址空间为 128 TB.

  3. 在Linux32的情况下
    32 位系统的内核空间占用 1G ,位于最高处,剩下的 3G 是用户空间;如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。

  4. 在Linux64的情况下
    64 位系统的内核空间和用户空间都是 128T ,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。

34.6 为什么进程上下文切换的代价比线程上下文切换的代价要高?
  1. 进程切换的过程:
  1. 切换页目录以使用新的地址空间。
  2. 切换内核栈和硬件上下文
  1. 性能消耗的不同
  1. 线程切换后,虚拟地址空间依然是相同,但进程切换完就不同了,进程一旦切换就伴随着会将寄存器中的内容切换出。

  2. 上下文的切换会扰乱处理器的缓存机制,一旦切换上下文,处理器中所有已经缓存的内存地址都作废。 还有就是处理器的页表缓冲也会被全部刷新,那么就会导致相当于一段时间内相当的低效。

36. 已经有进程了,为什么还要设计出线程这个东西

原因:做多件事情,防阻塞,利用多核。

  1. 进程属于在CPU和系统资源等方面提供的抽象,能够有效的利用CPU的利用率。
  2. 线程是在进程的这个层次上所提供的一层并发的抽象:

a. 能够使系统在同一时间内完成多件事情。
b. 当进程遇到阻塞时,例如等待输入,线程能够不依赖输入数据的工作继续执行。
c. 可以有效的利用多处理器和多核计算机,在线程出现之前,多核并不能提升一个进程的执行速度。

进程无法做的两件事:多件事,阻塞。

35. 浏览器输入url发生什么

  1. 解析该域名。2. Tcp连接。3. Http连接,发送数据。
    在这里插入图片描述

第一部分:浏览器解析该URL

  1. 首先,浏览器会去解析这个域名。会先去本地存储的hosts文件查找有没有和这个域名对应的规则,如果有的话,就直接使用hosts文件中的ip地址。
  2. 若在hosts文件中未找到该对应关系的话,浏览器会发出一个DNS请求到本地DNS(域名分布系统)服务器。本地DN提供,一般由你的网络接入服务提供商,比如中国电信,中国移动。
  3. 查询你输入网址的DNS请求到底本地DNS服务器后,会先查询它的缓存记录,如果缓存记录中有该记录,就直接返回。若没有,还得向DNS根服务器进行请求。
  4. 若根DNS服务器也没有,那么根DNS会告诉本地DNS服务器到域服务器上继续查询,并给出域服务器的地址。
  5. 本地DNS服务器会继续向域服务器发出请求,若请求的对象是.com域服务器的话,.com域名服务器也不会直接返回,而是告诉 本地DNS,你的域名的服务器的解析地址是哪。
  6. 最后,本地向对应的可以解析该域名的服务器的解析地址发出请求。获得域名和IP地址的对应关系。
    在这里插入图片描述
    在这里插入图片描述
    第二部分
    建立TCP连接。
    第三部分
    浏览器向web服务器发起Http请求。
    服务器返回HTMl代码
    浏览器解析该代码。
    浏览器发起请求,请求图片,js代码
    服务器回应该请求。

参考1

36.1 域名劫持

公寓域名解析服务器,或伪造域名服务器,把目标网络解析到错误的URL地址,从而实现用户无法访问目标网站,或是将恶意将用户引导到某个地址。

37. 为什么共享内存比管道快,不都是放在内存吗?

共享内存比管道和消息队列效率高的原因
共享内存区是最快的可用IPC形式,一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传递就不再通过执行任何进入内核的系统调用来传递彼此的数据,节省了时间。
共享内存和消息队列,FIFO,管道传递消息的区别:
后者,消息队列,FIFO,管道的消息传递方式一般为
1:服务器得到输入,输入拷贝到进程。
2:通过管道,消息队列写入数据,通常需要从进程拷贝到内核。
3:客户从内核拷贝到进程
4:然后再从进程中拷贝到输出文件
上述过程通常要经过4次拷贝,才能完成文件的传递。输入->进程->内核->进程->输出
而共享内存只需要
1:从输入文件到共享内存区
2:从共享内存区输出到文件
上述过程不涉及到内核的拷贝,所以花的时间较少。
在这里插入图片描述

38.死锁概念,死锁产生的四个必要条件,如何避免和预防死锁

在这里插入图片描述

概念
死锁,是指两个或多个进程在执行的过程中,因为竞争资源而造成互相等待的现象,若无外力作用,他们都将无法推进下去。
死锁产生的四个必要条件

  1. **互斥条件:**一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所用。此时,若有其他进程请求该资源,该进程只能进行等待。
  2. 请求与保持: 进程中已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占用,此时请求进程阻塞,但对自己已获得的资源保持不放。
  3. 不可剥夺:进程在未使用完资源前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放。
  4. 循环等待条件:若干进程形成首尾相接,资源相互等待的情况。

如何避免和预防死锁

  1. 因为互斥条件是资源固有属性,不可改变。
  2. 破坏"请求与保持":
    方法一:静待分配:每个进程在开始执行时就申请他所需要的全部资源。
    方法二:动态分配:每个进程在申请自己所需要的资源时,本身不占有任何系统资源。
  3. 破坏不可剥夺条件:若一个进程不可获得他所需要的所有资源,就释放目前所拥有的资源,待可获得全部所需资源时再进行申请。
  4. 破坏循环条件:采用资源有序分配的思想。将系统中的资源顺序进行编号,紧缺的编号较大,不紧缺的编号较小。

39. 堆和栈区别

在这里插入图片描述
在这里插入图片描述

  1. 图1中底端是内存地址为0的地方,顶端是内存线性地址最大的地方(如32位下线性地址最大值是0xFFFFFFFF),而占据顶端的就是内核空间,这里是操作系统内核预留的空间区域。第二部分是栈空间,也就是堆栈所在的内存空间,众所周知,栈是自高地址处向低地址处增长的,这在图中也有所反映。最后一部分是堆空间,在这个简化的理论模型上所有的剩余空间都预留给了堆,堆是从低地址向高地址增长的。
  2. 在Linux下分配堆内存需要使用brk系统调用,而这个系统调用只是简单的改变堆顶指针而已,也就是将堆扩大或是缩小。
    堆和栈的区别:
  3. 管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。堆中的内存需要手动申请和手动释放;栈中内存是由OS自动申请和自动释放,存放着参数、局部变量等内存
  4. 空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M
  5. 碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出.
  6. 扩展方向:堆是由高地址向低地址扩展;栈是由低地址向高地址扩展
  7. 效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

程序局部变量是使用的栈空间,new/malloc动态申请的内存是堆空间,函数调用时会进行形参和返回值的压栈出栈,也是用的栈空间。
在这里插入图片描述

栈的效率高的原因:

  1. 栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;
  2. 而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。
39.1 请说一说你理解的stack overflow,并举个简单例子导致栈溢出

栈溢出:是指程序向栈中的某个变量中写入的字节数超过了这个变量本身所申请的字节数,从而导致栈中及与其相邻的变量的值被改变。

栈溢出的原因

  1. 局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出,局部变量时存储在栈中的。解决这个问题的办法是一个是增大栈空间,一个是直接使用堆。
  2. 递归调用层次太多。递归函数有时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。
  3. 指针或数组越界,例如进行字符串拷贝,或处理用户输入。
    栈溢出例子:
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[]) 
{
	char buf[256];
	strcpy(buf,argv[1]);
	printf("Input:%s\n",buf);
	return 0;
}

上述代码中的strcpy(buf,argv[1]);这一行发生了缓冲区溢出错误,因为源缓冲区内容是用户输入的。

40.线程池的概念

什么是线程池
线程池就是以一个或多个线程循环执行多个应用逻辑的线程集合。
为什么使用线程池: 当进行并行的任务作业操作时,线程的建立与销毁的开销是,阻碍性能进步的关键,因此线程池,由此产生。使用多个线程,无限制循环等待队列,进行计算和操作。帮助快速降低和减少性能损耗。
线程池的组成

  1. 线程池管理器:初始化和创建线程,启动和停止线程,调配任务;管理线程池
  2. 工作线程:线程池中等待并执行分配的任务
  3. 任务接口:添加任务的接口,以提供工作线程调度任务的执行。
  4. 任务队列:用于存放没有处理的任务,提供一种缓冲机制,同时具有调度功能,高优先级的任务放在队列前面

40.1. 线程池中如何选择工作线程的数量?IO密集型和CPU密集型不同选法。

有个计算线程数量的公式:

线程数 = CPU 核心数 *1+平均等待时间/平均工作时间)

一般来说,数量的选择为:

  1. 如果是CPU密集型应用,则线程池大小设置为N+1 (N为CPU总核数)
  2. 如果是IO密集型应用,则线程池大小设置为2N+1 (N为CPU总核数)
  3. 线程等待时间(IO)所占比例越高,需要越多线程。IO密集型则主要任务都是在IO那块,CPU有可能比较空闲,所以,线程可以多开,多执行一些其他任务。
    线程CPU时间所占比例越高,需要越少线程。CPU密集型任务一般CPU已经很忙碌了,你多开些线程也没啥用。所以,一般CPU密集型线程的数量会比较少。
  4. 上面+1的原因是:即使当计算(CPU)密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保 CPU 的时钟周期不会被浪费

40.2 线程池核心的7个参数

  1. corePoolSize:核心线程数大小:不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut。
  2. maximumPoolSize:最大线程数:线程池中最多允许创建 maximumPoolSize 个线程.
  3. keepAliveTime:存活时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。
  4. unit:keepAliveTime 的时间单位.
  5. workQueue:存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。所以这里就不要翻译为工作队列了,好吗?不要自己给自己挖坑。
  6. threadFactory:线程工程:用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的,不会懵逼。
  7. handler :拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。

40.3 线程池的好处

a. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
b. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
c. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡(数量太多,调度就很困难,会花费大量的时间在调度线程上面),降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
d. 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

40.4 线程池解决的问题

a. 频繁创建和销毁调度资源,将会带来额外的消耗。
b. 没有一个较好的方式对资源的无限申请进行抑制,容易引发系统资源耗尽。
c. 系统无法合理管理内部的资源分布,降低系统的稳定性。

40.5 线程池的管理

a. 任务管理
(1)直接申请线程执行该任务
(2)缓冲到队列中等待线程执行;
(3)拒绝该任务
b. 线程管理

40.4 进程与线程的切换

进程切换分两步

  1. 切换页目录以使用新的地址空间。
  2. 切换内核栈和硬件上下文。

41. TCP的流量控制的原理

首先,TCP进行流量控制的原因是发送与接收的速率不匹配,导致大量数据被丢失。

流量控制的原因:发送与接收的速率不匹配,导致大量数据会被丢弃。
接口窗口的大小:
1.接收方每次收到数据包,可以在发送确定报文的时候,同时告诉发送方自己的缓存区还剩余多少是空闲的,我们也把缓存区的剩余大小称之为接收窗口大小,用变量win来表示接收窗口的大小。
2.发送方收到之后,便会调整自己的发送速率,也就是调整自己发送窗口的大小,当发送方收到接收窗口的大小为0时,发送方就会停止发送数据,防止出现大量丢包情况的发生。

  1. 有可能出现接收方发送的某个通知报文不知道为啥丢失了,所以出现,接收等发送方发数据,发送在等接收回复。
  2. 为了解决这种问题,我们采用了另外一种策略:当发送方收到接受窗口 win = 0 时,这时发送方停止发送报文,并且同时开启一个定时器,每隔一段时间就发个测试报文去询问接收方,打听是否可以继续发送数据了,如果可以,接收方就告诉他此时接受窗口的大小;如果接受窗口大小还是为0,则发送方再次刷新启动定时器。
  3. 一个用于发送数据,称之为拥塞窗口(即发送窗口)。指出接受窗口大小的通z知我们称之为窗口通告。
  4. 接收方在发送确认报文的时候,会告诉发送方自己的接收窗口大小,而发送方的发送窗口会据此来设置自己的发送窗口,但这并不意味着他们就会相等。首先接收方把确认报文发出去的那一刻,就已经在一边处理堆在自己缓存区的数据了,所以一般情况下接收窗口 >= 发送窗口。
41.1 tcp 里面的接收窗口(没get 到点)。接收窗口如果是0,什么情况。详细说一下。

当发送方收到接受窗口 win = 0 时,这时发送方停止发送报文,并且同时开启一个定时器,每隔一段时间就发个测试报文去询问接收方,打听是否可以继续发送数据了,如果可以,接收方就告诉他此时接受窗口的大小;如果接受窗口大小还是为0,则发送方再次刷新启动定时器。

42. TCP拥塞控制的原理

拥塞控制和流量控制不一样,后者是端对端的问题,它则是一个全局的问题,涉及主机,路由器等等通信设备。这涉及到三种算法:
针对丢包现象,TCP采取的首要措施是超时重传或快速重传。但有可能火上浇油。
但在TCP中,丢包是拥塞发生与否的指标。如时延测量显式拥塞通知(ECN)会使TCP能在丢包发生前检测拥塞。

  1. 慢开始算法(慢启动)
    在这里插入图片描述
  1. 拥塞窗口一开始从1开始,按照2的倍数开始逐渐增加。
  2. 直到整个值大于慢开始门限后,从整个门限16开始后,按照+1增长。
  3. 直到超时,拥塞窗口变成1.再按照2的倍数进行增长。
  4. 但注意,此时的门限变为之前的1/2.

慢启动阈值ssthresh和拥塞控制大小cwnd的关系决定了是采用慢启动还是拥塞控制。
当ssthresh<cwnd时,使用慢启动算法。
当ssthresh>cwnd时,使用拥塞避免算法。如上图

  1. 快重传。
    在这里插入图片描述
    快重传就是接收方,收到一个乱序的报文之后,立即发出重发确认,三次,没有必要把下面剩余的报文接收完了,再发送,要立马发送。快重传和快恢复是配合使用的。

  2. 快恢复
    我们在慢开始算法中讲到,超时时,cwnd要从1开始,这个快恢复不一样,它从超时串口的1/2处开始,即从ssthresh这个阈值这个地方开始。

43. 父进程和子进程的关系

43.1 父进程使用malloc得到一块内存,子进程可以使用吗?

那就是父子进程打印内存的地址都是一样的但为什么得到的数据确不一样呢?其实,父子进程的地址空间是相互独立的,两个进程都会copy原始的数据,因此dada的值是一样的,这个是个虚拟地址!须要经过映射变换得到实际的物理地址!!但由于父子进程此时拥有了各自独立的进程空间,即使是同样的虚拟地址,当映射到实际的物理内存时,也不会是同一个物理地址,所以不是同一个内存块(物理上)!!!每个进程对同样的虚拟地址映射结果是不同的。不能使用虚拟地址来做为判断的依据。
:就是说,其实malloc的这块内存即使内存地址是一样的,父进程和子进程也是相互独立的。因为其实此时父子进程格子拥有了独立的进程空间,所以,即使虚拟内存地址一样,映射到实际的物理内存时,也不会是同一个物理地址。

43.2 进程使用malloc分配一块100M的内存,是马上就得到这块内存了吗?

不是的。分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

43.3 利用虚拟内存机制的优点
  1. 既然每个进程的内存空间都是一致而且固定的(32位平台下都是4G),所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际内存地址,这交给内核来完成映射关系.
  2. 当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存。
  3. 在程序需要分配连续空间的时候,只需要在虚拟内存分配连续空间,而不需要物理内存时连续的,实际上,往往物理内存都是断断续续的内存碎片。这样就可以有效地利用我们的物理内存。
43.4 虚拟内存的作用
  1. 作为缓存工具,提高内存利用率:使用 DRAM 当做部分的虚拟地址空间的缓存(虚拟内存就是存储在磁盘上的 N 个连续字节的数组,数组的部分内容会缓存在 DRAM 中)。扩大了内存空间,当发生缺页异常时,将会把内存和磁盘中的数据进行置换。
  2. 作为内存管理工具,简化内存管理:每个进程都有统一的线性地址空间(但实际上在物理内存中可能是间隔、支离破碎的),在内存分配中没有太多限制,每个虚拟页都可以被映射到任何的物理页上。这样也带来一个好处,如果两个进程间有共享的数据,那么直接指向同一个物理页即可。
  3. 作为内存保护工具,隔离地址空间:进程之间不会相互影响;用户程序不能访问内核信息和代码。页表中的每个条目的高位部分是表示权限的位,MMU 可以通过检查这些位来进行权限控制(读、写、执行)。

44. 请你说一下哈夫曼编码

二叉树、字符

  1. 哈夫曼编码算法用字符在文件中出现的频率来建立使用0,1表示个字符的最优表示方式
  2. 哈夫曼编码时基于二叉树构建编码压缩结构的,它是数据压缩中的一种经典算法。算法根据文本字符出现的频率,重新对字符进行编码。因为为了缩短编码的长度,我们自然希望频率越高的词,编码越短,这样才能最大化压缩存储文本数据的空间。

45. 多线程编程需要注意什么?

安全性,死循环,线程太多
(1)线程之间的安全性

在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

(2)线程之间的死循环过程

为了解决线程之间的安全性引入了Java的锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。假如线程A获得了刀,而线程B获得了叉。线程A就会进入阻塞状态来等待获得叉,而线程B则阻塞来等待线程A所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生。

(3)线程太多了会将服务器资源耗尽形成死机宕机

线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及CPU的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢?

某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。

45.1 sleep 与wait的差别

五个方面来谈区别:

  1. 类:sleep属于的是Thread类,而wait属于Object类。
  2. 位置:sleep可以在任意位置进行使用。而wait只能在同步代码块的前面使用。
  3. 参数:sleep是一定要传入参数的,因为只有这样才能将sleep唤醒。而wait是既可以传入参数,也可以不必传入参数。但是如果不必传入参数的话,那么应该在同步代码块的后面用notify将其唤醒。或者用notify将所有线程唤醒。
  4. 释放锁:sleep使用完成是不释放锁的。而wait使用完成是要释放锁的。
  5. 使用场景:sleep一般用于当前线程休眠,或者轮询暂停操作。wait则一般是用于多线程之前的通讯。
45.2 4种解决线程安全问题的方式

方法一:使用synchronized关键字。使用synchronized可以拿来修饰类,静态方法,普通方法和代码块。
方法二:使用Lock接口下的实现类。
方法三:使用线程本地存储ThreadLocal。当多个线程操作同一个变量且互不干扰的场景下,可以使用ThreadLocal来解决。它会在每个线程中对该变量创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响。
方法四:使用乐观锁机制。

46. 继承有几种方式?

三种。
在这里插入图片描述

47. 子网掩码的作用

子网掩码是一种用来指明一个IP地址所标示的主机处于哪个子网中。子网掩码不能单独存在,它必须结合IP地址一起使用。子网掩码只有一个作用,就是将某个IP地址划分成网络地址和主机地址两部分。

48.你了解那些 stl的容器?

STL的容器可以分为以下几个大类:

一:序列容器, 有vector, list, deque, string.
二 : 关联容器, 有set, multiset, map,multimap,( hash_set, hash_map, hash_multiset, hash_multimap)
三: 其他的杂项: stack, queue, valarray , bitset
std::valarray 是表示并操作值数组的类。它支持逐元素数学运算与多种形式的广义下标运算符、切片及间接访问。
bitset一般用于解决内存放不下的问题.

49.解释下内存溢出,内存泄漏的概念?有没有出现过内存泄漏?该怎么处理内存泄露的问题?

内存溢出申请的不够用,out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
内存泄漏使用了不归还,但也不能再用,是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出.
以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

内存泄露 memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!

内存溢出的原因以及解决方法

引起内存溢出的原因有很多种,小编列举一下常见的有以下几种:
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
3.代码中存在死循环或循环产生过多重复的对象实体;
4.使用的第三方软件中的BUG;
5.启动参数内存值设定的过小

内存溢出的解决方案:

第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
2.检查代码中是否有死循环或递归调用。
3.检查是否有大循环重复产生新对象实体。
4.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
5.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
第四步,使用内存查看工具动态查看内存使用情况。

49.1 为什么会产生内存泄漏?

当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

49.2 内存泄漏对程序的影响?

内存泄漏是造成应用程序OOM的主要原因之一。我们知道Android系统为每个应用程序分配的内存是有限的,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过系统分配的内存限额,这就造成了内存溢出从而导致应用Crash。

49.3 如何检查和分析内存泄漏?

因为内存泄漏是在堆内存中,所以对我们来说并不是可见的。通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。
1、MAT是一款强大的内存分析工具,功能繁多而复杂。
2、LeakCanary则是由Square开源的一款轻量级的第三方内存泄漏检测工具,当检测到程序中产生内存泄漏时,它将以最直观的方式告诉我们哪里产生了内存泄漏和导致谁泄漏了而不能被回收。

  1. 外挂式的内存泄漏检测工具:BoundsChecker
  2. 使用Performance Monitor检测内存泄漏。
49.4 常见的内存泄漏?
  1. 单例造成的内存泄漏。
    由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。

  2. 堆内存泄漏(Heap leak)。
    对内存指的是程序执行中依据须要分配通过malloc,realloc new等从堆中分配的一块内存,再是完毕后必须通过调用相应的 free或者delete 删掉。假设程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.

  3. 系统资源泄露.
    主要指程序使用系统分配的资源比方 Bitmap,handle ,SOCKET等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

  4. Linux中当产生死锁的情况时也会产生内存泄漏(死锁:A,B线程占用着锁,同时又去请求对方的锁)

  5. 指针重新赋值:两个指针已经动态分配完空间,结果其中一个被重新赋值了,导致有内存变成孤立的,变成没有指针指向该内存,从而造成内存的泄漏。

  6. 错误的内存释放

  7. 返回值的不正确处理

  8. 在内存分配后忘记使用 free 进行释放

49. 5 对于内存泄漏,较为常见的解决办法?
  1. 良好的编程习惯。
  2. 当在堆上申请了内存,一定要记得及时释放。
  3. 使用C++的智能指针。
  4. 避免产生死锁的情况。
  5. 打开的文件描述符一定要关闭。
  6. 利用一些第三方工具,去检测内存泄漏。
  7. 还有一些方法:
  1. 确保没有在访问空指针。
    2.每个内存分配函数都应该有一个 free 函数与之对应,alloca 函数除外。
    3.每次分配内存之后都应该及时进行初始化,可以结合 memset 函数进行初始化,calloc 函数除外。
    4.每当向指针写入值时,都要确保对可用字节数和所写入的字节数进行交叉核对。
    5.在对指针赋值前,一定要确保没有内存位置会变为孤立的。
    6.每当释放结构化的元素(而该元素又包含指向动态分配的内存位置的指针)时,都应先遍历子内存位置并从那里开始释放,然后再遍历回父节点。
    7.始终正确处理返回动态分配的内存引用的函数返回值。
49. 6 Linux平台中调试C/C++内存泄漏方法

在这里插入图片描述

50.vector的底层工作原理

vector采用简单的线性连续空间。以两个迭代器start和end分别指向头尾,并以迭代器end_of_storage指向容量尾端。容量可能比(尾-头)还大,多余即备用空间。

51. map与unordered_map优点和缺点

对于map,其底层是基于红黑树实现的,优点如下:
1)有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
2)map的查找、删除、增加等一系列操作时间复杂度稳定,都为O(logN)
缺点如下:
1)查找、删除、增加等操作平均时间复杂度较慢,与n相关.
2)空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间
对于unordered_map来说,其底层是一个哈希表,优点如下:
查找、删除、添加的速度快,时间复杂度为常数级O©
缺点如下:
因为unordered_map内部基于哈希表,以(key,value)对的形式存储,因此空间占用率高
Unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O©,取决于哈希函数。极端情况下可能为O(n).时间复杂度为O(n),就代表数据量增大几倍,耗时也增大几倍。
总结:

  1. 内存占有率的问题就转化成红黑树 VS hash表 , 还是unorder_map占用的内存要高。
  2. 但是unordered_map执行效率要比map高很多
  3. 对于unordered_map或unordered_set容器,其遍历顺序与创建该容器时输入的顺序不一定相同,因为遍历是按照哈希表从前往后依次遍历的
51.1 何时使用map,何时使用HashMap?
  1. 删除和插入操作较多的情况下,map比hash_map的性能更好,添加和删除的数据量越大越明显。
  2. map的遍历性能高于hash_map,而查找性能则相反,hash_map比map要好,数据量越大查找次数越多,表现就越好。

总结:hash_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n)小, hash还有hash函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑hash_map。但若你对内存使用特别严格,希望 程序尽可能少消耗内存,那么一定要小心,hash_map可能会让你陷入尴尬,特别是当你的hash_map对象特别多时,你就更无法控制了,而且 hash_map的构造速度较慢。

权衡三个因素: 查找速度, 数据量, 内存使用

52. 对哈希函数的理解

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
定义:哈希函数能够将固定长度的输入值,转变成固定程度的值输出,该值成为散列值。
特点

  1. Hash的主要原理就是把大范围映射到小范围;所以,你输入的实际值的个数必须和小范围相当或者比它更小。不然冲突就会很多。
  2. 由于Hash逼近单向函数;所以,你可以用它来对数据进行加密。
  3. 不同的应用对Hash函数有着不同的要求;比如,用于加密的Hash函数主要考虑它和单项函数的差距,而用于查找的Hash函数主要考虑它映射到小范围的冲突率。
52.1 一致性哈希算法的理解

一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:

  1. 平衡性:平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。
  2. 单调性:单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
  3. 分散性:分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。
  4. 负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

在这里插入图片描述

参考1
白话解析:一致性哈希算法 consistent hashing

53. 如何实现栈?

53.1 如何用队列实现栈?

其实就是两个队列就可以了。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210530212854522.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzMjExMDYw,size_16,color_FFFFFF,t_70]

54. Vector,ArrayList 与LinkedList 的区别

vector:线程安全,自动增加容量
ArrayList:非线程安全,扩容增加不一致
LinkedList:双向链表

Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,同步有额外开销,Vector 内部是使用对象数组保存数据,也可以根据需要自动增加容量,当数组已满时,会创建新的数组,并拷贝原数组数据。
ArrayList 是应用更广泛的动态数组,本身不是线程安全的,与 Vector 相似, ArrayList 也是可以根据需要调整容量,不过两者间的调整有区别,Vector 在扩容时提高一倍, ArrayList 则是增加 50%。
LinkedList 是 Java 提供的双向链表,所有它不需要调整容量,它也不是线程安全的。
Vector、 ArrayList、 LinkedList均为线型的数据结构,但是从实现方式与应用场景中又存在差别,可以从下面几个方面总结。
底层实现方式
ArrayList内部用数组来实现; LinkedList内部采用双向链表实现; Vector内部用数组实现。
读写机制
ArrayList在执行插入元素是超过当前数组预定义的最大值时,数组需要扩容,扩容过程需要调用底层System.arraycopy()方法进行大量的数组复制操作;在删除元素时并不会减少数组的容量 (如果需要缩小数组容量,可以调用trimToSize()方法);在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找。

LinkedList在插入元素时,须创建一个新的Entry对象,并更新相应元素的前后元素的引用;在查找元素时,需遍历链表;在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元 素删除即可。

Vector与ArrayList仅在插入元素时容量扩充机制不一致。对于Vector,默认创建一个大小为10的Object数组,并将capacityIncrement设置为0;当插入元素数组大小不够时,如 果capacityIncrement大于0,则将Object数组的大小扩大为现有size+capacityIncrement;如果capacityIncrement<=0,则将Object数组的大小扩大为现有大小的2倍。
读写效率
ArrayList对元素的增加和删除都会引起数组的内存分配空间动态发生变化。因此,对其进行插入和删除速度较慢,但检索速度很快。

LinkedList由于基于链表方式存放数据,增加和删除元素的速度较快,但是检索速度较慢。
线程安全性
ArrayList、 LinkedList为非线程安全;
Vector是基于synchronized实现的线程安全的ArrayList。
单线程应尽量使用ArrayList, Vector因为同步会有性能损耗;即使在多线程环境下,我们可以利用Collections这个类中为我们提供的synchronizedList(List list)方法返回一个 线程安全的同步列表对象。

55.解析c++中函数重载的实现原理

倾轧技术

C++函数重载底层实现原理是C++利用倾轧技术,来改名函数名,区分参数不同的同名函数。
函数倾轧: 同名不同参函数(重载函数),C++底层如何区分他们,那就是对函数改名,也就是中文翻译的“倾轧”(苦涩难懂的词),改名也是有规律的,不是随便命名,具体参见下面:

56. const 和 define的区别

编译器、存储方式、类型和安全检查、定义域、数据拷贝

  1. 编译器处理不同宏定义是一个“编译时”概念,在预处理阶段展开,不能对宏定义进行调试,生命周期结束于编译时期。
    const变量是一个运行时概念,在程序运行时使用,类似于一个只读行数据。
  2. 存储方式不同:宏定义是直接替换,不会分配内存。const变量是按内存分配。
  3. 类型和安全检查不同
    宏定义是字符替换,没有数据类型的区别,同时这种替换没有类型安全检查,可能产生边际效应等错误;
    const常量是常量的声明,有类型区别,需要在编译阶段进行类型检查。
  4. 定义域不同
  5. 数据拷贝个数:const定义常量从汇编的角度来看,只是给出 没有了存储与读内存的操作,使得它的效率也很高。

57. 说一下几种IO模型

在Linux(UNIX)操作系统中,共有五种IO模型,分别是:阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动IO模型以及异步IO模型。
在这里插入图片描述

  1. 同步阻塞IO模型
    等鱼的一个过程:阻塞 I/O 是最简单的 I/O 模型,一般表现为进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作。
  2. 非阻塞IO模型
    这个虽然没有一直在等,但要一直询问。
  3. IO多路复用模型
    select轮询相对非阻塞的轮询的区别:前者可以等待多个Socket,能实现对多个IO端口进行监听,当其中任何一个Socket的数据准备好了,就能返回可读。
57.1 进程切换所需要做的工作?

保存上下文和PCB选择,选择另一个进程,恢复。

  1. 保存处理机上下文,包括程序计数器和其他寄存器。
  2. 更新PCB信息。
  3. 把进程的PCB移入响应的队列,如就绪或阻塞队列。
  4. 选择另一个进程执行,并更新其PCB。
  5. 更新内存管理的数据结构。
  6. 恢复处理机上下文。

58. 代码中有线程池的概念吗?如果结合你的项目设计线程池,你应该怎么设计?

线程创建和销毁的开销是很大的,需要为其分配内存,将其加入调度队列由操作系统进行调度。而线程池的目的就是减少线程的频繁创建和销毁,维持一定合理数量的线程,“需要时取,用完时还”。(连接池的目的也类似,其维持一定数量连接的缓存池,尽量重用已有的连接,减少创建和关闭连接的频率;)线程池和连接池在一定程度上缓解了频繁调用IO接口带来的资源占用。
线程池初始化时,会创建一定数量的线程并放入Threads链表中,每个线程处理函数开启一个死循环,通过条件变量等待信号的到来;当有新的任务到来时,会加入Jobs中,并同时通过信号唤醒线程处理相应任务。

59.怎么判断网络上发生了拥塞,重传?失序?(提示了quic 算法ppi 算法)

在这里插入图片描述
拥塞控制是为了防止过多的流量注入到网络中,使网络中的路由器或链路过载。拥塞控制是一个全局性的过程,而流量控制是指对点对点通信的控制。
如何判断网络是否陷入拥塞状态?

方法一
通过观察网络的吞吐量与网络负载间的关系
如果随着网络负载的增加,网络的吞吐量明显小于正常的吞吐量,那么网络就进入例如轻度拥塞的状况。
如果网络得吞吐量随着网络负载的增大反而下降,那么网络就可能进入拥塞状态。
如果网络的负载继续增大,而网络的吞吐量下降到零,网络就可能进入了死锁状态。

方法二
在TCP中,丢包被用作判断拥塞发生与否的指标,用来衡量是否应该实施相应的响应措施来避免或至少减缓拥塞。其他拥塞探测方法,如时延测量和显式拥塞通知(ECN)会使TCP能在丢包发生前检测拥塞。

60. C++11的新特性

  1. auto 关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导。

  2. nullptr:是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。而NULL一般被宏定义为0,在遇到函数重载的时候可能会遇到问题。

  3. 智能指针:C++11新增了std::shared_ptr,std::weaked_ptr等类型的智能指针,用于解决内存管理的问题。
    可变参数模板:对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号”。

  4. 右值引用:
    在这里插入图片描述

  5. lambda表达式:利用Lambda表达式,可以方便的定义和创建匿名函数。

60.1 智能指针
60.1_1 智能指针的作用

说法一:C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。自动释放
说法二:智能指针的作用就是为了保证在使用堆上对象时,对象一定会被释放,但只能释放一次,并且释放后指向该对象的指针应该马上归0.

60.1_2 智能指针的理解
  1. 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
  2. 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
  3. 智能指针还有一个作用是把值语义转换成引用语义。C++和Java有一处最大的区别在于语义不同,在Java里面下列代码:
  Animal a = new Animal();
  Animal b = a;
     你当然知道,这里其实只生成了一个对象,a和b仅仅是把持对象的引用而已。但在C++中不是这样,
     Animal a;
     Animal b = a;

这里却是就是生成了两个对象。

60.1_3 智能指针的使用

shared_ptr

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr p4 = new int(1);的写法是错误的
拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
get函数获取原始指针
注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。
weak_ptr

60.1_4 智能指针存在相互引用的问题,如何解决?
  1. 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
  1. 当A的生存期超过B的生存期的时候,B改为使用一个普通指针指向A。
  2. 使用弱引用的智能指针打破这种循环引用。
    虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。我们一般使用第三种方法:弱引用的智能指针weak_ptr。
60.1_5 智能指针的本质?

本质是存放在栈的模板对象,只是在栈内部包了一层指针。而栈在其声明周期结束的时候,其中的指针指向的堆内存也自然被释放了。

61. c++源文件从文本到可执行文件经历的过程(gcc编译的过程)

  1. 预处理阶段:---->变成.i文件。

    1. 打开头文件,插入到我们本身的程序之中。
    2. 把程序中所有的宏替换。
    3. 删除注释
    4. #ifdef 0的那一部分,机器也不会看到。
  2. 编译阶段---->变成.s文件

    1. 将程序编程汇编语言。
    2. 检查一下程序是否有语法错误。
  3. 汇编阶段------->变成.o文件

    1. 将汇编程序变.o文件。
    2. 将汇编程序变为机器代码
  4. 链接阶段------>变成可执行文件。

    1.使用动态链接或静态链接,链接一些需要的文件。

62.请你回答一下malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?

Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc采用内存池的方式。先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。
并且,Malloc采用隐式链表结构将堆区分成连续的,大小不一的块,包含已分配块和未分配块。用显式链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来。每一个空闲块记录了一个连续的,未分配的地址。
Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。

62.1 new和malloc的区别
  1. 申请的内存的区域是不同的。
    new申请内存的区域一般是在自由存储区,而malloc申请的区域一般是在堆区。c语言使用malloc从堆上分配内存,使用free释放已分配的内存。
  2. 返回类型安全性
    new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无需进行类型转换,而malloc内存分配成功后,返回的是void *。需要我们自身将其强制类型转换成为我们需要的类型。
  3. 内存分配失败时的返回值
    new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。所以,使用New的话,应该使用try catch捕捉异常。
  4. 是否需要指定内存大小
    用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
  5. 是否调用构造函数/析构函数
    使用new操作符来分配对象内存时会经历三个步骤:
    第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
    第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
    第三部:对象构造完成后,返回一个指向该对象的指针。
    使用delete操作符来释放对象内存时会经历两个步骤:
    第一步:调用对象的析构函数。
    第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
    总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会。
  6. 对数组的处理
    C++提供了new[]与delete[]来专门处理数组类型:
A * ptr = new A[10];//分配10个A对象

使用new[]分配的内存必须使用delete[]进行释放:
delete [] ptr;
new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。
至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:

int * ptr = (int *) malloc( sizeof(int) );//分配一个10个int元素的数组
  1. new与malloc是否可以相互调用
    operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new。
  2. 是否可以被重载
    opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本.
    而malloc/free并不允许重载。
  3. 能够直观地重新分配内存
    使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。
  4. 客户处理内存分配不足
    在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler。new_handler是一个指针类型:
namespace std{    typedef void (*new_handler)();}

指向了一个没有参数没有返回值的函数,即为错误处理函数。为了指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数:

namespace std{    new_handler set_new_handler(new_handler p ) throw();}

set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要发生替换)的那个new_handler函数。
对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。

62.2 new的底层实现

创建新对象时,new做了两件事:底层调用malloc函数分配内存、调用构造函数。

new在底层调用operator new全局函数来申请空间;其实就是调用malloc函数分配内存。若申请失败,则抛出bad_alloc异常,再用try catch进行捕捉就可以了。
delete在底层通过operator delete全局函数来释放空间;实际是通过free来释放空间的

62.2_1 new和delete的实现原理, delete是如何知道释放内存的大小的?
  1. new简单类型直接调用operator new分配内存.而对于复杂结构,先调用operator new分配内存,然后在分配的内存上调用构造函数;
  2. 对于简单类型,new[]计算好大小后调用operator new;对于复杂数据结构,new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;
    原理:
  3. new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;
  4. 编译器运行相应的构造函数以构造这些对象,并为其传入初始值;
  5. 对象被分配了空间并构造完成,返回一个指向该对象的指针。

delete的原理
delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。
delete 怎么知道释放内存的大小?
需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。

62.3 mmap了解过吗?

mmap是一种内存映射文件的方法,即将一个文件或其他对象映射到进程的地址空间。实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对应关系。实现这种的映射后,进程就可以使用指针的方式读写这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反应用户空间,从而实现不同进程间的文件共享。
在这里插入图片描述
映射的三个阶段

  1. 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域。
  2. 调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系。
  3. 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。

mmap和常规文件操作的区别

  1. 常规文件系统操作:

a. 进程发起读文件请求。
b. 内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的inode。
c. inode在address_space 上查找要请求的文件页是否已缓存在页缓存中。如果存在,则直接返回这片文件页的 内容。
d. 若不存在,则通过inode定位到文件磁盘地址,将数据从磁盘拷贝到页缓存中,之后再次发起读页面过程。进而将页缓存的数据发给用户进程。
在这里插入图片描述

  1. 使用mmap后的操作:
    而使用mmap操作文件中, a:创建新的虚拟内存区域,b:建立文件磁盘地址和虚拟内存映射这两步。没有任何文件拷贝操作。
    而之后访问数据发现内存中并无数据而引起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次拷贝,就从磁盘中将数据传入用户空间。

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。

63.树的储存

双亲表示法、孩子表示法、孩子兄弟表示法 。

64. 在有继承关系的父子类中,构建和析构一个子类对象时,父子构造函数和析构函数的执行顺序分别是怎样的?

  1. 无论如何继承,指针如何指向,构造函数都以最终实例化为准,顺序始终是先父类后子类
  2. 析构函数遵从类的多态性,非虚析构函数则以指针类型为准,虚析构函数则以最终实例为准,存在继承关系时顺序是先子类后父类
  3. 虚析构函数与普通虚函数还是有不同的,普通虚函数仅按最终实例执行一次,而虚析构函数按最终实例执行后仍会依次向上逐个执行其父类析构函数
  4. 可以通过"父类::函数名"来在子类中访问父类的函数,此时不论该函数是否虚函数,都会直接调用父类对应的函数
    在这里插入图片描述

65. 在有继承关系的类体系中,父类的构造函数和析构函数一定要申明为 virtual 吗?如果不申明为 virtual 会怎样?

  1. C++类有继承时,析构函数必须为虚函数。如果不是虚函数,则使用时可能存在内在泄漏的问题。若析构函数是虚函数(即加上virtual关键词),delete时基类和子类都会被释放;
    若析构函数不是虚函数(即不加virtual关键词),delete时只释放基类,不释放子类;

66. C++ 空类默认产生的类成员函数

C++的空类有哪些成员函数:
. 缺省构造函数。
. 缺省拷贝构造函数。
. 缺省析构函数。
. 缺省赋值运算符。
. 缺省取址运算符。
. 缺省取址运算符 const。
注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。
code

//C++ 空类默认产生的类成员函数:缺省构造函数,拷贝构造函数,析构函数,赋值运算符,取址运算符,取址运算符 const

#include<iostream>
using namespace std;

class class1
{
public:
	class1(){}//缺省构造函数
	class1(const class1&){}//拷贝构造函数
	~class1(){}//析构函数
	class1&operator=(const class1&){}//赋值运算符
	class1*operator&(){}//取址运算符
	const class1*operator&()const{}//取址运算符 const
};
//空类class2会产生class1一样的成员函数
class class2
{
};
void main()
{
	class2 obj1;//缺省构造函数
	class2 obj2;
	obj1=obj2;//赋值运算符
	&obj2;//取址运算符
	class2 obj3(obj1);//拷贝构造函数
	class2 const obj4;
	&obj4;//取址运算符 const
}

661. C++空类的大小,加一个函数呢?加一个虚函数呢?

C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址。这是由于:

new需要分配不同的内存地址,不能分配内存大小为0的空间
避免除以 sizeof(T)时得到除以0错误
故使用一个字节来区分空类。
下面为测试代码:
code

#include <iostream>
using namespace std;

class NoMembers
{
};
int main()
{
    NoMembers n;  // Object of type NoMembers.
    cout << "The size of an object of empty class is: "
         << sizeof(n) << endl;
}

值得注意的是,这并不代表一个空的基类也需要加一个字节到子类中去。这种情况下,空类并不是独立的,它附属于子类。子类继承空类后,子类如果有自己的数据成员,而空基类的一个字节并不会加到子类中去。例如,

class Empty {};
struct D : public Empty {int a;};

sizeof(D)为4。
再来看另一种情况,一个类包含一个空类对象数据成员。

class Empty {};
class HoldsAnInt {
    int x;
    Empty e;
};

在大多数编译器中,你会发现 sizeof(HoldsAnInt) 输出为8。这是由于,Empty类的大小虽然为1,然而为了内存对齐,编译器会为HoldsAnInt额外加上一些字节,使得HoldsAnInt被放大到足够又可以存放一个int。

总结

1.空类的大小占1个字节。因为不允许一个对象的大小为0。所以,用一个字节来表示对象的一些信息。
2. 空类加一个普通的函数的大小:1个字节。因为普通的函数跟类的实例是没有关系的。所以不改变实例的大小。
3.内存会对齐到类内定义的最大的那个数据类型的size上,类内所有的变量根据定义顺序占的内存都会按照那个size对齐。
4. static成员变量是不在类里占内存的。(计算sizeof时,忽略static变量)
5. 若空类加一个虚函数表,则其将会占用8个字节。这8个字节是一个指向虚函数表的指针。

C++空类的大小

66.1拷贝构造和移动构造

C++11之前,对象的拷贝控制由三个函数决定:拷贝构造,拷贝赋值,析构函数。
C++11之后,增加了两个移动构造函数,移动赋值函数。

构造函数与赋值运算符的区别是,构造函数在创建或初始化对象的时候调用,而赋值运算符在更新一个对象的值时调用。
移动语义的出现使得大对象可以避免频繁拷贝造成的性能下降,特别是对于临时对象,移动语义是传递它们的最佳方式。

66.2 什么情况下必须使用拷贝构造函数 ?string类的拷贝构造函数

C++默认的拷贝构造函数和赋值构造函数都是浅拷贝,所以当遇到类成员中有指针变量时,就得自己实现深拷贝。
什么情况必须使用拷贝构造?

  1. 当某个函数的入参是类对象时,拷贝构造函数会被调用。
  2. 当我们基于一个已有的对象去生成一个新的对象时,拷贝构造函数会被调用。

string类的拷贝构造函数
code

class String
{
	public:
	String(const char* str = NULL);//普通的构造函数
	String(const String &other);//拷贝构造函数,要使用引用传递
	~String(void);//析构函数
	String &operator = (const String &other);
	private:
	char *m_data;//用于保存字符串 	 
} 
String::String(const char* str)//普通构造函数 
{
	if(str==NULL)
	{
		m_data = new char[1];
		if(m_data)
			*m_data='\0';
		 
	}
	else
	{
		int length = strlen(str);
		m_data = new char[length+1];
		strcpy(m_data,str);
	}
 }
 
QSring::~String(void)
{
	delete [] m_data; 
 } 
 
String::String(const String&other)//拷贝构造函数 
{
	int length = strlen(other.m_data);
	m_data = new char[length+1];
	strcpy(m_data,other.m_data);
}

String & String::operate=(const String&other)//赋值函数 
{
	if(this==&other)
		return *this;
	delete [] m_data;
	int length = strlen(other.m_data);
	m_data = new char[length+1];
	strcpy(m_data,other.m_data);
	return *this;
}
66.3 左值与右值的区别
  1. 能出现在赋值号左边的表达式称为“左值”,不能出现在赋值号左边的表达式称为“右值”。一般来说,左值是可以取地址的,右值则不可以。

非 const 的变量都是左值。函数调用的返回值若不是引用,则该函数调用就是右值。一般的“引用”都是引用变量的,而变量是左值,因此它们都是“左值引用”。

C++11 新增了一种引用,可以引用右值,因而称为“右值引用”。无名的临时变量不能出现在赋值号左边,因而是右值。右值引用就可以引用无名的临时变量。定义右值引用的格式如下:

类型 && 引用名 = 右值表达式;

不能取地址的都是右值

对于a++

  1. a++首先产生一个临时变量,记录a的值
  2. 然后将a+1
  3. 接着返回临时变量

根据这个过程我们知道 int a = 0;int c = a++; 的值应该是c为0;而a变为了1,
所以a++此时将临时变量返回给了c,那么这个临时变量我们是不能获取地址的,也就
是使用“&”。所以结论就是a++为右值。

对于++b

  1. 进行了b = b + 1
  2. 返回变量b

++b是左值。能使用取地址符号的是左值。

67. STL的基本组件

在这里插入图片描述

67.1 一个空类占用的内存大小是多少?

实际上,这是类结构体实例化的原因,空的类或结构体同样可以被实例化,如果定义对空的类或者结构体取sizeof()的值为0,那么该空的类或结构体实例化出很多实例时,在内存地址上就不能区分该类实例化出的实例,所以,为了实现每个实例在内存中都有一个独一无二的地址,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以空类所占的内存大小是1个字节。

68. 递归太深有什么影响?

递归算法的代码很简洁。但同时也存在缺点。

递归由于函数要调用自身,而函数调用是有时间和空间的消耗的。每一次函数调用,都需要在内存栈中分配空间以保存参数、返回地址及临时变量,而且往栈里压入数据和弹出数据都需要时间。

递归有可能很多计算都是重复的,从而对性能带来很大的负面影响。递归的本质是把一个问题分解成两个或者多个小问题。如果小问题有重叠的部分,那么就存在重复的计算。

除了效率外,递归还可能存在调用栈溢出的问题。前面提到的每一次函数调用在内存栈中分配空间,而每个进程的栈容量是有限的。当递归调用的层级太多时,就会超出栈的容量,从而导致调用栈溢出。

69. 请你谈谈 C++内存模式

内存模型所要表达的内容主要是这么描述: 一个内存操作的效果,在其他线程中的可见性问题。我们知道,对计算机来说,通常内存的写操作相对于读操作是昂贵很多很多的,因此对写操作的优化是提升性能的关键,而这些对写操作的种种优化,导致了一个很普遍的现象出现:写操作通常会在 CPU 内部的 cache 中缓存起来。这就导致了在一个 CPU 里执行一个写操作之后,该操作导致的内存变化却不一定会马上就被另一个 CPU 所看到。

70. 堆内存和栈内存的区别?

  1. 数据结构中的堆和栈。
    栈:是一种连续储存的数据结构,具有先进后出的性质。通常的操作有入栈(圧栈)、出栈和栈顶元素。想要读取栈中的某个元素,就要将其之前的所有元素出栈才能完成。类比现实中的箱子一样。

堆:是一种非连续的树形储存数据结构,每个节点有一个值,整棵树是经过排序的。特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。常用来实现优先队列,存取随意。

  1. 内存中的栈区与堆区:
    在这里插入图片描述
    栈内存:由程序自动向操作系统申请分配以及回收,速度快,使用方便,但程序员无法控制。若分配失败,则提示栈溢出错误。注意,const局部变量也储存在栈区内,栈区向地址减小的方向增长。
    堆内存:程序员向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。分配的速度较慢,地址不连续,容易碎片化。此外,由程序员申请,同时也必须由程序员负责销毁,否则则导致内存泄露。

71. 重载和重写的区别

重载:函数名相同,函数的参数个数、参数类型或参数顺序三者中必须至少有一种不同。函数返回值的类型可以相同,也可以不相同。发生在一个类内部,不能跨作用域。

重定义:也叫做隐藏,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,指派生类的函数屏蔽了与其同名的基类函数。可以理解成发生在继承中的重载。

重写:也叫做覆盖,一般发生在子类和父类继承关系之间。子类重新定义父类中有相同名称和参数的虚函数。(override)
如果一个派生类,存在重定义的函数,那么,这个类将会隐藏其父类的方法,除非你在调用的时候,强制转换为父类类型,才能调用到父类方法。否则试图对子类和父类做类似重载的调用是不能成功的。

重写需要注意:
1、 被重写的函数不能是static的。必须是virtual的,如果不是virtual 的话,就是重定义了。
2 、重写函数必须有相同的类型,名称和参数列表
3 、重写函数的访问修饰符可以不同。

重定义规则如下:
a 、如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
b 、如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏(如果相同有Virtual就是重写覆盖了)。

72. 网页404是什么原因?

404页面是客户端在浏览网页时,服务器无法正常提供信息,或是服务器无法回应,且不知道原因所返回的页面。简单来说,也就是当你打开一个网站的内页的时候,发现这个页面是不存在或者出现问题时,所跳转的当前页面称之为404页面。404页面的出现,有时对于企业网站而言损害是无法弥补的,因此对于这点,一定要引起企业的高度重视。
原因

  1. 程序数据库出现异常。
  2. SEO优化导致的死链接。所谓的死链是指向已经不存在页面的链接。
  3. 文件移动或删除。互联网中有些网站的文件,最初的时候是存在某个路径的,但是后来由于某种原因移走了,那如果浏览者访问之前的链接的话。
  4. 原url地址失效。404页面的错误也就是意味着链接指向的网页不存在,即原始网页的URL失效。
  5. 配置文件web。xml里的配置错误。
72.1 http,了解哪些返回码?比如404,403等等

400(错误请求)

服务器不理解请求的语法。

401(未授权)

请求要求身份验证。对于登录后请求的网页,服务器可能返回此响应。

403(禁止)

服务器拒绝请求。如果您在 Googlebot 尝试抓取您网站上的有效网页时看到此状态码(您可以在 Google网站管理员工具诊断下的网络抓取页面上看到此信息),可能是您的服务器或主机拒绝了 Googlebot 访问。

404(未找到)

服务器找不到请求的网页。例如,对于服务器上不存在的网页经常会返回此代码。
如果您的网站上没有 robots.txt 文件,而您在 Google 网站管理员工具“诊断”标签的 robots.txt页上看到此状态码,则这是正确的状态码。但是,如果您有 robots.txt 文件而又看到此状态码,则说明您的 robots.txt文件可能命名错误或位于错误的位置(该文件应当位于顶级域,名为 robots.txt)。
如果对于 Googlebot 抓取的网址看到此状态码(在”诊断”标签的 HTTP 错误页面上),则表示 Googlebot跟随的可能是另一个页面的无效链接(是旧链接或输入有误的链接)。

73. DFS和BFS算法

DFS:深度优先算法
步骤:递归下去,回溯上来.不撞南墙不回头。 先一条路走到底,直到达到目标。这里称为递归下去。
code

void dfs(int x,int y)
{	
	field[x][y] = '*';
	for(int dx=-1;dx<=1;dx++)
	{
		for(int dy = -1;dy<=1;dy++)
		{			
				int nx = x+dx,ny=y+dy;			
				if(0<nx&&nx<n&&0<ny&&ny<m&&field[nx][ny]=='W')				
					dfs(nx,ny);		
		}	
	} 
} 

BFS:广度优先算法
BFS是从根节点开始,沿着树(图)的宽度遍历树(图)的节点。
如果所有节点均被访问,则算法中止。
BFS同样属于盲目搜索。
一般用队列数据结构来辅助实现BFS算法。

74. 数据库索引为什么快?

1、索引是对数据库表中一列或多列的值进行排序的一种结构,本身是一个有序键值列表。这样检索的数据直接可以以顺序列出。2、通常来说,索引比要检索或者排序的表本身小很多,足够放在内存中或者一次性读取,确定了数据位置,直接读取磁盘上相应的数据块,以减少磁盘I/O,对现在的计算机来说,磁盘I/O的开销很大。如果没有索引,排序一般需要全表扫描,大大增加磁盘I/O。

75.Linux命令:远程怎样查看端口连没连接?

https://www.huaweicloud.com/articles/1a9108396a7a5bec7715a1bcaa325fa3.html

76. select和epoll区别?它们算同步还是异步io,同步异步区别在哪里

  1. 通知机制
    select 基于轮训机制
    epoll基于操作系统支持的I/O通知机制 epoll支持水平触发和边沿触发两种模式
    https://blog.csdn.net/Jailman/article/details/78498193
  2. 都是同步io。
  3. 同步io需要在读写事件就绪后,自己负责读写,而异步IO无需进行读写,它只负责发起事件具体的实现由别的完成。
76.1 epoll的底层原理?

主要有三个函数。create,ctl,wait。create会生成一个红黑树和一个就绪链表。红黑树主要存储所监控的fd。就绪链表主要存储就绪的fd。当有fd的数据到来时,就会触发ctl.此时ctl会将该节点放入就绪链表中。wait也会收到信息,并将数据拷贝到用户空间,并清空链表。
并且,若当前处于LT模式下,且的确有数据未处理,则将该文件描述符重新放回就绪链表。
而如果是ET模式,则不再放入就绪链表,只放入一次。

在这里插入图片描述
在这里插入图片描述

epoll 是一种网络模式,采用的IO多路复用技术(可监控多个文件描述符)
code

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
76.1_1:epoll的ET(Edge Triger)和LT(Level Trig)模式

水平触发模式:一个事件只要有,就会一直触发;类比于Socket的数据,只要数据没有读取结束,那么就会一直触发,只有读取结束,才会结束触发。
边缘触发模式:只有一个事件从无到有才会触发。而这个的类比是,只有新的数据到来时,才会有触发。

76.2 select的底层原理

select会循环遍历它所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。
在这里插入图片描述

76.2 IO多路复用的含义

单进程或单线程同时监测若干个文件描述符是否有执行IO操作的能力。

I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。
其实就是单个线程通过记录跟着每个Sock的状态来同时管理多个IO流
也叫 时分复用

76.3 说出 你所知道的IO多路复用模型,并解释为什么IO多路复用效率高?

select,poll,epoll都是IO多路复用的一种机制,就是通过一种机制可以描述多个文件描述符,一旦某个文件描述符就绪,就能够通知进程进行相应的读写操作。他们三个本质上都是同步IO,因为它们都需要在读写事件就绪后自己负责读写操作,也就是读写过程中是阻塞的,而异步IO无需自己进行读写,它只负责发起事件具体的实现由别的完成。

select:
selelct本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步操作。
这样做的缺点是:

  1. 单个进程可监视的fd数量被限制,即能监听端口的数量有限。(32位一般是1024,64位一般为2048)
  2. 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低.所以,每次都得轮询一遍,很耗时间,要是能给套接字注册个回调函数,当他们活跃时,就自动完成相关操作。而这正是epoll与kquque所做的。
  3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递数据结构时复制开销大。

poll:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd的状态,若设备就绪,则在设备等待队列中加入一项并继续遍历。若遍历完所有的fd都没有就绪设备,则挂起当前进程,直到设备就绪或主动超时,被唤醒后,它又要再次遍历fd。这个倒是没有最大连接数的限制,因为它是基于链表的。
但这个同样有一个缺点是:

  1. 大量的fd被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义的。
  2. poll还有一个特点是水平触发,若报告了fd后,没有被处理,那么下次poll时会再次报告该fd。(水平触发的含义是只要处于水平状态就会一直触发)

epoll

  1. epoll支持边缘触发和水平触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。
  2. epoll使用事件的就绪通知状态,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd。
    优点:
  3. 有最大并发连接的限制,能打开的fd的上限远大于1024(1G的内存可以打开10万个).
  4. 效率提升:不是轮询的方式,不会随着FD数目的增加效率下降。是活跃的fd通过回调函数去通知即可。
  5. 内存拷贝:通过mmap文件映射内存加速与内核空间的消息传递,即epoll使用mmap减小消息复制。

在这里插入图片描述

96. IO多路复用是解决什么问题的?

解决之前单个线程只能处理一个I/0流的情况。

I/O多路复用 (单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 )。在ngnix中会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。

注:每个socket就是一个I/O流,服务端只会监听一个端口,每次来了新的请求,都会创建一个新的socket和客户端通信
当多个客户端与服务器通信时,若服务器阻塞在其中一个客户的read(sockfd1,…),当另一个客户数据到达sockfd2时,服务器无法及时处理,此时需要用到IO多路复用。即同时监听n个客户,当其中有一个发来消息时就从select的阻塞中返回,然后调用read读取收到消息的sockfd,然后又循环回select阻塞。这样就解决了阻塞在一个消息而无法处理其它的。即用来解决对多个I/O监听时,一个I/O阻塞影响其他I/O的问题。

96.1 IO多路复用如何实现

select, poll, epoll 都是I/O多路复用的具体的实现:

select特点:

  1. 单个进程打开的FD是有限制的,通过FD_SETSIZE设置,默认1024.
  2. 每次调用select,都需要把fd集合从用户态拷贝到内核态,开销很大。
  3. 对socket扫描时,采取的是轮训的方法,效率较低。
  4. select不是线程安全的。

poll特点

  1. poll和select是非常相似的,poll相对于select的优化仅仅在于解决了文件描述符不能超过1024个的限制。
  2. select和poll都会随着监控的文件描述符增加而出现性能下降,因此不适合高并发场景。

epoll特点
epoll 修复了poll 和select绝大部分问题, 比如:

  1. epoll 现在是线程安全的。
  2. epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
  3. 不过缺点是epoll只能工作在linux下
    在这里插入图片描述

77. ICMP协议?

https://www.cnblogs.com/ZZS-DYL/p/12204837.html

  1. 主要用于在IP主机,路由器之间传递控制消息。控制消息是指网络通不通,主机是否可通,路由是否可用等网络本身的消息。
  2. 网络层协议。

78. HashMap的面试题。

78.1 如何自己实现一个Hash?

用一个数组加一个链表。其实,数组本身就可以作为一个hash表,下标作为key ,value就是数组的值。HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体。 只是在JDK1.8中,链表长度大于8的时候,链表会转成红黑树! 为什么用数组+链表? 数组是用来确定桶的位置,利用元素的key的hash值对数组长度取模得到. 链表是用来解决hash冲突问题,当出现hash值一样的情形,就在数组上的对应位置形成一条链表。

78.2 你看过hashmap的源码吗?

数组加链表的方式就是其底层原理。

78.3 hash冲突你还知道哪些解决办法?

散列法,拉链法

线性探测,再频繁,伪随机,再哈希,公共溢出区域法

装填因子(装填因子=数据总数 / 哈希表长)
(1)开放定址法

1)线性探测
按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。 
2)再平方探测
顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。
3)伪随机探测
按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。

(2)链地址法

对于相同的值,使用链表进行连接。使用数组存储每一个链表。
优点:
1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
(3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
(4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
缺点:
指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。

(3)再哈希法
对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。

(4)公共溢出区域法
建立公共溢出区存储所有哈希冲突的数据。

78.4 为什么HashMap不用LinkedList,而选用数组?

因为用数组效率最高! 在HashMap中,定位桶的位置是利用元素的key的哈希值对数组长度取模得到。此时,我们已得到桶的位置。显然数组的查找效率比LinkedList大。

78.5 那ArrayList,底层也是数组,查找也快啊,为啥不用ArrayList?

因为采用基本数组结构,扩容机制可以自己定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率高。 而ArrayList的扩容机制是1.5倍扩容,那ArrayList为什么是1.5倍扩容这就不在本文说明了。

78.6 为什么hashmap的在链表元素数量超过8时改为红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。 当元素小于8个当时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于8个的时候,此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

78.7 HashMap在并发编程环境下有什么问题啊?

(1)多线程扩容,引起的死循环问题
(2)多线程put的时候可能导致元素丢失
(3)put非null元素后get出来的却是null

79 说一个你熟悉的设计模式

设计模式的好处:

  1. 可重用代码
  2. 保证代码可靠性
    3.使代码更易于被他人理解。

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
适配器模式

79.1 单例模式

在这里插入图片描述

其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。单例模式分为懒汉式和饿汉式。

懒汉式是在指系统运行中,实例并不存在,只有当需要该实例时,才创建该实例。但注意,懒汉式并不是线程安全的,若要达到线程安全,则一个进行加锁。
code

#include <iostream>
#include <mutex>
#include <pthread.h>
using namespace std;
classs SignleInstance
{
public:
	//获取单例对象
	static SingleInstance *GetInstance();
	//释放单例,进程退出时调用 
	static void deleteInstance();
	//打印单例地址 
	void Print();
private:
	//将其构造和析构函数都变成私有的,精致外部构造和析构 
	SingleInstance();
	~SingleInstance();
	
//将拷贝构造和赋值构造成为私有函数,禁止外部拷贝和赋值
	SingleInstance(const SingleInstance &signal);//拷贝构造
	const SingleInstance &operator(const SingleInstance &signal);//const 防止对实参的意外修改
	//使用引用的原因
//	因为实参是通过按值传递机制传递的。在可以传递对象cigar之前,编译器需要安排创建该对象的副本。因此,编译器为了处理复制构造函数的这条调用语句,需要调用复制构造函数来创建实参的副本。但是,由于是按值传递,第二次调用同样需要创建实参的副本,因此还得调用复制构造函数,就这样持续不休。最终得到的是对复制构造函数的无穷调用。(其实就是创建副本也是需要调用复制构造函数的)
//所以解决办法先是要将形参改为引用形参:
private:
	//唯一实例对象指针
	static SingleInstance *m_SingleInstance;
	static mutex m_Mutex;
};

//初始化静态成员变量	
SingleInstance *SingleInstance::m_SingleInstance = NULL;
mutex SingeInstance::m_Mutex;
SingleInstance* SingleInstance::GetInstance()
{
	//使用双检锁的方式 ,判断对象为空再进行加锁 
	if(m_SingleInstance ==NULLL)
	{
		unique_lock<mutex> lock<m_Mutex>;
		if(m_SingleInstance ==NULLL)
			m_SingleInstance = new (std::nothrow) SingleInstance;//线程并发的话有可能创建多个实例,非线程安全 
	}
	 
	return m_SingleInstance; 	
}  

void SingleInstance::deleteInstance()
{
	unique_lock<mutex> lock<m_Mutex> 
	if(m_SingleInstance)
	{
		delete m_SingleInstance;
		m_SingleInsance = NULL;//若没有置空,容易出现野指针。
	}
}

void SingleInstance::Print()
{
	std::cout<<"我的实例内存地址是"<<this<<std::endl; 
 } 
 
SingleInstance::SingleInstance()
{
    std::cout << "构造函数" << std::endl;
}

SingleInstance::~SingleInstance()
{
    std::cout << "析构函数" << std::endl;
}

//普通懒汉式实现---线程不安全
void *PrintHello(void *threadid)
{
	
}

int main(void)
{
	pthread_t threads[NUM_THREADS] = {0};
    int indexes[NUM_THREADS] = {0}; // 用数组来保存i的值

    int ret = 0;
    int i = 0;

    std::cout << "main() : 开始 ... " << std::endl;

    for (i = 0; i < NUM_THREADS; i++)
    {
        std::cout << "main() : 创建线程:[" << i << "]" << std::endl;
        
		indexes[i] = i; //先保存i的值
		
        // 传入的时候必须强制转换为void* 类型,即无类型指针
        ret = pthread_create(&threads[i], NULL, PrintHello, (void *)&(indexes[i]));
        if (ret)
        {
            std::cout << "Error:无法创建线程," << ret << std::endl;
            exit(-1);
        }
    }

    // 手动释放单实例的资源
    SingleInstance::deleteInstance();
    std::cout << "main() : 结束! " << std::endl;
	
    return 0;
} 

饿汉式是指系统一运行,就初始化创建实例,当需要时,直接调用即可。
code

//初始化静态成员变量	
#if 0 懒汉模式 
SingleInstance *SingleInstance::m_SingleInstance = NULL;
#endif  
#if 0 饿汉模式 
SingleInstance *SingleInstance::m_SingleInstance = new(nothrow) SingleInstance;
#endif  
mutex SingeInstance::m_Mutex;
SingleInstance* SingleInstance::GetInstance()
{
#if 0 懒汉模式 
	//使用双检锁的方式 ,判断对象为空再进行加锁 
	if(m_SingleInstance ==NULLL)
	{
		unique_lock<mutex> lock<m_Mutex>;
		if(m_SingleInstance ==NULLL)
			m_SingleInstance = new (std::nothrow) SingleInstance;//线程并发的话有可能创建多个实例,非线程安全 
	}
#endif	 
#if 0 饿汉模式 
	return m_SingleInstance;//get方法,就直接返回即可。
#endif 
}  

1.小林设计模式

79.2 工厂模式

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
工厂模式主要是为创建对象提供了接口。主要分为三类:

  1. 简单工厂模式。通过传入参数,控制生成的类为哪种。调用的时候,用父类的指针指向该函数即可。
  2. 工厂方法模式
  3. 抽象工厂模式
  1. 简单工厂模式

简单工厂模式(Simple Factory Pattern)属于类的创新型模式,又叫静态工厂方法模式(Static FactoryMethod Pattern),是通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。一个工厂,多个产品。产品需要有一个虚基类。通过传入参数,生成具体产品对象,并利用基类指针指向此对象。通过工厂获取此虚基类指针,通过运行时多态,调用子类实现。

简单工厂模式的核心思想就是:**有一个专门的类来负责创建实例的过程。**方法(type)根据type去new不同的对象。

  1. 工厂方法模式:工厂方法模式定义了一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到子类。把简单工厂模式中的工厂类抽象出一个接口,这个接口只有一个方法,就是创建抽象产品的工厂方法。然后所有的要生产具体类的工厂,就去实现这个接口,这样,一个简单工厂模式的工厂类,就变成了一个工厂抽象接口和多个具体生成对象的工厂。连type都是由工厂方法去创建的。

  2. 抽象工厂模式:抽象工厂模式是提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

80. 多线程是如何实现的?

多线程的出现是为了加快处理任务的效率,结合之前说过的底层 CPU 的介绍我们可以知道,在操作系统层面上,线程是操作系统任务调度的最小单位进程是资源分配的最小单位,一个进程可以包含多个线程,线程共享进程中的资源。

实现C++多线程并发程序的思路如下:将任务的不同功能交由多个函数分别实现,创建多个线程,每个线程执行一个函数,一个任务就这样同时分由不同线程执行了。

80.1 进程

进程结束的三种方式:
1.正常结束(程序执行流程已经结束)
2.异常结束(程序执行执行过程中遇到异常退出,如除0异常,地址越界(非法访问不属于该进程内存))
3.强制结束(受外界条件,如kill信号)

81. extern“C”{}有什么用?

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。

这个功能主要用在下面的情况:
1、C++代码调用C语言代码
2、在C++的头文件中使用
3、在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到

82. 孤儿进程与僵尸进程[总结]?

孤儿进程:父进程退出,子进程还在运行
僵尸进程:遭遇异常退出,但进程表仍存在进程控制块。
由于父进程和子进程的运行是一个异步的过程。即父进程永远无法预测子进程到底什么时候结束。当一个进程完成它的工作终止后,它的父进程需要调用wait()或是waitpid()系统调用取得子进程的终止状态。

孤儿进程:即一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程所收养(进程号为1)。并由init进程对它们完成状态收集工作。 父进程走了,剩子->孤儿
**僵尸进程:**一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。僵尸进程是指完成执行(通过 exit 系统调用,或运行时发生致命错误或收到终止信号所致),但在操作系统的进程表中仍然存在其进程控制块,处于"终止状态"的进程。 自己走了,没释放子进程的进程描述符,该子进程变成僵尸进程。
如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程,因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程,看有没有哪个进程是 刚刚结束的这个进程的子进程,如果是的话,就由Init来接管他,成为他的父进程,从而保证每个进程都会有一个父进程.而Init进程会自动wait其子 进程,因此被Init接管的所有进程都不会变成僵尸进程.

82.1 僵尸状态是每个子进程必经过的状态吗?

是的。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构(,Zombie中存储了该进程的进程号、退出码、退出状态、使用的CPU时间等信息。即僵尸进程是早已死亡的子进程,但在进程表中占了一个位置(slot)),等待父进程处理。这 是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。
如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

82.2 僵尸进程的避免
1、父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起
2. 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后,父进程会收到该信号,可以在handler中调用wait回收
3. 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号
4. 还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。
82.3 守护进程
  1. 什么是守护进程?
    守护进程是运行在后台的,独立于控制终端,周期性的执行某种任务或等待某些发生的事件。

83. static的作用

https://www.cnblogs.com/-believe-me/p/11603860.html
一. 作用于函数内部的局部变量
局部作用域静态变量的特点:当一个函数返回后,下一次再调用时,该变量还会保持上一回的值,函数内部的静态变量只开辟一次空间,且不会因为多次调用产生副本,也不会因为函数返回而失效。也就是修饰的该变量被所有实例所共享,也就是说当某个实例修改了该变量的时候,其修改值为该类的所有实例所见。
二. 作用于类的成员,解决同一个类的不同对象之间数据和函数共享问题

84.linux层级管理

/bin 常用的二进制目录。比如:ls cp mkdir等,和/usr/bin类似
/sbin 大多涉及系统管理的命令存放,是超级权限用户root可执行命令存放地
/boot linux内核及引导系统程序所需的文件目录。安装系统分区的时候一般要分一个boot分区。常见分区:/boot 200M swap内存的1.5倍,其余的都给/.
/dev 设备文件目录比如声卡磁盘光
/etc 操作系统的配置文件
/home 普通用户的家目录默认数据存放的目录(普通用户刚登陆的目录)
/lib64 库文件存放的目录
/lost+found 当系统出现问题,会产生一些文件过程中fsck工具会检查这里,修复损坏的文件系统
/mnt 临时挂载存储设备的挂载目录(相当于回收站,不重要的文件)
/opt 一些安装包会安装在这下面,/usr/local
/proc 操作系统运行时,进程信息及内核信息存放这里
/root Linux超级权限用户root的家目录
/tmp 临时文件目录,有时用户运行程序的时候,还会产生临时文件存放到这里
/usr 系统存放程序的目录,比如命令,帮助文件等。Linux发行版官方提供的软件包大多安装在这里。配置文件一般放在/etc/下面。帮助目录:/usr/share/doc,/usr/share/man。普通用户可执行的文件目录/usr/bin/或/usr/local/bin。
/var 这个目录的内容经常变动,存放改变的文件,如:记录程序产生的缓存、进程号、日志。
/etc目录下的重要目录文件说明

85.vector和list区别,resize和reserve区别,erase底层

vector和list区别

List封装了链表,Vector封装了数组, list和vector得最主要的区别在于vector使用连续内存存储的,他支持[]运算符,而list是以链表形式实现的,不支持[]。

Vector对于随机访问的速度很快,但是对于插入尤其是在头部插入元素速度很慢,在尾部插入速度很快。List对于随机访问速度慢得多,因为可能要遍历整个链表才能做到,但是对于插入就快的多了,不需要拷贝和移动数据,只需要改变指针的指向就可以了。另外对于新添加的元素,Vector有一套算法扩容,而List可以任意加入。

Map,Set属于标准关联容器,使用了非常高效的平衡检索二叉树:红黑树,他的插入删除效率比其他序列容器高是因为不需要做内存拷贝和内存移动,而直接替换指向节点的指针即可。

Set和Vector的区别在于Set不包含重复的数据。Set和Map的区别在于Set只含有Key,而Map有一个Key和Key所对应的Value两个元素。

Map和Hash_Map的区别是Hash_Map使用了Hash算法来加快查找过程,但是需要更多的内存来存放这些Hash桶元素,因此可以算得上是采用空间来换取时间策略。

resize和reserve区别
reserve是容器预留空间,但并不真正创建元素对象,在创建对象之前,不能引用容器内的元素,因此当加入新的元素时,需要用push_back()/insert()函数。
resize是改变容器的大小,并且创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。
再者,两个函数的形式是有区别的,reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。下面是这两个函数使用例子:

补充:ICMP协议是一种面向无连接的协议,用于传输出错报告控制信息。 … 它是TCP/IP协议族的一个子协议,属于网络层协议,主要用于在主机与路由器之间传递控制信息,包括报告错误、交换受限控制和状态信息等。

erase的底层

erase和remove的区别
vector中remove的作用是把元素放到vector的末尾,但并不减少vector的size。
vector中erase的作用是删除掉某个位置的position或一段区域(begin,end)中的元素,减少其size。

list中remove的函数原型是void remove (const value_type& val),其作用是删除list中与val相同的节点,释放该节点的资源。
list容器中erase成员函数,其函数原型是`iterator erase(iterator position),其作用是删除position位置的节点。这也是与remove所不同的地方。

对于set来说,只有erase Api 没有remove Api . erase 的作用是把符合要求的元素都删掉。

(1) void erase (iterator position);
(2) size_type erase (const value_type& val);
(3) void erase (iterator first, iterator last);
85.1 vector/list/map的底层实现,增删改查复杂度

86.ping背后的逻辑

ping 命令是基于 ICMP 协议来工作的,「 ICMP 」全称为 Internet 控制报文协议( Internet Control Message Protocol)。ping 命令会发送一份ICMP回显请求报文给目标主机,并等待目标主机返回ICMP回显应答。因为ICMP协议会要求目标主机在收到消息之后,必须返回ICMP应答消息给源主机,如果源主机在一定时间内收到了目标主机的应答,则表明两台主机之间网络是可达的。
过程

  1. 假设有两个主机,主机A(192.168.0.1)和主机B(192.168.0.2),现在我们要监测主机A和主机B之间网络是否可达,那么我们在主机A上输入命令:ping 192.168.0.2
  2. 此时,ping命令会在主机A上构建一个 ICMP的请求数据包(数据包里的内容后面再详述),然后 ICMP协议会将这个数据包以及目标IP(192.168.0.2)等信息一同交给IP层协议。
  3. IP层协议得到这些信息后,将源地址(即本机IP)、目标地址(即目标IP:192.168.0.2)、再加上一些其它的控制信息,构建成一个IP数据包。
  4. IP数据包构建完成后,还不够,还需要加上MAC地址,因此,还需要通过ARP映射表找出目标IP所对应的MAC地址。当拿到了目标主机的MAC地址和本机MAC后,一并交给数据链路层,组装成一个数据帧,依据以太网的介质访问规则,将它们传送出出去。
  5. 当主机B收到这个数据帧之后,会首先检查它的目标MAC地址是不是本机,如果是就接收下来处理,接收之后会检查这个数据帧,将数据帧中的IP数据包取出来,交给本机的IP层协议,然后IP层协议检查完之后,再将ICMP数据包取出来交给ICMP协议处理,当这一步也处理完成之后,就会构建一个ICMP应答数据包,回发给主机A
  6. 在一定的时间内,如果主机A收到了应答包,则说明它与主机B之间网络可达,如果没有收到,则说明网络不可达。除了监测是否可达以外,还可以利用应答时间和发起时间之间的差值,计算出数据包的延迟耗时。
    我们知道,ping命令是基于ICMP协议来实现的。那么我们再来看下图,就明白了ICMP协议又是通过IP协议来发送的,即ICMP报文是封装在IP包中。
    在这里插入图片描述
    IP协议是一种无连接的,不可靠的数据包协议,它并不能保证数据一定被送达,那么我们要保证数据送到就需要通过其它模块来协助实现,这里就引入的是ICMP协议。

87.DNS解析的过程

本地->域名->根->告诉去找哪个域名->返回

  1. DNS是由解析器以及域名服务器组成的。
  2. 域名服务器是指保存有该网络中所有主机的域名和对应IP地址,并具有将域名转换为IP地址功能的服务器。
  3. DNS使用TCP与UDP端口号都是53,主要使用UDP,服务器之间备份使用TCP。
  4. 域名到IP地址的解析过程的要点如下:
    1. 当某一个应用进程需要主机名解析为IP地址时,该应用进程就调用解析程序,并成为DNS的一个客户,把待解析的域名放在DNS请求报文中,以UDP用户数据报方式发给本地域名服务器。
    2. 本地域名服务器在查找域名后,把对应的IP地址放在回答报文中返回。应用进程获得目的主机的IP地址后即可进行通信。
    3. 若本地域名服务器不能回答该请求,则此域名服务器就暂时成为DNS中的另一个客户,并向其他域名服务器发出查询请求。这种过程直至找到能够回答该请求的域名服务器为止。
      在这里插入图片描述
86.1 DNS 递归VS 迭代
  1. 递归:问了DNS服务器后,它就自己去甲,甲不知道就去问乙,直到问出结果。
  2. 迭代:问了DNS服务器后,它会告诉你去问甲,甲不知道,告诉你去乙,不断的这样下去,直到有一个服务器能回给一个正确的信息。

(1)递归查询
递归查询是一种DNS 服务器的查询模式,在该模式下DNS 服务器接收到客户机请求,必须使用一个准确的查询结果回复客户机。如果DNS 服务器本地没有存储查询DNS 信息,那么该服务器会询问其他服务器,并将返回的查询结果提交给客户机。
(2)迭代查询
DNS 服务器另外一种查询方式为迭代查询,DNS 服务器会向客户机提供其他能够解析查询请求的DNS 服务器地址,当客户机发送查询请求时,DNS 服务器并不直接回复查询结果,而是告诉客户机另一台DNS 服务器地址,客户机再向这台DNS 服务器提交请求,依次循环直到返回查询的结果

87.2 DNS算法有哪些?
  1. 主机向本地域名服务器的查询一般都是采用递归查询,即如果主机所询问的本地域名服务器不知道被查询域名的IP地址,那么本地域名服务器就以DNS客户的身份,向其他根域名服务器继续发出查询请求报文,而不是让该主机自己进行下一步的查询。因此,递归查询返回的查询结果或是所要查询的IP地址,或是报错。
  2. 本地域名服务器向根服务器的查询通常采用迭代查询,即当根域名服务器收到本地域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的IP地址,要么告诉本地域名服务器“下一次应向哪个域名服务器进行查询”。
  3. 然后让本地域名服务器进行后续的查询。根域名服务器通常把自己知道的顶级域名服务器的IP地址告诉本地域名服务器,让本地域名服务器再向顶级域名服务器查询。
  4. 顶级域名服务器在收到本地域名服务器的查询请求后,要么给出所要查询的IP地址,要么告诉本地域名服务器下一步应当向哪一个权限域名服务器进行查询。本地域名服务器就这样进行迭代查询。

88.程序编译和链接了解吗,有什么作用?

在这里插入图片描述

88.1 静态链接和动态链接的差别

**静态链接方式:**在程序执行之前完成所有的组装工作,生成一个可执行的目标文件(EXE文件)。

**动态链接方式:**在程序已经为了执行被装入内存之后完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝。

静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终的EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。

采用动态链接库的优点:(1)更加节省内存;(2)DLL文件与EXE文件独立,只要输出接口不变,更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性。

动态链接是相对于静态链接而言的。所谓静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。换句话说,函数和过程的代码就在程序的exe文件中,该文件包含了运行时所需的全部代码。当多个程序都调用相同函数时,内存中就会存在这个函数的多个拷贝,这样就浪费了宝贵的内存资源。而动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。一般情况下,如果一个应用程序使用了动态链接库,Win32系统保证内存中只有DLL的一份复制品

88.2 动态链接库的两种链接方法

(1) 装载时动态链接(Load-time Dynamic Linking):这种用法的前提是在编译之前已经明确知道要调用DLL中的哪几个函数,编译时在目标文件中只保留必要的链接信息,而不含DLL函数的代码;当程序执行时,调用函数的时候利用链接信息加载DLL函数代码并在内存中将其链接入调用程序的执行空间中(全部函数加载进内存),其主要目的是便于代码共享。(动态加载程序,处在加载阶段,主要为了共享代码,共享代码内存)

(2) 运行时动态链接(Run-time Dynamic Linking):这种方式是指在编译之前并不知道将会调用哪些DLL函数,完全是在运行过程中根据需要决定应调用哪个函数,将其加载到内存中(只加载调用的函数进内存),并标识内存地址,其他程序也可以使用该程序,并用LoadLibrary和GetProcAddress动态获得DLL函数的入口地址。(dll在内存中只存在一份,处在运行阶段)

上述的区别主要在于阶段不同,编译器是否知道进程要调用的dll函数。动态加载在编译时知道所调用的函数,而在运行态时则必须不知道。

89. Linux nm 命令使用

很多时候我们并不首要关注库本身的实现,或者根本无法看到其底层逻辑,但又必须确认某些函数或变量的命名(如排查定义冲突)问题,这时候就需要用到一个很基础但是很有用的工具“nm”了。

NAME       nm - list symbols from object filesSYNOPSIS       nm [-A|-o|--print-file-name] [-a|--debug-syms]          [-B|--format=bsd] [-C|--demangle[=style]]          [-D|--dynamic] [-fformat|--format=format]          [-g|--extern-only] [-h|--help]          [-l|--line-numbers] [-n|-v|--numeric-sort]          [-P|--portability] [-p|--no-sort]          [-r|--reverse-sort] [-S|--print-size]          [-s|--print-armap] [-t radix|--radix=radix]          [-u|--undefined-only] [-V|--version]          [-X 32_64] [--defined-only] [--no-demangle]          [--plugin name] [--size-sort] [--special-syms]          [--synthetic] [--target=bfdname]          [objfile...]DESCRIPTION       GNU nm lists the symbols from object files objfile....  If no object       files are listed as arguments, nm assumes the file a.out.       For each symbol, nm shows:        # ... run man nm for detials.

简单说的话,就是可以帮你列举出该目标中定义的符合要求的符号。要求可以很多,主要通过参数实现:外部引入的、内部定义的、动态的… 也可以添加参数使nm同时打印行号、文件名等相关信息。

89. 对称加密与非对称加密

89.1 对称加密

对称加密是最快速、最简单的一种加密方式,加密(encryption)与解密(decryption)用的是同样的密钥。
对称加密通常使用的是相对较小的密钥,一般小于256 bit。因为密钥越大,加密越强,但加密与解密的过程越慢。如果你只用1 bit来做这个密钥,那黑客们可以先试着用0来解密,不行的话就再用1解;但如果你的密钥有1 MB大,黑客们可能永远也无法破解,但加密和解密的过程要花费很长的时间。密钥的大小既要照顾到安全性,也要照顾到效率,是一个trade-off。常见的 对称加密 算法主要有DES、3DES、AES

89.2 非对称加密

非对称加密为数据的加密与解密提供了一个非常安全的方法,它使用了一对密钥,公钥(public key)和私钥(private key)。私钥只能由一方安全保管,不能外泄,而公钥则可以发给任何请求它的人。非对称加密使用这对密钥中的一个进行加密,而解密则需要另一个密钥。比如,你向银行请求公钥,银行将公钥发给你,你使用公钥对消息加密,那么只有私钥的持有人–银行才能对你的消息解密。与对称加密不同的是,银行不需要将私钥通过网络发送出去,因此安全性大大提高。目前最常用的非对称加密算法是RSA算法。

90.MD5为何是不安全的?

因为MD5其实采用的是一种特殊的对应映射的方式。比如您的密码是“ qwerty”(不好的主意),则在数据库中您将拥有d8578edf8458ce06fbc5bb76a58c5ca4。但今天,关于这个加密,已经不再有太大的安全性了,理由如下:

  1. 暴力攻击速度很快
  2. 字典表很大.
  3. 碰撞.MD5算法在其加密方法中也证明了问题。冲突是当两个单词产生相同的哈希值时
    安全的算法具有良好的抗冲突性也就是说,对于不同的单词,您获得相同哈希值的可能性较低,但是MD5的抗冲突性较低.

90. 操作系统是如何处理中断的?

90.1 中断的概念

所谓中断是指处理器对系统中或系统外发生的异步事件的响应。异步事件是指无一定时序关系的随机发生的事件,如外部设备完成了数据传输任务,某一实时控制设备出现异常情况等。

“中断”这个名称来源于:当发生某个异步事件后,中断了处理器对当前程序的执行,而转去处理该异步事件(称作执行该事件的中断处理程序)。在该异步事件处理完了之后,处理器再转回原程序的中断点继续执行。这种情况很像我们日常生活中的一些情况。例如,某人正在看书,此时电话响了(异步事件),于是用书签记住正在看的那一页(中断点),再去接电话(响应异步事件并进行处理),接完电话后再从被打断那页继续向下看(返回原程序的中断点执行)。

最初,中断技术是用于向处理器报告某个“设备已完成操作”的一种手段,以免处理器不断地测试设备状态而消耗大量宝贵的处理器时间,后来,中断技术的应用越来越广泛。中断是所有要打断处理器的正常工作次序,并要求其去处理某一事件的一种常用手段。我们把引起中断的事件称为中断事件或中断源;中断源向处理器发出的请求信号称为中断请求;而把处理中断事件的程序称为中断处理程序;发生中断时正在执行的程序的暂停点叫作中断断点;处理器暂停当前程序转而处理中断的过程称为中断响应。中断处理结束之后恢复原来程序的执行被称为中断返回。

一个计算机系统提供的中断源的有序集合一般被称为中断字,这是一个逻辑结构,在不同的处理器有着很不相同的实现方式。在一台计算机中有多少中断源,是根据各个计算机系统的需要安排的。 Intel的x86微处理器能处理256种不同的中断。

为了使得中断装置可以找到恰当的中断处理程序,专门设计了中断处理程序入口地址映射表,又称中断向量表。表中的每一项称为一个中断向量,主要由程序状态字PSW和指令计数器PC的值组成。不同性质的中断源需要用不同的中断处理程序来处理,也就是对应不同的中断向量。通过中断向量,可以找到中断处理程序在内存中的位置。

中断技术解决了主机和外设并行工作的问题,消除了因外设的慢速而使得主机等待的现象,为多机操作和实时处理提供了硬件基础。一般来说中断具有以下作用。

能充分发挥处理器的使用效率。因为输入输出设备可以用中断的方式同处理器通信,报告其完成处理器所要求的数据传输的情况和问题,这样可以免除处理器不断地査询和等待,从而大大提高处理器的效率。

提高系统的实时能力。因为具有较高实时处理要求的设备,可以通过中断方式请求及时处理,从而使处理器立即运行该设备的处理程序(也是该中断的中断处理程序)。所以目前的各种微型机、小型机及大型机均有中断系统。

从用户的角度来看,中断正如字面的含义,即正常执行的程序被打断,当完成中断处理后再恢复执行。这完全由操作系统控制,用户程序不必做任何特殊处理。

90.1 中断分类

中断分为同步中断,和异步中断。
同步中断:是指当指令执行时,由CPU控制单元产生,之所以产生同步,是因为只有在一条指令执行完毕后,CPU才会发出中断。而不是发生在代码指令执行期间,比如系统调用。同步中断称为异常(exception)。中断是由外部事件引发的,而异常则是由正在执行的指令引发的。
异步中断:由其他硬件设备按照CPU时钟信号随机产生,即意味着中断能够在指令执行期间发生,例如键盘中断。异步中断被称为中断(interrupt)。

90.2 中断处理

中断处理:

  1. 屏蔽外部中断(FLAGS IF=0)
  2. 识别中断源,确定中断号
  3. 保存中断点,将中断的指令压栈保存到内存中
  4. 保存现场,压栈寄存器数据
  5. 执行中断服务程序,根据需要可选开放中断(FLAGS IF=1)
  6. 恢复现场,回到程序继续执行.

在这里插入图片描述

91. 移动构造函数在何时使用?

解决的问题:那么当类中包含指针类型的成员变量,使用其它对象来初始化同类对象时,怎样才能避免深拷贝导致的效率问题呢?C++11 标准引入了解决方案,该标准中引入了右值引用的语法,借助它可以实现移动语义。

所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。

事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

92. C++多重继承会带来什么问题?(菱形继承)

菱形继承是这个样子:
在这里插入图片描述

B和C从A中继承,而D多重继承于B,C。那就意味着D中会有A中的两个拷贝。因为成员函数不体现在类的内存大小上,所以实际上可以看到的情况是D的内存分布中含有2组A的成员变量。

92.1 所以菱形继承的解决方案是啥?

一种通过作用域访问符::来明确调用。
一种是引入虚拟继承。虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示
虚拟继承还要有一个指针,指向基类对象。这个指针叫虚类指针。
在这里插入图片描述

由于有了间接性和共享性两个特征,所以决定了虚继承体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同。

2.1时间:在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。

2.2空间:由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之 多继承节省空间。虚拟继承与普通继承不同的是,虚拟继承可以防止出现diamond继承时,一个派生类中同时出现了两个基类的子对象。也就是说,为了保证 这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针。

93. 协程是啥?

协程是运行在线程之上的,协程并没有增加线程数量,只是在线程的基础上通过分时复用的方式运行多个协程。而协程的切换在用户态完成。协程中不能调用导致线程阻塞的操作。

在有多用户并发的时候,多线程是其中的一种办法,但随之而来,也带来的两个问题是:一是系统线程会占用非常多的内存空间,二是过多的线程切换会占用大量的系统时间。
但协程可以解决上面的问题:
协程运行在线程之上,当一个协程执行完成之后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成。切换的代价比从用户态到内核态的代价小多了。
回到上面的问题,我们只需要启动100个线程,每个线程上运行100个协程,这样不仅减少了线程切换开销,而且还能够同时处理10000个读取数据库的任务,很好的解决了上述任务。
实际上协程并不是什么银弹,协程只有在等待IO的过程中才能重复利用线程。
假设协程运行在线程之上,并且协程调用了一个阻塞IO操作,这时候会发生什么?实际上操作系统并不知道协程的存在,它只知道线程,因此在协程调用阻塞IO操作的时候,操作系统会让线程进入阻塞状态,当前的协程和其它绑定在该线程之上的协程都会陷入阻塞而得不到调度,这往往是不能接受的。

因此在协程中不能调用导致线程阻塞的操作。也就是说,协程只有和异步IO结合起来,才能发挥最大的威力。
那么如何处理在协程中调用阻塞IO的操作呢?一般有2种处理方式:

在调用阻塞IO操作的时候,重新启动一个线程去执行这个操作,等执行完成后,协程再去读取结果。这其实和多线程没有太大区别。
对系统的IO进行封装,改成异步调用的方式,这需要大量的工作,最好寄希望于编程语言原生支持。

在协程中尽量不要调用阻塞IO的方法,比如打印,读取文件,Socket接口等,除非改为异步调用的方式,并且协程只有在IO密集型的任务中才会发挥作用。

协程只有和异步IO结合起来才能发挥出最大的威力。

**实现一个用户态线程有两个必须要处理的问题:**一是碰着阻塞式I\O会导致整个进程被挂起;二是由于缺乏时钟阻塞,进程需要自己拥有调度线程的能力。如果一种实现使得每个线程需要自己通过调用某个方法,主动交出控制权。那么我们就称这种用户态线程是协作式的,即是协程。

93. 协程的底层原理是什么?

类似于一个执行体
在这里插入图片描述

94.内存池怎么可以保证不会产生碎片?

内存池是在真正使用之前,先申请分配一定数量的,大小相等的内存块留作备用。当有新的需求时,就从内存块从分出一部分内存块,若内存块不够再继续申请新的内存。这样做,可以显著的避免了内存碎片。

94.1 内存池出现的原因及必要性

原因:当频繁调用Malloc/free,new/delete 进行内存分配与释放时,有如下弊端:

  1. 调用malloc或new,系统需要先利用最先匹配,最优匹配,在空闲内存块表中查找一块空闲内存,调用free/delete可能需要合并空闲内存块,这可能会产生额外的开销。
  2. 频繁使用时可能会产生大量内存碎片,从而降低程序运行效率。
  3. 容易造成内存泄漏。
    使用内存池的好处
  4. 比malloc/new申请内存的方式块。
  5. 不会或很少产生内存碎片。
  6. 可避免内存泄漏。
94.2内存碎片解决Linux系统还是Windows的,为什么操作系统设计这种产生内存碎片的机制

防止系统充斥着各种内部碎片和外部碎片,这样,再需要重新分配内存时,就找到可分配的内存,降低整个内存的空间利用率。频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。
内部碎片的产生:因为所有的内存分配必须起始于可被 4、8 或 16 整除(视处理器体系结构而定)的地址或者因为MMU的分页机制的限制,决定内存分配算法仅能把预定大小的内存块分配给客户。假设当某个客户请求一个 43 字节的内存块时,因为没有适合大小的内存,所以它可能会获得 44字节、48字节等稍大一点的字节,因此由所需大小四舍五入而产生的多余空间就叫内部碎片。
外部碎片的产生: 频繁的分配与回收物理页面会导致大量的、连续且小的页面块夹杂在已分配的页面中间,就会产生外部碎片。假设有一块一共有100个单位的 连续空闲内存空间,范围是099。如果你从中申请一块内存,如10个单位,那么申请出来的内存块就为09区间。这时候你继续申请一块内存,比如说5个 单位大,第二块得到的内存块就应该为10~14区间。如果你把第一块内存块释放,然后再申请一块大于10个单位的内存块,比如说20个单位。因为刚被释放 的内存块不能满足新的请求,所以只能从15开始分配出20个单位的内存块。现在整个内存空间的状态是09空闲,1014被占用,15~24被占 用,2599空闲。其中09就是一个内存碎片了。如果1014一直被占用,而以后申请的空间都大于10个单位,那么09就永远用不上了,变成外部 碎片。

94.11 Linux系统解决内存碎片的机制

方法一:伙伴系统,用于管理内存,避免内存碎片。伙伴系统的宗旨就是用最小的内存块来满足内核的对于内存的请求。
方法二:高速slab层,用于管理内核分配碎片,避免碎片。
详解
避免外碎片的方法有两种:

  1. 利用分页单元将一组非连续的空闲页框映射到连续的线性地址。核心词就是映射
  2. 开发一种适当的技术来记录当前现存的空闲的连续页框的地址,以尽量避免为满足小块的请求而分割大的空闲块。核心词是记录

第一种方案的意思是,我们使用地址转换技术,把非连续的物理地址转换成连续的线性地址。
第二种方案的意思是,开发一种特有的分配技术来记录下来空闲内存的情况,从而解决内存碎片问题。
Linux采用了第二种方案,因为在某些情况下,系统的确需要连续的物理地址(DMA处理器可以直接访问总线)

linux kernel 通过把整个物理内存划分成以一个个page进行管理,管理器就是伙伴系统,它的最小分配单元就是page。但是对于小于page的内存分配,如果直接分配一个page,是一个很大的浪费。linux kernel 通过slab来实现对小于page大小的内存分配。slab把page按2的m次幂进行划分一个个字节块,当kmalloc申请内存时,通过slab管理器返回需要满足申请大小的最小空闲内存块。核心词:伙伴系统划分page->slab划分成一个个字节块。

slub主要是针对slab的对象管理数据的优化版本,相比于slab,slub提供更小的管理成本开销。而且slub对多核系统的支持也更加友好。细节这里就不展开讲。
所以kernel的内存管理是个2层分层系统,从下往上依次为:
第一层为全部物理内存:其管理器为伙伴系统,最小管理单位为page;
第二层为slab page:其管理器为slab/slub,最小管理单位为2的m次幂的字节块;

  1. Linux系统内存管理之伙伴系统分析
94.12 谈谈伙伴系统

1.引入伙伴系统的原因?
为内核提供了一种用于分配一组连续的页而建立的一种高效的分配策略,有效的解决了外碎片的问题。

  1. 伙伴系统算法的管理内存方法
  1. 把所有的空闲页框分为11个块链表,每个块链表分配包含1、2、4、8、16、32、64、128、256、512和1024个连续页框的页框块。最大可以申请1024个连续页框,也即4MB大小的连续空间。

  2. 假设要申请一个256个页框的块,先从256个页框的链表中查找空闲块,如果没有,就去512个页框的链表中找,找到了即将页框分为两个256个页框的块,一个分配给应用,另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块,继续向1024个页框的链表查找,如果仍然没有,则返回错误。

  3. 页框块在释放时,会主动将两个连续的页框块合并成一个较大的页框块。
    当释放一个块时,先在其对应的链表中考查是否有伙伴存在,如果没有伙伴块,就直接把要释放的块挂入链表头;如果有,则从链表中摘下伙伴,合并成一个大块,然后继续考察合并后的块在更大一级链表中是否有伙伴存在,直到不能合并或者已经合并到了最大的块。

  1. 伙伴算法的缺点
  1. 小块会阻碍大块的合并。

  2. 算法会导致如果所需的内存大小不是2的幂次,就会有部分页面浪费严重。

  3. 拆分和合并也涉及到较多的链表和位图的操作,开销较大。

  1. 伙伴算法对伙伴的定义:
  1. 两个块大小相同;

  2. 两个块地址连续;

  3. 两个块必须是同一个大块中分离出来的;

94.13 请你谈谈slab机制?
  1. slab机制针对的目标?
    slab是Linux系统的一种内存分配机制,针对一些经常分配并释放的对象,如进程描述符。

  2. 为什么采用slab机制?
    对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,不仅会造成大量的内存碎片,而且处理速度也太慢。

  3. slab的内存分配机制是怎么样的?
    slab分配器是基于对象进行管理的,相同类型的对象归为一类(如进程描述符是一类),每次当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存到该列表中,而不是直接返回给伙伴系统,从而避免这些内存碎片。

94.2 为什么这个内存池效率更慢,一般设计内存池为了更快分配内存?

内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用。当程序员申请内存时,从池中取出一块动态分配,当程序员释放时,将释放的内存放回到池内,再次申请,就可以从池里取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。
设计内存碎片的原因

  1. 防止内部碎片或外部碎片。

95. 大小端的原理?

首先数据有高位和低位,如123,则1为高位,3为低位。
在无论什么机器上,地址都是从低往高的,也就是如000-001-002-003等。
大端模式(Big-endian):高位数放在低位地址上,低位数放在高位地址上。这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。
小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址。这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。将数据从低位向高位放。

95.1 平时的PC机是大端还是小端?

大部分用户的操作系统(如:Windows、FreeBsd、Linux)是小端字节序。少部分,如:Mac OS 是大端字节序。

95.2 如何判断是大端还是小端?

1.用指针的办法:把变量的地址强制类型转换为char*,这样就可以每次取出一个字节的内容。

声明一个int整数=1 ,使用强制类型转换,转换成(char *),若取得的低地址的值就是1的话,则证明小端存储。否则就是大端存储
code

#include<stdio.h>
int main()
{  
	int a = 1;//这里为了方便,以1为例 
	char*p = (char*)(&a);
    if (*p == 1)
    {	 
   		printf("little endian\n");//小端存储(你侬我侬)  
    }   
    else
    {	
         printf("big endian\n");//大端存储 (令人头大)
    } 
    return 0;
} 
  1. 用union的特性来实现:
int main()
{
    union{
      int a;  //4 bytes
      char b; //1 byte
    } data;
  
    data.a = 1; //占4 bytes,十六进制可表示为 0x 00 00 00 01

    //b因为是char型只占1Byte,a因为是int型占4Byte
    //所以,在联合体data所占内存中,b所占内存等于a所占内存的低地址部分 
    if(1 == data.b){ //走该case说明a的低字节,被取给到了b,即a的低字节存在了联合体所占内存的(起始)低地址,符合小端模式特征
      printf("Little_Endian\n");
     } else {
      printf("Big_Endian\n");
     }
   return 0;
}

97. 结构体和类的区别?

首先,结构体有数据,但没有对数据的操作。结构体默认的访问控制权限是public.而类的默认访问控制权限是private,

  1. 结构体是很多数据的结构,里面不能有对这些数据的操作,而类class是数据以及对这些数据的操作的封装,是面向对象的基础。
  2. class对成员变量有访问控制权限的控制,而struct是没有的,而在struct外可以访问结构体任何一个变量,而在类外,是不可以访问private的成员变量。
  3. 最终的一个差别是访问控制权限:struct的访问控制权限默认是public,而类的默认访问控制权限是private,

98. 哈希算法原理?

散列表是根据关键码值而直接进行访问的数据结构,也就是说,它通过把关键码值映射到一个位置来访问记录,从而加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
常见的解决冲突的几种方法:

  1. 线性探查法。
  2. 双散列函数法。

常见的构造散列函数的方法:

  1. 直接寻址法。
  2. 数字分析法。
  3. 平方取中法。
  4. 折叠法。
  5. 随机数法。
  6. 除留余数法。

99. mutable和volatile是啥呀?

mutable:
在C++中,mutable是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其mutable成员也可以被修改。
mutable在类中只能够修饰非静态数据成员。mutable 数据成员的使用看上去像是骗术,因为它能够使const函数修改对象的数据成员。然而,明智地使用 mutable 关键字可以提高代码质量,因为它能够让你向用户隐藏实现细节,而无须使用不确定的东西。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。

const承诺的是一旦某个变量被其修饰,那么只要不使用强制转换(const_cast),在任何情况下该变量的值都不会被改变,无论有意还是无意,而被const修饰的函数也一样,一旦某个函数被const修饰,那么它便不能直接或间接改变任何函数体以外的变量的值,即使是调用一个可能造成这种改变的函数都不行。这种承诺在语法上也作出严格的保证,任何可能违反这种承诺的行为都会被编译器检查出来。

保护类的成员变量不在成员函数中被修改,是为了保证模型的逻辑正确,通过用const关键字来避免在函数中错误的修改了类对象的状态。并且在所有使用该成员函数的地方都可以更准确的预测到使用该成员函数的带来的影响。而mutable则是为了能突破const的封锁线,让类的一些次要的或者是辅助性的成员变量随时可以被更改。没有使用const和mutable关键字当然没有错,const和mutable关键字只是给了建模工具更多的设计约束和设计灵活性,而且程序员也可以把更多的逻辑检查问题交给编译器和建模工具去做,从而减轻程序员的负担。
volatile
象const一样,volatile是一个类型修饰符。volatile修饰的数据,编译器不可对其进行执行期寄存于寄存器的优化。这种特性,是为了满足多线程同步、中断、硬件编程等特殊需要。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的直接访问。

volatile原意是“易变的”,但这种解释简直有点误导人,应该解释为“直接存取原始内存地址”比较合适。“易变”是相对与普通变量而言其值存在编译器(优化功能)未知的改变情况(即不是通过执行代码赋值改变其值的情况),而是因外在因素引起的,如多线程,中断等。编译器进行优化时,它有时会取一些值的时候,直接从寄存器里进行存取,而不是从内存中获取,这种优化在单线程的程序中没有问题,但到了多线程程序中,由于多个线程是并发运行的,就有可能一个线程把某个公共的变量已经改变了,这时其余线程中寄存器的值已经过时,但这个线程本身还不知道,以为没有改变,仍从寄存器里获取,就导致程序运行会出现未定义的行为。并不是因为用volatile修饰了的变量就是“易变”了,假如没有外因,即使用volatile定义,它也不会变化。而加了volatile修饰的变量,编译器将不对其相关代码执行优化,而是生成对应代码直接存取原始内存地址。
  一般说来,volatile用在如下的几个地方:
  1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
  2、多任务环境下各任务间共享的标志应该加volatile;
  3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;

volatile int i=10;   
int a = i;     
int b = i;  //其他代码,并未明确告诉编译器,对i进行过操作  

volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据(即10)放在b中,而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的直接访问。

100. vector的resize和reserve有什么不同啊?

std::vector的reserve和resize的区别

  1. reserve: 分配空间,更改capacity但不改变size
  2. resize: 分配空间,更改capacity也改变size

如果知道vector的大小,resize一下可以当数组来用,不会分配多余的内存。

reserve是容器预留空间,但并不真正创建元素对象,在创建对象之前,不能引用容器内的元素,因此当加入新的元素时,需要用push_back()/insert()函数。

resize是改变容器的大小,并且创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。

再者,两个函数的形式是有区别的,reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。

101. malloc是如何实现的?

可以基于伙伴系统实现,也可以使用基于链表的实现

  1. 将所有空闲内存块连成链表,每个节点记录空闲内存块的地址、大小等信息
  2. 分配内存时,找到大小合适的块,切成两份,一分给用户,一份放回空闲链表
  3. free时,直接把内存块返回链表
  4. 解决外部碎片:将能够合并的内存块进行合并

在Linux下是如何使用malloc的?

  1. 若申请分配的内存小于128k。调用brk函数,其主要移动_enddata指针(该指针指向的是堆段的末尾地址,而不是数据段的末尾地址。
  1. malloc分配了这块内存,然后如果从不去访问它,那么物理页是不会被分配的。
  2. 当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作
  1. 开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。

malloc实现原理:
Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。

当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。

1、空闲存储空间以空闲链表的方式组织(地址递增),每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。( 因为程序中的某些地方可能不通过malloc调用申请,因此malloc管理的空间不一定连续。)
2、当有申请请求时,malloc会扫描空闲链表,直到找到一个足够大的块为止(首次适应)(因此每次调用malloc时并不是花费了完全相同的时间)。
3、如果该块恰好与请求的大小相符,则将其从链表中移走并返回给用户。如果该块太大,则将其分为两部分,尾部的部分分给用户,剩下的部分留在空闲链表中(更改头部信息)。因此malloc分配的是一块连续的内存。
4、释放时,首先搜索空闲链表,找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合为一个更大的块,以减少内存碎片。

因为brk、sbrk、mmap都属于系统调用,若每次申请内存,都调用这三个,那么每次都会产生系统调用,影响性能;其次,这样申请的内存容易产生碎片,因为堆是从低地址到高地址,如果高地址的内存没有被释放,低地址的内存就不能被回收。
所以malloc采用的是内存池的管理方式(ptmalloc),Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。

102. C++ 内存管理

102. 1 C++简化管理内存的方法
  1. 使用智能指针。对于智能指针来说,如果存在互相引用现象,则会导致内存无法释放,所以使用时务必要规避这种情况。
  2. 少用指针,可尽量使用一些迭代器。
  3. 将堆中分配的内存组成森林,然后删除根结点的时候级联删除所有子结点。这样就简化了内存管理:从管理所有内存到只要管理根结点。
102.2 操作系统是如何进行内存管理?
  1. 在C++中,内存分为五个区,堆,栈,自由存储区,全局/静量存储区,常量区。然后分开介绍各个部分。

a. 栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
b. 堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
c. 自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。
d. 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
e. 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

参考1

103. TCP中的get和post的差別

HTTP协议定义了多种请求方式,具体如下:
GET:获取资源,用来请求访问已被URI(统一资源标志符,和URL是包含和被包含的关系)识别的资源。
POST:一般用来想URL指定的资源提交数据,数据就放在报文的body段之中。用来传输实体的主体,虽然GET也可以实现,但是一般不用。
PUT:传输文件。但是鉴于PUT方法自身不带验证机制,任何人都可以上传文件,存在安全性问题,因此一般网站都不采用该方法。
HEAD:获得报文首部。和GET请求一样,只是不返回报文主体部分。
DELETE:删除文件。同样不带验证机制,存在安全性问题。
OPTIONS:询问指定的请求URI支持哪些方法。
TRACE:追踪路径,让Web服务器将之前的请求通信环回给客户端的方法。
CONNECT:要求在与代理服务器通信时建立隧道,实现隧道协议进行TCP通信。

POST与GET请求区别的常见误区:
1.请求参数长度限制:GET请求长度最多1024kb,POST对请求数据没有限制
关于此点,在HTTP协议中没有对URL长度进行限制,这个限制是不同的浏览器及服务器由于有不同的规范而带来的限制。
2.GET请求一定不能用request body传输数据:GET可以带request body,但不能保证一定能被接收到。如果你用GET服务,在request body偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你读出数据,有些服务器直接忽略。
3.POST比GET安全性要高:这里的安全是相对性,通过GET提交的数据都将显示到URL上,页面会被浏览器缓存,其他人查看历史记录会看到提交的数据,而POST不会。另外GET提交数据还可能会造成CSRF攻击。
4.GET产生一个TCP数据包,POST产生两个TCP数据包:对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200 OK(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 OK(返回数据)。注意,尽管POST请求会分两次,但body 是紧随在 header 后面发送的,根本不存在『等待服务器响应』一说。

POST和GET请求的区别小结:
请求参数:GET请求参数是通过URL传递的,多个参数以&连接,POST请求放在request body中。
请求缓存:GET请求会被缓存,而POST请求不会,除非手动设置。
收藏为书签:GET请求支持,POST请求不支持。
安全性:POST比GET安全,GET请求在浏览器回退时是无害的,而POST会再次请求。
历史记录:GET请求参数会被完整保留在浏览历史记录里,而POST中的参数不会被保留。
编码方式:GET请求只能进行url编码,而POST支持多种编码方式。
对参数的数据类型:GET只接受ASCII字符,而POST没有限制。

这里还想补充说明一点,就是通过浏览器地址栏输入URL访问资源的方式都是GET请求。

http://blog.itpub.net/31561268/viewspace-2667140/

103.1 post 请求体 都有哪些形式?

在这里插入图片描述

参考1

104. linux怎么修改句柄数?

ulimit -a:可查看当前的文件句柄数
ulimit -n 2048:这样就可以修改当前的文件句柄数。

105. 操作系统里面fork得到的子进程和父进程有哪些是共享的?

fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。

fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。

由子进程自父进程继承到

  1. 进程的资格。
  2. 环境
  3. 堆栈
  4. 内存
  5. 打开文件的描述符
  6. 执行时关闭(close-on-exec) 标志
  7. 信号(signal)控制设定
  8. nice值
    9 进程组号

107. 智能指针中的强指针和弱指针。

一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。share_ptr就是强引用。相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

std::shared_ptr:shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指向的堆内存。shared_ptr内部的引用计数是安全的,但是对象的读取需要加锁。

但还不够,因为使用 std::shared_ptr 仍然需要使用 new 来调用,这使得代码出现了某种程度上的不对称。

std::make_shared 就能够用来消除显式的使用 new,所以std::make_shared 会分配创建传入参数中的对象, 并返回这个对象类型的std::shared_ptr指针。

std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全:

108. Qt的智能指针

QSharedPointer对拥有的资源数进行计数,当计数为0时,自动释放指向的资源

QPointer:特意用在QObject及其派生类上面。当其指向的对象呗删除后,能自动将指针置为NULL。

QSharedDataPointer:解决隐式共享类的问题。

QWeakPointer:解决QSharedPointer循环引用的问题。

  1. QSharedPointer:QSharedPointer 内部维持着对拥有的内存资源的引用计数。QSharedPointer 是线程安全的,因此即使有多个线程同时修改 QSharedPointer 对象也不需要加锁。这里要特别说明一下,虽然 QSharedPointer 是线程安全的,但是 QSharedPointer 指向的内存区域可不一定是线程安全的。所以多个线程同时修改 QSharedPointer 指向的数据时还要应该考虑加锁的。
    2.QScopedPointer:QScopedPointer 类似于 C++ 11 中的 unique_ptr。当我们的内存数据只在一处被使用,用完就可以安全的释放时就可以使用 QScopedPointer。
    3.QScopedArrayPointer:如果我们指向的内存数据是一个数组,这时可以用 QScopedArrayPointer。QScopedArrayPointer 与 QScopedPointer 类似,用于简单的场景。
  2. QPointer:QPointer 与其他的智能指针有很大的不同。其他的智能指针都是为了自动释放内存资源而设计的。 QPointer 智能用于指向 QObject 及派生类的对象。当一个 QObject 或派生类对象被删除后,QPointer 能自动把其内部的指针设为 0。这样我们在使用这个 QPointer 之前就可以判断一下是否有效了。这个能尽量避免产生野指针(悬挂指针)
  3. QSharedDataPointer:这个类是帮我们实现数据的隐式共享的。我们知道 Qt 中大量的采用了隐式共享和写时拷贝技术。Qt 中隐式共享和写时拷贝就是利用 QSharedDataPointer 和 QSharedData 这两个类来实现的。如果对象将要被改变并且其引用计数大于1,隐式共享会自动的从共享块中分离该对象。(这经常被称为写时复制)。隐式共享类可以控制它自己的内部数据。在它的要修改数据的成员函数中,它会在修改数据之前自动的分离。
    4.QWeakPointer:QWeakPointer不能用于直接取消引用指针,但它可用于验证指针是否已在另一个上下文中被删除。并且QWeakPointer对象只能通过QSharedPointer的赋值来创建。QWeakPointer不提供自动转换操作符来防止错误发生。即使QWeakPointer跟踪指针,也不应将其视为指针本身,因为它不能保证指向的对象保持有效。

109.gdb 调试的命令

https://www.bilibili.com/video/BV1W44y127Je?from=search&seid=6065740143463854871

110.指针数组与数组指针详解

指针数组:都是指针的数组

数组指针:用指针修饰的数组

指针数组:指针数组可以说成是”指针的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型,在32位系统中,指针占四个字节。都是指针的数组

char *arr[4] = {"hello", "world", "shannxi", "xian"};//arr就是我定义的一个指针数组,它有四个元素,每个元素是一个char *类型的指针,这些指针存放着其对应字符串的首地址。

数组指针:数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。

char (*pa)[4];

这代表的时候一个指针指向字符数组(*pa) char[4]

110.1 指针数组的使用?

主函数传参

指针数组常用在主函数传参,在写主函数时,参数有两个,一个确定参数个数,一个是用指针数组用来接收每个参数(字符串)的地址。
code

int main(int argc, char *argv[])

此时可以想象内存映像图,主函数的栈区有一个叫argv的数组,这个数组的元素是你输入的参数的地址,指向着只读数据区。

111.c语言函数参数的入栈顺序为从右向左的原因?

可动态变换参数个数

C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数。通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。

112. 断点是怎么实现的?

设断点->取得控制权->保留指令->替换成中断指令->执行中断指令->恢复运行

  1. 在具体的行号处设置断点。
  2. 通过 Ptrace 获得 tracee 的控制权。
  3. 保留当前 rip 的指令内容,并用 中断指令 替换
  4. 恢复运行,等待 trap 触发
  5. 恢复 rip 指令,结束调试

硬件断点:是由特殊的逻辑实现的,并且集成到了设备中。你可以想象硬件断点是一套可编程的比较器,连接到了程序地址总线上。这些比较器是由特定的地址赋值的。当代码执行的时候,如果该地址的所有bit都与程序总线一致,硬件断点逻辑就会产生一个暂停信号给CPU。

软件断电:a:在一些Opcode上,保留了一个专门的Bit,用于表明是软件断电。大多数的架构并不这样使用,原因是这个限制了指令集的灵活性,并且变长指令时也不易实现。而且还限制了代码的宽度。b:还有一种方式有一个专门的opcode,这个opcode有8位。无论何时断电被设置,开头的8bit的指令会被移除,然后被替换成8bit的断点opcode.原始的8bit指令被存储在断点表。

113.函数的调用栈是怎么实现的?

主函数中调用该函数的下一个命令入栈->参数从右向左入栈->局部变量入栈

入栈顺序:

  1. 主函数中函数调用后的下一个指令。
  2. 然后是函数的各个参数。在大多数的C编译器中,参数都是从右向左入栈的。
  3. 然后是函数中的局部变量。注意静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

114.操作系统的实模式和保护模式的区别

**区别:**在定义“逻辑地址”时看到保护模式和实模式的区别在于它是用段选择符而非段基地址,这也许就是保护模式的真谛所在,从段选择符入手,全面理解保护模式编程基本概念和寻址方式。在定义“逻辑地址”时看到保护模式和实模式的区别在于它是用段选择符而非段基地址,这也许就是保护模式的真谛所在,从段选择符入手,全面理解保护模式编程基本概念和寻址方式。

实模式:CPU复位(reset)或加电(power on)的时候就是以实模式启动,在这个时候处理器以实模式工作,不能实现权限分级,也不能访问20位以上的地址线,也就是只能访问1M内存。之后一般就加载操作系统模块,进入保护模式。
**保护模式:**操作系统接管CPU后.会使CPU进入保护模式.这时候可以发挥80x86的所有威力。包括权限分级.内存分页.等等等等各种功能
保护模式:在保护模式下,很重要的一点就是段寄存器不是直接存放段基址了,而是存放着段选择子(索引段描述符).那么段描述符存放在哪里呢?GDT,全局描述符表,全局描述符表会存放着所有的段描述符。接下来,我们描述一下保护模式下的寻址方式:

  1. 段寄存器存放段选择子
  2. CPU根据段选择子从GDT中找到段描述符。
  3. 从段描述符中取出段基址。
  4. 根据之前的公式,结合段基址和段内偏移,计算出物理地址。
    在这里插入图片描述

115. 32位和64位指的是一个什么样的概念

CPU在单位时间内能一次处理的二进制数的位数

通用寄存器的位数

区别一

32位CPU — 指的是该CPU在单位时间内能一次处理的二进制数的位数为32位,即一次处理4个字节。
64位CPU — 指的是该CPU在单位时间内能一次处理的二进制数的位数为64位,即一次处理8个字节。

区别二
目前32位和64位是指CPU的通用寄存器位宽(数据总线的位宽),所以64位的CPU数据处理位宽是32位CPU的2倍;

117.操作系统怎么维护页表的

多级页表映射全部的内存。

  1. 内核页表:主内核页表,在内存中实际上就是一段内存,存放在主内核页全局目录init_mm.pgd,内核并不直接使用。
  2. 每个进程自己的页表,放在进程自身的页目录task_struct.pgd中。
  3. 实际上操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。
  4. 分级的本质其实也是将页表项进行分组管理,考虑到进程内存大部分空闲以及局部性,因此实际记录的时候大部分的记录是没有的,所以相比上面提到的直接使用一个数组(也就是一级页表)大大地节省了存储需要的空间。
  5. 所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有1M个页表项来映射,而二级页表则最少只需要1K个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。

在保护模式下,从硬件角度看,其运行的基本对象为“进程”(或线程),而寻址则依赖于“进程页表”,在进程调度而进行上下文切换时,会进行页表的切换:即将新进程的pgd(页目录)加载到CR3寄存器中。

117.1 通过虚拟地址访问内存有什么好处?
  1. 程序可以通过一系列连续的虚拟地址来访问一系列不连续的物理内存。

  2. 程序可以通过虚拟内存访问大于可用物理内存的内存缓冲区。

  3. 不同进程的虚拟地址彼此隔离。 一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。

118.数据结构中数组和链表的区别

数组基于索引,链表基于引用

操作的优势不同

分配内存的时间点不同

数组 和 链表 之间的主要区别在于它们的结构。数组是基于索引的数据结构,其中每个元素与索引相关联。另一方面,链表 依赖于引用,其中每个节点由数据和对前一个和下一个元素的引用组成。

数组是数据结构,包含类似类型数据元素的集合,而链表被视为非基元数据结构,包含称为节点的无序链接元素的集合。
在数组中元素属于索引,即,如果要进入第四个元素,则必须在方括号内写入变量名称及其索引或位置。但是,在链接列表中,您必须从头开始并一直工作,直到达到第四个元素。
虽然访问元素数组很快,而链接列表需要线性时间,但速度要慢得多。
数组中插入和删除等操作会占用大量时间。另一方面,链接列表中这些操作的性能很快。
数组具有固定大小。相比之下,链接列表是动态和灵活的,可以扩展和缩小其大小。
在数组中,在编译期间分配内存,而在链接列表中,在执行或运行时分配内存。
元素连续存储在数组中,而它随机存储在链接列表中。
由于实际数据存储在数组中的索引中,因此对内存的要求较少。相反,由于存储了额外的下一个和前一个引用元素,因此链接列表中需要更多内存。
此外,阵列中的内存利用效率低下。相反,内存利用率在阵列中是有效的。

119.linux如何杀掉一个进程,具体的系统调用是怎样的?

调用kill这个系统调用。
在这里插入图片描述

120 .操作系统是如何进行调度的?

分为抢占式调度和协同式调度。所谓抢占式调度就是立刻停止执行当前的进程,将cpu分配给另一个更为重要的进程。而协同式调度,则会等待当前的进程执行完毕,或其进入阻塞状态,才会将cpu分配给其他进程。

在这里插入图片描述

121. 创建子进程的方式

在这里插入图片描述

122.自定义的信号都会进入内核态吗?

会的,要想将信号发送给进程,就要进入内核态,修改内核对每个进程安排的表,在那个表中写下信号。
在这里插入图片描述

123. 异常和中断的差别

中断:是指由于外部设备所引起的中断,如磁盘中断,打印机中断。
异常:是由CPU内部事件引起的中断,如程序出错。

124. 进程锁和线程锁的区别

主要看锁放在哪里,是放在多进程的共享区域还是私有的进程空间,若是私有的进程空间则是线程锁,若是多进程的共享区域,则是进程锁。
在这里插入图片描述

125.CPU指令执行的过程

在这里插入图片描述

126. 多核多线程如何保证Cache的一致性

每个缓存行都有特定的标识状态。若该行是共享状态,且被其中一个CPU给修改了,则其他CPU关于这行的状态都会变成无效状态。用这种方式,来保证每个Cache中的都是一致性。
在这里插入图片描述

参考1

127. 协程的底层原理是什么

类似于函数体一样的东西。
在这里插入图片描述

128. 浅析CPU高速缓存

在这里插入图片描述

129. C++ move用法

唯一的功能:将左值强制转化为右值引用。继而可以通过右值引用使用该值,以用于移动语义。使用std::move几乎没有任何代价,只是转换了资源的所有权。它实际上将左值变成右值引用,然后应用移动语义,调用移动构造函数,就避免了拷贝,提高了程序性能。如果一个对象内部有较大的对内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。

在这里插入图片描述
左值右值的概念
在这里插入图片描述
经典的用法
code

//摘自https://zh.cppreference.com/w/cpp/utility/move
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
    std::string str = "Hello";
    std::vector<std::string> v;
    //调用常规的拷贝构造函数,新建字符数组,拷贝数据
    v.push_back(str);
    std::cout << "After copy, str is \"" << str << "\"\n";
    //调用移动构造函数,掏空str,掏空后,最好不要使用str
    v.push_back(std::move(str));
    std::cout << "After move, str is \"" << str << "\"\n";
    std::cout << "The contents of the vector are \"" << v[0]
                                         << "\", \"" << v[1] << "\"\n";
}
129.1 std::move的底层原理

函数原型
code

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type&&>(t);

首先,函数参数T&&是一个指向模板类型参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配(可以传递左值或右值,这是std::move主要使用的两种场景.
std::move实现,首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后我们通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast之所以能使用类型转换,是通过remove_refrence::type模板移除T&&,T&的引用,获取具体类型T。

130. 移动语义和完美转发

主要就是避免有些地方其实就是使用简单的一个临时内存,却劳心费力的又复制了一大堆内存。使用移动语义,也就是移动构造函数,也就是将内存的所有权从一个对象转移到另一个对象。注意,原来的对象的值应该进行置空。不然,容易出现悬挂指针的问题。

移动构造函数的写法

MyString(MyString&& str) noexcept
  : _data(str._data), _len(str._len) {//类构造函数初始化列表
  std::cout << "MyString(&&)" << std::endl;
  str._len = 0;
  str._data = NULL;
}

参考1-值得再看一看,难理解

131.结构体对齐方式、意义

  1. 因为硬件对存储空间的处理,也就是取地址的方式有可能不同。2. 会对CPU的存储效率产生影响。
  1. 各个硬件平台对存储空间的处理不尽相同,比如一些CPU访问特定的变量必须从特定的地址进行读取,所以在这种架构下就必须进行字节对齐了,要不然读取不到数据或者读取到的数据是错误的。
  2. 会对CPU的存取效率产生影响:比如有些平台CPU从内存中偶数地址开始读取数据,如果数据起始地址正好为偶数,则1个读取周期就可以读出一个int类型的值,而如果数据其实地址为奇数,那我们就需要2个读取周期读出数据,并对高地址和低地址进行拼凑,这在读取效率上显然已经落后了很多了。

在这里插入图片描述

132. ARQ协议的解析

在这里插入图片描述

  1. 参考1:ARQ

133.c++与c的区别

C++ = C+面向对象+ STL+泛型编程
c与c++最主要的区别其实在于他们解决问题的思想方法不一样。C语言主要用于嵌入式领域,驱动开发等于硬件直接打交道的领域。C++可用于应用层开发,用户界面开发等与操作系统打交道的领域。

134.IPV4和IPV6的区别

寻址扩大、报头格式简化、身份验证加密、加强对移动设备的支持。
在这里插入图片描述

135.常量指针和指针常量区别

常量指针:是指指针指向的是常量。即,它指向的内容不能改变,不能通过指针来修改它指向的内容。但是指针自身不是常量,它自身的值是可以改变的,可以指向其他常量。const int * p
指针常量:指针本身是常量,它指向的地址是不可改变的,但地址里的内容可以通过指针改变。int * const p=&a;

const后面是*,则修饰的值是常量。若*在const的前面,则指针是常量。

参考

136.typedef和define区别

  1. 执行时间不同:typedef在编译阶段有效,由于是在编译阶段,因此typedef有类型检测的功能。#define则是宏定义,发生在预处理阶段,也就是编译之前,只是进行简单而机械的字符串替换,而不进行类型检查。
  2. 功能差异:typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用。#define不只是可以为类型取别名,还可以定义常量,变量,编译开关等。
  3. 作用域不同:#define没有作用域的限制,只要是之前定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。

137.VS 如何debug多线程

多线程调试

138.C++萃取器的理解

感觉还是挺难的,主要涉及到STL的泛型编程那一块。但那一块,自己比较薄弱。等以后加强了那部分的知识后,再补这个知识。

139.Tcp服务端一直sleep,客户端不断发送数据产生的问题

在这里插入图片描述
参考1

140. string的底层原理

code

   *  A string looks like this:
   *
   *  @code
   *                                     [_Rep]
   *                                     _M_length
   *  [basic_string<char_type>]          _M_capacity
   *  _M_dataplus                        _M_refcount
   *  _M_p ---------------->             unnamed array of char_type
   *  @endcode

从起始地址出开始,_M_length表示字符串的长度、_M_capacity是最大容量、_M_refcount是引用计数,_M_p指向实际的数据。
在这里插入图片描述
根据上图推测,一个空string,没有数据,内部开辟的内存应该是83=24字节,而sizeof(string)的值似乎为84=32字节,因为需要存储四个变量的值。而实际上并不是这样。_M_p是指向实际数据的指针,当调用string::data()或者string::c_str()时返回的也是该值。因此sizeof(string)的大小为8,等于该指针的大小,而不是之前猜测的32字节。

copy-on-write机制
copy-on-write顾名思义,就是写时复制。大多数的string对象拷贝都是用于只读,每次都拷贝内存是没有必要的,而且也很消耗性能,这就有了写时复制机制,也就是把内存复制延迟到写操作时,请看如下代码:
code

string s = "Fuck the code.";
string s1 = s; // 读操作,不实际拷贝内存 
cout << s1 << endl; // 读操作,不实际拷贝内存 
s1 += "I want it."; // 写操作,拷贝内存 

  1. 首先,要实现写时复制。对象拷贝的时候浅拷贝,即只复制地址指针,在所有的写操作里面重新开辟空间并拷贝内存,在新的内存空间做修改;
  2. 其次,多对象共享一段内存,必然涉及到内存的释放时机,就需要一个引用计数,当引用计数减为0时释放内存;
  3. 最后,要满足多线程安全性。c++要求所有内建类型具有相同级别的线程安全性,即多线程读同一对象时是安全的,多线程写同一类型的不同对象时时安全的。第一个条件很容易理解。第二个条件似乎有点多此一举,既然是不同对象,多线程读写都应该是安全的吧?对于int、float等类型确实如此,但是对于带引用计数的string则不然,因为不同的string对象可能共享同一个引用计数,而write操作会修改该引用计数,如果不加任何保护,必然会造成多线程不一致性。要解决这个问题很简单,引用计数用原子操作即可,加互斥锁当然也可以,但效率会低很多。

参考

141. 无锁编程

原子性访问共享内存
在这里插入图片描述

142. Linux的内存管理

Windows内存管理和linux内存管理
Linux和Windows内存管理的区别

142.1 C++的虚拟内存及其作用

简介
C++的虚拟内存相当于是给每个进程都绘制了一个大饼。告诉其虚拟内存所能访问到的空间都是他可以使用的。可其实只有等待进程访问虚拟内存时,系统才会根据页表将虚拟内存的地址映射到物理地址。这个翻译过程使用的是内存管理单元MMU(Memory Management Unit)
作用

  1. 核心作用:通过内存地址转换解决了多进程访问内存冲突的问题。
  2. 进程内存管理:

a. 内存完整性。由于虚拟内存对进程的欺骗,每个进程都认为自己获取到的内存时一块连续的内存。我们在编写应用程序时,就不用考虑大块内存地址的分配,总是认为有足够的地址空间就可以了。
b. 安全。由于进程访问内存时,都要通过页表来进行访问,我们如果 想要都某些内存地址增加访问控制,则可以直接在各个页表项上添加各种访问控制权限,就可以实现内存的权限控制。

  1. 数据共享
    通过虚拟内存更容易实现内存和数据的共享。

  2. 使用SWAP为进程扩充“内存”
    Linux 提出 SWAP 的概念,Linux 中可以使用 SWAP 分区,在分配物理内存,但可用内存不足时,将暂时不用的内存数据先放到磁盘上,让有需要的进程先使用,等进程再需要使用这些数据时,再将这些数据加载到内存中,通过这种”交换”技术,Linux 可以让进程使用更多的内存。

142.2 内存越界的原因

Segment fault,指进程需要访问的内存地址不在它的虚拟地址空间范围内,属于越界访问。内核就会报Segment fault.
原因

  1. 堆栈溢出。
  2. 内存访问越界,如数组下标访问越界。
  3. 非法指针:野指针,错误的指针转换。
  4. 多线程读写的数据未加锁保护,或使用了线程不安全的函数。

143. Final关键字的作用

  1. 禁用继承。
  2. 禁用重写。

144. 了解CPU缓存吗,缓存的目的是什么 ?

在这里插入图片描述

145. 在报文发送的过程中,报文中的源ip,目的ip会变吗?

答案:不会改变。除非做了Nat转换才能改变。不过mac地址是变化的,因为发送端开始不知道目的主机的mac地址,所以每经过一个路由器mac地址是变化的。
目的Mac地址如何获取
通过ARP。源IP+源Mac地址+目的IP地址 —>广播 如果有目的IP接受到该报文,就将自己的MAC给A。如果是子网的地址的话,那么路由器就会判断说是子网的IP地址,然后将路由器的MAC地址给ARP.

146. 虚继承的作用

注意点

  1. 虚继承是多重继承特有的概念,这里需要明确的是,虚继承与虚函数继承是完全不同的概念。
  2. 虚继承是为解决多重继承(菱形继承)而出现的,可以节省内存空间。

在这里插入图片描述

147. 用户态和内核态的差别

用户态与内核态最大的差别是权限的不同。限制不同程序的访问能力。
在这里插入图片描述

148.找出数组中只出现一次的2个数(异或的巧妙应用)(出现3次)

**题目:**一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

分析:这是一道很新颖的关于位运算的面试题。

首先我们考虑这个问题的一个简单版本:一个数组里除了一个数字之外,其他的数字都出现了两次。请写程序找出这个只出现一次的数字。

这个题目的突破口在哪里?题目为什么要强调有一个数字出现一次,其他的出现两次?我们想到了异或运算的性质:任何一个数字异或它自己都等于0。也就是说,如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现依次的数字,因为那些出现两次的数字全部在异或中抵消掉了。

有了上面简单问题的解决方案之后,我们回到原始的问题。如果能够把原数组分为两个子数组。在每个子数组中,包含一个只出现一次的数字,而其他数字都出现两次。如果能够这样拆分原数组,按照前面的办法就是分别求出这两个只出现一次的数字了。

我们还是从头到尾依次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。因为其他数字都出现了两次,在异或中全部抵消掉了。由于这两个数字肯定不一样,那么这个异或结果肯定不为0,也就是说在这个结果数字的二进制表示中至少就有一位为1。我们在结果数字中找到第一个为1的位的位置,记为第N位。现在我们以第N位是不是1为标准把原数组中的数字分成两个子数组,第一个子数组中每个数字的第N位都为1,而第二个子数组的每个数字的第N位都为0。

现在我们已经把原数组分成了两个子数组,每个子数组都包含一个只出现一次的数字,而其他数字都出现了两次。因此到此为止,所有的问题我们都已经解决。

参考

149.代码层面关闭编译器优化

方法一
#pragma optimize( "", off)

150. 服务器如何提高并发量

在这里插入图片描述

151. 20211028基础知识

  1. union:在不同的时间保存不同的数据类型和长度的变量,以达到节省空间的目的。但同一时间只能存储同一个成员变量的值。
  1. 大端:低地址存放高字节。高地址存放低字节。
  2. 几乎所有网络协议都是采用大端的方式来传输数据的,当两台采用不同字节序的主机通讯时,在发送数据之前都必须经过字节序的转换成为网络字节序。
  3. union中的对齐方式应以最长的基本数据类型的整数倍作为对齐。
  4. struct中的对齐方式也要以最长的基本数据类型的整数倍作为对齐。
  5. #define DEBUG
    #ifdef _DEBUG
    #else
    #endif
  6. #ifdef _cplusplus
    #endif

    #ifdef _cplusplus
    }
    #endif
  7. C++为了支持重载机制,在编译生成的汇编代码中,会对一些函数名字进行一些处理,比如加上函数的参数类型或是返回类型。而在C语言中,只是简单的函数名字。
  8. 面向对象编程的主要思想是把构成问题的各个事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述一个事务在解决问题中经过的步骤和行为。
  9. 结构体和类的区别:
    a. 结构体的成员访问限制符为public.而类的默认成员访问限制符为private.

152.20211029基础知识

  1. static局部对象在函数调用结束时对象不释放,所以也不执行析构函数,只有在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。
  2. 类的静态数据成员拥有一块单独的存储区,而不管创建了多少个该类的对象,所有对象的静态数据成员都共享这一块静态存储空间。包括该类的派生类的对象。
  3. 静态数据成员是程序在编译时被分配空间,到程序结束时释放空间。
  4. 静态成员函数也是类的一部分,而不是对象的一部分。
  5. 静态成员函数与非静态成员函数的根本区别是:非静态成员函数有this指针,而静态成员函数没有this指针。由此决定了静态成员函数不能访问本类中的非静态成员。若一定要再静态成员函数中使用非静态成员,则应加对象名和成员运算符".".
  6. 一个对象需要占用多大的内存空间:非静态成员变量总和加上编译器为了CPU计算做出的数据对齐处理和支持虚函数所产生的负担的总和。
  7. 静态数据成员是不占对象的内存空间的。
  8. 成员函数是不占内存空间的。
  9. 构造函数和析构函数也是不占空间的。
  10. 指针变量在64位的机器上占8byte。
  11. 单一继承的空类空间也是1,多重继承的空间空间还是1.但是虚继承涉及虚表(虚指针),所以sizeof(d) = 8;
  12. this指针是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。例如,当调用成员函数a.volume时,编译系统就把对象a的起始地址赋给this指针,在成员函数引用数据成员时,就按照this的指向找到对象a的数据成员。
  13. 函数模板与类模板的区别:

a. 第一行是template开头,后面跟着函数,则为函数模板,若为类,则为类模板。
b. 类模板不支持参数自动推导。
c. 类模板在模板参数列表中可以有默认参数。
template <class TypeName, class TypeAge = int>

153.有36辆自动赛车和6条跑道,没有计时器的前提下,最少用几次比赛可以筛选出最快的三辆赛车?

方法:

  1. 将36辆分成6组。每组取前3名。 6次
  2. 将6组的第一名进行比较。可以排除掉3组。比如排除掉4,5,6组。 1次
  3. 由于要求前3辆。所以A1>B1>C1>C2>C3。那么C2,C3可以排除掉。 并且,A1>B1>B2>B3.所以B3可以排除掉。
  4. 总决赛:A1,A2,A3,B1,B2,C1.再来一次比赛就可以了。 1次

总共需要6+1+1 = 8.

154. 如果用户反馈界面很卡,该怎么处理

一般来说,卡顿对于嵌入式来说,有可能是硬件也有可能是软件的问题。硬件方面有可能是核心板的电压与某些配件不匹配,如果是存在远程卡顿的话,那么也有可能是网线的问题。 而如果是软件方面,则要查看一下,是否整个系统的内存是否在逐步上升,若出现上升,则有可能是出现内存泄漏了,这将逐步导致程序卡顿乃至崩溃。

155. STL的相关内容笔记

  1. STL的六大部件
    a. 容器:存放数据
    b. 算法: 操作数据
    c. 迭代器: 扮演容器与算法的胶合剂。
    d. 仿函数: 仿函数是一种重载了operator()的class
    e. 适配器:用来修饰容器、仿函数、或迭代器接口的东西
    f. 空间配置器: 负责空间的配置和管理。

  2. STL 具有 高可用性,高性能,高移植性,跨平台的优点。

  3. 数据分为序列式容器和关联式容器。
    序列式容器: vector,deque,list

关联式容器: map,set,multiset,multimap

  1. Deque:最大的任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。

  2. Deque的中控器采用所谓的map作为主控,map是一小块连续空间,其中每个元素都是制作,指向另一端较大的连续线性空间,成为缓冲区。

  3. forward_list是单链表,是序列容器,允许在序列中的任何地方进行恒定的时间插入和擦除操作。

  4. list双向链表,是序列容器。

  5. queue 是一种容器适配器,用于在FIFO(先入先出)的操作,其中元素插入到容器的一端并从另一端提取。

  6. priority_queue:优先队列,其底层是用堆来实现的。

  7. unordered_set:unordered_set是基于哈希表。

  8. reserve:是直接扩充已经确定的大小,可以减少多次开辟,释放空间的问题,就可以提高效率。

  9. vector中的reserve与resize的区别?
    a. reserve是容器预留空间,但并不真正创建元素对象,在创建元素对象之前,不能引用容器内的元素。reserve函数之后一个参数,即需要预留的容器的空间。
    b. resize是改变容器的大小,并且创建对象,因为调用这个函数之后,就可以引用容器内的对象了。resize函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。

  10. vector中的size和capacity的区别?
    size表示当前vector中有多少个元素.(finish-start)
    capacity:表示它已经分配的内存中可以容纳多少个元素。(end_of_storage-start)

  11. vector中的erase和remove方法的区别?
    a. vector中的erase方法真正删除了元素,迭代器不能方法了。
    b. remove只是简单地将元素移到容器的最后面,迭代器还是可以访问到的。

  12. vector中的clear(),swap(),shrink_to_fit()的区别?
    vec.clear():清空内容,但是不释放内存。
    vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
    vec.shrink_to_fit():请求容器降低其capacity和size匹配。
    vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。

16.为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效?
a. 存储的是节点,不需要内存拷贝和内存移动。
b. 插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

参考

参考

  1. 牛客网面经
  2. 【C++基础之二十一】菱形继承和虚继承
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值