当前位置: 首页 > news >正文

网站引导图wordpress标签美化

网站引导图,wordpress标签美化,wordpress iis 分页 404,互联网广告营销方案原word文档[链接: https://pan.baidu.com/s/1CKnm7vHDmHSDskAgxgZgKA?pwdr4wv 提取码: r4wv 复制这段内容后打开百度网盘手机App#xff0c;操作更方便哦 --来自百度网盘超级会员v5的分享] 一、C / C基础 1、简述C的内存分区#xff1f; 一个C、C程序的内存分区主要有5个…原word文档[链接: https://pan.baidu.com/s/1CKnm7vHDmHSDskAgxgZgKA?pwdr4wv 提取码: r4wv 复制这段内容后打开百度网盘手机App操作更方便哦  --来自百度网盘超级会员v5的分享] 一、C / C基础 1、简述C的内存分区 一个C、C程序的内存分区主要有5个分别是堆区、栈区、全局区、文字常量区和程序代码区。可以将全局区和文字常量区理解为静态存储区在编译阶段就已经确定。 栈在执行函数时函数内局部变量的存储单元都可以在栈上创建函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中效率很高但是分配的内存容量有限。 堆就是那些由 new分配的内存块他们的释放编译器不去管由我们的应用程序去控制一般一个new就要对应一个 delete。如果程序员没有释放掉那么在程序结束后操作系统会自动回收。 自由存储区如果说堆是操作系统维护的一块内存那么自由存储区就是C中通过new和delete动态分配和释放对象的抽象概念。需要注意的是自由存储区和堆比较像但不等价。 全局/静态存储区全局变量和静态变量被分配到同一块内存中在以前的C语言中全局变量和静态变量又分为初始化的和未初始化的在C里面没有这个区分了它们共同占用同一块内存区在该区定义的变量若没有初始化则会被自动初始化例如int型变量自动初始为0。 常量存储区这是一块比较特殊的存储区这里面存放的是常量不允许修改。 代码区存放函数体的二进制代码。 2、简述malloc/free和new/delete内存分配和释放函数 malloc和free函数是搭配使用的。malloc函数负责向内存申请一块连续可用的空间这块空间开辟成功返回一个指向这个空间的void * 类型的指针所以在接收的时候需要强制类型转换成指定的具体类型的指针。开辟空间失败返回NULL所以一定要对malloc的返回进行检查防止程序使用了未成功开辟的指针。free函数负责释放动态开辟出来的内存并且这个内存只能释放一次多次释放会出现错误如果释放的是空指针相当于什么也没做并且可以多次释放。并且也不要去释放未被初始化过的指针。 new和delete是操作符并不是函数。new分配内存的步骤是先调用operator new函数然后调用对应类的构造函数构造对象初始化相对应的数据最后返回一个指向该对象的指针。delete释放内存的步骤是先调用析构函数然后调用operator delete函数释放内存空间即可。同freedelete也不能多次释放被释放过的对象但是可以多次delete空指针。 3、new/delete和malloc/free的区别 开辟位置 严格来说malloc动态开辟的内存在堆区new开辟的在自由存储区如果不重载new操作符C编译器默认在堆上实现自由存储此时等价于堆区。 重载 new、delete是操作符可以重载只能在C中使用。malloc、free是函数可以覆盖C和C都可以使用。 是否调用析构函数和构造函数 new可以调用对象的构造函数对应的delete调用相应的析构函数。malloc仅仅分配内存free仅仅回收内存并不执行构造和析构函数。 是否需要指定内存大小 malloc需要显示指出开辟内存的大小new无需指定编译器可以根据对应的类自动计算。 返回值类型 new返回的是某种数据类型的指针malloc返回的是void指针new比malloc更加安全。new内存分配失败时会抛出bac_alloc异常不会返回NULLmalloc开辟内存失败会返回NULL指针所以需要判断。 4、在C中使用malloc申请的内存能否通过delete释放使用new申请的内存能否用free释放 不能malloc/free主要为了兼容Cnew和 delete 完全可以取代malloc/free的。malloc/free 的操作对象都是必须明确大小的。而且不能用在动态类上。new 和 delete会自动进行类型检查和大小 malloc/free不能执行构造函数与析构函数 所以动态对象它是不行的。 从理论上说使用malloc 申请的内存是可以通过delete释放的 。不过一般不这样写的。而且也不能保证每个C的运行时都能正常。 5、预编译中头文件和””的区别 预处理器发现#include指令后就会寻找后面跟的文件名并把这个文件中的内容包含到当前文件中被包含文件中的文本将替换源代码文件中的#include指令就像是把包含的文件中的所有内容拷贝到了源文件中的这个位置。 #include 只搜索系统目录或者配置了的第三方库目录不会搜索本地目录 #include 首先搜索本地目录若找不到才会搜索系统目录。 #include相较于#include 快一些。 6、define和const的区别 编译阶段define是在编译的预处理阶段起作用而const是在编译、运行的时候起作用。 安全性define只做字符的替换不做类型检查和计算也不求解容易产生错误一般加上一个括号包括住其全部内容否则容易出错const常量有数据类型编译器可以对其进行类型安全检查。 内存占用define只是将宏名称进行替换在内存中会产生多份相同的备份。const在程序运行中只有一份备份且可以执行常量折叠能将复杂的的表达式计算出结果放入常量表   宏定义的数据没有分配内存空间只是插入到代码中替换掉const定义的变量只是值不能改变但要分配内存空间。 7、sizeof和strlen的区别 sizeof负责计算某一个类型或者变量在内存中所占用的字节数strlen负责计算字符串的字节长度。 sizeof是一个操作符strlen是库函数在string.h头文件中。 sizeof的参数可以是数据的类型也可以是变量。strlen只能用字符串作为参数。 数组做sizeof的参数不退化可以计算出整个数组所占用的内存做strlen的参数会退化为指针。 编译器在编译时就计算出了sizeof的结果所以可以用sizeof的表达式作为定义数组时的长度而strlen的结果必须在运行时才能计算。 8、关键字const的作用 const 用来定义一个只读的变量或对象。主要优点便于类型检查、同宏定义一样可以方便地进行参数 的修改和调整、节省空间避免不必要的内存分配、可为函数重载提供参考。 阻止一个变量被改变。通常需要对它进行初始化因为以后就没有机会再去改变它了。与指针类型并用。可以指定本身为const指针常量让该指针的指向不可改变也可以指定指针指向的数据为const常量指针将无法使用这个指针修改指针指向的数据。亦可以两者同时使用。在函数声明中const可以修饰形参表明它是一个输入参数在函数内部不能改变其值。当 const 修饰函数返回值时表示函数的返回值为只读不能被修改。这样做可以使函数返回的值更加安全避免被误修改。对于类的成员函数若是指定为const类型则表明他是一个常函数不能修改类的成员变量。类的常对象只能访问类的常成员函数。常函数无法调用未被const修饰的类成员函数。当const修饰类的成员变量时它将不能修改因此必须在类构造函数的初始化列表中对该成员初始化。一个没有明确声明为const的成员函数被看作是将要修改对象中数据成员的函数而且编译器不允许它 为一个const对象所调用。因此const对象只能调用const成员函数。const类型变量可以通过类型转换符const_cast将const类型转换为非const类型。const修饰引用常量引用使得无法通过这个引用修改变量的值。 9、mutable关键字的作用 如果需要在const成员函数中修改一个成员变量的值那么需要将这个成员变量修饰为mutabe。也就是用mutable修饰的成员变量不受const成员方法的限制。 还有就是在lambda函数表示中使用这个关键字可以让函数体内部能够修改被捕获的外部变量主要是针对按值捕获的变量。 10、extern的用法 extern修饰变量和函数的声明。如果文件a.c需要引用b.c中的变量int v就可以在a.c中声明extern int v然后就可以引用变量v。extern修饰符可用于指示C代码调用其他的C语言代码。 比如在C中调用C库函数就需要在C程序中用extern “C”声明要引用的函数。这是给链接器用 的告诉链接器在链接的时候用C函数规范来链接。主要原因是C和C程序编译完成后在目标代码中命名规则不同。 10.1、extern的作用 1. extern 可以置于变量声明或者函数声明前以表示变量或者函数的定义在别的文件中提示编译器遇到此变量和函数时在其它文件中寻找其定义。 2. extern 变量表示声明一个变量表示该变量是一个外部变量也就是全局变量所以 extern 修饰的变量保存在静态存储区全局区全局变量如果没有显示初始化会默认初始化为 0或者显示初始化为 0 则保存在程序的 BSS 段如果初始化不为 0 则保存在程序的 DATA 段。 3. extern C 的作用是为了能够正确的实现 C 代码调用 C 语言代码。加上 extern C 后会指示编译器这部分代码按照 C 语言而不是 C的方式进行编译。由于 C 支持函数重载因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中而不仅仅是函数名而C语言并不支持函数重载因此编译 C 语言代码的函数时不会带上函数的参数类型一般只包括函数名。 11、const的作用优点 可以定义const常量。便于类型检查。const常量有数据类型而宏常量没有数据类型。编译器可以对前者进行类型安全而对后者只是进行字符替换没有类型安全检查并且在字符替换时可能产生意想不到的错误。可以保护被修饰的东西防止意外的修改增强程序的健壮性。为函数重载提供了一个参考。可以节省空间避免不必要的内存分配。const定义常量从汇编的角度来看只是给出了对应的内存地址而不是像#define一样给出的是立即数所以const定义的常量在程序运行过程中只有一份拷贝而#define定义的常量在内存中有若干个拷贝。提高了效率。编译器通常不为普通const常量分配存储空间而是将它们保存在符号表中这使得它成为一个编译期间的常量没有了存储与读内存的操作使得它的效率也很高。 12、typedef和define的区别 用法不同typedef 用来定义一种数据类型的别名增强程序的可读性。define 主要用来定义 常量以及书写复杂使用频繁的宏。执行时间不同typedef 是编译过程的一部分有类型检查的功能。define 是宏定义是预编译的部分其发生在编译之前只是简单的进行字符串的替换不进行类型的检查。作用域不同typedef 有作用域限定。define 不受作用域约束只要是在define 声明后的引用都是正确的。对指针的操作不同typedef 和define 定义的指针时有很大的区别。 注意typedef 定义是语句 因为句尾要加上分号。 而define不是语句千万不能在句尾加分号。 13、C的内联函数 内联函数inline的目的是为了解决程序中函数调用的效率问题。程序在编译器编译的时候编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换而对于其他的函数都是在运行时候对函数入栈执行。这其实就是空间换时间。所以内联函数一般都是行数很少的小函数。 使用的要求在内联函数内不允许使用循环语句和开关语句内联函数的定义必须出现在内联函数第一次调用之前类结构中所在的类说明内部定义的函数是内联函数。 为什么不能把所有函数写成内联函数 内联函数以代码复杂为代价它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比函数调用开销较大则没有太大的意义另一方面每一处内联函数的调用都要复制代码消耗更多的内存空间。 14、宏定义define和内联函数inline的区别 宏定义#define和内联函数inline都可以减少函数调用开销和提高代码运行效率而引入的机制但是它们的实现方式和作用机制略有不同。 define主要有两种用途定义常量和创建宏函数。无论哪种都是用于在编译时替换文本也就是define实际上只是做文本的替换。 内联函数的定义和普通函数类似只需要在函数声明前加上inline即可。编译器不一定会将所有声明为内联函数的函数进行内联是否内联取决于编译器的实现和优化策略。内联函数的优点是类型安全、可调试、可优化但是也存在一些问题。由于函数体会被复制多次会占用更多的代码段空间而且在某些情况下可能会导致代码膨胀。 区别 语义不同宏定义使用预处理器指令 #define 定义。它在预处理期间将宏展开并替换宏定义中的代码。预处理器只进行简单的文本替换不涉及类型检查。内联函数使用 inline 关键字定义它是一个真正的函数。编译器会尝试将内联函数的调用处用函数体进行替换从而避免函数调用的开销。类型检查宏定义就是单纯的字符替换不涉及类型检查容易导致错误内联函数会进行类型检查更加安全。内联函数可以进行调试宏定义的函数无法调试。宏可能导致不合理的计算。在内联函数传递参数值计算一次而使用宏的情况下每次在程序中使用宏时都会传递表达式参数因此宏会对表达式参数计算多次。因为宏只做替换可能会把同样的表达式替换到其他地方。 15、指针常量和常量指针的区别 指针常量int * const vptr是指定义了一个指针这个指针只能在定义时初始化其他地方不能改变不能再改变这个指针的指向。 常量指针是指定义了一个指针指向了一个只读对象不能通过常量指针来改变这个对象的值。 指针常量强调的是指针的不可变性常量指针强调的是指针对其所指对象的不可变性。 16、函数指针和指针函数的区别 指针函数类型说明符 * 函数名参数是一个函数返回一个指针实际上就是返回一个地址给调用函数在调用指针时需要一个同类型的指针来接收其函数的返回值也可以将其返回值设置为void * 类型调用时强制转换返回值为需要的类型。 函数指针类型说明符 * 函数名参数是一个指针指向函数的指针包含了函数的地址可以用它来调用函数本质是一个指针变量该指针指向了这个函数。 17、指针数组和数组指针 指针数组是一个数组每个元素都是指针。int * a[3]数组中存放了3个int *指针变量。 数组指针是一个指针指向了整个数组。比如int (*a)[10]是指向了一个元素个数为10的数组。 定义 说明 int a 一个整型数 int * a 一个指向整型的指针 int **a 一个指向指针的指针指向的指针是一个整数类型 int a[10] 一个有10个整型的数组 int *a[10] 指针数组一个有10个指针的数组指针指向整型 int (*a)[10] 数组指针一个指向有10个整型数数组的指针 int (*a)(int) 函数指针一个指向函数的指针该函数有一个整型参数并返回一个整型 int (*a[10])(int) 一个有10个指针的数组该指针指向一个函数该函数有一个整型参数并返回一个整型 18、野指针和悬空指针的区别 野指针和悬空指针都是指向无效内存的指针他们的成因和表现有所不同。 野指针是一个指向不明确的指针主要是指未被初始化过的指针。所以它的值是不确定的可能指向任意内存地址访问野指针可能导致未定义行为如程序崩溃和数据损坏等。 悬空指针是指向已经被释放内存的指针。这种指针仍然具有以前分配的内存地址但是这块内存可能已经被其他对象或数据占用。访问空悬指针同样会导致未定义行为。 19、如何避免野指针 1 指针变量声明时没有被初始化。解决办法指针声明时初始化可以是具体的地址值也可让它指向 NULL。 2 指针 p 被 free 或者 delete 之后没有置为 NULL。解决办法指针指向的内存空间被释放后指针应该指向 NULL。 3 指针操作超越了变量的作用范围。解决办法在变量的作用域结束前释放掉变量的地址空间并且让指针指向 NULL。 20、定义和声明的区别 声明是通知编译器变量的类型和名字不会为变量分配空间。 定义需要分配空间同一个变量可以可以被声明多次但是只能被定义一次。 21、C语言关键字static和C的static有什么么区别 在C语言中static用来修饰局部静态变量和外部静态变量、函数。而C中除了上述功能外还用来定义类的成员变量和函数即静态成员和静态成员函数。 static的记忆性和全局性特点可以让在不同时期调用的函数进行通信传递信息而C的静态成员可以在多个对象实例间进行通信传递信息。 static用途静态变量局部 / 全局、静态函数、类的静态数据成员、类的静态成员函数。 22、static的用法和作用 1隐藏静态全局变量和全局函数。当同时编译多个文件时所有未加static前缀的全局变量和函数都具有全局可见性只要加上static就会被限制在当前文件。 2保持变量内容的持久性静态全局和局部变量。static变量存储在静态数据区在程序开始就已经完成了初始化在程序结束才会销毁这个变量。 3static的第三个作用时默认初始化为0。存储在静态数据区的变量都有这个特点这个区域默认值都是0x00。 4类成员函数体内static变量的作用范围是在该函数体该变量的内存只被分配一次因此其值在下次调用这个函数的时候仍然维持上次的值。 5在类中的static成员变量属于整个类所有对类的所有对象只有一份拷贝。 6在类中的static成员函数属于整个类所有这些函数不接收this指针因而只能访问类的static成员变量。 7static成员函数不能被virtual修饰否则编译无法通过。static成员不属于任何对象或者实例所以加上virtual没有任何实际意义静态成员函数没有this指针虚函数的实现是为了每一个对象分配一个vptr指针而vptr是通过指针调用的所以不能为virtual。虚函数的调用关系this-vptr-ctable-virtual function。 22.1、说说静态变量什么时候初始化 1、静态变量在程序启动时就会被初始化而且只会初始化一次。针对全局作用域内的静态变量在类内声明内外初始化的静态成员变量。 2、静态变量在函数内部则在函数第一次执行时进行初始化。 23、引用是什么常引用的作用 引用Reference是一种别名用于为已经存在的变量起一个新的名称。引用提供了对变量的间接访问方式允许使用引用来操作原始变量。 常引用的引入主要是为了避免使用变量的引用时在不知情的情况下改变变量的值。常引用主要用于定义一个普通变量的只读属性的别名、作为函数的传入形参避免实参在调用函数中被意外的改变。说明很多情况下需要用常引用做形参被引用对象等效于常对象不能在函数中改变实参的值这样的好处是有较高的易读性和较小的出错率。 24、指针和引用的区别 指针和引用在 C 中都用于间接访问变量但它们有一些区别 1指针是一个变量它保存了另一个变量的内存地址引用是另一个变量的别名与原变量共享内存地址。 2指针可以被重新赋值指向不同的变量引用在定义的时候必须初始化且之后不能更改始终指向同一个变量。 3指针可以为 nullptr表示不指向任何变量引用必须绑定到一个变量不能为 nullptr。 4使用指针需要对其进行解引用以获取或修改其指向的变量的值引用可以直接使用无需解引用。 5指针本身和其他变量一样对于直接对指针变量的操作都是针对指针本身而不是指向的变量比如sizeof、自增自减运算符等。而引用则是直接作用于原变量。 在什么时候使用指针或者引用 如果需要返回局部变量就要使用指针但是要求这个局部变量是动态开辟的。如果对栈空间大小比较敏感比如递归的时候使用引用。使用引用传递不需要创建临时对象开销相对更小。类对象作为传输传递时使用引用这是C类对象传递的标准方式。 25、C语言的struct和C的struct的区别 C语言中struct是用户自定义数据类型UDTC中struct是抽象数据类型ADT支持成员函数的定义C中的struct能继承能实现多态用类能实现的功能结构体基本上都能实现。C中struct是没有权限的设置的且struct中只能是一些变量的集合体可以封装数据却不可以隐藏数据而且成员不可以是函数。C中struct的成员默认访问说明符为public为了与C兼容而C语言中的struct的成员没有访问权限的概念。struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后在C中必须在结构标记前加上struct才能做结构类型名。 26、C 中 class 和 struct 区别 C 中为了兼容 C 语言而保留了 C 语言的 struct 关键字并且加以扩充了含义。 在 C 语言中struct 只能包含成员变量不能包含成员函数。 而在 C 中struct 类似于 class既可以包含成员变量又可以包含成员函数。用类能实现的功能结构体基本上都能实现 区别 class 中类中的成员默认都是 private 属性的而在 struct 中结构体中的成员默认都是 public 属性的。 class 继承默认是 private 继承而 struct 继承默认是 public 继承。 27、volatile的作用能够和const同时使用吗 1、volatile是 C 语言中的一个关键字用于修饰变量表示该变量的值可能在任何时候被外部因素更改例如硬件设备、操作系统或其他线程。当一个变量被声明为volatile时编译器会禁止对该变量进行优化以确保每次访问变量时都会从内存中读取其值而不是从寄存器或缓存中读取。避免因为编译器优化而导致出现不符合预期的结果。 2、volatile限定符是告诉计算机所修饰的变量随时都可能被外部因素修改比如操作系统其他线程等不要对这个变量进行优化每次取用的时候都要直接从内存中读取而不是从寄存器中读取数据。const和volatile可以一起使用volatile是防止编译器对代码进行优化这个值是可以变的。而const的含义是在代码中不能对变量进行修改。因此两者不矛盾。 28、内存字节对齐 在C/C中字节对齐是内存分配的一种策略。 当分配内存时编译器会自动调整数据结构的内存布局使得数据成员的起始地址与其自然对齐边界一般为自己大小的倍数相匹配。 理论上任何类型的变量都可以从任意地址开始存放。然而实际上访问特定类型的变量通常需要从特定对齐的内存地址开始。因为如果不对数据存储进行适当的对齐可能会导致存取效率降低。所以各种数据类型需要按照一定的规则在内存中排列起始地址而不是顺序地一个接一个排放这种排列就是字节对齐。 29、字节序 大端模式是指数据的高字节保存在内存的低地址中而数据的低字节保存在内存的高地址端。 小端模式是指数据的高字节保存在内存的高地址中而低位字节保存在内存的低地址端。 在网络传输中通常使用大端字节序网络字节序。在具体的操作系统中字节序取决于底层硬件架构。例如Linux和Windows操作系统主要运行在x86和x86_64interl和AMD处理器架构上这些处理器使用小端字节序。 检测字节序方式一 int i 1;     if (*((char *)i) 1) {         cout 小端 endl;     }     else if (*((char *)i) 0) {         cout 小端 endl;     } 检测字节序方式二 union Endian {     char a;     int b; }; Endian endi; endi.b 1; (endi.a 1) ? cout 小端 endl : cout 大端 endl; 31、什么是内存泄漏内存泄漏有哪几种情况 1、堆内存泄漏。在程序运行中根据需要分配一块内存在完成相关操作后必须通过调用对应的free或者delete删掉。如果及时释放掉之后将无法引用这块无用的内存就会产生堆内存泄漏。 2、系统资源泄露。系统分配给程序的资源没有使用相应的函数释放掉比如socket等导致系统资源浪费严重可导致系统效能降低系统运行不稳定。 3、没有将基类的析构定义为虚析构。在这种情况下如果使用多态并且子类存在动态分配的成员变量将无法调用子类析构释放资源从而造成内存泄漏。 4、在释放对象数组时没有使用delete[]而是使用了delete。当一个数组中的多个元素均为对象时在使用delete释放该数组是必须加上方括号否则只是调用一次析构函数释放数组的第一个对象而剩下的数组元素没有被析构掉从而造成了内存泄漏。 5、缺少拷贝构造函数。如果类中没有手动编写拷贝构造函数用该类对象进行拷贝赋值时会使用默认的拷贝构造函数即浅拷贝。 32、如何判断内存泄漏 内存泄露只发生一次小的可能不会有太大的影响但是大量泄漏内存的程序将会出现内存逐渐用完程序性能下降。甚至导致其他程序运行失败。 1、Windows平台下的Vs在主函数后面加上_CrtDumpMemoryLeaks();函数就可以在debug运行之后在输出窗口显示内存泄漏的情况 2、在Linux中可以使用valgrind工具 valgrind –leak-checkfull ./app 33、如何解决内存泄漏 内存泄漏解决方案 1、使用智能指针辅助帮助内存的维护。 2、注意手动内存管理在动态开辟空间后需要及时使用delete或者free释放空间。 3、RAII资源获取即初始化原则通过在对象的构造函数中分配资源然后再析构函数中释放资源确保在对象生命周期结束时被正确释放。 4、使用内存分析工具比如Valgrind检测和诊断内存泄漏问题。 5、编码规范和代码审查准许良好的编码规范和代码审查可以帮助发现潜在的内存泄漏问题。 34、什么是内存溢出越界如何解决 内存溢出是指程序在申请内存时没有足够的内存空间供其使用出现out of memory比如申请了一个int类型大小的内存但是存了long类型才能存下的数这就是内存溢出。动态内存分配过多导致堆内存耗尽引发内存溢出递归深度过大导致栈空间被耗尽。 内存溢出解决方案 1、检查内存泄漏内存泄漏会间接导致内存溢出。 2、限制递归深度。 35、C和C的区别 1. C 语言是面向过程的语言而 C 支持面向对象所以 C 语言自然没有面向对象的封装、继承、多态等特性也不支持面向对象的一些语法 2. C 支持函数重载C 语言不支持 3. C 程序中如果函数没有任何参数需要将参数定义为 void 以此来限定函数不可传递任何参数如果不进行限定让参数表默认为空其意义是可以传递任何参数在 C 中不带参数的函数表示函数不能传递任何参数 4. C 语言 struct 中不能有函数而 C 语言 struct 中可以有函数 5. C 语言函数参数不支持默认值而 C 语言支持参数默认值 6. C 语言支持内联函数而 C 语言不支持 7. C 语言支持引用而 C 语言不支持 8. C 语言采用 malloc 和 free 函数动态申请和释放内存而 C 使用 new 和 delete 运算符 9. C 语言中只有局部和全局两个作用域而 C 中有局部、全局、类、名称空间作用域。 36、C中的堆和栈的区别 1、管理方式不同堆中的资源由程序员控制容易产生内存泄漏栈资源由编译器自动管理无需手工控制。 2、空间大小不同堆是不连续的内存区域内部采用链表来存储空闲内存地址堆大小受限于计算机系统中有效的虚拟内存所以堆的空间比较灵活内存比较大。栈是一块连续的内存区域大小是操作系统预定好的。 3、碎片问题在堆中频繁分配和释放空间会产生大量碎片使程序效率降低。对于栈是一个先进后出的队列进出一一对应不会产生碎片。 4、增长方向堆是向高地址方向增长栈是向低地址方向增长。 5、分配方式堆是动态分配栈有静态分配和动态分配静态分配由编译器完成动态分配由alloca函数分配但栈的动态分配的资源由编译器进行释放无需程序员实现。 6、堆由C/C函数库提供机制很复杂所以堆的效率比栈低很多。 栈和堆哪个更快原因 栈更快因为操作系统提供了对栈的硬件资源的支持在底层会分配专门的寄存器存放栈的地址栈的入栈出栈操作也是简单有专门的指令执行所以栈的效率比较高。 而堆的操作是由C/C库函数提供的在分配堆内存时需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问第一次访问指针第二次根据指针保存的地址访问内存因此堆比较慢。 被free回收的内存会立即返还给操作系统吗 不会立即返还。被free回收的内存会首先被ptmalloc使用双链表保存起来当用户下一次申请内存的时候会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并避免过多的内存碎片。 37、如何防止头文件被重复包含 1、使用宏定义避免重复引入 #ifndef _NAME_H #define _NAME_H​ #endif​ 2、使用#pragma once指令避免重复引入 38、智能指针和指针的区别 智能指针可以自动释放使用了RAII资源分配即初始化原则所以当一个对象的生命周期到了以后就会调用析构函数完成指针指向内存的释放而指针需要程序员手动释放。 智能指针是类模板而指针是一种数据类型智能指针是在普通指针加了一层封装机制。 39、数组名和指针的区别 数组名是数组中第一个元素的地址二者均可以通过偏移量来访问数组中的元素。数组名不是真正意义上的指针可以理解为常指针所以数组名没有自增、自减等操作。 当数组名当作形参传递给调用函数后就会失去原有特性退化为一般指针可以进行自增自减操作因此sizeof运算符不能再得到原数组的大小。 40、char str[]与char * str的区别 1、概念不同C语言中没有特定的字符串类型常用以下两种方式定义字符串一种是字符数组另一种是指向字符串的指针。 char *str 声明的是一个指针这个指针可以指向任何字符串常量。 char str[] 声明的是一个字符数组数组的内容可以是任何任何字符严格意义上说末尾加上’\0’ 之后才能算是字符串。 2、变量不同 char *str里的str是指针变量str的值未初始化局部变量的话。全局则自动初始化为NULL。 char str[]里str是地址常量str的值是str[ ]的地址。 3、内存分配方式不同 字符串指针指向的内容是不可修改的不可单个字符修改字符数组是可以修改的即char * str定义的字符串保存在常量区是不可更改的char str[ ]定义的字符串保存在全局数据区或栈区是可修改的。 41、C中新增了string他与C语言中的char * 有什么区别是如何实现的 string继承自basic_string,其实是对char进行了封装封装的string包含了char数组容量长度等等属性。string可以进行动态扩展在每次扩展的时候另外申请一块原空间大小两倍的空间2^n然后将原字符串拷贝过去并加上新增的内容。 42、C程序是如何一步步生成的 1、预处理 1 将所有的#define删除并且展开所有的宏定义 2 处理所有的条件预编译指令如#if、#ifdef 3 处理#include预编译指令将被包含的文件插入到该预编译指令的位置。 4 过滤所有的注释 5 添加行号和文件名标识。 2、编译 1 词法分析将源代码的字符序列分割成一系列的记号。 2 语法分析对记号进行语法分析产生语法树。 3 语义分析判断表达式是否有意义。 4 代码优化比如内联函数、合并代码分支、公共子表达式消除等。 5 目标代码生成生成汇编代码并且进行优化。 3、汇编 这个过程主要是将汇编代码转变成机器可以执行的指令。 4、链接 将不同的源文件产生的目标文件进行链接从而形成一个可以执行的程序。 链接分为静态链接和动态链接。 静态链接是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中就算你在去把静态库删除也不会影响可执行程序的执行生成的静态链接库Windows下以.lib为后缀Linux下以.a为后缀。 动态链接是在链接的时候没有把调用的函数代码链接进去而是在执行的过程中再去找要链接的函数生成的可执行文件中没有函数代码只包含函数的重定位信息所以当你删除动态库时可执行程序就不能运行。生成的动态链接库Windows下以.dll为后缀Linux下以.so为后缀。 43、nullptr调用成员函数可以吗为什么 可以调用成员函数。因为在编译时对象就绑定了函数地址和指针空不空没关系。调用某个函数时会把nullptr传递给这个函数只要在这个函数中没有需要对这个空指针进行解引用的地方那么就不会出错。 44、i和i的区别 1、赋值顺序不同i是先加值后赋值i是赋值后加值。他们都分为两步进行。 2、效率不同后置执行速度比前置的慢。因为前置不会产生临时对象后置必须产生临时对象从而导致效率降低。 3、后置加加不能作为左值而i可以所以i可以取地址但是i不可以。因为前置返回一个引用后置返回一个对象。 4、两者都不是原子操作。 45、在main执行之前和之后执行的代码可能是什么 main函数执行之前 设置栈指针、初始化静态static变量和全局变量即.data段的内容 将未初始化的部分全局变量赋初值为0即.bss段的内容 全局对象初始化调用对应的构造函数 传递main函数的参数。 main函数执行之后 全局对象的析构函数调用 如果用atexit注册了一个函数则会在main函数之后执行。 46、宏定义和typedef区别 宏主要用于定义常量以及书写频繁使用且复杂的内容typedef主要用于定义类型的别名。 宏替换实在预处理阶段之前执行的属于文本插入替换tpyedef属于编译阶段。 宏不检查类型typedef会检查数据类型。 宏不是语句不需要加分号结尾typedef是语句需要用分号结尾。 宏没有作用域的限制定义宏之后都可以使用而typedef有作用域的限制。 47、C中有几种new 主要有三种典型的new是使用方法plain new、nothrow new和placement new。 plain new就是普通的new直接new A(); 在分配空间失败的情况下抛出异常std::bad_alloc而不是返回NULL因此通过返回值无法判断是否开辟成功。nothrow new在空间分配失败时不会抛出异常而是返回NULLnew(nothrow) A()。placement new 允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败因为它根本不分配内存它做的唯一一件事情就是调用对象的构造函数。 48、形参和实参的区别 1、形参变量只有在被调用时才分配内存单元在调用结束时 即刻释放所分配的内存单元。因此形参只有在函数内部有效。 函数调用结束返回主调函数后则不能再使用该形参变量。 2、实参可以是常量、变量、表达式、函数等 无论实参是何种类型的量在进行函数调用时它们都必须具有确定的值 以便把这些值传送给形参。 因此应预先用赋值输入等办法使实参获得确定值会产生一个临时变量。 3、实参和形参在数量上类型上顺序上应严格一致 否则会发生“类型不匹配”的错误。 4、函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参而不能把形参的值反向地传送给实参。 因此在函数调用过程中形参的值发生改变而实参中的值不会变化。 5、当形参和实参不是指针类型和引用类型时在该函数运行时形参和实参是不同的变量他们在内存中位于不同的位置形参将实参的内容复制一份在该函数运行结束的时候形参被释放而实参内容不会改变。 49、值传递、指针传递、引用传递的区别和效率 值传递有一个形参向函数所属的栈拷贝数据的过程如果值传递的对象是类对象 或是大的结构体对象将耗费一定的时间和空间。传值 指针传递同样有一个形参向函数所属的栈拷贝数据的过程但拷贝的数据是一个固定为4字节的地址。传值传递的是地址值 引用传递同样有上述的数据拷贝过程但其是针对地址的相当于为该数据所在的地址起了一个别名。传地址 效率上讲指针传递和引用传递比值传递效率高。一般主张使用引用传递代码逻辑上更加紧凑、清晰。 50、静态变量什么时候初始化 初始化只有一次赋值可以有多次。对于静态全局变量在主程序执行之前编译器已经为其分配了内存但在C和C中静态局部变量的初始化节点不一样。 在C语言中初始化发生代码执行之前编译阶段分配好内存之后就会进行初始化。 在C中静态局部变量的初始化发生在相关代码执行的时候。 51、malloc、calloc、realloc函数的区别及用法 malloc函数void * malloc(size_t size)。这个函数分配指定字节大小的内存并且返回一个指针指向这块分配的内存。这块内存中的数据是没有被初始化的如果获取都是一些随机值。 calloc函数void * calloc(size_t nmemb, size_t size)。这个函数有两个参数第一个参数是需要给多少个对象分配内存第二个参数是每个对象所占用的字节大小。这块内存会被初始化为0。可以用作于开辟数组 realloc函数void * realloc(void * ptr, size_t newSize)。用于对已有的空间进行扩容返回扩容后地址的首地址。 52、C中新增了string他与C语言中的char *有什么区别如何实现的 string继承自basic_string,其实是对char*进行了封装封装的string包含了char*数组容量长度等等属性。 string可以进行动态扩展在每次扩展的时候另外申请一块原空间大小两倍的空间2*n然后将原字符串拷贝过去并加上新增的内容。 53、对象复用的了解零拷贝 对象复用其本质是一种设计模式Flyweight享元模式。通过将对象存储到“对象池”中实现对象的重复利用这样可以避免多次创建重复对象的开销节约系统资源。 零拷贝是一种计算机数据传输技术旨在减少数据在不同内存区域之间的复制次数从而提高数据传输的效率和性能。传统的数据传输方式通常涉及多次数据复制而零拷贝技术可以最大程度地减少或避免这些复制操作从而减少CPU和内存的开销。。零拷贝技术可以减少数据拷贝和共享总线操作的次数。C中的vector容器的push_back函数需要调用拷贝构造函数和转移构造函数而使用emplace_back()插入元素原地构造不需要出发拷贝构造和移动构造效率更高。减少了中间的拷贝环节。此外在Linux中的sendfile系统调用可以将一个文件的内容直接传输到另一个文件或者网络套接字中不需要经过拷贝到用户空间。 54、动态绑定和静态绑定的区别 静态绑定发生在编译期动态绑定发生在运行期 对象的动态类型可以更改但是静态类型无法更改 要想实现动态必须使用动态绑定 在继承体系中只有虚函数使用的是动态绑定其他的全部是静态绑定。 55、引用是否能实现动态绑定为什么可以实现 可以。 引用在创建的时候必须初始化在访问虚函数时编译器会根据其所绑定的对象类型决定要调用哪个函数。注意只能调用虚函数才是动态绑定。 56、函数指针是什么 函数指针是指向函数的指针变量。因此“函数指针”本身首先应是指针变量只不过该指针变量指向函数。指向的是特殊的数据类型函数的类型尤其返回的数据类型和其参数列表共同决定而函数的名称则不是其类型的一部分。 57、为什么有函数指针 函数与数据项相似函数也有地址。使用函数指针可以在同一个函数中通过使用相同的函数指针形参在不同的时间使用产生不同的效果。 58、你知道strcpy和memcpy的区别是什么吗 char *stpcpy(char *restrict dst, const char *restrict src); void *memcpy(void dest[restrict .n], const void src[restrict .n], size_t n); 1、复制的内容不同。strcpy只能复制字符串而memcpy可以复制任意内容例如字符数组、整型、结构体、类等。 2、复制的方法不同。strcpy不需要指定长度它遇到被复制字符的串结束符\0才结束所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。 3、用途不同。通常在复制字符串时用strcpy而需要复制其他类型数据时则一般用memcpy。 4、效率不同。memcpy效率比strcpy更高。 60、介绍一下几种典型的锁 读写锁 1、多个读者可以同时进行读 2、写者必须互斥只允许一个写者写也不能读者写者同时进行 3、写者优先于读者一旦有写者则后续读者必须等待唤醒时优先考虑写者 互斥锁 一次只能一个线程拥有互斥锁其他线程只有等待。 互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒而操作系统负责线程调度为了实现锁的状态发生改变时唤醒阻塞的线程或者进程需要把锁交给操作系统管理所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的加锁的时间大概100ns左右而实际上互斥锁的一种可能的实现是先自旋一段时间当自旋的时间超过阀值之后再将线程投入睡眠中因此在并发运算中使用互斥锁每次占用锁的时间很短的效果可能不亚于使用自旋锁。 条件变量 互斥锁一个明显的缺点是他只有两种状态锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足他常和互斥锁一起使用以免出现竞态条件。当条件不满足时线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制条件变量则是同步机制。 自旋锁 如果线程无法取得锁线程不会立刻放弃CPU时间片而是一直循环尝试获取锁直到获取为止。如果别的线程长时期占有锁那么自旋就是在浪费CPU做无用功但是自旋锁一般应用于加锁时间很短的场景这个时候效率比较高。 61、delete和delete[]的区别 delete用于销毁单个对象只会调用一次析构函数。delete [ ]用于销毁数组对象销毁数组中的每个对象会多次调用析构函数。 62、什么是内存池如何实现 内存池Memory Pool 是一种内存分配方式。通常我们习惯直接使用new、malloc 等申请内存这样做的缺点在于由于所申请内存块的大小不定当频繁使用时会造成大量的内存碎片并进而降低性能。内存池则是在真正使用内存之前先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时就从内存池中分出一部分内存块 若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片使得内存分配效率得到提升。 63、各种基本数据类型的字节大小64位机器 64、什么是回调函数有什么缺点面试题 回调函数Callback Function是一种将一个函数作为参数传递给另一个函数以便在需要时执行传递的函数的机制。回调函数常用于实现事件处理、异步操作、自定义排序和过滤等场景。通常回调函数用于在某个特定事件发生时执行特定的逻辑从而实现代码的模块化和灵活性。 优点1、模块化和可重用性 使用回调函数可以将代码逻辑分为模块使得这些模块可以在多个地方重复使用从而提高了代码的可维护性和可重用性。 2、灵活性 回调函数允许在运行时动态地指定要执行的操作从而在不修改核心代码的情况下改变程序的行为。 缺点可读性较差 如果不适当使用回调函数可能会导致代码变得难以理解和维护。特别是当回调函数较复杂时代码的流程可能会变得混乱。 上下文传递 在一些情况下回调函数可能需要访问调用它的函数的上下文信息。这可能需要额外的参数传递或者使用全局变量导致代码的耦合度增加。 错误处理 回调函数中的错误处理可能会变得复杂因为错误的传递和处理需要更多的考虑。 异步操作 在涉及异步操作的场景中回调函数可能会导致代码嵌套过深使代码变得难以理解和调试。 缺点解决现代C中提供了更多高级的方式来处理回调如使用函数对象、Lambda 表达式、标准库中的函数指针容器如 std::function、异步编程库等。这些工具可以在一定程度上减轻回调带来的问题提供更好的代码结构和可读性。 二、面向对象 1、什么是面向对象面向对象的三大特性基本特征 面向对象思想面向对象的思想是尽可能模拟人类的思维方式使得软件的开发方法与过程尽可能接近人类认识世界、解决现实问题的方法和过程把客观世界中的实体抽象为问题域中的对象。面向对象以对象为核心该思想认为程序由一系列对象组成。 面向对象思想的特点一种更符合人类思维习惯的思可以将复杂的问题简单化将我们从执行者变成了指挥者。 C 面向对象编程 (OOP) 的三大特性包括封装、继承和多态。 封装是将数据属性和操作这些数据的函数方法组合在一个类Class中的过程。封装的主要目的是隐藏类的内部实现细节仅暴露必要的接口给外部。通过封装我们可以控制类成员的访问级别例如public、protected 和 private限制对类内部数据的直接访问确保数据的完整性和安全性。 继承是一个类派生类Derived Class从另一个类基类Base Class那里获得其属性和方法的过程。继承允许我们创建具有共享代码的类层次结构减少重复代码提高代码复用性和可维护性。在 C 中访问修饰符如 public、protected、private控制了派生类对基类成员的访问权限。 多态是允许不同类的对象使用相同的接口名字但具有不同实现的特性。在 C 中多态主要通过虚函数Virtual Function和抽象基类Abstract Base Class来实现。虚函数允许在派生类中重写基类的方法而抽象基类包含至少一个纯虚函数Pure Virtual Function不能被实例化只能作为其他派生类的基类。通过多态我们可以编写更加通用、可扩展的代码提高代码的灵活性。 总结封装、继承和多态是面向对象编程的三大核心特性能够帮助我们编写更加模块化、可重用和可维护的代码。 2、C类成员访问权限 在 C 中类成员的访问权限是通过访问修饰符来控制的。有三种访问修饰符public、private 和 protected分别定义了类成员的访问级别控制类成员的可见性和可访问性。 1在类成员中使用访问权限 public公共成员在任何地方都是可以访问的类内部外部都可以。调用方可以直接访问和修改公共成员公共访问修饰符通常用于类的外部接口。类成员不建议使用public修饰这不符合封装原则。protected受保护成员类似于私有成员但它们可以被派生类访问。受保护成员通常用于继承和多态等场景这样子类也可以访问父类的成员变量。private私有成员只能在类的内部访问即仅在类的成员函数中可以访问。私有成员用于实现类的内部实现细节这些细节对于类的用户来说是隐藏的。 2在继承时使用访问权限 当子类继承父类时类成员的访问权限会被修改规则是继承时的权限替代父类大于等于继承的变量的权限。 3、重载、重写和隐藏的区别 补充重载是指相同作用域内拥有相同的方法名但是有不同的参数类型和/或参数数量的方法。重载允许根据所提供的参数不同来调用不同的函数。方法具有相同的名称、方法具有不同的参数类型或者参数数量、返回类型可以相同也可以不相同同一作用域。 重写是指在派生类中重新定义基类中的方法。当派生类需要改变或者扩展基类方法的功能时就需要用到重写。方法具有相同的名称、方法具有相同的参数类型和数量、方法具有相同的返回类型、重写的基类中被重写的函数必须有virtual修饰发生在继承关系的类之间。 隐藏是指派生类的函数屏蔽了与其同名的基类函数。 当参数不同时无论基类中的函数是否被virtual修饰基类函数都是被隐藏的而不是被重写。 重载和重写的区别 范围不同重写和被重写的函数在不同的类中重载和被重载的函数在同一个类中也就是在同一个作用域中。参数不同重写和被重写的参数列表一定要相同重载和被重载的函数列表一定不同。virtual不同重写的基类的函数必须要有virtual修饰重载函数和被重载函数可以被virtual修饰也可以没有。 隐藏和重写、重载的区别 与重载返回不同隐藏函数和被隐藏函数在不同的类中。参数的区别隐藏函数和被隐藏函数参数列表可以相同也可以不相同但是函数名一定相同当参数不同时无论基类中的函数是否被virtual修饰基类函数都是被隐藏的而不是被重写。 4、谈谈菱形继承补充 菱形继承是在继承关系中有一个基类并且这个基类直接或者间接派生出了2个或者多个派生类这些派生类又被一个共同的类继承了。比如iostream继承自istream和ostream而这两个类由ios类派生。 菱形继承会引发一些问题主要有数据冗余和二义性。如果最初的基类有一个字段那么它的派生类都会有这个字段最后继承了多个类的派生类将会有多个这个字段当用子类对象调用这个字段时将会出现错误编译出实现不明确的问题。 为了解决菱形继承问题C引入了虚继承的概念通过在派生类对共同基类的声明中使用virtual关键字可以确保只有一个共享的基类实例从而避免二义性和数据冗余的问题。如下图最顶层的基类成为虚基类。 5、类的构造顺序和析构顺序 一、构造顺序 如果当前类继承了一个或者多个基类他们将按照声明顺序进行初始化但是如果有虚继承优先虚继承。类的成员变量按照它们在类定义中的声明顺序进行初始化成员变量的初始化顺序只与声明的顺序有关。执行本身的构造函数。 二、类的析构顺序与构造顺序完全相反。 类成员初始化方式为什么用成员初始化列表会快一点 初始化方式赋值初始化通过在函数体内进行赋值初始化列表初始化在构造函数之后使用初始化列表进行初始化。 两者区别 对于在函数体内初始化是在所有数据都被分配内存空间后才进行的 列表初始化是给数据成员分配内存空间时就进行初始化相比于赋值初始化他减少了中间状态临时对象此外编译器也能够对其优化。用初始化列表会快一些的原因是对于类类型它少了一次调用构造函数的过程而在函数体中赋值则会多一次调用。 哪些情况必须用到成员列表初始化作用是什么 ① 当初始化一个引用成员时 ② 当初始化一个常量成员时 ③ 当调用一个基类的构造函数而它拥有一组参数时 ④ 当调用一个成员类的构造函数而它拥有一组参数时。 6、析构函数可以抛出异常吗 首先从语法层面并没有禁止析构函数抛出异常但在实践中不要这样做。 由于析构函数常常被自动调用在析构函数中抛出的异常往往会难以捕获引发程序非正常退出或未定义行为。另外我们都知道在容器析构时会逐个调用容器中的对象析构函数而某个对象析构时抛出异常还会引起后续的对象无法被析构导致资源泄漏。资源可以是内存也可以是数据库连接或者其他类型的计算机资源。析构函数是由C来调用的源代码中不包含对它的调用因此它抛出的异常不可被捕获。 如果析构函数中真的可能存在异常需要直接在析构函数中捕获而不能向外抛出。 7、C中的深拷贝和浅拷贝 浅拷贝是一种简单的拷贝方式仅仅是复制对象的基本类型成员和指针成员的值而不复制指针所指向的内存。这可能会导致两个对象共享相同的资源从而引发潜在的问题如内存泄漏、意外修改共享资源等。一般来说编译器默认帮我们实现的拷贝构造函数就是一种浅拷贝。POD类型的数据就适合浅拷贝。 深拷贝不仅复制对象的基本类型成员还复制指针所指向的内存。因此两个对象不会共享相同的资源避免了潜在问题。深拷贝通常需要显式实现拷贝构造函数和赋值运算符重载。 写出深拷贝和浅拷贝的代码。面试题 #include iostream #include cstring class DataArray { private:     double* data;     size_t size; public:     // 构造函数     DataArray(size_t _size, double* _data) : size(_size) {         data new double[size];         std::memcpy(data, _data, size * sizeof(double));     }     // 拷贝构造函数 - 深拷贝     DataArray(const DataArray other) : size(other.size) {         data new double[size];         std::memcpy(data, other.data, size * sizeof(double));     }     // 拷贝构造函数 - 浅拷贝     // 该版本拷贝构造函数只是复制指针没有创建新的内存副本     DataArray(const DataArray other) : size(other.size), data(other.data) {}     // 析构函数     ~DataArray() {         delete[] data;     }     // 输出数组内容     void printArray() {         for (size_t i 0; i size; i) {             std::cout data[i] ;         }         std::cout std::endl;     } }; 8、this指针delete this会发生什么 this指针是一个指向当前对象的指针。在类的成员函数中访问类的成员变量或者调用成员函数时编译器会隐式地将当前对象的地址为this指针传递给成员函数。因此this 指针可以用来访问类的成员变量和成员函数以及在成员函数中引用当前对象。在常量成员函数const member function中this 指针的类型是指向常量对象的常量指针const pointer to const object因此不能用来修改成员变量的值。 在C中static 函数是一种静态成员函数它与类本身相关而不是与类的对象相关。大家可以将 static 函数视为在类作用域下的全局函数而非成员函数。因为静态函数没有 this 指针所以它不能访问任何非静态成员变量。如果在静态函数中尝试访问非静态成员变量编译器会报错。 在析构函数中调用delete this会导致堆栈溢出。原因很简单delete的本质是“为将被释放的内存调用一个或多个析构函数然后释放内存”。显然delete this会去调用本对象的析构函数而析构函数中又调用delete this形成无限递归造成堆栈溢出系统崩溃。 在成员函数中调用delete this会将类对象的内存空间被释放。在delete this之后进行的任何函数调用只要不涉及到this指针的内容都能正常运行。一旦涉及到this指针比如操作成员函数、调用虚函数等。就会出现不可预期的问题。 9、C多态的实现方式 多态是面向对象的三大特性之一。是指同一个函数或者操作在不同的对象上有不同的表现形式。C实现多态的方式主要有函数重载和虚函数重写。 前者是静态多态是指在同一个作用域多个重名的全局函数或者多个同名的类成员函数中有1个以上函数名称相同但是函数参数的类型或者个数不一致的函数在调用函数的地方通过传递的实参类型或者个数就能在程序编译期间就能确定具体调用哪个一个函数。此外模板函数也是一种静态多态的实现方式因为模板也是在编译器期间根据具体的调用确定了具体的类型。 后者是动态多态是指在类继承的关系中基类存在虚函数或者纯虚函数而派生类重写了基类的虚函数或者纯虚函数当使用基类的指针或者引用去指向派生类对象时可以调用到子类重写的函数。所以动态多态必须满足两个条件第一就是基类的指针或者引用调用虚函数第二就是被调用的是虚函数且派生类完成了对基类虚函数的重写。 10、动态多态的实现原理 C的动态多态是通过虚函数实现的。当基类指针或者引用指向一个派生类对象时调用虚函数时实际上会调用派生类中的虚函数而不是基类中的虚函数。 在底层当一个类声明一个虚函数时编译器会为该类创建一个虚函数表并且会给类插入一个vptr虚函数表指针的字段。虚函数表存储该类的虚函数指针这个指针指向实际实现该虚函数的代码地址。每个对象都包含一个指向该类的虚函数表的虚函数表指针vptr这个指针在对象构造时与该类的虚函数表绑定通常是作为对象的第一个成员变量。 当调用一个虚函数时编译器会通过对象的虚函数指针查找到该对象所属的类的虚函数表并根据函数的索引值通常是函数在表中的位置编译时就能确定来找到对应的虚函数地址。然后将控制转移到该地址实际执行该函数的代码。 对于派生类其虚函数表也是继承自基类的虚函数表然后根据派生类自身虚函数重写的情况来更新继承的这张虚函数表派生类的表继承之后也独立与基类将重写之后的虚函数的地址更新继承的虚函数表中对应的项。 11、补充C对象模型 虚函数Virtual Function是通过一张虚函数表Virtual Table来实现的简称为V-Table。在这个表中存放的是一个类的虚函数的地址表这张表解决了继承、覆盖的问题保证其真实反应实际的函数。 这样这个类的实例内存中都有一个虚函数表的指针所以当我们用父类的指针来操作一个子类的时候这张虚函数表就显得由为重要了它就像一个地图一样指明了实际所应该调用的函数。 在上面的示例中意思就是一个对象在内存中一般由成员变量非静态、虚函数表指针(vptr)构成。 虚函数表指针指向一个数组数组的元素就是各个虚函数的地址通过函数的索引我们就能直接访问对应的虚函数。 12、纯虚函数是什么能被实例化吗为什么 纯虚函数是一种在基类中声明但没有实现的虚函数。它的作用是定义了一种接口这个接口需要由派生类来实现。PS: C 中没有接口纯虚函数可以提供类似的功能。包含纯虚函数的类称为抽象类Abstract Class。抽象类仅仅提供了一些接口但是没有实现具体的功能。作用就是制定各种接口通过派生类来实现不同的功能从而实现代码的复用和可扩展性。另外抽象类无法实例化也就是无法创建对象。纯虚函数没有函数体不是完整的函数无法调用也无法为其分配内存空间。 带有纯虚函数的基类被称为抽象类或者接口它要求继承它的子类实现对应纯虚函数。纯虚函数没有函数体如果用它直接实例化对象将无法确定如何执行这些函数所以不能直接创建对象。 13、构造函数不能是虚函数 从语法层面上 虚函数的主要目的是实现多态即允许在派生类中覆盖基类的成员函数。但是构造函数负责初始化类的对象每个类都应该有自己的构造函数。在派生类中基类的构造函数会被自动调用用于初始化基类的成员。因此构造函数没有被覆盖的必要不需要使用虚函数来实现多态 从虚函数表机制上 虚函数使用了一种称为虚函数表vtable的机制。然而在调用构造函数时对象还没有完全创建和初始化所以虚函数表可能尚未设置。这意味着在构造函数中使用虚函数表会导致未定义的行为。只有执行完了对象的构造虚函数表才会被正确的初始化。 总之将构造函数设置为虚函数编译器就会报错。 14、为什么C基类析构函数需要是虚函数 析构函数的作用析构函数是进行类的清理工作比如释放内存、关闭DB链接、关闭Socket等等。 为什么当使用动态多态时如果派生类中增加了新的字段并且这个字段是指针类型那么在析构函数中需要释放这个字段的内存如果基类不声明析构函数为虚析构函数那么在调用析构函数时就会和成员函数一样会直接调用父类的构造函数而不会调用子类自身的所以无法释放这个资源。为此需要将基类析构定义为虚析构。在释放对象时会先调用子类的析构然后调用父类的析构。 15、友元函数和友元类 友元提供了不同类的成员函数之间类的成员函数和一般函数之间进行数据共享的机制。 通过友元另一个类中的成员函数可以访问类中的私有成员和保护成员。友元的正确使用能提高程序的运行效率但同时也破坏了类的封装性和数据的隐藏性导致程序可维护性变差。 1友元函数 友元函数是可以访问类的私有成员的非成员函数。它是定义在类外的普通函数不属于任何类但是需 要在类的定义中加以声明。 friend 类型 函数名(形式参数)一个函数可以是多个类的友元函数只需要在各个类中分别声明。 2友元类 友元类的所有成员函数都是另一个类的友元函数都可以访问另一个类中的隐藏信息包括私有成员和 保护成员。友元类声明friend class 类名; 使用友元类时继承关系不能被继承友元关系是单向的不具有交换性。如果类B是类A的友元类A不一定是类B的友元需要看具体的友元声明情况友元关系不具有传递性。若是类B是类A的友元类C是B的友元类C不一定是类A的友元。 16、为什么友元函数必须在类内部声明 因为编译器必须能够读取这个结构的声明以理解这个数据类型的行为等方面的所有规则。有一条 规则在任何关系中都很重要那就是谁可以访问我的私有部分。 17、explicit关键字的作用 在C中explicit通常用于构造函数的声明中用于防止隐式转换。当将一个参数传递给构造函数时如果构造函数声明中使用了explicit关键字则只能使用显示转换进行转换而不能使用隐式转换。这种机制可以防止编译器自动执行预期外的类型转换提高代码安全性。 结论Google编码规范 在类型定义中类型转换运算符和单参数构造函数都应用explicit进行标记一个例外是拷贝和移动构造函数不应当被标记因为他们并不执行类型转换。对于涉及目的就是用于对其他类型进行透明包装的类来说隐式类型转换有时是必要且合适的。不能以一个参数进行调用构造函数不应当加上explicit。接受一个std::initializer_list作为参数的构造函数也应当省略explicit以便支持拷贝初始化例如MyTpye m {1, 2}。 18、final和override关键字 override修饰的函数是指子类重写了父类的虚函数可以有效防止开发者在子类中写错了重写的函数签名。如果写错了编译期间就能够提示出来。 final可以修饰类和虚函数。如果希望某个类不被其他类继承可以使用final对类进行修饰class A final {}其次如果希望某个虚函数不被子类重写可以使用final修饰。 19、初始化和赋值的区别 对于简单数据类型初始化和赋值没什么区别。对于类和复杂类型有很大的区别。赋值运算符会将传入的对象的成员数据给被赋值的对象。 20、什么时候会调用拷贝构造函数 1、用一个类对象去创建另一个对象A a; A b(a) 2、用一个类对象初始化类一个对象 A a; A b a 3、函数参数是类void t(A a)在调用函数时调用的拷贝构造。 21、组合知道吗与继承相比有什么优缺点 1、继承。继承的优点是子类可以重写父类的方法来方便实现对父类的扩展。但是有以下几个缺点父类的内部细节对子类是可见的子类从父类继承的方法在编译时就确定下来了所以无法在运行期间改变从父类继承的方法的行为如果对父类的方法做了修改的话则子类的方法必须做出相对应的修改。所以子类与父类是一种高耦合。 2、组合。组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。组合的优点①当前对象只能通过所包含的那个对象去调用其方法所以所包含的对象的内部细节对当前对象时不可见的。②当前对象与包含的对象是一个低耦合关系如果修改包含对象的类中代码不需要修改当前对象类的代码。③当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。组合的缺点①容易产生过多的对象。②为了能组合多个对象必须仔细对接口进行定义。 22、成员函数里memset(this, 0, sizeof(*this))会发生什么 1、有时候类里面定义了很多int,char,struct等c语言里的那些类型的变量我习惯在构造函数中将它们初始化为0但是一句句的写太麻烦所以直接就memset(this, 0, sizeof *this);将整个对象的内存全部置为0。对于这种情形可以很好的工作但是下面几种情形是不可以这么使用的 2、类含有虚函数表这么做会破坏虚函数表后续对虚函数的调用都将出现异常 3、类中含有C类型的对象例如类中定义了一个list的对象由于在构造函数体的代码执行之前就对list对象完成了初始化假设list在它的构造函数里分配了内存那么我们这么一做就破坏了list对象的内存。 23、类的对象存储空间 类对象的大小包括非静态成员的数据类型大小之和编译器加入的额外成员变量比如指向虚函数表的指针为了字节对齐而加入的新的字节。 空类大小为1个字节保证创建的每个对象都有不同的地址当作为基类是大小为0。 24、C中类的数据成员和成员函数内存分布情况 一个类对象的地址就是类所包含的这一片内存空间的首地址这个首地址也就对应具体某一个成员变量的地址。对象的大小和对象中的数据成员的大小是一致的也就是说成员函数不占用对象的内存静态成员也不占用对象的内存。所有的函数都放在代码区不管是全局函数还是成员函数还有静态成员函数也放在代码区。 静态成员函数与一般成员函数的唯一区别是没有this指针因此不能访问非静态数据成员。 25、关于this指针你知道什么 1、说明 this指针是类的指针指向对象的首地址。 this指针只能在成员函数中使用在全局函数、静态成员函数中都不能用this。 this指针只有在成员函数中才有定义且存储位置会因为编译器不同有不同的存储位置。 2、this指针的用处 一个对象的this指针并不是对象本身的一部分不会影响 sizeof(对象) 的结果。this作用域是在类内部当在类的非静态成员函数中访问类的非静态成员的时候全局函数静态函数中不能使用this指针编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说即使你没有写上this指针编译器在编译的时候也是加上this的它作为非静态成员函数的隐含形参对各成员的访问均通过this进行。 3、this指针的使用 一种情况就是在类的非静态成员函数中返回类对象本身的时候直接使用 return *this 另外一种情况是当形参数与成员变量名相同时用于区分如this-n n 不能写成n n。 4、this指针的特点 第一、this只能在成员函数中使用全局函数静态函数都不能使用this。实际上传入参数为当前对象地址成员函数第一个参数为 T * const this。比如成员函数 int func(int p){ }从编译器的角度来看应该是int func(A * const this, int p);。 第二、this在成员函数的开始前构造在成员函数的结束后清除。这个生命周期同任何一个函数的参数是一样的没有任何区别。当调用一个类的成员函数时编译器将类的指针作为函数的this参数传递进去。A a; a.fun(1);  A::func(a, 10)。; 26、几个this指针的易混问题 Athis指针是什么时候创建的 this指针在成员函数的开始执行前构造在成员的执行结束后清除。 Bthis指针存放在何处堆栈、全局还是其他 this指针会因为编译器不同而有不同的放置位置。可能是栈也可能是寄存器设置全局变量。在汇编级别里面一个值只会以3种形式出现立即数、寄存器值和内存变量值。不是存放在寄存器就是存放在内存中他们并不是和高级语言变量对应的。 C每个类编译后是否创建一个类中函数表保存函数指针以便用来调用函数 普通的类函数不论是成员函数还是静态函数都不会创建一个函数表来保存函数指针。只有虚函数才会被放到函数表中。但是即使是虚函数如果编译期就能明确知道调用的是哪个函数编译器就不会通过函数表中的指针来间接调用而是会直接调用该函数。正是由于this指针的存在用来指向不同的对象从而确保不同对象之间调用相同的函数可以互不干扰。 27、this指针调用成员函数时堆栈会发生什么变化 当在类的非静态成员函数访问类的非静态成员时编译器会自动将对象的地址传给作为隐含参数传递给函数这个隐含参数就是this指针。 即使你并没有写this指针编译器在链接时也会加上this的对各成员的访问都是通过this的。 例如你建立了类的多个对象时在调用类的成员函数时你并不知道具体是哪个对象在调用此时你可以通过查看this指针来查看具体是哪个对象在调用。This指针首先入栈然后成员函数的参数从右向左进行入栈最后函数返回地址入栈。 28、基类的虚函数表存放在内存的什么区虚函数表指针vptr的初始化时间 虚函数表的特征 虚函数表是全局共享的元素即全局仅有一个在编译时就构造完成。虚函数表类似一个数组类对象中存储vptr指针指向虚函数表即虚函数表不是函数不是程序代码不可能存储在代码段。虚函数表存储虚函数的地址即虚函数表的元素是指向类成员函数的指针而类中虚函数的个数在编译时就确定了即虚函数表的大小可以在编译时期确定不必动态分配内存空间存储虚函数表所以不在堆中。 由于虚函数表指针vptr和虚函数密不可分对于有虚函数或者继承于用于虚函数的基类对该类进行实例化时在构造函数执行时会对虚函数表指针进行初始化并且存在内存布局的前面也就是vptr这个隐含成员在其他成员变量之前。 C中虚函数表位于只读数据段.rodata也就是C内存模型中的常量区而虚函数则位于代码段.text也就是C内存模型中的代码区。 29、模板函数和模板类的特例化 引入原因编写单一的模板它能适应多种类型的需求使每种类型都具有相同的功能但对于某种特定类型如果要实现其特有的功能单一模板就无法做到这时就需要模板特例化。 1、函数模板特例化 必须为原函数模板的每个模板参数都提供实参且使用关键字template后跟一个空尖括号对表明将原模板的所有模板参数提供实参。 本质特例化的本质是实例化一个模板而非重载它。特例化不影响参数匹配。参数匹配都以最佳匹配为原则。例如此处如果是compare(3,5)则调用普通的模板若为compare(“hi”,”haha”)则调用特例化版本因为这个cosnt char*相对于T更匹配实参类型注意二者函数体的语句不一样了实现不同功能。 2、类模板特例化 类模板可以全部特例化也可以部分特例化。 30、构造函数、析构函数、虚函数可否声明为内联函数 将构造函数和析构函数声明为内联函数是没有意义的编译器并不真正对声明为inline的构造和析构函数进行内联操作因为编译器会在构造和析构函数中添加额外的操作申请、释放内存等致使构造函数和析构函数没有表面看上去那么精简。其次类中的函数默认是inline内联的编译器也只是有选择性的内联所以将构造函数和析构函数声明为内联函数没什么意义。 如果虚函数在编译期间就能决定将要调用哪个函数时就能够内联。也就是不具备多态性的时候如果虚函数比较简短那么就能让内联生效。 31、C模板是什么 在C中模板Templates是一种通用编程工具允许编写通用的代码以适应多种不同的数据类型或数据结构。模板使得可以编写不特定于特定数据类型的代码从而提高代码的重用性和灵活性。模板在STL标准模板库中广泛使用例如容器如向量、列表、映射等和算法如排序、查找等。 编译器并不是把函数模板处理成能够处理任意类的函数编译器从函数模板通过具体类型产生不同的函数编译器会对函数模板进行两次编译在声明的地方对模板代码本身进行编译在调用的地方对参数替换后的代码进行编译。 32、构造函数和析构函数可以调用虚函数吗为什么 1、在C中提倡不在构造函数和析构函数中调用虚函数 2、构造函数和析构函数调用虚函数时都不使用动态联编如果在构造函数或析构函数中调用虚函数则运行的是为构造函数或析构函数自身类型定义的版本 3、因为父类对象会在子类之前进行构造此时子类部分的数据成员还未初始化因此调用子类的虚函数时不安全的故而C不会进行动态联编 4、析构函数是用来销毁一个对象的在销毁一个对象时先调用子类的析构函数然后再调用基类的析构函数。所以在调用基类的析构函数时派生类对象的数据成员已经销毁这个时候再调用子类的虚函数没有任何意义。 33、构造函数一般不定义为虚函数的原因 1创建一个对象时需要确定对象的类型而虚函数是在运行时动态确定其类型的。在构造一个对象时由于对象还未创建成功编译器无法知道对象的实际类型。 2虚函数的调用需要虚函数表指针vptr而该指针存放在对象的内存空间中若构造函数声明为虚函数那么由于对象还未创建还没有内存空间更没有虚函数表vtable地址用来调用虚构造函数了。 3虚函数的作用在于通过父类的指针或者引用调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的不可能通过父类或者引用去调用因此就规定构造函数不能是虚函数。 34、构造函数的几种关键字 default关键字可以显式要求编译器生成构造函数防止在调用时相关构造函数类型没有而报错。 delete关键字可以删除构造函数、赋值运算符函数等。 35、构造函数、拷贝构造函数和赋值运算符的区别 构造函数对象不存在没有用别的对象初始化在创建一个新的对象时调用构造函数。 拷贝构造函数对象不存在但是使用别的已经存在的对象进行初始化。 赋值运算符对象存在用别的对象给它赋值这属于重载“”号运算符的范畴“”号两侧的对象都是已存在的。 36、虚函数的代价是什么 1、带有虚函数的类每一个类会产生一个虚函数表用来存储指向虚成员函数的指针增大了类 2、带有虚函数的类的每一个对象都会有有一个指向虚表的指针会增加对象的空间大小 3、不能再是内联的函数因为内联函数在编译阶段进行替代而虚函数表示等待在运行阶段才能确定到低是采用哪种函数虚函数不能是内联函数。 三、STL 1、STL的介绍说说STL的基本组成部分 STL是标准模板库是C的标准库之一一套基于模板的容器类库还包括许多常用的算法提高了程序开发的效率和复用性。STL包含6大部件容器、迭代器、算法、仿函数、适配器和空间配置器。 容器 是一种数据结构 如list, vector, 和deques以模板类的方法提供。为了访问容器中的数据可以使用由容器类输出的迭代器。 算法   是用来操作容器中的数据的模板函数。例如STL用sort()来对一 个vector中的数据进行排序用find()来搜索一个list中的对象 函数本身与他们操作的数据的结构和类型无关因此他们可以用于从简单数组到高度复杂容器的任何数据结构上。 迭代器 提供了访问容器中对象的方法。例如可以使用一对迭代器指定list或vector中的一定范围的对象。 迭代器就如同一个指针。事实上C 的指针也是一种迭代器。 但是迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象; 仿函数 仿函数又称之为函数对象 其实就是重载了操作符的 ( ) 没有什么特别的地方。 适配器     简单的说就是一种接口类专门用来修改现有类的接口提供一中新的接口或调用现有的函数来实现所需要的功能。主要包括3中适配器Container Adaptor、Iterator Adaptor、Function Adaptor。 空间配置器   为STL提供空间配置的系统。其中主要工作包括两部分 1对象的创建与销毁 2内存的获取与释放。 2、vector容器的底层原理 vector底层是一个动态数组包含三个迭代器start和finish之间是已经被使用的空间范围end_of_storage是整块连续空间包括备用空间的尾部。 当空间不够装下数据vec.push_back(val)时会自动申请另一片更大的空间1.5倍Windows或者2倍Linux然后把原来的数据拷贝到新的内存空间接着释放原来的那片空间【vector内存增长机制】。当释放或者删除vec.clear()里面的数据时其存储空间不释放仅仅是清空了里面的数据。 对vector的任何操作一旦引起了空间的重新配置指向原vector的所有迭代器都会失效。 3、vector中的reseve和resize的区别 reserve是直接扩充容器到确定的大小可以减少多次开辟、释放空间的问题可以有效提高效率。reserve只是保证了vector容器中的空间大小也就是容量最少达到参数所指定的大小reserve只有一个参数是新的容量大小。如果目前容器中已经有了3个数据此时用reserve设置为2将是没有效果的。 resize不仅仅是改变容量还会给扩充的位置赋初始值。也就是容量和大小都会被改变。所以当设置之后可以通过size和capacity函数获取容量和大小两者都是一样的。 4、vector中的size和capacity的区别 size表示当前vector中有多少个元素start-finish而capacity函数则表示他已经分配的内存中可以容纳多少个元素。 5、vector容器中能存放引用吗 不能。引用不支持一般意义上的赋值操作而vector中的元素有两个要求元素必须能赋值元素必须能赋值。 6、vector迭代器失效的情况 插入或者删除某个元素时会导致该元素后面的所有元素向前或者向后移动一个位置。erase和insert方法会返回下一个有效的迭代器和当前插入位置的迭代器以解决迭代器失效的问题。需要注意的是如果size capcity的情况下插入迭代器并不会全部失效通过原来的迭代器还是可以实现相对应位置数据的获取如果出现重新分配内存的情况迭代器会全部失效。 7、vector内存相关的函数clear、swap、shrink_to_fit() vec.clear() 清空内容但是不释放内存。也就是size0capacity不变。 vectorint().swap(vec)清空内容且释放内存得到新的vector。即sizecapacity0 vec.shrink_to_fit() //请求降低size和capacity的匹配。 vec.clear();vec.shrink_to_fit();清空内容且释放内存。 8、list的底层原理 list的底层是一个双向链表以结点为单位存放数据结点的地址在内存中不一定连续每次插入或删除一个元素就配置或释放一个元素空间。list不支持随机存取如果需要大量的插入和删除而不关系随机存取则比较适合这种数据结构。 9、deque的底层原理 deque是一个双向开口的连续线性空间双端队列在头尾两端进行元素的插入跟删除操作都有理想的时间复杂度。 10、什么情况下用vectorlist和deque vector可以随机存储元素即可以通过公式直接计算出元素地址而不需要挨个查找但在非尾部插入删除数据时效率很低适合对象简单对象数量变化不大随机访问频繁。除非必要尽可能选择使用vector而非deque因为deque的迭代器比vector迭代器复杂很多。 list不支持随机存储适用于对象大对象数量变化频繁插入和删除频繁比如写多读少的场景。 需要从首尾两端进行插入或删除操作的时候需要选择deque。 11、priority_queue的底层原理 priority_queue优先队列其底层是用堆来实现的。在优先队列中队首元素一定是当前队列中优先级最高的那一个。优先队列具备队列的所有特性包括基本操作只是在这个基础上添加了内部的一个排序。 12、map、set、multiset和multimap的底层原理 map 、set、multiset、multimap的底层实现都是红黑树。 13、map、set、multiset和multimap的特点 set和multiset会根据特定的排序准则自动将元素排序set中元素不允许重复multiset可以重复。 map和multimap将key和value组成的pair作为元素根据key的排序准则自动将元素排序因为红黑树也是二叉搜索树所以map默认是按key排序的map中元素的key不允许重复multimap可以重复。 map和set的增删改查速度为都是logn是比较高效的。 14、为何map和set的插入删除效率比其他序列容器高而且每次insert之后以前保存的iterator不会失效 因为存储的是结点不需要内存拷贝和内存移动。 因为插入操作只是结点指针换来换去结点内存没有改变。而iterator就像指向结点的指针内存没变指向内存的指针也不会变。 15、为何map和set不能像vector一样有个reserve函数来预分配数据? 因为在map和set内部存储的已经不是元素本身了而是包含元素的结点。也就是说map内部使用的Alloc并不是mapKey, Data, Compare, Alloc声明的时候从参数中传入的Alloc。 16、unordered_map、unordered_set的底层原理 这两个容器是使用哈希表作为底层实现的提供了高效的查找、插入和删除操作。底层哈希表是一个数组每隔元素称为桶每个元素都是不同的插入相同元素无效。每个桶可以存储一个或者多个元素其中每个元素由键值对组成unordered_map是键值对key-valueunordered_set是只有键值。通过将键的哈希值映射到对应的桶可以快速定位元素。在桶中使用开放地址发和拉链法解决计算出来的哈希值的冲突问题。 17、unordered_map和map的区别使用场景 区别 内部实现是不同的unordered_map使用哈希表作为底层实现而map使用红黑树作为底层实现。哈希表具有平均0(1)的查找插入和删除操作红黑树则是0(logn)。元素顺序是不同的unordered_map中的键值对没有特定的顺序而map中的键值对按照键的比较顺序进行排序。效率由于哈希表的特性unordered_map的平均情况下提供了更快的查找、插入和删除操作。但是在最坏的情况下冲突很多性能可能下降而红黑树比较稳定。 场景 如果需要高效的查找操作而不关心元素的顺序可以选择unordered_map如果需要元素有序并且对性能要求不严格可以选择map如果对性能要求非常严格并且不关心元素顺序首选unordered_map。 unordered_map什么时候扩容 当向容器添加元素的时候会判断当前容器的元素个数如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候就要自动扩容。扩容需要重新计算容量。 18、迭代器种类 1、输入迭代器是只读迭代器在每个被遍历的位置上只能读取一次。 2、输出迭代器是只写迭代器在每个被遍历的位置上只能被写一次。• 3、前向迭代器兼具输入和输出迭代器的能力但是它可以对同一个位置重复进行读和写。但它不支持operator–所以只能向前移动。 4、双向迭代器很像前向迭代器只是它向后移动和向前移动同样容易。 5、随机访问迭代器有双向迭代器的所有功能。而且它还提供了“迭代器算术”即在一步内可以向前或向后跳跃任意位置 包含指针的所有操作可进行随机访问随意移动指定的步数。支持前面四种Iterator的所有操作并另外支持it n、it - n、it n、 it - n、it1 - it2和it[n]等操作。 19、说说push_back和emplace_back的区别 如果要将一个临时变量push到容器的末尾push_back()需要先构造临时对象再将这个对象拷贝到容器的末尾而emplace_back()则直接在容器的末尾构造对象这样就省去了拷贝的过程。 20、vector与list的区别和应用怎么找到vector或者list的倒数第二个元素 1、vector的随机访问效率高但在插入和删除时不包括尾部需要挪动数据不易操作。 2、list的访问要遍历整个链表它的随机访问效率低。但对数据的插入和删除操作等都比较方便改变指针的指向即可。 3、从遍历上来说list是单向的vector是双向的。 4、vector中的迭代器在使用后就失效了而list的迭代器在使用之后还可以继续使用。 list不提供随机访问所以不能用下标直接访问到某个元素要访问list里的元素只能遍历可以用反向迭代器遍历。 21、vector越界访问下标map越界访问下标vector删除元素时会不会释放空间 1、通过下标访问vector中的元素时会做边界检查如果超出很大可能导致程序崩溃。 2、map的下标运算符[ ]的作用是将键作为下标去执行查找并返回相应的值如果不存在这个键就将一个具有该key和value的值插入到这个map中。 erase删除某一个元素只会删除内容不会改变容器的容量。 22、map中的 [ ] 和find的区别 1、map的下标运算符[ ]的作用是将关键码作为下标去执行查找并返回对应的值如果不存在这个关键码就将一个具有该关键码和值类型的默认值项插入到这个map。 2、map的find函数用关键码执行查找找到了返回该位置的迭代器如果不存在这个关键码就返回尾迭代器。 四、C新特性 1、说一下C的左值引用和右值引用 1、什么是左值什么是右值 左值指在内存中给有明确存储地址的数据可以用运算符取地址。 右值指在内存中可以提供的不可以取地址的字面量或者临时对象。右值可以分为将亡值和纯右值。将亡值是与右值引用相关的表达式比如右值引用类型函数的返回值、move移动函数的返回值。纯右值是非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等等。 2、左值引用与右值引用 左值引用是对左值的引用要求初始化时右边的值是可以取地址的如果无法取地址就必须使用常引用。 右值引用是用来绑定到右值的绑定右值以后本来被销毁的右值的生存周期会延长到绑定到它的右值引用的生存期。 3、右值引用作用 右值引用的存在并不是为了取代左值引用而是充分利用右值特别是临时对象来减少对象构造和析构的操作次数以达到提高效率的目的。 带右值引用参数的拷贝构造和赋值函数叫做移动构造函数和移动赋值函数这里的移动指的是临时量资源转移给当前对象临时对象将不再持有这个资源。 在 C 中并不是所有情况下 都代表是一个右值引用具体的场景体现在模板和自动类型推导中。auto或者函数参数类型自动推导的T是一个未定的引用类型它可能是左值引用也可能是右值引用类型这取决于初始化的值类型。通过右值推导 T 或者 auto 得到的是一个右值引用类型其余都是左值引用类型。 4、完美转发是什么什么场景下用到完美转发 完美转发指的是函数模板可以将自己的参数完美转发给内部调用的其他函数。所谓完美是指不仅能够准确转发参数的值还能保证被转发参数的左右值属性不变。 forwardT(t) : 当T为左值引用类型时t将被转换为T类型的左值。当T不是左值引用类型时t将被转换为T类型的右值。 2、说说C11的新特性有哪些 C新特性主要包括包含语法改进和标准库扩充两个方面主要包括以下11点 语法的改进 1统一的初始化方法。增大了初始化列表的适用性可以用于任何类型的对象。 2成员变量默认初始化。构造一个类的对象不需要用构造函数初始化成员变量。 3auto关键字。用于定义变量编译器可以自动判断的类型前提定义一个变量时对其进行初始化 4decltype求表达式的类型在编译期间自动类型推导。 5智能指针 shared_ptr。使用RAII机制封装的一个类模板帮助管理指针类型。 6空指针 nullptr原来NULL。nullptr专门用于初始化空类型指针可以避免NULL的弊端。 7基于范围的for循环。 8右值引用和move语义。让程序员有意识减少进行深拷贝操作。 标准库扩充往STL里新加进一些模板类比较好用 9无序容器哈希表    用法和功能同map一模一样区别在于哈希表的效率更高。 10正则表达式。可以认为正则表达式实质上是一个字符串该字符串描述了一种特定模式的字符串。 11Lambda表达式。lambda 表达式定义了一个匿名函数并且可以捕获一定范围内的外部变量。可以在需要的时间和地点实现功能的就地闭包使得程序更加灵活。 3、说说C中智能指针和指针的区别是什么 所有权管理 普通指针不会自动释放内存需要手动给调用delete或delete [ ] 来释放。而智能指针会自动管理所指向的对象的内存当智能指针超出作用域或被显式释放时会自动调用delete或者delete [ ] 来释放内存。 多线程安全普通指针不提供多线程安全的保证如果多个线程同时访问同一个指针可能会导致竞态条件。而智能指针可以通过引用计数或其他机制来保证多线程安全。 拷贝和赋值普通指针可以随意拷贝和赋值这可能会导致多个指针指向同一个内存地址造成内存泄漏或悬空指针。而智能指针可以通过禁止拷贝和赋值或使用引用计数等机制来避免这种问题。 智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制区别是它负责自动释放所指的对象这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。智能指针相比普通指针更加安全和方便可以避免内存泄漏、悬空指针和竞态条件等问题。但是智能指针也有一些缺点例如可能会增加程序的开销和复杂度需要谨慎使用。 4、说说C中的智能指针有哪些分别解决的问题以及区别 C中的智能指针有4种分别为shared_ptr、unique_ptr、weak_ptr、auto_ptr其中auto_ptr被C11弃用。 使用智能指针的原因申请的空间即new出来的空间在使用结束时需要delete掉否则会内存泄漏。在程序运行期间new出来的对象在析构函数中delete掉但是这种方法不能解决所有问题因为有时候new发生在某个全局函数里面该方法会给程序员造成精神负担。此时智能指针就派上了用场。使用智能指针可以很大程度上避免这个问题因为智能指针就是一个类当超出了类的作用域时类会自动调用析构函数析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间避免了手动释放内存空间。 1、为什么使用智能指针 主要目的是为了更安全的管理内存防止内存泄漏。 智能指针是一个类模板用于管理普通的指针它采用了资源获取即初始化的原则当智能指针对象超出作用域的时候就会自动析构释放管理的指针或者减少引用计数。 2、auto_ptr智能指针 是C98引入的他是一个独占式的智能指针只能有一个智能指针对某个普通指针进行管理如果同时交给多个智能指针那么在超出作用域释放的时候会释放多次导致程序运行时崩溃此外如果将一个智能指针对象赋值给另一个就会触发所有权的转移被转移的智能指针在当前作用域就失效了如果再次引用获取数据就会导致程序崩溃。所以这个智能指针存在安全问题容易导致程序运行崩溃。 3、unique_ptr独占智能指针 类似于auto_ptr智能指针是一个独占的智能指针同一时间只能有一个智能指针管理普通的指针对象它比auto智能指针更加安全因为它是禁止拷贝操作的以此来保证独占。 4、share_ptr共享智能指针 共享智能指针是一种共享所有权的智能指针它允许多个智能指针指向同一个对象并使用引用计数的方式来管理指向对象的指针这个引用计数在多个智能指针对象之间也是一个共享数据。当某个智能指针对象创建出来了引用计数1销毁就-1。并判断-1之后引用计数是否为0如果是就需要销毁被管理的普通指针并且将引用计数这个指针指向的空间也销毁。 5、weak_ptr弱指针 弱指针是一种不控制对象生命周期的智能指针它指向一个share_ptr管理的对象进行该对象的内存管理的是共享智能指针所以弱指针不会改变引用计数只是提供了一种访问其管理对象的手段。用于防止share_ptr出现的循环引用导致内存泄漏的情况。 5、说说智能指针的特点 智能指针的作用是管理一个指针因为存在申请的空间在函数结束时忘记释放造成内存泄漏的情况。使用智能指针可以很大程度上避免这个问题因为智能指针就是一个类当超出了类的作用域时类会自动调用析构函数自动释放资源。 1auto_ptr auto指针存在的问题是两个智能指针同时指向一块内存就会两次释放同一块资源自然报错。 2unique_ptr unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存编译报错。 实现原理将拷贝构造函数和赋值拷贝构造函数申明为private或delete。不允许拷贝构造函数和赋值操作符但是支持移动构造函数通过std:move把一个对象指针变成右值之后可以移动给另一个unique_ptr 3shared_ptr 共享指针可以实现多个智能指针指向相同对象该对象和其相关资源会在引用为0时被销毁释放。 实现原理有一个引用计数的指针类型变量专门用于引用计数使用拷贝构造函数和赋值拷贝构造函数时引用计数加1当引用计数为0时释放资源。 注意weak_ptr、shared_ptr存在一个问题当两个shared_ptr指针相互引用时那么这两个指针的引用计数不会下降为0资源得不到释放。因此引入weak_ptrweak_ptr是弱引用weak_ptr的构造和析构不会引起引用计数的增加或减少。 6、weak_ptr能不能知道对象计数为0为什么 不能它获取的引用计数是shared_ptr的引用计数。 weak_ptr是一种不控制对象生命周期的智能指针它指向一个shared_ptr管理的对象。进行该对象管理的是那个引用的shared_ptr。weak_ptr只是提供了对管理 对象的一个访问手段。weak_ptr设计的目的只是为了配合shared_ptr而引入的一种智能指针配合shared_ptr工作它只可以从一个shared_ptr或者另一个weak_ptr对象构造它的构造和析构不会引起计数的增加或减少。 7、weak_ptr如何解决shared_ptr的循环引用问题 为了解决循环引用导致的内存泄漏引入了弱指针weak_ptrweak_ptr的构造函数不会修改引用计数的值从而不会对对象的内存进行管理其类似一个普通指针但是不会指向引用计数的共享内存。它可以检测到所管理的对象是否已经被释放从而避免非法访问。 8、shared_ptr、unique_ptr和weak_ptr的自定义实现 9、shared_ptr怎么知道跟它共享对象的指针释放了 多个shared_ptr对象可以同时托管一个指针系统会维护一个托管计数。当无shared_ptr托管该指针时delete该指针。 10、智能指针有没有内存泄漏的情况 智能指针有内存泄露的情况发生。 智能指针发生内存泄露的情况 à 当两个对象同时使用一个shared_ptr成员变量指向对方会造成循环引用使引用计数失效从而导致内存泄露。 智能指针的内存泄漏如何解决 à 为了解决循环引用导致的内存泄漏引入了弱指针weak_ptrweak_ptr的构造函数不会修改引用计数的值从而不会对对象的内存进行管理其类似一个普通指针但是不会指向引用计数的共享内存但是可以检测到所管理的对象是否已经被释放从而避免非法访问。 11、说说C11中四种类型转换 C中四种类型转换分别为const_cast、static_cast、dynamic_cast、reinterpret_cast四种转换功能分别如下 const_cast 将const变量转为非const变量。static_cast 最常用可以用于各种隐式转换比如非const转const基本数据类型之间的转换类向上转换但是向下类型转换不安全。dynamic_cast 用于含有虚函数的类层次之间的转换类向上和向下转换。 向上转换子类向基类转换 向下转换基类向子类转换。当父类转子类时可能出现非法内存访问是不安全的。 当 dynamic_cast 转换失败时返回一个空指针nullptr或者在指针类型的情况下返回一个空指针指针nullptr。如果转换失败并且是引用类型会抛出一个 std::bad_cast 异常。 reinterpret_cast 主要用于在不同类型之间进行低级别的转换。它仅仅是重新解释底层比特也就是对指针所指向的那片比特位换个类型解释而不进行任何类型检查。type-id必须是指针、引用、算术类型、函数指针或者成员指针。因此reinterpret_cast可能导致未定义的行为。 12、简述auto的具体用法 auto用于定义变量编译器可以自动判断变量的类型。auto 仅仅是一个占位符在编译器期间它会被真正的类型所替代。或者说C 中的变量必须是有明确类型的只是这个类型是由编译器自己推导出来的。 auto主要有以下几种用法 使用auto定义迭代器。迭代器类型比较复杂可以用auto替代。用于泛型编程不知道变量是什么类型或者不希望指明具体类型的时候。 五、C新特性学习使用 1、原始字面量 原始字面量可以解决在字符串中出现的转义字符等特殊字符但实际不想做转移的问题。语法格式R”xxx(原始字符串)xxx”。其中括号两边的xxx可以省略主要起到备注的作用相当于注释的作用但是不可以省略一边要么都不要要么都一样存在。 2、final和override final关键字用来限制某个类不能被继承或者某个虚函数不能被重写和Java的final关键字功能类似。如果使用final修饰函数只能修饰虚函数并且要把final关键字放到类或者函数的后面。 override关键字可以确保在派生类中声明的重写函数与基类的虚函数具有相同的签名同时也明确表明会重写基类的虚函数这样可以确保重写虚函数的正确性也提高了代码的可读性。如果意外出现错误可以及时提示。注意要求重写的是虚函数如果父类的不是虚函数这就不是重写了所以这个关键字是会报错的。 3、数值与字符串之间的转换 1、数值转为字符串 使用to_string()方法可以方便的将各种数值类型转为字符串类型这是一个重载函数函数声明位于头文件string中。 inline string to_string(int _Val) inline string to_string(unsigned int _Val) inline string to_string(long _Val) inline string to_string(unsigned long _Val) inline string to_string(long long _Val) inline string to_string(unsigned long _Val) inline string to_string(float _Val) inline string to_string(double _Val) inline string to_string(long double _Val) 2、字符串转数值 int stoi(const std::string str, std::size_t * pos 0, int base 10); long stol(const std::string str, std::size_t * pos 0, int base 10); long stoll(const std::string str, std::size_t * pos 0, int base 10); unsigned long stoul(const std::string str, std::size_t * pos 0, int base 10); unsigned long stoull(const std::string str, std::size_t * pos 0, int base 10); float stof(const std::string str, std::size_t * pos 0); double stod(const std::string str, std::size_t * pos 0); long double stold(const std::string str, std::size_t * pos 0); str: 源字符串 pos表示出现问题的位置是一个输出参数。比如123a456那么pos就是3因为在索引为3的地方出现无法转换的问题。 base表示将字符串str中的数字当作哪种进制转换返回的都是10进制数。         如果base为0那么会根据字符串的数字格式进行合理的转换如果是0x开头就是按照16进制转返回十进制如果是0开头按照8进制转换返回十进制。    如果直接指定字符串中数字的进制即使没有0x或者0开头亦可。 注意 如果字符串中所有字符都是数值类型整个字符串会被转换为对应的数值并返回。 如果字符串前部分是数值类型后部分不是那么前半部分会被转为对应的数值并返回。 如果字符的第一个就不是数值类型那么转换失败抛出异常。 4、静态断言static_cast 断言assertion是一种常用的手段在通常情况下断言就是将一个返回值总是需要为真的判断表示放在语句中用于排除在设计逻辑上不应该产生的情况。比如输入一个用户的年龄在函数体内就可以对这个年龄变量进行断言让其在0 age 100之间如果出了这个返回就会发生异常程序退出从而避免程序陷入逻辑的混乱。 从某种意义上讲断言并不是正常程序所必需的因为不能因为某些不合理的输入就让程序停止。不过对于调试程序可以很有效的定位某些前提条件的错误。 使用断言时需要在程序中包含头文件cassert或者assert.h头文件中提供了assert宏用于运行时断言。断言中的表达式返回true才能继续执行否则直接终止程序报错。 assert是一个运行时断言只有在程序运行时才能起作用。在某些情况下无法满足程序设计的需求比如想要知道当前是32位还是64位平台此时C11引入的静态断言就可以达到这个功能。 静态断言static_assert所谓静态就是在编译时就能够进行检查的断言使用时不需要引用头文件。此外可以自定义违反断言时的提示信息。静态断言比断言多一个参数也就是警告信息通常是一段字符串在违反断言时提示该信息。静态断言的表达式是在编译阶段进行检测的所以表达式中不能出现变量。 5、noexcept 异常通常用于处理逻辑上可能发生的错误在C98中提供了一套完整的异常处理机制可以直接在程序中将各种类型的异常抛出从而强制终止程序的运行。 为了加强程序的可读性可以在函数声明中列出可能抛出的所有异常类型通常有以下三种书写方式 1、显式指定可以抛出的异常类型。如果抛出了未指定类型将无法抛出。 2、如果在函数后面不显式指定抛出的类型表示可以抛出任意类型的异常。 3、如果在函数后面显式声明throw()不指定任何类型那么将不能抛出任何异常。 noexcept说明 上面的第一种指定抛出哪几种类型的异常在C11中被弃用了而第三种不抛出任何异常throw()也被新的noexcept异常声明所取代。noexcept表示其修饰的函数不会抛出异常不过与throw()动态异常声明不同如果用noexcept修饰的函数抛出了异常编译器会直接调用std::terminate()函数来终止程序的运行这比基于异常机制的throw()在效率上会高一些。这是因为异常机制会带来一些额外开销比如函数抛出异常会导致函数栈被依次展开并自动调用析构函数释放栈上的所有对象。 从语法上讲noexcept修饰符有两种形式 第一简单地在函数声明后加上noexcept关键字。 第二可以接受一个常量表达式作为参数。 常量表达式地结果会被转换成一个bool类型的值值为true表示函数不会抛出异常值为false表示有可能抛出异常。不带常量表达式相当于常量表达式为true。 6、自动类型推导 1、auto。在 C11 之前 auto 和 static 是对应的表示变量是自动存储的但是非 static 的局部变量默认都是自动存储的因此这个关键字变得非常鸡肋在 C11 中他们赋予了新的含义使用这个关键字能够像别的语言一样自动推导出变量的实际类型。 auto推导类型规则C11 中 auto 并不代表一种实际的数据类型只是一个类型声明的 “占位符”auto 并不是万能的在任意场景下都能够推导出变量的实际类型使用auto声明的变量必须要进行初始化以让编译器推导出它的实际类型在编译时将auto占位符替换为真正的类型。语法auto 变量名 变量值; auto 还可以和指针、引用结合起来使用也可以带上 const、volatile 限定符在不同的场景下有对应的推导规则规则内容如下 1、当变量不是指针或者引用类型时推导的结果中不会保留 const、volatile 关键字 2、当变量是指针或者引用类型时推导的结果中会保留 const、volatile 关键字     int temp 110;     auto *a temp; //auto被推导为int     auto b temp;  //auto被推导为int *类型     auto c temp;  //auto被推导为int类型     auto d temp;       //auto被推导为int     int tmp 250;     const auto a1 tmp;  //a1的数据类型为const int因此auto关键字被推导为int类型     auto a2 a1;  //a2的数据类型为int但是a2没有声明为指针或者引用因此const属性被去掉auto被推导为int     const auto a3 tmp; //a3的数据类型为const int auto a4 a3; //a4的数据类型为const int ,a4被声明为引用因此const属性被保留auto关键字被推导为const int auto的限制 auto 关键字并不是万能的在以下这些场景中是不能完成类型推导的 1、不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参auto 要求必须要给修饰的变量赋值因此二者矛盾。 2、不能用于类的非静态成员变量的初始化 3、不能使用 auto 关键字定义数组 4、无法使用 auto 推导出模板参数。 auto应用 1、用于STL容器遍历 2、用于泛型编程 2、decltype。 7、增强for循环范围遍历 在遍历的过程中需要给出容器的两端开头begin和结尾end因为这种遍历方式不是基于范围来设计的。在基于范围的for循环中不需要再传递容器的两端循环会自动以容器为范围展开并且循环中也屏蔽掉了迭代器的遍历细节直接抽取容器中的元素进行运算使用这种方式进行循环遍历会让编码和维护变得更加简便。 语法格式 declaration 表示遍历声明在遍历过程中当前被遍历到的元素会被存储到声明的变量中。expression 是要遍历的对象它可以是表达式、容器、数组、初始化列表等。 将容器中遍历的当前元素拷贝到了声明的变量 value 中因此无法对容器中的元素进行写操作如果需要在遍历过程中修改元素的值需要使用引用。 对容器的遍历过程中如果只是读数据不允许修改元素的值可以使用 const 定义保存元素数据的变量在定义的时候建议使用 const auto 这样相对于 const auto 效率要更高一些。 使用细节 1、关系型容器 使用基于范围的 for 循环有一些需要注意的细节比如关系型容器 map 的遍历 2、元素只读 在 for 循环内部声明一个变量的引用就可以修改遍历的表达式中的元素的值但是这并不适用于所有的情况对应 set 容器来说内部元素都是只读的这是由容器的特性决定的因此在 for 循环中 auto 会被视为 const auto  。 在遍历关联型容器时也会出现同样的问题基于范围的for循环中虽然可以得到一个std::pair引用但是我们是不能修改里边的first值的也就是key值。 3、访问次数 对于基于范围的 for 循环来说冒号后边的表达式只会被执行一次。在得到遍历对象之后会先确定好迭代的范围基于这个范围直接进行遍历。如果是普通的 for 循环在每次迭代的时候都需要判断是否已经到了结束边界。 8、nullptr指针空值类型 在 C 程序开发中为了提高程序的健壮性一般会在定义指针的同时完成初始化操作或者在指针的指向尚未明确的情况下都会给指针初始化为 NULL避免产生野指针没有明确指向的指针操作也这种指针极可能导致程序发生异常。C98/03 标准中将一个指针初始化为空指针的方式有 2 种 char *ptr 0; char *ptr NULL; 在底层源码中 NULL 这个宏是这样定义的 #ifndef NULL     #ifdef __cplusplus         #define NULL 0     #else         #define NULL ((void *)0)     #endif #endif 如果是 C 程序 NULL 就是 0如果是 C 程序 NULL 表示 (void*)0。 由于 C 中void * 类型无法隐式转换为其他类型的指针此时使用 0 代替 ((void *)0)用于解决空指针的问题。这个 00x0000 0000表示的就是虚拟地址空间中的 0 地址这块地址是只读的。 C 中将 NULL 定义为字面常量 0并不能保证在所有场景下都能很好的工作比如函数重载时NULL 和 0 无法区分 虽然调用 func(NULL); 最终链接到的还是 void func(int p) 和预期是不一样的其实这个原因已经很明白了在 C 中 NULL 和 0 是等价的。 出于兼容性的考虑C11 标准并没有对 NULL 的宏定义做任何修改而是另其炉灶引入了一个新的关键字 nullptr。nullptr 专用于初始化空类型指针不同类型的指针变量都可以使用 nullptr 来初始化。nullptr 无法隐式转换为整形但是可以隐式匹配指针类型。在 C11 标准下相比 NULL 和 0使用 nullptr 初始化空指针可以令我们编写的程序更加健壮。 9、Lambda表达式 1、基本用法 lambda表达式是C11最重要也是最常用的特性之一。具备以下优点 声明式的编程风格就地匿名定义目标函数不需要额外写一个命名函数。简洁避免了代码膨胀和功能分散让开发更加高效。在需要的时间和地点实现功能闭包使程序更加灵活。 lambda 表达式定义了一个匿名函数并且可以捕获一定范围内的外部变量。lambda 表达式的语法形式简单归纳如下 [capture](params) opt - ret {body;}; 其中capture 是捕获列表params 是参数列表opt 是函数选项ret 是返回值类型body 是函数体。 2、捕获列表 3、返回值 一般情况下不指定 lambda 表达式的返回值编译器会根据 return 语句自动推导返回值的类型但需要注意的是 labmda表达式不能通过列表初始化{1, 2, 3}自动推导出返回值类型。 4、什么通过值拷贝的方式捕获的外部变量是只读的 lambda表达式的类型在C11中会被看做是一个带operator()的类即仿函数。 按照C标准lambda表达式的operator()默认是const的一个const成员函数是无法修改成员变量值的。 对于没有捕获任何变量的 lambda 表达式还可以转换成一个普通的函数指针 10、常量表达式修饰符constexpr 1、const说明 在C11之前只有const关键字从功能上这个关键字有双重语义变量只读修饰常量变量只读不等于常量。 2、constexpr 这个关键字是用来修饰常量表达式的。常量表达式指的是由多个(1)常量组成并且在编译过程中就得到计算结果的表达式。常量表达式和非常量表达式的计算时机不同非常量表达式只能在程序运行阶段计算结果但是非常量表达式的计算往往发生在程序的编译阶段这可以极大提高程序的执行效率因为表达式只需要在编译阶段计算一次节省了每次程序运行时都需要计算一次的时间 编译器如何识别表达式是不是常量表达式constexpr关键字可以在程序中用来修饰常量表达式用来提高程序的执行效率。在使用中建议将const和constexpr的功能区分开即凡是表达只读语义的场景都是用const表达常量的语义的场景都用constexpr。 3、常量表达式函数 为了提高程序的执行效率可以将程序中值不需要发生变化的变量定义为常量也可以使用constexpr修饰函数的返回值这种函数被称为常量表达式函数。这些函数主要有普通函数、类成员函数、类构造函数和模板函数。 3.1修饰函数 constexpr并不能修改任意函数的返回值使这些函数称为常量表达式函数必须要满足几个条件同时也对类的成员函数适用 第一、函数必须要有返回值并且return返回的表达式必须是常量表达式。C11是无法编译通过的但是高版本放宽了限制 第二、在函数体中不能出现非常量表达式之外的语句using指令、tpyedef语句以及static_assert断言、return语句除外。注意C11中在constexpr的函数体中不能定义constexpr的局部变量只能用于函数和对象的声明C14以上则可以如下在C11中无法通过编译 3.2修饰模板函数 constexpr可以修饰函数模板但是由于模板中类型的不确定性因此函数模板实例化后的模板函数是否符合常量表达式函数的要求也是不确定的。如果constexpr修饰的模板函数实例化后的结果不满足常量表达式函数的要求则constexpr会被自动忽略这就相当于一个普通函数。 3.3修饰构造函数 如果想要直接得到一个常量对象也可以使用constexpr修饰一个构造函数常量构造函数有一个要求构造函数的函数体必须为空并且必须采用初始化列表的方式为各个成员赋值。 11、using关键字 using关键字通常用于声明命名空间。此外C11赋予了其新功能。 1、定义别名 using关键字作为别名声明的开始其后紧跟别名和等号其作用是把等号左侧的名字规定成等号右侧类型的别名。类型别名和类型的名字等价只要是类型的名字能出现的地方就能使用类型别名。使用typedef定义的别名和使用using是等效的。using newTypeoldType。在定义函数指针时using关键字的优势更能凸显。 //使用typedef定义函数指针 typedef int(*func_ptr)(int, double); //使用using定义函数指针 using func_ptr int(*)(int, double) 2、模板的别名 使用typedef重定义很方便但是他有一点限制比如无法重定义一个模板而using关键可以支持这个功能。 12、列表初始化 关于 C 中的变量数组对象等都有不同的初始化方法在这些繁琐的初始化方法中没有任何一种方式适用于所有的情况。为了统一初始化方式并且让初始化行为具有确定的效果在 C11 中提出了列表初始化的概念。 1、统一的初始化 2、列表初始化细节 对象 a 是对一个自定义的聚合类型进行初始化它将以拷贝的形式使用初始化列表中的数据来初始化 T1 结构体中的成员。 在结构体 T2 中自定义了一个构造函数因此实际的初始化是通过这个构造函数完成的。 如果使用列表初始化对对象初始化时还需要判断这个对象对应的类型是不是一个聚合体如果是初始化列表中的数据就会拷贝到对象中。 聚合体普通数组本身就是一种聚合类型{ 无用户自定义构造函数、无私有或者保护的非静态数据成员、无基类、无虚函数以及类中不能有使用 {} 和 直接初始化的非静态数据成员从 c14 开始就支持了} 非聚合体对于聚合类型的类可以直接使用列表初始化进行对象的初始化如果不满足聚合条件还想使用列表初始化其实也是可以的需要在类的内部自定义一个构造函数, 在构造函数中使用初始化列表对类成员变量进行初始化 综上对于一个聚合类型使用列表初始化相当于对其中的每个元素分别赋值而对于非聚合类型则需要先自定义一个合适的构造函数此时使用列表初始化将会调用它对应的构造函数。 3、std::initializer_list 在 C 的 STL 容器中可以进行任意长度的数据的初始化使用初始化列表也只能进行固定参数的初始化如果想要做到和 STL 一样有任意长度初始化的能力可以使用 std::initializer_list 这个轻量级的类模板来实现可变长参数。 特点 1、它是一个轻量级的容器类型内部定义了迭代器 iterator 等容器必须的概念遍历时得到的迭代器是只读的。 2、对于 std::initializer_listT 而言它可以接收任意长度的初始化列表但是要求元素必须是同种类型 T 3、在 std::initializer_list 内部有三个成员接口size(), begin(), end()。std::initializer_list 对象只能被整体初始化或者赋值。 场景1作为普通函数参数 自定义一个函数并且接收任意个数的参数变参函数只需要将函数参数指定为 std::initializer_list使用初始化列表 { } 作为实参进行数据传递即可。 场景2作为构造函数参数 自定义的类如果在构造对象的时候想要接收任意个数的实参可以给构造函数指定为 std::initializer_list 类型在自定义类的内部还是使用容器来存储接收的多个实参。 13、可调用对象包装器、绑定器 1、可调用对象 可调用对象就是类似于可以像函数调用一样执行的对象。 函数调用主要有以下几种定义方式 第一、函数指针 第二、仿函数具有operator()运算符的类对象 第三、可被转换为函数指针的类对象 第四、类成员函数指针或者类成员指针 由这几种方式可知可调用方式形式多样如果需要做统一的方式保存或者传递一个可调用对象时会是什么繁琐。为此C11引入了std::function和std::bind统一可调用对象的各种操作。 2、可调用对象包装器function std::function是可调用对象的包装器。它是一个类模板可以容纳除了类成员函数指针之外的所有可调用对象。通过指定它的模板参数它可以用统一的方式处理函数、函数对象、函数指针并允许保存和延迟执行它们。 第一、基本用法 总结std::function 可以将可调用对象进行包装得到一个统一的格式包装完成得到的对象相当于一个函数指针和函数指针的使用方式相同通过包装器对象就可以完成对包装的函数的调用了。 第二、作为回调函数使用 使用对象包装器 std::function 可以非常方便的将仿函数转换为一个函数指针通过进行函数指针的传递在其他函数的合适的位置就可以调用这个包装好的仿函数了。另外使用 std::function 作为函数的传入参数可以将定义方式不相同的可调用对象进行统一的传递这样大大增加了程序的灵活性。 3、可调用对象绑定器bind std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存并延迟调用到任何我们需要的时候。通俗来讲它主要有两大作用 将可调用对象与其参数一起绑定成一个仿函数。将多元参数个数为nn1可调用对象转换为一元或者n-1元可调用对象即只绑定部分参数。 第一、绑定非类成员函数/变量包括静态成员变量和函数 std::bind绑定器返回的是一个仿函数类型得到的返回值可以直接赋值给一个std::function在使用的时候我们并不需要关心绑定器的返回值类型使用auto进行自动类型推导就可以了。 placeholders::_1 是一个占位符代表这个位置将在函数调用时被传入的第一个参数所替代。同样还有其他的占位符 placeholders::_2、placeholders::_3、placeholders::_4、placeholders::_5 等…… 占位符细节说明 有了占位符的概念之后使用std::bind的使用会很灵活。 第二、绑定成员变量和成员函数 可调用对象包装器 std::function 是不能实现对类成员函数指针或者类成员指针的包装的但是通过绑定器 std::bind 的配合之后就可以完美的解决这个问题了。 14、默认函数控制default和delete 在 C 中声明自定义的类编译器会默认帮助程序员生成一些他们未自定义的成员函数。这样的函数版本被称为” 默认函数”。这样的函数一共有六个 在C11 标准中称 default 修饰的函数为显式默认【缺省】函数而称 delete 修饰的函数为删除deleted函数或者显示删除函数。 C11 引入显式默认和显式删除是为了增强对类默认函数的控制让程序员能够更加精细地控制默认版本的函数。 可以在类内部修饰满足条件的类函数为显示默认函数也可以在类定义之外修饰成员函数为默认函数。不能使用 default 修饰这六个函数以外的函数。 delete 表示显示删除显式删除可以避免用户使用一些不应该使用的类的成员函数使用这种方式可以有效的防止某些类型之间自动进行隐式类型转换产生的错误。 15、智能指针auto_ptr、shared_ptr、unique_ptr和weak_ptr 1、为什么使用智能指针 智能指针是存储指向动态分配堆对象指针的类用于生存期的控制能够确保在离开指针所在作用域时自动地销毁动态分配的对象防止内存泄露。智能指针的核心实现技术是引用计数每使用它一次内部引用计数加1每析构一次内部的引用计数减1减为0时删除所指向的堆内存。 C11 中提供了三种智能指针使用这些智能指针时需要引用头文件 memory std::shared_ptr共享的智能指针 std::unique_ptr独占的智能指针 std::weak_ptr弱引用的智能指针它不共享指针不能操作资源是用来监视 shared_ptr 的。 2、shared_ptr 共享智能指针是指多个智能指针可以同时管理同一块有效的内存共享智能指针 shared_ptr 是一个模板类如果要进行初始化有三种方式通过构造函数、std::make_shared 辅助函数以及 reset 方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数 use_count函数原型如下 2.1 通过构造函数初始化 2.2 通过拷贝和移动构造函数初始化 如果使用拷贝的方式初始化共享智能指针对象这两个对象会同时管理同一块堆内存堆内存对应的引用计数也会增加如果使用移动的方式初始智能指针对象只是转让了内存的所有权管理内存的对象并不会增加因此内存的引用计数不会变化。 2.3 通过std::make_shared初始化 使用 std::make_shared() 模板函数可以完成内存地址的创建并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型通过函数的可完成地址的初始化如果要创建一个类对象函数的内部需要指定构造对象需要的参数也就是类构造函数的参数。 2.4 通过reset方法初始化 共享智能指针类提供的 std::shared_ptr::reset 方法函数原型如下 对于一个未初始化的共享智能指针可以通过 reset 方法来初始化当智能指针中有值的时候调用 reset 会使引用计数减 1。 2.5 获取原始指针 通过智能指针可以管理一个普通变量或者对象的地址此时原始地址就不可见了。当我们想要修改变量或者对象中的值的时候就需要从智能指针对象中先取出数据的原始内存的地址再操作解决方案是调用共享智能指针类提供的 get() 方法其函数原型如下 2.6 指定删除器 当智能指针管理的内存对应的引用计数变为 0 的时候这块内存就会被智能指针析构掉了。另外我们在初始化智能指针的时候也可以自己指定删除动作这个删除操作对应的函数被称之为删除器这个删除器函数本质是一个回调函数我们只需要进行实现其调用是由智能指针完成的。 在 C11 中使用 shared_ptr 管理动态数组时需要指定删除器因为 std::shared_ptr的默认删除器不支持数组对象具体的处理代码如下 在删除数组内存时除了自己编写删除器也可以使用 C 提供的 std::default_deleteT() 函数作为删除器这个函数内部的删除功能也是通过调用 delete 来实现的要释放什么类型的内存就将模板类型 T 指定为什么类型即可。具体处理代码如下 模拟一个shared_ptr代码面试题 3、独占智能指针unique_ptr 3.1 初始化 std::unique_ptr 是一个独占型的智能指针它不允许其他的智能指针共享其内部的指针可以通过它的构造函数初始化一个独占智能指针对象但是不允许通过赋值将一个 unique_ptr 赋值给另一个 unique_ptr。 std::unique_ptr 不允许复制但是可以通过函数返回给其他的 std::unique_ptr还可以通过 std::move 来转移给其他的 std::unique_ptr这样原始指针的所有权就被转移了这个原始指针还是被独占的。 unique_ptr 独占智能指针类也有一个 reset 方法函数原型如下 使用 reset 方法可以让 unique_ptr 解除对原始内存的管理也可以用来初始化一个独占的智能指针。 如果想要获取独占智能指针管理的原始地址可以调用 get () 方法函数原型如下 3.2 删除器 4、弱引用的智能指针weak_ptr 弱引用智能指针 std::weak_ptr 可以看做是 shared_ptr 的助手它不管理 shared_ptr 内部的指针。std::weak_ptr 没有重载操作符 * 和 -因为它不共享指针不能操作资源所以它的构造不会增加引用计数析构也不会减少引用计数它的主要作用就是作为一个旁观者监视 shared_ptr 中管理的资源是否存在。 4.1 初始化 4.2 常用方法 4.3 返回管理this的shared_ptr 通过输出的结果可以看到一个对象被析构了两次其原因是这样的在这个例子中使用同一个指针 this 构造了两个智能指针对象 sp1 和 sp2这二者之间是没有任何关系的因为 sp2 并不是通过 sp1 初始化得到的实例对象。在离开作用域之后 this 将被构造的两个智能指针各自析构导致重复析构的错误。 上面的问题可以通过 weak_ptr 来解决通过 wek_ptr 返回管理 this 资源的共享智能指针对象 shared_ptr。C11 中为我们提供了一个模板类叫做 std::enable_shared_from_thisT这个类中有一个方法叫做 shared_from_this()通过这个方法可以返回一个共享智能指针在函数的内部就是使用 weak_ptr 来监测 this 对象并通过调用 weak_ptr 的 lock() 方法返回一个 shared_ptr 对象。 16、为什么要使用智能指针 为了更容易(同时也更安全的)地使用动态内存新的标准库提供了两种智能指针来管理动态对象。智能指针的行为类似于常规指针重要的区别是它负责自动释放所指向的对象。 shared_ptr允许多个指针指向同一个对象unique_ptr是“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类它是一种弱引用指向shared_ptr所管理的对象。这三种类型都定义在memeory头文件中。 它的原理是将动态分配的内存都交给有生命周期的对象来处理当对象过期时让他的析构函数删除指向的内存。C98提供了auto_ptr模板的解决方案C11增加了shared_ptr、unique_ptr和weak_ptr三种。其实就是一个模板类里面有析构函数能自动释放这个对象开辟的内存。 17、右值与右值引用 1.1、右值 C增加了新的类型称为右值引用标记为。 左值指存储在内存中、有明确存储地址可取地址的数据 右值指可以提供数据值的数据不可取地址。 通过描述可以看出区分左值与右值的便捷方法是可以对表达式取地址就是左值否则为右值 。所有有名字的变量或对象都是左值而右值是匿名的。 C11 中右值可以分为两种一个是将亡值 xvalue, expiring value另一个则是纯右值 prvalue, PureRvalue 纯右值非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等 将亡值与右值引用相关的表达式比如T 类型函数的返回值、 std::move 的返回值等。 1.2、右值引用 右值引用就是对一个右值进行引用的类型。因为右值是匿名的所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化因为引用类型本身并不拥有所绑定对象的内存只是该对象的一个别名。通过右值引用的声明该右值又“重获新生”其生命周期与右值引用类型变量的生命周期一样只要该变量还活着该右值临时量将会一直存活下去。 1.2、性能优化 在 C 中在进行对象赋值操作的时候很多情况下会发生对象之间的深拷贝如果堆内存很大这个拷贝的代价也就非常大在某些情况下如果想要避免对象的深拷贝就可以使用右值引用进行性能的优化。 通过输出的结果可以看到调用 Test t getObj(); 的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到了对象 t在 getObj() 函数中创建的对象虽然进行了内存的申请操作但是没有使用就释放掉了。如果能够使用临时对象已经申请的资源既能节省资源还能节省资源申请和释放的时间如果要执行这样的操作就需要使用右值引用了右值引用具有移动语义移动语义可以将资源堆、系统对象等通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建、拷贝以及销毁可以大幅提高 C 应用程序的性能。 在上面的代码给 Test 类添加了移动构造函数参数为右值引用类型这样在进行 Test t getObj(); 操作的时候并没有调用拷贝构造函数进行深拷贝而是调用了移动构造函数在这个函数中只是进行了浅拷贝没有对临时对象进行深拷贝提高了性能。 在测试程序中 getObj() 的返回值就是一个将亡值也就是说是一个右值在进行赋值操作的时候如果 右边是一个右值那么移动构造函数就会被调用。移动构造中使用了右值引用会将临时对象中的堆内存地址的所有权转移给对象t这块内存被成功续命因此在t对象中还可以继续使用这块内存。 注意对于需要动态申请大量资源的类应该设计移动构造函数以提高程序效率。需要注意的是一般在提供移动构造函数的同时也会提供常量左值引用的拷贝构造函数以保证移动不成还可以使用拷贝构造函数。 1.4、特性 在 C 中并不是所有情况下 都代表是一个右值引用具体的场景体现在模板和自动类型推导中如果是模板参数需要指定为 T如果是自动类型推导需要指定为 auto 在这两种场景下 被称作未定的引用类型。另外还有一点需要额外注意 const T 表示一个右值引用不是未定引用类型。 通过右值推导 T 或者 auto 得到的是一个右值引用类型 通过非右值右值引用、左值、左值引用、常量右值引用、常量左值引用推导 T 或者 auto 得到的是一个左值引用类型。 总结 1、左值和右值是独立于他们的类型的右值引用类型可能是左值也可能是右值。 2、编译器会将已命名的右值引用视为左值将未命名的右值引用视为右值。 3、auto或者函数参数类型自动推导的T是一个未定的引用类型它可能是左值引用也可能是右值引用类型这取决于初始化的值类型。 4、通过右值推导 T 或者 auto 得到的是一个右值引用类型其余都是左值引用类型。 18、转移move 在C11添加了右值引用并且不能使用左值初始化右值引用如果想要使用左值初始化一个右值引用需要借助std::move()函数使用std::move方法可以将左值转换为右值。使用这个函数并不能移动任何东西而是和移动构造函数一样都具有移动语义将对象的状态或者所有权从一个对象转移到另一个对象只是转移没有内存拷贝。 从实现上讲std::move基本等同于一个类型转换static_castT(lvalue);函数原型如下 场景假设一个临时容器很大并且要将这个容器赋值给另一个容器就可以执行如下操作 如果不使用std::move拷贝的代价很大性能较低。使用move几乎没有任何代价只是转换了资源的所有权。如果一个对象内部有较大的堆内存或者动态数组时使用move()就可以非常方便的进行数据所有权的转移。另外我们也可以给类编写相应的移动构造函数T::T(T another)和和具有移动语义的赋值函数T T::operator(T rhs)在构造对象和赋值的时候尽可能的进行资源的重复利用因为它们都是接收一个右值引用参数。 19、完美转发forward 六、C多线程C11多线程 C11 之前C 语言没有对并发编程提供语言级别的支持这使得我们在编写可移植的并发程序时存在诸多的不便。现在 C11 中增加了线程以及线程相关的类很方便地支持了并发编程使得编写的多线程程序的可移植性得到了很大的提高。 C11 中提供的线程类叫做 std::thread基于这个类创建一个新的线程非常的简单只需要提供线程函数或者函数对象即可并且可以同时指定线程函数的参数。 1、C线程的使用 2、公共成员函数 1、get_id() 应用程序启动之后默认只有一个线程这个线程一般称之为主线程或父线程通过线程类创建出的线程一般称之为子线程每个被创建出的线程实例都对应一个线程 ID这个 ID 是唯一的可以通过这个 ID 来区分和识别各个已经存在的线程实例这个获取线程 ID 的函数叫做 get_id()函数原型如下 当启动了一个线程创建了一个 thread 对象之后在这个线程结束的时候std::terminate ()如何去回收线程所使用的资源thread 库给我们两种选择且必须选择其中一个否则会报错 2、join() join() 字面意思是连接一个线程意味着主动地等待线程的终止线程阻塞。在某个线程中通过子线程对象调用 join() 函数调用这个函数的线程被阻塞但是子线程对象中的任务函数会继续执行当任务执行完毕之后 join() 会清理当前子线程中的相关资源然后返回同时调用该函数的线程解除阻塞继续向下执行函数在哪个线程中被执行那么函数就阻塞哪个线程。函数原型是void join(); 除了等待子线程结束回收资源外还有其他场景比如有3个线程2个子线程2个主线程2个子线程负责分段下载一个大文件然后主线程在等待下载完成之后做其他后续工作。 3、detach() detach() 函数的作用是进行线程分离分离主线程和创建出的子线程。在线程分离之后主线程退出也会一并销毁创建出的所有子线程在主线程退出之前它可以脱离主线程继续独立的运行任务执行完毕之后这个子线程会自动释放自己占用的系统资源。函数原型void detach() 线程分离函数 detach () 不会阻塞线程子线程和主线程分离之后在主线程中就不能再对这个子线程做任何控制了比如通过 join () 阻塞主线程等待子线程中的任务执行完毕或者调用 get_id () 获取子线程的线程 ID。有利就有弊鱼和熊掌不可兼得建议使用 join ()。 4、joinable() joinable() 函数用于判断主线程和子线程是否处理关联连接状态一般情况下二者之间的关系处于关联状态该函数返回一个布尔类型返回值为 true主线程和子线程之间有关联连接关系返回值为 false主线程和子线程之间没有关联连接关系。 5、operator 线程中的资源是不能被复制的因此通过 操作符进行赋值操作最终并不会得到两个完全相同的对象。 3、静态函数 thread 线程类还提供了一个静态方法用于获取当前计算机的 CPU 核心数根据这个结果在程序中创建出数量相等的线程每个线程独自占有一个CPU核心这些线程就不用分时复用CPU时间片此时程序的并发效率是最高的。 4、命名空间this_thread 在 C11 中不仅添加了线程类还添加了一个关于线程的命名空间 std::this_thread在这个命名空间中提供了四个公共的成员函数通过这些成员函数就可以对当前线程进行相关的操作了。 1、get_id() 调用命名空间 std::this_thread 中的 get_id() 方法可以得到当前线程的线程 ID。 2、sleep_for() 线程创建后一共有五种状态创建态就绪态运行态阻塞态(挂起态)退出态(终止态) 。 线程和进程的执行有很多相似之处在计算机中启动的多个线程都需要占用 CPU 资源但是 CPU 的个数是有限的并且每个 CPU 在同一时间点不能同时处理多个任务。为了能够实现并发处理多个线程都是分时复用CPU时间片快速的交替处理各个线程中的任务。因此多个线程之间需要争抢CPU时间片抢到了就执行抢不到则无法执行因为默认所有的线程优先级都相同内核也会从中调度不会出现某个线程永远抢不到 CPU 时间片的情况。 命名空间 this_thread 中提供了一个休眠函数 sleep_for()调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长因为阻塞态的线程已经让出了 CPU 资源代码也不会被执行所以线程休眠过程中对 CPU 来说没有任何负担。这个函数是函数原型如下参数需要指定一个休眠时长是一个时间段 程序休眠完成之后会从阻塞态重新变成就绪态就绪态的线程需要再次争抢 CPU 时间片抢到之后才会变成运行态这时候程序才会继续向下运行。 3、sleep_until() 命名空间 this_thread 中提供了另一个休眠函数 sleep_until()和 sleep_for() 不同的是它的参数类型不一样 函数原型如下 4、yield() 命名空间 this_thread 中提供了一个非常绅士的函数 yield()在线程中调用这个函数之后处于运行态的线程会主动让出自己已经抢到的 CPU 时间片最终变为就绪态这样其它的线程就有更大的概率能够抢到 CPU 时间片了。使用这个函数的时候需要注意一点线程调用了 yield () 之后会主动放弃 CPU 资源但是这个变为就绪态的线程会马上参与到下一轮 CPU 的抢夺战中不排除它能继续抢到 CPU 时间片的情况这是概率问题。 总结 1、std::this_thread::yield() 的目的是避免一个线程长时间占用CPU资源从而导致多线程处理性能下降 2、std::this_thread::yield() 是让当前线程主动放弃了当前自己抢到的CPU资源但是在下一轮还会继续抢。 5、call_once函数 在某些特定情况下某些函数只能在多线程环境下调用一次比如要初始化某个对象而这个对象只能被初始化一次就可以使用 std::call_once() 来保证函数在多线程环境下只能被调用一次。使用 call_once() 的时候需要一个 once_flag 作为 call_once() 的传入参数该函数的原型如下 6、C线程同步 解决多线程数据混乱的方案就是进行线程同步最常用的就是互斥锁在 C11 中一共提供了四种互斥锁 std::mutex独占的互斥锁不能递归使用 std::timed_mutex带超时的独占互斥锁不能递归使用 std::recursive_mutex递归互斥锁不带超时功能 std::recursive_timed_mutex带超时的递归互斥锁 1、std::mutex 不论是在 C 还是 C 中进行线程同步的处理流程基本上是一致的C 的 mutex 类提供了相关的 API 函数 lock() 函数lock() 函数用于给临界区加锁并且只能有一个线程获得锁的所有权它有阻塞线程的作用函数原型如下 独占互斥锁对象有两种状态锁定和未锁定。如果互斥锁是打开的调用 lock() 函数的线程会得到互斥锁的所有权并将其上锁其它线程再调用该函数的时候由于得不到互斥锁的所有权就会被 lock() 函数阻塞。当拥有互斥锁所有权的线程将互斥锁解锁此时被 lock() 阻塞的线程解除阻塞抢到互斥锁所有权的线程加锁并继续运行没抢到互斥锁所有权的线程继续阻塞。 try_lock() 函数也起到枷锁的作用与lock函数的区别是lock会阻塞线程而try_lock函数不会阻塞线程。如果互斥锁是未锁定状态得到互斥锁所有权并枷锁成功返回true如果互斥锁是锁定状态无法得到互斥锁所有权枷锁失败返回false。 unlock函数解锁。 通过上面三个函数基本就能实现线程同步了大致步骤如下 1、找到多个线程操作的共享资源全局变量、堆内存、类成员变量等也可以称之为临界资源 2、找到和共享资源有关的上下文代码也就是临界区下图中的黄色代码部分 3、在临界区的上边调用互斥锁类的 lock() 方法 4、在临界区的下边调用互斥锁的 unlock() 方法 线程同步的目的是让多线程按照顺序依次执行临界区代码这样做线程对共享资源的访问就从并行访问变为了线性访问访问效率降低了但是保证了数据的正确性。 在所有线程的任务函数执行完毕之前互斥锁对象是不能被析构的一定要在程序中保证这个对象的可用性。 互斥锁的个数和共享资源的个数相等也就是说每一个共享资源都应该对应一个互斥锁对象。互斥锁对象的个数和线程的个数没有关系。 2、std::lock_guard lock_guard 是 C11 新增的一个模板类使用这个类可以简化互斥锁 lock() 和 unlock() 的写法同时也更安全。这个模板类的定义和常用的构造函数原型如下 lock_guard 在使用上面提供的这个构造函数构造对象时会自动锁定互斥量而在退出作用域后进行析构时就会自动解锁从而保证了互斥量的正确操作避免忘记 unlock() 操作而导致线程死锁。lock_guard 使用了 RAII 技术就是在类构造函数中分配资源在析构函数中释放资源保证资源出了作用域就释放。 这种方式也有弊端在上面的示例程序中整个for循环的体都被当做了临界区多个线程是线性的执行临界区代码的因此临界区越大程序效率越低还是需要根据实际情况选择最优的解决方案。 7、线程同步之条件变量 1、条件变量 条件变量是 C11 提供的另外一种用于等待的同步机制它能阻塞一个或多个线程直到收到另外一个线程发出的通知或者超时时才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来使用C11 提供了两种条件变量 条件变量通常用于生产者和消费者模型大致使用过程如下 2、condition_variable condition_variable 的成员函数主要分为两部分线程等待阻塞函数 和线程通知唤醒函数这些函数被定义于头文件 condition_variable。 等待函数 调用wait函数的线程会被阻塞并且释放当前拿到的锁对象。 独占的互斥锁对象不能直接传递给 wait() 函数需要通过模板类 unique_lock 进行二次处理通过得到的对象仍然可以对独占的互斥锁对象做如下操作使用起来更灵活。 如果线程被该函数阻塞这个线程会释放占有的互斥锁的所有权当阻塞解除之后这个线程会重新得到互斥锁的所有权继续向下执行这个过程是在函数内部完成的其目的是为了避免线程的死锁。 wait_for函数和wait_until函数一个是有阻塞时长功能一个是阻塞到达一个时间点的功能只要阻塞时长到达或者到达一个时间点都会自动解除阻塞向下执行。 通知函数 生产者消费者模型 #include iostream #include thread #include mutex #include list #include functional #include condition_variable using namespace std; class SyncQueue { private:     listint m_queue;                   // 存储队列数据     mutex m_mutex;                       // 互斥锁     condition_variable m_notEmpty;    // 不为空的条件变量     condition_variable m_notFull;     // 没有满的条件变量     int m_maxSize;                       // 任务队列的最大任务个数 public:     //构造函数     SyncQueue(int maxSize) : m_maxSize(maxSize) {}     //往队列中填入数据     void put(const int x)     {         //自动拿到锁可以不用调用lock方法超出作用域则会自动解锁         unique_lockmutex locker(m_mutex);                 // 判断任务队列是不是已经满了         //while (m_queue.size() m_maxSize)         //{         //  cout 任务队列已满, 请耐心等待... endl;         //  // 阻塞线程等待取出数据的线程的通知表示不满了可以继续填入数据         //  m_notFull.wait(locker);         //}         //while循环简化         m_notFull.wait(locker, [this]() -bool {return m_queue.size() m_maxSize; });         // 将任务放入到任务队列中         m_queue.push_back(x);         cout x 被生产 endl;         // 通知消费者去消费         m_notEmpty.notify_one();     }     //往对类中取出数据     int take()     {         unique_lockmutex locker(m_mutex);         /*while (m_queue.empty())         {             cout 任务队列已空请耐心等待。。。 endl;             m_notEmpty.wait(locker);         }*/         //简化         m_notEmpty.wait(locker, [this]()-bool {return !m_queue.empty(); });         // 从任务队列中取出任务(消费)         int x m_queue.front();         m_queue.pop_front();         // 通知生产者去生产         m_notFull.notify_one();         cout x 被消费 endl;         return x;     }     //判断是否为空     bool empty()     {         lock_guardmutex locker(m_mutex);         return m_queue.empty();     }     //判断是否队列是否满     bool full()     {         lock_guardmutex locker(m_mutex);         return m_queue.size() m_maxSize;     }     //队列的大小     int size()     {         lock_guardmutex locker(m_mutex);         return m_queue.size();     } }; int main() {     SyncQueue taskQ(50);     auto produce bind(SyncQueue::put, taskQ, placeholders::_1);     auto consume bind(SyncQueue::take, taskQ);     thread t1[3];     thread t2[3];     for (int i 0; i 3; i)     {         t1[i] thread(produce, i 100);         t2[i] thread(consume);     }     for (int i 0; i 3; i)     {         t1[i].join();         t2[i].join();     }     return 0; } 3、condition_variable_any condition_variable_any 的成员函数也是分为两部分线程等待阻塞函数 和线程通知唤醒函数这些函数被定义于头文件 condition_variable。 等待函数 此外还有两个阻塞时长和时间点的。 通知函数 生产者和消费者模型 #include iostream #include thread #include mutex #include list #include functional #include condition_variable using namespace std; class SyncQueue { public:     SyncQueue(int maxSize) : m_maxSize(maxSize) {}     void put(const int x)     {         lock_guardmutex locker(m_mutex);         // 根据条件阻塞线程         m_notFull.wait(m_mutex, [this]() {             return m_queue.size() ! m_maxSize;         });         // 将任务放入到任务队列中         m_queue.push_back(x);         cout x 被生产 endl;         // 通知消费者去消费         m_notEmpty.notify_one();     }     int take()     {         lock_guardmutex locker(m_mutex);         m_notEmpty.wait(m_mutex, [this]() {             return !m_queue.empty();         });         // 从任务队列中取出任务(消费)         int x m_queue.front();         m_queue.pop_front();         // 通知生产者去生产         m_notFull.notify_one();         cout x 被消费 endl;         return x;     }     bool empty()     {         lock_guardmutex locker(m_mutex);         return m_queue.empty();     }     bool full()     {         lock_guardmutex locker(m_mutex);         return m_queue.size() m_maxSize;     }     int size()     {         lock_guardmutex locker(m_mutex);         return m_queue.size();     } private:     listint m_queue;     // 存储队列数据     mutex m_mutex;         // 互斥锁     condition_variable_any m_notEmpty;   // 不为空的条件变量     condition_variable_any m_notFull;    // 没有满的条件变量     int m_maxSize;         // 任务队列的最大任务个数 }; int main() {     SyncQueue taskQ(50);     auto produce bind(SyncQueue::put, taskQ, placeholders::_1);     auto consume bind(SyncQueue::take, taskQ);     thread t1[3];     thread t2[3];     for (int i 0; i 3; i)     {         t1[i] thread(produce, i 100);         t2[i] thread(consume);     }     for (int i 0; i 3; i)     {         t1[i].join();         t2[i].join();     }     return 0; } 4、总结 总结以上介绍的两种条件变量各自有各自的特点condition_variable 配合 unique_lock 使用更灵活一些可以在在任何时候自由地释放互斥锁而 condition_variable_any 如果和 lock_guard 一起使用必须要等到其生命周期结束才能将互斥锁释放亦可以手动调用lock和unlock加锁和解锁。但是condition_variable_any 可以和多种互斥锁配合使用应用场景也更广而 condition_variable 只能和独占的非递归互斥锁mutex配合使用有一定的局限性。 8、原子变量 C11 提供了一个原子类型 std::atomicT通过这个原子类型管理的内部变量就可以称之为原子变量我们可以给原子类型指定 bool、char、int、long、指针等类型作为模板参数不支持浮点类型和复合类型。 原子指的是一系列不可被 CPU 上下文交换的机器指令这些指令组合在一起就形成了原子操作。在多核 CPU 下当某个 CPU 核心开始运行原子操作时会先暂停其它 CPU 内核对内存的操作以保证原子操作不会被其它 CPU 内核所干扰。 由于原子操作是通过指令提供的支持因此它的性能相比锁和消息传递会好很多。相比较于锁而言原子类型不需要开发者处理加锁和释放锁的问题同时支持修改读取等操作还具备较高的并发性能几乎所有的语言都支持原子类型。 可以看出原子类型是无锁类型但是无锁不代表无需等待因为原子类型内部使用了 CAS 循环当大量的冲突发生时该等待还是得等待但是总归比锁要好。 C11 内置了整形的原子变量这样就可以更方便的使用原子变量了。在多线程操作中使用原子变量之后就不需要再使用互斥量来保护该变量了用起来更简洁。因为对原子变量进行的操作只能是一个原子操作atomic operation原子操作指的是不会被线程调度机制打断的操作这种操作一旦开始就一直运行到结束中间不会有任何的上下文切换。多线程同时访问共享资源造成数据混乱的原因就是因为 CPU 的上下文切换导致的使用原子变量解决了这个问题因此互斥锁的使用也就不再需要了。 CAS 全称是 Compare and swap, 它通过一条指令读取指定的内存地址然后判断其中的值是否等于给定的前置值如果相等则将其修改为新的值 1、atomic类成员 – 构造函数 2、atomic类成员 – 公共成员函数 原子类型在类内部重载了 操作符并且不允许在类的外部使用 进行对象的拷贝。 原子地以 desired 替换当前值。按照 order 的值影响内存。 desired       存储到原子变量中的值 order         强制的内存顺序 原子地加载并返回原子变量的当前值。按照 order 的值影响内存。直接访问原子对象也可以得到原子变量的当前值。 4、atomic内存顺序约束 如上API在调用 atomic 类提供的 API 函数的时候需要指定原子顺序在 C11 给我们提供的 API 中使用枚举用作执行原子操作的函数的实参以指定如何同步不同线程上的其他操作。 9、线程异步 C11 中增加的线程类使得我们能够非常方便的创建和使用线程但有时会有些不方便比如需要获取线程返回的结果就不能通过 join() 得到结果只能通过一些额外手段获得比如定义一个全局变量在子线程中赋值在主线程中读这个变量的值整个过程比较繁琐。C 提供的线程库中提供了一些类用于访问异步操作的结果。 1、std::future 作用是C11中引入的一个模板类用于表示异步任务的结果。通过std::future对象可以获取异步任务的返回值或处理异步任务的状态。 类定义 future 是一个模板类这个类可以存储任意指定类型的数据。 构造函数 常用成员函数public) 一般情况下使用 进行赋值操作就进行对象的拷贝但是 future 对象不可用复制因此会根据实际情况进行处理 取出 future 对象内部保存的数据其中 void get() 是为 futurevoid 准备的此时对象内部类型就是 void该函数是一个阻塞函数当子线程的数据就绪后解除阻塞就能得到传出的数值了。 因为 future 对象内部存储的是异步线程任务执行完毕后的结果是在调用之后的将来得到的因此可以通过调用 wait() 方法阻塞当前线程等待这个子线程的任务执行完毕任务执行完毕当前线程的阻塞也就解除了。 如果当前线程 wait() 方法就会死等直到子线程任务执行完毕将返回值写入到 future 对象中调用 wait_for() 只会让线程阻塞一定的时长但是这样并不能保证对应的那个子线程中的任务已经执行完毕了。wait_until() 和 wait_for() 函数功能是差不多前者是阻塞到某一指定的时间点后者是阻塞一定的时长。 当 wait_until() 和 wait_for() 函数返回之后并不能确定子线程当前的状态因此我们需要判断函数的返回值这样就能知道子线程当前的状态了 2、std::async async用于方便地启动异步任务并获取其结果。它位于future头文件中。 这函数可以直接启动一个子线程并在这个子线程中执行对应的任务函数异步任务执行完成返回的结果也是存储到一个 future 对象中当需要获取异步任务的结果时只需要调用 future 类的get() 方法即可如果不关注异步任务的结果只是简单地等待任务完成的话可以调用 future 类的wait()或者wait_for() 方法。 该函数的函数原型如下 这是一个模板函数在 C11 中这个函数有两种调用方式 函数①直接调用传递到函数体内部的可调用对象返回一个 future 对象 函数②通过指定的策略调用传递到函数内部的可调用对象返回一个 future 对象 两种策略的使用 七、Linux网络编程IO多路复用 1、什么是IO多路复用 IO多路复用是一种同步IO模型实现一个线程可以监视多个文件句柄一旦某个文件句柄就绪就能够通知应用程序进行相应的读写操作没有文件句柄就绪时就会阻塞应用程序交出cpu的占用权。多路是指网络连接复用指的是同一个线程。 2、为什么有IO多路复用机制 没有IO多路复用时有BIO同步阻塞和NIO同步非阻塞两种实现方式但是都存在一些问题。 同步阻塞BIO服务器采用单线程当accept一个请求后在recv和send调用阻塞时将无法accept其他请求必须等上一个请求处理完无法处理并发。 为此服务器端采用多线程当accept一个请求后开启子线程进行recv可以完成并发处理但是随着请求数增加需要增加系统线程大量的线程占用很大的内存空间并且线程切换会带来很大的开销10000个线程真正发生读写事件的线程数不会超过20%每次accept一个连接后开启新的线程也会带来非常大的资源消耗。 2、同步非阻塞NIO服务器端当accept一个请求后加入fds文件句柄集合每次轮询一遍fds文件句柄集合recv(非阻塞)数据没有数据则立即返回错误每次轮询所有fd文件句柄包括没有发生读写事件的fd会很浪费cpu资源。如果有10000个连接可能只有10几个才有数据可读取。 3、IO多路复用服务端采用单线程通过select/poll/epoll等系统调用获取socket文件句柄遍历有事件的socket文件句柄进行accept/recv/send等操作使其能够支持更多的并发连接请求。 3、select接口select的原理select优缺点 原理首先构造一个关于文件描述符的数组将要监听的文件描述符添加到该数组中。调用select这个系统调用时监听该数组中的文件描述符直到这些描述符中的一个或者多个进行IO操作操作时该函数才返回。默认情况下select是阻塞的函数对文件描述符的事件检测是由内核完成的。在返回时他会告诉进程有多少文件描述符有事件发生。需要遍历select修改后的文件描述符数组判断是否有哪种事件发生才进行相对应的处理。 优点可移植性好连接数少并且连接都十分活跃的情况下效率也不错。 缺点1、每次调用select都需要把fd集合从用户态拷贝到内核态这个开销在fd很多时会很大。2、每次调用select都需要在内核遍历传递进来的所有fd这个开销在fd很多时也很大。3、select支持的文件描述符太小默认是1024可以用ulimit -n 2048命令进行修改。4、文件描述符集合不能重用每次都需要重置重新传入。 4、poll接口poll原理poll优缺点 poll原理poll与select差不多但是他没有文件描述符数量的限制但是依然采用轮询遍历的方式检查是否有事件发生。所以poll和select的缺点都差不多。 5、epoll接口 6、epoll原理工作模式 原理epoll是一种更加高效的IO多路复用的方式可以监听的文件描述符数量突破了1024的限制同时不需要通过轮询遍历的方式去检查文件描述符是否有事件发生因为epoll_wait返回的就是有事件发生的文件描述符。本质上是事件驱动的。 内部具体是通过红黑树和就绪链表实现的红黑树存储所有的文件描述符就绪链表存储有事件发生的文件描述符 epoll_ctl可以对文件描述符节点进行增删改查并且告知内核注册回调函数事件。一旦文件描述符上有事件发生那么内核将该文件描述符节点插入到就绪链表里面。这时候epoll_wait将会接收到消息并且将数据拷贝到用户空间。 在连接数少并且连接都十分活跃的情况下select和poll的性能可能比epoll好毕竟epoll的通知机制需要更多回调函数。 工作模式 首先epoll_create函数会创建一个epoll实例返回值是一个文件描述符指向内核的一块空间这块空间是epoll的工作空间主要由两块重要的内存一块是红黑树类型的rbr里面存放的是所有要监测的文件描述符另一块是双链表类型的rdlist里面存放的是被监测到有数据变动的文件描述符来自于rbr 。遇到一个新的文件描述符就将这个文件描述符通过 epoll_ctl 函数添加到上面的epoll实例中也就是将其放到rbr空间中作为待监测的文件描述符。同时这个函数可以设置监测文件描述符发生的行为注册事件比如客户端发送到服务端数据。如果内核监测到rbr中的文件描述符出现了 epoll_ctl 设置的要监听的的行为那么就会将其拷贝的 rdlist 就绪链表中。 epoll_wait 函数则是可以获取到rdlist中的数据通过传入传出参数返回它的返回值就是数据变动的文件描述符的数量。根据epoll_wait的传出参数遍历之这是一个结构体数组。获取每个元素中的文件描述符判断它是监听文件描述符还是其他的 如果是监听文件描述符那么就有新的客户端连接此时就要将其添加到rbr空间中使用epoll_ctl如果是其他文件描述符就说明有客户端发送了数据此时可以根据文件描述符读取数据 。 7、epoll的LT和ET模式的区别 LT模式水平模式 水平触发模式是缺省的工作方式并且同时支持block和no-block socket。在这种做法中内核会告诉一个文件描述符是否就绪了然后可以对这个就绪的fd文件描述符进行IO操作。如果不做任何操作或者只做部分操作内核会继续通知也就是在下一次epoll_wait时继续触发直到处理完成。 假设委托内核检测读事件 - 检测fd的读缓冲区 读缓冲区有数据 - epoll检测到了会给用户服务端通知 a.用户不读数据数据一直在缓冲区epoll 会一直通知 b.用户只读了一部分数据epoll会继续通知 c.缓冲区的数据读完了不通知 ET 模式边沿触发 ETedge - triggered是高速工作方式只支持 no-block socket。在这种模式下当描述符从未就绪变为就绪时内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪并且不会再为那个文件描述符发送更多的就绪通知直到你做了某些操作导致那个文件描述符不再为就绪状态了。 但是注意如果一直不对这个 fd 作 IO 操作从而导致它再次变成未就绪内核不会发送更多的通知only once。 ET 模式在很大程度上减少了 epoll 事件被重复触发的次数因此效率要比 LT 模式高。epoll工作在 ET 模式的时候必须使用非阻塞套接口以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。 ET模式需要主动开启在添加事件的时候epev.events EPOLLIN | EPOLLET 假设委托内核检测读事件 - 检测fd的读缓冲区 读缓冲区有数据 - epoll检测到了会给用户通知 a.用户不读数据数据一致在缓冲区中epoll下次检测的时候就不通知了 b.用户只读了一部分数据epoll不通知 c.缓冲区的数据读完了不通知。 为此需要循环读取直到读取完成否则会导致有数据停留在缓冲区数据不完整。 8、epoll的LT和ET模式的区别 1、epoll的水平触发模式是默认的模式边沿触发模式是需要手动设置的。 2、水平触发模式下只要这个文件描述符有数据可读每次epoll_wait都会返回他的事件提醒用户操作。而边沿触发模式下他只会提示一次直到下次在有新的数据流入。 3、水平触发模式支持文件描述符的阻塞和非阻塞而边沿模式只支持非阻塞。 9、select /epoll之间的区别 1每次调用select都需要把fd集合从用户态拷贝到内核态这个开销在fd很多时会很大而epoll保证了每个fd在整个过程中只会拷贝一次。 2每次调用select都需要在内核遍历传递进来的所有fd而epoll只需要轮询一次fd集合同时查看就绪链表中有没有就绪的fd就可以了。 3select支持的文件描述符数量太小了默认是1024而epoll没有这个限制它所支持的fd上限是最大可以打开文件的数目这个数字一般远大于2048。 10、epoll为什么高效 1selectpoll实现需要自己不断轮询所有fd集合直到设备就绪期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了这节省了大量的CPU时间。 2selectpoll每次调用都要把fd集合从用户态往内核态拷贝一次并且要把当前进程往设备等待队列中挂一次而epoll只要一次拷贝而且把当前进程往等待队列上挂也只挂一次这也能节省不少的开销。 11、说说多路IO复用计数有哪些区别是什么 selectpollepoll都是IO多路复用的机制I/O多路复用就是通过一种机制可以监视多个文件描述符一旦某个文件描述符就绪一般是读就绪或者写就绪能够通知应用程序进行相应的读写操作。 区别 1poll与select不同通过一个pollfd数组向内核传递需要关注的事件故没有描述符个数的限制pollfd中的events字段和revents分别用于标示关注的事件和发生的事件故pollfd数组只需要被初始化一次。 2selectpoll实现需要自己不断轮询所有fd集合直到设备就绪期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了这节省了大量的CPU时间。 3selectpoll每次调用都要把fd集合从用户态往内核态拷贝一次并且要把当前进程往设备等待队列中挂一次而epoll只要一次拷贝而且把当前进程往等待队列上挂也只挂一次这也能节省不少的开销。 12、端口和地址复用 在默认情况下如果一个网络应用程序的一个套接字绑定了一个端口这时候别的套接字就无法使用这个端口8080。 但是端口复用允许在一个应用程序可以把多个套接字绑在一个端口上而不出错。通过设置socket的SO_REUSEADDR选项即可实现端口复用 13、为什么要有端口复用 因为在服务端结束后也就是第三次挥手的时候会有个等待释放时间time_wait这个时间段大概是1-4分钟2MSL 在这个时间内端口不会迅速的被释放所以可通过端口复用的方法来解决这个问题。 SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上但每个socket绑定的ip地址不同。 14、说说Reactor、Proactor模式。 Reactor模式用于同步I/O而Proactor运用于异步I/O操作。 Reactor模式 Reactor模式应用于同步I/O的场景。Reactor中读操作的具体步骤如下 读取操作 1应用程序注册读就需事件和相关联的事件处理器。 2事件分离器等待事件的发生。 3当发生读就需事件的时候事件分离器调用第一步注册的事件处理器。 4事件处理器首先执行实际的读取操作然后根据读取到的内容进行进一步的处理。 Proactor模式Proactor模式应用于异步I/O的场景。Proactor中读操作的具体步骤如下 1应用程序初始化一个异步读取操作然后注册相应的事件处理器此时事件处理器不关注读取就绪事件而是关注读取完成事件这是区别于Reactor的关键。 2事件分离器等待读取操作完成事件。 3在事件分离器等待读取操作完成的时候操作系统调用内核线程完成读取操作并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点Proactor中应用程序需要传递缓存区。 4事件分离器捕获到读取完成事件后激活应用程序注册的事件处理器事件处理器直接从缓存区读取数据而不需要进行实际的读取操作。 综上Reactor中需要应用程序自己读取或者写入数据而Proactor模式中应用程序不需要用户再自己接收数据直接使用就可以了操作系统会将数据从内核拷贝到用户区。 IO模型的类型 1阻塞IO调用者调用了某个函数等待这个函数返回期间什么也不做不停的检查这个函数有没有返回必须等这个函数返回后才能进行下一步动作。 2非阻塞IO非阻塞等待每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。 3信号驱动IOLinux用套接口进行信号驱动IO安装一个信号处理函数进程继续运行并不阻塞当IO事件就绪进程收到SIGIO信号然后处理IO事件。 4IO多路复用Linux用select/poll函数实现IO复用模型这两个函数也会使进程阻塞但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检查。知道有数据可读或可写时才真正调用IO操作函数。 5异步IOLinux中可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式然后立即返回当内核将数据拷贝到缓冲区后再通知应用程序。用户可以直接去使用数据。 前四种模型--阻塞IO、非阻塞IO、多路复用IO和信号驱动IO都属于同步模式因为其中真正的IO操作(函数)都将会阻塞进程只有异步IO模型真正实现了IO操作的异步性。 17、介绍一下5中IO模型 1、阻塞IO调用者调用了某个函数等待这个函数返回期间什么也不做不停的检查这个函数有没有返回必须等这个函数返回后才能进行下一步动作。 2、非阻塞IO非阻塞等待每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。 3、信号驱动IOLinux用套接口进行信号驱动IO安装一个信号处理函数进程继续运行并不阻塞当IO事件就绪进程收到SIGIO信号然后处理IO事件。 4、IO多路复用Linux用select/poll函数实现IO复用模型这两个函数也会使进程阻塞但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检查。知道有数据可读或可写时才真正调用IO操作函数。 5、异步IOLinux中可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式然后立即返回当内核将数据拷贝到缓冲区后再通知应用程序。用户可以直接去使用数据。 前四种模型--阻塞IO、非阻塞IO、多路复用IO和信号驱动IO都属于同步模式因为其中真正的IO操作(函数)都将会阻塞进程只有异步IO模型真正实现了IO操作的异步性。 异步和同步的区别就在于异步是内核将数据拷贝到用户区不需要用户再自己接收数据直接使用就可以了而同步是内核通知用户数据到了然后用户自己调用相应函数去接收数据。 异步IO操作从文件中读取然后输出。 同步非阻塞完成上述功能 18、说说socket网络编程中客户端和服务端用到哪些函数 服务器端 1socket创建一个套接字 2bind绑定ip和port 3listen使套接字变为可以被动链接 4accept等待客户端的链接 5write/read接收发送数据 6close关闭连接 客户端 1创建一个socket用函数socket( ) 2bind绑定ip和port 可以不做bind 3连接服务器用函数connect() 4收发数据用函数send()和recv()或read()和write() 5close关闭连接 八、Linux系统编程 1、说说静态库和动态库怎么制作以及如何使用区别是什么 1、静态库的制作 生成对应项目文件的对象文件 将生成的对象文件打包成静态库 2、使用静态库  -l小写L表示生成的静态库名称除去前缀lib和后缀 .a  -L 表示静态库缩放的路径  -I大写i表示静态库所需要的头文件所在目录。 1、动态库的制作 生成项目对象文件 生成动态库文件 -fpic 表示生成和位置无关的代码 -shared 表示生成共享库 2、使用动态库 同静态库需要指定以来的动态库的名称、路径已经需要的头文件。 在执行app可执行文件时会报错无法找到对应的动态库。通过 ldd ./app命令可以查看这个可执行文件依赖的动态库是否能够都找到。 动态库是在程序加载时动态引入的查找先后顺序环境变量LD_LIBARY_PATH à /etc/ld.so.cache文件列表 à /lib/ 目录 à  /usr/lib目录。 静态库和动态库的区别 静态库代码代码装载的速度快执行速度略比动态库快。动态库更加节省内存可执行文件体积比静态库小很多。静态库是在编译时加载动态库是在运行时加载。生成的静态链接库Windows下以.lib为后缀Linux下以.a为后缀生成的动态链接库Windows下以.dll为后缀Linux下以.so为后缀。 2、简述GDB常见的调试命令什么是条件断点多进程下如何调试 GDB调试GDB调试的是可执行文件在gcc编译时加入-g参数告诉gcc在编译时加入调试信息这样gdb才能调试这个被编译的文件。此外还会加上-Wall参数尽量显示所有警告信息。 GDB命令格式 1、start程序在第一行停止run遇到断点才停止。 2、continue继续运行到下一个断点停止next向下执行一行代码不进入函数体step向下单步执行遇到函数调用可以进入函数体 finish可以跳出函数体util可以跳出循环体。 3、print 变量名打印变量的值ptype 变量名打印变量类型。display 变量名自动打印指定变量的值之后每执行一步都会自动打印这个变量undisplay 编号将自动打印的变量关掉info display查看当前正在自动打印的变量有什么。 4、list 从头默认位置显示list 行号从指定的行显示这个行在显示的中间list 函数名从指定的函数显示list 文件名行号函数名从指定文件名的行号或者函数名显示。show listsize 查看显示的行数默认10行set listsize 行数设置显示的行数。 5、break 行号在指定的行号位置打断点break 函数名在指定的函数位置打断点break 文件名行号/函数名在指定的文件中的行号或者函数位置打断点。info break 显示所有的断点信息delete 断点编号删除断点disable 断点编号设置断点无效enable 断点编号设置断点生效。break 10 if a 5 在指定行设置条件断点a 5时断点生效。 GDB多进程断点调试 set follow-fork-mode [parent默认 | child ] 设置调试父进程还是子进程默认父进程。show follow-fork-mode 查看调试父进程还是子进程。set detach-on-fork [ on | off ] 设置调试模式。show detach-on-fork查看调试模式。默认为on表示调试当前进程的时候其他进程继续运行如果为off调试当前进程的时候其他进程会被GDB挂起。info inferiors 查看调试的进程inferior id 切换当前调试的进程detach inferiors id 使进程脱离GDB调试。 3、说说什么是大端小端如何判断大端小端 大端数据的高位数据存储在低的存储器地址数据的低位数据存储在高的存储器地址。 小端数据的低位数据存储在高的存储器地址数据的高位数据存储在低的存储器地址。 可以通过联合体判断因为联合体变量总是从低地址开始存储的。联合体第一个字段为字符类型第二个字段是整数类型。用这个联合体的对象给第二个整型字段赋值为1然后取第一个字符字段的值如果这个值是1那么系统是小端的如果是0就是大端的。 4、说说进程调度算法有哪些 1、先来先服务调度算法每次调度都是从后备进程队列中选择一个或者多个最先进入该队列的进程将他们调入内存为他们分配资源、创建进程然后放入就绪队列。 2、短作业(进程)优先调度算法短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业进程将它们调入内存运行。 3、高优先级优先调度算法当把该算法用于作业调度时系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时该算法是把处理机分配给就绪队列中优先权最高的进程。 4、时间片轮转法每次调度时把CPU 分配给队首进程并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时由一个计时器发出时钟中断请求调度程序便据此信号来停止该进程的执行并将它送往就绪队列的末尾然后再把处理机分配给就绪队列中新的队首进程同时也让它执行一个时间片。 5、多级反馈队列调度算法综合前面多种调度算法。 5、简述操作系统如何申请以及管理内存的 1、物理内存物理内存有四个层次分别是寄存器、高速缓存、主存和磁盘。操作系统会对物理内存进行管理有一个部分称为内存管理器(memory manager)它的主要工作是有效的管理内存记录哪些内存是正在使用的在进程需要时分配内存以及在进程完成时回收内存。 2、虚拟内存操作系统为每一个进程分配一个独立的地址空间作为虚拟内存。虚拟内存与物理内存存在映射关系通过页表寻址完成虚拟地址和物理地址的转换。 从操作系统角度来看进程分配内存有两种方式分别是brk和mmap。 6、简述Linux内核态和用户态什么时候会进入内核态 1、内核态与用户态内核态系统态与用户态是操作系统的两种运行级别。内核态拥有最高权限可以访问所有系统指令用户态则只能访问一部分指令。 2、什么时候进入内核态共有三种方式a、系统调用。b、异常。c、设备中断。其中系统调用是主动的另外两种是被动的。 3、为什么区分内核态与用户态在CPU的所有指令中有一些指令是非常危险的如果错用将导致整个系统崩溃。比如清内存、设置时钟等。所以区分内核态与用户态主要是出于安全的考虑。 7、简述LRU算法及其实现方法 1、LRULeast Recently Used算法LRU算法用于缓存淘汰。思路是将缓存中最近最少使用的对象删除掉。 2、实现方式利用链表和hashmap。当需要插入新的数据项的时候如果新数据项在链表中存在一般称为命中则把该节点移到链表头部如果不存在则新建一个节点放到链表头部若缓存满了则把链表最后一个节点删除即可。 在访问数据的时候如果数据项在链表中存在则把该节点移到链表头部否则返回-1.这样在链表尾部的节点就是最近最久未被访问的数据项。 8、一个线程占用多大内存 大概占用8M内存。 9、什么是页表为什么会有页表 页表是虚拟内存的概念。操作系统虚拟内存到物理内存的映射表就被称为页表。 原因不可能每一个虚拟内存的 Byte 都对应到物理内存的地址。这张表将大得真正的物理地址也放不下于是操作系统引入了页Page的概念。进行分页这样可以减小虚拟内存页对应物理内存页的映射表大小。 如果将每一个虚拟内存的 Byte 都对应到物理内存的地址每个条目最少需要 8字节32位虚拟地址-32位物理地址在 4G 内存的情况下就需要 32GB 的空间来存放对照表那么这张表就大得真正的物理地址也放不下了于是操作系统引入了页Page的概念。 在系统启动时操作系统将整个物理内存以 4K 为单位划分为各个页。之后进行内存分配时都以页为单位那么虚拟内存页对应物理内存页的映射表就大大减小了4G 内存只需要 8M 的映射表即可一些进程没有使用到的虚拟内存也并不需要保存映射关系而且Linux 还为大内存设计了多级页表可以进一页减少了内存消耗。 10、简述操作系统中的缺页中断 缺页异常malloc和mmap函数在分配内存时只是建立了进程虚拟地址空间并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时处理器自动触发一个缺页异常引发缺页中断。 缺页中断缺页异常后将产生一个缺页中断此时操作系统会根据页表中的外存地址在外存中找到所缺的一页将其调入内存。 缺页中断与一般中断的区别缺页中断与一般中断一样需要经历四个步骤保护CPU现场、分析中断原因、转入缺页中断处理程序、恢复CPU现场继续执行。 缺页中断与一般中断区别 1在指令执行期间产生和处理缺页中断信号 2一条指令在执行期间可能产生多次缺页中断 3缺页中断返回的是执行产生中断的一条指令而一般中断返回的是执行下一条指令。 11、说说虚拟内存分布什么时候会由用户态陷入内核态 1代码段.text存放程序执行代码的一块内存区域。只读代码段的头部还会包含一些只读的常数变量。 2数据段.data存放程序中已初始化的全局变量和静态变量的一块内存区域。 3BSS 段.bss存放程序中未初始化的全局变量和静态变量的一块内存区域。 4可执行程序在运行时又会多出两个区域堆区和栈区。 堆区动态申请内存用。堆从低地址向高地址增长。 栈区存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。 5最后还有一个共享区位于堆和栈之间。 共有三种方式进入内核态系统调用、异常、设备中断。系统调用是主动的。 12、简述一下虚拟内存和物理内存为什么要用虚拟内存好处是什么 1、虚拟内存和物理内存 物理内存物理内存有四个层次分别是寄存器、高速缓存、主存、磁盘。 寄存器速度最快、量少、价格贵。 高速缓存次之。 主存再次之。 磁盘速度最慢、量多、价格便宜。 操作系统会对物理内存进行管理有一个部分称为内存管理器(memory manager)它的主要工作是有效的管理内存记录哪些内存是正在使用的在进程需要时分配内存以及在进程完成时回收内存。 虚拟内存操作系统为每一个进程分配一个独立的地址空间但是虚拟内存。虚拟内存与物理内存存在映射关系通过页表寻址完成虚拟地址和物理地址的转换。 2、为什么要用虚拟内存 1进程地址空间不隔离。会导致数据被随意修改。 2内存使用效率低。 3程序运行的地址不确定。操作系统随机为进程分配内存空间所以程序运行的地址是不确定的。 3、使用虚拟内存的好处 1扩大地址空间。每个进程独占一个4G空间虽然真实物理内存没那么多。 2内存保护防止不同进程对物理内存的争夺和践踏可以对特定内存地址提供写保护防止恶意篡改。 3可以实现内存共享方便进程通信。 4可以避免内存碎片虽然物理内存可能不连续但映射到虚拟内存上可以连续。 4、使用虚拟内存的缺点 1虚拟内存需要额外构建数据结构占用空间。 2虚拟地址到物理地址的转换增加了执行时间。 3页面换入换出耗时。 4一页如果只有一部分数据浪费内存。 13、虚拟地址到物理地址是怎么映射的 操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构叫页表。页表中的每一项都记录了这个页的基地址。 14、说说堆栈溢出是什么会怎么样 堆栈溢出就是不顾堆栈中分配的局部数据块大小向该数据块写入了过多的数据导致数据越界。常指调用堆栈溢出本质上一种数据结构的满溢情况。堆栈溢出可以理解为两个方面堆溢出和栈溢出。 堆溢出比如不断的new 一个对象一直创建新的对象而不进行释放最终导致内存不足。将会报错OutOfMemory Error。 栈溢出一次函数调用中栈中将被依次压入参数返回地址等而方法如果递归比较深或进去死循环就会导致栈溢出。将会报错StackOverflow Error。 15、简述malloc的实现原理 当开辟的空间小于 128K 时调用 brk函数当开辟的空间大于 128K 时调用mmap。malloc采用的是内存池的管理方式以减少内存碎片。先申请大块内存作为堆区然后将堆区分为多个内存块。当用户申请内存时直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块每一个空闲块记录了一个未分配的、连续的内存地址。 16、说说并发和并行及其区别 并发对于单个CPU在一个时刻只有一个进程在运行但是线程的切换时间则减少到纳秒数量级多个任务不停来回快速切换。 并行对于多个CPU多个进程同时运行。 区别。通俗来讲它们虽然都说是多个进程同时运行但是它们的同时不是一个概念。并行的同时是同一时刻可以多个任务在运行(处于running)并发的同时是经过不同线程快速切换使得看上去多个任务同时都在运行的现象。 17、说说进程、线程、协程是什么他们的区别是什么 进程操作系统提供的抽象概念是系统进行资源分配和调度的基本单位是操作系统结构的基础。程序是指令、数据及其组织形式的描述进程是程序的实体。程序本身是没有生命周期的它只是存在磁盘上的一些指令,程序一旦运行就是进程。 线程是程序执行中一个单一的顺序控制流程是程序执行流的最小单元是处理器调度和分派的基本单位。一个进程可以有一个或多个线程同一进程中的多个线程将共享该进程中的全部系统资源如虚拟地址空间文件描述符和信号处理等等。 协程协程Coroutine又称微线程是一种比线程更加轻量级的存在协程不是被操作系统内核所管理而完全是由程序所控制。 线程和进程的区别 1、进程是资源的分配和调度的独立单元。进程拥有完整的虚拟地址空间当发生进程切换时不同的进程拥有不同的虚拟地址空间。而同一进程的多个线程共享同一地址空间不同进程之间的线程无法共享 2、线程是CPU调度的基本单元一个进程包含若干线程至少一个线程。 3、线程比进程小基本上不拥有系统资源。线程的创建和销毁所需要的时间比进程小很多 4、由于线程之间能够共享地址空间因此需要考虑同步和互斥操作 5、一个线程的意外终止会影响整个进程的正常运行但是一个进程的意外终止不会影响其他的进程的运行。因此多进程程序安全性更高。 线程与协程的区别 1协程执行效率极高。协程直接操作栈基本没有内核切换的开销所以上下文的切换非常快切换开销比线程更小。 2协程不需要多线程的锁机制因为多个协程从属于一个线程不存在同时写变量冲突效率比线程高。 3一个线程可以有多个协程。 18、说说Linux中的fork函数的作用 fork函数用来创建一个子进程。对于父进程fork函数返回新创建的子进程的PID对于子进程fork函数调用成功会返回0。如果出错返回-1。 #include unistd.h  pid_t fork(); fork()函数创建一个新进程后会为这个新进程分配进程空间将父进程的进程空间中的内容复制到子进程的进程空间中包括父进程的数据段和堆栈段并且和父进程共享代码段。这时候子进程和父进程一模一样都接受系统的调度。读时共享写时复制。 19、说说什么是孤儿进程什么是僵尸进程如何解决僵尸进程 孤儿进程是指一个父进程退出后而它的一个或多个子进程还在运行那么这些子进程将成为孤儿进程。孤儿进程将被init进程进程号为1所收养并且由init进程对它们完整状态收集工作。 僵尸进程是指一个进程使用fork函数创建子进程如果子进程退出而父进程并没有调用wait()或者waitpid()系统调用取得子进程的终止状态那么子进程的进程描述符仍然保存在系统中占用系统资源这种进程称为僵尸进程。 如何解决僵尸进程 1一般为了防止产生僵尸进程在fork子进程之后我们都要及时使用wait系统调用同时当子进程退出的时候内核都会给父进程一个SIGCHLD信号所以我们可以建立一个捕获SIGCHLD信号的信号处理函数在函数体中调用wait或waitpid就可以清理退出的子进程以达到防止僵尸进程的目的。 2使用kill命令。打开终端并输入 ps aux | grep Z 命令会输出所有的僵尸进程的详细内容然后输入kill -s SIGCHLD pid父进程pid。 20、说说什么是守护进程如何实现 守护进程是运行在后台的一种生存期长的特殊进程。它独立于控制终端处理一些系统级别任务。 如何实现 1创建子进程终止父进程。方法是调用fork() 产生一个子进程然后使父进程退出。 2调用setsid() 创建一个新会话。 3将当前目录更改为根目录修改工作目录。使用fork() 创建的子进程也继承了父进程的当前工作目录。 4重设文件权限掩码。文件权限掩码是指屏蔽掉文件权限中的对应位。 5关闭不再需要的文件描述符默认标准输入、标准输出、标准错误是打开的。子进程从父进程继承打开的文件描述符。 21、进程通信的方式有哪些 进程间通信主要包括管道、系统IPC包括消息队列、信号量、信号、共享内存、套接字socket内存映射区。 1、管道 无名管道PIPE内存文件管道是一种半双工的通信方式数据只能单向流动而且只能在具有亲缘关系的进程之间使用。进程的亲缘关系通常是指父子进程关系。 有名管道FIFO文件借助文件系统有名管道也是半双工的通信方式但是允许在没有亲缘关系的进程之间使用管道是先进先出的通信方式。 2、共享内存共享内存就是映射一段能被其他进程所访问的内存这段共享内存由一个进程创建但多个进程都可以访问。共享内存是最快的IPC方式它是针对其他进程间通信方式运行效率低而专门设计的。它往往与信号量配合使用来实现进程间的同步和通信。 3、消息队列消息队列是有消息的链表存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 4、套接字适用于不同机器间进程通信在本地也可作为两个进程通信的方式。 5、信号用于通知接收进程某个事件已经发生比如按下ctrl C就是信号。 6、信号量信号量是一个计数器可以用来控制多个进程对共享资源的访问。它常作为一种锁机制实现进程、线程的对临界区的同步及互斥访问。 22、说说进程同步的方式 信号量semaphore是一个计数器可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。P操作(递减操作)可以用于阻塞一个进程V操作(增加操作)可以用于解除阻塞一个进程。 管道一个进程通过调用管程的一个过程进入管程。在任何时候只能有一个进程在管程中执行调用管程的任何其他进程都被阻塞以等待管程可用。 消息队列消息的链接表放在内核中。消息队列独立于发送与接收进程进程终止时消息队列及其内容并不会被删除消息队列可以实现消息的随机查询可以按照消息的类型读取。 23、说说进程有多少种状态 进程有五种状态创建、就绪、执行、阻塞、终止。一个进程创建后被放入队列处于就绪状态等待操作系统调度执行执行过程中可能切换到阻塞状态并发任务完成后进程销毁终止。 1、创建状态 一个应用程序从系统上启动首先就是进入创建状态需要获取系统资源创建进程管理块PCBProcess Control Block完成资源分配。 2、就绪状态 在创建状态完成之后进程已经准备好处于就绪状态但是还未获得处理器资源无法运行。 3、运行状态 获取处理器资源被系统调度当具有时间片开始进入运行状态。如果进程的时间片用完了就进入就绪状态。 4、阻塞状态 在运行状态期间如果进行了阻塞的操作如耗时的I/O操作此时进程暂时无法操作就进入到了阻塞状态在这些操作完成后就进入就绪状态。等待再次获取处理器资源被系统调度当具有时间片就进入运行状态。 5、终止状态 进程结束或者被系统终止进入终止状态 24、进程通信中的管道实现原理是什么 操作系统在内核中开辟一块缓冲区称为管道用于通信。管道是一种两个进程间进行单向通信的机制。因为这种单向性管道又称为半双工管道所以其使用是有一定的局限性的。半双工是指数据只能由一个进程流向另一个进程一个管道负责读一个管道负责写如果是全双工通信需要建立两个管道。管道分为无名管道和命名管道无名管道只能用于具有亲缘关系的进程直接的通信父子进程或者兄弟进程可以看作一种特殊的文件管道本质是一种文件命名管道可以允许无亲缘关系进程间的通信。 25、简述mmap内存映射的原理和使用场景 mmap是一种内存映射文件的方法即将一个文件或者其它对象映射到进程的地址空间实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后进程就可以采用指针的方式读写操作这一段内存而系统会自动回写脏页面到对应的文件磁盘上即完成了对文件的操作而不必再调用read, write等系统调用函数。相反内核空间对这段区域的修改也直接反映用户空间从而可以实现不同进程间的文件共享。 使用场景 对同一块区域频繁读写操作 可用于实现用户空间和内核空间的高效交互 可提供进程间共享内存及相互通信 可实现高效的大规模数据传输。 26、协程是轻量级的线程轻量级表现在哪里 协程调用和切换比线程效率高协程执行效率极高。协程不需要多线程的锁机制可以不加锁的访问全局变量所以上下文的切换非常快。 协程占用内存少执行协程只需要极少的栈内存大概是45KB而默认情况下线程栈的大小为1MB。 切换开销更少协程直接操作栈基本没有内核切换的开销所以切换开销比线程少。 27、说说常见的信号有哪些表示什么含义 1号信号SIGHUP该信号让进程立即关闭.然后重新读取配置文件之后重启。 2号信号SIGINT程序中止信号用于中止前台进程。相当于输出 CtrlC 快捷键。 8号信号SIGFPE在发生致命的算术运算错误时发出。不仅包括浮点运算错误还包括溢出及除数为 0 等其他所有的算术运算错误。 9号信号SIGKILL用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。一般用于强制终止进程。 14号信号SIGALRM时钟定时信号计算的是实际的时间或者时钟时间。alarm函数使用该信号。 15号信号SIGTERM正常结束进程的信号kill 命令的默认信号。如果进程已经发生了问题那么这 个信号是无法正常中止进程的这时我们才会尝试 SIGKILL 信号也就是信号 9。 17号信号SIGCHLD子进程结束时父进程会收到这个信号。 18号信号SIGCONT该信号可以让暂停的进程恢复执行。本信号不能被阻断。 19号信号SIGSTOP该信号可以暂停前台进程相当于输入 CtrlZ 快捷键。本信号不能被阻断。该信号不能被阻塞、处理和忽略。 信号的 5 中默认处理动作 1、 Term 终止进程 2、 Ign 当前进程忽略掉这个信号 3、 Core 终止进程并生成一个Core文件 4、 Stop 暂停当前进程 5、 Cont 继续执行当前被暂停的进程 28、说说线程间通信的方式有哪些 线程间的通信方式包括临界区、互斥量、信号量、条件变量、读写锁 临界区每个线程中访问临界资源的那段代码称为临界区Critical Section临界资源是一次仅允许一个线程使用的共享资源。每次只准许一个线程进入临界区进入后不允许其他线程进入。不论是硬件临界资源还是软件临界资源多个线程必须互斥地对它进行访问。 互斥量采用互斥对象机制只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个所以可以保证公共资源不会被多个线程同时访问。 信号量计数器允许多个线程同时访问同一个资源。 条件变量通过条件变量通知操作的方式来保持多线程同步。 读写锁读写锁与互斥量类似。但互斥量要么是锁住状态要么就是不加锁状态。读写锁一次只允许一个线程写但允许一次多个线程读这样效率就比互斥锁要高。 29、说说线程同步方式有哪些 线程同步的实现方式主要有6种互斥锁、自旋锁、读写锁、条件变量、屏障、信号量。 1、互斥锁。互斥锁在访问共享资源前对互斥量进行加锁在访问完成后释放互斥量进行解锁。对互斥量加锁以后任何其他试图再次对互斥量加锁的线程都会被阻塞直至当前线程释放该互斥量。 2、自旋锁。自旋锁与互斥量类似但它不使线程进入阻塞态而是在获取锁之前一直占用CPU处于忙等自旋状态。自旋锁适用于锁被持有的时间短且线程不希望在重新调度上花费太多成本的情况。 3、读写锁。读写锁有三种状态读模式加锁、写模式加锁和不加锁一次只有一个线程可以占有写模式的读写锁但是多个线程可以同时占有读模式的读写锁。读写锁非常适合对数据结构读的次数远大于写的情况。 4、条件变量。条件变量允许线程睡眠直到满足某种条件当满足条件时可以向该线程发送信号通知并唤醒该线程。条件变量通常与互斥量配合一起使用。条件变量由互斥量保护线程在改变条件状态之前必须首先锁住互斥量其他线程在获得互斥量之前不会察觉到条件的改变因为必须在锁住互斥量之后它才可以计算条件是否发生变化。 5、屏障。屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待直到所有的合作线程都到达某一点然后从该点继续执行。 6、信号量。信号量本质上是一个计数器用于为多个进程提供共享数据对象的访问。编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限当信号量值大于 0 时则可以访问否则将阻塞。PV 原语是对信号量的操作一次 P 操作使信号量减一次 V 操作使信号量加。 30、说说什么是死锁 死锁: 是指多个进程在执行过程中因争夺资源而造成了互相等待。此时系统产生了死锁。比如两只羊过独木桥若两只羊互不相让争着过桥就产生死锁。 死锁发生的场景1、没有释放锁。2、重复加锁。一个线程加了一个锁之后在没有释放锁之前又加了锁。3、多线程多锁抢占锁资源。每个共享资源都会有一个锁进行同步控制当有多个线程且对多个不同的共享资源进行操作此时多线程之间可能会抢占锁资源。比如一个锁被一个线程抢占了继续执行时有需要另一个锁但是在此之前这个锁被其他线程占用了并且这个线程刚好需要第一个锁此时就会造成死锁。 产生条件四个必要条件 1互斥条件进程对所分配到的资源不允许其他进程访问若其他进程访问只能等待直到进程使用完成后释放该资源 2请求保持条件进程获得一定资源后又对其他资源发出请求但该资源被其他进程占有此时请求阻塞而且该进程不会释放自己已经占有的资源 3不可剥夺条件进程已获得的资源只能自己释放不可剥夺 4环路等待条件若干进程之间形成一种头尾相接的循环等待资源关系。 避免死锁 1破坏请求和保持条件在系统中不允许进程在已获得某种资源的情况下申请其他资源即要想出一个办法阻止进程在持有资源的同时申请其它资源。 2破坏不可抢占条件允许对资源实行抢夺。 3破坏循环等待条件。 避免多次锁定, 多检查 对共享资源访问完毕之后, 一定要解锁或者在加锁的使用 trylock 如果程序中有多把锁, 可以控制对锁的访问顺序(顺序访问共享资源但在有些情况下是做不到的)另外也可以在对其他互斥锁做加锁操作之前先释放当前线程拥有的互斥锁。 项目程序中可以引入一些专门用于死锁检测的模块 31、有了进程为什么还要线程 原因 进程在早期的多任务操作系统中是基本的执行单元。每次进程切换都要先保存进程资源然后再恢复这称为上下文切换。但是进程频繁切换将引起额外开销从而严重影响系统的性能。为了减少进程切换的开销人们把两个任务放到一个进程中每个任务用一个更小粒度的执行单元来实现并发执行这就是线程。 线程与进程对比 1进程间的信息难以共享。由于除去只读代码段外父子进程并未共享内存因此必须采用一些进程间通信方式在进程间进行信息交换。 但多个线程共享进程的内存如代码段、数据段、扩展段线程间进行信息交换十分方便。 2调用 fork() 来创建进程的代价相对较高即便利用写时复制技术仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性这意味着 fork() 调用在时间上的开销依然不菲。 但创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的无需采用写时复制来复制内存也无需复制页表。 32、在单核机器上写多线程程序是否需要考虑加锁为什么 需要加锁。 原因因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序仍然存在线程同步的问题。因为在抢占式操作系统中通常为每个线程分配一个时间片当某个线程时间片耗尽时操作系统会将其挂起然后运行另一个线程。如果这两个线程共享某些数据不使用线程锁的前提下可能会导致共享数据修改引起冲突。 33、简述互斥锁的机制互斥锁与读写锁的区别 互斥锁机制mutex用于保证在任何时刻都只能有一个线程访问该对象。当获取锁操作失败时线程会进入睡眠等待锁释放时被唤醒。 互斥锁和读写锁 1 读写锁区分读者和写者而互斥锁不区分。 2互斥锁同一时间只允许一个线程访问该对象无论读写读写锁同一时间内只允许一个写者但是允许多个读者同时读对象。 34、说说什么是信号量有什么作用 概念信号量本质上是一个计数器用于多进程对共享数据对象的读取它主要是用来保护共享资源信号量也属于临界资源使得资源在一个时刻只有一个进程独享。 原理由于信号量只能进行两种操作等待和发送信号即P(sv)和V(sv)具体的行为如下 1P(sv)操作如果sv的值大于零就给它减1如果它的值为零就挂起该进程的执行信号量的值为正进程获得该资源的使用权进程将信号量减1表示它使用了一个资源单位。 2V(sv)操作如果有其他进程因等待sv而被挂起就让它恢复运行如果没有进程因等待sv而挂起就给它加1若此时信号量的值为0则进程进入挂起状态直到信号量的值大于0若进程被唤醒则返回至第一步。 作用用于多进程对共享数据对象的读取它主要是用来保护共享资源信号量也属于临界资源使得资源在一个时刻只有一个进程独享。 35、什么是上下文切换进程、线程的切换过程是什么 上下文切换指的是操作系统停止当前运行进程从运行态改变成其它状态并且调度其它进程就绪态转变成运行态。操作系统必须在切换之前存储许多部分的进程上下文必须能够在之后恢复他们所以进程不能显示它曾经被暂停过同时切换上下文这个过程必须快速因为上下文切换操作是非常频繁的。 上下文指的是任务所有共享资源的工作现场每一个共享资源都有一个工作现场包括用于处理函数调用局部变量分配以及工作现场保护的栈顶指针和用于指令执行等功能的各种寄存器。 1、进程上下文切换 1保护被中断进程的处理器现场信息 2修改被中断进程的进程控制块有关信息如进程状态等 3把被中断进程的进程控制块加入有关队列 4选择下一个占有处理器运行的进程 5根据被选中进程设置操作系统用到的地址转换和存储保护信息 切换页目录以使用新的地址空间 切换内核栈和硬件上下文包括分配的内存数据段堆栈段等 6根据被选中进程恢复处理器现场 2、线程上下文切换 1保护被中断线程的处理器现场信息 2修改被中断线程的线程控制块有关信息如线程状态等 3把被中断线程的线程控制块加入有关队列 4选择下一个占有处理器运行的线程 5根据被选中线程设置操作系统用到的存储保护信息 切换内核栈和硬件上下文切换堆栈以及各寄存器 6根据被选中线程恢复处理器现场 35、线程之间私有和共享的资源有哪些 私有每个线程都有独立的私有的栈区程序计数器栈指针以及函数运行使用的寄存器。 共有代码区堆区 35、线程是如何实现的 用户线程在用户空间实现的线程机制它不依赖于操作系统的内核由一组用户级的线程库函数来完成线程的管理包括进程的创建终止同步和调度等。 内核线程是指在操作系统的内核中实现的一种线程机制由操作系统的内核来完成线程的创建终止和管理。 36、自旋锁和互斥锁的使用场景是什么 互斥锁用于临界区持锁时间比较长的操作比如下面这些情况都可以考虑 1临界区有IO操作 2临界区代码复杂或者循环量大 3临界区竞争非常激烈 4单核处理器 自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下。 37、说说sleep和wait的区别 sleep是一个延时函数让进程或线程进入休眠。休眠完毕后继续运行。 在linux下面sleep函数的参数是秒而windows下面sleep的函数参数是毫秒。 wait是父进程回收子进程PCB资源的一个系统调用。进程一旦调用了wait函数就立即阻塞自己本身然后由wait函数自动分析当前进程的某个子进程是否已经退出当找到一个已经变成僵尸的子进程wait就会收集这个子进程的信息并把它彻底销毁后返回如果没有找到这样一个子进程wait就会一直阻塞直到有一个出现为止。 区别 1sleep是一个延时函数让进程或线程进入休眠。休眠完毕后继续运行。 2wait是父进程回收子进程PCBProcess Control Block资源的一个系统调用。 38、简述Linux零拷贝的原理 所谓「零拷贝」描述的是计算机操作系统当中CPU不执行将数据从一个内存区域拷贝到另外一个内存区域的任务。通过网络传输文件时这样通常可以节省 CPU 周期和内存带宽。 零拷贝的好处 1节省了 CPU 周期空出的 CPU 可以完成更多其他的任务 2减少了内存区域之间数据拷贝节省内存带宽 3减少用户态和内核态之间数据拷贝提升数据传输效率 4应用零拷贝技术减少用户态和内核态之间的上下文切换 在传统 IO 中用户态空间与内核态空间之间的复制是完全不必要的因为用户态空间仅仅起到了一种数据转存媒介的作用除此之外没有做任何事情。 1Linux提供了sendfile( ) 用来减少数据拷贝和上下文切换次数。 a. 发起 sendfile() 系统调用操作系统由用户态空间切换到内核态空间第一次上下文切换 b. 通过 DMA 引擎将数据从磁盘拷贝到内核态空间的输入的 socket 缓冲区中第一次拷贝 c. 将数据从内核空间拷贝到与之关联的 socket 缓冲区第二次拷贝 d. 将 socket 缓冲区的数据拷贝到协议引擎中第三次拷贝 e. sendfile() 系统调用结束操作系统由内核态空间切换到用户态空间第二次上下文切换 根据以上过程一共有 2 次的上下文切换3 次的 I/O 拷贝。我们看到从用户空间到内核空间并没有出现数据拷贝从操作系统角度来看这个就是零拷贝。内核空间出现了复制的原因: 通常的硬件在通过DMA访问时期望的是连续的内存空间。 2mmap数据零拷贝。 39、内存交换和覆盖有什么区别 内存覆盖程序运行时并非任何时候都要访问程序及数据的各个部分尤其是大程序因此可以把用户空间分为一个固定区和若干个覆盖区。将经常活跃的部分放在固定区其余部分按照调用关系分段首先将那些即将要访问的段放入覆盖区其他段放在外存中在需要调用前系统将其调入覆盖区替换覆盖区中原有的段。 内存交换内存空间紧张时系统将内存中某些进程暂时换出外存把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)。 换入把准备好竞争CPU运行的程序从辅存移到内存。 换出把处于等待状态或CPU调度原则下被剥夺运行权力的程序从内存移到辅存把内存空间腾出来。中级调度策略就是釆用交换技术。 与覆盖技术相比交换不要求程序员给出程序段之间的覆盖结构而且交换主要是在进程或作业之间进行而覆盖则主要在同一个作业或进程中进行。另外覆盖只能覆盖与覆盖程序段无关的程序段。 40、虚拟技术你了解吗 虚拟技术把一个物理实体转换为多个逻辑实体。 主要有两种虚拟技术时时间分复用技术和空空间分复用技术。 多进程与多线程多个进程能在同一个处理器上并发执行使用了时分复用技术让每个进程轮流占用处理器每次只执行一小个时间片并快速切换。 虚拟内存使用了空分复用技术它将物理内存抽象为地址空间每个进程都有各自的地址空间。地址空间的页被映射到物理内存地址空间的页并不需要全部在物理内存中当使用到一个没有在物理内存的页时执行页面置换算法将该页置换到内存中。 41、mmap内存映射区的原理 主要分为三个阶段 1、进程启动映射过程并在虚拟地址空间中为映射创建虚拟映射区域。 进程在用户空间调用库函数mmap原型void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)。在当前进程的虚拟地址空间中寻找一段空闲的满足要求的连续的虚拟地址。为此虚拟区分配一个vm_area_struct结构接着对这个结构的各个域进行了初始化。将新建的虚拟区结构vm_area_struct插入进程的虚拟地址区域链表或树中。 2、调用内核空间的系统调用函数mmap不同于用户空间函数实现文件物理地址和进程虚拟地址的一一映射关系。 为映射分配了新的虚拟地址区域后通过待映射的文件指针在文件描述符表中找到对应的文件描述符通过文件描述符链接到内核“已打开文件集”中该文件的文件结构体struct file每个文件结构体维护着和这个已打开文件相关各项信息。 通过该文件的文件结构体链接到file_operations模块调用内核函数mmap其原型为int mmap(struct file *filp, struct vm_area_struct *vma)不同于用户空间库函数。 内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。 通过remap_pfn_range函数建立页表即实现了文件地址和虚拟地址区域的映射关系。此时这片虚拟地址并没有任何数据关联到主存中。 3、进程发起对这片映射空间的访问引发缺页异常实现文件内容到物理内存主存的拷贝。 进程的读或写操作访问虚拟地址空间这一段映射地址通过查询页表发现这一段地址并不在物理页面上。因为目前只建立了地址映射真正的硬盘数据还没有拷贝到内存中因此引发缺页异常。 缺页异常进行一系列判断确定无非法操作后内核发起请求调页过程。 调页过程先在交换缓存空间swap cache中寻找需要访问的内存页如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。 之后进程即可对这片主存进行读或者写的操作如果写操作改变了其内容一定时间后系统会自动回写脏页面到对应磁盘地址也即完成了写入到文件的过程。 42、虚拟内存的作用 虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存从而让程序获得更多的可用内存。 为了更好的管理内存操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间这个地址空间被分割成多个块每一块称为一页。 这些页被映射到物理内存但不需要映射到连续的物理内存也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时由硬件执行必要的映射将缺失的部分装入物理内存并重新执行失败的指令。 从上面的描述中可以看出虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存也就是说一个程序不需要全部调入内存就可以运行这使得有限的内存运行大程序成为可能。 例如有一台计算机可以产生 16 位地址那么一个程序的地址空间范围是 0~64K。该计算机只有 32KB 的物理内存虚拟内存技术允许该计算机运行一个 64K 大小的程序。 43、介绍几种典型的线程锁 读写锁 1、多个读者可以同时进行读 2、写者必须互斥只允许一个写者写也不能读者写者同时进行 3、写者优先于读者一旦有写者则后续读者必须等待唤醒时优先考虑写者 互斥锁 一次只能一个线程拥有互斥锁其他线程只有等待。 互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒而操作系统负责线程调度为了实现锁的状态发生改变时唤醒阻塞的线程或者进程需要把锁交给操作系统管理所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的加锁的时间大概100ns左右而实际上互斥锁的一种可能的实现是先自旋一段时间当自旋的时间超过阀值之后再将线程投入睡眠中因此在并发运算中使用互斥锁每次占用锁的时间很短的效果可能不亚于使用自旋锁。 条件变量 互斥锁一个明显的缺点是他只有两种状态锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足他常和互斥锁一起使用以免出现竞态条件。当条件不满足时线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制条件变量则是同步机制。 自旋锁 如果线程无法取得锁线程不会立刻放弃CPU时间片而是一直循环尝试获取锁直到获取为止。如果别的线程长时期占有锁那么自旋就是在浪费CPU做无用功但是自旋锁一般应用于加锁时间很短的场景这个时候效率比较高。 44、快表是什么 快表又称联想寄存器(TLB) 是一种访问速度比内存快很多的高速缓冲存储器用来存放当前访问的若干页表项以加速地址变换的过程。与此对应内存中的页表常称为慢表。 45、进程和线程两者区别 进程Process是操作系统的一个重要概念它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程进程可以包括一个或多个线程。进程之间是相对独立的一个进程无法访问另一个进程的数据除非利用分布式计算方式一个进程运行的失败也不会影响其他进程的运行Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。是应用程序的一个运行例程是应用程序的一次动态执行过程。 线程Thread是进程中的基本执行单元是操作系统分配CPU时间的基本单位一个进程可以包含若干个线程在进程入口执行的第一个线程被视为这个进程的主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器Thread Local StorageTLS组成的。CPU寄存器主要记录当前所执行线程的状态调用栈主要用于维护线程所调用到的内存与数据TLS主要用于存放线程的状态信息。 两者区别 1、一个程序至少有一个进程一个进程至少有一个线程。 2、线程的划分尺度小于进程使得多线程程序的并发性高。 3、进程在执行过程中拥有独立的内存单元虚拟地址空间而多个线程共享内存从而极大地提高了程序的运行效率。 4、线程在执行过程中与进程还是有区别的。每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行必须依存在应用程序中由应用程序提供多个线程执行控制。 5、从逻辑角度来看多线程的意义在于一个应用程序中有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。 46、同步与异步、阻塞与非阻塞 IO读取数据分为阶段第一个阶段是内核准备好数据第二个阶段是内核把数据从内核态拷贝到用户态。比如一个网络读写操作。服务器等待接收来自客户端的数据如果客户端一直没有发送数据那么内核中是没有数据的这就是第一个阶段如果接收到了数据会将数据存放在内核缓冲区中准备好后向用户态发出拷贝这就是第二个阶段。 阻塞IO是当用户调用read系统调用后用户线程会被阻塞等内核数据准备好后并且从内核缓冲区拷贝到用户缓存区后read才会返回并继续后续操作。阻塞IO是在第一个阶段和第二个阶段都会阻塞没有数据第一个阶段也会等待阻塞。 非阻塞IO是调用read后如果内核没有数据就立马返回需要通过不断轮询的方式去调用read否则直接就跳过了读取数据的操作直到数据被拷贝到用户态的应用程序缓冲区read请求才获取到结果。非阻塞IO阻塞的是第二个阶段第一阶段没有数据时不会阻塞第二阶段等待内核把数据从内核态拷贝到用户态的过程中才会阻塞。所以非阻塞IO不是不阻塞而是在内核中没有数据的时候才是不阻塞有数据往用户区拷贝时是阻塞的。 比如在epoll多路复用模型中的边沿触发模式使用的就是同步非阻塞当有读事件触发时就说明内核区有数据了第一阶段满足此时可以去调用read函数读取且需要循环读取因为边沿触发模式不会因为没有读取完而再次触发事件。 同步IO是应用程序发起一个IO操作后必须等待内核把IO操作第一个阶段没有数据不存在IO操作处理完成后才返回。无论是阻塞IO还是非阻塞IO都是同步的因为在read调用时第二个阶段内核将数据从内核空间拷贝到用户空间的过程都是需要等待的。某种程度上都是阻塞操作。 异步IO是应用程序发起一个IO操作后调用者不能立刻得到结果而是在内核完成IO操作后通过信号或者回调来通知调用者。异步IO是内核数据准备好和数据从内核拷贝到用户态这两个阶段都不需要等待。所以只有同步才有阻塞和非阻塞之分异步必定是非阻塞的。异步IO也是需要控制的如果不加以控制可能导致异步IO没有执行完程序就结束了那么异步IO也会结束。如下程序如果不等待异步操作完成那么程序直接完后走就结束了执行时可能出现读取了一部分数据也有可能没有读取任何数据情况是未知的。 47、异步的优缺点 因为异步操作无须额外的线程负担并且使用回调的方式进行处理在设计良好的情况下处理函数可以不必使用共享变量即使无法完全不用最起码可以减少 共享变量的数量减少了死锁的可能。 编写异步操作的复杂程度较高程序主要使用回调方式进行处理与普通人的思维方式有些出入而且难以调试。 50、Linux基础命令 所有的linux命令基本是都是command [参数] [作用对象] ls [-a -l -h] 不加任何参数以平铺展示当前目录中的所有文件-l以列表形式展示-a展示所有包括隐藏文件-h以人性化的方式展示需要与-l并用可以有数据单位。 cd [路径] 切换到某一个目录中。不需要任何参数。如果直接cd回车则回到HOME目录/home/yoyo pwd 不需要参数也不要作用对象直接打印出当前的工作目录 mkdir [-p] 路径 用于创建目录。如果要创建多级目录需要加上-p参数。 touch 文件名 用于创建文件不需要任何参数。 cat 文件路径 输出指定文件的全部内容不能是目录作为作用对象 more 文件路径 类似于cat支持翻页如果文件内容超过了一页不会全部显示通过空格翻页q退出查看。 cp [-r] 参数1 参数2 将参数1拷贝到参数2如果是文件夹拷贝需要增加-r mv 参数1 参数2 将参数1移动到参数2的位置可以是文件或者是目录 rm [-r -f] 参数1….参数n 删除删除文件或者文件夹-r表示删除目录-f强制删除不会弹出提示确认信息。多个参数表示可以指定删除多个目录或者文件夹 which 命令 查找指定命令所在执行文件的路径 find 起始路径 -name “被查找的文件” 按照文件名在指定的起始路径一下找指定的文件所在的路径递归搜索 可以不按照文件名搜索 -size |-n[kMG] 按照文件大小搜索 -type f|c|d|p…      按照文件类型搜索 grep [-n] “搜索的内容” 文件名 在文件中搜索指定的内容并把搜索到内容的所在行输出加上参数-n可以显示匹配行所在的行号 wc [-c -m -l -w] 文件名 做数量统计。-c 统计文件的bytes字节数-m 统计文件中的字符数-l 统计文件中的行数-w统计文件中的单词数。 echo 输出的内容 直接将内容输出。如果输出命令的直接结果echo pwd输出pwd命令直接后的结果 重定向符和 覆盖写 追加写 tail [-f -num] 文件名 查看文件中的尾部内容并且跟踪文件的最新更改常用于日志跟踪。 tail -f xxx 表示持续跟踪文件尾部内容 -num 表示查看尾部多少行 history 查看历史记录 51、Linux系统命令 systemctl start | stop | status | enable | disable 服务名 控制服务的启动关闭等。 ln -s 参数1 参数2 -s表示创建软连接默认硬链接。参数1表示被连接的文件或者文件夹参数2是目的地 date [-d] [格式化字符串] 显示日期时间也可以根据自定义格式显示 hostname   hostnamectl set-hostname 主机名 hostname显示主机名 hostnamectl set-hostname 主机名  修改主机名 wget [-b] url 下载网络文件-b表示后台下载会将日志写入到当前工作目录的wget-log文件中。 curl [-O] url 可以发送http网络请求用于下载文件、获取信息等。-O用于下载文件。 nmap ip地址 查看指定ip的对外暴露的端口 netstat -anp 查看网络状态 ps [-e -f] 默认显示当前终端中存在的进程-e 显示全部的进程 -f以完全格式化的形式展示信息 kill [-9] 进程ID -9表示强制关闭进程。 top 通过top命令查看CPU进程、内存使用情况类似Windows的任务管理器 默认每5秒刷新一次语法直接输入top即可按q或ctrl c退出 df [-h] 查看磁盘的使用情况已用、可用等信息-h以更加人性化的方式展示 iostat [-x][num1][num2] 查看CPU、磁盘的相关信息。选项-x显示更多信息  num1数字刷新间隔num2数字刷新几次 网络监控状态 使用sar命令查看网络的相关统计 语法sar -n DEV num1 num2 选项-n查看网络DEV表示查看网络接口 num1刷新间隔不填就查看一次结束num2查看次数不填无限次数 rz  sz 分别表示上传下载   52、Linux权限命令 su switch user 切换用户的命令。su – username 切换用户-表示切换用户后加载环境变量可选操作。通过ctrl d退回到之前的用户 getent getent passwd 查看当前系统中有哪些用户getent group查看系统全部组信息 chmod [-R] 权限 文件或者文件夹 修改文件或者文件夹的权限。-R表示对文件夹内部的全部内容执行相同操作。chmod urwx, grx, ox hello.txt   chmod 751 hello.txt chown [-R] [用户][:][用户组] 文件或者文件夹 选项-R同chmod对文件夹内全部内容应用相同规则 用户修改所属用户 用户组修改所属用户组 : 用于分隔用户和用户组 chown root hello.txt将hello.txt所属用户修改为root chown :root hello.txt将hello.txt所属用户组修改为root 53、谈谈对线程池的理解 线程池是创建一堆就绪状态线程的池化技术避免线程的重复创建和销毁带来的性能损耗。使用线程池可以降低资源消耗提高响应速度并且方便对线程的管理。 在项目中也自定义过线程池当时使用了C11的新特性主要有包装器functional、多线程和泛型技术。这也是为了能够更加让线程池能够更加通用化。其中我定义了三个主要的类首先是任务模板类一个任务类中封装了需要执行的操作及其所需要的参数这个操作使用包装器funcational来封装要求传入的回调函数是一个返回值为void参数类型是泛型的实参。在这个类中还提供了执行任务的接口便于外部的调用。 其次设计了一个任务队列的类这个类中用于存放传入的多个任务对象是通过c中的队列管理的。在这个类中还提供了多个接口包括增加任务、判断任务队列是为空和取出任务等操作。因为多个线程之间会竞相从任务队列中获取队列所以在任务队列中取出数据时需要用互斥锁来进行控制。 最后是一个线程池类线程池类中封装了一些线程池的状态信息比如初始化时创建最小创建的线程个数线程池最大能容纳多少线程正在忙碌中的线程个数和正在工作中的线程个数等等。此外还会开启一个管理者线程这个线程主要是为了管理工作线程的主要功能是当任务量很多时增加工作线程个数或者当任务量很少减少存活的线程个数。工作线程则有若干个用于获取任务队列中的任务进行执行。 九、MySQL MySQL的架构分为两层Server层和存储引擎层。 Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现主要包括连接器查询缓存、解析器、预处理器、优化器、执行器等。另外所有的内置函数如日期、时间、数学和加密函数等和所有跨存储引擎的功能如存储过程、触发器、视图等。都在 Server 层实现。 存储引擎层负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎不同的存储引擎共用一个 Server 层。现在最常用的存储引擎是 InnoDB从 MySQL 5.5 版本开始 InnoDB 成为了 MySQL 的默认存储引擎。我们常说的索引数据结构就是由存储引擎层实现的不同的存储引擎支持的索引类型也不相同比如 InnoDB 支持索引类型是 B树 且是默认使用也就是说在数据表中创建的主键索引和二级索引默认使用的是 B 树索引。 1、执行一条select语句期间发生了什么 1、连接器 与mysql服务建立连接tcp连接过程需要三次握手。如果用户名密码出错就会收到一个Access denied for user的错误然后客户端程序结束运行。如果用户名密码正常连接器就会获取该用户的权限然后保存起来后续该用户在此链接上的操作都会给予连接开始时读到的权限进行逻辑判断。所以如果对已经建立连接的用户权限修改也不会影响本次连接的权限。 MySQL连接数是有限制的。可以通过命令show variables like ‘max_connection’ 命令查看最大连接数。 长连接与短连接共用一条tcp连接执行一条sql语句之后就断开连接这就是短连接在断开连接之前可以执行多条sql就是长连接。一般推荐使用长连接可以减少资源消耗。但是长连接可能会占用内存增多因为mysql在执行查询过程中临时使用内存管理连接对象这些链接对象资源只有在断开连接的时候才会释放。如果长连接累计很多将导致mysql服务占用内存增大有可能会被系统强制杀死会出现mysql服务异常重启的现象。 解决长连接的问题1、定期断开长连接。释放内存资源。2、客户端主动重置连接。MySQL5.7实现了mysql_reset_connection()函数接口当客户端执行了一个很大的操作后在代码里调用mysql_reset_connection函数来重置连接达到释放内存的效果。这个过程不需要重连和重新做权限验证会将连接恢复到刚刚创建完成时的状态。 连接器工作总结 与客户端进行tcp三次握手建立连接校验客户端的用户名和密码如果出错则会报错如果用户名和密码都正确会读取该用户的权限后续操作基于该权限。 2、查询缓存 连接器工作完成后就进入了mysql的工作环境。客户端就可以向MySQL服务发送SQL语句了MySQL服务收到SQL语句后就会解析出SQL语句的第一个字段看看是什么类型的语句。 如果是查询语句selectMySQL就会先去查询缓存中查找缓存数据看看之前有没有执行这一条命令这个查询缓存是以key-value形式保存在内存中的key为SQL语句value是查询结果。 如果查询的语句命中查询缓存那么就会直接返回 value 给客户端。如果查询的语句没有命中查询缓存中那么就要往下继续执行等执行完后查询的结果就会被存入查询缓存中。对于频繁更新的表查询缓存的命中率很低因为只要有一个表有更新操作那么这个表的查询缓存就会被清空。所以查询缓存显得很鸡肋在MySQL8.0直接删除了查询缓存将不再走这个阶段。对于 MySQL 8.0 之前的版本如果想关闭查询缓存我们可以通过将参数 query_cache_type 设置成 DEMAND。 3、解析器解析SQL 在正式执行 SQL 查询语句之前 MySQL 会先对 SQL 语句做解析这个工作交由「解析器」来完成。 解析器主要做两件事1、词法分析。MySQL会根据你输入的字符串识别出关键字构建出SQL语法树方便后面模块获取SQL类型、表名、字段名、where条件等等。2、语法分析。根据词法分析的结果语法解析器会根据语法规则判断输入的这个SQL语句是否满足MySQL语法。如果输入的语法不对解析器会在这个阶段报错。不检查表名是否存在解析器只做语法检查 4、执行SQL 经过解析器后接着就要进入执行 SQL 查询语句的流程了每条SELECT 查询语句流程主要可以分为下面这三个阶段 prepare阶段预处理阶段 optimize阶段优化阶段 execute阶段执行阶段。 1、预处理器 a、检查SQL查询语句中的表或者字段是否存在 b、将select * 中的 * 符号扩展为表上所有的列。 2、优化器 经过预处理阶段后需要为SQL查询语句先指定一个执行计划优化器。优化器主要负责将SQL查询语句的执行方案确定下来比如在表里面有多个索引的时候优化器会基于查询成本的考虑来决定使用哪个索引。 当查询语句为select * from students where student_id 1 很简单选择的就是主键索引。如果想要知道使用的是哪个索引可以在这条语句前面加上explain关键字。 如果表中没有主键或者设置索引那么就会全表扫描这种查询扫描效率最低type all。 在students表中只有student_id这个主键一级索引现在将其中的last_name字段设置为普通索引二级索引create index sname on students (last_name); 此时执行select student_id from students student_id 1 and last_name like ‘J%’; 这条查询语句的结果既可以使用主键索引也可以使用普通索引但是执行的效率会不同。这时就需要优化器来决定使用哪个索引了。 很显然这条查询语句是覆盖索引直接在二级索引就能查找到结果因为二级索引的 B 树的叶子节点的数据存储的是主键值就没必要在主键索引查找了因为查询主键索引的 B 树的成本会比查询二级索引的 B 的成本大优化器基于查询成本的考虑会选择查询代价小的普通索引。 3、执行器 经历完优化器后就确定了执行方案接下来 MySQL 就真正开始执行语句了这个工作是由「执行器」完成的。在执行的过程中执行器就会和存储引擎交互了交互是以记录为单位的。有主键索引查询、全表扫描、索引下推等。 总结 执行一条SQL查询语句期间发生了 连接器建立连接管理连接、校验用户身份查询缓存查询语句如果命中查询缓存则直接返回否则继续往下执行。MySQL 8.0 已删除该模块解析 SQL通过解析器对 SQL 查询语句进行词法分析、语法分析然后构建语法树方便后续模块读取表名、字段、语句类型执行 SQL执行 SQL 共有三个阶段预处理阶段检查表或字段是否存在将 select * 中的 * 符号扩展为表上的所有列。 优化阶段基于查询成本的考虑选择查询成本最小的执行计划 执行阶段根据执行计划执行 SQL 查询语句从存储引擎读取记录返回给客户端。 扩展执行器中的主键索引查询、全表扫描和索引下推执行过程分析 1、主键索引查询 语句select * from students where student_id 1; 这条查询语句的查询条件用到了主键索引而且是等值查询同时主键 id 是唯一不会有 id 相同的记录所以优化器决定选用访问类型为 const 进行查询也就是使用主键索引查询一条记录那么执行器与存储引擎的执行流程是这样的 a、执行器第一次查询会调用 read_first_record 函数指针指向的函数因为优化器选择的访问类型为 const这个函数指针被指向为 InnoDB 引擎索引查询的接口把条件 id 1 交给存储引擎让存储引擎定位符合条件的第一条记录。 b、存储引擎通过主键索引的 B 树结构定位到 id 1的第一条记录如果记录是不存在的就会向执行器上报记录找不到的错误然后查询结束。如果记录是存在的就会将记录返回给执行器 c、执行器从存储引擎读到记录后接着判断记录是否符合查询条件如果符合则发送给客户端如果不符合则跳过该记录。 d、执行器查询的过程是一个 while 循环所以还会再查一次但是这次因为不是第一次查询了所以会调用 read_record 函数指针指向的函数因为优化器选择的访问类型为 const这个函数指针被指向为一个永远返回 - 1 的函数所以当调用该函数的时候执行器就退出循环也就是结束查询了。 至此这个语句就执行完成了。 2、全表扫描 语句select * from students where first_name ‘Jone’; 这条查询语句的查询条件没有用到索引所以优化器决定选用访问类型为 ALL 进行查询也就是全表扫描的方式查询那么这时执行器与存储引擎的执行流程是这样的 a、执行器第一次查询会调用 read_first_record 函数指针指向的函数因为优化器选择的访问类型为 all这个函数指针被指向为 InnoDB 引擎全扫描的接口让存储引擎读取表中的第一条记录 b、执行器会判断读到的这条记录的 name 是不是 iphone如果不是则跳过如果是则将记录发给客户端Server 层每从存储引擎读到一条记录就会发送给客户端之所以客户端显示的时候是直接显示所有记录的是因为客户端是等查询语句查询完成后才会显示出所有的记录。 c、执行器查询的过程是一个 while 循环所以还会再查一次会调用 read_record 函数指针指向的函数因为优化器选择的访问类型为 allread_record 函数指针指向的还是 InnoDB 引擎全扫描的接口所以接着向存储引擎层要求继续读刚才那条记录的下一条记录存储引擎把下一条记录取出后就将其返回给执行器Server层执行器继续判断条件不符合查询条件即跳过该记录否则发送到客户端 d、一直重复上述过程直到存储引擎把表中的所有记录读完然后向执行器Server层 返回了读取完毕的信息 e、执行器收到存储引擎报告的查询完毕的信息退出循环停止查询。 3、索引下推 索引下推能够减少二级索引在查询时的回表操作提高查询的效率因为它将 Server 层部分负责的事情交给存储引擎层去处理了。 假设有select * from t_user  where age 20 and reward 100000; age和reward是联合索引。 联合索引当遇到范围查询 (、) 就会停止匹配也就是 age 字段能用到联合索引但是 reward 字段则无法利用到索引。 那么不使用索引下推MySQL 5.6 之前的版本时执行器与存储引擎的执行流程是这样的 1、Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录也就是定位到 age 20 的第一条记录 2、存储引擎根据二级索引的 B 树快速定位到这条记录后获取主键值然后进行回表操作将完整的记录返回给 Server 层 3、Server 层在判断该记录的 reward 是否等于 100000如果成立则将其发送给客户端否则跳过该记录 4、接着继续向存储引擎索要下一条记录存储引擎在二级索引定位到记录后获取主键值然后回表操作将完整的记录返回给 Server 层 如此往复直到存储引擎把表中的所有记录读完。 没有索引下推的时候每查询到一条二级索引记录都要进行回表操作然后将记录返回给 Server接着 Server 再判断该记录的 reward 是否等于 100000。 1、Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录也就是定位到 age 20 的第一条记录 2、存储引擎定位到二级索引后先不执行回表操作而是先判断一下该索引中包含的列reward列的条件reward 是否等于 100000是否成立。如果条件不成立则直接跳过该二级索引。如果成立则执行回表操作将完成记录返回给 Server 层。 3、Server 层再判断其他的查询条件本次查询没有其他条件是否成立如果成立则将其发送给客户端否则跳过该记录然后向存储引擎索要下一条记录。 如此往复直到存储引擎把表中的所有记录读完。 可以看到使用了索引下推后虽然 reward 列无法使用到联合索引但是因为它包含在联合索引agereward里所以直接在存储引擎过滤出满足 reward 100000 的记录后才去执行回表操作获取整个记录。相比于没有使用索引下推节省了很多回表操作。 2、什么是索引 索引是帮助存储引擎快速获取数据的一种数据结构形象的说索引就是数据的目录。索引使用了空间换时间的设计思想。 存储引擎是实现如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法。MySQL存储引擎有InnoDB、MyISAM和Mymory。 2、为什么使用索引 1、通过创建唯一的索引可以保证数据库表中的每一行数据都是唯一的。 2、可以大大加快数据的检索速度这是使用索引的主要原因。 3、可以帮助排序避免不必要的排序操作和临时表提高查找性能。 4、将随机IO变为顺序IO加快了磁盘IO的读写速度和减少了磁盘IO操作。 5、可以加快表和表之间的连接。 3、索引分类 按照四个角度分类 按「数据结构」分类Btree索引、Hash索引、Full-text索引。 按「物理存储」分类聚簇索引主键索引、二级索引辅助索引。 按「字段特性」分类主键索引、唯一索引、普通索引、前缀索引。 按「字段个数」分类单列索引、联合索引。 再创建表时InnoDb存储引擎会根据不同的场景选择不同的列作为索引 如果有主键默认会使用主键作为聚簇索引的索引键如果没有主键就选择第一个不包含NULL值的唯一列作为聚簇索引的索引键如果上面两种都没有InnoDB会自动生成一个隐式自增id作为聚簇索引的索引键。 除了聚簇索引其他索引都是二级索引聚簇索引是只有一个的二级索引可以有多个。 4、什么时候需要创建索引什么时候不需要 索引最大的好处是提高查询速度但是索引也是有缺点的比如 1、需要占用物理空间数量越大占用空间越大 2、创建索引和维护索引要耗费时间这种时间随着数据量的增加而增大 3、会降低表的增删改的效率因为每次增删改索引B 树为了维护索引有序性都需要进行动态维护。 所以索引不是万能钥匙它也是根据场景来使用的。 需要创建索引的情况 1、字段有唯一性限制的比如商品编码 2、经常用于 where查询条件的字段这样能够提高整个表的查询速度如果查询条件不是一个字段可以建立联合索引。 3、经常用于 GROUP BY 和 ORDER BY 的字段这样在查询的时候就不需要再去做一次排序了因为我们都已经知道了建立索引之后在 BTree 中的记录都是排序好的。 不需要创建索引的情况 1、WHERE 条件GROUP BYORDER BY 里用不到的字段索引的价值是快速定位如果起不到定位的字段通常是不需要创建索引的因为索引是会占用物理空间的。 2、字段中存在大量重复数据不需要创建索引比如性别字段只有男女如果数据库表中男女的记录分布均匀那么无论搜索哪个值都可能得到一半的数据。在这些情况下还不如不要索引因为 MySQL 还有一个查询优化器查询优化器发现某个值出现在表的数据行中的百分比很高的时候它一般会忽略索引进行全表扫描。 3、表数据太少的时候不需要创建索引 4、经常更新的字段不用创建索引比如不要对电商项目的用户余额建立索引因为索引字段频繁修改由于要维护 BTree的有序性那么就需要频繁的重建索引这个过程是会影响数据库性能的。 5、为什么MySQL InnoDB选择Btree作为索引的数据结构 B树相比于其他数据结构有其特有的优点 B树 与 B树相比 存储相同数据量级别的情况下B树高比B树低磁盘IO次数更少B树叶子节点用双向链表串起来适合范围查询B树无法做到这一点。 B树 与二叉树相比 随着数据量的增加二叉树的树高会越来越高磁盘IO次数也会更多B树在千万级别的数据量下高度依然维持在3-4层左右也就是说一次数据查询操作只需要做3-4次的磁盘IO操作就能找到目标数据。 B树 与哈希表相比 虽然hash的查询效率很高但是无法做到范围查询。 要设计一个 MySQL 的索引数据结构不仅仅考虑数据结构增删改的时间复杂度更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘硬盘是一个非常慢的存储设备我们在查询数据的时候最好能在尽可能少的磁盘 I/0 的操作次数内完成。 B 树的非叶子节点不存放实际的记录数据仅存放索引因此数据量相同的情况下相比存储既存索引又存记录的 B 树B树的非叶子节点可以存放更多的索引因此 B 树可以比 B 树更「矮胖」查询底层节点的磁盘 I/O次数会更少。 B 树有大量的冗余节点所有非叶子节点都是冗余索引这些冗余索引让 B 树在插入、删除的效率都更高比如删除根节点的时候不会像 B 树那样会发生复杂的树的变化 B 树叶子节点之间用链表连接了起来有利于范围查询而 B 树要实现范围查询因此只能通过树的遍历来完成范围查询这会涉及多个节点的磁盘 I/O 操作范围查询效率不如 B 树。 5、为什么Innodb使用自增id作为主键 1、如果表使用自增主键那么每次插入新的记录记录就会顺序添加到当前索引节点的后续位置当一页写满就会自动开辟一个新的页。 2、如果使用非自增主键如果身份证号或学号等由于每次插入主键的值近似于随机因此每次新纪录都要被插到现有索引页的中间某个位置 频繁的移动、分页操作造成了大量的碎片得到了不够紧凑的索引结构后续不得不通过OPTIMIZE TABLEoptimize table来重建表并优化填充页面。 5、MyISAM和Innodb的区别 1. InnoDB支持事务MyISAM不支持对于InnoDB每一条SQL语言都默认封装成事务自动提交这样会影响速度所以最好把多条SQL语言放在begin和commit之间组成一个事务 2. InnoDB支持外键而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败 3. InnoDB是聚集索引使用BTree作为索引结构数据文件是和主键索引绑在一起的表数据文件本身就是按BTree组织的一个索引结构必须要有主键通过主键索引效率很高。但是辅助索引需要两次查询先查询到主键然后再通过主键查询到数据。因此主键不应该过大因为主键太大其他索引也都会很大。 MyISAM是非聚集索引也是使用BTree作为索引结构索引和数据文件是分离的索引保存的是数据文件的指针。主键索引和辅助索引是独立的。 4、InnoDB不保存表的具体行数执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数执行上述语句时只需要读出该变量即可速度很快注意不能加有任何WHERE条件 5.、MyISAM表格可以被压缩后进行查询操作 6、InnoDB支持表、行(默认)级锁而MyISAM支持表级锁 7、InnoDB表必须有唯一索引如主键用户没有指定的话会自己找/生产一个隐藏列Row_id来充当默认主键而Myisam可以没有。 MyISAM适合插入不频繁查询非常频繁如果执行大量的SELECTMyISAM是更好的选择 没有事务。 InnoDB适合可靠性要求比较高或者要求事务 表更新和查询都相当的频繁 大量的INSERT或UPDATE 5、MyISAM和InnoDB实现B树索引方式的区别是什么 1、MyISAMB树叶节点的data域存放的是数据记录的地址在索引检索的时候首先按照B树搜索算法搜索索引如果指定的key存在则取出其data域的值然后以data域的值为地址读取相应的数据记录这被称为非聚簇索引。 2、InnoDB其数据文件本身就是索引文件相比MyISAM索引文件和数据文件是分离的其表数据文件本身就是按BTree组织的一个索引结构树的节点data域保存了完整的数据记录这个索引的key是数据表的主键因此InnoDB表数据文件本身就是主索引这被称为“聚簇索引”或者聚集索引而其余的索引都作为辅助索引辅助索引的data域存储相应记录主键的值而不是地址这也是和MyISAM不同的地方。 在根据主索引搜索时直接找到key所在的节点即可取出数据在根据辅助索引查找时则需要先取出主键的值再走一遍主索引。因此在设计表的时候不建议使用过长的字段为主键也不建议使用非单调的字段作为主键这样会造成主索引频繁分裂。 5、什么是覆盖索引 如果一个索引包含所有需要查询的字段的值我们就称之为覆盖索引。 6、什么时候索引失效 1、如果条件中有or即使其中有条件带索引也不会使用索引。如果想要使用or又要使用索引只能将or条件中的每个列都加上索引。 2、对于联合索引不是使用的第一部分则不会使用使用索引。假设联合索引c1,c2,c3执行以下查询select * from tablename where c2 ‘xxx’; 尽管c2在联合索引中但是没有从最左的c1开始匹配则索引失效。 3、like模糊查询以%开头的列索引失效。 4、如果列类型是字符串那一定要在条件中将数据使用引号引用起来否则不使用索引。 5、不等于(! )EXISTSnot inis  not null等会导致索引失效 6、如果mysql优化器估计是用全表扫描要比使用索引快则不使用索引。 7、当我们在查询条件中对索引列做了计算、函数、类型转换操作这些情况下都会造成索引失效。 7、索引优化的方法 1、前缀索引优化 2、覆盖索引优化 3、主键索引最好是自增 4、索引最好设置为NOT NULL 8、InnoDB如何存储数据 记录是按照行来存储的但是数据库的读取并不以「行」为单位否则一次读取也就是一次 I/O 操作只能处理一行数据效率会非常低。 因此InnoDB 的数据是按「数据页」为单位来读写的也就是说当需要读一条记录的时候并不是将这个记录本身从磁盘读出来而是以页为单位将其整体读入内存。 数据库的 I/O 操作的最小单位是页InnoDB 数据页的默认大小是 16KB意味着数据库每次读写都是以 16KB 为单位的一次最少从磁盘中读取 16K 的内容到内存中一次最少把内存中的 16K 内容刷新到磁盘中。 9、B树如何进行查询 B树的每个节点都是一个数据页。 1、只有叶子节点最底层的节点才存放了数据非叶子节点其他上层节仅用来存放目录项作为索引。 2、非叶子节点分为不同层次通过分层来降低每一层的搜索量 3、所有同层次节点按照索引键大小排序构成一个双向链表便于范围查询。 如果叶子节点存储的是实际数据的就是聚簇索引一个表只能有一个聚簇索引如果叶子节点存储的不是实际数据而是主键值则就是二级索引一个表中可以有多个二级索引。 在使用二级索引进行查找数据时如果查询的数据能在二级索引找到那么就是「索引覆盖」操作如果查询的数据不在二级索引里就需要先在二级索引找到主键值需要去聚簇索引中获得数据行这个过程就叫作「回表」。 9、为什么说B树比B树更适合实际应用中操作系统的文件索引和数据库索引 Btree的磁盘读写代价更低Btree的查询效率更加稳定。数据库索引采用B树而不是B树的主要原因B树只要遍历叶子节点就可以实现整棵树的遍历而且在数据库中基于范围的查询是非常频繁的而B树只能中序遍历所有节点效率太低。 B树特点 所有关键字都出现在叶子结点的链表中(稠密索引)且链表中的关键字恰好是有序的; 不可能在非叶子结点命中; 非叶子结点相当于是叶子结点的索引(稀疏索引)叶子结点相当于是存储(关键字)数据的数据层。 10、使用like “%x”索引一定会失效吗 使用左模糊匹配like %xx并不一定会走全表扫描关键还是看数据表中的字段。 如果数据库表中的字段只有主键二级索引那么即使使用了左模糊匹配也不会走全表扫描typeall而是走全扫描二级索引树(typeindex)。否则全表扫描。 相似的联合索引要遵循最左匹配才能走索引但是如果数据库表中的字段都是索引的话即使查询过程中没有遵循最左匹配原则也是走全扫描二级索引树(typeindex) 11、count(*)和count(1)有什么区别哪个性能最好 count(*) count(1) count(主键字段) count(字段) 12、count() 是什么 count() 是一个聚合函数函数的参数不仅可以是字段名也可以是其他任意表达式该函数作用是统计符合查询条件的记录中函数指定的参数不为 NULL 的记录有多少个。 select count(name) from t_order;  // 统计name字段不为空的记录有多少个。 select count(1) from t_order;       // 1不为NULL表示表中的记录数。 select count(id) from t_order; // id主键字段通常设为NOT NULL可以统计表中记录数。 select count(*) from t_order;       //统计所有记录数执行过程类似于count(1) 13、如何优化count(*) 经常用count(*)来做统计其实是很不好的。 1、近似值。如果业务对于统计个数不需要很精确比如搜索引擎在搜索关键字的时候给出的搜索结果条数是一个大概值。可以使用show table status 或者explain命令来估计算。 2、额外表保存计数值 如果想要精确获取表的记录总数可以将这个计数值保存在单独一张计数表中。当插入数据时将计数表中的计数字段 1。 14、MySQL单表上限超过2000万行合不合理 LRU的预读 大内存就解决了这个问题。 14、事务有哪些特性 事务是由存储引擎实现的InnoDB支持事务。 原子性Atomicity一个事务中的所有操作要么全部完成要么全部不完成不会结束在中间某个环节而且事务在执行过程中发生错误会被回滚到事务开始前的状态就像这个事务从来没有执行过一样。 一致性Consistency是指事务操作前和操作后数据满足完整性约束数据库保持一致性状态。 隔离性Isolation数据库允许多个并发事务同时对其数据进行读写和修改的能力隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致因为多个事务同时使用相同的数据时不会相互干扰每个事务都有一个完整的数据空间对其他并发事务是隔离的。 持久性Durability事务处理结束后对数据的修改就是永久的即便系统故障也不会丢失。 InnoDB 引擎通过什么技术来保证事务的这四个特性的呢 持久性是通过 redo log 重做日志来保证的 原子性是通过 undo log回滚日志 来保证的 隔离性是通过 MVCC多版本并发控制 或锁机制来保证的 一致性则是通过持久性原子性隔离性来保证。 15、并发事务会引发什么问题   MySQL 服务端是允许多个客户端连接的这意味着 MySQL 会出现同时处理多个事务的情况。 那么在同时处理多个事务的时候就可能出现脏读dirty read、不可重复读non-repeatable read、幻读phantom read的问题。 1、脏读 如果一个事务「读到」了另一个「未提交事务修改过的数据」就意味着发生了「脏读」现象。 因为事务 A 是还没提交事务的也就是它随时可能发生回滚操作如果在上面这种情况事务 A 发生了回滚那么事务 B 刚才得到的数据就是过期的数据这种现象就被称为脏读。 2、不可重复读 在一个事务内多次读取同一个数据如果出现前后两次读到的数据不一样的情况就意味着发生了「不可重复读」现象。 3、幻读 在一个事务内多次查询某个符合查询条件的「记录数量」如果出现前后两次查询到的记录数量不一样的情况就意味着发生了「幻读」现象。 16、不可重复读和幻读区别是什么可以举个例子吗 不可重复读的重点是修改幻读的重点在于新增或者删除。 首先事务A读取数据然后事务B对该数据进行了修改此时事务A再次读取然后发现前后数据不一致了。这就是不可重复读。假设某工资单中工资大于3000的有4人事务A读取了所有工资大于3000的人共四条记录此时事务B又插入了一条工资大于3000的记录事务A再次读取时查到的记录就变为了5条。这就是幻读。 16、事务的隔离级别有哪些 当多个事务并发执行时可能会遇到「脏读、不可重复读、幻读」的现象这些现象会对事务的一致性产生不同程序的影响。 脏读读到其他事务未提交的数据 不可重复读前后读取的数据不一致 幻读前后读取的记录数量不一致。 按照严重性脏读 不可重复读 幻读 SQL标准提出了四种隔离级别来规避这些现象隔离级别越高越安全效率越低。 1、读未提交read uncommitted指一个事务还没提交时它做的变更就能被其他事务看到 2、读提交read committed指一个事务提交之后它做的变更才能被其他事务看到 3、可重复读repeatable read指一个事务执行过程中看到的数据一直跟这个事务启动时看到的数据是一致的MySQL InnoDB 引擎的默认隔离级别 4、串行化serializable 会对记录加上读写锁在多个事务对这条记录进行读写操作时如果发生了读写冲突的时候访问的事务必须等前一个事务执行完成才能继续执行。 在「读未提交」隔离级别下可能发生脏读、不可重复读和幻读现象 在「读提交」隔离级别下可能发生不可重复读和幻读现象但是不可能发生脏读现象 在「可重复读」隔离级别下可能发生幻读现象但是不可能脏读和不可重复读现象 在「串行化」隔离级别下脏读、不可重复读和幻读现象都不可能会发生。 对于幻读现象不建议将隔离级别升级为串行化因为这会导致数据库并发时性能很差。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」但是它很大程度上避免幻读现象解决的方案有两种 针对快照读普通 select 语句是通过 MVCC 方式解决了幻读因为可重复读隔离级别下事务执行过程中看到的数据一直跟这个事务启动时看到的数据是一致的即使中途有其他事务插入了一条数据是查询不出来这条数据的所以就很好了避免幻读问题。 针对当前读select ... for update 等语句是通过 next-key lock记录锁间隙锁方式解决了幻读因为当执行 select ... for update 语句的时候会加上 next-key lock如果有其他事务在 next-key lock 锁范围内插入了一条记录那么这个插入语句就会被阻塞无法成功插入所以就很好了避免幻读问题。 对于「读提交」和「可重复读」隔离级别的事务来说它们是通过 Read View 来实现的它们的区别在于创建 Read View 的时机不同 「读提交」隔离级别是在每个 select 都会生成一个新的 Read View也意味着事务期间的多次读取同一条数据前后两次读的数据可能会出现不一致因为可能这期间另外一个事务修改了该记录并提交了事务。 「可重复读」隔离级别是启动事务时生成一个 Read View然后整个事务期间都在用这个 Read View这样就保证了在事务期间读到的数据都是事务启动前的记录。 这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对来控制并发事务访问同一个记录时的行为这就叫 MVCC多版本并发控制。 谈谈什么是MVCC以及对它的了解 InnoDB默认的隔离级别是RRRR解决脏读快照读、不可重复、幻读等问题使用的是MVCC。MVCC全称多版本并发控制。它的最大优点是读不加锁因此读写不冲突并发性好。InnoDB实现MVCC多个版本的数据可以共存主要基于以下技术和数据结构 第一隐藏列InnoDB中每行数据都有隐藏列隐藏列中包含了本行数据的事务id、指向undo log的指针等。 第二基于回滚日志的版本链每行数据的隐藏列中包含了指向回滚日志的指针而每行回滚日志也会指向更早版本的回滚日志形成一条版本链。 第三ReadView通过隐藏列和版本链MySQL可以将数据恢复到指定版本。但是具体要恢复到哪个版本则需要根据ReadView来确定。所谓ReadView是指事务记做事务A在某一时刻给整个事务系统trx_sys打快照之后再进行读操作时会将读取到的数据中的事务id与trx_sys快照比较从而判断数据对该ReadView是否可见即对事务A是否可见。 对于当前读出现幻读和解决幻读的问题解析 两个事务都开启了事务1先进入当前读读出了此时内存中的表数据如果此时事务2尝试插入数据或者修改数据但是因为事务1进入了当前读会加上锁此时事务2无法进行插入和修改操作。除非事务1提交或者回滚了事务。如果事务1长时间不提交或者回滚那么其他事务的插入和修改操作就会产生超时导致执行失败。所以一个事务不能长时间进入当前读这样会导致其他事务无法正常执行。此外这种情况也说明了mysql使用这种机制可以解决幻读的问题在事务开启之初就进入当前读此时会对加上next-key lock从而避免其他事务插入一条新数据或者更改数据。 2、与上面的操作相反同时开启了两个事务此时事务2率先插入一条数据然后事务1希望使用当前读那么此时执行之后就会进入阻塞状态因为事务2因为插入数据获取了锁导致其他事务无法直接进入当前读而阻塞如果事务2长时间不提交或者回滚事务1的当前读阻塞到超时失败。 如果事务2插入数据之后提交了然后事务1当前读将不会阻塞而是读出了不属于事务1一开始所看到的数据。这也就出现了幻读现象。 16、数据库如何保证一致性 从数据库层面数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中C(一致性)是目的A(原子性)、I(隔离性)、D(持久性)是手段是为了保证一致性数据库提供的手段。数据库必须要实现AID三大特性才有可能实现一致性。例如原子性无法保证显然一致性也无法保证。 从应用层面通过代码判断数据库数据是否有效然后决定回滚还是提交数据 16、数据库如何保持原子性 主要是利用 Innodb 的undo log。 undo log名为回滚日志是实现原子性的关键当事务回滚时能够撤销所有已经成功执行的 SQL语句他需要记录你要回滚的相应日志信息。 undo log记录了这些回滚需要的信息当事务执行失败或调用了rollback。导致事务需要回滚便可以利用undo log中的信息将数据回滚到修改之前的样子。 16、数据库如何保持持久性 主要是利用Innodb的redo log。重写日志。 当做数据修改的时候不仅在内存中操作还会在redo log中记录这次操作。当事务提交的时候会将redo log日志进行刷盘(redo log一部分在内存中一部分在磁盘上)。当数据库宕机重启的时候会将redo log中的内容恢复到数据库中再根据undo log和binlog内容决定回滚数据还是提交数据。 好处就是将redo log进行刷盘比对数据页刷盘效率高具体表现如下 redo log体积小毕竟只记录了哪一页修改了什么数据因此体积小刷盘快。 redo log是一直往末尾进行追加属于顺序IO。效率显然比随机IO来的快。 17、MySQL可重复读隔离级别完全解决幻读了吗 MySQL 可重复读隔离级别并没有彻底解决幻读只是很大程度上避免了幻读现象的发生。 要避免这类特殊场景下发生幻读的现象的话就是尽量在开启事务之后马上执行 select ... for update 这类当前读的语句因为它会对记录加 next-key lock从而避免其他事务插入一条新记录。 18、MySQL的可重复读隔离级别如何解决幻读 在可重复读隔离级别下InnoDB采用了MVCC机制来解决幻读的问题MVCC就是一种乐观锁机制它通过对于不同事务生成不同的的快照版本然后通过UNDO的版本链来进行管理并且在MVCC中规定高版本能够看到低版本的一个数据变更低版本看不到高版本的一个数据变更从而实现了不同事务之间的数据隔离解决幻读问题。但是如果在事务里存在当前读的情况那么它是直接读取内存里面的数据跳过了快照读所以还是会出现幻读问题。 可以通过两种方式解决幻读1、尽量避免当前读的情况2、引入一个LBCC的方式来解决。 18、关系型数据库和非关系型数据库 非关系型数据库也叫NOSQL采用键值对的形式进行存储。 它的读写性能度很高易于扩展可分为内存型数据库和文档型数据库比如Redis、Mongodb等。适用于日志系统、地理位置存储、数据量巨大的场景等。 关系型数据库优点1、容易理解。因为它采用了关系模型来组织数据。2、可以保持数据的一致性。3、数据更新的开销比较小。4、支持复杂查询带where子句的查询。 非关系型数据库优点1、不需要经过SQL层的解析读写效率高。2、基于键值对数据的扩展性很好。3、可以支持多种类型数据的存储如图片文档等等。 19、说说delete、drop和truncate的共同点 三种都用来做删除操作。 delete用来删除表的全部或者部分数据行执行delete之后用户可以通过提交或者回滚来执行删除或者撤销删除。会触发这个表上所有的delete触发器。 truncate删除表中的所有数据这个操作不能回滚也不能触发这个表上的触发器它的速度比delete更快占用的空间更小。 drop用来删除表结构会将整个表的数据行和表本身删除索引和权限也会被删除这个命令不能被回滚。 20、MySQL性能优化从哪些方面可以优化 1、为搜索字段创建索引 2、避免使用select * 列出需要查询的字段 3、垂直分割分表 4、选择正确的存储引擎。 21、说说视图游标呢 视图是一种虚拟的表通常是有一个表或者多个表的行或列的子集具有和物理表相同的功能。游标是对查询出来的结果集作为一个单元来有效的处理。一般不使用游标但是需要逐条处理数据的时候游标显得十分重要。 视图的作用使用视图可以简化复杂的 sql 操作隐藏具体的细节保护数据视图创建后可以使用与表相同的方式利用它们。 22、MySQL中为什么要有事务回滚机制 在 MySQL 中恢复机制是通过回滚日志undo log实现的所有事务进行的修改都会先记录到这个回滚日志中然后在对数据库中的对应行进行写入。 当事务已经被提交之后就无法再次回滚了。 回滚日志作用 1、能够在发生错误或者用户执行 ROLLBACK 时提供回滚相关的信息 2、 在整个系统发生崩溃、数据库进程直接被杀死后当用户再次启动数据库进程时还能够立刻通过查询回滚日志将之前未完成的事务进行回滚这也就需要回滚日志必须先于数据持久化到磁盘上是我们需要先写日志后写数据库的主要原因。 23、假设你在的公司选择MySQL数据库作数据存储一天五万条以上的增量预计运维三年你有哪些优化手段 1、设计良好的数据库结构允许部分数据冗余减少join连接查询 2、选择合适的表字段数据类型和存储引擎适当的添加索引 3、MySQL库主从读写分离减少数据库读写压力 4、添加缓存机制防止每次都直接从数据库中搜索 5、书写高效的SQL语句。比如select * 改为指定字段。 24、聚集索引和非聚集索引是什么区别是什么 聚集索引 该索引中键值的逻辑顺序与数据行的物理顺序相同每个表 (InnoDB) 只能有一个聚集索引。 在 InnoDB 中聚集索引 B 树的叶子节点中除了存放主键信息还存放了主键对应的行数据因此我们可以直接在聚集索引中查找到想要的数据。 查找时间短但是内存占用高。 非聚集索引 非聚集索引又称辅助索引、非聚簇索引该索引中键值的逻辑顺序与数据行的物理顺序不同每个表 (InnoDB、MyISAM) 可以有多个非聚集索引。 在 InnoDB 中非聚集索引 B 树的叶子节点中存放了主键信息当我们要查找数据时需要先在非聚集索引中查找到对应的主键然后再根据主键去聚集索引中查找到想要的数据。 如果使用了覆盖索引则不需要回表直接通过非聚集索引就可以查找到想要的数据 覆盖索引是指 select 查询的数据只需要在索引中就能取得而不必去读取数据行换句话说就是查询列要被所建的索引覆盖。 非聚集索引索引占用的存储空间较小但查找时间较长 25、MySQL中的char和varchar有什么区别 1、char的长度是不可变的用空格填充到指定长度大小而varchar的长度是可变的。 2、char的存取速度还是要比varchar要快得多 3、char的存储方式是对英文字符ASCII占用1个字节对一个汉字占用两个字节。varchar的存储方式是对每个英文字符占用2个字节汉字也占用2个字节。 26、索引有那么多优点为什么不对表总的每一列创建一个索引呢 当对表中的数据进行增加、删除和修改的时候索引也要动态的维护这样就降低了数据的维护速度。 索引需要占物理空间除了数据表占数据空间之外每一个索引还要占一定的物理空间如果要建立簇索引那么需要的空间就会更大。 创建索引和维护索引要耗费时间这种时间随着数据量的增加而增加。 27、介绍一下间隙锁 InnoDB存储引擎有3种行锁的算法间隙锁Gap Lock是其中之一。间隙锁用于锁定一个范围但不包含记录本身。它的作用是为了阻止多个事务将记录插入到同一范围内而这会导致幻读问题的产生。 27、InnoDB中行级锁是怎么实现的 InnoDB行级锁是通过给索引上的索引项加锁来实现的。只有通过索引条件检索数据InnoDB才使用行级锁否则InnoDB将使用表锁。 当表中锁定其中的某几行时不同的事务可以使用不同的索引锁定不同的行。另外不论使用主键索引、唯一索引还是普通索引InnoDB都会使用行锁来对数据加锁。 28、说说SQL语法中内连接、自连接、外连接左、右、全、交叉连接的区别分别是什么 内连接只有两个元素表相匹配的才能在结果集中显示。 外连接左外连接: 左边为驱动表驱动表的数据全部显示匹配表的不匹配的不会显示。 右外连接:右边为驱动表驱动表的数据全部显示匹配表的不匹配的不会显示。 全外连接连接的表中不匹配的数据全部会显示出来。 交叉连接 笛卡尔效应显示的结果是链接表数的乘积。 29、数据库高并发经常遇到怎么解决 1、使用缓存减少数据库的读取负担将高频访问的数据存入缓存中 2、增加数据库索引提高查询速度 3、主从读写分离让主服务器负责写从服务器负责读 4、将数据库拆分是的数据库的表尽可能小提高查询速度 5、使用分布式架构分散计算压力。 30、说说数据库设计的三大范式 第一范式在关系模型中数据库表的每一列都是不可分割的原子数据项而不能是集合数组记录等非原子数据项。简而言之第一范式就是无重复的域。 第二范式在1NF的基础上非码属性必须完全依赖于候选码在1NF基础上消除非主属性对主码的部分函数依赖。 第三范式在2NF基础上任何非主属性不依赖于其它非主属性在2NF基础上消除传递依赖。 31、说说对redo log、undo log和binlog日志的了解 binlogBinary Log 二进制日志文件就是常说的binlog。二进制日志记录了MySQL所有修改数据库的操作然后以二进制的形式记录在日志文件中其中还包括每条语句所执行的时间和所消耗的资源以及相关的事务信息。默认情况下二进制日志功能是开启的。 redo log 重做日志用来实现事务的持久性即事务ACID中的D。它由两部分组成一是内存中的重做日志缓冲redo log buffer其是易失的二是重做日志文件redo log file它是持久的。 redo log用来保证事务的持久性undo log用来帮助事务回滚及MVCC的功能。redo log基本上都是顺序写的在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。 undo log redo存放在重做日志文件中如果用户执行的事务或语句由于某种原因失败了又或者用户用一条ROLLBACK语句请求回滚就可以利用这些undo信息将数据回滚到修改之前的样子。 33、MySQL主从复制怎么实现 主要分为以下三步 1、主服务器master把数据更改记录到二进制日志binlog中。 2、从服务器slave把主服务器的二进制日志复制到自己的中继日志relay log中。 3、从服务器重做中继日志中的日志把更改应用到自己的数据库上以达到数据的最终一致性。 34、说说为什么有Buffer Pool? InnoDB储存引擎设计了一个缓冲池buffer pool来提高数据库的读写性能防止每次都去磁盘读写数据。Buffer Pool 以页为单位缓冲数据可以通过 innodb_buffer_pool_size 参数调整缓冲池的大小默认是 128 M。 十、Redis 1、为什么用Redis做MySQL缓存 具备高性能假如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中这样下一次再访问这些数据的时候就可以直接从缓存中获取了操作 Redis 缓存就是直接操作内存所以速度相当快。如果 MySQL 中的对应数据改变的之后同步改变 Redis 缓存中相应的数据即可。 具备高并发单台设备的 Redis 的 QPSQuery Per Second每秒钟处理完请求的次数 是 MySQL 的 10 倍Redis 单机的 QPS 能轻松破 10w而 MySQL 单机的 QPS 很难破 1w。所以直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的所以我们可以考虑把数据库中的部分数据转移到缓存中去这样用户的一部分请求会直接到缓存这里而不用经过数据库。 1、Redis和Memcached有什么区别 共同点1、都是基于内存的数据库一般都当作缓存使用。2、都有过期策略。3、两者的性能都非常高。 不同点1、redis支持更加丰富的数据类型而memcached只支持最简单的key-value数据类型。2、redis支持数据的持久化可以将内存中的数据保持在硬盘中重启之后可以再次加载使用而memcached没有持久化功能数据全部存在于内存之中memcached重启或者挂掉之后数据就没了。3、redis原生支持集群模式memcached没有原生集群模式。4、redis支持发布订阅模式Lua脚本和事务等功能而memcached不支持。 2、Redis数据类型以及使用场景分别是什么 Redis 提供了丰富的数据类型常见的有五种数据类型String字符串Hash哈希List列表Set集合、Zset有序集合。 1、String 类型的应用场景缓存对象、常规计数、分布式锁、共享 session 信息等。 2、List 类型的应用场景消息队列但是有两个问题1. 生产者需要自行实现全局唯一 ID2. 不能以消费组形式消费数据等。 3、Hash 类型缓存对象、购物车等。 4、Set 类型聚合计算并集、交集、差集场景比如点赞、共同关注、抽奖活动等。 5、Zset 类型排序场景比如排行榜、电话和姓名排序等。 Redis 后续版本又支持四种数据类型它们的应用场景如下 1、BitMap2.2 版新增二值状态统计的场景比如签到、判断用户登陆状态、连续签到用户总数等 2、HyperLogLog2.8 版新增海量数据基数统计的场景比如百万级网页 UV 计数等 3、GEO3.2 版新增存储地理位置信息的场景比如滴滴叫车 4、Stream5.0 版新增消息队列相比于基于 List 类型实现的消息队列有这两个特有的特性自动生成全局唯一消息ID支持以消费组形式消费数据。 2、五种数据类型 3、Redis是单线程模型吗 Redis 单线程指的是「接收客户端请求-解析请求 -进行数据读写等操作-发送数据给客户端」这个过程是由一个线程主线程来完成的这也是我们常说 Redis 是单线程的原因。 Redis程序本身并不是单线程的redis在启动的时候是会启动后台线程的 Redis在2.6版本会启动2个后台线程分别处理关闭文件、AOF刷盘这两个任务。Redis在4.0之后新增了一个后台线程用来异步释放redis内存也就是lazyfree线程。执行 unlink key / flushdb async / flushall async 等命令会把这些删除操作交给后台线程来执行好处是不会导致 Redis 主线程卡顿。因此当我们要删除一个大 key 的时候不要使用 del 命令删除因为 del 是在主线程处理的这样会导致 Redis 主线程卡顿因此我们应该使用 unlink 命令来异步删除大key。 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理是因为这些任务的操作都是很耗时的如果把这些任务都放在主线程来处理那么 Redis 主线程就很容易发生阻塞这样就无法处理后续的请求了。 关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列 BIO_CLOSE_FILE关闭文件任务队列当队列有任务后后台线程会调用 close(fd) 将文件关闭 BIO_AOF_FSYNCAOF刷盘任务队列当 AOF 日志配置成 everysec 选项后主线程会把 AOF 写日志操作封装成一个任务也放到队列中。当发现队列有任务后后台线程会调用 fsync(fd)将 AOF 文件刷盘 BIO_LAZY_FREElazy free 任务队列当队列有任务后后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象 4、Redis单线程模式是怎么样的 首先调用epoll_create()创建一个epoll对象和调用socket()创建一个服务端socket 然后调用bind()绑定端口和调用listen()监听该socket 然后将调用epoll_ctl将listen socket加入到epoll中同时注册连接时间处理函数。 初始化完后主线程就进入到一个事件循环函数主要会做以下事情 首先先调用处理发送队列函数看是发送队列里是否有任务如果有发送任务则通过 write 函数将客户端发送缓存区里的数据发送出去如果这一轮数据没有发送完就会注册写事件处理函数等待 epoll_wait 发现可写后再处理 。 接着调用 epoll_wait 函数等待事件的到来 如果是连接事件到来则会调用连接事件处理函数该函数会做这些事情调用 accpet 获取已连接的 socket - 调用 epoll_ctl 将已连接的 socket 加入到 epoll - 注册「读事件」处理函数 如果是读事件到来则会调用读事件处理函数该函数会做这些事情调用 read 获取客户端发送的数据 - 解析命令 - 处理命令 - 将客户端对象添加到发送队列 - 将执行结果写到发送缓存区等待发送 如果是写事件到来则会调用写事件处理函数该函数会做这些事情通过 write 函数将客户端发送缓存区里的数据发送出去如果这一轮数据没有发送完就会继续注册写事件处理函数等待 epoll_wait 发现可写后再处理 。 5、Redis采用单线程为什么还这么快 1、Redis 的大部分操作都在内存中完成并且采用了高效的数据结构因此 Redis 瓶颈可能是机器的内存或者网络带宽而并非 CPU既然 CPU 不是瓶颈那么自然就采用单线程的解决方案了 2、Redis 采用单线程模型可以避免了多线程之间的竞争省去了多线程切换带来的时间和性能上的开销而且也不会导致死锁问题。 3、Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求IO 多路复用机制是指一个线程处理多个 IO 流就是我们经常听到的 select/epoll 机制。简单来说在 Redis 只运行单线程的情况下该机制允许内核中同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达就会交给 Redis 线程处理这就实现了一个 Redis 线程处理多个 IO 流的效果。 6、Redis 6.0之后为什么引入多线程 虽然 Redis 的主要工作网络 I/O 和执行命令一直是单线程模型但是在 Redis 6.0 版本之后也采用了多个 I/O 线程来处理网络请求这是因为随着网络硬件的性能提升Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。 所以为了提高网络 I/O 的并行度Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行Redis 仍然使用单线程来处理。 因此 Redis 6.0 版本之后Redis 在启动的时候默认情况下会额外创建 6 个线程这里的线程数不包括主线程 Redis-server Redis的主线程主要负责执行命令 bio_close_file、bio_aof_fsync、bio_lazy_free三个后台线程分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务 io_thd_1、io_thd_2、io_thd_3三个 I/O 线程io-threads 默认是 4 所以会启动 34-1个 I/O 多线程用来分担 Redis 网络 I/O 的压力。 7、Redis如何实现数据不丢失 Redis 的读写操作都是在内存中所以 Redis 性能才会高但是当 Redis 重启后内存中的数据就会丢失那为了保证内存中的数据不会丢失Redis 实现了数据持久化的机制这个机制会把数据存储到磁盘这样在 Redis 重启就能够从磁盘中恢复原有的数据。 Redis 共有三种数据持久化的方式 AOF 日志每执行一条写操作命令就把该命令以追加的方式写入到一个文件里 RDB 快照将某一时刻的内存数据以二进制的方式写入磁盘 混合持久化方式Redis 4.0 新增的方式集成了 AOF 和 RBD 的优点 8、AOF日志是如何实现的 Redis 在执行完一条写操作命令后就会把该命令以追加的方式写入到一个文件里然后 Redis 重启时会读取该文件记录的命令然后逐一执行命令的方式来进行数据恢复。 9、为什么先执行命令再把数据写入日志呢 Reids 是先执行写操作命令后才将该命令记录到 AOF 日志里的这么做其实有两个好处 1、避免额外的检查开销因为如果先将写操作命令记录到 AOF 日志里再执行该命令的话如果当前的命令语法有问题那么如果不进行命令语法检查该错误的命令记录到 AOF 日志里后Redis 在使用日志恢复数据时就可能会出错。 2、不会阻塞当前写操作命令的执行因为当写操作命令执行成功后才会将命令记录到 AOF 日志。 这样做也会带来风险 1、数据可能会丢失 执行写操作命令和记录日志是两个过程那当 Redis 在还没来得及将命令写入到硬盘时服务器发生宕机了这个数据就会有丢失的风险。 2、可能阻塞其他操作 由于写操作命令执行成功后才记录到 AOF 日志所以不会阻塞当前命令的执行但因为 AOF 日志也是在主线程中执行所以当 Redis 把日志文件写入磁盘的时候还是会阻塞后续的操作无法执行。 10、AOF写回策略有哪几种 Redis 提供了 3 种写回硬盘的策略控制的就是上面说的第三步的过程。 在 Redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填 Always这个单词的意思是「总是」所以它的意思是每次写操作命令执行完后同步将 AOF 日志数据写回硬盘 Everysec这个单词的意思是「每秒」所以它的意思是每次写操作命令执行完后先将命令写入到 AOF 文件的内核缓冲区然后每隔一秒将缓冲区里的内容写回到硬盘 No意味着不由 Redis 控制写回硬盘的时机转交给操作系统控制写回的时机也就是每次写操作命令执行完后先将命令写入到 AOF 文件的内核缓冲区再由操作系统决定何时将缓冲区内容写回硬盘。 11、AOF日志过大会触发什么机制 AOF 日志是一个文件随着执行的写操作命令越来越多文件的大小会越来越大。 如果当 AOF 日志文件过大就会带来性能问题比如重启 Redis 后需要读 AOF 文件的内容以恢复数据如果文件过大整个恢复的过程就会很慢里面是一条条的命令需要执行。 所以Redis 为了避免 AOF 文件越写越大提供了 AOF 重写机制当 AOF 文件的大小超过所设定的阈值后Redis 就会启用 AOF 重写机制来压缩 AOF 文件。比如内存中的命令set name xxx 可能设置了很多次那么重写时只保留最新的即可 首先根据当前redis内存里面的数据重新构建一个新的AOF文件 读取redis里面的数据写入到新的AOF文件里面 重写完成以后用新的AOF文件覆盖现有的AOF文件。 因为redis在重写的时候需要读取redis内存中的所有键值数据再去生成指令对数据进行保存这个过程比较耗时对用户会产生影响所以会放到后台子进程中去处理。因此主进程仍然可以去处理请求此时可能出现AOF重写文件中的数据和redis内存中的数据不一致的问题redis做了一层优化子进程在重写过程中主进程的数据变更需要追加到AOF的重写缓冲区里面等到AOF文件重写完成以后再将重写缓冲区里的数据追加到AOF文件里面。 12、RDB快照是如何实现的 因为 AOF 日志记录的是操作命令不是实际的数据所以用 AOF 方法做故障恢复时需要全量把日志都执行一遍一旦 AOF 日志非常多势必会造成 Redis 的恢复操作缓慢。 为了解决这个问题Redis 增加了 RDB 快照。所谓的快照就是记录某一个瞬间东西(全量)比如当我们给风景拍照时那一个瞬间的画面和信息就记录到了一张照片。 RDB 快照就是记录某一个瞬间的内存数据记录的是实际数据而 AOF 文件记录的是命令操作的日志而不是实际的数据。在 Redis 恢复数据时 RDB 恢复数据的效率会比 AOF 高些因为直接将 RDB 文件读入内存就可以不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。 13、RDB做快照时会阻塞线程吗 Redis 提供了两个命令来生成 RDB 文件分别是 save 和 bgsave他们的区别就在于是否在「主线程」里执行 执行了 save 命令就会在主线程生成 RDB 文件由于和执行操作命令在同一个线程所以如果写入 RDB 文件的时间太长会阻塞主线程 执行了 bgsave 命令会创建一个子进程来生成 RDB 文件这样可以避免主线程的阻塞。 Redis 的快照是全量快照也就是说每次执行快照都是把内存中的「所有数据」都记录到磁盘中。所以执行快照是一个比较重的操作如果频率太频繁可能会对 Redis 性能产生影响。如果频率太低服务器故障时丢失的数据会更多。 14、RDB在执行快照的时候数据能修改吗 可以的执行 bgsave 过程中Redis 依然可以继续处理操作命令的也就是数据是能被修改的关键的技术就在于写时复制技术Copy-On-Write, COW。 执行 bgsave 命令的时候会通过 fork() 创建子进程此时子进程和父进程是共享同一片内存数据的因为创建子进程的时候会复制父进程的页表但是页表指向的物理内存还是一个此时如果主线程执行读操作则主线程和 bgsave 子进程互相不影响。 如果主线程执行写操作则被修改的数据会复制一份副本然后 bgsave 子进程会把该副本数据写入 RDB 文件在这个过程中主线程仍然可以直接修改原来的数据。 15、为什么会有混合持久化这个机制 RDB 优点是数据恢复速度快但是快照的频率不好把握。频率太低丢失的数据就会比较多频率太高就会影响性能。RDB缺点 AOF 优点是丢失数据少但是数据恢复不快。AOF缺点 为了集成了两者的优点 Redis 4.0 提出了混合使用 AOF 日志和内存快照也叫混合持久化既保证了 Redis 重启速度又降低数据丢失风险。 混合持久化工作在 AOF 日志重写过程当开启了混合持久化时在 AOF 重写日志时fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件然后主线程处理的操作命令会被记录在重写缓冲区里重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。 使用了混合持久化AOF 文件的前半部分是 RDB 格式的全量数据后半部分是 AOF 格式的增量数据。 好处在于重启 Redis 加载数据的时候由于前半部分是 RDB 内容这样加载的时候速度会很快。加载完 RDB 的内容后才会加载后半部分的 AOF 内容这里的内容是 Redis 后台子进程重写 AOF 期间主线程处理的操作命令可以使得数据更少的丢失。 混合持久化优点 混合持久化结合了 RDB 和 AOF 持久化的优点开头为 RDB 的格式使得 Redis 可以更快的启动同时结合 AOF 的优点有减低了大量数据丢失的风险。 混合持久化缺点 AOF 文件中添加了 RDB 格式的内容使得 AOF 文件的可读性变得很差 兼容性差如果开启混合持久化那么此混合持久化 AOF 文件就不能用在 Redis 4.0 之前版本了。 16、Redis如何实现服务高可用 要想设计一个高可用的 Redis 服务一定要从 Redis 的多服务节点来考虑比如 Redis 的主从复制、哨兵模式、切片集群。 1、主从复制 主从复制是 Redis 高可用服务的最基础的保证实现方案就是将从前的一台 Redis 服务器同步数据到多台从 Redis 服务器上即一主多从的模式且主从服务器之间采用的是「读写分离」的方式。 主服务器可以进行读写操作当发生写操作时自动将写操作同步给从服务器而从服务器一般是只读并接受主服务器同步过来写操作命令然后执行这条命令。 注意主从服务器之间的命令复制是异步进行的。 具体来说在主从服务器命令传播阶段主服务器收到新的写命令后会发送给从服务器。但是主服务器并不会等到从服务器实际执行完命令后再把结果返回给客户端而是主服务器自己在本地执行完命令后就会向客户端返回结果了。如果从服务器还没有执行主服务器同步过来的命令主从服务器间的数据就不一致了。 所以无法实现强一致性保证主从数据时时刻刻保持一致数据不一致是难以避免的。 2、哨兵模式 在使用 Redis 主从服务的时候会有一个问题就是当 Redis 的主从服务器出现故障宕机时需要手动进行恢复。 为了解决这个问题Redis 增加了哨兵模式Redis Sentinel因为哨兵模式做到了可以监控主从服务器并且提供主从节点故障转移的功能。 功能 1、集群监控负责监控 Redis master 和 slave 进程是否正常工作。 2、消息通知如果某个 Redis 实例有故障那么哨兵负责发送消息作为报警通知给管理员。 3、故障转移如果 master node 挂掉了会自动转移到 slave node 上。 4、配置中心如果故障转移发生了通知 client 客户端新的 master 地址。 3、主从赋值核心原理 当启动一个 slave node 的时候它会发送一个 PSYNC 命令给 master node。 如果这是 slave node 初次连接到 master node那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程开始生成一份 RDB 快照文件同时还会将从客户端 client 新收到的所有写命令缓存在内存中。 RDB 文件生成完毕后 master 会将这个 RDB 发送给 slaveslave 会先写入本地磁盘然后再从本地磁盘加载到内存中接着 master 会将内存中缓存的写命令发送到 slaveslave 也会同步这些数据。slave node 如果跟 master node 有网络故障断开了连接会自动重连连接之后 master node 仅会复制给 slave 部分缺少的数据。 17、Redis 使用的过期删除策略是什么 Redis可以对key设置过期时间对应的是过期键值删除策略。每当对一个key设置了过期时间时redis会把该key带上过期时间存储到一个过期字典中如果读取的数据不在这个过期字典中则正常读取键值如果存在则获取该key的过期时间然后与当前系统时间进行对比如果比系统时间大就没有过期否则判断该key已过期。 Redis 使用的过期删除策略是「惰性删除定期删除」这两种策略配和使用。 18、什么是惰性删除策略 惰性删除策略不主动删除过期键每次从数据库访问key时都检测key是否过期如果过期才会删除该key。 惰性删除策略的优点 因为每次访问时才会检查 key 是否过期所以此策略只会使用很少的系统资源因此惰性删除策略对 CPU 时间最友好。 惰性删除策略的缺点 如果一个 key 已经过期而这个 key 又仍然保留在数据库中那么只要这个过期 key 一直没有被访问它所占用的内存就不会释放造成了一定的内存空间浪费。所以惰性删除策略对内存不友好。 19、什么是定期删除策略 定期删除策略的做法是每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查并删除其中的过期key。 Redis 的定期删除的流程 1、从过期字典中随机抽取 20 个 key 2、检查这 20 个 key 是否过期并删除已过期的 key 3、如果本轮检查的已过期 key 的数量超过 5 个20/4也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%则继续重复步骤 1如果已过期的 key 比例小于 25%则停止继续删除过期 key然后等待下一轮再检查。 定期删除是一个循环的流程。那 Redis 为了保证定期删除不会出现循环过度导致线程卡死现象为此增加了定期删除循环流程的时间上限默认不会超过 25ms。 定期删除策略的优点 通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响同时也能删除一部分过期的数据减少了过期键对空间的无效占用。 定期删除策略的缺点 难以确定删除操作执行的时长和频率。如果执行的太频繁就会对 CPU 不友好如果执行的太少那又和惰性删除一样了过期 key 占用的内存不会及时得到释放。 惰性删除策略和定期删除策略都有各自的优点所以 Redis 选择「惰性删除定期删除」这两种策略配和使用以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。 20、Redis持久化时对过期键值会如何处理 RDB文件分为两个阶段 RDB 文件生成阶段从内存状态持久化成 RDB文件的时候会对 key 进行过期检查过期的键「不会」被保存到新的 RDB 文件中因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。 RDB 加载阶段RDB 加载阶段时要看服务器是主服务器还是从服务器分别对应以下两种情况 1、如果 Redis 是「主服务器」运行模式的话在载入 RDB 文件时程序会对文件中保存的键进行检查过期键「不会」被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响 2、如果 Redis 是「从服务器」运行模式的话在载入 RDB 文件时不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时从服务器的数据会被清空。所以一般来说过期键对载入 RDB 文件的从服务器也不会造成影响。 AOF 文件分为两个阶段AOF 文件写入阶段和 AOF 重写阶段。 AOF 文件写入阶段当 Redis 以 AOF 模式持久化时如果数据库某个过期键还没被删除那么 AOF 文件会保留此过期键当此过期键被删除后Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。 AOF 重写阶段执行 AOF 重写时会对 Redis 中的键值对进行检查已过期的键不会被保存到重写后的 AOF 文件中因此不会对 AOF 重写造成任何影响。 21、Redis主从模式中对过期键会如何处理 当 Redis 运行在主从模式下时从库不会进行过期扫描从库对过期的处理是被动的。也就是即使从库中的 key 过期了如果有客户端访问从库时依然可以得到 key 对应的值像未过期的键值对一样返回。 从库的过期键处理依靠主服务器控制主库在 key 到期时会在 AOF 文件里增加一条 del 指令同步到所有的从库从库通过执行这条 del 指令来删除过期的 key。 22、Redis内存满了会发生什么 在 Redis 的运行内存达到了某个阀值就会触发内存淘汰机制这个阀值就是我们设置的最大运行内存此值在 Redis 的配置文件中可以找到配置项为 maxmemory。 23、redis内存淘汰策略有哪些 Redis 内存淘汰策略共有八种这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。 1、不进行数据淘汰的策略 noevictionRedis3.0之后默认的内存淘汰策略 它表示当运行内存超过最大设置内存时不淘汰任何数据而是不再提供服务直接返回错误。 2、进行数据淘汰的策略 针对「进行数据淘汰」这一类策略又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。 在设置了过期时间的数据中进行淘汰 1、volatile-random随机淘汰设置了过期时间的任意键值 2、volatile-ttl优先淘汰更早过期的键值。 3、volatile-lruRedis3.0 之前默认的内存淘汰策略淘汰所有设置了过期时间的键值中最久未使用的键值 4、volatile-lfuRedis 4.0 后新增的内存淘汰策略淘汰所有设置了过期时间的键值中最少使用的键值 在所有数据范围内进行淘汰 1、allkeys-random随机淘汰任意键值; 2、allkeys-lru淘汰整个键值中最久未使用的键值 3、allkeys-lfuRedis 4.0 后新增的内存淘汰策略淘汰整个键值中最少使用的键值。 24、什么是LRU算法 LRU 全称是 Least Recently Used 翻译为最近最少使用会选择淘汰最近最少使用的数据。 传统 LRU 算法的实现是基于「链表」结构链表中的元素按照操作顺序从前往后排列最新操作的键会被移动到表头当需要内存淘汰时只需要删除链表尾部的元素即可因为链表尾部的元素就代表最久未被使用的元素。 Redis 并没有使用这样的方式实现 LRU 算法因为传统的 LRU 算法存在两个问题 1、需要用链表管理所有的缓存数据这会带来额外的空间开销 2、当有数据被访问时需要在链表上把该数据移动到头端如果有大量数据被访问就会带来很多链表移动操作会很耗时进而会降低 Redis 缓存性能。 Redis 实现的是一种近似 LRU 算法目的是为了更好的节约内存它的实现方式是在 Redis 的对象结构体中添加一个额外的字段用于记录此数据的最后一次访问时间。 当 Redis 进行内存淘汰时会使用随机采样的方式来淘汰数据它是随机取 5 个值此值可配置然后淘汰最久没有使用的那个。 优缺点 不用为所有的数据维护一个大链表节省了空间占用 不用在每次数据访问时都移动链表项提升了缓存的性能 但是 LRU 算法有一个问题无法解决缓存污染问题比如应用一次读取了大量的数据而这些数据只会被读取这一次那么这些数据会留存在 Redis 缓存中很长一段时间造成缓存污染。 25、什么是LFU算法Least Frequently Used LFU 全称是 Least Frequently Used 翻译为最近最不常用的LFU 算法是根据数据访问次数来淘汰数据的它的核心思想是“如果数据过去被访问多次那么将来被访问的频率也更高”。 所以 LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后数据留存在缓存中很长一段时间的问题相比于 LRU 算法也更合理一些。 26、什么是缓存雪崩、缓存击穿、缓存穿透怎么解决 缓存雪崩当大量缓存数据在同一时间过期失效时如果此时有大量的用户请求都无法在 Redis 中处理于是全部请求都直接访问数据库从而导致数据库的压力骤增严重的会造成数据库宕机从而形成一系列连锁反应造成整个系统崩溃这就是缓存雪崩的问题。简而言之缓存中大量数据过期同时面临大量业务请求这些数据将直接去数据库MySQL等中请求压力骤增可能会导致宕机。 解决方案1、将缓存失效时间随机打散比如在原有失效时间基础上增加一个随机值这样每个缓存的过期时间不会过多重复降低了缓存集体失效的概率。2、设置缓存不过期通过后台服务来更新缓存数据。 缓存击穿如果缓存中的某个热点数据过期了此时大量的请求访问了该热点数据就无法从缓存中读取直接访问数据库数据库很容易就被高并发的请求冲垮这就是缓存击穿的问题。 解决方案1、互斥锁方案保证同一时间只有一个业务线程请求缓存未能获取互斥锁的请求要么等待锁释放完后重新读取缓存要么就返回空值或者默认值。2、不给热点数据设置过期时间由后台异步更新缓存或者在热点数据准备要过期前提前通知后台线程更新缓存以及重新设置过期时间。 缓存穿透当用户访问的数据既不在缓存中也不在数据库中导致请求在访问缓存时发现缓存缺失再去访问数据库时发现数据库中也没有要访问的数据没办法构建缓存数据来服务后续的请求。那么当有大量这样的请求到来时数据库的压力骤增这就是缓存穿透的问题。业务误操作黑客恶意攻击 解决方案1、非法请求限制当有大量恶意请求访问不存在的数据直接返回错误2、设置空值或者默认值发现缓存穿透的现象时可以针对查询的数据在缓存中设置一个空值或者默认值这样后续请求就可以从缓存中读取到空值或者默认值返回给应用而不会继续查询数据库。3、使用布隆过滤器快速判断数据是否存在避免通过查询数据库来判断数据是否存在。 27、如何设计一个缓存策略可以动态缓存热点数据 热点数据动态缓存的策略总体思路通过数据最新访问时间来做排名并过滤掉不常访问的数据只留下经常访问的数据。 以电商平台场景中的例子现在要求只缓存用户经常访问的 Top 1000 的商品。具体细节如下 1、先通过缓存系统做一个排序队列比如存放 1000 个商品系统会根据商品的访问时间更新队列信息越是最近访问的商品排名越靠前 2、同时系统会定期过滤掉队列中排名最后的 200 个商品然后再从数据库中随机读取出 200 个商品加入队列中 3、这样当请求每次到达的时候会先从队列中获取商品 ID如果命中就根据 ID 再从另一个缓存数据结构中读取实际的商品信息并返回。 在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。 28、如何保证缓存与数据库双写时的数据一致性 最经典的缓存数据库读写的模式就是 预留缓存模式Cache Aside Pattern。 1、读的时候先读缓存缓存没有的话就读数据库然后取出数据后放入缓存同时返回响应。 2、更新的时候先删除缓存然后再更新数据库这样读的时候就会发现缓存中没有数据而直接去数据库中拿数据了。 29、redis如何实现延迟队列 延迟队列是指把当前要做的事情往后推迟一段时间再做。延迟队列的常见使用场景有以下几种 1、在淘宝、京东等购物平台上下单超过一定时间未付款订单会自动取消 2、打车的时候在规定时间没有车主接单平台会取消你的单并提醒你暂时没有车主接单 3、点外卖的时候如果商家在10分钟还没接单就会自动取消订单 在 Redis 可以使用有序集合ZSet的方式来实现延迟消息队列的ZSet 有一个 Score 属性可以用来存储延迟执行的时间。 使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务 通过循环执行队列任务即可。 30、Redis的大key如何处理 大 key 并不是指 key 的值很大而是 key 对应的 value 很大。 String 类型的值大于 10 KB Hash、List、Set、ZSet 类型的元素的个数超过 5000个 大key会带来哪些影响 1、客户端超时阻塞。由于 Redis 执行命令是单线程处理然后在操作大 key 时会比较耗时那么就会阻塞 Redis从客户端这一视角看就是很久很久都没有响应。 2、引发网络阻塞。每次获取大 key 产生的网络流量较大如果一个 key 的大小是 1 MB每秒访问量为 1000那么每秒会产生 1000MB 的流量这对于普通千兆网卡的服务器来说是灾难性的。 3、阻塞工作线程。如果使用 del 删除大 key 时会阻塞工作线程这样就没办法处理后续的命令。 4、内存分布不均。集群模型在 slot 分片均匀情况下会出现数据和查询倾斜情况部分有大 key 的 Redis 节点占用内存多QPS 也会比较大。 如何查找大key redis-cli --bigkeys 查找大key 使用 SCAN 命令查找大 key 使用 RdbTools 工具查找大 key 如何删除大key 分批次删除1、对于删除大hash使用hscan命令每次获取100个字段再用hdel命令每次删除一个字段。2、对于大list通过ltrim命令每次删除少量元素。3、对于大set使用sscan命令每次扫描集合中100个元素再用srem命令每次删除一个键。4、对于大zset使用zremrangebyrank命令每次删除top 100个元素。异步删除用unlink命令代替del命令。Redis 会将这个 key 放入到一个异步线程中进行删除这样不会阻塞主线程。 31、Redis管道有什么用 管道技术Pipeline是客户端提供的一种批处理技术用于一次处理多个 Redis 命令从而提高整个交互的性能。 使用管道技术可以解决多个命令执行时的网络等待它是把多个命令整合到一起发送给服务器端处理之后统一返回给客户端这样就免去了每条命令执行后都要等待的情况从而有效地提高了程序的执行效率。 但使用管道技术也要注意避免发送的命令过大或管道内的数据太多而导致的网络阻塞。 要注意的是管道技术本质上是客户端提供的功能而非 Redis 服务器端的功能。 十一、计算机网络 1、说说网络体系结构 计算机网络体系结构主要有三类OSI七层网络模型、TCP/IP四层模型和五层模型。其中OSI是理论上的网络模型TCP/IP是实际上的网络模型五层是一种这种的网络模型。 OSI七层网络模型是国际标准化组织指定的一个用于计算机或者通信系统互联的标准体系。 物理层功能利用传输介质为数据链路层提供物理连接实现比特流的透明传输模拟信号和数字信息之间的相互转换。作用实现相邻计算机节点之间比特流的透明传输尽可能屏蔽掉具体传输介质的和物理设备的差异使得数据链路层不必考虑网络的具体传输介质是什么。 数据链路层功能在物理层提供的比特流的基础上通过差错控制、流量控制方法使有差错的物理线路变为无差错的数据链路即提供可靠的通过物理介质传输数据的方法。 网络层 2、说说各个网络层级对应的网络协议有哪些 3、数据在各个层级中是怎么传输的 对于发送方而言从上层到下层层层包装对于接收方而言从下层到上层层层解开包装。 4、从浏览器地址栏输入url到显示主页的过程是什么 1、解析url生成对应的http请求信息。 2、首先根据输入的域名地址进行域名解析找到对应的ip地址。浏览器缓存、操作系统缓存、hosts文件、本地域名服务器[联通移动提供]、根域名服务器、顶级域名服务器、授权域名服务器。 3、通过TCP与服务器三次握手建立tcp连接。 4、向服务器发送请求数据服务器解析并返回响应数据。 5、浏览器渲染解析响应数据并渲染页面。 5、四次挥手断开tcp连接。 5、说说DNS的解析过程 DNS英文全称是 domain name system域名解析系统它的作用也很明确就是域名和 IP 相互映射。 搜索路径主要是浏览器缓存 à 操作系统缓存 à hosts文件 à 本地域名服务器 à 根域名服务器 à 顶级域名服务器  à 授权域名服务器。 6、说说WebSocket和Socket的区别 Socket是TCP/IP网络的API是为了方便使用TCP或UDP而抽象出来的一层是位于应用层和传输控制层之间的一组接口为了方便开发者更好的网络编程而WebSocket则是一个典型的应用层协议。 WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信能更好的节省服务器资源和带宽并达到实时通讯它建立在 TCP 之上同 HTTP 一样通过 TCP 来传输数据。主要是用来解决http不支持持久化连接的问题。 7、说说你了解的端口及其服务 8、说说HTTP常用的状态码及其含义 HTTP状态码首先应该知道个大概的分类 1XX信息性状态码提示目前是协议处理的中间状态还需要后续操作 2XX成功状态码 3XX重定向状态码 4XX客户端错误状态码 5XX服务端错误状态码 「200 OK」是最常见的成功状态码表示一切正常。 「301 Moved Permanently」表示永久重定向说明请求的资源已经不存在了需改用新的 URL 再次访问。 「302 Found」表示临时重定向说明请求的资源还在但暂时需要用另一个 URL 来访问。 「304 Not Modified」不具有跳转的含义表示资源未修改重定向已存在的缓冲文件也称缓存重定向也就是告诉客户端可以继续使用缓存资源用于缓存控制。 「400 Bad Request」表示客户端请求的报文有错误但只是个笼统的错误。 「403 Forbidden」表示服务器禁止访问资源并不是客户端的请求出错。 「404 Not Found」表示请求的资源在服务器上不存在或未找到所以无法提供给客户端。 「500 Internal Server Error」与 400 类型是个笼统通用的错误码服务器发生了什么错误我们并不知道。 「501 Not Implemented」表示客户端请求的功能还不支持类似“即将开业敬请期待”的意思。 「502 Bad Gateway」通常是服务器作为网关或代理时返回的错误码表示服务器自身工作正常访问后端服务器发生了错误。 「503 Service Unavailable」表示服务器当前很忙暂时无法响应客户端类似“网络服务正忙请稍后重试”的意思。 301和302的区别 301永久性移动请求的资源已被永久移动到新位置。服务器返回此响应时会返回新的资源地址。 302临时性性移动服务器从另外的地址响应资源但是客户端还应该使用这个地址。 9、HTTP有哪些请求方法 10、GET和POST的区别 1、从传参角度GET是将信息放在url中在地址栏就可以看到POST请求是将信息放在请求体中。这一带你使得GET请求传递的参数是有限的因为url本身有长度限制。 2、GET请求是幂等和安全的而POST请求不是幂等和安全的。幂等性是指一次或者多次操作都是一样的结果。安全是指请求之后不会修改服务器中的数据。 3、GET请求的url会原原本本的缓存在浏览器的历史记录中。而POST请求则不会被缓存。 限制url长度的是浏览器url本身并没有长度限制。firefox浏览器支持65536个字符。chrome浏览器支持8182个字符。 11、说说http的报文格式 HTTP报文有两种HTTP请求报文和HTTP响应报文 HTTP请求由请求行、请求头部、空行和请求体四个部分组成。 请求行包括请求方法访问的资源URL使用的HTTP版本。 GET 和 POST 是最常见的HTTP方法除此以外还包括 DELETE、HEAD、OPTIONS、PUT、TRACE 。 请求头格式为“属性名:属性值”服务端根据请求头获取客户端的信息主要有 cookie、host、connection、accept-language、accept-encoding、user-agent 。 请求体用户的请求数据如用户名密码等。 HTTP响应也由四个部分组成分别是状态行、响应头、空行和响应体。 状态行协议版本状态码及状态描述。 响应头响应头字段主要有 connection、content-type、content-encoding、contentlength、set-cookie、Last-Modified、Cache-Control、Expires 。 响应体服务器返回给客户端的内容。 12、URI和URL的区别 URI统一资源标识符(Uniform Resource Identifier URI)标识的是Web上每一种可用的资源如 HTML文档、图像、视频片段、程序等都是由一个URI进行标识的。 URL统一资源定位符Uniform Resource Location)它是URI的一种子集主要作用是提供资源的路径。 它们的主要区别在于URL除了提供了资源的标识还提供了资源访问的方式。这么比喻URI 像是身份证可以唯一标识一个人而 URL 更像一个住址可以通过 URL 找到这个人-à人类住址协议://地球/中国/北京市/海淀区/xx职业技术学院/14号宿舍楼/525号寝/张三.男。 13、说说HTTP/1.01.1和2.0的区别 关键HTTP/1.0 默认是短连接可以强制开启HTTP/1.1 默认长连接HTTP/2.0 采用多路复用。 HTTP/1.0 默认使用短连接每次请求都需要建立一个 TCP 连接。它可以设置Connection: keep-alive 这个字段强制开启长连接。 HTTP/1.1 引入了长连接连接即 TCP 连接默认不关闭可以被多个请求复用。 管道机制即在同一个 TCP 连接里面客户端可以同时发送多个请求可以减少整体的响应时间。 十二、Qt 1、Qt信号和槽的本质是什么 信号槽类似观察者模式回调函数。当某个事件发生之后比如按钮检测到自己被点击了一下它就会发出一个信号signal。这种发出是没有目的的类似广播。如果有对象对这个信号感兴趣它就会使用连接connect函数意思是将想要处理的某个对象的信号和自己的一个函数称为槽slot绑定来处理这个信号。也就是说当信号发出时被连接的槽函数会自动被回调。信号和槽是Qt特有的信息传输机制是Qt设计程序的重要基础它可以让互不干扰的对象建立一种联系。 槽的本质是类的成员函数其参数可以是任意类型的。和普通C成员函数几乎没有区别它可以是虚函数也可以被重载可以是公有的、保护的、私有的、也可以被其他C成员函数调用。唯一区别的是槽可以与信号连接在一起每当和槽连接的信号被发射的时候就会调用这个槽。 2、信号槽机制有什么优势和不足 ************ 优势 ①类型安全。需要关联的信号槽的签名必须是等同的。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。若信号和槽签名不一致编译器会报错。 ②松散耦合。信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是那个对象的那个信号槽接收它发出的信号它只需在适当的时间发送适当的信号即可而不需要关心是否被接受和那个对象接受了。Qt就保证了适当的槽得到了调用即使关联的对象在运行时被删除。程序也不会奔溃。 ③灵活性。一个信号可以关联多个槽或多个信号关联同一个槽。 不足之处速度较慢。与回调函数相比信号和槽机制运行速度比直接调用非虚函数慢10倍。 原因①需要定位接收信号的对象。②安全地遍历所有关联槽。③编组、解组传递参数。④多线程的时候信号需要排队等待。然而与创建对象的new操作及删除对象的delete操作相比信号和槽的运行代价只是他们很少的一部分。 3、多线程下信号和槽分别在什么线程中执行如何控制 ***** 可以通过connect的第五个参数进行控制信号槽执行时所在的线程。 connect有几种连接方式直接连接和队列连接、自动连接 直接连接Qt::DirectConnection信号槽在信号发出者所在的线程中执行。 队列连接 (Qt::QueuedConnection)信号在信号发出者所在的线程中执行槽函数在信号接收者所在的线程中执行。 自动连接  (Qt::AutoConnection)多线程时为队列连接函数单线程时为直接连接函数。 4、描述QT中的文件流(QTextStream)和数据流(QDataStream)的区别 **** 文件流(QTextStream)。操作轻量级数据int,double,QString数据写入文本件中以后以文本的方式呈现。 数据流(QDataStream)。通过数据流可以操作各种数据类型包括对象存储到文件中数据为二进制。 文件流数据流都可以操作磁盘文件也可以操作内存数据。通过流对象可以将对象打包到内存进行数据的传输。 5、描述Qt的TCP通讯流程      **** 服务端QTcpServer 创建QTcpServer对象监听listen需要的参数是地址和端口号当有新的客户端连接成功会发送newConnect信号在newConnection信号槽函数中调用nextPendingConnection函数获取新连接QTcpSocket对象连接QTcpSocket对象的readyRead信号在readyRead信号的槽函数中使用read接收数据调用write成员函数发送数据。 客户端QTcpSocket ①创建QTcpSocket对象 ②当对象与Server连接成功时会发送connected 信号 ③调用成员函数connectToHost连接服务器需要的参数是服务端地址和端口号 ④connected信号的槽函数开启发送数据 ⑤使用write发送数据read接收数据 6、描述UDP之UdpSocket通讯   ****** UDPUser Datagram Protocol即用户数据报协议是一个轻量级的不可靠的面向数据报的无连接协议。在网络质量令人十分不满意的环境下UDP协议数据包丢失严重。由于UDP的特性它不属于连接型协议因而具有资源消耗小处理速度快的优点所以通常音频、视频和普通数据在传送时使用UDP较多因为它们即使偶尔丢失一两个数据包也不会对接收结果产生太大影响。所以QQ这种对保密要求并不太高的聊天程序就是使用的UDP协议。 ①创建QUdpSocket套接字对象 ②如果需要接收数据必须绑定端口 发送者需要确定接收者的端口号和IP ③发送数据用writeDatagram接收数据用 readDatagram 。 7、多线程使用方法 在进行桌面应用程序开发的时候 假设应用程序在某些情况下需要处理比较复杂的逻辑 如果只有一个线程去处理就会导致窗口卡顿无法处理用户的相关操作。这种情况下就需要使用多线程其中一个线程处理窗口事件其他线程进行逻辑运算多个线程各司其职不仅可以提高用户体验还可以提升程序的执行效率。 使用方式一①创建一个类从QThread类派生类②在子线程类中重写 run 函数, 将处理操作写入该函数中 ③在主线程中创建子线程对象, 启动子线程, 调用start()函数。 使用方式二 创建一个新的类可以理解为业务类这个类从QObject中派生在这个类中添加一个公共的成员函数函数体就是要让子线程去执行的业务逻辑 在主线程创建一个QThread对象这就是子线程对象 在主线程中创建工作的类对象千万不要指定给创建的对象指定父对象 将MyWork对象移动到创建的子线程对象中, 需要调用QObject类提供的moveToThread()方法 启动子线程调用 start(), 这时候线程启动了, 但是移动到线程中的对象并没有工作调用MyWork类对象的工作函数让这个函数开始执行这时候是在移动到的那个子线程中运行的。 第二种创建方式的优点假设有多个不相关的业务流程需要被处理那么就可以创建多个类似于MyWork的类将业务流程在这多个业务类的公共成员函数中然后将这个业务类的实例对象移动到对应的子线程中moveToThread()就可以了这样可以让编写的程序更加灵活可读性更强更易于维护。 8、C协程 线程内核态线程、用户态线程。 协程本质是处理自身挂起和恢复的用户态线程。协程切换要比线程切换速度更快适合I0密集型任务。 协程分类有栈协程(改变函数调用栈)和无栈协程(基于状态机或闭包)。 9、说说读写锁、互斥锁、自旋锁 1、互斥锁。互斥锁在访问共享资源前对互斥量进行加锁在访问完成后释放互斥量进行解锁。对互斥量加锁以后任何其他试图再次对互斥量加锁的线程都会被阻塞直至当前线程释放该互斥量。 2、自旋锁。自旋锁与互斥量类似但它不使线程进入阻塞态而是在获取锁之前一直占用CPU处于忙等自旋状态。自旋锁适用于锁被持有的时间短且线程不希望在重新调度上花费太多成本的情况。 3、读写锁。读写锁有三种状态读模式加锁、写模式加锁和不加锁一次只有一个线程可以占有写模式的读写锁但是多个线程可以同时占有读模式的读写锁。读写锁非常适合对数据结构读的次数远大于写的情况。 10、Qt的智能指针 在Qt框架中有两种主要类型的智能指针QSharedPointer 和 QScopedPointer。 QSharedPointer 是Qt的共享指针它基于引用计数来管理动态分配的对象。多个 QSharedPointer 实例可以共享同一个对象并在没有引用时自动释放内存。通过与QWeakPointer弱指针配合使用可以解决循环引用的问题。 QScopedPointer 是Qt的本地指针它用于管理动态分配的对象的生命周期但不支持共享。 11、Qt连接数据库及其步骤 在Qt中连接MySQL数据库的原理和机制涉及使用Qt的数据库模块其中包括了Qt的数据库驱动和Qt的SQL类。 1、包含必要的头文件和库 QtSql配置Qt的项目配置文件Qt sql 2、建立数据库连接设置必要的用户名和密码以及需要连接的数据库名。 3、执行sql语句 4、关闭连接。 12、数组和链表的区别 1、内存分配方式不同数组在内存中分配在一块连续的内存空间中数组元素按照索引顺序一次存储在这块内存中支持随机存取。链表的元素在内存不一定按照连续的内存地址存储每个节点都包含了数据和指向下一个节点的指针。不支持随机存取。 2、大小可变性数组是大小固定的一旦分配难以改变。链表没有大小限制只需要将新的节点链接到链表中即可。 3、插入和删除操作在数组中插入和删除操作可能会涉及到数组元素后移前移效率较低。链表的插入和删除操作简单只需要修改链表指向即可。 4、内存开销数组通常会分配一块固定大小的内存无论数据量多少都会占用这个内存空间。链表内存空间更加灵活仅分配所需的节点空间。 5、使用场景数组适用于需要快速随机访问元素、元素数量固定或者很少变化的情况。链表适用于需要频繁插入和删除元素、元素数量动态变化的情况以及内存有限或需要动态分配内存的情况。 13、栈和队列的区别 栈是一种先进后出的数据结构队列是一种先进先出的数据结构。 栈是在栈顶删除元素队列是在队头 删除元素。 栈适用于需要按照先进后出的顺序处理数据的场景如函数调用的执行过程函数调用栈或回退操作的实现。队列适用于需要按照先进先出的顺序处理数据的场景如任务队列、缓冲区管理、广度优先搜索等。 14、自定义控件流程 1、继承需要自定义的控件类如QPushButton. 2、外观设计上设计Qss继承绘制函数实现重绘继承QStyle相关类重绘、组合拼装等。 3、功能行为上添加或者修改信号槽等等。 15、Qt的优势 1、优良的跨平台特性。 2、完全面向对象 3、丰富的API                     4、跨平台集成开发环境QT Creator 16、Qt的MVD了解吗 Qt的MVD包含三个部分Model(模型)View(视图)代理(Delegate)。Model否则保存数据View负责展示数据Delegate负责ltem样式绘制或处理输入。这三部分通过信号槽来进行通信当Model中数据发生变化时将会发送信号到View在View中编辑数据时Delegate负责将编辑状态发送给Model层。基类分别为QAbstractitemModel、QAbstractitemView、QAbstractitemDelegate。Qt中提供了默认实现的MVD类如QTableWidget、QListWidget、QTreeWidget等。 17、Qt对象树 QT提供了对象树机制能够自动、有效的组织和管理继承自QObject的对象 每个继承自QObject类的对象通过它的对象链表(QObjectList)来管理子类对象当用户创建一个子对象时其对象链表相应更新子类对象的信息对象链表可通过children0获取。当父类对象析构的时候其对象链表中的所有(子类)对象也会被析构父对象会自动将其从父对象列表中删除QT保证没有对象会被delete两次。开发中手动回收资源时建议使用deleteLater代替delete,因为deleteLater多次是安全的。 18、Qt三大核心机制 1、信号槽。实现对象之间的通信。 2、元对象系统。元对象系统分为三大类QObject类、Q_OBJECT宏和元对象编译器moc。Qt的类包含Q_OBJECT宏moc元对象编译器实际是一个预处理器会对该类编译成标准的C代码。 3、事件模型。Qt的事件主要有鼠标事件、键盘事件、窗口调整事件等。Qt通过调用虚函数QObject::event()来交付事件。主事件循环通过调用QCoreApplication::exec()启动。一般来说事件是由触发当前的窗口系统产生的但是也可以通过QCoreApplication::sendEvent()函数和postEvent()来手动产生事件。sendEvent会立即发送事件postEvent则会将事件放在事件队列中分发。 19、对QObject的理解 1、QObject类是Qt所有类的基类。 2、QObject是Qt对象模型的核心。这个模型的中心要素就是一种强大的叫做信号与槽无缝对象沟通机制。可以用connect()函数来把一个信号连接到槽也可以用disconnect()函数来破坏这个连接。为了免永无止境的通知循环你可以用blocksignal0 函数来暂时阻塞信号。保护函数connectNotify()和disconnectNotify()可以用来跟踪连接。 3、对象树都是通过QObject 组织起来的当以一个对象作为父类创建一个新的对象时这个新对象会被动加入到父类的 children()队列中。这个父类有子类的所有权。能够在父类的析构函数中自动删除子类。可以通过findChild()和findChildren()函数来寻找子类。 4、每个对象都一个对象名称objectName()而且它的类名也可以通过metaObject()函数获取。你可以通过inherits()函数来决定一个类是否继承其他的类。当一个对象被删除时它会发射destory() 信号你可以抓住这个信号避免某些事情。 5、对象可以通过event()函数来接收事情以及过减来自其他对象的事件。就好比installEventFiter() 函数和eventFilter()函数。childEvent()函数能够重载实现子对象的事件。 十三、设计模式 1、什么是设计模式六大原则是什么 设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。 六大原则开放封闭原则尽量通过扩展软件实体来解决需求变化而不是通过修改已有代码来完成变化里氏代换原则依赖倒转原则对接口编程在程序代码中传递参数时或者在关联关系中尽量引用层次高的抽象类接口隔离原则使用多个隔离的接口比使用单一接口更好最少知道原则单一职责原则一个方法只负责一件事情降低耦合。 2、什么是单例模式在哪些场景中有所使用 保证在整个应用程序运行过程中只有一个实体对象。 1、网站的计数器一般也是采用单例模式实现否则难以同步。 2、应用程序的日志应用一般都是单例模式实现只有一个实例去操作才好否则内容不好追加显示。 3、多线程的线程池的设计一般也是采用单例模式因为线程池要方便对池中的线程进行控制 4、Windows的任务管理器就是很典型的单例模式他不能打开俩个 5、windows的回收站也是典型的单例应用。在整个系统运行过程中回收站只维护一个实例。 3、单例模式优缺点 优点 在单例模式中活动的单例只有一个实例对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化确保所有的对象都访问一个实例 单例模式具有一定的伸缩性类自己来控制实例化进程类就在改变实例化进程上有相应的伸缩性。 提供了对唯一实例的受控访问。 由于在系统内存中只存在一个对象因此可以 节约系统资源当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。 允许可变数目的实例。 避免对共享资源的多重占用。 缺点 不适用于变化的对象如果同一类型的对象总是要在不同的用例场景发生变化单例就会引起数据的错误不能保存彼此的状态。 由于单利模式中没有抽象层因此单例类的扩展有很大的困难。 单例类的职责过重在一定程度上违背了“单一职责原则”。 滥用单例将带来一些负面问题如为了节省资源将数据库连接池对象设计为的单例类可能会导致共享连接池对象的程序过多而出现连接池溢出如果实例化的对象长时间不被利用系统会认为是垃圾而被回收这将导致对象状态的丢失。
http://www.zqtcl.cn/news/102271/

相关文章:

  • 网站查看空间商网站不提交表单
  • 空间怎么上传网站企业所得税怎么算公式
  • 网站建设wix建筑公司网站设计思路
  • 门户型网站都有哪些网页制作的视频教程
  • 虚拟主机 多个网站没有备案的网站
  • 河南网站建设推广公司汕尾网站建设
  • 海南省建设网站首页公司网站图片传不上去
  • 中国建设银行网站评价广告投放都有哪些平台
  • 网站系统免费wordpress附件不在数据库
  • 网站开发国外研究状况电商推广是什么意思
  • 太原建高铁站wordpress分级菜单显示
  • 工信部网站备案变更运营一个app大概多少钱
  • 杭州网站建设公司哪家好网站建设 中国联盟网
  • 成都手机网站建设价格网站安全检测软件
  • 长沙申请域名网站备案找个做游戏的视频网站
  • 网站平台开发与应用面试西安seo优化顾问
  • 苏州网站制作及推广中国优秀的企业网站
  • 网站开发语言太老东莞哪家公司做网站比较好
  • 单位网站制作费用报价单博客和个人网站建设情况
  • 山东网站建设公司电话全球建筑设计网站
  • wordpress 站点描述国外优秀网页设计赏析
  • php红酒网站建设软件开发外包项目合作
  • 做网站的都改行做什么了上海推牛网络科技有限公司
  • 在哪里建设网站dedecms做网站注意事项
  • 垂直类网站怎么做推广互联网站的建设维护营销
  • 手机网站大全排行江西省赣州市邮政编码
  • 集团网站建设建站模板seo优化工具软件
  • 大连项目备案网站网站建设一下需要多少费用
  • 松溪网站建设做网站外包
  • sdcms网站建设模板WordPress自定义连接菜单