├── C++面试常见问题.md ├── README.md ├── 操作系统.md ├── 数据库面试问题.md ├── 笔试常见题.md ├── 计算机网路.md └── 项目.md /C++面试常见问题.md: -------------------------------------------------------------------------------- 1 | ###C++面试常见问题 2 | 3 | ####一、C/C++中static关键字的作用 4 | 5 | - **C面向过程的static关键字:** 6 | 7 | **1、静态全局变量:**在全局变量前加上关键字static,该变量就被定义成为一个静态全局变量。 8 | 9 | 静态全局变量有以下特点: 10 | • 该变量在全局数据区分配内存; 11 | • 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化); 12 | • 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;  13 | 14 | **代码区** 15 | **全局数据区** 16 | **堆区** 17 | **栈区** 18 | 19 | 一般程序的由new产生的动态数据存放在堆区,函数内部的自动变量存放在栈区。自动变量一般会随着函数的退出而释放空间,静态数据(即使是函数内部的静态局部变量)也存放在全局数据区。全局数据区的数据并不会因为函数的退出而释放空间。 20 | 21 | **2、静态局部变量:**在局部变量前加上关键字static,该变量就被定义成为一个静态局部变量。 22 | 23 | 静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。 24 | 25 | 静态局部变量有以下特点: 26 | • 该变量在全局数据区分配内存; 27 | • 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化; 28 | • 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0; 29 | • 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束; 30 | 31 | **3、静态函数:**在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。 32 | 33 | 定义静态函数的好处: 34 | • 静态函数不能被其它文件所用; 35 | • 其它文件中可以定义相同名字的函数,不会发生冲突; 36 | 37 | - **C++面向对象的static关键字:** 38 | 39 | **1、类静态数据成员:**在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。 40 | 41 | • 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新; 42 | • 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。 43 | • 静态数据成员和普通数据成员一样遵从public,protected,private访问规则; 44 | • 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它; 45 | • 静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为: 46 | <数据类型><类名>::<静态数据成员名>=<值> 47 | • 类的静态数据成员有两种访问形式: 48 | <类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名> 49 | 如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员 ; 50 | • 静态数据成员主要用在各个对象都有相同的某项属性的时候。 51 | • 同全局变量相比,使用静态数据成员有两个优势: 52 | 53 | 1. 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性; 54 | 2. 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能; 55 | 56 | **2、类静态成员函数:**与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。 57 | 58 | 关于静态成员函数,可以总结为以下几点: 59 | • 出现在类体外的函数定义不能指定关键字static; 60 | • 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数; 61 | • 非静态成员函数可以任意地访问静态成员函数和静态数据成员; 62 | • 静态成员函数不能访问非静态成员函数和非静态数据成员; 63 | • 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长; 64 | • 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:<类名>::<静态成员函数名>(<参数表>)调用类的静态成员函数。 65 | 66 | ------ 67 | 68 | ####二、C和C++的区别: 69 | 70 | **1、语言** 71 | 72 | - C语言是面向过程的语言,是具体化,流程化的,解决一个问题,需要一步一步的分析,一步一步的实现; 73 | 74 | - C++是模型化的,你只要抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以,不必一步一步去实现; 75 | 76 | **2、宏与模板** 77 | 78 | C++ 的模板在设计之初的一个用途就是用来替换宏定义。模板不同于宏的文字替换,在编译时会得到更全面的编译器检查,便于编写更健全的代码,利用 inline 关键字还能获得编译器充分的优化。 79 | 80 | **3、struct和class** 81 | 82 | C 中的 struct 用来描述一种固定的内存组织结构,而 C++ 中的 struct 就是一种类, **它与类唯一的区别就是它的成员和继承行为默认是 public 的** ,而一般类的默认成员是 private 的。 83 | 84 | 4、C++ 对 C 的增强,表现在六个方面: 85 | 86 | - 增强了类型检查机制 87 | - 增加了面向对象的机制 88 | - 增加了泛型编程的机制(template) 89 | - 增加了异常处理 90 | - 增加了重载的机制 91 | - 增加了标准模板库(STL) 92 | 93 | ------ 94 | 95 | ####三、C++中的类型转化: 96 | 97 | **C++类型转换大体上包括隐式类型转换和显式类型转换。** 98 | 99 | **1、隐式类型转化** 100 | 101 | 隐式类型转换是自动执行的,无需显式的操作符。 隐式类型转换发生在很多地方,比如函数实参到形参的类型转换、函数返回值类型的自动转换等等。 102 | 103 | **1.1、数值类型转化:**从小整数类型(char、short)转换到int,或者从float转换到double,这种“提升型”的转换通常不会造成数值差异。 104 | 105 | **1.2、指针类型转化:**指针通常存在以下转换: 106 | 107 | - 空指针可以转换到任意指针类型; 108 | - 任意指针类型都可以转换到void* 指针; 109 | - 继承类指针可以转换到可访问的明确的基类指针, 同时不改变const或者volatile属性; 110 | - 一个C风格的数组隐式把数组的第一个元素转换为一个指针。 111 | 112 | **2、显示类型转化** 113 | 114 | **2.1、explicit关键字:**C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。即声明为explicit的构造函数不能在隐式转换中使用。 115 | 116 | **2.2、强制类型转化:** 117 | 118 | **reinterpret_cast:**可以用于任意类型的指针之间的转换,对转换的结果不做任何保证 119 | **dynamic_cast:**这种其实也是不被推荐使用的,更多使用static_cast,dynamic本身只能用于存在虚函数的父子关系的强制类型转换,对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常 120 | **const_cast:**对于未定义const版本的成员函数,我们通常需要使用const_cast来去除const引用对象的const,完成函数调用。另外一种使用方式,结合static_cast,可以在非const版本的成员函数内添加const,调用完const版本的成员函数后,再使用const_cast去除const限定。 121 | **static_cast:**完成基础数据类型转化;同一个继承体系中类型的转换;任意类型与空指针类型void* 之间的转换。 122 | 123 | **2.2.1、static_cast**: 124 | 125 | ``` 126 | static_cast (expression) 127 | ``` 128 | 129 | **static_cast强制转换只会在编译时检查,但没有运行时类型检查来保证转换的安全性。所以这类型的强制转换和C语言风格的强制转换都有安全隐患。**主要应用场景: 130 | 131 | - 用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的。 132 | - 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。 133 | - 把void指针转换成目标类型的指针(不安全!!) 134 | - 把任何类型的表达式转换成void类型 135 | - 将enum class值转化为整数或者浮点数 136 | - 转换为右值引用 137 | 138 | **2.2.2、const_cast:** 139 | 140 | ``` 141 | const_cast (expression) 142 | ``` 143 | 144 | new_type 必须是一个指针、引用或者指向对象类型成员的指针。**const_cast用于去除除对象的const或者volatile属性。** 145 | 146 | - 常量指针被转化成非常量的指针,并且仍然指向原来的对象; 147 | - 常量引用被转换成非常量的引用,并且仍然指向原来的对象; 148 | - const_cast一般用于修改指针。如const char *p形式。 149 | 150 | **2.2.3、dynamic_cast:** 151 | 152 | ``` 153 | dynamic_cast (expression) 154 | ``` 155 | 156 | **dynamic_cast 在运行时执行转换,验证转换的有效性。**new_type 必须是一个指针或引用或“指向 void 的指针”。 如果 new_type 是指针,则expression 的类型必须是指针,如果 type-id 是引用,则expression为左值。 如果转型失败会返回null(转型对象为指针时)或抛出异常(转型对象为引用时)。dynamic_cast 会动用运行时信息(RTTI)来进行类型安全检查,因此dynamic_cast 存在一定的效率损失。 157 | 158 | dynamic_cast 的一个重要作用就是要确保转换结果应该指向一个完整的目标类型。此外,dynamic_cast只有在基类存在虚函数(虚函数表)的情况下才有可能将基类指针转化为子类。 159 | 160 | **2.2.4、reinterpret_cast:** 161 | 162 | ``` 163 | reinterpret_cast (expression) 164 | ``` 165 | 166 | **reinterpret_cast 运算符把某种指针改为其他类型的指针。它可以把一个指针转换为一个整数,也可以把一个整数转换为一个指针。**new_type必须是一个指针、引用、算术类型、函数指针或者成员指针。其转换结果与编译平台息息相关,不具有可移植性,因此在一般的代码中不常见到它。**reinterpret_cast 常用的一个用途是转换函数指针类型,即可以将一种类型的函数指针转换为另一种类型的函数指针,但这种转换可能会导致不正确的结果。** 167 | 168 | ------ 169 | 170 | #### 四、C++中的四个智能指针 171 | 172 | **1、为什么要使用智能指针?** 173 | 174 | 如果一块内存被多个指针引用,但其中的一个指针释放且其余的指针并不知道,这样的情况下,就发生了挂起引用。而内存泄露,就如你知道的一样,当从堆中申请了内存后不释放回去,这时就会发生内存泄露。使用智能指针可以很大程度上避免这个问题,智能指针是一个`RAII`(`Resource Acquisition is initialization`)类模型,用来动态的分配内存。它提供所有普通指针提供的接口,却很少发生异常。在构造中,它分配内存,当离开作用域时,它会自动释放已分配的内存。 175 | 176 | **2、四个智能指针:** 177 | 178 | - **auto_ptr**:**auto_ptr这个智能指针在新的C++11标准中已经不太常用**,它有以下几个问题: 179 | 180 | 1、**当把一个auto_ptr赋给另外一个auto_ptr时,它的所有权(ownship)也转移了。**当我在函数间传递`auto_ptr`时,这就是一个问题。例如,我在`Foo()`中有一个`auto_ptr`,然后在`Foo()`中我把指针传递给了`Fun()`函数,当`Fun()`函数执行完毕时,指针的所有权不会再返还给`Foo`。 181 | 182 | 2、**auto_ptr不能指向一组对象,就是说他不能和操作符new[]一起使用。**因为当`auto_ptr`离开作用域时,`delete`被默认用来释放关联的内存空间。 183 | 184 | 3、**auto_ptr不能和标准容器(vector,list,map……)一起使用** 185 | 186 | - **shared_ptr:shared_ptr共享所有权。可以被多个指针共享,多个指针可以同时指向一个对象,当最后一个shared_ptr离开作用域时,内存才会自动释放。** 187 | 188 | **创建:**尽量使用make_shared宏来加速创建的过程。make_shared以一种更有效率的方法来实现创建工作。可以通过调用use_count()得到资源的引用计数,找到shared_ptr的数量。 189 | 190 | **析构**:shared_ptr默认调用delete释放关联的资源。如果用户采用一个不一样的析构策略时,他可以自由指定构造这个shared_ptr的策略。比如:shared_ptr指向一组对象,但是当离开作用域时,默认的析构函数调用delete释放资源。实际上,我们应该调用delete[]来销毁这个数组。用户可以通过调用一个函数,例如一个lamda表达式,来指定一个通用的释放步骤。 191 | 192 | ``` 193 | shared_ptr sptr1( new Test[5], [ ](Test* p) { delete[ ] p; } ); 194 | ``` 195 | 196 | **接口:**就像一个普通指针一样,shared_ptr也提供解引用操作符*,->。除此之外,它还提供了一些更重要的接口: 197 | 198 | - `get()`: 获取`shared_ptr`绑定的资源. 199 | - `reset()`: 释放关联内存块的所有权,如果是最后一个指向该资源的`shared_ptr`,就释放这块内存。 200 | - `unique`: 判断是否是唯一指向当前内存的`shared_ptr`. 201 | - `operator bool` : 判断当前的`shared_ptr`是否指向一个内存块,可以用if 表达式判断。 202 | - release():当前指针会释放资源所有权,计数减一。 203 | 204 | **存在的问题:** 205 | 206 | - 如果几个`shared_ptrs`指向的内存块属于不同组,将产生错误。 207 | 208 | - 如果从一个普通指针创建一个`shared_ptr`还会引发另外一个问题。 209 | 210 | - 循环引用:如果共享智能指针卷入了循环引用,资源都不会正常释放 211 | 212 | - **weak_ptr:**weak_ptr用来解决shared_ptr循环引用的的问题。`weak_ptr` 拥有共享语义和不包含语义。这意味着,`weak_ptr`可以共享`shared_ptr`持有的资源。所以可以从一个包含资源的`shared_ptr`创建`weak_ptr`。 213 | 214 | **创建**:可以以`shared_ptr`作为参数构造`weak_ptr`.从`shared_ptr`创建一个`weak_ptr`增加了共享指针的弱引用计数,意味着`shared_ptr`与其它的指针共享着它所拥有的资源。但是当`shared_ptr`离开作用域时,这个计数不作为是否释放资源的依据。换句话说,就是除非强引用计数变为`0`,才会释放掉指针指向的资源, 215 | 216 | 如何判断`weak_ptr`是否指向有效资源,有两种方法: 217 | 218 | 1. 调用`use-count()`去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数。 219 | 2. 调用`expired()`方法。比调用`use_count()`方法速度更快。 220 | 221 | 从`weak_ptr`调用`lock()`可以得到`shared_ptr`或者直接将`weak_ptr`转型为`shared_ptr` 222 | 223 | - **unique_ptr:**`unique_ptr`也是对`auto_ptr`的替换。`unique_ptr`遵循着独占语义。在任何时间点,资源只能唯一地被一个`unique_ptr`占有。当`unique_ptr`离开作用域,所包含的资源被释放。如果资源被其它资源重写了,之前拥有的资源将被释放。所以它保证了他所关联的资源总是能被释放。 224 | 225 | **创建:**unique_ptr的创建方法和shared_ptr一样,但是unique_ptr提供了创建数组对象的特殊方法,当指针离开作用域时,调用delete[]代替delete。当创建unique_ptr时,这一组对象被视作模板参数的部分。 226 | 227 | 当把`unique_ptr`赋给另外一个对象时,资源的所有权就会被转移。 228 | 229 | `unique_ptr`不提供复制语义(拷贝赋值和拷贝构造都不可以),只支持移动语义(`move semantics`). 230 | 231 | ------ 232 | 233 | #### 四、野指针和悬空指针 234 | 235 | 野指针:野指针指,访问一个已删除或访问受限的内存区域的指针,野指针不能判断是否为NULL来避免。 236 | 产生的原因1、指针变量未初始化化。指针变量默认的值不是NULL,而是随机指。 237 | 悬空指针:一个指针的指向对象已被删除,那么就成了悬空指针。野指针是那些未初始化的指针。 238 | 239 | ####五、C++编译流程 240 | 241 | - 预处理:处理所有的条件编译指令,比如#if,#include,预编译指令、删除所有的注释、添加行号和文件名标识等。 242 | 243 | - 编译:编译会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。 244 | - 静态编译:编译器在编译可执行文件时,把需要用到的对应动态链接库(.so或.ilb)中的部分提取出来,链接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库. 245 | - 动态编译: 动态编译的可执行文件需要附带一个的动态链接库,在执行时,需要调用其对应动态链接库中的命令。 246 | 247 | - 汇编:汇编过程调用汇编器AS来完成,是用于将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。(非底层的程序员不需要考虑)汇编后的.o文件是纯二进制文件。 248 | 249 | - 链接:链接是将所有的.o文件和库(动态库、静态库)链接在一起,得到可以运行的可执行文件(Windows的.exe文件或Linux的.out文件)等。它的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配、符号决议和重定向。 250 | 251 | ------ 252 | 253 | ####五、指针和引用的区别 254 | 255 | **指针和引用主要有以下区别:** 256 | 257 | - 引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。 258 | 259 | - 引用初始化后不能被改变,指针可以改变所指的对象。 260 | 261 | - 不存在指向空值的引用,但是存在指向空值的指针。 262 | 263 | 从概念上讲。指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。 264 | 265 | 而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。 266 | 267 | - 指针是一个实体,而引用仅是个别名; 268 | - 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”; 269 | - **引用没有const,指针有const,const的指针不可变**; 270 | - **引用不能为空,指针可以为空**; 271 | - “**sizeof 引用**”得到的是所指向的变量(对象)的大小,而“**sizeof 指针**”得到的是指针本身的大小; 272 | - 指针和引用的自增(++)运算意义不一样; 273 | - 引用是类型安全的,而指针不是 (引用比指针多了类型检查) 274 | 275 | ------ 276 | 277 | #### 六、数组和链表的区别 278 | 279 | 数组是一种具有固定大小的数据结构,它将相同数据类型的元素在内存中连续存放,可通过下标快速访问数组中的任何元素。适合查询操作,插入和删除元素代价昂贵。 280 | 281 | 链表是一种采用链式结构的数据组织形式。它采用动态分配内存的形式实现,需要时可以用new分配内存空间,不需要时用delete释放已分配的空间,不会造成内存空间的浪费。适合插入和删除,查询操作开销较大。 282 | 283 | 数组元素存储在栈区,链表元素在堆区; 284 | 285 | 区别总结: 286 | 287 | 数组静态分配内存,链表动态分配内存; 288 | 289 | 数组元素在内存中连续存放,链表元素不连续; 290 | 291 | (在程序执行过程中,申请的内存空间属于堆区,而栈区用于存放程序函数中的局部变量) 292 | 293 | 数组利用下标定位,查询操作的时间复杂度为O(1),链表查询元素的时间复杂度为O(n); 294 | 295 | 数据插入和删除元素的时间复杂度为O(n),链表的时间复杂度为O(1)。 296 | 297 | 298 | 299 | #### 六、封装、继承和多态 300 | 301 | 面向对象的三个基本特征是:封装、继承、多态。其中,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用! 302 | 303 | ##### 1、封装 304 | 305 | 封装可以隐藏实现细节,使得代码模块化;封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。在面向对象编程上可理解为:**把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。** 306 | 307 | ##### 2、继承 308 | 309 | 继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。其继承的过程,就是从一般到特殊的过程。 310 | 311 | 通过继承创建的类称为子类或者派生类,被继承的类叫做父类或者基类。一个子类可以继承多个基类。但是一般情况下,一个子类只能有一个基类,要实现多重继承,可以通过多级继承来实现。 312 | 313 | 继承概念的实现方式有三类:实现继承、接口继承和可视继承。 314 | 315 | - 实现继承是指使用基类的属性和方法而无需额外编码的能力; 316 | - 接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力; 317 | - 可视继承是指子窗体(类)使用基窗体(类)的外观和实现代码的能力。 318 | 319 | ##### 3、多态 320 | 321 | 多态允许将子类类型的指针赋值给父类类型的指针,可以简单地概括为“一个接口,多种方法”,程序在运行时才决定要调用的函数。 322 | 323 | 多态有两种实现方式,覆盖和重载。 324 | 325 | C++的多态性是通过虚函数来实现的,虚函数允许派生类重新定义成员函数,而派生类重新定义基类的做法称为覆盖,或者称为重写。(重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性) 326 | 327 | 而重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。 328 | 329 | ------ 330 | 331 | #### 七、C++虚函数机制 332 | 333 | **C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区** 334 | 335 | C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数。虚函数的实现是通过虚函数表+虚表指针来实现的。编译器为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表。即,每个类使用一个虚函数表,每个类对象使用一个虚表指针。 336 | 337 | C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要再派生类中声明该方法为虚方法。 338 | 339 | - 虚表指针vptr(只要基类有虚函数),具体存在位置因编译器而定。 340 | 341 | - 虚表指针指向虚表。虚表中动态绑定虚函数 342 | - 类的非虚函数是静态的。地址不变,虚函数则是动态绑定。即如果子类重写了虚函数,就指向自己的虚函数地址。否则指向基类该函数的地址。 343 | 344 | 如果使用了**virtual**关键字,程序将根据**引用或指针**指向的 **对象类型** 来选择方法,否则使用**引用类型或指针类型**来选择方法。 345 | 346 | 虚函数的实现是通过虚函数表+虚表指针来实现的。编译器为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表。即,每个类使用一个虚函数表,每个类对象使用一个虚表指针。 347 | 348 | **基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表**。*看下面两种情况:* 349 | 350 | - 如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。 351 | - 如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。 352 | 353 | 354 | 355 | 虚函数实现 356 | 虚函数表和虚标指针 357 | 现代的C++编译器对于每一个多态类型,其所有的虚函数的地址都以一个表V-Table的方式存放在一起,虚函数表的首地址储存在每一个对象之中,称为虚表指针vptr,这个虚指针一般位于对象的起始地址。通过虚指针和偏移量计算出虚函数的真实地址实现调用。 358 | 单继承模式 359 | 单继承就是派生类只有1个基类,派生类的虚函数表中包含了基类和派生类的全部虚函数,如果发生覆盖则以派生类为准。 360 | 多继承模式 361 | 当派生类有多个基类,在派生类中将出现多个虚表指针,指向各个基类的虚函数表,在派生类中会出现非覆盖和覆盖的情况 362 | 虚继承 363 | 虚继承是面向对象编程中的一种技术,是指一个指定的基类在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类。 364 | 365 | 366 | 虚继承将共同基类设置为虚基类,从不同途径继承来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。从而解决了二义性问题、节省了内存,避免了数据不一致的问题。 367 | 368 | 369 | 为什么构造函数不能是虚函数 370 | 371 | 其他语言中可能会成立,但是在C++中存在问题,原因主要有: 372 | 373 | 构造对象时需要知道对象的实际类型,而虚函数行为是在运行期间才能确定实际类型的,由于对象还未构造成功,编译器无法知道对象的实际类型,俨然是个鸡和蛋的问题。 374 | 375 | 如果构造函数是虚函数,那么构造函数的执行将依赖虚函数表,而虚函数表又是在构造函数中初始化的,而在构造对象期间,虚函数表又还没有被初始化,又是个死循环问题。 376 | 377 | 总结:这块有点绕,从编译器的角度去看,构造函数就是为了在编译阶段确定对象类型、分配空间等工作,虚函数为了实现动态多态需要在运行期间才能确定具体的行为,显然构造函数不可能同时具备静态特性和动态特性。 378 | 379 | 虚函数的优缺点 380 | 381 | 虚函数的优点主要实现了C++的多态,提高代码的复用和接口的规范化,更加符合面向对象的设计理念,但是其缺点也比较明显,主要包括: 382 | 383 | 编译器借助于虚表指针和虚表实现时,导致类对象占用的内存空间更大,这种情况在子类无覆盖基类的多继承场景下更加明显。 384 | 385 | 虚函数表可能破坏类的安全性,可以根据地址偏移来访问Private成员 386 | 387 | 执行效率有损耗,因为涉及通过虚函数表寻址真正执行函数 388 | 389 | ------ 390 | 391 | #### 八、虚函数和纯虚函数的区别 392 | 393 | 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” virtual void funtion1()=0 394 | 395 | **引入原因:**1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 396 | 397 | 将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。 398 | 声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。 399 | **纯虚函数最显著的特征是:**它们必须在继承类中重新声明函数,而且它们在抽象类中往往没有定义。 400 | 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。 401 | 402 | ------ 403 | 404 | #### 九、C++中那些函数不能是虚函数 405 | 406 | 不能声明为虚函数的有:非成员函数、静态成员函数、内联成员函数、构造函数和友元函数 407 | 408 | - **非成员函数不为虚函数:**普通函数(非成员函数)只能被overload,不能被override,因为编译器会在编译时邦定函数。 409 | - **构造函数不为虚函数:**1、构造函数主要是为了明确初始化对象产生的,而虚函数主要是为了在信息不全的情况下,使得覆盖的函数得到对应的调用。如果构造函数是虚函数,找不到对应的调用。2、存储空间:虚函数对应一个指向虚函数表的指针,而指向虚函数表的指针存储在对象的内存空间。如果构造函数是虚的,那么就需要通过虚函数表来调用,而此时对象还没有初始化,内存空间还没有虚函数表。 410 | - **内联函数不为虚函数:**内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,没法统一。内联函数在编译时被展开,虚函数在运行时才能动态的邦定函数 411 | - **静态成员函数不为虚函数:**静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他不归某个具体对象所有。 412 | - **友元函数不为虚函数:**因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。 413 | 414 | #### 十、如果一个类A定义了虚函数,另一个类B定义了虚函数,一个派生类多重继承这两个类,问有派生类中多少个虚函数表? 415 | 416 | 如果不是虚继承的话即派生类没有重写虚函数,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针; 417 | 418 | 如果是虚继承的话,即派生类重写了虚函数,那么派生类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表。多重继承时,虚基表与虚基表指针有且只有一份。 419 | 420 | ------ 421 | 422 | #### 八、覆盖、重载和隐藏的区别 423 | 424 | **重载(overload)是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。** 425 | 426 | - 同一个类中的同名成员函数才存在重载关系 427 | - 这些函数的参数列表或返回类型不一样,从而实现了重载 428 | - 重载和成员函数是否为虚函数无关 429 | 430 | **覆盖(override)(也叫重写)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样。** 431 | 432 | - 在派生类中覆盖基类的同名函数 433 | - 基类函数为虚函数 434 | - 两个函数的参数个数,参数类型,返回类型都相同 435 | 436 | **隐藏(hide)是指派生类中的函数把基类中相同名字的函数屏蔽掉了。隐藏与另外两个概念表面上看来很像,很难区分,其实他们的关键区别就是在多态的实现上。** 437 | 438 | - 派生类中的函数或成员屏蔽了基类中的同名函数或同名成员 439 | - 隐藏和覆盖的区别:函数的参数相同,但是基类的函数不是虚函数(覆盖中基类的函数是虚函数) 440 | - 隐藏和重载的区别:隐藏的概念是存在与两个不同的类中,即使两个函数参数不同,无论基类函数是否是虚函数,基类函数都会被屏蔽。 441 | - 不仅能隐藏函数,也能隐藏数据成员。 442 | 443 | ------ 444 | 445 | #### 九、C++的设计模式 446 | 447 | 设计模式分为创建型设计模式、结构型设计模式和行为型设计模式。 448 | 449 | 单例模式是一种创建型的设计模式,保证一个类只有一个实例,并提供一个访问该实例的全局节点。通常运用在配置信息类、连接池类、ID生成器类等表示一些全局唯一类。单例模式的实现一般都是两个步骤,一个是将默认构造函数设置为私有,防止其他对象使用单例类的new运算符,第二个是创建一个静态构建方法作为构造函数,该函数调用构造函数来创建对象,并将其保存在一个静态成员变量中。之后所有对该函数的调用都将返回这一个缓存对象。(如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。日志系统需要服务器单例进行记录) 450 | 451 | 工厂模式也是一种创建型的设计模式,通过“对象创建”模式绕开new,使我们在创建对象的时候可以不依赖具体实例(所导致的紧耦合)。定义一个用于创建对象的接口,让子类决定实例化具体哪一个类,使得一个类的实例化进行延迟(解耦),将对象的创建和使用的过程分开。 452 | 453 | 原型模式也是一种创建型设计模式, 能够复制已有对象, 而又无需使代码依赖它们所属的类。 454 | 455 | 创建型的设计模式还有:生成器模式、抽象工厂模式 456 | 457 | 结构型的设计模式有:适配器模式、桥接模式、组合模式、代理模式、装饰模式、外观模式等 458 | 459 | 行为型设计模式:责任链模式、命令模式、迭代器模式、中介者模式、观察者模式、状态模式、策略模式等 460 | 461 | #####1、单例模式 462 | 463 | 单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。 464 | 465 | 单例模式解决了两个问题: 466 | 467 | - **保证一个类只有一个实例。**为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。 468 | 469 | - **为该实例提供一个全局访问节点**。 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。 470 | 471 | 和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。 472 | 473 | 还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。 474 | 475 | **所有单例的实现都包含以下两个相同的步骤:** 476 | 477 | - 将默认构造函数设为私有, 防止其他对象使用单例类的 `new`运算符。 478 | - 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。 479 | 480 | 如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。 481 | 482 | **单例模式适合应用的场景:** 483 | 484 | - **如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。** 485 | 486 | 单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。 487 | 488 | - **如果你需要更加严格地控制全局变量, 可以使用单例模式。** 489 | 490 | 单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。 491 | 492 | **实现方式:** 493 | 494 | - 在类中添加一个私有静态成员变量用于保存单例实例。 495 | 496 | - 声明一个公有静态构建方法用于获取单例实例。 497 | 498 | - 在静态方法中实现"延迟初始化"。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。 499 | 500 | - 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。 501 | 502 | - 检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。 503 | 504 | ```c 505 | class CSingleton 506 | { 507 | private: 508 | CSingleton(){} 509 | static CSingleton *m_pInstance; 510 | public: 511 | static CSingleton * GetInstance() 512 | { 513 | if(m_pInstance == NULL) 514 | m_pInstance = new CSingleton(); 515 | return m_pInstance; 516 | } 517 | } 518 | ``` 519 | 520 | **单例模式的优缺点:** 521 | 522 | 优: 523 | 524 | - 可以保证一个类只有一个实例 525 | - 获得了一个指向该实例的全局访问节点 526 | - 仅在首次请求单例对象时对其进行初始化 527 | 528 | 缺: 529 | 530 | - 违反了单一职责原则 531 | - 该模式在多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象。 532 | - 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多。 533 | 534 | #####2、工厂模式 535 | 536 | **抽象工厂模式**是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。给一个抽象类,然后暴露出一个接口,在接口中给出一个具体类。 537 | 538 | **目的:** 539 | 540 | - 工厂模式让我们在创建对象的时候可以不依赖具体实例。而依赖抽象。当我们想要修改的时候,只要进行扩展即可。不需要修改。对扩展开放,对修改关闭 541 | - 当创建对象是一个复杂的过程时,需要使用工厂方法。 542 | 543 | **工厂模式的适用场景:** 544 | 545 | - **如果代码需要与多个不同系列的相关产品交互, 但是由于无法提前获取相关信息, 或者出于对未来扩展性的考虑, 你不希望代码基于产品的具体类进行构建, 在这种情况下, 你可以使用抽象工厂。** 546 | 547 | 抽象工厂为你提供了一个接口, 可用于创建每个系列产品的对象。 只要代码通过该接口创建对象, 那么你就不会生成与应用程序已生成的产品类型不一致的产品。 548 | 549 | - **如果你有一个基于一组抽象方法的类, 且其主要功能因此变得不明确, 那么在这种情况下可以考虑使用抽象工厂模式。** 550 | 551 | 在设计良好的程序中, *每个类仅负责一件事*。 如果一个类与多种类型产品交互, 就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类中。 552 | 553 | **实现方式:** 554 | 555 | 1. 以不同的产品类型与产品变体为维度绘制矩阵。 556 | 2. 为所有产品声明抽象产品接口。 然后让所有具体产品类实现这些接口。 557 | 3. 声明抽象工厂接口, 并且在接口中为所有抽象产品提供一组构建方法。 558 | 4. 为每种产品变体实现一个具体工厂类。 559 | 5. 在应用程序中开发初始化代码。 该代码根据应用程序配置或当前环境, 对特定具体工厂类进行初始化。 然后将该工厂对象传递给所有需要创建产品的类。 560 | 6. 找出代码中所有对产品构造函数的直接调用, 将其替换为对工厂对象中相应构建方法的调用。 561 | 562 | **工厂模式的优缺点:** 563 | 564 | **优:** 565 | 566 | - 可以确保同一工厂生成的产品相互匹配。 567 | - 可以避免客户端和具体产品代码的耦合。 568 | - *单一职责原则*。 可以将产品生成代码抽取到同一位置, 使得代码易于维护。 569 | - *开闭原则*。 向应用程序中引入新产品变体时, 无需修改客户端代码。 570 | 571 | **缺:** 572 | 573 | - 由于采用该模式需要向应用中引入众多接口和类, 代码可能会比之前更加复杂。 574 | 575 | ------ 576 | 577 | #### 十、C++中的RTTI机制 578 | 579 | RTTI是Runtime Type Identification的缩写,意思是运行时类型识别。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型(int,指针等)的变量对应的类型。 580 | 581 | C++通过以下的两个操作提供RTTI: 582 | 583 | - typeid运算符,该运算符返回其表达式或类型名的实际类型。 584 | - dynamic_cast运算符,该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用。 585 | 586 | ------ 587 | 588 | #### 十一、STL顺序容器 589 | 590 | - vector:可变大小数组。支持快速随机访问,在尾部之外的位置插入或删除元素可能很慢。 591 | - deque:双端队列。支持快速随机访问。在头尾位置插入/删除速度很快。 592 | - list:双向链表。只支持双向顺序访问,在list中任何位置进行插入/删除操作速度都很快。 593 | - forward_list:单向链表。只支持单项顺序访问,在链表任何位置进行插入/删除造作速度都很快。 594 | - array:固定大小数组。支持快速随机访问,不能添加或删除元素。 595 | - string:与vector相似得容器。但专门用于保存字符,随机访问快,在尾部插入删除快。 596 | 597 | ------ 598 | 599 | #### 十二、关联容器 600 | 601 | 关联容器中得元素是按关键字来保存和访问的。关联容器支持高效的关键字查找和访问,两个主要的关联容器是map和set。map中的元素是一些关键字--值对:关键字起到索引的作用,值则表示与索引相关联的数据。set中每个元素只包含一个关键字;标准库提供8个关联容器: 602 | 603 | **按关键字有序保存元素:** 604 | 605 | - map:关联数组;保存关键字-值对 606 | 607 | - set:关键字即值,即保存关键字的容器 608 | 609 | - multimap:关键字可重复出现的map 610 | - multiset:关键字可重复出现的set 611 | 612 | **无序集合:** 613 | 614 | - unordered_map:用哈希函数组织的map 615 | - unordered_set:用哈希函数组织的set 616 | - unordered_multimap:哈希组织的map;关键字可以重复出现 617 | - unordered_multiset:哈希组织的set;关键字可以重复出现 618 | 619 | ------ 620 | 621 | ####十三、vector、list、deque、map和set的底层实现 622 | 623 | - **vector** 624 | vector就是动态数组.它也是在堆中分配内存,元素连续存放,有保留内存,**如果减少大小后,内存也不会释放.如果新值>当前大小时才会再分配内存.** 625 | 626 | 它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随即存取,即[]操作符,**但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝**,另外,**当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。对最后元素操作最快(在后面添加删除最快 ), 此时一般不需要移动内存,只有保留内存不够时才需要** 627 | 628 | - **list** 629 | list就是**双向链表**,**元素也是在堆中存放,**每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的**随机存取变的非常没有效率**,因此它没有提供[]操作符的重载。但由于链表的特点,它可以以很好的效率支持任意地方的删除和插入。 630 | 631 | list**没有空间预留习惯**,每次插入或者删除一个元素,就配置一个或释放一个元素空间。对于任何位置的元素插入或元素移除,list永远都是常熟时间。 632 | 633 | - **deque** 634 | 635 | deque是一种双向开口的连续线性空间。所谓双向开口,意思是可以在头尾两端分别做元素的插入和删除操作。vector当然也可以在头尾两端进行操作,但是头部操作效率奇差。 636 | 637 | deque允许于常数时间内对起头端进行元素的插入或移除操作,deque没有容量的概念,它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并连接起来。因此,deque没有必要提供所谓的空间保留(reserve)功能。 638 | 639 | - **map** 640 | 641 | Map是关联容器,以键值对的形式进行存储,方便进行查找。关键词起到索引的作用,值则表示与索引相关联的数据。以红黑树的结构实现,插入删除等操作都在O(logn)时间内完成 642 | 643 | - **set** 644 | 645 | Set是关联容器,set中每个元素只包含一个关键字。set支持高效的关键字查询操作——检查一个给定的关键字是否在set中。set也是以红黑树的结构实现,支持高效插入、删除等操作。 646 | 647 | #####请你讲讲STL有什么基本组成 648 | 649 | STL主要由:以下几部分组成:容器,迭代器,仿函数,算法分配器,配接器 650 | 他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数 651 | 652 | #####C++ STL 的实现: 653 | 654 | 1.vector 底层数据结构为数组 ,支持快速随机访问 655 | 656 | 2.list 底层数据结构为双向链表,支持快速增删 657 | 658 | 3.deque 底层数据结构为一个中央控制器和多个缓冲区,详细见STL源码剖析P146,支持首尾(中间不能)快速增删,也支持随机访问 659 | 660 | 4.stack 底层一般用list,deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时 661 | 662 | 5.queue 底层一般用list,deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时 663 | 664 | 6.stack,queue是适配器,而不叫容器,因为是对容器的再封装 665 | 666 | 7.priority_queue 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现 667 | 668 | 8.set 底层数据结构为红黑树,有序,不重复 669 | 670 | 9.multiset 底层数据结构为红黑树,有序,可重复 671 | 672 | 10.map 底层数据结构为红黑树,有序,不重复 673 | 674 | 11.multimap 底层数据结构为红黑树,有序,可重复 675 | 676 | 12.hash_set 底层数据结构为hash表,无序,不重复 677 | 678 | 13.hash_multiset 底层数据结构为hash表,无序,可重复 679 | 680 | 14.hash_map 底层数据结构为hash表,无序,不重复 681 | 682 | 15.hash_multimap 底层数据结构为hash表,无序,可重复 683 | 684 | #### 十三、map和set有什么区别? 685 | 686 | map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。 687 | 688 | map和set区别在于: 689 | 690 | (1)map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。 691 | 692 | (2)set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。 693 | 694 | (3)map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。 695 | 696 | ##### 红黑树 697 | 698 | 红黑树:是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。且符合以下性质: 699 | 700 | 1. 节点是红色或黑色。 701 | 702 | 2. 根节点是黑色。 703 | 704 | 3. 每个叶节点(NIL节点,空节点)是黑色的。 705 | 706 | 4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点) 707 | 708 | 5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 709 | 710 | 这些约束保持了树的相对平衡,同时又比AVL的插入删除操作的复杂性要低许多。 711 | 712 | ##### 为什么要使用红黑树实现 713 | 714 | 为什么要用红黑树实现 715 | 716 | map的场景本质上就是动态查找过程,所谓动态就是其中包含了插入和删除,并且数据量会比较大而且元素的结构也比较随意,并且都是基于内存来实现的,因此我们就需要考虑效率和成本,既要节约内存又要提高调整效率和查找速度。 717 | 718 | 红黑树优点: 719 | 720 | 1、首先红黑树是不符合AVL树的平衡条件的,即每个节点的左子树和右子树的高度最多差1的二叉查找树。但是提出了为节点增加颜色,红黑树是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高 721 | 722 | 2、红黑树能够以O(log2 (n)) 的时间复杂度进行搜索、插入、删除操作 723 | 724 | 3、简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 725 | 726 | 727 | 728 | BST和AVL是二叉搜索树和平衡二叉树,这两个比较容易排除,BST可能退化成为链表,那么树就相当于很高,时间无法保证,AVL作为严格平衡的二叉搜索树对平衡性要求很高,因此在插入和删除数据时会造成不断地重度调整,影响效率,有点学术派而非工程派,但是AVL是后面很多变种树的基础也很重要,但是确实不适合用在map中。 729 | 730 | Hash_Table其实目前已经有基于哈希表的map版本了,相比红黑树查找更快,然而时间的提升也是靠消耗空间完成的,哈希表需要考虑哈希冲突和装载因子的处理,在1994年左右内存很小并且很贵,因此哈希表在当时并没有被应用于实现map,现在内存相对来说已经很大并且不再昂贵,哈希表自然也有用武之地了。 731 | 732 | Splay-Tree伸展树也是一种变种,它是一种能够自我平衡的二叉查找树,它能在均摊O(log n)的时间内完成基于伸展(Splay)操作的插入、查找、修改和删除操作。它是由丹尼尔·斯立特(Daniel Sleator)和罗伯特·塔扬在1985年发明的。 733 | 734 | Treap就是Tree+heap,树堆也是一种二叉搜索树,是有一个随机附加域满足堆的性质的二叉搜索树,其结构相当于以随机数据插入的二叉搜索树。其基本操作的期望时间复杂度为O(log{n})。相对于其他的平衡二叉搜索树,Treap的特点是实现简单,且能基本实现随机平衡的结构。 735 | 736 | B-Tree这里可以认为是B树族包括B树、B+树、B*树,我们都知道B树在MySQL索引中应用广泛,构建了更矮更胖的N叉树,这种结构结点可以存储更多的值,有数据块的概念,因此应对磁盘存储很有利,事实上即使内存中使用B树也可以提高CacheHit的成功率,从而提高效率,网上有的文章提到STL之父说如果再有机会他可能会使用B树来实现一种map,也就是借助于局部性原理来提高速度。 737 | 738 | 739 | 740 | ------ 741 | 742 | ####十四、C++中的内存管理 743 | 744 | 在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。 745 | 746 | 代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。 747 | 748 | 数据段:存储程序中已初始化的全局变量和静态变量 749 | 750 | bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。 751 | 752 | 堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。 753 | 754 | 映射区:存储动态链接库以及调用mmap函数进行的文件映射 755 | 756 | 栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值 757 | 758 | ------ 759 | 760 | ####十五、STL里resize和reserve的区别 761 | 762 | resize():改变当前容器内含有元素的数量(size()),eg: vectorv; 763 | 764 | v.resize(len);v的size变为len,如果原来v的size小于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1; 765 | reserve():改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象,如果reserve(len)的值大于当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前v.size()个对象通过copy construtor复制过来,销毁之前的内存; 766 | 767 | ------ 768 | 769 | #### 十六、C++11有哪些新特性 770 | 771 | - auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导 772 | - nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。 773 | - 智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。 774 | - 初始化列表:使用初始化列表来对类进行初始化 775 | - 右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率 776 | - atomic原子操作用于多线程资源互斥操 777 | 778 | ------ 779 | 780 | #### 十七、右值引用 781 | 782 | C++中,左值通常指可以取地址,有名字的值就是左值,而不能取地址,没有名字的就是右值。而在指C++11中,右值是由两个概念构成,将亡值和纯右值。纯右值是用于识别临时变量和一些不跟对象关联的值,比如1+3产生的临时变量值,2、true等,而将亡值通常是指具有转移语义的对象,比如返回右值引用T&&的函数返回值等。 783 | 784 | 基于右值引用可以实现转移语义和完美转发新特性。 785 | 786 | **移动语义:** 787 | 788 | 对于一个包含指针成员变量的类,由于编译器默认的拷贝构造函数都是浅拷贝,所有我们一般需要通过实现深拷贝的拷贝构造函数,为指针成员分配新的内存并进行内容拷贝,从而避免悬挂指针的问题。C++使用移动构造函数,从而保证使用临时对象构造时不分配内存,从而提高性能。 789 | 790 | 将内存的所有权从一个对象转移到另外一个对象,高效的移动用来替换效率低下的复制,对象的移动语义需要实现移动构造函数(move constructor)和移动赋值运算符(move asssignment operator)。 791 | 792 | **完美转发:** 793 | 794 | 完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另一个函数,即传入转发函数的是左值对象,目标函数就能获得左值对象,转发函数是右值对象,目标函数就能获得右值对象,而不产生额外的开销。 795 | 796 | 因此转发函数和目标函数参数一般采用引用类型,从而避免拷贝的开销。其次,由于目标函数可能需要能够既接受左值引用,又接受右值引用,所以考虑转发也需要兼容这两种类型。 797 | 798 | ------ 799 | 800 | ####十八、C++中的atomic原子操作 801 | 802 | 它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。 803 | 804 | 在以往的C++标准中并没有对原子操作进行规定,我们往往是使用汇编语言,或者是借助第三方的线程库,例如intel的pthread来实现。在新标准C++11,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如,atomic_bool,atomic_int等等,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。 805 | 806 | ------ 807 | 808 | #### 十九、深拷贝和浅拷贝 809 | 810 | 深拷贝,浅拷贝:当出现类的等号赋值时,即会调用拷贝函数 811 | 812 | - C++在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的;但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象,所以,此时,必须采用深拷贝。深拷贝指的就是当拷贝对象中有对其他资源(如堆、文件、系统等)的引用时(引用可以是指针或引用)时,对象的另开辟一块新的资源,而不再对拷贝对象中有对其他资源的引用的指针或引用进行单纯的赋值。 813 | - 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。 814 | 815 | ------ 816 | 817 | #### 二十、构造函数为什么不能是虚函数,析构函数为什么是虚函数 818 | 819 | ##### 1、构造函数为什么不能是虚函数 820 | 821 | 822 | 823 | 从存储空间角度来说:虚函数是通过对象的内存空间的一张虚函数表和虚函数指针来调用的,而构造函数本身就是初始化实例,给对象分配内存空间。如果构造函数时虚函数的话,那么也需要通过虚函数表来进行调用,但是这时候对象的内存空间还没有,无法找到虚函数表,无法完成调用。所以构造函数不能是虚函数。 824 | 825 | 从实现上来看,构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,而虚函数是动态绑定。所以构造函数也没必要成为虚函数。 826 | 827 | 828 | 829 | 830 | 831 | 构造对象时需要知道对象的实际类型,而虚函数行为是在运行期间才能确定实际类型的,由于对象还未构造成功,编译器无法知道对象的实际类型,俨然是个鸡和蛋的问题。 832 | 833 | 如果构造函数是虚函数,那么构造函数的执行将依赖虚函数表,而虚函数表又是在构造函数中初始化的,而在构造对象期间,虚函数表又还没有被初始化,又是个死循环问题。 834 | 835 | - 从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。 836 | 837 | - 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。 838 | 839 | - 从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有必要成为虚函数。 840 | 841 | ##### 2、虚析构 842 | 843 | 这是因为,通过基类指针来销毁派生类对象这个行为,当基类没有虚析构函数时会产生问题。我们知道删除指针对象是没有问题的,指针对象的析构函数会正确调用,但仅限于指针的类型所表示的对象大小。**如果以一个基类指针指向其派生类**,删除这个基类指针只能删除基类对象部分,而不能删除整个派生类对象,原因是通过基类指针无法访问派生类的析构函数。 844 | 但是,如果像其它虚函数一样,基类的析构函数也是虚的,那么派生类的析构函数也必然是虚的,删除基类指针时,它就会通过虚函数表找到正确的派生类析构函数并调用它,从而正确析构整个派生类对象。 845 | 846 | ------ 847 | 848 | #### 二十一、网络编程 849 | 850 | ##### socket阻塞和非阻塞、同步和异步 851 | 852 | 网络IO的操作主要分为两个阶段,准备阶段和操作阶段。准备阶段是等待数据是否可用,在内核进程完成;操作阶段执行实际的IO调用,将数据从内核缓冲区拷贝到用户缓冲区。 853 | 854 | **阻塞和非阻塞主要发生在第一阶段**,阻塞是指访问的数据如果没有准备就绪,当前线程会被挂起一直等待,等待数据准备就绪才继续下一个阶段,将数据拷贝到用户空间才返回;非阻塞是指访问的时候如果数据没有准备就绪,不会将线程挂起,会立即返回一个错误,然后当前线程会采用轮询的方式来检测数据是否准备好,如果准备好了,再进行下一个阶段,将数据拷贝到用户空间才返回。 855 | 856 | **举个例子(阻塞):**比如到你某个时候到A楼一层(假如是内核缓冲区)取快递,但是你不知道快递什么时候过来,你又不能干别的事,只能死等着。但你可以睡觉(进程处于休眠状态),因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。 857 | 858 | **举个例子(非阻塞):**还是如果用忙轮询的方法,每隔5分钟到A楼一层(内核缓冲区)去看快递来了没有。如果没来,立即返回。而快递来了,就放在A楼一层,等你去取。 859 | 860 | **同步和异步主要发生在第二个阶段**,同步和异步是指访问数据的机制。同步一般指主动请求并等待数据从内核到用户空间的拷贝的IO操作,在未完成前,会导致当前线程被挂起,直到完成拷贝为止;异步是指用户触发IO操作以后便开始做自己的事情,而当数据拷贝的IO操作已经完成的时候会得到IO完成的通知,这可以使在读写数据时也不阻塞。 861 | 862 | **同步IO和异步IO的区别就在于:数据访问的时候进程是否阻塞!** 863 | 864 | **阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回!** 865 | 866 | ##### Linux下的五种I/O模型 867 | 868 | - 阻塞I/O(blocking I/O) 869 | - 非阻塞I/O (nonblocking I/O) 870 | - I/O复用(select 和poll) (I/O multiplexing) 871 | - 信号驱动I/O (signal driven I/O (SIGIO)) 872 | - 异步I/O (asynchronous I/O (the POSIX aio_functions)) 873 | 874 | 前四种都是同步,只有最后一种才是异步IO。 875 | 876 | **阻塞I/O模型** 877 | 878 | 使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,使用阻塞模式来开发网络程序比较合适。 879 | 880 | 阻塞模式套接字的不足表现为,在大量建立好的套接字线程之间进行通信时比较困难。当使用“生产者-消费者”模型开发网络程序时,为每个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。**其最大的缺点是当希望同时处理大量套接字时,将无从下手,其扩展性很差.** 881 | 882 | 阻塞模式给网络编程带来了一个很大的问题,如在调用 send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这给多客户机、多业务逻辑的网络编程带来了挑战。 883 | 884 | 多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求,但其实并不尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。 885 | 886 | 由此可能会考虑使用“**线程池**”或“**连接池**”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如apache,mysql数据库等。 887 | 888 | **非阻塞I/O** 889 | 890 | 非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,**不要将进程睡眠**,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。**在这个不断测试的过程中,会大量的占用CPU的时间。** 891 | 892 | 当使用socket()函数和WSASocket()函数创建套接字时,默认都是阻塞的。在创建套接字之后,通过调用ioctlsocket()函数,将该套接字设置为非阻塞模式。Linux下的函数是:fcntl(). 893 | 894 | 非阻塞套接字在控制建立的多个连接,在数据的收发量不均,时间不定时,明显具有优势。这种套接字在使用上存在一定难度,但只要排除了这些困难,它在功能上还是非常强大的。通常情况下,可考虑使用套接字的“I/O模型”,它有助于应用程序通过异步方式,同时对一个或多个套接字的通信加以管理。 895 | 896 | **IO复用模型**(同步,阻塞) 897 | 898 | 主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听; 899 | 900 | I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。 901 | 902 | ![1.png](http://ww1.sinaimg.cn/large/0062FU9hly1gho52d4w6bj30st0j3jyi.jpg) 903 | 904 | **信号驱动IO** 905 | 906 | 两次调用,两次返回; 907 | 908 | 首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。 909 | 910 | ![2.png](http://ww1.sinaimg.cn/large/0062FU9hly1gho53bumsjj30ru0hygs6.jpg) 911 | 912 | **异步IO模型** 913 | 914 | 数据拷贝的时候进程无需阻塞。 915 | 916 | 当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作 917 | 918 | ![3.png](http://ww1.sinaimg.cn/large/0062FU9hly1gho54xew0pj30qh0fy0xv.jpg) 919 | 920 | **同步IO引起进程阻塞,直至IO操作完成。** 921 | **异步IO不会引起进程阻塞。** 922 | **IO复用是先通过select调用阻塞。** 923 | 924 | **5个IO模型的比较** 925 | 926 | ![4.png](http://ww1.sinaimg.cn/large/0062FU9hly1gho56r6o5dj30rb0gd7b7.jpg) 927 | 928 | #####IO复用 929 | 930 |  IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),blocking IO只调用了recvfrom;select/poll/epoll 核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞IO好,多路复用模型中,每一个socket,设置为non-blocking,阻塞是被select这个函数block,而不是被socket阻塞的。 931 | 932 | **select机制:** 933 | 基本原理: 934 |   客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操作。 935 | 优点: 936 |   几乎在所有的平台上支持,跨平台支持性好 937 | 缺点: 938 |   由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。 939 |   每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间) 940 |   默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢。 941 | 942 | **poll机制:** 943 |   基本原理与select一致,也是轮询+遍历;唯一的区别就是poll没有最大文件描述符限制(使用链表的方式存储fd)。 944 | 945 | **epoll机制:** 946 | 947 | 基本原理: 948 |   没有fd个数限制,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过epoll_ctl注册fd,一旦fd就绪就会通过callback回调机制来激活对应fd,进行相关的io操作。 949 | epoll之所以高性能是得益于它的三个函数 950 |   1)epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fd 951 |   2)epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数 952 |   3)epoll_wait() 轮训所有的callback集合,并完成对应的IO操作 953 | 优点: 954 |   没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄 955 |   效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降 956 |   内核和用户空间mmap同一块内存实现(mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间) 957 | 958 | - 首先来看看socket可读的条件. 959 | 960 | 一、下列四个条件中的任何一个满足时,socket准备好读: 961 | 1. socket的接收缓冲区中的【已用】数据字节大于等于该socket的接收缓冲区低水位标记的当前大小。对这样的socket的读操作将不阻塞并返回一个大于0的值(也就是返回准备好读入的数据)。我们可以用SO_RCVLOWAT socket选项来设置该socket的低水位标记。对于TCP和UDP .socket而言,其缺省值为1. 962 | 2. 该连接的读这一半关闭(也就是接收了FIN的TCP连接)。对这样的socket的读操作将不阻塞并返回0 963 | 3.socket是一个用于监听的socket,并且已经完成的连接数为非0.这样的soocket处于可读状态,是因为socket收到了对方的connect请求,执行了三次握手的第一步:对方发送SYN请求过来,使监听socket处于可读状态;正常情况下,这样的socket上的accept操作不会阻塞; 964 | 4.有一个socket有异常错误条件待处理.对于这样的socket的读操作将不会阻塞,并且返回一个错误(-1),errno则设置成明确的错误条件.这些待处理的错误也可通过指定socket选项SO_ERROR调用getsockopt来取得并清除; 965 | 966 | - 再来看看socket可写的条件. 967 | 968 | 二、下列三个条件中的任何一个满足时,socket准备好写: 969 | 1. socket的发送缓冲区中的【剩余】数据字节大于等于该socket的发送缓冲区低水位标记的当前大小。对这样的socket的写操作将不阻塞并返回一个大于0的值(也就是返回准备好写入的数据)。我们可以用SO_SNDLOWAT socket选项来设置该socket的低水位标记。对于TCP和UDP socket而言,其缺省值为2048 970 | 971 | 2. 该连接的写这一半关闭。对这样的socket的写操作将产生SIGPIPE信号,该信号的缺省行为是终止进程。 972 | 3.有一个socket异常错误条件待处理.对于这样的socket的写操作将不会阻塞并且返回一个错误(-1),errno则设置成明确的错误条件.这些待处理的错误也可以通过指定socket选项SO_ERROR调用getsockopt函数来取得并清除; 973 | 974 | ##### select、poll、epoll简介 975 | 976 | **select:** 977 | 978 | select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是: 979 | 980 | - 单个进程可监视的fd数量被限制,即能监听端口的大小有限。 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048. 981 | - 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。 982 | - 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大 983 | 984 | **poll:** 985 | 986 | poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。 987 | 988 | 它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点: 989 | 990 | - 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。 991 | - poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。 992 | 993 | **epoll:** 994 | 995 | epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知 996 | 997 | epoll的优点: 998 | 999 | - **没有最大并发连接的限制**,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口); 1000 | - **效率提升**,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数; 1001 | 即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。 1002 | - **内存拷贝**,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。 1003 | 1004 | **select、poll、epoll 区别总结:** 1005 | 1006 | 1、支持一个进程所能打开的最大连接数 1007 | 1008 | | select | 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。 | 1009 | | ------ | ------------------------------------------------------------ | 1010 | | poll | poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的 | 1011 | | epoll | 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接 | 1012 | 1013 | 2、FD剧增后带来的IO效率问题 1014 | 1015 | | select | 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 | 1016 | | ------ | ------------------------------------------------------------ | 1017 | | poll | 同上 | 1018 | | epoll | 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。 | 1019 | 1020 | 3、 消息传递方式 1021 | 1022 | | select | 内核需要将消息传递到用户空间,都需要内核拷贝动作 | 1023 | | ------ | ------------------------------------------------ | 1024 | | poll | 同上 | 1025 | | epoll | epoll通过内核和用户空间共享一块内存来实现的。 | 1026 | 1027 | **总结:** 1028 | 1029 | 综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。 1030 | 1031 | - 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。 1032 | - select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善 1033 | 1034 | ##### ET模式(边缘触发)和LT模式(水平触发) 1035 | 1036 | 水平触发:当采用水平触发通知时,我们可以在任意时刻检查文件描述符的就绪状态。这表示当我们确定了文件描述符处于就绪状态时(比如存在有输入数据),就可以对其执行一些I/O操作,然后重复检查文件描述符,看看是否仍然处于就绪态(比如还有更多的输入数据),此时我们就能执行更多的I/O。**比如说我们采用epoll水平触发模式监听一个文件描述符的可读,当这个文件可读就绪时,epoll会触发一个通知,然后我们执行一次读取操作,但这次操作我们并没有把该文件描述符的数据全部读取完。当下一次调用epoll监听该文件描述符时,epoll还会再次触发通知,直到该事件被处理完。这就意味着,当epoll触发通知后,我们可以不立即处理该事件,当下次调用epoll监听时,然后会再次向应用程序通告此事件,此时我们再处理也不晚。** 1037 | 1038 | 边沿触发:与之相反的是,当我们采用边沿触发时,只有当I/O事件发生时我们才会收到通知。**还是上个例子,如果这次我们采用epoll的边沿触发模式监听一个文件描述符的可读,当可读就绪时,epoll会触发一个通知,如果我们此时不立即处理该事件,当下次再调用epoll监听时,虽然该文件描述符的状态是可读的,但是此时epoll并不会再给应用程序发送通知。因为在边沿触发工作模式下,只有下一个新的I/O事件到来时,才会再次发送通知**。 1039 | 1040 | 下表是I/O多路复用select,poll和epoll所支持的通知模型: 1041 | 1042 | | I/O模式 | 水平触发 | 边沿触发 | 1043 | | -------- | -------- | -------- | 1044 | | select() | 支持 | 不支持 | 1045 | | poll() | 支持 | 不支持 | 1046 | | epoll() | 支持 | 支持 | 1047 | 1048 | select和poll只支持LT工作模式,epoll的默认的工作模式是LT模式。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。 1049 | 1050 | #### 二十二、排序算法 1051 | 1052 | | 算法种类 | | s时间复杂度 | | | | 1053 | | -------- | --------- | ----------- | --------- | ---------- | -------- | 1054 | | | 最好 | 平均 | 最坏 | 空间复杂度 | 是否稳定 | 1055 | | 插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 | 1056 | | 冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 | 1057 | | 选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 否 | 1058 | | 希尔排序 | | | | O(1) | 否 | 1059 | | 快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn) | 否 | 1060 | | 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 否 | 1061 | | 归并排序 | O(nlogn | O(nlogn) | O(nlogn) | O(n) | 是 | 1062 | | 基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(r) | 是 | 1063 | 1064 | #### 二十三、socket编程调用的API 1065 | 1066 | 服务器端的主要步骤:(Win) 1067 | 1068 | - WSAStartup:初始化socket库(Winsock) 1069 | - WSACleanup:清除/终止socket库的使用(WinSock) 1070 | - socket:创建套接字 1071 | - connect:”连接“远程服务器(仅用于客户端) 1072 | - closesocket:释放/关闭套接字 1073 | - bind:绑定套接字的本地IP地址和端口号(通常客户端不需要) 1074 | - listen:置服务器端TCP套接字为监听模式,并设置队列大小(仅用于服务器端TCP套接字) 1075 | - accept:接受/提取一个连接请求,创建新套接字(仅用于服务器端的TCP套接字) 1076 | - recv:接收数据(用于TCP套接字或连接模式的客户端套接字) 1077 | 1078 | linux:服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。 1079 | 1080 | 1081 | 1082 | #### 二十四、extern C的作用 1083 | 1084 | extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。 1085 | 1086 | 这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # interview 2 | C++面试问题 3 | -------------------------------------------------------------------------------- /操作系统.md: -------------------------------------------------------------------------------- 1 | ###操作系统 2 | 3 | ####1、进程和线程以及它们的区别 4 | 5 | #####基本概念: 6 | 7 | - 进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发; 8 | 9 | - 线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。 10 | 11 | #####区别: 12 | 13 | 1.一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。 14 | 15 | 2.**进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。**(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。) 16 | 17 | 3.进程是资源分配的最小单位,线程是CPU调度的最小单位; 18 | 19 | 4.系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,**进程切换的开销也远大于线程切换的开销。** 20 | 21 | 5.通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预 22 | 23 | 6.进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。 24 | 25 | 7.进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉 26 | 27 | 8.进程适应于多核、多机分布;线程适用于多核 28 | 29 | #####linux理解: 30 | 31 | 从资源的角度来看 32 | 通俗的来讲无非是,计算资源,存储资源 33 | 进程可以说是操作系统分配存储资源的最小单元, 34 | 线程则是操作系统分配计算资源的最小单元,它可以独立调度 35 | 但是线程无法独立运行,它必须从属于某一进程,共享进程的存储资源,才能运行 36 | 进程占有了大量的存储资源,所以导致进程的创建和切换开销很大 37 | 线程只占有了少量的存储资源,所以创建和切换的开销小 38 | 从属于一个进程的所有线程,共享进程拥有的资源,所以它们之间可以方便的进行通信 39 | 理论上说Linux内核是没有线程这个概念的,只有内核调度实体(Kernal Scheduling Entry, KSE)这个概念。Linux的线程本质上是一种轻量级的进程,是通过clone系统调用来创建的。它提供了一系列的参数来表示线程可以共享父类的哪些资源,比如页表,打开文件表等等。共享只是简单地用指针指向同一个物理地址,不会在父进程之外开辟新的物理内存。所以创建线程开销比创建进程开销小 40 | 41 | ####2、进程间的通信的几种方式 42 | 43 | - 管道(pipe)及命名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信; 44 | - 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生; 45 | - 消息队列:消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息; 46 | - 共享内存:可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等; 47 | - 信号量:它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。 48 | - 套接字:这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。 49 | 50 | ------ 51 | 52 | #### 3、为什么说共享内存是最有效的进程间通信方式? 53 | 54 | 使用管道(FIFO/消息队列)从一个文件传输信息到另外一个文件需要复制4次。一是,服务器端将信息从相应的文件复制到server临时缓冲区中;二是,从临时缓冲区中复制到管道(FIFO/消息队列);三是,客户端将信息从管道(FIFO/消息队列)复制到client端的缓冲区中;四是,从client临时缓冲区将信息复制到输出文件中。 55 | 56 | 而使用共享内存的话,进程可以直接读写内存,而不需要任何数据的拷贝。从输入文件到共享内存;从共享内存到输出文件。这样就很大程度上提高了数据存取的效率。 57 | 58 | 它将同一块内存区域映射到共享它的不同进程的地址空间中,使得这些进程间的通信就不需要再经过内核,只需对该共享的内存区域进程操作就可以了,但是它需要用户自己进行同步操作。 59 | 60 | ------ 61 | 62 | ####3、线程同步的方式 63 | 64 | ##### 临界区: 65 | 66 | - 通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问; 67 | 68 | ##### 互斥量Synchronized/Lock: 69 | 70 | - 采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。互斥量又称互斥锁,主要用于线程互斥,不能保证按序访问,可以和条件锁一起实现同步。当进入临界区时,需要获得互斥锁并且加锁;当离开临界区时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。其主要的系统调用如下: 71 | 72 | - pthread_mutex_init:初始化互斥锁 73 | 74 | - pthread_mutex_destroy:销毁互斥锁 75 | 76 | - pthread_mutex_lock:以原子操作的方式给一个互斥锁加锁,如果目标互斥锁已经被上锁,pthread_mutex_lock调用将阻塞,直到该互斥锁的占有者将其解锁。 77 | 78 | - pthread_mutex_unlock:以一个原子操作的方式给一个互斥锁解锁。 79 | 80 | ##### 信号量Semphare: 81 | 82 | - 为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。其有关的系统调用为: 83 | 84 | - sem_wait(sem_t *sem):以原子操作的方式将信号量减1,如果信号量值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。 85 | 86 | - sem_post(sem_t *sem):以原子操作将信号量值+1。当信号量大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。 87 | 88 | #####条件变量: 89 | 90 | - 条件变量,又称条件锁,用于在线程之间同步共享数据的值。条件变量提供一种线程间通信机制:当某个共享数据达到某个值时,唤醒等待这个共享数据的一个/多个线程。即,当某个共享变量等于某个值时,调用 signal/broadcast。此时操作共享变量时需要加锁。其主要的系统调用如下: 91 | 92 | - pthread_cond_init:初始化条件变量 93 | 94 | - pthread_cond_destroy:销毁条件变量 95 | 96 | - pthread_cond_signal:唤醒一个等待目标条件变量的线程。哪个线程被唤醒取决于调度策略和优先级。 97 | 98 | - pthread_cond_wait:等待目标条件变量。需要一个加锁的互斥锁确保操作的原子性。该函数中在进入wait状态前首先进行解锁,然后接收到信号后会再加锁,保证该线程对共享资源正确访问。 99 | 100 | ------ 101 | 102 | #### 4、协程 103 | 104 | ##### 1、概念 105 | 106 | 协程,又称微线程。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。 107 | 108 | #####2、协程和线程区别 109 | 110 | 那和多线程比,协程最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。 111 | 112 | 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。 113 | 114 | #####3、其他 115 | 116 | 在协程上利用多核CPU呢——多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。 117 | 118 | Python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。 119 | 120 | 121 | 122 | ------ 123 | 124 | ####4、进程有哪几种状态? 125 | 126 | - 就绪状态:进程已获得除处理机以外的所需资源,等待分配处理机资源; 127 | 128 | - 运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数; 129 | 130 | - 阻塞状态: 进程等待某种条件,在条件满足之前无法执行 131 | 132 | - ![undefined](http://ww1.sinaimg.cn/large/0062FU9hly1ghjr0h1vj9j30ay06z3ym.jpg) 133 | 134 | 135 | 136 | ####5、线程有几种状态? 137 | 138 |   在 Java虚拟机 中,线程从最初的创建到最终的消亡,要经历若干个状态:创建(new)、就绪(runnable/start)、运行(running)、阻塞(blocked)、等待(waiting)、时间等待(time waiting) 和 消亡(dead/terminated)。在给定的时间点上,一个线程只能处于一种状态,各状态的含义如下图所示: 139 | 140 | ![undefined](http://ww1.sinaimg.cn/large/0062FU9hly1ghjr139ptsj30lm0erwfv.jpg) 141 | 142 | ####6、线程安全发生的原因(共享内存) 143 | 144 | 在多个线程并发的情况下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写入已开始,但还没结束),其他线程对这个写了一半的资源进行了读/写操作,导致资源出现数据错误 145 | 146 | ####7、如何避免线程安全问题 147 | 148 | 保证共享资源在同一时间只能由一个线程进行操作(原子性:互斥锁;有序性:线程等待) 149 | 150 | 将线程操作结果及时刷新,保证其他线程可以立即获取到修改后的最新数据(可见性) 151 | 152 | #### 8、什么是死锁?死锁产生的条件? 153 | 154 | 1). 死锁的概念 155 | 156 |   在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲,就是两个或多个进程无限期的阻塞、相互等待的一种状态。 157 | 158 | ------ 159 | 160 | 2). 死锁产生的四个必要条件 161 | 162 | - 互斥:至少有一个资源必须属于非共享模式,即一次只能被一个进程使用;若其他申请使用该资源,那么申请进程必须等到该资源被释放为止; 163 | - 占有并等待:一个进程必须占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有; 164 | - 非抢占:进程不能被抢占,即资源只能被进程在完成任务后自愿释放 165 | - 循环等待:若干进程之间形成一种头尾相接的环形等待资源关系 166 | 167 | ####9、互斥锁(mutex)机制,以及互斥锁和读写锁的区别 168 | 169 | #####1、互斥锁和读写锁区别: 170 | 171 | 互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。 172 | 173 | 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。 174 | 175 | 互斥锁和读写锁的区别: 176 | 177 | 1)读写锁区分读者和写者,而互斥锁不区分 178 | 179 | 2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。 180 | 181 | #####2、Linux的4种锁机制: 182 | 183 | 互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒 184 | 185 | 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。 186 | 187 | 自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。 188 | 189 | RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。 190 | 191 | ####9、分页和分段有什么区别(内存管理)? 192 | 193 |   段式存储管理是一种符合用户视角的内存分配管理方案。在段式存储管理中,将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰。段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片) 194 | 195 |   页式存储管理方案是一种用户视角内存与物理内存相分离的内存分配管理方案。在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的帧,程序加载时,可以将任意一页放入内存中任意一个帧,这些帧不必连续,从而实现了离散分离。页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满)。 196 | 197 | **两者的不同点:** 198 | 199 | - 目的不同:分页是由于系统管理的需要而不是用户的需要,它是信息的物理单位;分段的目的是为了能更好地满足用户的需要,它是信息的逻辑单位,它含有一组其意义相对完整的信息; 200 | - 大小不同:页的大小固定且由系统决定,而段的长度却不固定,由其所完成的功能决定; 201 | - 地址空间不同: 段向用户提供二维地址空间;页向用户提供的是一维地址空间; 202 | - 信息共享:段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制; 203 | - 内存碎片:页式存储管理的优点是没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满);而段式管理的优点是没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)。 204 | 205 | ####10、操作系统中进程调度策略有哪几种? 206 | 207 | - FCFS(先来先服务,队列实现,非抢占的):先请求CPU的进程先分配到CPU 208 | - SJF(最短作业优先调度算法):平均等待时间最短,但难以知道下一个CPU区间长度 209 | - 优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;解决方案:**老化** 210 | - 时间片轮转调度算法(可抢占的):队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。 211 | - 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。 212 | - 多级反馈队列调度算法:与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。 213 | 214 | ####11、说一说进程同步有哪几种机制 215 | 216 |   原子操作、信号量机制、自旋锁管程、会合、分布式系统 217 | 218 | ------ 219 | 220 | ####12、什么是虚拟内存? 221 | 222 | #####虚拟内存 223 | 224 |   虚拟内存允许执行进程不必完全在内存中。虚拟内存的基本思想是:每个进程拥有独立的地址空间,这个空间被分为大小相等的多个块,称为页(Page),每个页都是一段连续的地址。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻进行必要的映射;当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的命令。这样,**对于进程而言,逻辑上似乎有很大的内存空间,实际上其中一部分对应物理内存上的一块(称为帧,通常页和帧大小相等),还有一些没加载在内存中的对应在硬盘上,如图5所示。** 225 | 注意,请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。 226 | 227 | ![undefined](http://ww1.sinaimg.cn/large/0062FU9hly1ghkaeb383kj309r0bg3ys.jpg) 228 | 229 | 由图5可以看出,虚拟内存实际上可以比物理内存大。当访问虚拟内存时,会访问MMU(内存管理单元)去匹配对应的物理地址(比如图5的0,1,2)。如果虚拟内存的页并不存在于物理内存中(如图5的3,4),会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。 230 | 231 | ------ 232 | 233 | #####页面置换算法 234 | 235 | - FIFO先进先出算法:在操作系统中经常被用到,比如作业调度(主要实现简单,很容易想到); 236 | - LRU(Least recently use)最近最少使用算法:根据使用时间到现在的长短来判断; 237 | - LFU(Least frequently use)最少使用次数算法:根据使用次数来判断; 238 | - OPT(Optimal replacement)最优置换算法:理论的最优,理论;就是要保证置换出去的是不再被使用的页,或者是在实际内存中最晚使用的算法。 239 | 240 | ------ 241 | 242 | #####虚拟内存好处 243 | 244 | - 扩大地址空间; 245 | - 内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。 246 | 247 | - 公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。 248 | 249 | - 当进程通信时,可采用虚存共享的方式实现。 250 | 251 | - 当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存 252 | 253 | - 虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高 254 | 255 | - 在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片 256 | 257 | - 在内存中可以保留多个进程,系统并发度提高 258 | - 解除了用户与内存之间的紧密约束,进程可以比内存的全部空间还大 259 | 260 | #####虚拟内存的代价: 261 | 262 | - 虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存 263 | 264 | - 虚拟地址到物理地址的转换,增加了指令的执行时间。 265 | 266 | - 页面的换入换出需要磁盘I/O,这是很耗时的 267 | 268 | - 如果一页中只有一部分数据,会浪费内存。 269 | 270 | #### 13、linux命令 271 | 272 | - **iostat -x 磁盘使用** 273 | 274 | - **free命令。**查看内存使用多少。常用free -h 275 | 276 | - **swap**;现时可用的交换内存(k表示) 277 | 278 | - **ps命令。**ps查看系统进程。ps命令经常会连同管道符一起使用,用来查看某个进程或者它的数量 279 | 280 | **ps-aux或ps-elf.查看当前系统都有哪些进程** 281 | 282 | - **netstat 查看网络状况。**netstat命令用来打印网络连接状况、系统所开放端口、路由表等信息。 283 | 284 | 1、netstat -lnp 打印当前系统启动哪些端口 285 | 286 | 2、netstat -an 打印网络连接状况 287 | 288 | - **linux抓包工具tcpdump** 289 | 有时候想看一下某个网卡上都有哪些数据包,尤其是当你初步判定你的服务器上有流量攻击。这时,使用抓包工具来抓一下数据包,就可以知道有哪些IP在攻击你了 290 | 291 | - **top命令**。实时显示系统中各个进程的资源占用状况。 292 | 293 | 0.1%us:用户态进程占用CPU时间百分比 294 | 0.2%sy:内核占用CPU时间百分比 295 | 0.2%ni:renice值为负的任务的用户态进程的CPU时间百分比。nice是优先级的意思 296 | 99.4%id:空闲CPU时间百分比 297 | 0.0%wa:等待I/O的CPU时间百分比 298 | 0.0%hi:CPU硬中断时间百分比 299 | 0.0%si:CPU软中断时间百分比 300 | 301 | - **vmstat 是用来实时查看内存使用情况,反映的情况比用top直观一些.** 302 | 303 | r即running,表示正在跑的任务数 304 | 305 | b即blocked,表示被阻塞的任务数 306 | 307 | si表示有多少数据从交换分区读入内存 308 | 309 | so表示有多少数据从内存写入交换分区 310 | 311 | bi表示有多少数据从磁盘读入内存 312 | 313 | bo表示有多少数据从内存写入磁盘 314 | 315 | - **ipcs -m  查看系统共享内存信息** 316 | 317 |   ipcs -q  查看系统消息队列信息 318 | 319 |   ipcs -s  查看系统信号量信息 320 | 321 |   ipcs [-a] 系统默认输出信息,显示系统内所有的IPC信息 322 | 323 | - **ipcs -a命令**可以查看当前使用的共享内存、消息队列及信号量所有信息 324 | 325 | - **ipcs -p命令可以得到与共享内存、消息队列相关进程之间的消息**。消息队列id,根据id则可以获取到lspid、lrpid消息,其中lspid代表最近一次向消息队列中发送消息的“进程号”,lrpid对应最近一次从消息队列中读取消息的“进程号” 326 | 327 | - **ipcs -u命令可以查看各个资源的使用总结信息,**其中可以看到使用的信号量集的个数、信号量的个数,以及消息队列中当前使用的消息个数总数、占用的空间字节数。 328 | 329 | - **ipcs -l命令可以查看各个资源的系统限制信息,**可以看到系统允许的最大信号量集及信号量个数限制、最大的消息队列中消息个数等信息。 330 | 331 | - **ipcrm命令。删除消息队列、共享内存、信号灯** 332 | 333 | -M 以shmkey删除共享内存 334 | 335 | -m 以shmid删除共享内存 336 | 337 | -Q 以msgkey删除消息队列 338 | 339 | -q 以msgid删除消息队列 340 | 341 | -S 以semkey删除信号灯 342 | 343 | -s 以semid删除信号灯 344 | 345 | - **disk 显示每秒的磁盘操作。** 346 | 347 | - **df命令。df查看文件系统中磁盘的使用情况。**硬盘已用和可用的存储空间以及其他存储设备。 348 | - grep**命令可以**指定文件中搜索特定的内容,并将含有这些内容的行标准输出。 349 | 350 | **系统信息:** 351 | 352 | uname -a # 查看内核/操作系统/CPU信息 353 | head -n 1 /etc/issue # 查看操作系统版本 354 | cat /proc/cpuinfo # 查看CPU信息 355 | hostname # 查看计算机名 356 | lspci -tv # 列出所有PCI设备 357 | lsusb -tv # 列出所有USB设备 358 | lsmod # 列出加载的内核模块 359 | env # 查看环境变量 360 | 361 | **资源:** 362 | 363 | free -m # 查看内存使用量和交换区使用量 364 | df -h # 查看各分区使用情况 365 | du -sh <目录名> # 查看指定目录的大小 366 | grep MemTotal /proc/meminfo # 查看内存总量 367 | grep MemFree /proc/meminfo # 查看空闲内存量 368 | uptime # 查看系统运行时间、用户数、负载 369 | cat /proc/loadavg # 查看系统负载 370 | 371 | -------------------------------------------------------------------------------- /数据库面试问题.md: -------------------------------------------------------------------------------- 1 | ### 数据库面试问题: 2 | 3 | ####一、数据库范式: 4 | 5 | 范式是关系数据库理论的基础迷失在设计数据库结构过程种索要遵循的规则和指导方法。目前共有8种饭时,1NF,2NF,3NF,BCNF,4NF,5NF,6NF。通常所用到的都是前三个范式。 6 | 7 | - **第一范式(1NF) 无重复的列** 8 | 9 | 强调的是列的原子性,即列不能够再分成其他几列。即数据表中的字段都是单一属性的,不可再分。简而言之,第一范式就是无重复的列。第一范式是对关系模式的基本要求,不满足第一范式的数据库就不是关系数据库。 10 | 11 | - **第二范式(2NF) 属性完全依赖于主键【消除部分子函数依赖】** 12 | 13 | 第二范式是在第一范式的基础上建立起来的,满足第二范式必须先满足第一范式 14 | 15 | 第二范式要求数据库白哦中的每个实例或行必须可以被唯一的区分。为实现区分通常需要为表加上一个列,以存储各个实例的唯一标识。这个唯一属性列被称为主关键字或主键,主码。 16 | 17 | 所有单关键字的数据库表都符合第二范式,因为不可能存在组合关键字。 18 | 19 | - **第三范式(3NF)属性不依赖于其他非主属性【消除传递依赖】** 20 | 21 | 如果关系模式R是第二范式,且每个非主属性都不传递依赖于R的候选键,则称为第三范式模式。满足第三范式必须先满足第二范式。第三范式要求一个数据库表中不包含已在其它表中已包含的非主键关键信息。 22 | 23 | 在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。简言之,第三范式就是属性不依赖于其他非主属性。 24 | 25 | ####二、数据库事务: 26 | 27 | 事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。 28 | 29 | 事务的特征: 30 | 31 | - 原子性:事务所包含的一系列数据库操作要么全部执行,要么全部回滚 32 | 33 | - 一致性:事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态 34 | 35 | - 隔离性:并发执行的事务之间不能相互影响 36 | 37 | - 持久性:事务一旦提交,对数据库中数据的改变使永久性的 38 | 39 | Mysql对事务的支持:mysql对事务的支持不是绑定在mysql服务器本身,而是与存储引擎有关 40 | 41 | MyISAM:不支持事务,用于只读程序提高性能 42 | 43 | InnoDB:支持ACID事务,行级锁、并发。 44 | 45 | Berkeley DB:支持事务 46 | 47 | ####三、如何优化MySql: 48 | 49 | mysql优化主要涉及SQL语句及索引的优化、数据表结构的优化、系统配置的优化和硬件的优化 50 | 51 | ####四、NOSQL数据库——Redis: 52 | 53 | Redis是一款基于内存的且持久化、高性能的Key-value NoSQL数据库,其支持丰富数据类型(string,list,set,sorted set,hash),常被用作缓存的解决方案。Redis具有以下显著特点: 54 | 55 | - 速度快,因为数据在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1) 56 | 57 | - 支持丰富数据类型,支持string,list,set,sorted set, hash 58 | 59 | - 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行 60 | 61 | - 丰富的特性:可用于缓存,消息,按Key设置过期时间,过期后将会自动删除 62 | 63 | ####五、数据库索引 64 | 65 | - **概念** 66 | 67 | **索引是对数据库表中一个或多个列的值进行排序的数据结构,以协助快速查询、更新数据库表中数据。**索引的实现通常使用B_tree及其变种。索引加速了数据访问,因为存储引擎不会再去扫描整张表得到需要的数据;相反它从根节点开始,根节点保存了子节点的指针,存储引擎会根据指针快速寻找数据。 68 | 69 | **优势:**可以快速检索,减少I/O次数,加快检索速度;根据索引分组和排序,可以加快分组和排序; 70 | 71 | **劣势:**索引本身也是表,因此会占用存储空间,一般来说,索引表占用的空间的数据表的1.5倍;索引表的维护和创建需要时间成本,这个成本随着数据量增大而增大;构建索引会降低数据表的修改操作(删除,添加,修改)的效率,因为在修改数据表的同时还需要修改索引表; 72 | 73 | - **索引的分类:** 74 | 75 | 主键索引:即主索引,根据主键pk_clolum(length)建立索引,**不允许重复,不允许空值**; 76 | 77 | 唯一索引:用来建立索引的列的值必须是**唯一的,允许空值**。**主键是唯一索引,这样说没错;但反过来说,唯一索引也是主键就错误了,因为唯一索引允许空值,主键不允许有空值,所以不能说唯一索引也是主键。** 78 | 79 | 普通索引:用表中的普通列构建的索引,没有任何限制(非主键索引) 80 | 81 | 全文索引:用大文本对象的列构建的索引 82 | 83 | 组合索引:用多个列组合构建的索引,这多个列中的值不允许有空值 84 | 85 | - **索引类型** 86 | 87 | MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如**BTree索引,B+Tree索引,哈希索引,全文索引** 88 | 89 | **哈希索引**:只有内存存储引擎支持哈希索引,哈希索引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存执呆滞所在行数据的物理位置,因为使用散列算法,**因此访问速度特别快,但是一个之只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能。** 90 | 91 | **全文索引:**FULLTEXT(全文)索引,仅适用MyISAM和InnoDB,针对较大的数据,生成全文索引非常的消耗时间和空间。对于文本的大对象,或者较大的char类型的数据,如果使用普通索引,那么匹配文本前几个字符还是可以的,但是想要匹配文本中间的几个单词,那么就要使用like%word%来匹配,这样需要很长的时间来处理,响应时间会大大增加,这种情况,就可以使用FULLTEXT索引了,在生成全文索引时,会为文本生成一份单词的清单,在索引时根据这个单词的清单来索引。 92 | 93 | **BTree索引:**BTree是平衡搜索多叉树,设树的度为2d(d>1),高度为h,那么BTree要满足以一下条件: 94 | 95 | 1.每个叶子结点的高度一样,等于h; 96 | 97 | 2.每个非叶子结点由**n-1个key**和**n个指针point**组成,其中d<=n<=2d,key和point相互间隔,结点两端一定是key; 98 | 99 | 3.叶子结点指针都为null; 100 | 101 | 4.非叶子结点的key都是[key,data]二元组,其中key表示作为索引的键,data为键值所在行的数据; 102 | 103 | 在BTree的机构下,就可以使用二分查找的查找方式,查找复杂度为h*log(n),一般来说树的高度是很小的,一般为3左右,因此BTree是一个非常高效的查找结构。 104 | 105 | **B+Tree索引:**B+Tree是BTree的一个变种,设d为树的度数,h为树的高度,B+Tree和BTree的不同主要在于: 106 | 107 | 1.B+Tree中的非叶子结点不存储数据,只存储键值; 108 | 109 | 2.B+Tree的叶子结点没有指针,所有键值都会出现在叶子结点上,且key存储的键值对应data数据的物理地址; 110 | 111 | 3.B+Tree的每个非叶子节点由**n个键值key**和**n个指针point**组成; 112 | 113 | - **B+Tree对比BTree的优点:** 114 | 115 | 1.一般来说B+Tree比BTree更适合实现外存的索引结构,因为存储引擎的设计专家巧妙的利用了外存(磁盘)的存储结构。那么**提升查找速度的关键就在于尽可能少的磁盘I/O,那么可以知道,每个节点中的key个数越多,那么树的高度越小,需要I/O的次数越少,因此一般来说B+Tree比BTree更快,因为B+Tree的非叶节点中不存储data,就可以存储更多的key**。 116 | 117 | 2.查询速度更稳定 118 | 119 | 由于B+Tree非叶子节点不存储数据(data),因此所有的数据都要查询至叶子节点,而叶子节点的高度都是相同的,因此所有数据的查询速度都是一样的。 120 | 121 | - **主键就是聚集索引吗?主键和索引有什么区别?** 122 | 123 | **主键是一种特殊的唯一性索引,其可以是聚集索引,也可以是非聚集索引。**在SQLServer中,主键的创建必须依赖于索引,默认创建的是聚集索引,但也可以显式指定为非聚集索引。**InnoDB作为MySQL存储引擎时,默认按照主键进行聚集,**如果没有定义主键,InnoDB会试着使用唯一的非空索引来代替。如果没有这种索引,InnoDB就会定义隐藏的主键然后在上面进行聚集。所以,**对于聚集索引来说,你创建主键的时候,自动就创建了主键的聚集索引。** 124 | 125 | - **聚集索引和非聚集索引的区别:** 126 | 127 | 1、聚集索引表示表中存储的数据按照索引的顺序存储,检索效率比非聚集索引高,但对数据更新影响较大。(比如主键索引) 128 | 129 | 2、非聚集索引表示数据存储在另一个地方,索引存储在另一个地方,索引带有指针指向数据的存储位置。非聚集索引检索效率比聚集索引低,但对数据更新影响较小。 130 | 131 | #### 六、什么是存储过程?有哪些有点? 132 | 133 | 存储过程事先现经过编译并存储在数据库中的一段SQL语句的集合。进一步地说,存储过程是由一些T-SQL语句组成地代码块,这些T-SQL语句代码像一个方法一样实现一些功能,然后再给这个代码块取一个名字,在用到这个功能的时候直接调用就行。存储过程具有以下特点: 134 | 135 | - 存储过程只在创建时进行编译,以后每次执行存储过程都不需要再进行重新编译,而一般SQL语句每执行一次就编译一次,所以存储过程可以提高数据库执行效率 136 | - 当SQL语句有变动时,可以只修改数据库中的存储过程而不必修改代码 137 | - 减少网络传输,在客户端调用一个存储过程当然比执行一串SQL传输的数据量要小 138 | - 通过存储过程能够使没有权限的用户在控制之下间接的存取数据库,从而确保数据的安全 139 | 140 | #### 七,简单说说drop、delete与truncate的区别 141 | 142 | SQL中的drop、delete、truncate都表示删除,但是三者有一些区别: 143 | 144 | - delete用来删除表中的全部数据或者部分数据行,执行delete之后,用户需要提交或者回滚来执行删除或者撤销删除,delete命令会触发这个表上所有的delete触发器 145 | - turncate删除表中的所有数据,这个操作不能回滚,也不会触发这个表上的触发器,turncate比delete更快,占用的空间更小 146 | - drop命令从数据库中删除表,所有的数据行,索引和权限也会被删除,所有的DML触发器也不会被触发,这个命令也不能回滚。 147 | 148 | #### 八、视图 149 | 150 | 视图是一种虚拟的表,通常是有一个表或者多个表的行或列的子集,具有和物理表相同的功能,可以对视图进行增、删、改、查等操作。特别地,对视图的修改不影响基本表。相比多表查询,它使得我们获取数据更容易。 151 | 152 | #### 九、触发器 153 | 154 | 触发器是与表相关的数据库对象,在满足定义条件时出发,并执行触发器中定义的语句集合。触发器的这种特性可以协助应用再数据库端确保数据库的完整性 155 | 156 | #### 十、Mysql中的悲观锁和乐观锁的实现 157 | 158 | 悲观锁与乐观锁是两种常见的资源并并发锁设计思路,也是并发编程中一个非常基础的概念 159 | 160 | - 悲观锁 161 | 162 | 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。通常来讲,在数据库上的悲观锁需要数据库本身提供支持,即通过常用的select … for update操作来实现悲观锁。 163 | 164 | - 乐观锁 165 | 166 | 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。乐观锁的特点先进行业务操作,只在最后实际更新数据时进行检查数据是否被更新过,若未被更新过,则更新成功;否则,失败重试。乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。 167 | 168 | - 乐观锁与悲观锁的应用场景 169 | 170 | 一般情况下,读多写少更适合用乐观锁,读少写多更适合用悲观锁。乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能。 171 | 172 | #### 十一、MySQL存储引擎的MyISAM和InnoDB详解区别 173 | 174 | MyISAM是mysql的默认存储引擎,虽然MyISAM性能极佳,但是有一个明确的缺点:不支持事务处理。mySQL也导入了另一种数据库引擎InnoDB,以强化参考完整性与并发违规处理机制,**InnoDB的最大特色就是支持ACID兼容的事务功能**。 175 | 176 | - **存储结构不同:**每个MyISAM在磁盘上存储成三个文件:.frm文件存储表定义,数据文件的扩展名为.MYD,索引文件的扩展名.MYI。InnoDB所有的表都保存在同一个数据文件中,InnoDB的表只受限于操作系统文件的大小 177 | - **存储空间:**MyISAM可被压缩,占据的存储空间小,支持静态表、动态表、压缩表三种不同的存储格式。InnoDB需要更多的内存和存储,它会在内存中建立其专用的缓冲池用于告诉缓冲数据和索引 178 | - **可移植性、备份及恢复:**MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便,同时在备份和回复时也可单独针对某个表进行操作。InnoDB免费的方案可以是拷贝数据文件、备份binlog,或者用mysqldump,数据量达到几十G就很痛苦了 179 | - **事务支持:**MyISAM强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。InnoDB提供事务,外键等高级数据库功能,具有事务提交、回滚和崩溃修复能力。 180 | - **表锁差异:**MyISAM只支持表级锁,用户在操作MyISAM表时,select、update、delete和insert语句都会给表自动加锁,如果加锁以后的表满足insert并发的情况下,可以在表的尾部插入新的数据。InnoDB支持事务和行级锁。行锁大幅度提高了多用户并发操作的新能,但是InnoDB的行锁,只是在WHERE的主键是有效的,非主键的WHERE都会锁全表的。 181 | - **全文索引:**MyISAM支持 FULLTEXT类型的全文索引;InnoDB不支持FULLTEXT类型的全文索引,但是innodb可以使用sphinx插件支持全文索引,并且效果更好。 182 | - **表主键:**MyISAM允许没有任何索引和主键的表存在,索引都是保存行的地址。对于InnoDB,如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见),数据是主索引的一部分,附加索引保存的是主索引的值。 183 | - **外键:**MyISAM不支持外键,而InnoDB支持外键。 184 | 185 | #### 十二、数据库连接池 186 | 187 | 对于一个简单的数据库应用,由于对于数据库的访问不是很频繁。这时可以简单地在需要访问数据库时,就新创建一个连接,用完后就关闭它,这样做也不会带来什么明显的性能上的开销。但是对于一个复杂的数据库应用,情况就完全不同了。频繁的建立、关闭连接,会极大的减低系统的性能,因为对于连接的使用成了系统性能的瓶颈。 188 | 189 | 连接复用。通过建立一个数据库连接池以及一套连接使用管理策略,使得一个数据库连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。数据库连接池的基本原理是在内部对象池中维护一定数量的数据库连接,并对外暴露数据库连接获取和返回方法。 190 | 191 | **数据库连接池技术带来的优势**: 192 | 193 | 1. 资源重用 194 | 195 | 由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增进了系统运行环境的平稳性(减少内存碎片以及数据库临时进程/线程的数量)。 196 | 197 | 2. 更快的系统响应速度 198 | 199 | 数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。 200 | 201 | 3. 新的资源分配手段 202 | 203 | 对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接池技术,几年钱也许还是个新鲜话题,对于目前的业务系统而言,如果设计中还没有考虑到连接池的应用,那么…….快在设计文档中加上这部分的内容吧。某一应用最大可用数据库连接数的限制,避免某一应用独占所有数据库资源。 204 | 205 | 4. 统一的连接管理,避免数据库连接泄漏 206 | 207 | 在较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用连接。从而避免了常规数据库连接操作中可能出现的资源泄漏。 208 | 209 | #### 十三、事务的隔离级别 210 | 211 | #####事务的并发问题: 212 | 213 | 1、**脏读:**脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。(事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据) 214 | 215 | 2、**不可重复读:**是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。) 216 | 217 | 3、**幻读:**第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。(系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。) 218 | 219 | **小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表** 220 | 221 | #####**隔离级别(InnoDB默认是可重复读)**: 222 | 223 | | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 224 | | :------: | :----: | :--------: | :----: | 225 | | 未提交读 | 可能 | 可能 | 可能 | 226 | | 已提交读 | 不可能 | 可能 | 可能 | 227 | | 可重复读 | 不可能 | 不可能 | 可能 | 228 | | 可串行化 | 不可能 | 不可能 | 不可能 | 229 | 230 | - 未提交读(Read Uncommitted):一个事务在执行过程中可以看到其他事务没有提交的新插入的记录,而且还能看到其他事务没有提交的对已有记录的更新。允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。 231 | - 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读) 232 | - 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读 233 | - 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞 234 | 235 | **1、事务隔离级别为读提交时,写数据只会锁住相应的行** 236 | 237 | **2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。** 238 | 239 | **3、事务隔离级别为串行化时,读写数据都会锁住整张表** 240 | 241 | **4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。** 242 | 243 | ####十四、数据库主从同步 244 | 245 | 数据库的操作往往会成为一个系统的瓶颈所在,而且一般系统“读”的压力远远大于“写”,因此我们可以通过实现数据库的读写分离来提高系统的性能。主数据库负责“写操作”,从数据库负责读操作,根据压力情况,数据库可以部署多个提高“读”的速度。 246 | 247 | 主服务器master记录数据库操作日志到Binary log,从服务器开启i/o线程将二进制日志记录的操作同步到relay log(中继日志)(存在从服务器的缓存中),另外sql线程将relay log(中继)日志记录的操作在从服务器执行。 248 | 249 | #### 十五、数据库索引 250 | 251 | 我先说下什么是索引,索引是对数据库表中一个或多个列的值进行排序的数据结构,协助快速查询、更新数据库表中数据。主要目的就是加快检索表中数据,能协助信息搜索者尽快的找到符合限制条件的记录ID的辅助数据结构。 252 | 253 | 索引的优点是:通过建立索引可以极大地提高在数据库中获取所需信息的速度,同时还能提高服务器处理相关搜索请求的效率。 254 | 255 | 但是索引也有缺点,它的缺点是:在数据库中创建索引,需要占用一定的物理存储空间;索引表的维护和创建需要时间成本,这个成本随着数据量增大越来越大;对表中的数据进行修改时,还需要修改索引表,这给数据库的维护带来了一定的麻烦。 256 | 257 | 索引的底层实现是B+树,B+树比B树更适合数据库索引。 258 | 259 | **B+树的磁盘读写代价更低:**B+树的中间节点不保存数据,能容纳更多节点元素,如果把同一内部节点的关键字存放在同一盘块中,那么能容纳的关键字数量就越多,一次性读入内存需要查找的关键字就越多,相对IO读写次数就降低了。 260 | **B+树的查询效率更加稳定:**任何关键字的查找必须走一条从根节点到叶子节点的路,所有关键字查找的路径长度相同,导致每一个数据的查询效率更加稳定。 261 | **B+树的区间查找遍历更高效:**由于B+树的数据都存储在叶子节点中,叶子节点用链表连接,区间查找和遍历只需要扫一遍叶子节点即可,B树因为其分支节点同样存储数据,要找到具体的数据,需要进行每一层的递归和遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好,通常用B+树作数据库索引。 262 | 263 | **聚集索引(聚簇索引)**。表数据按照索引的顺序来存储的,也就是说索引项的顺序与表中记录的物理顺序一致。对于聚集索引,叶子结点即存储了真实的数据行,不再有另外单独的数据页。 在一张表上最多只能创建一个聚集索引,因为真实数据的物理顺序只能有一种。(B+树)如果不创建索引,系统会自动创建一个隐含列表作为聚集索引。 264 | 265 | **非聚集索引(非聚簇索引)。**该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。对于非聚集索引,叶结点包含索引字段值及指向数据页数据行的逻辑指针,其行数量与数据表行数据量一致。 266 | 267 | MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。 268 | 269 | InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。InnoDB的数据文件本身就是索引文件,数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。InnoDB的辅助索引data域存储相应记录主键的值而不是地址。 270 | 271 | MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分。 272 | 273 | 274 | 275 | 唯一索引:用来建立索引的列的值必须是**唯一的,允许空值**。**主键是唯一索引,这样说没错;但反过来说,唯一索引也是主键就错误了,因为唯一索引允许空值,主键不允许有空值,所以不能说唯一索引也是主键。** 276 | 277 | 普通索引:用表中的普通列构建的索引,没有任何限制(非主键索引) 278 | 279 | 全文索引:用大文本对象的列构建的索引 280 | 281 | #### 十六、连接池 282 | 283 | 对于一个简单的数据库应用,由于对于数据库的访问不是很频繁。这时可以简单地在需要访问数据库时,就新创建一个连接,用完后就关闭它,这样做也不会带来什么明显的性能上的开销。但是对于一个复杂的数据库应用。频繁的建立、关闭连接,会极大的减低系统的性能,因为对于连接的使用成了系统性能的瓶颈。 284 | 285 | 怎样解决:连接复用。通过建立一个数据库连接池以及一套连接使用管理策略,使得一个数据库连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。就是共享连接资源,数据库连接池的基本原理是在内部对象池中维护一定数量的数据库连接,并对外暴露数据库连接获取和返回方法。 286 | 287 | 外部使用者可通过getConnection 方法获取连接,使用完毕后再通过releaseConnection 方法将连接返回,注意此时连接并没有关闭,而是由连接池管理器回收,并为下一次使用做好准备。 288 | 289 | #####数据库连接池技术带来的优势: 290 | 291 | - 资源重用 292 | 293 | 由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增进了系统运行环境的平稳性(减少内存碎片以及数据库临时进程/线程的数量)。 294 | 295 | - 更快的系统响应速度 296 | 297 | 数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。 298 | 299 | - 新的资源分配手段 300 | 301 | 对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接池技术,几年钱也许还是个新鲜话题,对于目前的业务系统而言,如果设计中还没有考虑到连接池的应用,那么…….快在设计文档中加上这部分的内容吧。某一应用最大可用数据库连接数的限制,避免某一应用独占所有数据库资源。 302 | 303 | - 统一的连接管理,避免数据库连接泄漏 304 | 305 | 在较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用连接。从而避免了常规数据库连接操作中可能出现的资源泄漏。 306 | 307 | 308 | 309 | #### 十七、数据库语句 310 | 311 | ``` 312 | 选择:``select` `* ``from` `table1 ``where` `范围 313 | 插入:``insert` `into` `table1(field1,field2) ``values``(value1,value2) 314 | 删除:``delete` `from` `table1 ``where` `范围 315 | 更新:``update` `table1 ``set` `field1=value1 ``where` `范围 316 | 查找:``select` `* ``from` `table1 ``where` `field1 ``like` `’%value1%’ ``---like的语法很精妙,查资料! 317 | 排序:``select` `* ``from` `table1 ``order` `by` `field1,field2 [``desc``] 318 | 总数:``select` `count` `as` `totalcount ``from` `table1 319 | 求和:``select` `sum``(field1) ``as` `sumvalue ``from` `table1 320 | 平均:``select` `avg``(field1) ``as` `avgvalue ``from` `table1 321 | 最大:``select` `max``(field1) ``as` `maxvalue ``from` `table1 322 | 最小:``select` `min``(field1) ``as` `minvalue ``from` `table1 323 | ``` 324 | 325 | -------------------------------------------------------------------------------- /笔试常见题.md: -------------------------------------------------------------------------------- 1 | #### 1、顺时针打印矩阵 2 | 3 | ##### 1、行列相等,直接打印矩阵 4 | 5 | 从右上角开始,不断向下走,然后向左,然后向右,继续第二层循环,以知道填满为止。最后输出这个矩阵。n的范围是1-20之间。 6 | 7 | ```c++ 8 | #include 9 | #include 10 | using namespace std; 11 | int main() 12 | { 13 | int n; 14 | cin >> n; 15 | if (n>20 || n < 1) 16 | cout << "error" << endl; 17 | else 18 | { 19 | int i = 1; 20 | vector>res(n,vector(n)); 21 | int left = 0, right = n - 1, top = 0, bottom = n - 1;//四个对应的边界 22 | while (left <= right && top <= bottom) 23 | { 24 | for (int row = top; row <= bottom; row++) 25 | { 26 | res[row][right] = i; 27 | ++i; 28 | } 29 | for (int col = right-1; col >= left; --col) 30 | { 31 | res[bottom][col] = i; 32 | ++i; 33 | } 34 | if (left < right && top < bottom) 35 | { 36 | for (int row = bottom - 1; row >= top; row--) 37 | { 38 | res[row][left] = i; 39 | ++i; 40 | } 41 | for (int col = left + 1; col < right; col++) 42 | { 43 | res[left][col] = i; 44 | ++i; 45 | } 46 | } 47 | left++; 48 | right--; 49 | top++; 50 | bottom--; 51 | } 52 | for (int i = 0; i < n; i++) 53 | { 54 | for (int j = 0; j < n; j++) 55 | { 56 | cout << res[i][j] << " "; 57 | } 58 | cout << endl; 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | 从左上角开始: 65 | 66 | ```c++ 67 | #include 68 | #include 69 | using namespace std; 70 | int main() 71 | { 72 | int n; 73 | cin >> n; 74 | if (n < 1 && n > 20) 75 | cout << "error" << endl; 76 | else 77 | { 78 | int i = 1; 79 | vector>res(n, vector(n)); 80 | int left = 0, right = n - 1, top = 0, bottom = n - 1; 81 | while (left <= right && top <= bottom) 82 | { 83 | for (int col = left; col <= right; col++) 84 | { 85 | res[top][col] = i; 86 | i++; 87 | } 88 | for (int row = top + 1; row <= bottom; row++) 89 | { 90 | res[row][right] = i; 91 | i++; 92 | } 93 | if (left < right && top < bottom) 94 | { 95 | for (int col = right - 1; col > left; col--) 96 | { 97 | res[bottom][col] = i; 98 | i++; 99 | } 100 | for (int row = bottom; row > top; row--) 101 | { 102 | res[row][left] = i; 103 | i++; 104 | } 105 | } 106 | left++; 107 | right--; 108 | top++; 109 | bottom--; 110 | } 111 | for (int i = 0; i < n; i++) 112 | { 113 | for (int j = 0; j < n; j++) 114 | { 115 | cout << res[i][j] << " "; 116 | } 117 | cout << endl; 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | #####2、针对一个矩阵,按照从外向里的顺时针顺序依次打出每一个数字 124 | 125 | ``` 126 | if (matrix.size() == 0 || matrix[0].size() == 0) { 127 | return {}; 128 | } 129 | int rows = matrix.size(), columns = matrix[0].size(); 130 | vector order; 131 | int left = 0, right = columns - 1, top = 0, bottom = rows - 1; 132 | while (left <= right && top <= bottom) { 133 | for (int col = left; col <= right; col++) { 134 | order.push_back(matrix[top][col]); 135 | } 136 | for (int row = top + 1; row <= bottom; row++) { 137 | order.push_back(matrix[row][right]); 138 | } 139 | if (left < right && top < bottom) { 140 | for (int col = right - 1; col > left; col--) { 141 | order.push_back(matrix[bottom][col]); 142 | } 143 | for (int row = bottom; row > top; row--) { 144 | order.push_back(matrix[row][left]); 145 | } 146 | } 147 | left++; 148 | right--; 149 | top++; 150 | bottom--; 151 | } 152 | return order; 153 | } 154 | ``` 155 | 156 | ####2、矩阵中的最小路径和 157 | 158 | ``` 159 | //矩阵种的最短路径 160 | #include 161 | #include 162 | #include 163 | using namespace std; 164 | int main() 165 | { 166 | vector>grid = { { 1, 3, 1 }, { 1, 5, 1 }, { 4, 2, 1 } }; 167 | int row = grid.size(), col = grid[0].size(); 168 | if (row == 0 || col == 0) 169 | return 0; 170 | auto dp = vector < vector >(row, vector (col)); 171 | dp[0][0] = grid[0][0]; 172 | for (int i = 1; i < row; i++) { 173 | dp[i][0] = dp[i - 1][0] + grid[i][0]; 174 | } 175 | for (int j = 1; j < col; j++) { 176 | dp[0][j] = dp[0][j - 1] + grid[0][j]; 177 | } 178 | for (int i = 1; i < row; i++) { 179 | for (int j = 1; j < col; j++) { 180 | dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; 181 | } 182 | } 183 | cout << dp[row - 1][col - 1] << endl; 184 | } 185 | ``` 186 | 187 | ####3、两地调度 188 | 189 | 公司计划面试 2N 人。第 i 人飞往 A 市的费用为 costs【i】【0】,飞往 B 市的费用为 costs【i】【1】。返回将每个人都飞到某座城市的最低费用,要求每个城市都有 N 人抵达。 190 | 191 | **解题思路:**首先,将全部的人去A市,计算总和sum;然后计算每一个人去A市比去B市多的价钱(差额),根据差额的大小进行排序,用sum减去差额最大的前一半的钱数。即在原来sum的基础上减去去A 市花费最多人数前一半的和,加上去B市花费最少前一半的人数的花费,即得到最低总费用。 192 | 193 | ``` 194 | #include 195 | #include 196 | #include 197 | using namespace std; 198 | int main() 199 | { 200 | vector>vec = { { 10, 20 }, { 30, 200 }, { 400, 50 }, { 30, 20 } }; 201 | vectortmp; 202 | int sum = 0; 203 | for (int i = 0; i < vec.size(); i++) 204 | { 205 | tmp.push_back(vec[i][0]-vec[i][1]); 206 | sum += vec[i][0]; 207 | } 208 | sort(tmp.rbegin(),tmp.rend()); 209 | for (int i = 0; i < tmp.size() / 2; i++) 210 | { 211 | sum -= tmp[i]; 212 | } 213 | cout << sum << endl; 214 | return 0; 215 | } 216 | ``` 217 | 218 | 219 | 220 | ####4、最长上升子序列 221 | 222 | 给定一个无序的整数数组,找到其中最长上升子序列的长度。 223 | 224 | 示例: 225 | 226 | 输入: [10,9,2,5,3,7,101,18] 227 | 输出: 4 228 | 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。 229 | 230 | 思路:动态规划。 231 | 232 | ``` 233 | int lengthOfLIS(vector& nums) { 234 | int n = nums.size(); 235 | if (n == 0) return 0; 236 | vectordp(n,0); 237 | for(int i = 0;i < n;i++) 238 | { 239 | dp[i] = 1; 240 | for(int j = 0;j < i;j++) 241 | { 242 | if(nums[j] < nums[i]) 243 | { 244 | dp[i] = max(dp[i],dp[j]+1); 245 | } 246 | } 247 | } 248 | sort(dp.rbegin(),dp.rend()); 249 | return *(dp.begin()); 250 | } 251 | ``` 252 | 253 | 254 | 255 | #### 5、二分查找 256 | 257 | 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。 258 | 259 | 260 | 示例 1: 261 | 262 | 输入: nums = [-1,0,3,5,9,12], target = 9 263 | 输出: 4 264 | 解释: 9 出现在 nums 中并且下标为 4 265 | 266 | ``` 267 | int search(vector& nums, int target) { 268 | int left = 0,right = nums.size()-1,mid; 269 | while(left <= right) 270 | { 271 | mid = (left+right)/2; 272 | if(target == nums[mid]) 273 | return mid; 274 | if(nums[mid] < target) 275 | left = mid+1; 276 | else 277 | right = mid-1; 278 | } 279 | return -1; 280 | } 281 | ``` 282 | 283 | ####6、滑动窗口的最大值 284 | 285 | 给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。 286 | 287 | 示例: 288 | 289 | 输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 290 | 输出: [3,3,5,5,6,7] 291 | 解释: 292 | 293 | 滑动窗口的位置 最大值 294 | --------------- ----- 295 | [1 3 -1] -3 5 3 6 7 3 296 | 1 [3 -1 -3] 5 3 6 7 3 297 | 1 3 [-1 -3 5] 3 6 7 5 298 | 1 3 -1 [-3 5 3] 6 7 5 299 | 1 3 -1 -3 [5 3 6] 7 6 300 | 1 3 -1 -3 5 [3 6 7] 7 301 | 302 | **思路:**我们观察一下窗口移动的过程类似于队列出队入队的过程,每次队尾出一个元素,然后队头插入一个元素,求该队列中的最大值,这个和 包含min函数的栈 有点类似,我们可以维护一个递减队列,队列用来保存可能是最大值的数字的index。当前窗口最大值的index在队首,当窗口滑动时,会进入一个新值,出去一个旧值,我们需要给出当前窗口的最大值。 303 | 304 | 需要先检查队首(上一窗口的最大值)的index是否还在当前窗口内,如果不在的话需要淘汰。 305 | 然后新进入的值要和队尾元素做比较,如果比队尾元素大,那么队尾元素出队(用到双端队列特性的地方),直到队列为空或者前面的值不比他小为止。 306 | 307 | ``` 308 | class Solution { 309 | public: 310 | vector maxSlidingWindow(vector& nums, int k) { 311 | vector ans; 312 | deque deq; 313 | int n = nums.size(); 314 | for (int i = 0; i < n; i++){ 315 | //新元素入队时如果比队尾元素大的话就替代队尾元素 316 | while(!deq.empty() && nums[i] > nums[deq.back()]){ 317 | deq.pop_back(); 318 | } 319 | //检查队首的index是否在窗口内,不在的话需要出队 320 | if (!deq.empty() && deq.front() < i - k + 1) deq.pop_front(); 321 | deq.push_back(i); 322 | if (i >= k -1) ans.push_back(nums[deq.front()]); 323 | } 324 | return ans; 325 | } 326 | }; 327 | ``` 328 | 329 | ####7、最长回文子串 330 | 331 | 给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。 332 | 333 | 示例 1: 334 | 335 | 输入: "babad" 336 | 输出: "bab" 337 | 注意: "aba" 也是一个有效答案。 338 | 339 | 示例 2: 340 | 341 | 输入: "cbbd" 342 | 输出: "bb" 343 | 344 | 思路与算法 345 | 346 | ![微信图片_20200828154112.png](http://ww1.sinaimg.cn/large/0062FU9hly1gi6ki984h4j30md0hygn6.jpg) 347 | 348 | ``` 349 | class Solution { 350 | public: 351 | string longestPalindrome(string s) { 352 | int n = s.size(); 353 | vector> dp(n, vector(n)); 354 | string ans; 355 | for (int l = 0; l < n; ++l) { 356 | for (int i = 0; i + l < n; ++i) { 357 | int j = i + l; 358 | if (l == 0) { 359 | dp[i][j] = 1; 360 | } 361 | else if (l == 1) { 362 | dp[i][j] = (s[i] == s[j]); 363 | } 364 | else { 365 | dp[i][j] = (s[i] == s[j] && dp[i + 1][j - 1]); 366 | } 367 | if (dp[i][j] && l + 1 > ans.size()) { 368 | ans = s.substr(i, l + 1); 369 | } 370 | } 371 | } 372 | return ans; 373 | } 374 | }; 375 | ``` 376 | 377 | #### 8、图的广度优先搜索(BFS)和深度优先搜索(DFS) 378 | 379 | #####1、图的广度优先搜索(BFS) 380 | 381 | 广度优先搜索是按层来处理顶点,距离开始点最近的那些顶点首先被访问,而最远的那些顶点则最后被访问,这个和树的层序变量很像,BFS的代码使用了一个队列。搜索步骤: 382 |   a .首先选择一个顶点作为起始顶点,并将其染成灰色,其余顶点为白色。 383 |   b. 将起始顶点放入队列中。 384 |   c. 从队列首部选出一个顶点,并找出所有与之邻接的顶点,将找到的邻接顶点放入队列尾部,将已访问过顶点涂成黑色,没访问过的顶点是白色。如果顶点的颜色是灰色,表示已经发现并且放入了队列,如果顶点的颜色是白色,表示还没有发现 385 |   d. 按照同样的方法处理队列中的下一个顶点。 386 |   基本就是出队的顶点变成黑色,在队列里的是灰色,还没入队的是白色。 387 | 388 | **2.1无向图的广度优先搜索** 389 | 390 | ![345.png](http://ww1.sinaimg.cn/large/0062FU9hly1gia49ctx9uj30f50ehgmr.jpg) 391 | 392 | 如上图访问步骤为: 393 | 394 | **第1步**:访问A。 395 | **第2步**:依次访问C,D,F。 396 | 在访问了A之后,接下来访问A的邻接点。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,C在"D和F"的前面,因此,先访问C。再访问完C之后,再依次访问D,F。 397 | **第3步**:依次访问B,G。 398 | 在第2步访问完C,D,F之后,再依次访问它们的邻接点。首先访问C的邻接点B,再访问F的邻接点G。 399 | **第4步**:访问E。 400 | 在第3步访问完B,G之后,再依次访问它们的邻接点。只有G有邻接点E,因此访问G的邻接点E。 401 | 402 | 因此访问顺序是:**A -> C -> D -> F -> B -> G -> E** 403 | 404 | **2.2有向图的广度优先搜索** 405 | 406 | ![456.png](http://ww1.sinaimg.cn/large/0062FU9hly1gia4bnl0e9j30g70cyq48.jpg) 407 | 408 | **第1步**:访问A。 409 | **第2步**:访问B。 410 | **第3步**:依次访问C,E,F。 411 | 在访问了B之后,接下来访问B的出边的另一个顶点,即C,E,F。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,因此会先访问C,再依次访问E,F。 412 | **第4步**:依次访问D,G。 413 | 在访问完C,E,F之后,再依次访问它们的出边的另一个顶点。还是按照C,E,F的顺序访问,C的已经全部访问过了,那么就只剩下E,F;先访问E的邻接点D,再访问F的邻接点G。 414 | 415 | 因此访问顺序是:**A -> B -> C -> E -> F -> D -> G** 416 | 417 | ##### 2、图的深度优先搜索(DFS) 418 | 419 | 图的深度优先搜索(Depth First Search),和树的先序遍历比较类似。 420 | 421 | 它的思想:假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。 若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。 422 | 423 | 显然,深度优先搜索是一个递归的过程。 424 | 425 | **2.1无向图的深度优先搜索** 426 | 427 | ![123.png](http://ww1.sinaimg.cn/large/0062FU9hly1gia43xrgw4j30c008oglm.jpg) 428 | 429 | 如上图访问步骤: 430 | 431 | **第1步**:访问A。 432 | **第2步**:访问(A的邻接点)C。 433 | 在第1步访问A之后,接下来应该访问的是A的邻接点,即"C,D,F"中的一个。但在本文的实现中,顶点ABCDEFG是按照顺序存储,C在"D和F"的前面,因此,先访问C。 434 | **第3步**:访问(C的邻接点)B。 435 | 在第2步访问C之后,接下来应该访问C的邻接点,即"B和D"中一个(A已经被访问过,就不算在内)。而由于B在D之前,先访问B。 436 | **第4步**:访问(C的邻接点)D。 437 | 在第3步访问了C的邻接点B之后,B没有未被访问的邻接点;因此,返回到访问C的另一个邻接点D。 438 | **第5步**:访问(A的邻接点)F。 439 | 前面已经访问了A,并且访问完了"A的邻接点B的所有邻接点(包括递归的邻接点在内)";因此,此时返回到访问A的另一个邻接点F。 440 | **第6步**:访问(F的邻接点)G。 441 | **第7步**:访问(G的邻接点)E。 442 | 443 | 因此访问顺序是:**A -> C -> B -> D -> F -> G -> E** 444 | 445 | **2.2有向图的深度优先搜索** 446 | 447 | ![234.png](http://ww1.sinaimg.cn/large/0062FU9hly1gia464n5kej308y0993yk.jpg) 448 | 449 | 如上图访问步骤: 450 | 451 | **第1步**:访问A。 452 | **第2步**:访问B。 453 | 在访问了A之后,接下来应该访问的是A的出边的另一个顶点,即顶点B。 454 | **第3步**:访问C。 455 | 在访问了B之后,接下来应该访问的是B的出边的另一个顶点,即顶点C,E,F。在本文实现的图中,顶点ABCDEFG按照顺序存储,因此先访问C。 456 | **第4步**:访问E。 457 | 接下来访问C的出边的另一个顶点,即顶点E。 458 | **第5步**:访问D。 459 | 接下来访问E的出边的另一个顶点,即顶点B,D。顶点B已经被访问过,因此访问顶点D。 460 | **第6步**:访问F。 461 | 接下应该回溯"访问B的出边的另一个顶点F"。 462 | **第7步**:访问G。 463 | 464 | 因此访问顺序是:**A -> B -> C -> E -> D -> F -> G** 465 | 466 | #####3、例题 467 | 468 | 有 N 个房间,开始时你位于 0 号房间。每个房间有不同的号码:0,1,2,...,N-1,并且房间里可能有一些钥匙能使你进入下一个房间。 469 | 470 | 在形式上,对于每个房间 i 都有一个钥匙列表 rooms[i],每个钥匙 rooms[i][j] 由 [0,1,...,N-1] 中的一个整数表示,其中 N = rooms.length。 钥匙 rooms【i】【j】 = v 可以打开编号为 v 的房间。最初,除 0 号房间外的其余所有房间都被锁住。你可以自由地在房间之间来回走动。如果能进入每个房间返回 true,否则返回 false。 471 | 472 | 示例 1: 473 | 474 | 输入: [[1],[2],[3],[]] 475 | 输出: true 476 | 解释: 477 | 我们从 0 号房间开始,拿到钥匙 1。 478 | 之后我们去 1 号房间,拿到钥匙 2。 479 | 然后我们去 2 号房间,拿到钥匙 3。 480 | 最后我们去了 3 号房间。 481 | 由于我们能够进入每个房间,我们返回 true。 482 | 483 | 示例 2: 484 | 485 | 输入:[[1,3],[3,0,1],[2],[0]] 486 | 输出:false 487 | 解释:我们不能进入 2 号房间。 488 | 489 | **解题思路:** 490 | 491 | 当 xx 号房间中有 yy 号房间的钥匙时,我们就可以从 xx 号房间去往 yy 号房间。如果我们将这 nn 个房间看成有向图中的 nn 个节点,那么上述关系就可以看作是图中的 xx 号点到 yy 号点的一条有向边。 492 | 493 | 这样一来,问题就变成了给定一张有向图,询问从 00 号节点出发是否能够到达所有的节点。 494 | 495 | **广度优先搜索:** 496 | 497 | 我们也可以使用广度优先搜索的方式遍历整张图,统计可以到达的节点个数,并利用数组 \textit{vis}*vis* 标记当前节点是否访问过,以防止重复访问。 498 | 499 | ``` 500 | bool canVisitAllRooms(vector>& rooms) { 501 | int n = rooms.size(), num = 0; 502 | vector vis(n); 503 | queue que; 504 | vis[0] = true; 505 | que.emplace(0); 506 | while (!que.empty()) { 507 | int x = que.front(); 508 | que.pop(); 509 | num++; 510 | for (auto& it : rooms[x]) { 511 | if (!vis[it]) { 512 | vis[it] = true; 513 | que.emplace(it); 514 | } 515 | } 516 | } 517 | return num == n; 518 | } 519 | ``` 520 | 521 | **深度优先搜索:** 522 | 523 | 我们可以使用深度优先搜索的方式遍历整张图,统计可以到达的节点个数,并利用数组 \textit{vis}*vis* 标记当前节点是否访问过,以防止重复访问。 524 | 525 | ``` 526 | class Solution { 527 | public: 528 | vector vis; 529 | int num; 530 | 531 | void dfs(vector>& rooms, int x) { 532 | vis[x] = true; 533 | num++; 534 | for (auto& it : rooms[x]) { 535 | if (!vis[it]) { 536 | dfs(rooms, it); 537 | } 538 | } 539 | } 540 | 541 | bool canVisitAllRooms(vector>& rooms) { 542 | int n = rooms.size(); 543 | num = 0; 544 | vis.resize(n); 545 | dfs(rooms, 0); 546 | return num == n; 547 | } 548 | }; 549 | ``` 550 | 551 | #### 9、岛屿数量(华为笔试) 552 | 553 | 给你一个由 'S'(陆地)和 'H'(水)组成的的二维网格,请你计算网格中岛屿的数量。 554 | 555 | 岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。 556 | 557 | 此外,你可以假设该网格的四条边均被水包围。 558 | 559 | 输入格式: 560 | 561 | 4,5 562 | SSHHH 563 | SSHHH 564 | HHSHH 565 | HHHSS 566 | 567 | 输出格式: 568 | 569 | 3 570 | 571 | 使用无向图的广度优先遍历解法: 572 | 573 | ``` 574 | #include 575 | #include 576 | #include 577 | #include 578 | using namespace std; 579 | 580 | int main() 581 | { 582 | 583 | string str1, str2; 584 | getline(cin, str1, ','); 585 | getline(cin, str2); 586 | 587 | int rows = stoi(str1); 588 | int cols = stoi(str2); //stoi将字符串直接转化为整数,c_str将string转化为const char*,itoa将整数转化成const char*,to_string将整数转化成string 589 | 590 | const char *aa; 591 | aa = str1.c_str(); 592 | 593 | vector>vec; 594 | char ch; 595 | for (int i = 0; i < rows; i++) 596 | { 597 | vectortmp; 598 | for (int j = 0; j < cols; j++) 599 | { 600 | cin >> ch; 601 | tmp.push_back(ch); 602 | } 603 | vec.push_back(tmp); 604 | } 605 | int count = 0; 606 | for (int i = 0; i < rows; i++) 607 | { 608 | for (int j = 0; j < cols; j++) 609 | { 610 | if (vec[i][j] == 'S') 611 | { 612 | ++count; 613 | vec[i][j] = 'H'; 614 | queue>deq; 615 | deq.push({i,j}); 616 | while (!deq.empty()) 617 | { 618 | auto ij = deq.front(); 619 | deq.pop(); 620 | int row = ij.first, col = ij.second; 621 | if (row - 1 >= 0 && vec[row - 1][col] == 'S') 622 | { 623 | deq.push({row-1,col}); 624 | vec[row - 1][col] = 'H'; 625 | } 626 | if (row + 1 < rows && vec[row + 1][col] == 'S') 627 | { 628 | deq.push({row+1,col}); 629 | vec[row + 1][col] = 'H'; 630 | } 631 | if (col - 1 >= 0 && vec[row][col - 1] == 'S') 632 | { 633 | deq.push({row,col-1}); 634 | vec[row][col - 1] = 'H'; 635 | } 636 | if (col + 1 < cols && vec[row][col + 1] == 'S') 637 | { 638 | deq.push({row,col+1}); 639 | vec[row][col + 1] = 'H'; 640 | } 641 | } 642 | } 643 | } 644 | } 645 | cout << count << endl; 646 | return 0; 647 | } 648 | ``` 649 | 650 | 使用深度优先遍历搜索: 651 | 652 | ``` 653 | void DFS(vector> &vec,int row,int col) 654 | { 655 | int rows = vec.size(), cols = vec[0].size(); 656 | vec[row][col] = '0'; 657 | if(row - 1 >= 0 && vec[row-1][col] == '1') 658 | DFS(vec,row-1,col); 659 | if(row + 1 < rows && vec[row+1][col] == '1') 660 | DFS(vec,row+1,col); 661 | if(col - 1 >= 0 && vec[row][col-1] == '1') 662 | DFS(vec,row,col-1); 663 | if(col + 1 < cols && vec[row][col+1] == '1') 664 | DFS(vec,row,col+1); 665 | } 666 | 667 | int numIslands(vector>& grid) { 668 | if (grid.empty() || grid[0].empty()) 669 | return 0; 670 | int rows = grid.size(),cols = grid[0].size(); 671 | int count = 0; 672 | for(int i = 0; i < rows;i++) 673 | { 674 | for(int j = 0;j < cols;j++) 675 | { 676 | if(grid[i][j] == '1') 677 | { 678 | ++count; 679 | DFS(grid,i,j); 680 | } 681 | } 682 | } 683 | return count; 684 | }/*深度优先搜索*/ 685 | ``` 686 | 687 | #### 10、二叉树的遍历 688 | 689 | 二叉树的定义 690 | 691 | ``` 692 | typedef struct BiTNode{ 693 | int data; 694 | struct BiTNode *lchild,*rchild; 695 | }BiTNode,*BiTree; 696 | ``` 697 | 698 | #####先序遍历(DLR) 699 | 700 | 递归算法: 701 | 702 | ``` 703 | void PreOrder(BiTree T) 704 | { 705 | if(T == NULL) 706 | return; 707 | if(T != NULL) 708 | { 709 | cout<data<lchild); 711 | PreOrder(T->Rchild); 712 | } 713 | } 714 | ``` 715 | 716 | 非递归算法: 717 | 718 | ``` 719 | /*大体思路是给出一个栈,从根节点开始向左访问每个结点并把它们入栈,直到左孩子全被访问,此时弹出一个结点以和上面同样的方式访问其右孩子。直到栈空。*/ 720 | vector NPreOrder(TreeNode* root) 721 | { 722 | vector result; 723 | stack s; 724 | 725 | if (root == NULL) 726 | return result; 727 | 728 | while (root || !s.empty()) 729 | {//结束遍历的条件是root为空且栈为空 730 | while(root) 731 | {//找到最左结点,并把路径上的所有结点一一访问后入栈 732 | s.push(root); 733 | result.push_back(root->val); 734 | root = root->left; 735 | } 736 | root = s.top();//取栈顶结点 737 | s.pop();//弹出栈顶结点 738 | root = root->right;//左和中都访问了再往右访问 739 | } 740 | return result; 741 | } 742 | ``` 743 | 744 | ##### 中序遍历(LDR) 745 | 746 | 递归算法: 747 | 748 | ``` 749 | void PreOrder(BiTree T) 750 | { 751 | if(T == NULL) 752 | return; 753 | if(T != NULL) 754 | { 755 | PreOrder(T->lchild); 756 | cout<data<Rchild); 758 | } 759 | } 760 | ``` 761 | 762 | 非递归算法: 763 | 764 | ``` 765 | /*中序遍历和前序遍历大同小异,只是访问元素的时间不同,中序遍历访问是在元素出栈的时候访问,而前序遍历是在元素入栈的时候访问。 766 | */ 767 | vector NInOrder(TreeNode* root) 768 | { 769 | vector result; 770 | stack s; 771 | 772 | if (root == NULL) 773 | return result; 774 | 775 | while (root || !s.empty()) 776 | { 777 | while (root) 778 | { 779 | s.push(root); 780 | root = root->left; 781 | } 782 | root = s.top(); 783 | result.push_back(root->val); 784 | s.pop(); 785 | root = root->right; 786 | } 787 | return result; 788 | } 789 | ``` 790 | 791 | ##### 后序遍历(LRD) 792 | 793 | 递归算法: 794 | 795 | ``` 796 | void PreOrder(BiTree T) 797 | { 798 | if(T == NULL) 799 | return; 800 | if(T != NULL) 801 | { 802 | PreOrder(T->lchild); 803 | PreOrder(T->Rchild); 804 | cout<data< PostOrder(TreeNode* root) 813 | { 814 | vector result; 815 | stack s; 816 | TreeNode* cur = root; 817 | TreeNode* pre = NULL; 818 | if (root == NULL) 819 | return result; 820 | while (cur) 821 | {//走到最左孩子 822 | s.push(cur); 823 | cur = cur->left; 824 | } 825 | while (!s.empty()) 826 | { 827 | cur = s.top(); 828 | if (cur->right == NULL || cur->right == pre) 829 | {//当一个结点的右孩子为空或者被访问过的时候则表示该结点可以被访问 830 | result.push_back(cur->val); 831 | pre = cur; 832 | s.pop(); 833 | } 834 | else 835 | {//否则访问右孩子 836 | cur = cur->right; 837 | while (cur) 838 | { 839 | s.push(cur); 840 | cur = cur->left; 841 | } 842 | } 843 | } 844 | return result; 845 | } 846 | ``` 847 | 848 | #### 11、根据二叉树的前序和中序构建二叉树 849 | 850 | 输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 851 | 852 | 例如,给出 853 | 854 | 前序遍历 preorder = [3,9,20,15,7] 855 | 中序遍历 inorder = [9,3,15,20,7] 856 | 857 | 返回如下的二叉树: 858 | 859 | ``` 860 | 3 861 | / \ 862 | 9 20 863 | / \ 864 | 15 7 865 | 3 9 20 null null 15 7 866 | ``` 867 | 868 | 思路:采用递归的方法先找到头节点,左子树和右子树。再将左子树按照同样的方法遍历一次,右子树也是一样 869 | 870 | ``` 871 | TreeNode* buildTree(vector& preorder, vector& inorder) 872 | { 873 | return recursionBuild(preorder.begin(),preorder.end(),inorder.begin(),inorder.end()); } 874 | 875 | //递归分治 876 | TreeNode* recursionBuild(vector::iterator preBegin, vector::iterator preEnd,vector::iterator inBegin, vector::iterator inEnd ) 877 | { 878 | if(inEnd==inBegin) return NULL; 879 | TreeNode* cur = new TreeNode(*preBegin); 880 | auto root = find(inBegin,inEnd,*preBegin); 881 | cur->left = recursionBuild(preBegin+1,preBegin+1+(root-inBegin),inBegin,root); 882 | cur->right = recursionBuild(preBegin+1+(root-inBegin),preEnd,root+1,inEnd); 883 | return cur; 884 | } 885 | ``` 886 | 887 | #### 12、两个栈实现队列 888 | 889 | 一个栈专门入栈,存储入队列的数据,当要执行出队列的时候,(先判断另一个栈是否为空,如果不为空直接出栈)将这个栈的数据全部出栈放到另一个栈里面,则这个栈里面的出栈数据顺序和队列的出队列顺序是一致的。 890 | 891 | ``` 892 | class CQueue { 893 | public: 894 | CQueue() {} 895 | void appendTail(int value) { 896 | st1.push(value); 897 | } 898 | int deleteHead() { 899 | if(st2.empty()) 900 | { 901 | while(!st1.empty()) 902 | { 903 | st2.push(st1.top()); 904 | st1.pop(); 905 | } 906 | } 907 | if(st2.size() == 0) 908 | return -1; 909 | int ret = st2.top(); 910 | st2.pop(); 911 | return ret; 912 | } 913 | private: 914 | stackst1; 915 | stackst2; 916 | }; 917 | ``` 918 | 919 | #### 13、青蛙跳台 920 | 921 | 一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 `n` 级的台阶总共有多少种跳法。 922 | 923 | ``` 924 | int numWays(int n) { 925 | if (n == 0) 926 | return 1; 927 | if (n> 0 && n <= 2) 928 | return n; 929 | int way_max = 2; 930 | int way_min = 1; 931 | int way_N = 0; 932 | for (int i = 3; i < n+1; i++) 933 | { 934 | way_N = (way_max + way_min) % 1000000007; 935 | way_min = way_max; 936 | way_max = way_N; 937 | } 938 | return way_N; 939 | } 940 | ``` 941 | 942 | #### 14、删除链表的一个节点 943 | 944 | ``` 945 | struct ListNode { 946 | int val; 947 | ListNode *next; 948 | ListNode(int x) : val(x), next(NULL) {} 949 | }; 950 | 951 | ListNode* deleteNode(ListNode* head, int val) { 952 | ListNode *temp = new ListNode(0); 953 | ListNode *prev = new ListNode(); 954 | temp->next = head; 955 | prev = temp; 956 | while(prev && prev->next) 957 | { 958 | if(prev->next->val == val) 959 | prev->next = prev->next->next; 960 | else 961 | prev = prev->next; 962 | } 963 | return temp->next; 964 | } 965 | ``` 966 | 967 | #### 15、二进制数中的1的个数 968 | 969 | ``` 970 | int hammingWeight(uint32_t n) { 971 | int count = 0; 972 | while(n != 0) 973 | { 974 | count++; 975 | n = n & (n-1); 976 | } 977 | return count; 978 | } 979 | //1、思路是n与n-1与运算,那么就会将一个1变为0,不断进行.有多少个1就循环多少次 980 | //2、将n和1与运算,如果为1个数加1,最后再将n右移一位 981 | ``` 982 | 983 | 984 | 985 | ####16、调整数组顺序使奇数位于偶数前面 986 | 987 | ``` 988 | vector exchange(vector& nums) { 989 | int left = 0,right = nums.size()-1; 990 | while(left < right) 991 | { 992 | if((nums[left] & 1) == 1) 993 | { 994 | ++left; 995 | continue; 996 | } 997 | if((nums[right] & 1) == 0) 998 | { 999 | --right; 1000 | continue; 1001 | } 1002 | swap(nums[left],nums[right]); 1003 | } 1004 | return nums; 1005 | } 1006 | 1007 | //定义两个指针,一个从前开始,一个从后开始。分别和1相与,和0相与,奇数与1相与结果为1,偶数与0相遇结果为0,最后直接交换不是奇数,不是偶数的两个数。 1008 | ``` 1009 | 1010 | ####17、链表中倒数第k个节点 1011 | 1012 | 快慢指针法。基本思路: 1013 | 倒数第 k 个节点,其实就是正数第 n - k + 1 个节点。但是我们不知道 n 的值,所以没办法用一个指针来完成任务。 1014 | 定义一快一慢两个指针,初始都指向 head。然后快指针先从 head 处往前走 k - 1 步,慢指针全程不动。这样快指针走完 k - 1 步之后,它们之间的距离是 k - 1。 1015 | 然后快慢指针同时开始往前走,当快指针到达链表最后一个元素的时候,二者停下。此时快指针加上之前的一共走了 n 步,而慢指针与快指针之间的距离始终为 k - 1,所以慢指针走了 n - (k - 1)步,即 n - k + 1步,刚好是倒数第 k 个节点的位置。 1016 | 1017 | ``` 1018 | struct ListNode { 1019 | int val; 1020 | ListNode *next; 1021 | ListNode(int x) : val(x), next(NULL) {} 1022 | }; 1023 | class Solution { 1024 | public: 1025 | ListNode* getKthFromEnd(ListNode* head, int k) { 1026 | if(!head || k == 0) return nullptr; // 鲁棒性1、2 1027 | ListNode *fast = head, *slow = head; 1028 | for(int i=1; i<=k-1; ++i) 1029 | { 1030 | if(fast -> next) 1031 | fast = fast -> next; // 鲁棒性3 1032 | else return nullptr; 1033 | } 1034 | while(fast -> next) 1035 | { 1036 | fast = fast -> next; 1037 | slow = slow -> next; 1038 | } 1039 | return slow; 1040 | } 1041 | }; 1042 | ``` 1043 | 1044 | #### 18、反转链表 1045 | 1046 | ``` 1047 | struct ListNode { 1048 | int val; 1049 | ListNode *next; 1050 | ListNode(int x) : val(x), next(NULL) {} 1051 | }; 1052 | 1053 | class Solution { 1054 | public: 1055 | ListNode* reverseList(ListNode* head) { 1056 | ListNode* cur = NULL, *pre = head; 1057 | while (pre != NULL) { 1058 | ListNode* t = pre->next; 1059 | pre->next = cur; 1060 | cur = pre; 1061 | pre = t; 1062 | } 1063 | return cur; 1064 | } 1065 | }; 1066 | ``` 1067 | 1068 | ####19、合并两个排序的链表 1069 | 1070 | ``` 1071 | struct ListNode { 1072 | int val; 1073 | ListNode *next; 1074 | ListNode(int x) : val(x), next(NULL) {} 1075 | }; 1076 | class Solution { 1077 | public: 1078 | ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { 1079 | if(!l1) return l2; 1080 | if(!l2) return l1; 1081 | ListNode *dummy = new ListNode(INT_MAX); 1082 | ListNode *res = dummy; 1083 | while(l1 && l2) 1084 | { 1085 | if(l1 -> val < l2 -> val) 1086 | { 1087 | res -> next = l1; 1088 | l1 = l1 -> next; 1089 | } 1090 | else 1091 | { 1092 | res -> next = l2; 1093 | l2 = l2 -> next; 1094 | } 1095 | res = res -> next; 1096 | } 1097 | res -> next = l1 ? l1 : l2; // 当有一条链表遍历到头,while循环退出之后,pos->next应指向尚未遍历完的那条链表 1098 | return dummy -> next; 1099 | } 1100 | }; 1101 | ``` 1102 | 1103 | ####20、连续子数组的最大和 1104 | 1105 | 输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。 1106 | 1107 | 要求时间复杂度为O(n)。 1108 | 1109 | ``` 1110 | int maxSubArray(vector& nums) { 1111 | int n = nums.size(); 1112 | for(int i=1;i> levelOrder(TreeNode* root) { 1133 | vector>res; 1134 | if(root==NULL)return res; 1135 | queueq; 1136 | q.push(root); 1137 | while(!q.empty()){ 1138 | vectorr; 1139 | int l=q.size(); 1140 | for(int i=0;ival); 1143 | q.pop(); 1144 | if(t->left)q.push(t->left); 1145 | if(t->right)q.push(t->right); 1146 | } 1147 | res.push_back(r); 1148 | } 1149 | return res; 1150 | } 1151 | }; 1152 | 1153 | ``` 1154 | 1155 | DFS 1156 | 1157 | ``` 1158 | struct TreeNode { 1159 | int val; 1160 | TreeNode *left; 1161 | TreeNode *right; 1162 | TreeNode(int x) : val(x), left(NULL), right(NULL) {} 1163 | }; 1164 | 1165 | class Solution { 1166 | public: 1167 | vector> re; 1168 | void dfs(TreeNode* root,int depth){ 1169 | if (root == NULL) return; 1170 | if (depth > re.size()){ 1171 | vector temp; 1172 | temp.push_back(root->val); 1173 | re.push_back(temp); 1174 | } 1175 | else{ 1176 | re[depth-1].push_back(root->val); 1177 | } 1178 | dfs(root->left,depth + 1); 1179 | dfs(root->right,depth + 1); 1180 | return; 1181 | } 1182 | 1183 | vector> levelOrder(TreeNode* root) { 1184 | dfs(root,1); 1185 | return re; 1186 | } 1187 | }; 1188 | 1189 | ``` 1190 | 1191 | ####22、最小的K个数 1192 | 1193 | 我们用一个大根堆实时维护数组的前 kk 小值。首先将前 kk 个数插入大根堆中,随后从第 k+1k+1 个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后将大根堆里的数存入数组返回即可。在下面的代码中,由于 C++ 语言中的堆(即优先队列)为大根堆,我们可以这么做。而 Python 语言中的对为小根堆,因此我们要对数组中所有的数取其相反数,才能使用小根堆维护前 kk 小值。 1194 | 1195 | ``` 1196 | class Solution { 1197 | public: 1198 | vector getLeastNumbers(vector& arr, int k) { 1199 | vectorvec(k, 0); 1200 | if (k == 0) return vec; // 排除 0 的情况 1201 | priority_queueQ; 1202 | for (int i = 0; i < k; ++i) Q.push(arr[i]); 1203 | for (int i = k; i < (int)arr.size(); ++i) { 1204 | if (Q.top() > arr[i]) { 1205 | Q.pop(); 1206 | Q.push(arr[i]); 1207 | } 1208 | } 1209 | for (int i = 0; i < k; ++i) { 1210 | vec[i] = Q.top(); 1211 | Q.pop(); 1212 | } 1213 | return vec; 1214 | } 1215 | }; 1216 | 1217 | ``` 1218 | 1219 | ####23、两个链表的第一个公共节点 1220 | 1221 | 通过链表拼接消除差值,如果一个链表先走到尾部,则指向另一个链表。 1222 | 1223 | ``` 1224 | class Solution { 1225 | public: 1226 | ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { 1227 | if (!headA || !headB){ 1228 | return nullptr; 1229 | } 1230 | ListNode *pA = headA, *pB = headB; 1231 | while (pA != pB) { 1232 | pA = pA == nullptr ? headB : pA->next; 1233 | pB = pB == nullptr ? headA : pB->next; 1234 | } 1235 | return pA; 1236 | } 1237 | } 1238 | ``` 1239 | 1240 | ####24、在排序数组中查找数字 I 1241 | 1242 | **利用两次二分法分别找到重复相同数字的左边界x和右边界y** 1243 | 1244 | ``` 1245 | class Solution { 1246 | public: 1247 | int search(vector& nums, int target) { 1248 | if(nums.empty())return 0; 1249 | int n=nums.size(); 1250 | int left=0,right=n-1,mid; 1251 | int x; 1252 | int y; 1253 | while(left=target)right=mid; 1256 | else left=mid+1; 1257 | } 1258 | if(nums[left]!=target)return 0; 1259 | x=left; 1260 | right=n; 1261 | while(leftssthresh时,改用拥塞避免算法。** 158 | **当cwnd=ssthresh时,慢开始与拥塞避免算法任意** 159 | 160 | - **拥塞避免算法** 161 | 162 | 拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口按线性规律缓慢增长。 163 | 164 | 无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。 165 | 166 | **无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半,并执行慢开始算法,所以当网络频繁出现拥塞时,ssthresh下降的很快,以大大减少注入到网络中的分组数。** 167 | 168 | - **快重传算法** 169 | 170 | 快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。 171 | 172 | - **快恢复算法** 173 | 174 | 快重传配合使用的还有快恢复算法,有以下两个要点: 175 | 176 | 当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半(为了预防网络发生拥塞)。但是接下去并不执行慢开始算法 177 | 考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh减半后的值,然后执行拥塞避免算法,使cwnd缓慢增大。 178 | 179 | ####11、从输入网址到获得页面的过程 180 | 181 | - **1、域名解析,其实就是根据用户输入的网址去寻找它对应的IP地址,比如输入www.baidu.com的网址就会经历以下过程** 182 | 183 | 1.先从浏览器缓存里找IP,因为浏览器会缓存DNS记录一段时间 184 | 185 | 2.如没找到,再从Hosts文件查找是否有该域名和对应IP 186 | 187 | 3.如没找到,再从路由器缓存找 188 | 189 | 4.如没好到,再从DNS缓存查找 190 | 191 | 5.如果都没找到,浏览器域名服务器向根域名服务器(baidu.com)查找域名对应IP,还没找到就把请求转发到下一级,直到找到IP 192 | 193 | - **2、建立TCP连接 (这里使用五层协议更详细的描述如何建立这个TCP链接的)** 194 | 195 | 先是客户端发起请求过程: 196 | 1. 使用应用层发起HTTP请求(这个可以根据你本身输入的url访问时,用的什么协议就发起对应协议去进行请求) 197 | 2. 然后是传输层的TCP协议为传输报文提供可靠的字节流服务,这里也就使用了TCP三次握手 198 | 3. 网络层是把TCP分割好的各种数据包传送给接收方。而要保证确实能传到接收方还需要接收方的MAC地址,也就是物理地址 199 | 4. 然后才是链路层将数据发送到数据链路层传输。至此请求报文已发出,客户端发送请求的阶段结束 200 | 201 | **然后是服务端接受请求处理阶段:** 202 | 原路进行处理:**链路层—>网络层—>传输层—>应用层**然后响应客户端发送报文。 203 | 204 | - **3、根据SpringMVC后台业务返回数据,并把数据填充到HTML页面上,然后返回给浏览器** 205 | 206 | - **4、浏览器进行处理** 207 | 服务器通过后台语言程序处理,找到数据返回给浏览器,HTML字符串被浏览器接受后被一句句读取解析,解析到link标签后重新发送请求获取css,解析到sript标签后发送请求获取js,并执行代码 208 | 209 | - **5、绘制网页** 210 | 然后浏览器会进行渲染,浏览器根据HTML和CSS计算得到渲染树,绘制到屏幕上,js会被执行 211 | 212 | ####12、OSI网络体系结构与TCP/IP协议模型 213 | 214 | 我们知道TCP/IP与OSI最大的不同在于:OSI是一个理论上的网络通信模型,而TCP/IP则是实际上的网络通信标准。但是,它们的初衷是一样的,都是为了使得两台计算机能够像两个知心朋友那样能够互相准确理解对方的意思并做出优雅的回应。 215 | 216 | ![undefined](http://ww1.sinaimg.cn/large/0062FU9hly1ghjnqo9eiyj30g70agab7.jpg) 217 | 218 | - 物理层 219 | 220 | 参考模型的最低层,也是OSI模型的第一层,实现了相邻计算机节点之间比特流的透明传送,并尽可能地屏蔽掉具体传输介质和物理设备的差异,使其上层(数据链路层)不必关心网络的具体传输介质。 221 | 222 | - 数据链路层(data link layer) 223 | 224 | 接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层。这一层在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路,即提供可靠的通过物理介质传输数据的方法。 225 | 226 | - 网络层 227 | 228 | 将网络地址翻译成对应的物理地址,并通过路由选择算法为分组通过通信子网选择最适当的路径。 229 | 230 | - 传输层(transport layer) 231 | 232 | 在源端与目的端之间提供可靠的透明数据传输,使上层服务用户不必关系通信子网的实现细节。在协议栈中,传输层位于网络层之上,传输层协议为不同主机上运行的进程提供逻辑通信,而网络层协议为不同主机提供逻辑通信。 233 | 234 | - 会话层(Session Layer) 235 | 236 | 会话层是OSI模型的第五层,是用户应用程序和网络之间的接口,负责在网络中的两节点之间建立、维持和终止通信。 237 | 238 | - 表示层(Presentation Layer):数据的编码,压缩和解压缩,数据的加密和解密 239 | 240 | 表示层是OSI模型的第六层,它对来自应用层的命令和数据进行解释,以确保一个系统的应用层所发送的信息 241 | 242 | - 应用层(Application layer):为用户的应用进程提供网络通信服务 243 | 244 | ####13、TCP和UDP分别对应的常见应用层协议 245 | 246 | 1). TCP对应的应用层协议 247 | 248 | - FTP:定义了文件传输协议,使用21端口。常说某某计算机开了FTP服务便是启动了文件传输服务。下载文件,上传主页,都要用到FTP服务。 249 | - Telnet:它是一种用于远程登陆的端口,用户可以以自己的身份远程连接到计算机上,通过这种端口可以提供一种基于DOS模式下的通信服务。如以前的BBS是-纯字符界面的,支持BBS的服务器将23端口打开,对外提供服务。 250 | - SMTP:定义了简单邮件传送协议,现在很多邮件服务器都用的是这个协议,用于发送邮件。如常见的免费邮件服务中用的就是这个邮件服务端口,所以在电子邮件设置-中常看到有这么SMTP端口设置这个栏,服务器开放的是25号端口。 251 | - POP3:它是和SMTP对应,POP3用于接收邮件。通常情况下,POP3协议所用的是110端口。也是说,只要你有相应的使用POP3协议的程序(例如Fo-xmail或Outlook),就可以不以Web方式登陆进邮箱界面,直接用邮件程序就可以收到邮件(如是163邮箱就没有必要先进入网易网站,再进入自己的邮-箱来收信)。 252 | - HTTP:从Web服务器传输超文本到本地浏览器的传送协议。 253 | 254 | 2). UDP对应的应用层协议 255 | 256 | - DNS:用于域名解析服务,将域名地址转换为IP地址。DNS用的是53号端口。 257 | - SNMP:简单网络管理协议,使用161号端口,是用来管理网络设备的。由于网络设备很多,无连接的服务就体现出其优势。 258 | - TFTP(Trival File Transfer Protocal):简单文件传输协议,该协议在熟知端口69上使用UDP服务。 259 | 260 | ####14、 常见状态码及原因短语 261 | 262 |   HTTP请求结构: 请求方式 + 请求URI + 协议及其版本 263 |   HTTP响应结构: 状态码 + 原因短语 + 协议及其版本 264 | 265 | ------ 266 | 267 | - **1xx(临时响应)表示临时响应并需要请求者继续执行操作的状态代码。代码 说明** 268 | 269 | 100 (继续) 请求者应当继续提出请求。 服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。 270 | 271 | 101 (切换协议) 请求者已要求服务器切换协议,服务器已确认并准备切换。 272 | 273 | ------ 274 | 275 | - **2xx (成功)表示成功处理了请求的状态代码。代码 说明** 276 | 277 | 200 (成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。 278 | 279 | 201 (已创建) 请求成功并且服务器创建了新的资源。 280 | 281 | 202 (已接受) 服务器已接受请求,但尚未处理。 282 | 283 | 203 (非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源。 284 | 285 | 204 (无内容) 服务器成功处理了请求,但没有返回任何内容。 286 | 287 | 205 (重置内容) 服务器成功处理了请求,但没有返回任何内容。 288 | 289 | 206 (部分内容) 服务器成功处理了部分 GET 请求。 290 | 291 | ------ 292 | 293 | - **3xx (重定向) 表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。代码 说明** 294 | 295 | 300 (多种选择) 针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。 296 | 297 | 301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。 298 | 299 | 302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。 300 | 301 | 303 (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。 302 | 303 | 304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。 304 | 305 | 305 (使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。 306 | 307 | 307 (临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。 308 | 309 | ------ 310 | 311 | - **4xx(请求错误) 这些状态代码表示请求可能出错,妨碍了服务器的处理。代码 说明** 312 | 313 | 400 (错误请求) 服务器不理解请求的语法。 314 | 315 | 401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。 316 | 317 | 403 (禁止) 服务器拒绝请求。 318 | 319 | 404 (未找到) 服务器找不到请求的网页。 320 | 321 | 405 (方法禁用) 禁用请求中指定的方法。 322 | 323 | 406 (不接受) 无法使用请求的内容特性响应请求的网页。 324 | 325 | 407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。 326 | 327 | 408 (请求超时) 服务器等候请求时发生超时。 328 | 329 | 409 (冲突) 服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。 330 | 331 | 410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应。 332 | 333 | 411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。 334 | 335 | 412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。 336 | 337 | 413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。 338 | 339 | 414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。 340 | 341 | 415 (不支持的媒体类型) 请求的格式不受请求页面的支持。 342 | 343 | 416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。 344 | 345 | 417 (未满足期望值) 服务器未满足"期望"请求标头字段的要求。 346 | 347 | ------ 348 | 349 | - **5xx(服务器错误)这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。代码 说明** 350 | 351 | 500 (服务器内部错误) 服务器遇到错误,无法完成请求。 352 | 353 | 501 (尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。 354 | 355 | 502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。 356 | 357 | 503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。 358 | 359 | 504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。 360 | 361 | 505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本。 362 | 363 | 364 | 365 | #####简单介绍状态码: 366 | 367 | 1开头的表示临时响应并需要请求者继续执行操作的状态代码 368 | 369 | 比如101是请求者已要求服务器切换协议,服务器已经确认并准备切换 370 | 371 | 2开头的表示成功处理了请求的状态码 372 | 373 | 比如201 (已创建) 请求成功并且服务器创建了新的资源。 374 | 375 | 3开头的表示重定向 376 | 377 | 304 (未修改) 自从上次请求后,请求的网页未修改过。 378 | 379 | 4开头的表示请求可能出错,妨碍了服务器的处理 380 | 381 | 404服务器找不到请求的网页 382 | 383 | 5开头的表示服务器错误,服务器在处理请求时发生内部错误。 384 | 385 | 500服务器内部遇到错误,无法完成请求 386 | 387 | ####15、TCP/IP协议: 388 | 389 | TCP/IP协议 390 | 链路层:对0和1进行分组,定义数据帧,确认主机的物理地址,传输数据; 391 | 392 | 网络层:定义IP地址,确认主机所在的网络位置,并通过IP进行MAC寻址,对外网数据包进行路由转发; 393 | 394 | 传输层:定义端口,确认主机上应用程序的身份,并将数据包交给对应的应用程序; 395 | 396 | 应用层:定义数据格式,并按照对应的格式解读数据。 397 | 398 | 通过http发起一个请求时,应用层、传输层、网络层和链路层的相关协议依次对该请求进行包装并携带对应的首部,最终在链路层生成以太网数据包,以太网数据包通过物理介质传输给对方主机,对方接收到数据包以后,然后再一层一层采用对应的协议进行拆包,最后把应用层数据交给应用程序处理。 -------------------------------------------------------------------------------- /项目.md: -------------------------------------------------------------------------------- 1 | ### 项目 2 | 3 | #### 恶意软件检测系统研究 4 | 5 | 这个项目是一个学术研究性质的一个项目,主要是研究Android下的恶意软件的检测。使用genymotion安卓模拟器,动态运行apk文件;使用ebpf工具动态监控运行的apk文件,从而获得恶意软件内核底层的相关行为特征;使用特征选择方法,进行数据处理,消除冗余特征;再使用机器学习分类算法进行分类训练。形成一个区别恶意软件和正常软件的分类器,到达检测恶意软件的目的,根据定义的相关指标得出检测效果。 6 | 7 | 该项目主要是研究Android下的恶意软件的检测,判断apk文件是否带有病毒。使用genymotion安卓模拟器,动态运行apk文件;使用ebpf工具动态监控运行的apk文件,从而获得恶意软件内核底层的相关行为特征;使用特征选择方法,进行数据处理,消除冗余特征;再使用机器学习分类算法进行分类训练。形成一个区别恶意软件和正常软件的分类器,达到 检测恶意软件的目的,根据定义的相关指标得出检测效果。 8 | 9 | **主要负责:**行为特征数据处理和分类训练检测。直接使用特征集作分类检测,检测效率和准确率稍低,使用特征选择方法卡方检验+主成分分析法对数据集进行处理,降低特征集维度,消除冗余特征。再进行分类测试。 10 | 11 | 12 | 13 | 这个项目是一个学术研究性质的一个项目,主要是研究Android下的恶意软件的检测,判断一个APK文件是否是恶意软件还是正常软件。 14 | 15 | 项目主要分为三个大模块:第一个是特征提取 16 | 17 | 首先使用genymotion安卓模拟器运行apk文件,然后通过监控apk文件在内核下的行为,获取相应的行为特征。由于内核中的一些数据要拿到,比如内核函数的调用、CPU利用率计算的各个指标,网路流量等特征,可能需要写一个内核模块,插入到内核中,通过运行内核模块拿到相关数据。这样比较麻烦,耗时。这个项目采用了一种新技术ebpf技术。eBPF支持在用户态将C语言编写的一小段“内核代码”注入到内核中运行,注入时要先用llvm编译得到使用BPF指令集的elf文件,然后从elf文件中解析出可以注入内核的部分,最后用bpf_load_program方法完成注入。 用户态程序和注入到内核中的程序通过共用一个位于内核中`map`实现通信。为了防止注入的代码导致内核崩溃,eBPF会对注入的代码进行严格检查,拒绝不合格的代码的注入。BCC是一个python库,实现了map创建、代码编译、解析、注入等操作,使开发人员只需聚焦于用C语言开发要注入的内核代码。通过使用BCC工具获取相关特征。 18 | 19 | 第二个模块是:特征选择 20 | 21 | 这个模块主要是对所拿到的特征数据做个特征选择,因为拿到的数据过多,而且有些数据特征对分类没有作用,反而会影响分类训练的效果,所以要对数据集及逆行处理。主要是采用主成分分析和卡方检验对特征集进行数据处理,消除冗余特征,降低特征集的维度,增加特征集的可靠性和代表性。 22 | 23 | 第三个模块是:分类检测 24 | 25 | 将处理完的数据集导入weka工具中,采用不同的分类算法对数据集进行分类训练,这里采用是折交叉验证的方式来进行测试,就是将数据集分为10份,9份作为训练集,1份作为测试集,不断轮询。进行测试,通过分类指标来判断算法的检测效果,最终得出分类效果较好的算法,实现恶意软件的检测。 26 | 27 | 28 | 29 | 30 | 31 | eBPF支持在用户态将C语言编写的一小段“内核代码”注入到内核中运行,注入时要先用llvm编译得到使用BPF指令集的elf文件,然后从elf文件中解析出可以注入内核的部分,最后用bpf_load_program方法完成注入。 用户态程序和注入到内核中的程序通过共用一个位于内核中`map`实现通信。为了防止注入的代码导致内核崩溃,eBPF会对注入的代码进行严格检查,拒绝不合格的代码的注入。 32 | 33 | BCC是一个python库,实现了map创建、代码编译、解析、注入等操作,使开发人员只需聚焦于用C语言开发要注入的内核代码。 34 | 35 | **应用:**eBPF 程序可以被设计用于各种各样的情形下,其分为两个应用领域。其中一个应用领域是内核跟踪和事件监控。BPF 程序可以被附着到静态 tracepoints 上或者动态探针(kprobe) 上,而且它与其它跟踪模式相比,有很多的优点。 36 | 37 | 另外一个应用领域是网络编程。除了套接字过滤器外,eBPF 程序还可以附加到 tc(Linux 流量控制工具)的入站或者出站接口上,以一种很高效的方式去执行各种包处理任务。 38 | 39 | **安全性:**eBPF 不需要陷入内核便可通过编写函数从用户态获取内核态的相关数据,减少了陷入内核的消耗。eBPF 也是加强了在和用户空间交互的安全性。在内核中的检测器会拒绝加载引用了无效指针的字节码或者是以达到最大栈大小限制。循环也是不允许的(除非在编译时就知道是有常数上线的循环),字节码只能够调用一小部分指定的 eBPF 帮助函数。 40 | 41 | 使用 eBPF 提取内核更底层的特征,如内核函数调用、CPU 利用率等特征,网络流量、文件访问等特征 42 | 用机器学习算法进行分类检测。提高检测的效率,准确率,减少系统的资源消耗。 43 | 44 | 2、对通过ebpf拿到的一些数据做数据处理,拿到的数据中可能有些是对分类没有贡献,反而影响分类结果的特征,使用特征选择对拿到的数据,进行数据集降维,消除相关特征冗余。保证数据集的有效性和可靠性。 45 | 46 | 3、使用四种机器学习分类算法对数据集进行训练,定义评价指标,准确率、效率、误报率等,根据最终的分类结果,计算出评价指标。对比各种分类算法,得出最适合恶意软件检测的分类算法,最终进行分类检测。 47 | 48 | 49 | 50 | ####Linux下轻量级web服务器 51 | 52 | 该项目是Linux下C++轻量级Web服务器的实现。使用线程池 +非阻塞socket + epoll+事件处理的并发模型;使用状态机解析HTTP请求报文,⽀持解析GET和POST请求;访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件;实现同步/异步日志系统,记录服务器运行状态。 53 | 54 | **主要负责:**使用线程池 +非阻塞socket + epoll+事件处理的并发模型,采用单例模式和线程池,数据库连接池的方式,完成访问服务器数据库和日志系统的实现。 55 | 56 | 57 | 58 | 这个项目主要是疫情期间,应学校的课程要求,和同学做的一个项目。而且我们刚好也想提升下自己在C++方面的一个编程经验,增加些C++的项目经验,了解C++的项目流程。 59 | 60 | 项目是linux下C++轻量级web服务器的实现。项目的主要功能就是实现一个web端的多用户注册、登录,然后可以请求访问服务器上的图片的视频文件,实现并发处理。服务器可以记录各个用户登录,注册和访问的一些日志信息,并且使用压测软件webbeach,测试该服务器的负载性能。 61 | 62 | 由于项目中要实现多用户的并发访问,当进行并行的任务作业时,线程的建立与销毁的开销是阻碍性能的关键,因此项目中主要采用线程池,维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。避免线程的的创建和销毁带来的性能开销,避免大量的线程间因相互抢占资源导致的阻塞现象。采用非阻塞socket的和epoll并发模式,实现多用户的并发访问。非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,**不要将进程睡眠**,而是返回一个错误。使用epoll I/O复用,采用边缘触发模式,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,只有活跃可用的FD才会调用callback函数,即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关。 63 | 64 | 数据库连接使用单例模式,保证有唯一的连接对象。使用STL list容器实现数据库连接池,连接池的大小使用静态大小,使用互斥锁实现线程安全,在每次进行线程池操作之前进行加锁,操作完成之后解锁。同步/异步日志系统主要涉及了两个模块,一个是日志模块,一个是阻塞队列模块,其中加入阻塞队列模块主要是解决异步写入日志做准备.这里实现是使用的队列使**STL中的queue**为底层,使用单例模式确保全局只有唯一的一个对象,保证使用相同的队列。 65 | 66 | 67 | 68 | 69 | 70 | **http解析:** 71 | 72 | 根据状态转移,通过主从状态机封装了http连接类。其中,主状态机在内部调用从状态机,从状态机将处理状态和数据传给主状态机。 73 | 74 | 客户端发出http连接请求,从状态机读取数据,更新自身状态和接收数据,传给主状态机 75 | 76 | 主状态机根据从状态机状态,更新自身状态,决定响应请求还是继续读取 77 | 78 | 79 | 80 | 81 | 82 | #####线程池 83 | 84 | **线程池:** 当进行并行的任务作业操作时,线程的建立与销毁的开销是,阻碍性能进步的关键,因此线程池,由此产生。使用多个线程,无限制循环等待队列,进行计算和操作。帮助快速降低和减少性能损耗。 85 | 86 | 线程池的组成 87 | 88 | 1. 线程池管理器:初始化和创建线程,启动和停止线程,调配任务;管理线程池 89 | 2. 工作线程:线程池中等待并执行分配的任务 90 | 3. 任务接口:添加任务的接口,以提供工作线程调度任务的执行。 91 | 4. 任务队列:用于存放没有处理的任务,提供一种缓冲机制,同时具有调度功能,高优先级的任务放在队列前面 92 | 93 | 线程池的优点 94 | 95 | 1)避免线程的创建和销毁带来的性能开销。 96 | 2)避免大量的线程间因互相抢占系统资源导致的阻塞现象。 97 | 3}能够对线程进行简单的管理并提供定时执行、间隔执行等功能。 98 | 99 | 100 | 101 | 阻塞I/O 102 | 103 | 使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,使用阻塞模式来开发网络程序比较合适。 104 | 105 | 阻塞模式套接字的不足表现为,在大量建立好的套接字线程之间进行通信时比较困难。当使用“生产者-消费者”模型开发网络程序时,为每个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。其最大的缺点是当希望同时处理大量套接字时,将无从下手,其扩展性很差. 106 | 107 | 阻塞模式给网络编程带来了一个很大的问题,如在调用 send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这给多客户机、多业务逻辑的网络编程带来了挑战。 108 | 109 | 多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求,但其实并不尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。 110 | 111 | 由此可能会考虑使用“**线程池**”或“**连接池**”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如apache,mysql数据库等。 112 | 113 | 非阻塞I/O 114 | 115 | 非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,**不要将进程睡眠**,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。 116 | 117 | 118 | 119 | **select:** 120 | 121 | select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是: 122 | 123 | - 单个进程可监视的fd数量被限制,即能监听端口的大小有限。 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048. 124 | 125 | - 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。 126 | 127 | - 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大 128 | 129 | **poll:** 130 | 131 | poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。 132 | 133 | 它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点: 134 | 135 | - 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。 136 | - poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。 137 | 138 | **epoll:** 139 | 140 | epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知 141 | 142 | epoll的优点: 143 | 144 | - 没有最大并发连接的限制,**能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);** 145 | - **效率提升**,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数; 146 | 即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。 147 | - 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。 148 | 149 | **select、poll、epoll 区别总结:** 150 | 151 | 1、支持一个进程所能打开的最大连接数 152 | 153 | | select | 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。 | 154 | | ------ | ------------------------------------------------------------ | 155 | | poll | poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的 | 156 | | epoll | 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接 | 157 | 158 | 2、FD剧增后带来的IO效率问题 159 | 160 | | select | 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 | 161 | | ------ | ------------------------------------------------------------ | 162 | | poll | 同上 | 163 | | epoll | 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。 | 164 | 165 | 3、 消息传递方式 166 | 167 | | select | 内核需要将消息传递到用户空间,都需要内核拷贝动作 | 168 | | ------ | ------------------------------------------------ | 169 | | poll | 同上 | 170 | | epoll | epoll通过内核和用户空间共享一块内存来实现的。 | 171 | 172 | **总结:** 173 | 174 | 综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。 175 | 176 | - 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。 177 | 178 | - select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善 179 | 180 | **http解析:** 181 | 182 | 根据状态转移,通过主从状态机封装了http连接类。其中,主状态机在内部调用从状态机,从状态机将处理状态和数据传给主状态机 183 | 184 | 客户端发出http连接请求 185 | 186 | 从状态机读取数据,更新自身状态和接收数据,传给主状态机 187 | 188 | 主状态机根据从状态机状态,更新自身状态,决定响应请求还是继续读取 189 | 190 | 191 | 192 | **服务器数据库模块:**数据库连接使用单例模式,保证有唯一的连接对象。使用STL list容器实现数据库连接池,连接池的大小使用静态大小,使用互斥锁实现线程安全,在每次进行线程池操作之前进行加锁,操作完成之后解锁。web端的一个用户注册和用户登录,以及请求服务器中的图片及视频文件,http都采用post方式。 193 | 194 | **连接池** 195 | 196 | 对于一个简单的数据库应用,由于对于数据库的访问不是很频繁。这时可以简单地在需要访问数据库时,就新创建一个连接,用完后就关闭它,这样做也不会带来什么明显的性能上的开销。但是对于一个复杂的数据库应用。频繁的建立、关闭连接,会极大的减低系统的性能,因为对于连接的使用成了系统性能的瓶颈。 197 | 198 | 怎样解决:连接复用。通过建立一个数据库连接池以及一套连接使用管理策略,使得一个数据库连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。就是共享连接资源,数据库连接池的基本原理是在内部对象池中维护一定数量的数据库连接,并对外暴露数据库连接获取和返回方法。 199 | 200 | 外部使用者可通过getConnection 方法获取连接,使用完毕后再通过releaseConnection 方法将连接返回,注意此时连接并没有关闭,而是由连接池管理器回收,并为下一次使用做好准备。 201 | 202 | ##### 数据库连接池技术带来的优势: 203 | 204 | - 资源重用 205 | 206 | 由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增进了系统运行环境的平稳性(减少内存碎片以及数据库临时进程/线程的数量)。 207 | 208 | - 更快的系统响应速度 209 | 210 | 数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。 211 | 212 | - 新的资源分配手段 213 | 214 | 对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接池技术,几年钱也许还是个新鲜话题,对于目前的业务系统而言,如果设计中还没有考虑到连接池的应用,那么…….快在设计文档中加上这部分的内容吧。某一应用最大可用数据库连接数的限制,避免某一应用独占所有数据库资源。 215 | 216 | - 统一的连接管理,避免数据库连接泄漏 217 | 218 | 在较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用连接。从而避免了常规数据库连接操作中可能出现的资源泄漏。 219 | 220 | 221 | 222 | **日志模块:**同步/异步日志系统主要涉及了两个模块,一个是日志模块,一个是阻塞队列模块,其中加入阻塞队列模块主要是解决异步写入日志做准备.这里实现是使用的队列使**STL中的queue**为底层,使用单例模式确保全局只有唯一的一个对象,保证使用相同的队列。 223 | 224 | 225 | 226 | #### 数学建模大赛 227 | 228 | 利用现有的统计数据建立简化的气候模型和极端天气模型。建立的模型区别于复杂的专业气候模型,有利于非专业人士理解和认识全球气候变化的态势,解释极端天气现象的发生,寻找、求证影响气候变化的因素,从而增强人们气候变化的意识。 229 | 230 | 问题一:挖掘加拿大地区温度的时空变化趋势、探索海洋表面温度变化规律 231 | 232 | 对加拿大地区整体气候进行分析,将整个加拿大的城市按地区与气候分为七个类别,并从每个类型的地区气候中挑选一个城市作为样本,探索温度变化规律。 233 | 234 | 从美国国家海洋和大气管理局(NOAA)官网筛选下载海洋表面温度(SST)历史数据,处理数据后对海洋表面温度变化建立模型,探索温度变化的规律。 235 | 236 | 使用线性倾向估计进行建模分析,得出加拿大地区的温度随着时间的推移,在显著性的上升。全球海洋表面温度也在在显著上升。 237 | 238 | 问题二:建立一个刻画气候变化的模型对未来25年的气候变化进行预测,该模型考虑地球的吸热、散热以及海洋的温度变化等要素。 239 | 240 | 根据分析气候变化原因,我们总结了四个影响气候变化的因素:太阳总辐照度、地球长波辐射、海洋表面温度和大气中温室气体含量。确定这四个维度,收集前30年的历史数据,使用岭估计模型和ARMA模型预测未来25年的气候变化。 241 | 242 | 问题三:“极寒天气”是某地的天气现象,这种极端气象的出现,与气候变化有无关系?请建立相应的模型,并利用题目所提供的数据以及你能收集的数据说明:全球变暖和局地极寒现象的出现之间是否矛盾? 243 | 244 | 使用显著性检验,定性的说局部地区的极寒天气与当地的气候类型有很大关系。各地地理因素差异较大,所以各地气候呈现出各自的变化特征,单单某些地区的极寒天气出现,并不能否认全球平均气温上升的趋势。 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | ####恶意软件检测系统研究 253 | 254 | 这个项目是一个学术研究性质的一个项目,主要是研究Android下的恶意软件的检测,判断一个APK文件是否是恶意软件还是正常软件。 255 | 256 | 项目主要分为三个大模块:第一个是特征提取 257 | 258 | 首先使用genymotion安卓模拟器运行apk文件,然后通过监控apk文件在内核下的行为,获取相应的行为特征。由于内核中的一些数据要拿到,比如内核函数的调用、CPU利用率计算的各个指标,网路流量等特征,可能需要写一个内核模块,插入到内核中,通过运行内核模块拿到相关数据。这样比较麻烦,耗时。这个项目采用了一种新技术ebpf技术。eBPF支持在用户态将C语言编写的一小段“内核代码”注入到内核中运行,注入时要先用llvm编译得到使用BPF指令集的elf文件,然后从elf文件中解析出可以注入内核的部分,最后用bpf_load_program方法完成注入。 用户态程序和注入到内核中的程序通过共用一个位于内核中`map`实现通信。为了防止注入的代码导致内核崩溃,eBPF会对注入的代码进行严格检查,拒绝不合格的代码的注入。BCC是一个python库,实现了map创建、代码编译、解析、注入等操作,使开发人员只需聚焦于用C语言开发要注入的内核代码。通过使用BCC工具获取相关特征。 259 | 260 | 第二个模块是:特征选择 261 | 262 | 这个模块主要是对所拿到的特征数据做个特征选择,因为拿到的数据过多,而且有些数据特征对分类没有作用,反而会影响分类训练的效果,所以要对数据集及逆行处理。主要是采用主成分分析和卡方检验对特征集进行数据处理,消除冗余特征,降低特征集的维度,增加特征集的可靠性和代表性。 263 | 264 | 第三个模块是:分类检测 265 | 266 | 将处理完的数据集导入weka工具中,采用不同的分类算法对数据集进行分类训练,这里采用十折交叉验证的方式来进行测试,就是将数据集分为10份,9份作为训练集,1份作为测试集,不断轮询。进行测试,通过分类指标来判断算法的检测效果,最终得出分类效果较好的算法,实现恶意软件的识别检测。 267 | 268 | 269 | 270 | #### Linux下轻量级web服务器 271 | 272 | 该项目是Linux下C++轻量级Web服务器的实现。使用线程池 +非阻塞socket + epoll+事件处理的并发模型;使用状态机解析HTTP请求报文,⽀持解析GET和POST请求;访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件;实现同步/异步日志系统,记录服务器运行状态。 273 | 274 | **主要负责:**使用线程池 +非阻塞socket + epoll+事件处理的并发模型,采用单例模式和线程池,数据库连接池的方式,完成访问服务器数据库和日志系统的实现。 275 | 276 | 277 | 278 | 这个项目主要是疫情期间,应学校的课程要求,和同学做的一个项目。而且我们刚好也想提升下自己在C++方面的一个编程经验,增加些C++的项目经验,了解C++的项目流程。 279 | 280 | 项目是linux下C++轻量级web服务器的实现。项目的主要功能就是实现一个web端的多用户注册、登录,然后可以请求访问服务器上的图片的视频文件,实现并发处理。服务器可以记录各个用户登录,注册和访问的一些日志信息。 281 | 282 | 第一:由于项目中要实现多用户的并发访问,当进行并行的任务作业时,线程的建立与销毁的开销是阻碍性能的关键,因此项目中主要采用线程池,维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。避免线程的创建和销毁带来的性能开销,避免大量的线程间因相互抢占资源导致的阻塞现象。 283 | 284 | 第二:采用非阻塞socket的和epoll并发模式,实现多用户的并发访问。非阻塞IO通过进程多次系统调用,并马上返回,我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,**不要将进程睡眠**,而是返回一个错误。此时进程就可以干其他事,减少不必要的时间等待。使用epoll I/O复用,采用边缘触发模式,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦这个fd就绪,内核就会采用回调机制激活fd。只有活跃的socket才会主动调用callback,不会采用轮询的方式去判断fd是否就绪,提升效率。 285 | 286 | 第三:HTTP解析,根据HTTP的状态转移,通过主从状态机封装了http的连接类。其中主状态机在内部调用从状态机,从状态机将处理状态和数据传给主状态机。客户端发出一个http连接请求,从状态机读取数据,更新自身状态和接受数据,传给主状态机。主状态机根据从状态机的状态,更新自身状态,决定响应请求还是继续读取。(这一部分不是很了解,参与的不是很多) 287 | 288 | 第四:数据库的连接实现。数据库连接使用单例模式,保证有唯一的连接对象。由于是多用户访问,可能会产生频繁的建立连接和关闭连接,这样会极大的减低系统的性能。采用连接服用技术,通过建立一个数据库连接池以及一些连接使用策略,使得一个数据库连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。这个项目使用STL list容器实现数据库连接池,连接池的大小使用静态大小初始大小设置为10。在对数据库的相关操作中,使用互斥锁实现线程安全,在每次进行连接池操作之前进行加锁,操作完成之后解锁。 289 | 290 | 第五:日志系统。同步/异步日志系统主要涉及了两个模块,一个是日志模块,一个是阻塞队列模块,其中加入阻塞队列模块主要是为异步写入日志做准备。这里阻塞队列的实现是使用的**STL中的queue**为底层,使用单例模式确保全局只有唯一的一个对象,保证使用相同的队列。 291 | 292 | 最后使用webbeach压力测试软件,根据每秒钟响应请求数和每秒钟传输数据量,测试该服务器的负载性能,对应调整线程池大小和连接池大小。 293 | 294 | 295 | 296 | 297 | 298 | **http解析:** 299 | 300 | 根据状态转移,通过主从状态机封装了http连接类。其中,主状态机在内部调用从状态机,从状态机将处理状态和数据传给主状态机。 301 | 302 | 客户端发出http连接请求,从状态机读取数据,更新自身状态和接收数据,传给主状态机 303 | 304 | 主状态机根据从状态机状态,更新自身状态,决定响应请求还是继续读取 --------------------------------------------------------------------------------