八股文


C++面试常见问题

1 C和C++的区别

C程序的设计首要考虑的如何设计一个过程,对输入进行运算处理得到输出。而对于C++而言我们在c的基础上增加了类,这就是面向对象这一个思想,我们在设计程序的时候首先要考录如何构造一个对象模型,让这个对象契合对应的问题域,这样就可以通过获得对象的状态信息得到输出。

C 和 C++动态管理内存的方法不一样,C 是使用 malloc/free 函数,而 C++除此之外还有 new/delete 关键字;(关于 malooc/free 与 new/delete 的不同又可以说一大堆,最后的扩展_1 部分列出十大区别);

接下来就不得不谈到 C 中的 struct 和 C++的类,C++的类是 C 所没有的,但是 C中的 struct 是可以在 C++中正常使用的,并且 C++对 struct 进行了进一步的扩展,使 struct 在 C++中可以和 class 一样当做类使用,而唯一和 class 不同的地方在于 struct 的成员默认访问修饰符是 public,而 class 默认的是 private;

C++支持函数重载,而 C 不支持函数重载,而 C++支持重载的依仗就在于 C++的名字修饰与 C 不同,例如在 C++中函数 int fun(int ,int)经过名字修饰之后变为 _fun_int_int ,而 C 是_fun,一般是这样的,所以 C++才会支持不同的参数调用不同的函数;

C++中有引用,而 C 没有;这样就不得不提一下引用和指针的区别

2 如何理解抽象、封装、继承、多态

抽象

就是把现实世界中的某一类东西,提取出来,用程序代码表示,抽象出来的一般叫做类或者接口。抽象并不打算了解全部问题,而是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一个数据抽象,而是过程抽象。

数据抽象 –>表示世界中一类事物的特征,就是对象的属性。比如鸟有翅膀,羽毛等(类的属性)

过程抽象 –>表示世界中一类事物的行为,就是对象的行为。比如鸟会飞,会叫(类的方法)

封装

封装又称信息的隐藏,利用抽象类型,也就是一个类,将数据和基于数据的操作封装在一起,使得数据被保护在抽象数据类型的内部,同时尽可能隐藏一些细节,只保留一些对外的接口与外界保持联系。用户无需直到对象内部的方法实现细节,但是可以根据对象提供的外部接口访问对象。

优点:实现了专业的分工,将某一个题顶功能的代码封装成独立的实体之后,各个程序员可以在需要的时候调用,从而实现了专业的分工。

同时我认为模块化是封装的本质。在软件设计的时候,进行模块化设计。将复杂的系统拆分成各种模块。各种模块可以单独设计调试,让很多人可以同时开发一个项目。封装是模块化不可缺少的一部分,和你一起开发的人员可以调用你的模块而不用管实现的细节,大大加快工作效率。比如。一块银特尔芯片,有强大的功能,但是我们不需要知道内部如何实现,就可以很好的应用他。

多态

多态指同一个对象同时具有多种形式。

多态分为静态多态和动态多态,静态多态主要是重载,动态多态使用虚函数机制实现的。

在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

继承

继承可以使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类别追加新的属性和方法也是常见的做法。

重新理解面向对象

变化是复用的天敌,面向对象设计最大的优势在于抵御变化。将变化的范围降到最小。

隔离变化

  • 从宏观角度上,面向对象的构建方式更能适应软件的变化,能将变化带来的影响降低到最小。

各司其职

  • 从微观角度上来看,面向对象的方式更强调对象的“责任”。
  • 由于需求变化导致的新增的类型不应该影响原来类型的实现,——各负其责,主要使用了多态机制,接口一样,实现不一样,多态机制也实现了责任的分派。

对象是什么?

  • 从语言角度上来看,对象封装了代码和数据。
  • 从规格上讲,对象是一系列可被使用的公共接口。
  • 从概念上讲,对象是某种拥有责任的抽象。

五大基本原则

  • 单一职责原则SRP(Single Responsibility Principle)

一个类应该仅有一个引起变化的原因变换的方向隐藏着类的责任是指一个类的功能要单一,不能包罗万象。如同一个人一样,分配的工作不能太多,否则一天到晚虽然忙忙碌碌的,但效率却高不起来。

  • 开放封闭原则OCP(Open-Close Principle)

一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。在不改动原有的功能上添加新的功能。

  • 里式替换原则LSP(the Liskov Substitution Principle LSP)

子类应当可以替换父类并出现在父类能够出现的任何地方。

  • 接口隔离原则ISP(the Interface Segregation Principle ISP)

不应该强迫客户程序依赖他们不用的方法,接口应该小而完备。有必要暴露的方法才做成public。

  • 依赖倒置原则DIP(the Dependency Inversion Principle DIP)

高层模块(稳定)不应该依赖于低层模块(变化),二者应该依赖于抽象(稳定)。

抽象(稳定)不应该依赖于实现细节(变化),实现细节(变化)依赖于抽象(稳定)。

  • 优先使用对象的组合,而不是类继承

类继承在莫种程度上破坏了封装性,暴露的东西过多,也就是说子类父类耦合度高。而对象组合则只要求被组合的对象具有良好定义的接口。耦合度低。

  • 封装变化点

一侧变化,一侧稳定。使用封装来创建对象之间的分界层,让这设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现松耦合

  • 针对接口编程,而不是针对实现编程

不将变量类型声明为莫个特定的具体类,而是声明为某个接口。客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。减少系统中的依赖关系,从而实现,高內聚,松耦合。

3 虚函数的作用及实现原理

虚函数的作用:虚函数实现了多态的机制。基类定义了虚函数,子类重新定义父类的虚函数之后,父类根据赋值给他的子类指针,动态的调用子类的该函数。

实现原理:当一个类声明了一个虚函数的时候,那么在这个类实例化的时候,类的对象模型就会生成一个额外的数据,就是一个虚函数表指针,这个虚函数表指针就指向虚函数表的地址,虚函数表就是一个函数的指针数组,虚函数表中的每个元素对应一个函数指针指向该类的一个虚函数。当我们调用虚函数的时候,就跟据虚函数指针找到虚函数表,在根据虚函数表找到虚函数的具体实现。

如果子类覆盖了父类的虚函数,将覆盖虚函数表中该虚函数的地址。

4 深拷贝和浅拷贝

深拷贝指定的是对整个对象所有的资源进行一个拷贝,两个对象的内存资源不同释放一个不会影响另一个。浅拷贝值来嗯个对象均指向同一个内存空间,释放一个对象,另一个对象的资源也没了,造成野指针。

5 虚函数、纯虚函数怎么实现

1.用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。

2.存在虚函数的类都有一个1维的虚函数表,类的对象都有一个指向虚表的虚指针。虚指针是和对象对应的,虚表是和类对应的。

3.多态是一个接口多种实现,是面向对象的核心。

4.多态由虚函数来实现,结合动态绑定。

5.纯虚函数是虚函数在加上=0

6.抽象类是指至少包括一个纯虚函数的类。抽象基类不能定义对象。必须在子类实现这个函数,即现有名称,没有内容,在派生类中实现内容。

6 为什么有纯虚函数

1,为了方便使用多态的特性,我们常常在需要的基类定义虚函数。

2 ,很多情况下,基类本身生成对象是不合情理的。例如作为一个动物基类可以派生出老虎,熊猫等子类。但是生成动物对象明显不合理。

所以引入了纯虚函数,将函数定义为纯虚函数,则编译器要求在派生类中必须重写该虚函数以实现多态的特性,同时含有纯虚函数的对象称为抽象类,它不能生成对象。

7 为什么有虚析构函数

C++中基类采用virtual虚析构函数是为了防止泄漏。具体的说,如果派生类中申请了内存空间,并且在其析构函数中对这些内存空间的内容进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象的时候,调用基类的析构函数,而不会发生动态绑定调用派生类的析构函数。导致申请的内存没得到释放产生内存泄漏。

8 构造函数可以是虚函数吗

不能,

从存储的角度来讲,虚函数相应一个存储在虚函数表中的函数指针,但是指向这个虚函数表的指针,虚函数指针是存储在对象的内存空间的。那么问题来了,我们还没有实例化对象,也就没有虚函数表指针,也就找不到虚构造函数,也就不能实例化对象。

从应用的角度上来说,虚函数的目的就是为了在不完全了解细节的情况下也能正确的处理对象。另虚函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数完成你想完成的动作。

9 C++里面构造函数能有返回值吗?

构造函数是没有返回值的,他知识描述了类的初始化行为。但是new一个类实例是有返回值的,因为new返回的是类实例的指针。

10 构造函数和析构函数能被继承吗

子类可以继承父类的所有构造方法,但是不能继承父类的构造和析构函数。因此,在创建子类对象的时候,为了初始化从父类继承来的数据成员,系统需要调用父类的构造方法。

构造原则:

1 如果子类没有定义构造方法,则调用父类的构造方法。

2 如果子类定义了构造方法,不论无参数还是有参数,在创建子类对象的时候,首先执行父类无参数的构造方法,然后执行自己的构造方法。

3 创建子类对象的时候,如果子类的构造函数没有显示调用父类构造函数,则会调用父类的默认无参数构造函数。

4 在创建子类对象的时候,如果子类的构造函数没有显示调用父类的构造函数,且父类只定义了自己的有参构造函数,则会出错。

子类只有在构造对象的时候才能默认(或者用初始化列表显示调用特定的父类构造函数)调用父类的构造函数,在构造完成后不能向调用父类成员函数一样调用父类的构造函数,这样保证了父类构造函数只能调用一次的原则。

11 C++中重载和覆盖、隐藏的区别

Overload重载(模板),静态多态:在C++程序中,可以将语义,功能相似的函数用同一个名字表示,但是参数或者返回值不同,即为函数重载。(在相同范围内,同一个类中)

Override覆盖,动态多态:值派生类函数覆盖父类的函数;其特征是1在不同的范围内基类和派生类中,函数名字相同,参数相同,基类必须要有virtual关键字

隐藏:值得是派生类屏蔽了与其同名的基类函数,派生类与基类的方法同名,参数不同时,不论有无virtual关键字,基类函数将被隐藏。

如果参数相同,但是基类没有virtual关键字,基类的函数将被隐藏。

12 一个空的class 里有什么

构造函数,析构函数 ,拷贝复制构造函数,赋值运算符重载,取地址运算符重载,被const修饰的取地址值操作符号重载

13 C++中一个空类的大小为什么是1

这是实例化的原因,实例化一个对象一定在内存中有独一无二的地址,那么这个地址就一定要有实际指向的位置,编译器就会往往给空类隐含的加一个字节,这样空类在实例化之后才能在内存中获得一个独一无二的地址。

14 一个结构体有int ,char,stactic int这个结构体占多少内存

假设结构体运行环境都是64位,那么结构体中int占4字节,char为了对其占4个字节,静态不计算,解哦固体一共占8字节内存。

特别注意的是: c结构体中不允许定义static变量; C++结构体中可以定义static变量,sizeof时不计算该变量。

对其原则,当结构体内元素小于处理器位数的时候,便以结构体里最长的元素作为对其单位。如果结构体里有元素大于处理器位数,就已处理器位数作为对其单位。

15 结构体与联合体的区别

结构体

结构体中各成员拥有自己的内存,各自使用互补相干涉,同时存在的遵循内存对其规则。

联合体union

各个变量共用一块内存空间,并且同时只有一个成员能得到这块内存的使用权,各个变量共用一个内存的首地址。因此,联合体比结构体更节省内存。

16 函数和宏的差别

1 宏在预处理的阶段展开,进行文本替换,占用的是编译过程的时间,而函数占用执行时间。

2 宏直接展开,没有函数执行时候的压栈消耗,所以比函数执行快,规模小。

3 函数执行的的时候需要空间开销,既要保存现场,函数执行完之后又要恢复现场,宏则不需要。

4 宏在使用的时候会出现优先级问题,导致程序出错

5 宏函数定义的参数没有类型,预处理只负责形式上的替换,不做类型检查,危险性高。

6 宏不方便调试,也不允许递归

17 define和typedef的区别

1 执行时间不同,define只是在预处理阶段进行机械简单的文本难题,不做正确性检查。typedef在编译阶段有效,有类型检查的功能。

2 功能不同 typedef 用来定义类型的别名起到便于记忆的功能。另一个是定义机器无关的类型。比如定义一个REAL的浮点类型,在目标机器上,他可以获最高精度:typdef long doubl REAL;不支持long double的机器上看起来是这样的typedef double REAL; 在不支持double的机器上看起来是这样的typedef float REAL;

#define 不止可以为变量取名,还可以定义常量,变量等等其他的东西。

3 作用域不同,define没有作用域的限制,只要是之前定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域

18 在C++中include”“”和include <>有什么区别

include <>一般从编译器自带的库文件中寻找文件,而“”一般是从自定义的文件夹中寻找文件,如果不存在从函数库中寻找文件。

19 C语言中的malloc/new和C++中的free/delete的区别和联系

区别1:类型

malloc/free是函数,需要函数库支持,而new/delete是关键字,操作符,需要编译器支持。

区别2:作用

malloc和free知识简单的进行内存的申请和释放,new和delete除了内存的申请和释放还会调用对象的构造函数和析构函数对分配到的空间进行初始化和清理 。

3:参数和返回值

malloc/free需要手动计算申请空间的大小,返回值是void*,需要自己转换为自己需要的类型。new/和delete可以根据对象自动计算要分配内存的大小,返回相应对象的指针。

4 分配失败的时候new会跑粗bac_alloc异常。malloc分配内存失败时返回NULL.

20 new和delete的实现原理,delete是如何知道释放内存的大小

new 的实现过程,先调用operator new(operator new[]) 申请足够的内存(内部通常由malloc实现),然后在分配的内存上调用构造函数,对象分配了空间并且构造完成,返回一个指向该对象的指针。

delete的实现过程,先调用析构函数,然后由operator delete 函数释放内存(底层通常由free实现)。

delete怎么知道要释放的大小,需要在new[]一个数组对象的时候,分配数组空间的时候多分配了4个字节的大小,专门保护数组的大小,用delete[]的时候就会取出这个保存的数,就知道调用几次析构函数了。

21 malloc申请的内存能用delete释放吗

不能,new和delete完全可以取代malloc和free的。malloc/和free操作的对象必须是明确大小的。new和delete会自动进行类型的检查和大小,malloc和free不能执行构造函数和析构函数,如果我的内存中有一个指针指向堆中的一块内存,我指定对象内存大小,调用了free释放到了我本身的内存,就会产生内存泄漏的情况,有一块之前在创建对象时分配的内存没有被释放。

22 malloc和free的实现原理

在标准c库中,提供了malloc/free函数分配和释放内存,两个函数是通过brk、mmap、munmap来实现的;

malloc小于128k的内存的时候,使用brk分配内存,brk是将数据段的最该地址指针_edata往高地址推,在malloc大于128k的时候,使用mmap分配内存,在堆和栈之间找一对空闲的内存分配。这两种方式分配的是虚拟内存,没有分配物理内存。真正的分配要在第一次访问已经分配的虚拟地址空间的时候,发生缺页中断,由操作希用分配物理内存,建立虚拟内存和物理内存之间的映射关系。

工作机制

  malloc函数的实质体现在它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。操作系统中记录一个记录空闲内存地址的链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。

23 malloc 、realloc、calloc的区别

1malloc函数

void* malloc(unsigned int num_size);

int *p = (int *)malloc(20*sizeof(int));

申请20个int类型的空间

2 calloc函数

void* calloc(size_t n,size_t size);

int *p = calloc(20,sizeof(int));

malloc的申请空间是随机化的,calloc申请的空间的值是初始化为0的。

3 realloc函数

void realloc(void *p,size_t new_size)

给分配的额外分配空间,用于扩充容量。

这里面有几个问题:

1.堆和栈最大可分配的内存的大小

2.堆和栈的内存管理方式

3.堆和栈的分配效率

首先针对第一个问题,一般来说对于一个进程栈的大小远远小于堆的大小,在linux中,你可以使用ulimit -s (单位kb)来查看一个进程栈的最大可分配大小,一般来说不超过8M,有的甚至不超过2M,不过这个可以设置,而对于堆你会发现,针对一个进程堆的最大可分配的大小在G的数量级上,不同系统可能不一样,比如32位系统最大不超过2G,而64为系统最大不超过4G,所以当你需要一个分配的大小的内存时,请用new,即用堆。

其次针对第二个问题,栈是系统数据结构,对于进程/线程是唯一的,它的分配与释放由操作系统来维护,不需要开发者来管理。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元会被自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,不同的操作系统对栈都有一定的限制。 堆上的内存分配,亦称动态内存分配。程序在运行的期间用malloc申请的内存,这部分内存由程序员自己负责管理,其生存期由开发者决定:在何时分配,分配多少,并在何时用free来释放该内存。这是唯一可以由开发者参与管理的内存。使用的好坏直接决定系统的性能和稳定。

由上可知,但我们需要的内存很少,你又能确定你到底需要多少内存时,请用栈。而当你需要在运行时才知道你到底需要多少内存时,请用堆。

最后针对第三个问题,栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率 比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在 堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会 分 到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

24 C++关键字mutable的作用

如果需要修改在const成员方法里的成员变量值,那么需要将这个成员变量修饰为mutable。即用mutable修饰的成员变量不受const成员方法的限制;

我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成 const 的。但是,有些时候,我们需要在 const 的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被 mutalbe 来修饰。

25 引用和指针有什么区别

1 引用必须被初始化,指针可以不用

2 引用是变量的一个别名,内部实现是只读指针

3 引用初始化之后不能改变,指针改变指向不同的对象。

26 什么是黑盒测试和白盒测试

白盒测试:是通过程序的源代码进行测试而不是用户界面。这种类型的测试需要从代码语句发现内部代码在的算法,溢出,路径,条件等等中的缺点或者错误,进而加以改正。

黑盒测试:通过使用整个软件或者莫种软件功能来严格的测试,而并没有通过检查程序的源码或者很清晰的了解软件的源代码程序是怎样设计的。测试人员通过输入他们的数据然后看输出结果从而了解软件怎样工作。在测试其间,把程序看作一个不能打开的黑匣子,完全不考虑内部结构和内部特性,测试者在程序接口进行测试,它只检查程序是否能适当地接受和正确的输出。

27 野指针是什么?如何检测内存泄漏

野指针,指向被释放的内存或者没有访问权限的内存指针。

野指针主要分为三种

1 指针变量没有被初始化,指针变量没有被初始化它的值将会是随机的。

2 指针p别free或者delete之后没有设置为NULL

3 指针操作超越了变量的作用范围

如何避免野指针

1 对指针变量进行初始化

2 指针用完释放内存之后,将指针赋值为null.

28 悬空指针和野指针的区别

1 野指针:访问一个已经删除或者访问受限的内存区域的指针。

2 悬空指针:一个指针的指向对象已经被删除,那么就成了悬空指针。野指针指的是那些未初始化的指针。

29 内存泄漏

内存泄漏是由于疏忽或者误操作造成程序未能释放掉不再使用的内存情况。

内存泄漏的分类:

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

  2. 系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

  3. 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

后果: 只发生一次小的泄漏可能并不在意,但是泄漏大量的内存的程序将会出现各种症状,性能下降到内存逐渐用完,导致另一个程序失败。

如何排查:使用工具软件

LeakTracer: Linux、Solaris和HP-UX下跟踪和分析C++程序中的内存泄漏

BoundsCheacker,BoundsCheaker是一个运行错误检查工具,它主要定位程序运行其间发生的各种错误。

调试运行DEBUG版本程序,

定位错误:

) 检查、定位内存泄漏
检查方法:在 main 函数最后面一行,加上一句_CrtDumpMemoryLeaks()。调
试程序,自然关闭程序让其退出,查看输出:
输出这样的格式{453}normal block at 0x02432CA8,868 bytes long
被{}包围的 453 就是我们需要的内存泄漏定位值,868 bytes long 就是说这个
地方有 868 比特内存没有释放。
定位代码位置

在 main 函数第一行加上_CrtSetBreakAlloc(453);意思就是在申请 453 这块内
存的位置中断。然后调试程序,程序中断了,查看调用堆栈。加上头文件
#include <crtdbg.h>

30 内存泄漏的解决方案

1 使用内存分配函数一定记得要释放掉

2 将分配内存的指针以链表的形式进行管理,使用完毕之后从链表中删除,程序结束之后可以检查该链表,防止出现野指针。

3 使用智能指针

31 函数指针和指针函数是什么

函数指针本质就是一个指针,指向一个函数

int (*fun)(int x,int y);

指针函数就是返回值为指针的函数

int *fun(int x,int y);

32 c++11新特性了解吗

一般从这四方面回答

1 nullptr,auto,decltype自动类型推导,范围for循环,初始化列表,lambda表达式等等。

2 右值引用和移动语义

3 智能指针

4 C++11多线程编程,thread库及配套原语mutex,lock_guard,condtion_varieable,以及异步std::furture

5 新增容器std::array保存在栈内存中,相比堆内存的std::vector,我们能够灵活的访问这里面的元素,从而获得高性能。

1 nullptr

传统C++会把NULL、0视为一种东西。c++不允许void*隐式转换为其他类型,但是如果将NULL定义为((void) *) 那么当编译 char * ch = NULL时,NULL只好被定义为0;而这依然产生问题,c++重载发生问题

void foo(char*)
void foo(int)

对于这两个函数,调用foo(NULL)时会调用fun(int),自从导致代码违反直观。

c++引入nullptr关键字专门区分空指针和0。nullptr能够隐士的转化为任何指针。

2 类型推导

auto 和 decltype

auto对一个变量自动进行推导,最常见的例子就死和迭代器以前我们需要一个迭代器需要

for(vector<int>::const_iterator itr = vec.begin(); itr != vec.end();++itr)

有了auto之后

for(auto itr = vec.begin()....)

注意 auto不能用于传递参数,不能推导数组类型,无法通过编译

decltype关键字解决关键字只能对变量推导的缺陷。可以对表达式的类型进行推倒

decltype(表达式)

C++11 还引入了一个叫做拖尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:

template<typedef T,typedef U>
auto add(T x,U y) -> decltype(x+y){
    return x+y;
}

C++14中开始可以直接让普通的函数具备返回值推导,,下面写法合法

template<typedef T,typedef U>
auto add(T x,U y){
    return x+y;
}

3 范围for循环

基于范围的迭代写法,简洁

for(auto &i : arr)

4 初始化列表

统一语法在初始化对象

struct A{
    int a;
    float b;
}
struct B{
    B(int _a,float _b):a(_a),b(_b){}
    private:
        int a;
        float b;
}

A a{1,1.1} B b{1,1.1}

5 构造函数

委托构造函数,这使得构造函数可以在一个构造函数中调用另一个构造函数,从而达到简化代码的目的。

继承构造函数

在继承体系中,如果派生类想要使用基类的构造函数,需要在构造函数中显式声明。
假若基类拥有为数众多的不同版本的构造函数,这样,在派生类中得写很多对应的“透传”构造函数。如下:

struct A{
 2     A(int i){}
 3     A(double d,int i){}
 4     A(float f,int i,const char* c){}
 5     //....等等系列的构造函数版本
 6 };
 7 struct B:A{
 8     B(int i):A(i){}
 9     B(double d,int i):A(d,i){}
10     B(float f,int i,const char* c):A(f,i,e){}
11     //....等等好多个和基类构造函数对应的构造函数
12 };
struct A{
 2     A(int i){}
 3     A(double d,int i){}
 4     A(float f,int i,const char* c){}
 5     //....等等系列的构造函数版本
 6 };
 7 struct B:A{
 8     B(int i):A(i){}
 9     B(double d,int i):A(d,i){}
10     B(float f,int i,const char* c):A(f,i,e){}
11     //....等等好多个和基类构造函数对应的构造函数
12 };

C++11继承构造函数

 1 struct A{
 2     A(int i){}
 3     A(double d,int i){}
 4     A(float f,int i,const char* c){}
 5     //....等等系列的构造函数版本
 6 };
 7 struct B:A{
 8     using A::A;
 9     //关于基类各构造函数的继承一句话搞定
10     //.....
11 };

如果继承构造函数不被相干的代码使用,编译器不会产生真正的函数代码,这样比产头基类各种构造按数更加节省目标空间。

6 Lambda 表达式

Lambda表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。

可将lambda表达式视为包含公有operator()的匿名结构或类。

编译器见到lambda表达式时,自动张开类似于结构或类的形式

[](int& element) {cout << element << ' ';}
//自动展开
struct Node{
    void operator() (const int& element){  //  默认为const无法修改
        cout << element << ' ';
    }
}

Lambda表达式的基本语法如下:

1 [caputrue](params)opt->ret{body;};
  1. capture是捕获列表;
  2. params是参数表;(选填)
  3. opt是函数选项;可以填mutable,exception,attribute(选填)
    mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
    exception说明lambda表达式是否抛出异常以及何种异常。
    attribute用来声明属性。
  4. ret是返回值类型(拖尾返回类型)。(选填)
  5. body是函数体。

捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量

  1. []不捕获任何变量。

  2. [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。

  3. [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。

  4. [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。

  5. [bar]按值捕获bar变量,同时不捕获其他变量。

  6. [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lambda中使用当前类的成员函数和成员变量。

7)如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。

在多种捕获方式中,最好不要使用[=]和[&]默认捕获所有变量。

默认引用捕获所有变量,你有很大可能会出现悬挂引用(Dangling references),因为引用捕获不会延长引用的变量的生命周期:

1 std::function<int(int)> add_x(int x){
2     return [&](int a){return x+a;};
3 }

上面函数返回了一个lambda表达式,参数x仅是一个临时变量,函数add_x调用后就被销毁了,但是返回的lambda表达式却引用了该变量,当调用这个表达式时,引用的是一个垃圾值,会产生没有意义的结果。上面这种情况,使用默认传值方式可以避免悬挂引用问题。

7 右值引用和move语义

首先什么是引用,引用就是给一个变量起了别名,那右值引用就是将一个右值起了别名,主要用途有两个,右值引用实现了转移语义和完美转发。

移动语义:对于一个包含指针成员变量的类,由于编译器默认的拷贝构造函数都是浅拷贝,所有我们一般需要通过实现深拷贝的拷贝构造函数,为指针成员分配新的内存并进行内容拷贝,从而避免悬挂指针的问题。移动构造函数,可以将资源(堆,系统对象等)从一个对象转移到另一个对象。这样可以减少不必要的拷贝,创建和销毁。

完美转发:需要将一组参数原封不懂的传递给另一个函数。不仅仅是值不变,还有两组属性:左值右值,const/non_const。为了保证这些属性,泛型函数需要重载各种版本,,还需要对应各种const关系。但是如果指定义一个右值引用就迎刃而解了,原因在于:

C++11对T&&的类型推导,右值实参依然是右值,左值实参依然是左值。

8 智能指针

c++里有四个智能指针auto_ptr,shared_ptr,weak_ptr,unique_ptr其中后三个是c++11支持,并且第一个已经被c++11弃用。

作用,智能指针的作用是管理一个指针,防止申请内存后忘记释放造成内存泄漏。智能指针可以很大程度的避免这个问题,因为智能指针就是一个类,用一个对象管理指针,构造时候获得管理权限,析构时自动delete.

1 auto_ptr(c++98的方案),采用所有权的模型。缺点存在潜在的内存崩溃问题。

auto_ptr<string> p1(new string ("i am great"));
auto_ptr<string> p2; p2=p1;
// auto_ptr不会报错,此时不会报错,p2剥夺了p1的所有权,当程序运行时,访问p1会报错。相当于对左值进行了一个转移,相当危险的操作

2 unique_ptr 实现独占式拥有或者严格拥有的概念,保证同一时间只有一个指针能够指向该对象。避免资源泄漏。当离开作用域时自动析构。资源所有权转移只能通过std::move();

unique_ptr<string> p3 (new string ("auto"));
unique_ptr<string> p4;
p4 = p3;//此时会报错!!
//编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

3 shared_ptr,基于引用计数智能指针,多个指针可以指向相同的对象,会统计有多少个对象会同时拥有该内部指针,它使用计数机制来表明资源被几个指针共享,当引用计数为0的时候,自动释放资源。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

成员函数:

use_count 返回引用计数的个数

unique 返回是否是独占所有权( use_count 为 1)

swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的

4 weak_ptr 是一种不控制对象生命周期的智能指针,指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。**,两个shared_ptr相互引用造成死锁,两个指针的引用计数不能下降为0,资源永远不释放。** 基于引用计数的智能指针在面对循环引用的时候无能为力,这个时候就引入了weak_ptr值引用,不计数。weak只可以从一个shared_ptr或者weak_ptr构造,他的构造和析构函数不会引起引用计数的增加或者减小,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:_

_shared_ptr p = pa->pb_.lock();

p->print();

8.1autoptr简单实现

 1 template<class T>
 2 class AutoPointer
 3 {
 4 public:
 5     AutoPointer(T* ptr)
 6       :mPointer(ptr){}
 7 
 8     AutoPointer(AutoPointer<T>& other)
 9     {
10         mPointer= other.mPointer;  //管理权进行转移
11         other.mPointer= NULL;
12     }
13 
14     AutoPointer& operator = (AutoPointer<T>& other)
15     {
16         if(this != &other)
17         {
18             delete mPointer;
19             mPointer = other.mPointer;  //管理权进行转移
20             other.mPointer= NULL;
21         }
22 
23         return *this;
24     }
25 
26     ~AutoPointer()
27     {
28         delete mPointer;
29     }
30 
31     T& operator * ()
32     {
33         return *mPointer;
34     }
35 
36     T* operator -> ()
37     {
38         return mPointer;
39     }
40 
41 private:
42 
43     T* mPointer;
44 };

8.2 shared_ptr

1 template <typename T>
 2 class SharedPointer
 3 {
 4 private:
 5     
 6     class Implement
 7     {
 8     public:
 9         Implement(T* p) : mPointer(p), mRefs(1){}
10         ~Implement(){ delete mPointer;}
11         
12         T* mPointer;  //实际指针
13         size_t mRefs;  // 引用计数
14     };
15     
16     Implement* mImplPtr;
17     
18 public:
19     
20     explicit SharedPointer(T* p)
21       : mImplPtr(new Implement(p)){}
22         
23     ~SharedPointer()
24     {
25         decrease();  // 计数递减
26     }
27     
28     SharedPointer(const SharedPointer& other)
29       : mImplPtr(other.mImplPtr)
30     {
31         increase();  // 计数递增
32     }
33     
34     SharedPointer& operator = (const SharedPointer& other)
35     {
36         if(mImplPtr != other.mImplPtr)  // 避免自赋值
37         {
38             decrease();
39             mImplPtr = other.mImplPtr;
40             increase();
41         }
42         
43         return *this;
44     }
45     
46     T* operator -> () const
47     {
48         return mImplPtr->mPointer;
49     }
50     
51     T& operator * () const
52     {
53         return *(mImplPtr->mPointer);
54     }
55     
56 private:
57     
58     void decrease()
59     {
60         if(--(mImplPtr->mRefs) == 0)
61         {
62             delete mImplPtr;
63         }
64     }
65     
66     void increase()
67     {
68         ++(mImplPtr->mRefs);
69     }
70 };

9 noexcept

我们通过提供noexcept说明,指定某个函数不会抛出异常。其形式是关键字noexcept紧跟在函数的参数列表后面。对于编译器来说,直到函数不会抛出异常有助于简化调用该函数的代码;其次,如果编译器确定函数不会抛出异常,他就能执行某些特殊的优化操作,这些优化操作不适用于可能出错的代码。

需要清楚的一点是,编译器并不会在编译是检查noexcept说明,实际上,如果一个函数在说明了noexcept的同时又含有throw语句或调用了可能抛出异常的其他函数,编译器将顺利的编译通过,并不会因为这种违反异常说明的情况而报错。一旦一个noexcept函数抛出了异常,程序就会调用terminate以确保遵守不再运行时抛出异常的承诺。因此noexcept可以在两种情况下使用:意识我们确定啊哈你输buhl抛出异常,而是我们根本不知道如何处理异常。

noxcept有两层含义,当跟在函数参数列表后边时它是异常说明符号;而当作noexcept异常说明的bool参数出现的时候,它是一个运算符

异常说明的实参,实参必须是一个可以转化为bool类型:如果实参为true,则函数不会抛出异常,如果为false则函数可能抛出异常。

void excpt_func() noexcept;
void excpt_func() noexcept (常量表达式);
void recoup(int) noexcept(true); //recoup不会抛出异常
//声明了recoup使用了noexcept说明符,下列表达式为true
noexcept(recoup(i))
//更加一般的用法,f和g的异常说明一致
    void f() noexcept(noexcept(g()));

noexcept更大的作用是保证应用程序的安全。比如一个类析构函数不应该抛出异常,那么对于常被析构函数调用的delete函数来说,C++11默认将delete函数设置成noexcept,就可以提高应用程序的安全性。

void operator delete(void*) noexcept;
void operator delete[](void*) noexcept;

而同样出于安全考虑,C++11标准中让类的析构函数默认也是noexcept(true)的。当然,如果程序员显式地为析构函数指定了noexcept,或者类的基类或成员有noexcept(false)的析构函数,析构函数就不会再保持默认值。我们可以看看下面的例子:

#include <iostream> 
#include <exception> 
using namespace std;
  
struct A {  
    ~A() noexcept(true){ throw 1; }  
};  
  
struct B {  
    ~B() noexcept(false) { throw 2; }  
};  
  
struct C {  
    B b;  
};  
  
int funA() { A a; }  
int funB() { B b; }  
int funC() { C c; }  
  
int main() {  
    try {  
        funB();  
    }  
    catch(...){  
        cout << "caught funB." << endl; // caught funB.  
    }  
  
    try {  
        funC();  
    }  
    catch(...){  
        cout << "caught funC." << endl; // caught funC.  
    }  
  
    try {  
        funA(); // terminate called after throwing an instance of 'int'  
    }  
    catch(...){  
        cout << "caught funA." << endl;  
    }  
}

在代码中,无论是析构函数声明为noexcept(false)的类B,还是包含了B类型成员的类C,其析构函数都是可以抛出异常的。只有什么都没有声明的类A,其析构函数被默认为noexcept(true),从而阻止了异常的扩散。这在实际的使用中,应该引起程序员的注意。

异常说明与指针、虚函数和拷贝控制

函数指针以及该指针所指的函数必须具有一致的异常说明。也就是说,如果我们为了莫个指针做了不抛出异常的声明,则该指针只能执行不抛出异常的函数。如果指针显示或者隐式说明了指针可能抛出异常,则该指针可以指向任何函数。

void (*pf1)(int) noexcept = recoup;

void (*pf1)(int) = recoup;

虚函数承诺不会抛出异常,则后续派生出来的虚函数页必须作出同样的承诺;与之相反,基类虚函数允许抛出异常,则派生类对应的函数任意。

10、可变模板参数

C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号”。

一个典型的可变模版参数的定义是这样的:

template <class... T>
void f(T... args);

上面的可变模版参数的定义当中,省略号的作用有两个:
1.声明一个参数包T… args,args就相当于一个参数包,这个参数包中可以包含0到任意个模板参数;
2.省略号在模板定义的右边,可以将参数包展开成一个一个独立的参数。

可变模版参数和普通的模版参数语义是一致的,所以可以应用于函数和类,即可变模版参数函数和可变模版参数类,然而,模版函数不支持偏特化

C++11可以使用递归函数的方式展开参数包,获得可变参数的每个值。通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。例如:

#include <iostream>
using namespace std;
//递归终止函数
template <class T>
void print(T t)
{
   cout << t << endl;
}
//递归终止函数
void print()
{
   cout << "empty" << endl;
}
//展开函数,参数包Args...在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用非模板函数print终止递归过程。
template <class T, class ...Args>
void print(T head, Args... rest)
{
   cout << "parameter " << head << endl;
   print(rest...);
}


int main(void)
{
   print(1,2,3,4);
   return 0;
}
//例子2 通过可变参数模板求和
template<typename T>
T sum(T t)
{
    return t;
}
template<typename T, typename ... Types>
T sum (T first, Types ... rest) 
{
    return first + sum<T>(rest...);
}

sum(1,2,3,4); //10

还可以用,逗号和初始化列表展开参数包这种展开参数包的方式,不需要通过递归终止函数,

//是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。
template <class T>
void printarg(T t)
{
   cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
   int arr[] = {(printarg(args), 0)...};
}

expand(1,2,3,4);

我们知道逗号表达式会按顺序执行逗号前面的表达式,比如:

d = (a = b, c);

这个表达式会按顺序执行:b会先赋值给a,接着括号中的逗号表达式返回c的值,因此d将等于c。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

我们可以把上面的例子再进一步改进一下,将函数作为参数,就可以支持lambda表达式了,从而可以少写一个递归终止函数了,具体代码如下:

template<class F, class... Args>void expand(const F& f, Args&&...args) 
{
  //这里用到了完美转发
  initializer_list<int>{(f(std::forward< Args>(args)),0)...};
}
expand([](int i){cout<<i<<endl;}, 1,2,3);

模版偏特化和递归方式来展开参数包

可变参数模板类的展开一般需要定义两到三个类,包括类声明和偏特化的模板类。如下方式定义了一个基本的可变参数模板类:

//前向声明
template<typename... Args>
struct Sum;

//基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...>
{
    enum { value = Sum<First>::value + Sum<Rest...>::value };
};

//递归终止,是特化的递归终止类:
template<typename Last>
struct Sum<Last>
{
    enum { value = sizeof (Last) };
};

这个Sum类的作用是在编译期计算出参数包中参数类型的size之和,通过sum<int,double,short>::value就可以获取这3个类型的size之和为14。

11、可变模板参数类tuple

可变参数模板类是一个带可变模板参数的模板类,类模板 std::tuple 是固定大小的异类值汇集。它是 std::pair 的推广。它的定义如下:

template< class... Types >
class tuple;

这个可变参数模板类可以携带任意类型任意个数的模板参数:


std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, “”);

常用的非成员函数:

make_tuple 创建一个 tuple 对象,其类型根据各实参类型定义 (函数模板)
tie 创建左值引用的 tuple,或将 tuple 解包为独立对象 (函数模板)
forward_as_tuple 创建转发引用tuple (函数模板)
tuple_cat 通过连接任意数量的元组来创建一个tuple (函数模板)
std::get(std::tuple) 元组式访问指定的元素 (函数模板)
#include <tuple>
#include <iostream>
#include <string>
#include <stdexcept>
 
std::tuple<double, char, std::string> get_student(int id)
{
    if (id == 0) return std::make_tuple(3.8, 'A', "Lisa Simpson");
    if (id == 1) return std::make_tuple(2.9, 'C', "Milhouse Van Houten");
    if (id == 2) return std::make_tuple(1.7, 'D', "Ralph Wiggum");
    throw std::invalid_argument("id");
}
 
int main()
{
    auto student0 = get_student(0);
    std::cout << "ID: 0, "
              << "GPA: " << std::get<0>(student0) << ", "
              << "grade: " << std::get<1>(student0) << ", "
              << "name: " << std::get<2>(student0) << '\n';
 
    double gpa1;
    char grade1;
    std::string name1;
    std::tie(gpa1, grade1, name1) = get_student(1);
    std::cout << "ID: 1, "
              << "GPA: " << gpa1 << ", "
              << "grade: " << grade1 << ", "
              << "name: " << name1 << '\n';
}

33 堆和栈的区别

堆是由低地址向高地址扩展,栈是由高地址向低扩展

堆总的内存是程序员分配和释放的,栈中的内存是有操作系统分配和释放的,里面存局部变量,参数等等。

堆中频繁的调用内存会产生内存碎片,降低程序效率。由于栈的先进现出特性不会产生碎片。

栈是操作系统提供的数据结构,计算机底层提供了一系列支持,分配专有的寄存器存储栈地址,压栈出栈独有指令;而堆是由c/c++库函数提供的,机制复杂,需要分配内存,合并内存和释放内存算法,效率低。

34 数组和指针区别?数组和链表呢?双向链表和单向链表?

数组和指针区别:
1、把数组作为参数传递的时候,会退化为指针
2、数组名可作为指针常量
3、数组是开辟一块连续的内存空间,数组本身的标示符代表整个数组,可以用sizeof取得真实的大小;指针则是只分配一个指针大小的内存,并可把它的值指向某个有效的内存空间

数组和链表区别:
不同:

  1. 链表是链式的存储结构;数组是顺序的存储结构。
  2. 链表通过指针来连接元素与元素,数组则是把所有元素按次序依次存储。
  3. 链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难;数组寻找某个元素较为简单,但插入与删除比较复杂,由于最大长度需要再编程一开始时指定,故当达到最大长度时,扩充长度不如链表方便。

相同:
两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构。

35 四种类型转换cast

  • static_cast:用于相关联类型指针之间的转换,还可以显示执行标准数据类型的类型转换,这种转换原本应该自动或隐士的进行。不执行运行阶段的检查。

  • dynamic_cast:在运行阶段执行类型转换,常用于将基类指针转化为子类指针。可检查dynamic_cast是否转化成功。

    • 转型失败会返回null(转型对象为指针时)或抛出异常 bad_cast(转型对象为引用时)。 dynamic_cast 会动用运行时信息(RTTI)**来进行类型安全检查 ,因此 dynamic_cast 存在一定的效率损失。当使用 dynamic_cast 时,该类型必须含有虚函数,这是因为dynamic_cast 使用了存储在 VTABLE 中的信息来判断实际的类型,**RTTI 运行时类型识别用于判断类型。
      destination_type* pDest = dynamic_cast<class_type*>(pSource);
      if(pDest)
          pDest -> Callfuc;         //检查dynamic_cast的操作结果,以判断转换是否成功。  
  • reinterpret_cast(重新解释):与C风格相近,不管相关与否强行重新解释类型。

  • const_cast: 让程序员关闭对象的访问修饰符const,其去除常量性的对象必须为指针或引用。

class Someclass
{
    public :
        void DisplayMember();  //在这里没有定义为CONST
}
viod DisplayDate(const Someclass& mDate)
{
    mDate.DisplayMembers();  //编译失败,以const引用传入mDate对象,不能调用非CONST函数。
    //这样写,也可以用与指针。
    Someclass& refDate = cosnt_cast<Someclass&>(mDate);
    refDate.DisplayMembers;
    //用指针的方式
    Someclass *refData = const_cast<Someclass*>(mData)
    refData -> DisplayMembers;
}
//都可以用c风格的类型转换代替
Derived* pDerivedSimple = (Derived*)pBase;

36.迭代器++it,和it++哪个好,为什么

前置返回一个引用,后置返回一个对象,后置产生临时对象的过程会导致效率降低。

int & operator++(){
    *this += 1;
    return *this;
}
int operator++(){
    int temp = *this;
    ++*this;
    return temp;
}

37 static关键字的用法

  • 将全局变量,函数修饰为静态全局变量,存储在静态存储区域,静态全局变量在声明它的文件之外是不可见的,只要声明他的文件可见,而普通的全局变量则是所有文件可见。
  • 将局部变量修饰为静态局部变量

当局部比那量离开作用域的时候,并没有销毁,而是依然存储在内存中,只不过目前暂时不能对他进行访问,直到函数再次调用,值不变,只初始化一次。

  • 将类的成员修饰为静态成员函数

静态成员函数属于类的,而不属于对象,需要注意的是在静态成员函数的实现中,不可以直接引用类中的非静态数据成员,但是可以引用静态成员,如果非要引用非静态成员,则可以通过对象来引用。

  • 类成员变量修饰为静态成员变量

静态成员变量是属于类的,而不是属于对象的,实现多个对象之间数据共享。

38 const?

  • 阻止一个变量发生变化,可以使用const关键字。在定义一个变量的时候一定要初始化,因为以后没有机会改变他了。

  • 将const变量用于指针,有三种情况。1指针指向的数据为常量,但是指针本身可以更改const int*,2指针本身不能修改,但是可以修改指向的数据,int * const,3两者都不能修改。

  • 在函数声明中,将一个参数设置为const,在函数内部不能改变其值。

  • 对于类成员函数,若是指定为cosnt,函数内部不能改变类的成员变量,类的对象只能访问类的成员函数。

  • 函数返回值为const,使得返回值不能是左值,返回后不能被修改。

  • 编译阶段,只有引用传递和指针传递可以用是否加const来区分开来。

39 C++模板是什么,底层怎么实现

模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。

编译器并不是把函数模板能处理成能够处理任意类的函数:编译器从函数模板通过具体类型产生不同的函数,实现函数重载的这样一个目的;编译器对函数模板进行两次编译,在声明的地方编译一次,在调用的地方对参数替换后的代码进行编译。

函数模板要被实例化之后才能称为真正的函数。在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。使用模板的目的就是能够让程序员编写与类型无关的代码。

39.1模板非类型形参的详细阐述

什么是非类型形参?顾名思义,就是表示一个固定类型的常量而不是一个类型。先举一个简单的例子(模板类与模板函数都可以用非类型形参)

//例子1:
template<class T, int MAXSIZE> 
class List {
  private:
    T elems[MAXSIZE];  
  public:
    Print() { 
      cout<<"The maxsize of list is"<<MAXSIZE; 
    }
}
List<int,5> list;
list.Print(); //打印"The maxsize of list is 5"
//c++11的特性
//c++11以前的版本对于函数模板而言,非类型参数不能设置默认值
//非类型参数必须是整形类数据(bool、char、int、long、long long)
template <typename T,int num=10>
int func(T x,T y)
{
    return x*y*num;
}

这个固定类型是有局限的,只有整形,指针和引用才能作为非类型形参,而且绑定到该形参的实参必须是常量表达式,即编译期就能确认结果。常量表达式基本上是字面值以及const修饰的变量

这里要强调一点,我们对于非类型形参的限定要分两个方面看

1.对模板形参的限定,即template<>里面的参数

2.对模板实参的限定,即实例化时<>里面的参数

下面逐个解释一下非类型形参的局限

1.浮点数不可以作为非类型形参,包括float,double。具体原因可能是历史因素,也许未来C++会支持浮点数。

2.类不可以作为非类型形参。

3.字符串不可以作为非类型形参

4.整形,可转化为整形的类型都可以作为形参,比如int,char,long,unsigned,bool,short(enum声明的内部数据可以作为实参传递给int,但是一般不能当形参)

5.指向对象或函数的指针与引用(左值引用)可以作为形参

下面解释一下非类型实参的局限

1.实参必须是编译时常量表达式,不能使用非const的局部变量,局部对象地址及动态对象

2.非Const的全局指针,全局对象,全局变量(下面可能有个特例)都不是常量表达式。

3.由于形参的已经做了限定,字符串,浮点型即使是常量表达式也不可以作为非类型实参

备注:常量表达式基本上是字面值以及const修饰的变量

39.2模板的实例化

模板本身不会生成函数或类定义,它只是一个用于生成函数或类的方案,编译器使用模板为特定类型生成函数或类定义的过程叫做模板的实例化。

1.隐式实例化
式实例化就是这种情况:

template<typename Any>  
void swap(Any &a, Any &b){  
    Any temp;  
    temp = a;  
    a = b;  
    b = temp;  
}  
int main(){  
    ....  
    swap<int>(a,b);  
    ....  
}  

函数模板隐式实例化指的是在发生函数调用的时候,如果没有发现相匹配的函数存在,编译器就会寻找同名函数模板,如果可以成功进行参数类型推演,就对函数模板进行实例化。很显然的影响效率这里顺便提一下swap(a,b);中的是可选的,因为编译器可以根据函数参数类型自动进行判断,也就是说如果编译器不不能自动判断的时候这个就是必要的;

2.显式实例化

前面已经提到隐式实例化可能影响效率,所以需要提高效率的显式实例化,显示实例化也称为外部实例化。在不发生函数调用的时候将函数模板实例化,或者在不适用类模板的时候将类模板实例化称之为模板显示实例化。

template void swap<int>(int &a,int &b);  

39.3 特化偏特化

有时候需要在为特殊类型实例化时,对模板进行修改使其行为不同。

具体化模板定义的格式如下:
template <> class 类名<具体化类型名> {...};

#include <iostream>
using namespace std;
template<typename T1,typename T2>
class Test{
public:
    Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}
private:
    T1 a;
    T2 b;
};
template<>   //全特化,由于是全特化,参数都指定了,参数列表故为空。
class Test<int ,char>{
public:
    Test(int i,char j):a(i),b(j){cout<<"全特化"<<endl;}
private:
    int a;
    int b;
};
template<typename T2> //由于只指定了一部分参数,剩下的未指定的需在参数列表中,否则报错。
class Test<char,T2>{
public:
    Test(char i,T2 j):a(j),b(j){cout<<"个数偏特化"<<endl;}
private:
    char a;
    T2 b;
};

39.4模板类和模板函数的区别是什么?

函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必
须由程序员在程序中显式地指定。即函数模板允许隐式调用和显式调用而类模板只能
显示调用。在使用时类模板必须加< T >,而函数模板不必

39.5为什么模板类一般都是放在一个 h 文件中

1)模板定义很特殊。由 template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

  1. 在分离式编译的环境下**,编译器编译某一个.cpp 文件时并不知道另一个.cpp 文件的**

存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来,所以**,当编译器只看到模板的声明时,它不能实例化该模板**,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp 文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj 中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。

vector底层实现

底层数据结构是一个动态数组。默认构造的大小是0, 之后插入按照1 2 4 8 16 二倍扩容。注(GCC是二倍扩容,VS13是1.5倍扩容。原因可以考虑内存碎片和伙伴系统,内存的浪费)。扩容后是一片新的内存,需要把旧内存空间中的所有元素都拷贝进新内存空间中去,之后再在新内存空间中的原数据的后面继续进行插入构造新元素,并且同时释放旧内存空间,并且,由于vector 空间的重新配置,导致旧vector的所有迭代器都失效了。

vector的初始的扩容方式代价太大,初始扩容效率低, 需要频繁增长,不仅操作效率比较低,而且频繁的向操作系统申请内存容易造成过多的内存碎片,所以这个时候需要合理使用resize()和reserve()方法提高效率减少内存碎片的。

resize():
void resize (size_type n);
void resize (size_type n, value_type val);
1、resize方法被用来改变vector中元素的数量,我们可以说,resize方法改变了容器的大小,且创建了容器中的对象;
2、如果resize中所指定的n小于vector中当前的元素数量,则会删除vector中多于n的元素,使vector得大小变为n;
3、如果所指定的n大于vector中当前的元素数量,则会在vector当前的尾部插入适量的元素,使得vector的大小变为n,在这里,如果为resize方法指定了第二个参数,则会把后插入的元素值初始化为该指定值,如果没有为resize指定第二个参数,则用默认值填充新位置,一般为0;
4、如果resize所指定的n不仅大于vector中当前的元素数量,还大于vector当前的capacity容量值时,则会自动为vector重新分配存储空间;

reserve():避免了频繁的申请内存空间,造成过多内存碎片
void reserve (size_type n);
1、reserve的作用是更改vector的容量,使vector至少可以容纳n个元素。
2、如果n大于vector当前的容量,reserve会对vector进行扩容。其他情况下都不会重新分配vector的存储空间
3、reserve方法对于vector元素大小没有任何影响,不创建对象。

vector中数据的随机存取效率很高,O(1)的时间的复杂度,但是在vector 中随机插入元素,需要移动的元素数量较多,效率比较低

40 请你来说一下什么时候会发生段错误

段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:

使用野指针

试图修改字符串常量的内容

41 C++STL的内存优化

1)二级配置器结构
STL内存管理使用二级内存配置器。
1、第一级配置器
第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。
一级空间配置器分配的是大于128字节的空间
如果分配不成功,调用句柄释放一部分内存
如果还不能分配成功,抛出异常
2、第二级配置器
在STL的第二级配置器中多了一些机制,避免太多小区块造成的内存碎片,小额区块带来的不仅是内存碎片,配置时还有额外的负担。区块越小,额外负担所占比例就越大。
3、分配原则
如果要分配的区块大于128bytes,则移交给第一级配置器处理。
如果要分配的区块小于128bytes,则以内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。
当用户申请的空间小于128字节时,将字节数扩展到8的倍数,然后在自由链表中查找对应大小的子链表
如果在自由链表查找不到或者块数不够,则向内存池进行申请,一般一次申请20块如果内存池空间足够,则取出内存
如果不够分配20块,则分配最多的块数给自由链表,并且更新每次申请的块数
如果一块都无法提供,则把剩余的内存挂到自由链表,然后向系统heap申请空间,如果申请失败,则看看自由链表还有没有可用的块,如果也没有,则最后调用一级空间配置器
2)二级内存池
二级内存池采用了16个空闲链表,这里的16个空闲链表分别管理大小为8、16、24……120、128的数据块。这里空闲链表节点的设计十分巧妙,这里用了一个联合体既可以表示下一个空闲数据块(存在于空闲链表中)的地址,也可以表示已经被用户使用的数据块(不存在空闲链表中)的地址。

1、空间配置函数allocate
首先先要检查申请空间的大小,如果大于128字节就调用第一级配置器,小于128字节就检查对应的空闲链表,如果该空闲链表中有可用数据块,则直接拿来用(拿取空闲链表中的第一个可用数据块,然后把该空闲链表的地址设置为该数据块指向的下一个地址),如果没有可用数据块,则调用refill重新填充空间。
2、空间释放函数deallocate
首先先要检查释放数据块的大小,如果大于128字节就调用第一级配置器,小于128字节则根据数据块的大小来判断回收后的空间会被插入到哪个空闲链表。
3、重新填充空闲链表refill
在用allocate配置空间时,如果空闲链表中没有可用数据块,就会调用refill来重新填充空间,新的空间取自内存池。缺省取20个数据块,如果内存池空间不足,那么能取多少个节点就取多少个。
从内存池取空间给空闲链表用是chunk_alloc的工作,首先根据end_free-start_free来判断内存池中的剩余空间是否足以调出nobjs个大小为size的数据块出去,如果内存连一个数据块的空间都无法供应,需要用malloc去堆中申请内存。
假如山穷水尽,整个系统的堆空间都不够用了,malloc失败,那么chunk_alloc会从空闲链表中找是否有大的数据块,然后将该数据块的空间分给内存池(这个数据块会从链表中去除)。
3、总结:

  1. 使用allocate向内存池请求size大小的内存空间,如果需要请求的内存大小大于128bytes,直接使用malloc。
  2. 如果需要的内存大小小于128bytes,allocate根据size找到最适合的自由链表。
    a. 如果链表不为空,返回第一个node,链表头改为第二个node。
    b. 如果链表为空,使用blockAlloc请求分配node。
    x. 如果内存池中有大于一个node的空间,分配竟可能多的node(但是最多20个),将一个node返回,其他的node添加到链表中。
    y. 如果内存池只有一个node的空间,直接返回给用户。
    z. 若果如果连一个node都没有,再次向操作系统请求分配内存。
    ①分配成功,再次进行b过程。
    ②分配失败,循环各个自由链表,寻找空间。
    I. 找到空间,再次进行过程b。
    II. 找不到空间,抛出异常。
  3. 用户调用deallocate释放内存空间,如果要求释放的内存空间大于128bytes,直接调用free。
  4. 否则按照其大小找到合适的自由链表,并将其插入。

42、Python 与C++的区别

PYTHON是一种脚本语言,是解释执行的,不需要经过编译,所以很方便快捷,且能够很好地跨平台,写一些小工具小程序特别合适。
而C++则是一种需要编译后运行语言,在特定的机器上编译后在特定的机上运行,运行效率高,安全稳定。但编译后的程序一般是不跨平台的。

python内存管理是由私有堆空间管理的,所有的python对象和数据结构都存储在私有堆空间中。程序员没有访问堆的权限,只有解释器才能操作。为python的堆空间分配内存的是python的内存管理模块进行的,核心api会提供一些访问该模块的方法供程序员使用。python自有的垃圾回收机制回收并释放没有被使用的内存供别的程序使用。

43、python内存管理

关于python的存储问题

(1)由于python中万物皆对象,所以python的存储问题是对象的存储问题,并且对于每个对象,python会分配一块内存空间去存储它

(2)对于整数和短小的字符等,python会执行缓存机制,即将这些对象进行缓存,不会为相同的对象分配多个内存空间

(3)容器对象,如列表、元组、字典等,存储的其他对象,仅仅是其他对象的引用,即地址,并不是这些对象本身

关于引用计数器

(1)一个对象会记录着引用自己的对象的个数,每增加一个引用,个数加一,每减少一个引用,个数减一

(2)查看引用对象个数的方法:导入sys模块,使用模块中的getrefcount(对象)方法,由于这里也是一个引用,故输出的结果多1

(3)增加引用个数的情况:1.对象被创建p = Person(),增加1;2.对象被引用p1 = p,增加1;3.对象被当作参数传入函数func(object),增加2,原因是函数中有两个属性在引用该对象;4.对象存储到容器对象中l = [p],增加1

(4)减少引用个数的情况:1.对象的别名被销毁del p,减少1;2.对象的别名被赋予其他对象,减少1;3.对象离开自己的作用域,如getrefcount(对象)方法,每次用完后,其对对象的那个引用就会被销毁,减少1;4.对象从容器对象中删除,或者容器对象被销毁,减少1

(5)引用计数器用法:

import sys
class Person(object):
pass
p = Person()
p1 = p
print(sys.getrefcount(p))
p2 = p1
print(sys.getrefcount(p))
p3 = p2
print(sys.getrefcount(p))
del p1
print(sys.getrefcount(p))
多一个引用,结果加1,销毁一个引用,结果减少1

(6)引用计数器机制:利用引用计数器方法,在检测到对象引用个数为0时,对普通的对象进行释放内存的机制

关于循环引用问题

(1)循环引用即对象之间进行相互引用,出现循环引用后,利用上述引用计数机制无法对循环引用中的对象进行释放空间,这就是循环引用问题

(2)循环引用形式:

class Person(object):
pass
class Dog(object):
pass
p = Person()
d = Dog()
p.pet = d
d.master = p
即对象p中的属性引用d,而对象d中属性同时来引用p,从而造成仅仅删除p和d对象,也无法释放其内存空间,因为他们依然在被引用。深入解释就是,循环引用后,p和d被引用个数为2,删除p和d对象后,两者被引用个数变为1,并不是0,而python只有在检查到一个对象的被引用个数为0时,才会自动释放其内存,所以这里无法释放p和d的内存空间。

44、python垃圾回收

从基本原理上,当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。

垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python只会在特定条件下,自动启动垃圾回收。当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。

分代回收
Python同时采用了分代(generation)回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,我们相信它们的用处,所以减少在垃圾回收中扫描它们的频率。

Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。

孤立的引用环

引用环的存在会给上面的垃圾回收机制带来很大的困难。这些引用环可能构成无法使用,但引用计数不为0的一些对象。为了回收这样的引用环,Python复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i。对于每个对象i引用的对象j,将相应的gc_ref_j减1。

在结束遍历后,gc_ref不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。

45、说一下C++的内存管理是怎么样的?

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。

代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

数据段:存储程序中已初始化的全局变量和静态变量

bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。

堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。

映射区:存储动态链接库以及调用mmap函数进行的文件映射

栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值

46 extern 用法?

1) extern 修饰变量的声明
如果文件 a.c 需要引用 b.c 中变量 int v,就可以在 a.c 中声明 extern int v,然后
就可以引用变量 v。
2) extern 修饰函数的声明
如果文件 a.c 需要引用 b.c 中的函数,比如在 b.c 中原型是 int fun(int mu),那么就可以在 a.c 中声明 extern int fun(int mu),然后就能使用 fun 来做任何事情。就像变量的声明一样,extern int fun(int mu)可以放在 a.c 中任何地方,而不一定非要放在 a.c 的文件作用域的范围中。
3) extern 修饰符可用于指示 C 或者 C++函数的调用规范。比如在 C++中调用 C 库函数,就需要在 C++程序中用 extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用 C 函数规范来链接。主要原
因是 C++和 C 程序编译完成后在目标代码中命名规则不同。

47、类的初始化方式?构造函数执行顺序?为什么用成员初始化列表会快一些?

  1. 赋值初始化,通过在函数体内进行赋值初始化;列表初始化,**在冒号后使用初始化列表进行初始化。
    这两种方式的主要区别在于:
    对于
    在函数体中初始化**,是在所有的数据成员被分配内存空间后才进行的。列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。
  2. 一个派生类构造函数的执行顺序如下:
    1虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
    2基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
    3类类型的成员对象的构造函数(按照初始化顺序)
    4派生类自己的构造函数。
  3. 方法一是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效

率。

48、c++ cout 保留小数点位

需要头文件

输出时需要用 fixed 和 setprecision()

fixed代表输出浮点数,setprecision()设置精度。

`#include <iostream>``#include <iomanip>``#include <cstdio>` `using` `namespace` `std;` `int` `main(``int` `argc, ``char` `const` `*argv[]) {``    ``printf``(``"%.2lf\n"``, 12.345);``    ``cout <<``/* fixed << */``setprecision(2) << 12.345 << endl;``    ``cout << fixed << setprecision(2) << 12.345 << endl;``    ``return` `0;`

49、了解C++默默编写并调用哪些函数

  1. 成员函数只有被需要(被调用)才必须有定义,同理,只有当默认构造函数,拷贝构造函数,赋值操作符,析构函数被需要而类定义它们时,它们才会被编译器创建出来(除非函数在基类中被声明为虚函数,编译器产生的函数是非虚的,public的).
  2. 并不是只要类没有定义默认构造函数,拷贝构造函数,赋值操作符时编译器就会自动合成它们,它们只有在”被需要”的时候才被产生.
  3. 虽然编译器在类的创建者没有声明但是需要的情况下产生赋值操作符,但是有些时候编译器无法产生它,包括以下两种情况:

​ 1). 数据成员是const对象或引用.

​ 2). 某个基类将赋值操作符声明为private.

49、C++编译器合成默认构造函数和复制控制成员(拷贝构造函数,赋值操作符,析构函数)的条件

”C++新手一般有两个常见的误解:

任何class如果没有定义default constructor,就会被合成一个出来.

编译器合成出来的default constructor会明确设定class 内每一个data member的默认值.”

现在主要解释第一条为什么是错误的,根据《深入理解C++对象模型》,”default constructor 在需要的时候被编译器产生出来”,以下就是4种”需要的时候”:

1). 该类含有一个成员对象而后者有一个默认构造函数.

如果一个类没有任何构造函数,但它含有一个成员函数,这个成员函数含有默认构造函数,那么编译器就需要为这个类合成一个默认构造函数并调用那个成员的默认构造函数.

2). 该类继承自一个基类且后者带有默认构造函数.

原理和1)类似

3). 该类带有一个虚函数

编译器需要合成一个默认构造函数并在编译期发生两种扩张操作:**”一个virtual function table(在cfront中被称为vtbl)会被编译器产生出来**,内放class的virtual function地址”,”在每一个class object中,一个额外的pointer member(也就是vptr)会被编译器合成出来,内含相关的class vtbl地址”.

4). 该类派生自一个继承串链,其中有一个或多个虚基类

在派生类继承基类时,加上一个virtual关键词则为虚拟基copy类继承,虚基类主要解决在多重继承时,基类可能被多次继承,虚基类主要提供一个基类给派生类

不同编译器对虚基类的实现不同,但编译器需要合成一个默认构造函数并改变对虚基类”执行存取操作”的那些码,使得对虚基类的操作延迟至执行期才决定下来.

同理,如果类没有定义**拷构造函数,**那么编译器会视该类有没有展现”bitwise copy semantics“(位逐次拷贝语义来决定是否合成拷贝构造函数),在以下四种情况下类不展现出”bitwise copy semantics“:

1). 该类含有一个成员对象而后者有一个拷贝构造函数.

2). 该类继承自一个基类且后者带有拷贝构造函数.

3). 该类带有一个虚函数

4). 该类派生自一个继承串链,其中有一个或多个虚基类

可见,编译器在类没有定义拷贝构造函数时合成拷贝构造函数的要求与默认构造函数类似,唯一不同的是如果编译器不合成默认构造函数,那么将不会对成员进行任何初始化操作,如果编译器不合成拷贝构造函数,那么将会进行”bitwise copy”(位逐次拷贝,按位逐位拷贝),如果成员有指针,那么位逐次拷贝进行的是浅复制.

50 、讲讲移动构造函数

1 为什么要有移动构造函数呢?我们想这个问题,我们用a初始化对象b,之后对象a我们就不用了,但是a的空间还在啊,既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么我们为什么不直接用a的空间呢?这样就避免了新的空间的分配,大大降低了构造成本。这就是设置移动构造函数的初衷。

2 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如 a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为 NULL 的语句,所以析构 a的时候并不会回收 a->value 指向的空间;

3 移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个 move 语句,就是将一个左值变成一个将亡值。

51、vector 与list的区别与应用?

vector 和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为 o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,**时间复杂度为 o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。它与数组最大的区别就是 vector **不需程序员自己去考虑容量问题,库里面本身已经实现了容量的动态增长,而数组需要程序员手动写入扩容函数进形扩容。

ist 是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以 list 的随机存取非常没有效率,时间复杂度为 o(n);但由于链表的特点,能高效地进行插入和删除。非连续存储结构:list 是一个双链表结构,支持对链表的双向遍历。每个节点包括三个信息:元素本身,指向前一个元素的节点( prev)和指向下一个元素的节点(next)。因此 list 可以高效率的对数据元素任意位置进行访问和插入删除等操作。由于涉及对额外指针的维护,所以开销比较大。

52、vector的扩容机制?

我们知道,vector 在需要的时候会扩容,在 VS 下是 1.5倍,在 GCC 下是 2 倍。那么会产生两个问题:

(1)为什么是成倍增长,而不是每次增长一个固定大小的容量呢?

(2)为什么是以 2 倍或者 1.5 倍增长,而不是以 3 倍或者 4 倍等增长呢?

(1).如果已成倍方式增长。假定有 n 个元素,倍增因子为 m; 完成这 n 个元素往一个 vector 中的 push_back操作,需要重新分配内存的次数大约为 logm(n); 第 i 次重新分配将会导致复制 m^(i) (也就是当前的vector.size() 大小)个旧空间中元素; *n 次 push_back 操作所花费的时间复制度为O(n)**,$\sum_{i =1}^{\log_mn} m^i= nm/(m-1)$

m / (m - 1),这是一个常量,均摊分析的方法可知,vector 中 push_back 操作的时间复杂度为常量时间.

如果一次增加固定值大小 。假定有 n 个元素,每次增加k个;第i次增加复制的数量为为:ki ;$\sum_{i=1}^{n/k}ki$

n次 push_back 操作所花费的时间复杂度为O(n^2):

均摊下来每次push_back 操作的时间复杂度为O(n);

(2)使用 k=2 增长因子的问题在于,**每次扩展的新尺寸必然刚好大于之前分配的总和,**也就是说,之前分配的内存空间不可能被使用。这样对内存不友好。最好把增长因子设为(1,2).

53、请你来说一下map和set有什么区别,分别又是怎么实现的?

map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。

map和set区别在于:

(1)map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。

(2)set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。

(3)map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。

54、请你来介绍一下STL的allocator

STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:

new运算分两个阶段:(1)调用::operator new配置内存;(2)调用对象构造函数构造对象内容

delete运算分两个阶段:(1)调用对象希构函数;(2)掉员工::operator delete释放内存

为了精密分工,STL allocator将两个阶段操作区分开来:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。

同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

55、volatile 关键字的作用?

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候**,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。**
volatile 用在如下的几个地方:

  1. 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
  2. 多任务环境下各任务间共享的标志应该加 volatile;
  3. 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;

56、讲讲大端小端,如何检测

大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址端。
小端模式,是指数据的高字节保存在内存的高地址中,低位字节保存在在内存的低地址端。

一个32位int数的十六进制值为0x01234567(最高有效位——最低有效位),位于地址0x100~0x103(每个地址单元一般容量为1字节)

//将int 48存起来,然后取得其地址,再将这个地址转为char* 这时候,如果是小端存储,那么char*指针就指向48; 48对应的ASCII码为字符‘0’;
void judge_bigend_littleend1()
{
    int i = 48;
    int* p = &i;
    char c = 0;
    c = *((char*)p);

    if (c == '0')
        printf("小端\n");
    else
        printf("大端\n");
}

//定义变量int i=1;将 i 的地址拿到,强转成char*型,这时候就取到了 i 的低地址,这时候如果是1就是小端存储,如果是0就是大端存储。
void judge_bigend_littleend2()
{
    int i = 1;
    char c = (*(char*)&i);

    if (c)
        printf("小端\n");
    else
        printf("大端\n");
}
//定义联合体,一个成员是多字节,一个是单字节,给多字节的成员赋一个最低一个字节不为0,其他字节为0 的值,再用第二个成员来判断,如果第二个字节不为0,就是小端,若为0,就是大端。
void judge_bigend_littleend3()
{
    union
    {
        int i;
        char c;
    }un;
    un.i = 1;

    if (un.c == 1)
        printf("小端\n");
    else
        printf("大端\n");
}

57、讲一下友元?

私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行。这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦。

C++ 是从结构化的C语言发展而来的,需要照顾结构化设计程序员的习惯,所以在对私有成员可访问范围的问题上不可限制太死。

C++ 设计者认为, 如果有的程序员真的非常怕麻烦,就是想在类的成员函数外部直接访问对象的私有成员,那还是做一点妥协以满足他们的愿望为好,这也算是眼前利益和长远利益的折中。因此,C++ 就有了友元(friend)的概念。打个比方,这相当于是说:朋友是值得信任的,所以可以对他们公开一些自己的隐私。

友元提供了一种 普通函数或者类成员函数 访问另一个类中的私有或保护成员 的机制。也就是说有两种形式的友元:

(1)友元函数:普通函数对一个访问某个类中的私有或保护成员。 friend 函数原型;

(2)友元类:友元类可以访问声明为朋友的类的私有成员和受保护成员。友元类的重要用途是用于表示由类表示的数据结构的一部分,以提供对表示该数据结构的主类的访问。

类A作为类B的友元时,类A称为友元类。A中的所有成员函数都是B的友元函数,都可以访问B中的所有成员。

A可以在B的public部分或private部分进行声明,方法如下:

friend [class]<类名>; //友元类类名

友元函数的宣告可以放在类声明的任何地方,不受访问限定关键字private、protected、public的限制。

友谊关键字应该谨慎使用。如果一个拥有private或者protected成员的类,宣告过多的友元函数,可能会降低封装性的价值,也可能对整个设计框架产生影响。友元的存在,使得类的接口扩展更为灵活,使用友元进行运算符重载从概念上也更容易理解一些,而且,C++规则已经极力地将友元的使用限制在了一定范围内,它是单向的、不具备传递性、不能被继承,所以,应尽力合理使用友元。

要想使得一组重载函数全部成为类的友元,必须一一声明,否则只有匹配的那个函数会成为类的友元,编译器仍将其他的当成普通函数来处理。


class Exp
{
public:
firend void test(int);  
};
void test();
void test(int);
void test(double);
//上述代码中,只有“void test(int)”函数是Exp类的友元函数,“void test()”和“void test(double)”函数都只是普通函数

58、说一下STL中迭代器的作用,有指针为什么还要有迭代器?

迭代器是一种抽象的设计理念**,通过迭代器可以在不了解容器内部原理的情况下遍历容器,**除此之外,STL 中迭代器一个最重要的作用就是作为容器与 STL 算法的粘合剂。

迭代器的作用就是提供一个遍历容器内部所有元素的接口,因此迭代器内部必须保存一个与容器相关联的指针,然后重载各种运算操作来遍历,其中最重要的是*运算符与->运算符,以及++、–等可能需要重载的运算符重载。

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,

->、*、++、–等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。

迭代器返回的是迭代器引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身。

59、容器内部删除一个元素迭代器的变化?

  1. 顺序容器
    erase 迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list 除外),所以不能使用 erase(it++)的方式,但是 erase 的返回值是下一个有效迭代器;It = c.erase(it);
  2. 关联容器
    erase 迭代器只是被删除元素的迭代器失效 ,但是返回值是 void,所以要采用erase(it++)的方式删除迭代器;
    c.erase(it++)

60、如何在共享内存上使用 stl 标准库?

假设进程 A 在共享内存中放入了数个容器,进程 B 如何找到这些容器呢?一个方法就是进程 A 把容器放在共享内存中的确定地址上(fixed offsets),则进程 B可以从该已知地址上获取容器。另外一个改进点的办法是,**进程 A 先在共享内存某块确定地址上放置一个 map 容器,**然后进程 A 再创建其他容器,然后给其取个名字和地址一并保存到这个 map 容器里。进程 B 知道如何获取该保存了地址映射的map 容器,然后同样再根据名字取得其他容器的地址。

61、请你来说一下共享内存相关api

Linux允许不同进程访问同一个逻辑内存,提供了一组API,头文件在sys/shm.h中。

1)新建共享内存shmget

int shmget(key_t key,size_t size,int shmflg);

key:共享内存键值,可以理解为共享内存的唯一性标记。

size:共享内存大小

shmflag:创建进程和其他进程的读写权限标识。

返回值:相应的共享内存标识符,失败返回-1

2)连接共享内存到当前进程的地址空间shmat

void *shmat(int shm_id,const void *shm_addr,int shmflg);

shm_id:共享内存标识符

shm_addr:指定共享内存连接到当前进程的地址,通常为0,表示由系统来选择。

shmflg:标志位

返回值:指向共享内存第一个字节的指针,失败返回-1

3)当前进程分离共享内存shmdt

int shmdt(const void *shmaddr);

4)控制共享内存shmctl

和信号量的semctl函数类似,控制共享内存

int shmctl(int shm_id,int command,struct shmid_ds *buf);

shm_id:共享内存标识符

command: 有三个值

IPC_STAT:获取共享内存的状态,把共享内存的shmid_ds结构复制到buf中。

IPC_SET:设置共享内存的状态,把buf复制到共享内存的shmid_ds结构。

IPC_RMID:删除共享内存

buf:共享内存管理结构体。

62、sizeof(string)

sizeof关键字给出了与变量或类型(包括聚合类型)相关联的存储数量(以字节为单位)。这个关键字返回一个大小为t的值。

1、sizeof()返回的是string对象所占用的空间,而不是string所存储的字符串的大小。
2、string的实现在各库中可能有所不同,但是在同一库中相同的一点是,无论string里放多长的字符串,它的sizeof()都是固定的,字符串所占的空间是从堆中动态分配的,与sizeof()无关。

63、函数调用过程栈的变化,返回值和参数变量哪个先入栈?

在x86处理器中,EIP(Instruction Pointer)是指令寄存器,指向处理器下条等待执行的指令地址(代码段内的偏移量),每次执行完相应汇编指令EIP值就会增加。ESP(Stack Pointer)是堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址(也是系统栈的顶部),且始终指向栈顶;EBP(Base Pointer)是栈帧基址指针寄存器,存放执行函数对应栈帧的栈底地址,用于C运行库访问栈中的局部变量和参数。

1 、调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中 , 即 : 从右向左依次把被调函数所需要的参数压入栈 ;
2 、调用者函数使用 call 指令调用被调函数 , 并把 call 指令的下一条指令的地址当成返回地址压入栈中 ( 这个压栈操作隐含在 call 指令中 );
3 、在被调函数中 , 被调函数会先保存调用者函数的栈底地址EBP (push ebp), 并将调用者调函数的栈顶指针ESP值赋给被调函数的EBP(作为被调函数的栈底) (mov esp,ebp);
4 、接着改变ESP值来为函数局部变量预留空间。在被调函数中 , 从 ebp 的位置处开始存放被调函数中的局部变量和临时变量 , 并且这些变量的地址按照定义时的顺序依次减小 , 即 : 这些变量的地址是按照栈的延伸方向排列的 , 先定义的变量先入栈 , 后定义的变量后入栈 ;

调用结束后:将EBP指针值赋给ESP,使ESP再次指向被调函数栈底以释放局部变量;再将已压栈的主调函数帧基指针弹出到EBP,并弹出返回地址到EIP。ESP继续上移越过参数,最终回到函数调用前的状态,即恢复原来主调函数的栈帧。

访问函数的局部变量和访问函数参数的区别:**局部变量总是通过将ebp减去偏移量来访问,函数参数总是通过将ebp加上偏移量**来访问。对于32位变量而言,第一个局部变量位于ebp-4,第二个位于ebp-8,以此类推,32位局部变量在栈中形成一个逆序数组;第一个函数参数位于ebp+8,第二个位于ebp+12,以此类推,32位函数参数在栈中形成一个正序数组。

64、模板和实现可不可以不写在一个文件里面?为什么?

因为在编译时模板并不能生成真正的二进制代码,而是在编译调用模板类或函数的 CPP 文件时才会去找对应的模板声明和实现,**在这种情况下编译器是不知道实现模板类或函数的 CPP 文件的存在**,所以它只能找到模板类或函数的声明而找不到实现,而只好创建一个符号寄希望于链接程序找地址。但模板类或函数的实现并不能被编译成二进制代码,结果链接程序找不到地址只好报错了。

65、隐式转换,如何消除隐式转换?

C++的基本类型中并非完全的对立,部分数据类型之间是可以进行隐式转换的。所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换

基本数据类型 **基本数据类型的转换以取值范围的作为转换基础(保证精度不丢失)**。隐式转换发生在从小->大的转换中。比如从 char 转换为 int。从 int-long。自定义对象 子类对象可以隐式的转换为父类对象。

C++面向对象的多态特性,就是通过父类的类型实现对子类的封装。通过隐式转换,你可以直接将一个子类的对象使用父类的类型进行返回。

C++中提供了 explicit 关键字,在构造函数声明的时候加上 explicit 关键字,能够禁止隐式转换。

如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。
可以通过将构造函数声明为 explicit 加以制止隐式类型转换,关键字 e**xplicit 只对一个实参的构造函数有效,**需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为 explicit。

66、cout 和 printf 有什么区别?

cout<<是一个函数,cout<<后可以跟不同的类型是因为 cout<<已存在针对各种
类型数据的重载,所以会自动识别数据的类型。输出过程会首先将输出字符放入缓冲
区,然后输出到屏幕。
cout 是有缓冲输出:
cout < < “abc “ < <endl;
或 cout < < “abc\n “;cout < <flush; 这两个才是一样的.
endl 相当于输出回车后,再强迫缓冲输出。
flush 立即强迫缓冲输出。
printf 是无缓冲输出。有输出时立即输出

67、C++中类成员的访问权限和继承权限问题

三种访问权限
1 public:用该关键字修饰的成员表示公有成员,该成员不仅可以在类内可以被访问,在类外也是可以被访问的,是类对外提供的可访问接口;
2 private:用该关键字修饰的成员表示私有成员,该成员仅在类内可以被访问,在类体外是隐藏状态;
3 protected:用该关键字修饰的成员表示保护成员,保护成员在类体外同样是隐藏状态,但是对于该类的派生类来说,相当于公有成员,在派生类中可以被访问。

三种继承方式

1若继承方式是 public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;

  • 派生类中的成员函数:可以直接访问基类的公有和保护成员,但不能访问基类的私有成员。
  • 通过派生类的对象(即用“的”的方式):只能访问基类的公有成员。

2若继承方式是 private,基类所有成员在派生类中的访问权限都会变为私有(private)权限;都变成私有了,也就是说,继承到此为止了,该派生类曾经继承的基类,在该派生类下级的派生
类中完全被屏蔽了。因为是私有的,再往下继承,都变得对于下一代的派生类不可见的。
3 若继承方式是 protected,基类的共有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限,私有成员在派生类中的访问权限仍然是私有(private)权限。

终极原则:私有成员永远只能被本类的成员函数访问,或者是友元访问,除此以外,即使是其派生出的子类,也不能直接访问这些私有成员。

68、动态联编与静态联编

在 C++中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可以分为静态联编和动态联编;

静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,**又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,**确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差。

动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,**实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。C++中一般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使用动态联编。动态联编的优点是灵活性强,但效率低。动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式为:**指向基类的指针变量名->虚函数名(实参表)或基类对象的引用名.虚函数名(实参表)

69、动态链接和静态链接区别

1 静态连接库就是把(lib)文件中用到的函数代码直接链接进目标程序 ,程序运行的时
候不再需要其它的库文件;动态链接就是把调用的函数所**在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,**程序运行的时候再从 DLL 中寻找相应函数代码,因此需要相应 DLL 文件的支持。

2 静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的 EXE 文件中了。但是若使用
DLL,该 DLL 不必被包含在 最终 EXE 文件中**,EXE 文件执行时可以 “ 动态 ” 地引用和卸载这个与 EXE 独立的 DLL 文件。**静态链接库和动态链接库的另外一个区别
在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。

3动态库就是在需要调用其中的函数时,根据函数映射表找到该函数然后调入堆栈执行。如果在当前工程中有多处对 dll 文件中同一个函数的调用,那么执行时,这个函
数只会留下一份拷贝。但是如果有多处对 lib 文件中同一个函数的调用,那么执行时,该函数将在当前程序的执行空间里留下多份拷贝,而且是一处调用就产生一份拷贝。

70、为什么拷贝构造函数必须传引用不能传值?

拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。

i)值传递:
对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);

ii)引用传递:
无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型).

拷贝构造函数用来初始化一个非引用类类型对象**,如果用传值的方式进行传参数,那么构造实参需要调用拷贝构造函数,而拷贝构造函数需要传递实参,**所以会一直递归。

71、深拷贝与浅拷贝?

浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。深复制 —-在计算机中开辟了一块新的内存地址用于存放复制的对象。

72、虚函数可以声明为 inline 吗?

虚函数用于实现运行时的多态,或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。

虚函数要求在运行时进行类型确定,而内敛函数要求在编译期完成相关的函数替换;

73、类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?

1 赋值初始化,**通过在函数体内进行赋值初始化;列表初始化,在冒号后使用初始化列表进行初始化。**
这两种方式的主要区别在于:
对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。列表初始化是给数据成员分配内存空间时就进行初始化,**就是说分配一个数据成员
只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),**那么分配了内存空间后在进入函数体之前给数据成员赋值
,就是说初始化这个数据成员此时函数体还未执行。

一个派生类构造函数的执行顺序如下:
1 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
2 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
3 类类型的成员对象的构造函数(按照初始化顺序)
4 派生类自己的构造函数。

74、成员列表初始化?好在哪里?

但是在大多数情况下,两者实际上没有区别。有两个原因使得我们选择第二种语法,它被称为成员初始化列表:一个原因是必须的,另一个只是出于效率考虑。

让我们先看一下第一个原因——必要性。设想你有一个类成员,它本身是一个类或者结构,而且只有一个带一个参数的构造函数。


class CMember {
public:
    CMember(int x) { ... }
};

因为Cmember有一个显式声明的构造函数,编译器不产生一个缺省构造函数(不带参数),所以没有一个整数就无法创建Cmember的一个实例。

CMember* pm = new CMember; // Error!!
CMember* pm = new CMember(2); // OK

如果Cmember是另一个类的成员,你怎样初始化它呢?你必须使用成员初始化列表。


class CMyClass {
    CMember m_member;
public:
    CMyClass();
};
//必须使用成员初始化列表
CMyClass::CMyClass() : m_member(2)
{
•••
}

没有其它办法将参数传递给m_member,如果成员是一个常量对象或者引用也是一样。根据C++的规则,常量对象和引用不能被赋值,它们只能被初始化。

必须使用成员初始化的四种情况
1当初始化一个引用成员时;
2当初始化一个常量成员时;
3当调用一个基类的构造函数,**而它拥有一组参数时;
4当调用一个
成员类的构造函数**,而它拥有一组参数时;

为什么快一点?分情况讨论

首先把数据成员按类型分类
1。内置数据类型,复合类型(指针,引用)
2。用户定义类型(类类型)

对于类型1,在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
对于类型2,结果上相同,但是性能上存在很大的差别

因为类类型的数据成员对象在进入函数体是已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,这是调用一个构造函数,在进入函数体之后,进行的是 对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)

重复的函数调用是浪费资源的,尤其是当构造函数和赋值操作符分配内存的时候。在一些大的类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负 责分配大量内存空间的Init函数。在这种情况下,你必须使用初始化列表,以避免不要的分配两次内存。

75、构造函数为什么不能为虚函数?析构函数为什么要虚函数?

C++中的虚函数的作用主要是实现了多态机制,即父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数。多态机制可以简单地概括为“一个接口,多种方法”。

  虚函数是通过一张虚函数表(Virtual Table)来实现的,简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得极为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

  1. 从存储空间角度,虚函数相应一个指向 vtable 虚函数表的指针,这大家都知道,但
    是这个指向 vtable 的指针事实上是存储在对象的内存空间的。当一个构造函数被调用时,它做的首要的事情之中的一个是初始化它的 VPTR。问题出来了,假设构造
    函数是虚的,就须要通过 vtable 来调用,但是对象还没有实例化**,也就是内存空间还**
    **没有,怎么找 vtable 呢?**所以构造函数不能是虚函数。
  2. 从使用角度,虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,**不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。因为构造函数本来就是为了明确初始化对象成员才产生的,然而 virtual
    function 主要是为了再不完全了解细节的情况下也能正确处理对象。另外,**virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用 virtual函数来完成你想完成的动作。

直接的讲,C++中基类采用 virtual 虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,**而不会调用派生类的析构函数。那么在这种情况下,**派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用 virtual 虚析构函数。

76、析构函数的作用,如何起作用?

1)构造函数只是起初始化值的作用,但实例化一个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。规则,只要你一实例化对象,系统自动回调用一个构造函数,就是你不写,编译器也自动调用一次。

析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~ 。 析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。当撤销对象时,编译器也会自动调用析构函数。 每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。

77、构造函数和析构函数可以调用虚函数吗,为什么

在 C++中,提倡不在构造函数和析构函数中调用虚函数;

构造函数和析构函数调用虚函数时都不使用动态联编,**如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本;**

因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时**不安全的,**故而 C++不会进行动态联编;

析构函数是用来销毁一个对象的,在销毁一个对象时**,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,**派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。

78、构造函数的执行顺序?析构函数的执行顺序?

  1. 构造函数顺序
    1 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
    2 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象**在类中被声明的顺序,**而不是它们出现在成员初始化表中的顺序。
    3 派生类构造函数。
    2)析构函数顺序
    1 调用派生类的析构函数;
    2 调用成员类对象的析构函数;

3调用基类的析构函数。

79、构造函数析构函数可否抛出异常

根基情况不可以,C++只会析构已经完成的对象,**对象只有在其构造函数执行完毕才算是完全构造妥当。**在构造函数中发生异常,控制权转出构造函数外。因此,在对象 b 的构造函数中发生异常,对象 b 的析构函数不会被调用。因此会造成内存泄漏。用 auto_ptr 对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时
发生资源泄漏的危机,不再需要在析构函数中手动释放资源;

如果异常从析构函数抛出,而且没有在当地进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,就是没有完成他应该执行的每一件事情。

80、类如何实现只能静态分配和只能动态分配

建立类的对象有两种方式:
1 静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;
2 动态建立,A *p = new A();动态建立一个类对象,就是使用 new 运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行 operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;

前者是把 new、delete 运算符重载为 private 属性。后者是把构造、析构函数设为 protected 属性。这样类对象不能够访问,但是派生类能够访问,能够正常的继承。同时创建另外两个create和destory函数类创建对象。

class A  
{  
protected:  
    A(){}  
    ~A(){}  
public:  
    static A* create()  
    {  
        return new A();  
    }  
    void destory()  
    {  
        delete this;  
    }  
};  
class A  
{  
private:  
    void* operator new(size_t t){}     // 注意函数的第一个参数和返回值都是固定的  
    void operator delete(void* ptr){} // 重载了new就需要重载delete  
public:  
    A(){}  
    ~A(){}  
};  

81、如果想将某个类用作基类,为什么该类必须定义而非声明?

派生类中**包含并且可以使用它从基类继承而来的成员,**为了使用这些成员,派生类必须知道他们是什么。

82、什么是组合?

一个类里面的数据成员是另一个类的对象,**即内嵌其他类的对象作为自己的成员;创建组合类的对象:首先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对基本类型的成员进行初始化,又要对内嵌对象进行初始化。**

创建组合类对象,构造函数的执行顺序:先调用内嵌对象的构造函数,然后按照内嵌对象成员在组合类中的定义顺序,**与组合类构造函数**的初始化列表顺序无关。然后执行组合类构造函数的函数体,析构函数调用顺序相反。

83、抽象基类为什么不能创建对象?

1)抽象类的定义:
称带有纯虚函数的类为抽象类。

纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。

(2)抽象类的作用:
抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,**由它来为派生类提供一个公共的根,**派生类将具体实现在其基类中作为接口的操作。所以
派生类实际上**刻画了一组子类的操作接口的通用语义,**这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

(3)使用抽象类时注意:
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类,如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

纯虚函数引入原因?

1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。

2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔 雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:
virtual ReturnType Function()= 0;)。若要使派生类为非抽象类,则编译器要求在派生类中,必须对纯虚函数予以重载以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

84、类什么时候会析构?

  1. 对象生命周期结束,被销毁时;
  2. delete 指向对象的指针时,或 delete 指向对象的基类类型指针,而其基类虚构函数是虚函数时;
  3. 对象 i 是对象 o 的成员,o 的析构函数被调用时,对象 i 的析构函数也被调用。

85、为什么友元函数必须在类内部声明?

因为编译器必须能够读取这个结构的声明以理解这个数据类型的大、行为等方面的所有规则。有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分。

86、继承机制中对象之间如何转换?指针和引用之间如何转换?

向上类型转换
将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的。
向下类型转换
将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派 生 类 , 所 以 在 向 下 类 型 转 换 时 必 须 加 动 态 类 型 识 别 技 术 。 RTTI 技 术 , 用dynamic_cast 进行向下类型转换。

87、组合和继承的优缺点?

一:继承
**继承是 Is a 的关系,**比如说 Student 继承 Person,则说明 Student is a Person。
继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。

继承的缺点有以下几点:

1:父类的内部细节对子类是可见的。

2:子类从父类继承的方法在编译时就确定下来了**,所以无法在运行期间改变从父类继承的方法的行为。**
3:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

二:组合

组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

组合的优点:
1:当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。
2:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。
3:当前对象可以在运行时动态的绑定所包含的对象。可以通过 set 方法给所包含对象赋值。

组合的缺点:1:容易产生过多的对象。2:为了能组合多个对象,必须仔细对接口进行定义。

88、函数指针?

什么是函数指针?指向函数类型的指针,函数类型是由其返回的数据类型和其参数列表共同决定的。一个具体的函数名就是一个函数指针,它指向函数的代码。一个函数地址是该函数的进入点,也就是调用函数的地址。函数调用根据函数名,也可以通过指向函数的指针来调用。我们希望在同一个函数中通过使用相同的形
参在不同的时间使用产生不同的效果。

函数指针的声明方法

int(*pf)(const int &,const int &) 上面的pf就是一个函数指针,返回类型为int,并带有两个const int&参数的函数。注意*pf两边的括号是必需的。

89、指针和引用的区别?

1.指针有自己的一块空间,而引用只是一个别名;
2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
5.可以有const指针,但是没有const引用;
6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
7.指针可以有多级指针(**p),而引用至于一级;
8.指针和引用使用++运算符的意义不一样;

90、内存对其原理?为什么要内存对其?

1 、 分配内存的顺序是按照声明的顺序。
2 、 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
3 、 最后整个结构体的大小必须是里面变量类型最大值的整数倍。

为什么?

1、平台原因(移植原因)

  1. 不是所有的硬件平台都能访问任意地址上的任意数据的;
  2. 某些硬件平台只能在**某些地址处取某些特定类型的数据,**否则抛出硬件异
    2、性能原因:
  3. 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
  4. 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

91、printf 实现原理?

printf 的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,**函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,**通过这些就可算出数据需要的堆栈指针的偏移量了。

92、内部连接和外部连接?

编译单元:一个cpp文件经过预处理阶段,形成一个包含所有必要信息的单个源文件。这个编译单元会被编译成为一个与cpp文件名同名的目标文件(.o或是.obj)。连接程序把不同编译单元中产生的符号联系起来,构成一个可执行程序。

内部连接:

  如果一个名称对于它的编译单元来说是透明的,并且在连接时不会与其它编译单元中的同样的名称相冲突,那么这个名称有内部连接

以下情况为内部连接:

  a)所有的声明

  b)名字空间(包括全局名字空间)中的静态自由函数、静态友元函数、静态变量的定义。如: static int x;

  c)enum定义

  d)inline函数定义(包括自由函数和非自由函数) 。内部连接的一个好处是这个名字可以放在一个头文件中而不用担心连接时发生冲突。

  e)类的定义, 类中函数的定义也okk

  f)名字空间中const常量定义

  g)union的定义

外部连接:

  在一个多文件程序中,如果一个名称在连接时可以和其它编译单元交互,那么这个名称就有外部连接。

以下情况有外部连接:

  a)类非inline函数总有外部连接。包括类成员函数和类静态成员函数

  b)类静态成员变量总有外部连接。

  c)名字空间(包括全局名字空间)中非静态自由函数、非静态友元函数及非静态变量

​ 因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。

对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已经知道的const对象和inline函数。这些实体可在多个源文件中定义,只要每个源文件中的定义是相同的。

​ 在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。

92、C++中的名字空间?

名字空间(namespace)是由标准C++引入的,是一种新的作用域级别。原来C++标识符的作用域分为三级:代码块({…}和函数体)、类域和全局作用域。如今,在类作用域和全局作用域之间,C++标准又添加了名字空间域这一个作用域级别。用来处理程序中常见的同名冲突。

访问一个具体的标识符的时候,可以使用如下形式:space_name::identifier。即用作用域指示符“::”将名字空间的名称和该空间下的标识符连接起来,这要,即使使用同名的标识符,由于它们处于不同的名字空间,也不会发生冲突。

名字空间的作用?

名字空间的作用主要是为了解决日益严重的名称冲突问题。随着可重用代码的增多,各种不同的代码体系中的标识符之间同名的情况就会显著增多。解决的办法就是将不同的代码库放到不同的名字空间中。

访问一个具体的标识符的时候,可以使用如下形式:space_name::identifier。即用作用域指示符“::”将名字空间的名称和该空间下的标识符连接起来,这要,即使使用同名的标识符,由于它们处于不同的名字空间,也不会发生冲突。有两种形式的命名空间——有名的和无名的。

名字空间的注意要点?

(1)一个名字空间可以在多个头文件或源文件中实现,成为分段定义。如果想在当前文件访问定义在另一个文件中的同名名字空间内的成员变量,需要在当前文件的名字空间内部进行申明。extern

(2)名字空间内部可以定义类型、函数、变量等内容,但名字空间不能定义在类和函数的内部

(3)在一个名字空间中可以自由地访问另一个名字空间的内容,因为名字空间并没有保护级别的限制。

(4)虽然经常可以见到using namespace std;这样的用法,我们也可以用同样的方法将名字空间中的所有标识符一次性“引入”到当前的名字空间中来,但这并不是一个值得推荐的用法。因为这样做的相当于取消了名字空间的定义,使发生名称冲突的机会增多。所以,用using单独引入需要的内容,这样会更有针对性。例如,要使用标准输入对象,只需用using std::cin;就可以了。

(5)不能在名字空间的定义中声明另一个嵌套的子命名空间,只能在命名空间中定义子命名空间。

(6)名字空间的成员,可以在命名空间的内部定义,也可以在名字空间的外部定义,但是要在名字空间进行声明。

(7)名字空间在进行分段定义时,不能定义同名的变量,否则连接出现重定义错误。因为名字空间不同于类,具有外部连接的特性。由于外部连接特性,请不要将名字空间定义在头文件,因为当被不同的源文件包含时,会出现重定义的错误。

(8)为了避免命名空间的名字与其他的命名空间同名,可以用较长的标识符作为命名空间的名字。但是书写较长的命名空间名时,有些冗余,因此,我们可以在特定的上下文环境中给命名空间起一个相对简单的别名。

namespace MyNewlyCreatedSpace{
    void show(){
        std::cout<<"a function within a namespace"<<std::endl;
    }
}
 
int main(int argc,char* argv[])
{
    namespace sp=MyNewlyCreatedSpace;
    sp::show();
}

匿名名字空间

匿名名字空间提供了类似在全局函数前加 static 修饰带来的限制作用域的功能。它的这种特性可以被用在struct和class上, 而普通的static却不能。比如,在两个源文件中定义了相同的全局变量(或函数),就会发生重定义的错误。如果将它们声明为全局静态变量(函数)就可以避免重定义错误。在C++中,除了可以使用static关键字避免全局变量(函数)的重定义错误,还可以通过匿名名字空间的方式实现。

与static的不同:

包含在匿名名字空间中的全局变量(函数)具有外部连接特性,而用static修饰的全局变量具有内部连接特性,不能用来实例化模板的非类型参数。而类模板的非类型参数要求是编译时常量表达式,或者是指针类型的参数要求指针指向的对象具有外部连接性。

#include <iostream>
using namespace std;
 
template <char*p> class Example{
public:
    void display(){
        cout<<*p<<endl;
    }
};
 
static  char c='a';
int main(int argc,char* argv[])
{
 
    Example<&c> a; //编译出错
    a.display();
}

操作系统

1 请你说一下源码到可执行文件的过程

1)预编译

主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下

1、删除所有的#define,展开所有的宏定义。

2、处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。

3、处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。

4、删除所有的注释,“//”和“/**/”。

5、保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。

6、添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。

2)编译

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

1、词法分析:将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。

2、语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。 在语法分析的同时,就把运算法优先级确定了下来,如果出现表达式不合法,各种括号不匹配,表达式中缺少操作,编译器报错。

3、语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分清的语义,相对应的动态语义是在运行期才能确定的语义。例如,将一个int型赋值给int*型,语义分析程序就发现类型不匹配,编译器会报错。

4、优化:源代码级别的一个优化过程。

5、目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。

6、目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。

3)汇编

将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。

4)链接

链接的过程:地址和空间的分配、符号决议(也叫“符号绑定”,倾向于动态链接)和重定位 。将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:

1、静态链接:

函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。

空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;

更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

2、动态链接:

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件动态库就是在需要调用其中的函数时,根据函数映射表找到该函数然后调入堆栈执行。

共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;

更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

2 常见I/O模型?5种?异步IO应用场景?有什么缺点?

几个概念同步,异步,阻塞,非阻塞

首先来解释同步和异步的概念,这两个概念与消息的通知机制有关。也就是同步与异步主要是从消息通知机制角度来说的。

同步异步概念

所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。

所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列

1)同步

就是在发出一个功能调用时,没有得到结果之前,该调用就不返回。同步 IO 指的是,必须等待 IO 操作完成后,控制权才返回给用户进程 。也就是必须一件一件事做 , 等前一件做完了才能做下一件事。就是我调用一个功能,该功能没有结束前,我死等结果。

2)异步

当一个异步过程调用发出后,调用者 不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和调用来通知调用者。我调用一个功能,不知道该功能的结果,该功能有结果后通知我(回调通知)

阻塞与非阻塞

阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的。

3)阻塞

阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之前才会返回。对于同步调用来说,很多时候当前进程还是激活的,只是逻辑上当前函数没有返回而已。就是调用函数,函数在没有接受完数据或者没有得到结果之前我不会返回。

4)非阻塞

指在不能立刻得到结果之前,该函数不会阻塞当前线程,而是会立刻返回。

5种常见IO模型

1 阻塞IO

应用程序调用一个 IO 函数,导致应用程序阻塞,等待数据准备好 。 如果数据没有准备好,一直等待 … .数据准备好了,从内核拷贝到用户空间,IO 函数返回成功指示。

2 非阻塞IO

我们把一个 SOCKET 接口设置为非阻塞就是告诉内核,当所请求的 I/O 操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的 I/O 操作函数将不断的测试
数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用 CPU 的时间。

3 IO复用

I/O 复用模型会用到 select、poll、epoll 函数,这几个函数也会使进程阻塞,但是和阻塞 I/O 所不同的的,这三个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用I/O 操作函数。

4 信号驱动IO

开启套解字的信号驱动式IO功能,通过sigaction系统调用安装一个信号处理函数。该系统调用让内核在描述符就绪时发送SIGIO信号通知我们。在信号处理函数调用IO操作函数处理数据。

5 异步IO模型

当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。与信号驱动IO的主要区别,信号驱动IO主要是通知我们何时开启一个IO操作,而异步IO模型是由内核通知我们IO操作何时完成。

3 IO 复用的原理?零拷贝?三个函数?epoll 的 LT 和 ET 模式的理解。

select 、 poll 和 epoll 都是多路 IO 复用的机制 。 多路 IO 复用就通过一种机制,可以监视多个描述符, 一旦某个描述符就绪( 一般是读就绪或者写就绪),能够通知程序进行相应的读写操作 。 但 select 、 poll 和 epoll 本质上都是同步 IO ,因为它们都需要在读写事件就绪后自己负责进行读写,即是阻塞的,而异步 IO 则无须自己负 责进行读写 ,异步 I/O 的实现会负责把数据从内核拷贝到用户 空 间 。

Select
select 的缺点:
1 单个进程能够监视的文件描述符的数量存在最大限制 ,通常是 1024。由于select 采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;

2 内核/用户空间内存拷贝问题,select 需要大量句柄数据结构,产生巨大开销;

3 **Select 返回的是含有整个句柄的数组,**应用程序需要遍历整个数组才能发现哪些句柄发生事件;

4 select ,所监控的描述符集在返回之后会发生边哈,所以在下次进入select之前都需要重新初始化需要监控的描述符集fd_set中的每一个比特位比较费时。

5 Select 的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行 IO 操作,那么每次 select 调用还会将这些文件描述符通知进程。

( 2) select()的优点如下所述 。
1 ) select()的可移植性更好,在某些 UNIX 系统上不支持 poll () 。
2) select()对于超时值提供了更好的精度,而 poll() 是精度较差 。

Poll
与 select 相比,poll 使用链表保存文件描述符,一你才没有了监视文件数量的限制,poll函数将监控的输入时间和输出时间分开,允许被监控文件数组被复用不需要重新初始化
但其他三个缺点依然存在

Epoll

1 支持一进程打开大数目的 socket 描述符( FD ) 。

se l ect()均不能忍受的是一个进程所打开的 FD 是有一定限制的,由 FD_SETSIZE 的 默认值是 1024/2048 。 对于那些需要支持上万连接数目的 IM 服务器来说显然太少了 。 这时候可以选择修改这个宏然后重新编译内核 。 不过 epoll 则没有这个限制,它所支持 的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 2048 。 举个例子,在 lGB 内存的 空 间中这个数字一般是 10 万左右,具体数目可以使用 cat/proc/sys/fs/file-max 查看, 一般来说这个数目和系统内存关系很大 。

2 IO 效率不随 FD 数目增加而线性下降 。

传统的 select/poll 另 一个致命弱点就是当你拥 有一个很大的 socket 集合,不过由于网络延迟,任一 时间只有部分的 socket 是 “活跃” 的,但是 se lect/poll 每次调用都会线性扫描全部的集合,导致效率呈现线性下降 。 但是 epoll 不存在这个问题 ,它 只会对“活跃”的 socket进行操作,这是因为在内核中实现 epoll 是根据每个fd上面的 callback 函数实现的 。 那么,只有“活跃”的 socket 才会主动去调用 callback 函数,其他 idle 状态 socket 则不会,在这点上, epoll 实现了 一个“伪” AIO ,因为这时候推动力由 Linux 内 核提供 。

3 使用 mmap 加速内核与用户 空 间的消息传递。

这点实际上涉及 epoll 的具体实现 。 无论是 select 、 poll 还是 epoll 都需要 内核把“消息
通知给用户空间,如何避免不必要的内存拷贝就显得尤为重要 。 在这点上, epoll 是通过内核
与用户空间 mmap 处于同一块内存实现的 。

4 请你说一下进程与线程的概念

进程是操作系统进行资源调度和分配的的基本单位,实现了操作系统的并发;

线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。

5 以及为什么要有进程线程,其中有什么区别

进程是指在系统中正在运行的一个应用程序,程序一旦运行起来就是进程。进程是系统资源分配的最小单位,且每个进程拥有独立的地址空间,实现了操作系统的并发;线程是进程的子任务,是CPU最小执行和调度的基本单位,实现了进程内部的并发;

线程是进程的一个实体,是进程的一条执行路径;比进程更小的独立运行的基本单位,线程也被称为轻量级进程,一个程序至少有一个进程,一个进程至少有一个线程;

每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。

为什么要有线程?

进程在同一时间只能干一件事

进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于等待的资源,仍然不会执行。单进程各个函数之间不是并发执行,影响资源使用效率,利用多进程解决另外维护进程系统开销大:创建进程时,分配资源,建立PCB,进程撤销时,回收资源,撤销PCB;进程切换,保存当前进程的状态信息。从通信机制上来讲,线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进城下的线程之间贡献数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序才会利于理解和修改。

区别:

1、一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。每个独立的进程有一个程序的入口、程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

2、进程是资源分配的最小单位,线程是CPU调度的最小单位;

3、进程在执行过程中拥有独立的地址空间,而多个线程共享进程的资源。(同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)

4、系统开销:由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备,PCB程序控制块等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。

5、进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉

6、通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。

6 进程通信方式

1 、信号:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。软件中断通知事件处理

接收到信号

catch:指定信号处理函数被调用

ignore: 依靠操作系统的默认操作

Mask:闭塞信号因此不会传送

缺点,不能交换任何数据

2、管道

管道 - 内核中一块缓存,buffer容量是有限的,同时管道是父进程帮忙创建的

shell

  • 创建管道
  • 为ls创建一个进程,设置stdout为管道写端口
  • 为more创建一个进程,设置stdin为管道读端口

我们来看一条 Linux 的语句

netstat -tulnp | grep 8080

学过 Linux 命名的估计都懂这条语句的含义,其中”|“是管道的意思,它的作用就是把前一条命令的输出作为后一条命令的输入。在这里就是把 netstat -tulnp 的输出结果作为 grep 8080 这条命令的输入。如果两个进程要进行通信的话,就可以用这种管道来进行通信了,并且我们可以知道这条竖线是没有名字的,所以我们把这种通信方式称之为匿名管道

并且这种通信方式是单向的,只能把第一个命令的输出作为第二个命令的输入,如果进程之间想要互相通信的话,那么需要创建两个管道。

居然有匿名管道,那也意味着有命名管道,下面我们来创建一个命名管道。它以一种特殊设备文件形式存在于文件系统中。

mkfifo  test

这条命令创建了一个名字为 test 的命名管道。

接下来我们用一个进程向这个管道里面写数据,然后有另外一个进程把里面的数据读出来。

echo "this is a pipe" > test   // 写数据

这个时候管道的内容没有被读出的话,那么这个命令就会一直停在这里,只有当另外一个进程把 test 里面的内容读出来的时候这条命令才会结束。接下来我们用另外一个进程来读取

cat < test  // 读数据

我们可以看到,test 里面的数据被读取出来了。上一条命令也执行结束了。

从上面的例子可以看出,管道的通知机制类似于缓存,就像一个进程把数据放在某个缓存区域,然后等着另外一个进程去拿,并且是管道是单向传输的。

这种通信方式有什么缺点呢?显然,这种通信方式效率低下,你看,a 进程给 b 进程传输数据,然后就a进程阻塞住了,只能等待 b 进程取了数据之后 a 进程才能返回。

所以管道不适合频繁通信的进程。当然,他也有它的优点,例如比较简单,能够保证我们的数据已经真的被其他进程拿走了。我们平时用 Linux 的时候,也算是经常用。

3、消息队列

那我们能不能把进程的数据放在某个内存之后就马上让进程返回呢?无需等待其他进程来取就返回呢?

答是可以的,我们可以用消息队列的通信模式来解决这个问题,例如 a 进程要给 b 进程发送消息,只需要把消息放在对应的消息队列里就行了,b 进程需要的时候再去对应的
消息队列里取出来。同理,b 进程要个 a 进程发送消息也是一样。这种通信方式也类似于缓存吧。

特点:

1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。

2)消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

这种通信方式有缺点吗?答是有的,如果 a 进程发送的数据占的内存比较大,并且两个进程之间的通信特别频繁的话,消息队列模型就不大适合了。因为 a 发送的数据很大的话,意味发送消息(拷贝)这个过程需要花很多时间来读内存。

4、共享内存

共享内存这个通信方式就可以很好着解决拷贝所消耗的时间了。

这个可能有人会问了,每个进程不是有自己的独立内存吗?两个进程怎么就可以共享一块内存了?

我们都知道,系统加载一个进程的时候,分配给进程的内存并不是实际物理内存,而是虚拟内存空间。那么我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样,两个进程虽然有着独立的虚拟内存空间,但有一部分却是映射到相同的物理内存,这就完成了内存共享机制了。

1)共享内存是最快的一种IPC,因为进程是直接对内存进行存取

2)因为多个进程可以同时操作,所以需要进行同步

3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问

5、Socket

上面我们说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?

答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。

7、什么是信号量?内部怎么实现的?

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

特点:

1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。抽象数据类型,信号量

  • 一个整形(semaphore棋语),两个原子操作,进入临界区之前执行P操作,离开临界区会执行V操作。
  • P():sem减1,如果sem < 0,等待,否则 继续
  • V():sem加1,如果sem <=0,就代表有进程在等待,唤醒一个挂在该信号量上等待的P,FIFO原则
  • 信号量是一个整数被保护,只有P,V操作能改变值

3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

class Semaphore{
    int sem;
    // 等待队列,sem小于零,线程被存入等待队列中
    WaitQueue q;
}
Semaphore::P(){
    sem--;
    if(sem < 0){
        Add this thread t to q
        //线程放入等待队列中
        block(p);
    }
}
Semophore::V(){
    sem++;
    if(sem <= 0){
        Remove a thead t from q
        // 唤醒一个线程
        wakeup(t);
    }
}

其系统调用为:

sem_wait(sem_t *sem):以原子操作的方式将信号量减1,如果信号量值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。

sem_post(sem_t *sem):以原子操作将信号量值+1。当信号量大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。

8、线程通信的方式?

临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;

互斥量Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。。当进入临界区时,需要获得互斥锁并且加锁;那么其他线程先要访问临界区就要等待锁释放。当离开临界区时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。

锁是一个抽象的数据结构

  • 一个二进制状态(解锁/绑定)
  • Lock::Acquire() - 锁被释放 之前一直等待,然后得到锁
  • Lock::Release() -释放锁,唤醒任何等待的进程
  • 怎么实现?
    • 原子操作
      • test -and set
        • 从内存中读取值
        • 测试该值是否为1,不为1进入临界区
        • 内存值设为1

其主要的系统调用如下:

pthread_mutex_init:初始化互斥锁

pthread_mutex_destroy:销毁互斥锁

pthread_mutex_lock:以原子操作的方式给一个互斥锁加锁,如果目标互斥锁已经被上锁,pthread_mutex_lock调用将阻塞,直到该互斥锁的占有者将其解锁。

pthread_mutex_unlock:以一个原子操作的方式给一个互斥锁解锁。

信号量Semphare:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。

条件变量,顾名思义,一个或者多个线程等待某个布尔表达式为真,即等待别的进程唤醒他。信号量会与互斥量一起使用,条件本身是由互斥量保护的。线程在改变条件状态之前必须锁住互斥量。

其主要的系统调用如下:

pthread_cond_init:初始化条件变量

pthread_cond_destroy:销毁条件变量

pthread_cond_signal:唤醒一个等待目标条件变量的线程。哪个线程被唤醒取决于调度策略和优先级。

pthread_cond_wait:等待目标条件变量。需要一个加锁的互斥锁确保操作的原子性。该函数中在进入wait状态前首先进行解锁,然后接收到信号后会再加锁,保证该线程对共享资源正确访问。

// 条件变量实现
//需要维持每个条件队列
//线性等待的条件等待signal()
class Condition{
    // 等待的线程数
    int numWaiting = 0;
    WaitQueue q;
}
Condition::Wait(lock){
    numWaiting++;
    Add this thread t to q;
    // 一定要先释放锁
    release(lock);
    schedule();//需要互斥锁,阻塞在这里,等待信号量满足条件
    require(lock);
}
Condition::Signal(){
    // 和信号量不一样,不一定执行--操作
    if(numWaiting > 0){
        remove a tread from q;
        wakeup(t);// 需要互斥锁
        numWaiting--;
    }
}
//从管程看生产者消费者问题
class BoundBuffer{
    //保证互斥
    Lock lock;
    //buffer内容计数
    int count = 0;
    //两个条件变量
    Condition notFull,notEmpty;
}
BoundeBuffer::Deposite(c){
    //线程进入管程,只有一个线程能进去
    lock -> Acquire();
    while(count == n)
        // 当前已经满了,睡眠,在Wait中一定要释放互斥锁,
        notFull.Wait(&lock);
    Add c to Buffer;
    count++;
    notEmpty.Signal();
    lock -> Release();
}
BoundeBuffer::Remove(c){
    lock -> Acquire();
    while(count == 0)
        notEmpty.Wait(&lock);
    remove c frome buffer;
    count--;
    // 有空闲了,唤醒notFull中线程
    notFull.Signal();
    lock -> Release();
}

9、虚拟内存?为什么要有虚拟内存?什么是虚拟地址空间?

虚拟内存,虚拟内存是一种内存管理技术,它会使程序自己认为自己拥有一块很大且连续的内存,然而,这个程序在内存中不是连续的,并且有些还会在磁盘上,在需要时进行数据交换;

为什么要有虚拟内存?

在早期的计算机中,是没有虚拟内存的概念的。我们运行一个程序,会把程序全部装入内存,然后运行。

当运行多个程序的时候,经常会出现如下问题:

  • 地址空间不隔离,没有权限保护

由于程序都是直接访问物理内存,所以一个进程可以修改其他进程的内存数据,甚至修改内核地址空间里的数据。

  • 内存使用效率底

当内存空间不足的时候,将其他程序暂时拷贝到硬盘,然后新程序装入内存运行。由于大量的数据会装入装出,内存效率会十分低下。

  • 程序运行地址不稳定

因为程序内存地址都随机分配的,所以程序运行的地址也是不确定的。内存管理复杂。

虚拟地址的优点?

  • 避免用户直接访问物理内存,防止一些破坏性操作,保护操作系统
  • 每个进程都分配了4GB的虚拟内存,用户程序可使用比实际物理内存更大的地址空间。
  • 当进程通信时,可采用虚存共享的方式实现。当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
  • 简化了链接器,加载器这样的程序的内存管理。

什么是虚拟地址空间?

虚拟地址空间是对于一个单一进程的概念,这个进程看到的将是地址从 0000 开始的整个内存空间。虚拟存储器是一个抽象概念,它为每一个进程提供了一个假象,好像每一个进程都在独占的使用主存。每个进程看到的存储器都是一致的,称为虚拟地址空间。从最低的地址看起:程序代码和数据,堆,共享库,栈,内核虚拟存储器。

虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。可以认为虚拟空间都被映射到了磁盘空间中,(事实上也是按需要映射到磁盘空间上,通过mmap),并且由页表记录映射位置,当访问到某个地址的时候,通过页表中的有效位,可以得知此数据是否在内存中,如果不是,则通过缺页异常,将磁盘对应的数据拷贝到内存中,如果没有空闲内存,则选择牺牲页面,替换其他页面。

10、mmap原理

mmap是用来建立从虚拟空间到磁盘空间的映射的,可以将一个虚拟空间地址映射到一个磁盘文件上,当不设置这个地址时,则由系统自动设置,函数返回对应的内存地址(虚拟地址)。使用mmap分配内存,在堆和栈之间找一对空闲的内存分配。这两种方式分配的是虚拟内存,没有分配物理内存。真正的分配要在第一次访问已经分配的虚拟地址空间的时候,发生缺页中断,由操作希用分配物理内存,建立虚拟内存和物理内存之间的映射关系。

11、进程控制块包含信息和组织方式?

进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,所以说,PCB(进程控制块)是系统感知进程存在的唯一标志。

PCB含有以下三大类信息

进程标识信息:本进程标识,父进程表示,用户标识

处理机状态信息保存区:

保存进程的运行现场信息。用户可见的寄存器,控制和状态寄存器,栈指针(过程调用,系统调用,中断处理和返回需要用到)

进程控制信息

调度和状态信息:如进程状态、等待事件和等待原因、进程优先级、进程通信信息:如消息队列指针、信号量等互斥和同步机制,这些信息存放在接收方的进程控制块中。存储管理信息:进程在辅存储器内的地址,包含指向本进程映像存储空间的数据结构。进程所用资源:包括进程所需全部资源、已经分得的资源,如主存资源、I/0设备、打开文件表等。有关数据结构连接信息:父子进程连接起来,进程可以连接到一个进程队列中,或连接到相关的其他进程PCB。

  • PCB组织方式

一般是链表:更好的完成动态插入删除。一个状态的进程对应PCB中的一个链表。如就绪链表,阻塞链表。

12、请你说一说操作系统中的程序的内存结构

可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。

BSS段(未初始化数据区):通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段属于静态分配,程序结束后静态变量资源由系统自动释放。

数据段:存放程序中已初始化的全局变量的一块内存区域。数据段也属于静态内存分配

代码段:存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量

text段和data段在编译时已经分配了空间,而BSS段并不占用可执行文件的大小,它是由链接器来获取内存的。

bss段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面。当这个内存进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。

可执行程序在运行时又多出两个区域:栈区和堆区。

栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。

堆区:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。

13、进程创建和进程切换步骤?

●进程的创建来源于以下四个事件:

1.提交一个批处理作业。

2.在终端上交互式的登录。

3.操作系统创建一个服务进程

4.存在的进程孵化(spawn)新的进程

●进程的创建过程如下描述:

1.在主进程表中增加一项,并从PCB池中取一个空白PCB,或者创建PCB控制块。对于我们一般写的程序,主进程是最初始的父进程。

2.为新进程的进程映像中的所有成分分配地址空间。对于进程孵化操作还需要传递环境变量,构造共享地址空间。

3.为新进程分配资源,除内存空间外,还有其它各种资源。

4.查找辅助存储器,找到进程正文段并装入到正文区

5.初始化进程控制块,**为新进程分配一个唯一的进程标识符,**初始化PSW。

6.把进程加入某一就绪进程队列,或直接将进程投入运行。

7.通知操作系统的某些模块,如记账程序、性能监控程序。

●进程切换的步骤

1.保存被中断进程的处理器现场信息

2.修改被中断进程的进程控制块的有关信息,如进程状态等。

3.把被中断进程的进程控制块加入有关队列

4.选择下一个占有处理器运行的进程

5.修改被选中进程的进程控制块的有关信息。

6.根据被选中进程设置操作系统用到的地址转换和存储保护信息。

7.根据被选中进程的信息来恢复处理器现场。

14、进程加载终止调用的函数?fork和vfork的区别?

fork()创建一个继承的子进程

当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。

  • 父进程返回子进程id,子进程返回0,不成功id<0
  • 复制父进程所有的变量和内存
  • 复制父进程所有CPU寄存器
int pid = fork(); //创建子进程
if(pid == 0){    
    //做什么都行(关闭网络连接。。)    
    //调用exec()加载新程序取代当前运行进程,地址空间,代码数据咱也换掉了    
exec("program",argc,arvc0,argv1..)}
//等待子进程结束child_status = wait(pid);

在fork后立刻执行exec所造成的地址空间的浪费。vfork()轻量级fork,创建进程时不再创建同样的内存映像

子进程必须要立刻执行一次对exec的系统调用,或者调用_exit( )退出 ,vfork( )会挂起父进程直到子进程终止或者运行了一个新的可执行文件的映像。通过这样的方式,vfork( )避免了地址空间的按页复制。

COPY on Write (写的时候在进行复制),在实际地址空间复制的时候并没有真实的复制,而只是复制父进程地址空间所需要的元数据(页表),指向同一块地址空间,当父进程或者子进程对某一个地址单元写操作会触发一个异常,使得触发异常的页复制成两份。只有写的时候才会复制成两份。

fork和vfork的区别:

fork( )的父子进程的执行次序不确定;vfork( )保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。

15、请问如何修改文件最大句柄数?

linux默认最大文件句柄数是1024个,在linux服务器文件并发量比较大的情况下,系统会报”too many open files”的错误。故在linux服务器高并发调优时,往往需要预先调优Linux参数,修改Linux最大文件句柄数。

有两种方法:

  1. ulimit -n <可以同时打开的文件数>,将当前进程的最大句柄数修改为指定的参数(注:该方法只针对当前进程有效,重新打开一个shell或者重新开启一个进程,参数还是之前的值)

  2. 对所有进程都有效的方法,修改Linux系统参数

vi /etc/security/limits.conf 添加

​ soft  nofile  65536

 hard  nofile  65536

将最大句柄数改为65536

修改以后保存,注销当前用户,重新登录,修改后的参数就生效了

16、并发和并行

并发(concurrency):指宏观上看起来两个程序在同时运行,比如说在单核cpu上的多任务。但是从微观上看两个程序的指令是交织着运行的,你的指令之间穿插着我的指令,我的指令之间穿插着你的,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率。

并行(parallelism):指严格物理意义上统一时刻的同时运行,比如多核cpu,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu都是往多核方面发展。

17、 常见的几种内存管理机制?

  1. 块式管理 :远古时代的计算机操系统的内存管理方式。将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片。

  2. 页式管理 :把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表将虚拟地址空间和物理地址对应起来。

  3. 段式管理 :页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义。段式管理把主存分为一段段的。但是,最重要的是段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。段式管理通过段表对应逻辑地址和物理地址

    每个段由三个参数定义:段基地址、段限长和段属性。

  4. 段页式管理机制 :段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。

18、请你说一说操作系统中的段式管理,页式管理寻址

Linux把虚拟内存空间分成若干个存储区域,Linux把这样的分区叫做。为了换入,换出的方便,物理内存也得按照大小分成若干个块。由于物理内存中的块空间是容纳虚拟页的容器,所以物理内存中的页叫做页框。一般来说一个页为4k大小。页与页框是linux实现虚拟内存技术的基础。页式管理实现了虚拟地址到物理地址之间的映射。我们将地址结构分为两个部分页号和页偏移量。

给出逻辑地址如何计算出物理地址?

  1. 处理器要访问虚拟地址,并把它传送给 MMU
  2. MMU 生成根据虚拟地址生成 VPN(页码),然后请求高速缓存/主存,获取 PTE 的数据。
  3. 高速缓存/主存向 MMU 返回 PTE 的数据,在被调进程的PCB中取出页表始址和页表大小,装入页表寄存器。
  4. 页号与页表寄存器的页表长度比较,若页号大于等于页表长度,发生地址越界中断,停止调用,否则继续
  5. 由页表起始地址和块号找出页表中的物理页号PPN。用物理页的基址加上页偏移 PPO(假设页大小为 4KB,那么页偏移就是虚拟地址的低 12 位,物理页的页偏移和虚拟页的页偏移相同),获取对应的物理地址。
  6. 主存/高速缓存将数据返回给 CPU。

段式存储的步骤?

  段的长度由相应的逻辑信息组的长度决定,因而各段长度不等,引入分段存储管理方式的目的主要是为了满足用户(程序员)在编程和使用上多方面的要求。

  要注重理解,完整的逻辑意义信息,就是说将程序分页时,页的大小是固定的,只根据页面大小大小死生生的将程序切割开;而分段时比较灵活,只有一段程序有了完整的意义才将这一段切割开。

地址结构分为两部分:段号、位移量(段内地址)

逻辑地址 = 段号&段内地址

物理地址= 基址+段内地址

段表中内容,段号,该段长,基址

  1.   在被调进程的PCB中取出段表始址和段表长度,装入控制寄存器
    
  2.   段号与控制寄存器的段表长度比较,若页号大于等于段表长度,发生地址越界中断,停止调用,否则继续
    
  3. 由段号结合段表始址求出基址

4 检查段内位移量是否超出该段的段长,若超过,产生越界中断。

  1.   基址**+**段内地址,即得物理地址
    

段页式存储

用户程序先分段,每个段内部再分页(内部原理同基本的分页、分段相同)逻辑地址分三部分:段号、段内页号、页内地址

为实现段页式存储管理,系统应为每个进程设置一个段表,包括每段的段号,该段的页表始址页表长度。每个段有自己的页表,记录段中的每一页的页号和存放在主存中的物理块号。

(1)程序执行时,从PCB中取出段表始址和段表长度,装入段表寄存器。
(2)由地址变换机构将逻辑地址自动分成段号、页号和页内地址。
(3)将段号与段表长度进行比较,若段号大于或等于段表长度,则表示本次访问的地址已超越进程的地址空间,产生越界中断。
(4)将段表始址与段号和段表项长度的乘积相加,便得到该段表项在段表中的位置。
(5)取出段描述子得到该段的页表始址和页表长度。
(6)将页号与页表长度进行比较,若页号大于或等于页表长度,则表示本次访问的地址已超越进程的地址空间,产生越界中断。
(7)将页表始址与页号和页表项长度的乘积相加,便得到该页表项在页表中的位置。
(8)取出页描述子得到该页的物理块号。
(9)对该页的存取控制进行检查。
(10)将物理块号送入物理地址寄存器中,再将有效地址寄存器中的页内地址直接送入物理地址寄存器的块内地址字段中,拼接得到实际的物理地址。

优点:(1) 段的逻辑独立性使其易于编译、管理、修改和保护,也便于多道程序共享。
(2) 段长可以根据需要动态改变,允许自由调度,以便有效利用主存空间。
(3) 方便编程,分段共享,分段保护,动态链接,动态增长。


19、分页机制和分段机制的共同点和区别

  1. 共同点

    • 分页机制和分段机制都是为了提高内存利用率,较少内存碎片。
    • 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。
  2. 区别

    • 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
    • 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。

20、快表TLB和多级页表

在分页内存管理中,很重要的两点是:

  1. 虚拟地址到物理地址的转换要快。
  2. 解决虚拟地址空间大,页表也会很大的问题。

为了解决虚拟地址到物理地址的转换速度,操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把块表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时CPU要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。

使用快表之后的地址转换流程是这样的:

  1. 根据虚拟地址中的页号查快表;
  2. 如果该页号在快表中,直接从快表中读取相应的物理地址;
  3. 如果该页号不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中;
  4. 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。

多级页表

引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中。

在学习计算机组成原理时,书中谈到,”使用多级页表可以压缩页表占用的内存”,在了解了多级页表的原理后,恐怕对这句话还是理解不了:把页表换成多级页表了就能节约内存了?不是还是得映射所有的虚拟地址空间么?

比如做个简单的数学计算,假如虚拟地址空间为32位(即4GB)、每个页面映射4KB以及每条页表项占4B,则进程需要1M个页表项(4GB / 4KB = 1M),即页表(每个进程都有一个页表)占用4MB(1M * 4B = 4MB)的内存空间。而假如我们使用二级页表,还是上述条件,但一级页表映射4MB、二级页表映射4KB,则需要1K个一级页表项(4GB / 4MB = 1K)、每个一级页表项对应1K个二级页表项(4MB / 4KB = 1K),这样页表占用4.004MB(1K * 4B + 1K * 1K * 4B = 4.004MB)的内存空间。多级页表的内存空间占用反而变大了?

其实我们应该换个角度来看问题,还记得计算机组成原理里面无处不在的局部性原理么?

如何节约内存

我们分两方面来谈这个问题:第一,二级页表可以不存在;第二,二级页表可以不在主存。

二级页表可以不存在

我们反过来想,每个进程都有4GB的虚拟地址空间,而显然对于大多数程序来说,其使用到的空间远未达到4GB,何必去映射不可能用到的空间呢?

也就是说,一级页表覆盖了整个4GB虚拟地址空间,但如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。做个简单的计算,假设只有20%的一级页表项被用到了,那么页表占用的内存空间就只有0.804MB(1K * 4B + 0.2 * 1K * 1K * 4B = 0.804MB),对比单级页表的4M是不是一个巨大的节约?

那么为什么不分级的页表就做不到这样节约内存呢?我们从页表的性质来看,保存在主存中的页表承担的职责是将虚拟地址翻译成物理地址;假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有1M个页表项来映射,而二级页表则最少只需要1K个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。

二级页表可以不在主存

其实这就像是把页表当成了页面。回顾一下请求分页存储管理,当需要用到某个页面时,将此页面从磁盘调入到内存;当内存中页面满了时,将内存中的页面调出到磁盘,这是利用到了程序运行的局部性原理。我们可以很自然发现,虚拟内存地址存在着局部性,那么负责映射虚拟内存地址的页表项当然也存在着局部性了!这样我们再来看二级页表,根据局部性原理,1024个第二级页表中,只会有很少的一部分在某一时刻正在使用,我们岂不是可以把二级页表都放在磁盘中,在需要时才调入到内存?我们考虑极端情况,只有一级页表在内存中,二级页表仅有一个在内存中,其余全在磁盘中(虽然这样效率非常低),则此时页表占用了8KB(1K * 4B + 1 * 1K * 4B = 8KB),对比上一步的0.804MB,占用空间又缩小了好多倍!

总结

我们把二级页表再推广到多级页表,就会发现页表占用的内存空间更少了,这一切都要归功于对局部性原理的充分应用。

回头想想,这么大幅度地解决内存空间,我们失去了什么呢?计算机的很多问题无外乎就是时间换空间和空间换时间了,而多级页表就是典型的时间换空间的例子了,动态创建二级页表、调入和调出二级页表都是需要花费额外时间的,远没有不分级的页表来的直接;而我们也仅仅是利用局部性原理让这个额外时间开销降得比较低了而已。

21、局部性原理

局部性原理是虚拟内存技术的基础,正是因为程序运行具有局部性原理,才可以只装入部分程序到内存就开始运行。

早在1968年的时候,就有人指出我们的程序在执行的时候往往呈现局部性规律,也就是说在某个较短的时间段内,程序执行局限于某一小部分,程序访问的存储空间也局限于某个区域。

局部性原理表现在以下两个方面:

  1. 时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
  2. 空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。

时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。

22、虚拟存储器

基于局部性原理,在程序装入时,可以将程序的一部分装入内存,而将其他部分留在外存,就可以启动程序执行。由于外存往往比内存大很多,所以我们运行的软件的内存大小实际上是可以比计算机系统实际的内存大小大的。在程序执行过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后继续执行程序。另一方面,操作系统将内存中暂时不使用的内容换到外存上,从而腾出空间存放将要调入内存的信息。这样,计算机好像为用户提供了一个比实际内存大的多的存储器——虚拟存储器

实际上,我觉得虚拟内存同样是一种时间换空间的策略,你用 CPU 的计算时间,页的调入调出花费的时间,换来了一个虚拟的更大的空间来支持程序的运行。

23、虚拟内存的技术实现

虚拟内存的实现需要建立在离散分配的内存管理方式的基础上。 虚拟内存的实现有以下三种方式:

  1. 请求分页存储管理 :建立在基本分页系统基础之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是目前最常用的一种实现虚拟存储器的方法。
  2. 请求分段存储管理
  3. 请求段页式存储管理

不管是上面那种实现方式,我们一般都需要:

  1. 一定容量的内存和外存:在载入程序的时候,只需要将程序的一部分装入内存,而将其他部分留在外存,然后程序就可以执行了;
  2. 缺页中断:如果需执行的指令或访问的数据尚未在内存(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段调入到内存,然后继续执行程序;
  3. 虚拟地址空间 :逻辑地址到物理地址的变换。

24、缺页步骤?

缺页是指当 CPU 请求一个虚拟地址时,虚拟地址所对应的页在物理内存中不存在。此时 MMU 会产生缺页错误,然后由内核的缺页处理程序从磁盘中调入对应的页到主存中。在处理完成后,CPU 会重新执行导致错误的指令,从而读取到对应的内存数据。

下面是缺页时的地址翻译的过程(第 1 步到第 3 步与页命中时相同):

  1. 处理器生成一个虚拟地址,并把它传送给 MMU
  2. MMU 生成根据虚拟地址生成 VPN,然后请求高速缓存/主存,获取 PTE 的数据。
  3. 高速缓存/主存向 MMU 返回 PTE 的数据
  4. 由于判断出 PTE 的有效位是 0,所以 CPU 将出发一次异常,将控制权转移给内核中的缺页异常处理程序。
  5. 缺页异常处理程序确定出物理内存中的牺牲页,如果这个页面被修改过了(D 标志位为 1),那么将牺牲页换出到磁盘。
  6. 缺页处理程序从磁盘中调入新的页面到主存中,并且更新 PTE
  7. 缺页处理程序将控制权返回给原来的进程,再次执行导致缺页的指令。再次执行后,就会产生页命中时的情况了。

25、页面置换算法?

上文提到缺页,但是当内存满了的时候,就需要从内存中按照一定的置换算法决定内存把哪个页面放弃,存入新的页。

最佳置换算法OPT

算法思想:每次选择淘汰的页面将是以后永不使用,或者在最长时间内不再被访问的页面,这样可以保证最低的缺页率。 最佳置换算法可以保证最低的缺页率,但是实际上,只有进程执行的过程中才能知道接下来会访问到的是哪个页面。操作系统无法提前预判页面的访问序列。因此,最佳置换算法是无法实现的

先入现出置换算法FIFO

总是选择在主存中停留时间最长(即最老)的一页置换,即先进入内存的页,先退出内存。

当为进程分配的物理块数增大时,缺页次数不减反增的异常现象称为贝莱迪(Belay)异常
只有FIFO算法会产生Belay异常。另外,FIFO算法虽然实现简单,但是该算法与进程实际运行时的规律不适应。因为先进入的页面也有可能最经常被访问。因此,算法性能差。

最长时间未用置换算法LRU

算法思想:每次淘汰的页面是最近最久未使用的页面。
实现方法:赋予每个页面对应的页表项中,用访问字段记录该页面自上次被访问以来所经历的时间t。当需要淘汰一个页面时,选择现有页面中t最大的页面,即最近最久未使用。

LRU 置换算法虽然是一种比较好的算法,但要求系统有较多的支持硬件。为了了解一个进程在内存中的各个页面各有多少时间未被进程访问,以及如何快速地知道哪一页是最近最久未使用的页面,须有两类硬件之一的支持:寄存器或栈。

寄存器

为了记录某进程在内存中各页的使用情况,须为每个在内存中的页面配置一个移位寄存器,可表示为

R = Rn-1 Rn-2 Rn-3 … R2 R1 R0

当进程访问某物理块时,要将相应寄存器的 R n -1 位置成 1。此时,定时信号将每隔一定时间(例如 100 ms)将寄存器右移一位。 如果我们把 n 位寄存器的数看做是一个整数, 那么,具有最小数值的寄存器所对应的页面,就是最近最久未使用的页面。

可利用一个特殊的栈来保存当前使用的各个页面的页面号。每当进程访问某页面时,便将该页面的页面号从栈中移出,将它压入栈顶。因此,栈顶始终是最新被访问页面的编号,而栈底则是最近最久未使用页面的页面号。

时钟置换算法

最佳置换算法那性能最好,但无法实现。先进先出置换算法实现简单,但是算法性能差。最近最久未使用置换算法性能好,是最接近OPT算法性能的,但是实现起来需要专门的硬件支持,算法开销大。时钟置换算法是一种性能和开销均平衡的算法。又称CLOCK算法,或最近未用算法NRU,Not Recently Used)

简单CLOCK算法算法思想:为每个页面设置一个访问位,再将内存中的页面都通过链接指针链接成一个循环队列。当某个页被访问时,其访问位置1.当需要淘汰一个页面时,只需检查页的访问位。如果是0,就选择该页换出;如果是1,暂不换出,将访问位改为0,继续检查下一个页面,若第一轮扫描中所有的页面都是1,则将这些页面的访问位一次置为0后,再进行第二轮扫描(第二轮扫描中一定会有访问位为0的页面,因此简单的CLOCK算法选择一个淘汰页面最多会经过两轮扫描)。

改进时钟置换算法

简单的时钟置换算法仅考虑到了一个页面最近是否被访问过。事实上,如果淘汰的页面没有被修改过,就不需要执行I/O操作写回外存。只有淘汰的页面被修改过时,才需要写回外存。
因此,除了考虑一个页面最近有没有被访问过之外,操作系统还需要考虑页面有没有被修改过。
改进型时钟置换算法的算法思想在其他在条件相同时,应该优先淘汰没有被修改过的页面,从而来避免I/O操作。 为了方便讨论,用(访问位,修改位)的形式表示各页面的状态。

如(1,1)表示一个页面近期被访问过,且被修改过。
算法规则:将所有可能被置换的页面排成一个循环队列

第一轮:从当前位置开始扫描第一个(0,0)的页用于替换,本轮扫描不修改任何标志位。
第二轮:若第一轮扫描失败,则重新扫描,查找第一个(0,1)的页用于替换。本轮将所有扫描的过的页访问位设为0。
第三轮:若第二轮扫描失败,则重新扫描,查找第一个(0,0)的页用于替换。本轮扫描不修改任何标志位。
第四轮:若第三轮扫描失败,则重新扫描,查找第一个(0,1)的页用于替换。

由于第二轮已将所有的页的访问位都设为0,因此第三轮、第四轮扫描一定会选中一个页,因此改进型CLOCK置换算法最多会进行四轮扫描。

(1) 第一优先级淘汰的是最近没有访问且没有修改的页面。
(2) 第二优先级淘汰的是最近没有访问但修改的页面。
(3) 第三优先级淘汰的是最近访问但没有修改的页面。
(4) 第四优先级淘汰的是最近访问且修改的页面。

26、调度算法?

调度评价指标

  • CPU使用率

    • CPU处于繁忙状态的时间百分比
  • 吞吐量(操作系统的计算带宽)

    • 在单位时间内的完成进程数量
  • 周转时间

    • 一个进程从初始化到结束,包括所有等待时间所花费的时间
  • 等待时间

    • 进程在就绪队列中的总时间
  • 响应时间(操作系统的计算延迟)

    • 从一个请求被提交到产生第一次响应所花费的时间。(鼠标,键盘响应。。)
  • FCFS(先来先服务Fisrt Come,First Served)

所谓 FCFS 就是「先来先服务(First Come First Serve)」,每个进程按进入内存的时间先后排成一队。每当 CPU 上的进程运行完毕或者阻塞,我就会选择队伍最前面的进程,带着他前往 CPU 执行。

缺点:我收到了一个短进程的抱怨:”上次我前面排了一个长进程,等了足足 200 秒他才运行完。我只用 1 秒就运行结束了,就因为等他,我多花了这么长时间,太不值得了。” 平均等待时间也会增长

我仔细一想, FCFS 算法确实有这个缺陷——短进程的响应时间太长了,用户交互体验会变差

  • SPN(短进程优先shortest Remaining time)

为了改进上述缺点,减少平均响应时间。每次选择预计处理时间最短的进程。因此,在排队的时候,我会把短进程从队列里提到前面。

但是缺点又出现了:但长进程们不干了:那些短进程天天插队,导致他们经常得不到 CPU 资源,造成了「饥饿」现象。这可是个大问题。FCFS 虽然响应时间长,但最后所有进程一定有使用 CPU 资源的机会。但 SPN 算法就不一样了,如果短进程源源不断加入队列,长进程们将永远得不到执行的机会——太可怕了。

那么前两种方法都不行,怎样又能照顾短进程,又能照顾长进程呢。

  • HRRN(最高相应比优先 Highest Response Ratio Next)

综合考虑等待时间和执行时间,响应比 = (等待时间+执行时间)/ 执行时间。响应比高的算法会先执行。我们称之为「高响应比优先」。关注了进程等待了多长时间,防止了无限期推迟。

问题:执行时间很难准确知道。工作量增加,每次调度计算相应比。

并发时代来临

随着计算机的普及,个人用户大量增长,并发,即一次运行多个程序的需求出现了。这可难倒我了——处理器只有一个,怎么运行多个程序?

每个进程短时间交替使用我的资源,但在人类看来,这些进程就像在「同时」运行。”

  • RR (轮寻 Round Robin)
    • 使用时间切片和抢占来轮流执行任务

在这个算法里,每个进程将轮流使用 CPU 资源,只不过在他们开始运行时,我会为他们打开定时器,如果定时器到时间(或者执行阻塞操作),进程将被迫「下机」,切换至下一个进程。至于下一个进程的选择嘛,直接用 FCFS 就好了。

直观来看,时间片越短,固定时间里可运行的进程就越多,可 CPU 说过,切换进程是要消耗他不少指令周期的,时间片过短会导致大量 CPU 资源浪费在切换上下文上。时间片过长,短交互指令响应会变慢。所以具体怎么取,还得看交互时间大小(感觉像没说一样,但至少给了个标准嘛)。

这一阶段,我的工作量大大提升——以前十几秒都不用切换一次程序,现在倒好,一秒钟就得切换数十次。

问题:IO密集型进程认为这算法不公平,时间片轮转没有照顾到我们这类进程啊!我们经常在 CPU 没呆到一半时间片,就遇到了阻塞操作,被你赶下去。而且我们在阻塞队列,往往要停留很长时间。等阻塞操作结束,我们还得在就绪队列排好长时间队。那些处理器密集型进程,使用了大部分的处理器时间,导致我们性能降低,响应时间跟不上

  • MFQ(多级反馈队列 Mutilevel FeedBack Queue)
    • 优先级队列中的轮循

例如有n级的优先级——优先级调度在所有级别当中,RR使用在每个优先级中。

时间量子大小随着优先级增加而增加,等待时间越长优先级越高。

如果任务在当前的时间量子中没有完成,则降级,用的时间片越多级别越降低。

优点:CPU密集任务的优先级下降很快。

I/O笔记型任务临流在高优先级。

  • Fair share scheduling (公平调度原则)

服务器怎么调度进程?有一个用户会使用多个进程,一个用户使用一个进程。在用户级别能够实现公平调度。

27、什么是系统调用呢?能不能详细介绍一下。

根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:

  1. 用户态(user mode) : 用户态运行的进程或可以直接读取用户程序的数据。
  2. 系统态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。

说了用户态和系统态之后,那么什么是系统调用呢?

我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能咋办呢?那就需要系统调用了!

也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。

这些系统调用按功能大致可分为如下几类:

  • 设备管理。完成设备的请求或释放,以及设备启动等功能。
  • 文件管理。完成文件的读、写、创建及删除等功能。
  • 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
  • 进程通信。完成进程之间的消息传递或信号传递等功能。
  • 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。

28、LRU具体实现?

class LRUCache {
public:
    LRUCache(int capacity):capLRU(capacity){
    }
    
    int get(int key) {
        auto itr = keyItrMap.find(key);
        if(itr == keyItrMap.end())
            return -1;
        // 找到链表里对应的value值
        int value = itr -> second -> second;
        // 找到了就删除原有位置,插相应元素到队头
        keyValueList.erase(itr -> second);
        keyValueList.push_front(make_pair(key,value));
        keyItrMap[key] = keyValueList.begin();
        return value;
    }
    
    void put(int key, int value) {
        auto itr = keyItrMap.find(key);
        // 找到重复元素修改就可以
        if(itr != keyItrMap.end()){
            keyValueList.erase(itr -> second);

        }
            keyValueList.push_front(make_pair(key,value));
            keyItrMap[key] = keyValueList.begin();        
        if(capLRU < keyValueList.size()){
            // 删除处于队位的元素
            keyItrMap.erase(keyValueList.back().first);
            keyValueList.pop_back();
        }
           
    }
         
private:
    // 存入key,和指向list的迭代器
    unordered_map<int,list<pair<int,int>>::iterator> keyItrMap;
    // 存入key和value
    list<pair<int,int>> keyValueList;
    int capLRU;
};
// 自己实现链表
struct myListNode
{
    int key;
    int value;
    myListNode* pre;
    myListNode* next;
    myListNode(int tmpKey,int tmpValue):key(tmpKey),value(tmpValue),
        pre(nullptr),next(nullptr){};
};
class LRUCache{
public:
    LRUCache(int tmpCap){
        capacity = tmpCap;
        head = new myListNode(-1,-1);
        tail = new myListNode(-1,-1);
        head -> next = tail;
        tail -> pre = head;
    }
    int get(int key){
        auto itr = keyAndAdress.find(key);
        if(itr == keyAndAdress.end())
            return -1;
        myListNode* deleNode = itr -> second;
        int value = deleNode -> value;
        removeNode(deleNode);
        myListNode* pNode = new myListNode(key,value);
        InsertNode(pNode);
        keyAndAdress[key] = pNode;
        return value;
    }
    void put(int key,int value){
        auto itr = keyAndAdress.find(key);
        if(itr != keyAndAdress.end()){
            removeNode(itr -> second);   
            size--;    
        }
        myListNode* pNode = new myListNode(key,value);
        size++;
        InsertNode(pNode);
        keyAndAdress[key] = pNode;
        if(size > capacity){
            int lastKey = tail -> pre -> key;
            keyAndAdress.erase(lastKey);
            //删除节点释放内存
            removeNode(tail -> pre);
            size--;
        }

    }
    //删除指定节点
    void removeNode(myListNode* pNode){
        pNode -> pre -> next = pNode -> next;
        pNode -> next -> pre = pNode ->pre;
        delete pNode;
        pNode = nullptr;
    }
    // 插入元素插入到链表的前边就可以了
    void InsertNode(myListNode* pNode){
        pNode -> next = head -> next;
        head -> next -> pre = pNode;
        head -> next = pNode;
        pNode -> pre = head;
    }
    ~LRUCache(){
        myListNode* pNode = head;
        while(pNode != nullptr){
            myListNode* nextNode = pNode -> next;
            delete pNode;
            pNode = nullptr;
            pNode = nextNode;
        }
    }
private:
    unordered_map<int,myListNode*> keyAndAdress;
    myListNode* head;
    myListNode* tail;
    int capacity = 0;
    int size = 0;

};

29、请问单核机器上写多线程程序,是否需要考虑加锁,为什么?

在单核机器上写多线程程序,仍然需要线程锁。因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突

30、请你说一说死锁发生的条件以及如何解决死锁

  • 互斥:某个资源在一个时间段内只能由一个进程占有,不能被两个或两个以上的进程占有。
  • 持有并等待:进程保持至少一个资源正在等待获取其他进程持有的额外资源。
  • 无抢占:一个资源只能进程自愿释放,进程已经完成了他的任务。
  • 循环等待:资源分配图里有回环,存在进程集合{P0,P1,..PN},P0正在等待P1所占用的资源,P1等待P2占用的资源,PN-1等待PN占用的资源,PN等待P0占用的资源。

死锁预防是保证不进入死锁状态的一种策略。打破死锁四个必要条件的一个就可以了。

打破前三种必要条件都不太好。

  • 打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
  • 打破持有并等待 - 要拿资源就把需要的资源全部拿到,资源占有时间长,别的进程得不到资源,可能发生饥饿,资源利用率低。不好
  • 打破无抢占 - 把别的进程kill掉,把资源抢过来这种预防死锁的方法实现起来困难,会降低系统性能。
  • 打破循环等待。对所有资源类型进行排序,并要求每个进程按照资源的顺序进行申请

死锁避免

  • 要求每个资源声明他可能需要的每个类型资源的最大数目
  • 资源的分配状态是通过限定提供与分配的资源数量,和进程的最大需求。
  • 动态检查资源的分配状态,以确保永远不会有一个环形等待状态。(假如我分配给你了,其他进程不会出现死锁,就分配给你)

31、银行家算法?

银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁的算法。

n = 进程数量,m= 资源类型数量

  • Max(总需求量):n*m矩阵,如Max[i][j] = k表示进程Pi最多请求k个资源类型Rj的实例。
  • Avaliable**(剩余空闲量**):长度为m的向量。如果Avaliable[j] = k,有k个类型Rj的资源实例可用。
  • Allocation(已分配量):n*m 矩阵。Allocation[i][j] = k,则Pi当前分配了k个Rj的实例。
  • Need(未来需求量):n*m矩阵,Need[i][j] = k,则Pi可能需要至少k个Rj来完成任务。
思路
Need[i][j]= Max[i][j]
设进程 cusneed 提出请求 REQUEST [i],则银行家算法按如下规则进行判断。
(1)如果 REQUEST [cusneed] [i]<= NEED[cusneed][i],则转(2);否则,出
错。因为进程已经超过了最大要求。
(2)如果 REQUEST [cusneed] [i]<= AVAILABLE[i],则转(3);否则,等待。
(3)系统试探分配资源,修改相关数据:
AVAILABLE[i]-=REQUEST[cusneed][i];
ALLOCATION[cusneed][i]+=REQUEST[cusneed][i];
NEED[cusneed][i]-=REQUEST[cusneed][i];
(4)系统执行安全性检查,如安全,则分配成立;否则试探险性分配作废,系统恢复原状,进程等待。


总流程
1 Work 和Finsh 分别是长度为m和n的向量
初始化:
Work = Avaliable;  //当前资源的剩余空闲量
Finish[i] = false for i = 1,2,....,n  //线程i没有结束,n线程个数,m资源类型量
2,找到这样的i://接下来找出Need比Work小的进程,我需要的资源当前还有
(a)Finish[i] = false;
(b)Need <= Work(否则不安全,不给他分配)
没有找到这样的i,转到43. Work = Work + Allcation   //使用完了回收资源
    Finish[i] = true;2.
4. if Finsh[i] == true for all i
表明系统处于安全状态

32、Linux的锁机制

互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒

读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。

自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。

计算机网络提纲

1,计算机网络主要指的就是TCP/IP协议栈,是互联网的基石
2,容易忘是因为TCP/IP技术栈有一大部分都隐藏于操作系统的内核态,较少被接触。
3,重点脉络:
(1,反复记忆。2,多问为什么)
一、TCP/IP协议体系的认知
(1)分层。一部分处于用户态,一部分处于内核态。数据链路层,网络层,传输层封装于操作系统内核态。应用层存在于操作系统的用户空间,包括DNS,FTP,HTTPs,HTTP,工作中接触较多的是应用层的部分。但其它层的原理必须理解,面试考察。
(2)层与层之间下层对上层是透 明的,传输在每一层是对等的。
二、数据链路层。
(1)以太网帧的格式。(2)MTU(最大传输单元)的概念。(3)ARP协议和RARP协议(地址协议和逆地址协议,网卡MAC地址和IP地址互查机制)(网络层和链路层的中间层)ARP报文格式,查询原理,ARP缓存机制,RARP原理差不多,不用细究
三、网络层
(1)掌握IP首部格式:如16位分片标识、DF不分片标志、MF更多分片标志、13位片偏移、8位生存时间TTL、16位的首部检验和等等。
(2)掌握如何IP分片:如总长大于MTU值,画分片情况;如何避免IP分片(在应用层或传输层做限制);确定分片顺序;确定分片是否全部到达。
(3)掌握IP选路。会看路由表。Route print 。路由表每个字段的含义
(4)掌握ICMP(因特网控制报文协议):(理解为网络层和传输层的中间协议)报文格式;2种查询报文+5种差错报文。(次要)

四、传输层
(1)掌握UDP协议:无连接,不可靠的特点;首部各个字段(次要)
(2)掌握TCP协议(面试集中考察):面向连接,可靠;首部各字段(序号,确认号,首部长度,窗口大小,校验和等特别的,完成可靠功能的部分);TCP连接控制机制(三次握手,四次挥手,同时打开,同时关闭,半关闭);TCP流量控制机制(滑动窗口、慢启动、拥塞避免、快速重传、快速恢复的算法原理);TCP超时重传机制(四个定时器);一些问题(为什么三次握手四次挥手?为什么TCP和UDP都存在伪包头?
五、应用层
(1)掌握DNS(域名解析)协议:名字空间;DNS指针查询(反向查找或逆向解析)基本原理、DNS缓存
(2)FTP协议(活化石)(了解 ):控制连接和数据连接(为什么需要这两种连接);两种工作模式(PASV+PORT);各种FTP指令和响应码;FTP断点续传,匿名FTP
(3)HTTP协议:报文格式(请求报文、响应报文、请求头各种字段、响应头各种字段);HTTP状态码。
(4)HTTPS协议:详细握手过程;各种算法(摘要算法、数字签名、数字证书的原理与过程)

1 DNS的概念,用途,DNS查询的实现算法

概念:

  • DNS(domian Name system)域名系统,是一个由分层的DNS服务器实现的分布式的数据库,提供了主机名和IP地址之间的相互转换的服务。

  • 域名解析就是我们平常输入的比如说www.baidu.com转化为ip地址,能够是用户方便的访问互联网,而不用去记住能够被机器直接读取的ip地址

  • DNS协议大部分情况下使用UDP传输,使用端口号53。要求域名解析器和域名服务器都必须自己处理超时和重传来保证可靠性。在两种情况下会使用TCP进行传输:

    • 返回的响应超过512字节,(UDP最大只支持521字节的数据)。
    • 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)

主机域名解析顺序

  • 浏览器缓存

  • 本机hosts文件

  • 路由器缓存

  • 找DNS服务器(本地域名服务器,权威域名服务器,顶级域名服务器,根域名服务器)

    • 迭代查询

    • 递归查询

DNS缓存

为了改善时延性能并减少因特网上到处传输DNS报文的数量,当莫个DNS服务器接收到一个DNS回答,他能将此回答中的信息缓存在本地存储器上。

2 http协议

超文本传输协议:一个专门在计算机世界里专门在两个点之间传输文字,图片,视频等超文本数据的约定和规范。首部行里存有各种字段

http有两种报文请求报文和相应报文

例子

//请求行,正在请求的对象 http版本号
GET /some/dir/page.html HTTP/1.1
//指定服务器的域名,可以将请求发往同一台服务器上的不同网站
Host: www.someschool.edu
//一个可复用的TCP连接就建立了,直到客户端或者服务器主动断开连接。
connection:keep-Alive
// 服务器在发完被请求的对象之后关闭连接
connection:close
// 浏览器的类型和版本
User-agent:Mozilla/1.0
// 声明自己能够接收的请求
Accept:*/ *    //都能收
// 语言类型
Accept-Lauguage:en-us
//我可以接收什么哪些压缩方式
Accept-Encoding:gzip,deflate

例子

Http/1.1 200 Ok
//发送完报文关闭tcp连接
Connection:close
// 服务器产生该响应并且发送响应报文的时间
data:Tue,09.Aug..
//服务器类型
Server:..
//最后对象修改的日期
Last-Modified:
//被发送对象的字节数
Content-Length:6180
Content-Type:test/html
//我采用什么压缩方式
Contest-Encoding:gzip

常见的状态码

200 OK:请求成功,信息在返回响应的报文之中

301 Moved Permanently: 请求的对象被永久转移了,新的URL在定义相应报文段的location字段中,浏览器自动重定向新的URL

400 Bad request:一个通用差错编码,指示请求不能被服务器理解。

404 Not Found:请求的文档不再服务器上

505 HTTP Version Not Supported:服务器不支持请报文使用的HTTP协议版本。

GET和Post的区别

Get方法的含义是请求服务器获取资源,这个资源可以是静态的文本,页面,图片视频等。

而Post方法则是相反的操作,他向URL指定的资源中中提交数据,比如说一个人的博客可以留言,我写完留言点击提交,我的留言就会执行一次post请求。

Get方法是安全并且幂等的,也就是说get是一个只读操作,无论执行多少次,服务器上的数据都是安全的,并且每次的结果都是相同的。

Post方法不是安全的并且不是幂等的,Post因为是新增或者提交数据,会修改服务器上的资源,所以不是安全的,并多次提交数据会执行多次操作创建多个资源,所以不是幂等的。

Get 能够被缓存,而 post 不可以;
Get 参数保留在浏览器历史中,而 post 参数不会保留在浏览器历史中;
当发生数据时,get 方法向 URL 添加数据,URL 的数据长度是受限的,而 post没有数据长度限制;
Get 只允许 ASCII 编码,而 post 没有限制;
Get 安全性没有 post 安全性好;
Get 数据在 URL 中对所有人是可见的,而在 post 中数据不会显示在 URL 中。
Get 产生一个 TCP 数据包,post 产生两个 TCP 数据包;对于 get 方式的请求,
浏览器会把 header 和 data 一并发送出去;对于 post,浏览器先发送 header再发送 data;

参考回答:

1、概括

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)

2、区别:

1、get参数通过url传递,post放在request body中。

2、get请求在url中传递的参数是有长度限制的,而post没有。

3、get比post更不安全,因为参数直接暴露在url中,所以不能用来传递敏感信息。

4、get请求只能进行url编码,而post支持多种编码方式。

5、get请求会浏览器主动cache,而post支持多种编码方式。

6、get请求参数会被完整保留在浏览历史记录里,而post中的参数不会被保留。

7、GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。

8、GET产生一个TCP数据包;POST产生两个TCP数据包。

序号 方法 描述
1 GET 发送请求来获得服务器上的资源,请求体中不会包含请求数据,请求数据放在协议头中。另外get支持快取、缓存、可保留书签等。幂等
2 POST 和get一样很常见,向服务器提交资源让服务器处理,比如提交表单、上传文件等,可能导致建立新的资源或者对原有资源的修改。提交的资源放在请求体中。不支持快取。非幂等,POST的数据存放位置由服务器自己决定
3 HEAD 本质和get一样,但是响应中没有呈现数据,而是http的头信息,主要用来检查资源或超链接的有效性或是否可以可达、检查网页是否被串改或更新,获取头信息等,特别适用在有限的速度和带宽下。
4 PUT 和post类似,html表单不支持发送资源与服务器,并存储在服务器指定位置,要求客户端事先知道该位置;比如post是在一个集合上(/province),而put是具体某一个资源上(/province/123)。所以put是安全的,无论请求多少次,都是在123上更改,而post可能请求几次创建了几次资源。幂等
5 DELETE 请求服务器删除某资源。和put都具有破坏性,可能被防火墙拦截。如果是https协议,则无需担心。幂等
6 CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。就是把服务器作为跳板,去访问其他网页然后把数据返回回来,连接成功后,就可以正常的get、post了。
7 OPTIONS 获取http服务器支持的http请求方法,允许客户端查看服务器的性能,比如ajax跨域时的预检等。
8 TRACE 回显服务器收到的请求,主要用于测试或诊断。一般禁用,防止被恶意攻击或盗取信息。

Cookies和session的区别

  • 登录网站,今输入用户名密码登录了,第二天再打开很多情况下就直接打开了。这个时候用到的一个机制就是cookie。
  • session一个场景是购物车,添加了商品之后客户端处可以知道添加了哪些商品,而服务器端如何判别呢,所以也需要存储一些信息就用到了session。

cookies:一个web站点通常希望能够识别用户,将内容和用户身份连接起来。当客户端发送http请求的时候,服务器就会在返回的报文中含有set-cookies字段,并且这个字段保存在客户端的硬盘上,这样客户端再次发送请求的时候就会加上自己的cookeis,从而服务器就可以识别用户。可以用来在莫个站点持久的保存数据。但是也会造成隐私泄漏问题,结合cookies和用户的账户信息,购物信息等等,Web站点可以知道很多关于用户的信息。cookie存在于客户端,所以也可以伪造

session:Session是存在服务器的一种用来存放用户数据的类HashTable结构。当浏览器第一次发起请求的时候,服务器就会自动生成session id 和 hashTable,当第二次发起请求的时候,将前一次服务器响应的session id放在请求中一并发给服务器 ,这时服务器进行和原来的session Id进行对比,就可以找到这个用户的hashTable

cookie数据保存在客户端,session保存在服务器端。

\1. 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。
\2. 思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
\3. Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。
所以,总结一下:
Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

3 一次完整的http 请求所经历的步骤

1 浏览器键入URL,通过DNS服务器请求解析该URL中域名对应的IP地址

2 解析出Ip地址之后,根据该IP地址和默认端口80,和服务器建立TCP连接;

3 浏览器发出读取文件(URL中域名后边的部分)的HTTP请求,该请求报文作为TCP三次握手的底数哪个报文的数据发送给服务器;

4 HTTP服务器从TCP套解字读取HTTP GET 报文,生成一个HTTP响应报文,并把相应的html文本放入到相应报文主体中,发送给浏览器

6 浏览器将接收HTTP响应报文,抽取WEB页面内容,之后进行渲染显示

7 释放TCP链接

4 http 和https的区别

1 http是超文本传输协议,信息传输是明文,存在安全风险。Https则解决了HTTP不安全的缺陷,在TCP和http网络层之间加入了SSL/TLS安全协议,使得报文能够加密传输。

2 http连接的建立相对见简单,TCP三次握手之后便可以进行http传输。而https协议在三次握手之后还要进行

SSl/Tls的握手过程,才能加入加密报文传输

3 HTTP端口号是80,HTTPS端口号是443

4 HTTPS协议需要向CA(证书权威机构申请证书),确保服务器的身份是可信的。

Https解决了HTTP哪些问题?

窃听风险,比如通信链路上可以 获取通信内容,用户号容易没。

篡改风险,比如强制加入垃圾广告

冒充风险,比如冒充淘宝网站,用户钱容易没

HTTPs可以很好的解决上述问题:

信息加密:交互信息无法被窃取。采用混合加密的方式,对称加密,非对称加密。

校验机制:无法篡改通信内容,篡改了就不能正常显示。使用摘要算法实现完整性,他能够为数据生成独一无二的指纹,指纹用于校验数据的完整性,解决了篡改的风险。

身份证书:证明淘宝网真是淘宝网,将服务器的公钥放到数字证书中去。

SSL/TLS协议的基本流程

  • 客户端向服务器所要并验证服务器公钥
  • 双方协商产生会话秘钥
  • 双反次熬夜嗯会话秘钥进行加密通信

前两步就是涉及握手阶段的四次通信

步骤:

1 ClientHello:客户端发送给服务器一个随机数,还有支持的TLS协议版本,还有我能使用的加密算法。

2 serverHello:

  • 确认TLs版本,版本的相同我们才能继续加密通信。
  • 我服务器也产生一个随机数,用于产生会话秘钥。确认咱们的加密算法
  • 然后我将我的公钥和我的CA数据签名发给客户端。

3 客户端回应,

  • 首先通过客户端浏览器的CA公钥,对服务器发来的数字签名进行验证取出服务端的公钥。
  • 然后再次向服务端发送一个随机数,这时候这个随机数就被公钥加密,
  • 因为客户端和服务器利用之前两步产生的三个随机数利用加密算法产生秘钥。随后的加密都将使用秘钥加密。
  • 客户端握手结束通知,表示客户端握手结束,同时对以前的内容做一个摘要,供给服务器校验

4 服务器最后响应

  • 服务器通过三个随机数通过加密算法,得到会话秘钥,向客户端发送最后的信息,加密算法改变通知,之后都用秘钥加密。
  • 握手结束,所有内容摘要供给客户端检验。

5 http的演进和改变

http1.1 相比http1.0提高了什么性能

  • 使用TCP长连接的方式改善了http1.0断连造成的性能开销
  • 支持管道网络传输,即在同一个TCP连接里,客户端可以同时发送多个请求,只要一个请求发出去了,不必等待它的相应回来,就可以发送第二个请求出去,可以减少整体的响应时间。

但是HTTP/1.1还是有性能瓶颈:

  • 请求/响应头部没有进行压缩就进行发送,首部信息越多延迟越大,只能压缩Body部分;
  • 发送冗长的首部。每次相互发送相同的内容量浪费较多;
  • 服务器按照请求顺序相应的,如果服务器响应慢,会招致客户端一直请求不到数据,也就是队头堵塞。
  • 没有请求优先级控制
  • 请求只能从客户端开始,服务端只能被动响应

Http2相比http1进行性能改进

  • HTTP2会压缩头部信息,同时发送多个请求,他们的头是相似的,协议会帮你消除重复的部分。

  • 全面采用二进制格式,不再采用HTTP1.1纯文本的格式。

  • 多路复用,Http2可以在一个连接中并发多个请求或者回应,而不用按照顺序意义对应。移除了HTTP1.1中的串行请求,不再有头部阻塞的问题,降低了延迟,大幅度提高了连接的利用率。

  • 服务器推送,一定程度改善了请求应答模式,服务器不再是被动相应,可以主动向客户端发送消息。

HTTP3做了什么呢?

http2中的问题在于,多个http请求复用一个TCP连接,下层的TCP协议不知道有多少个HTTP请求的。一旦发生丢包现象,就会出发TCP重传机制,在这样的一个TCP连接机制中所有的HTTP请求都必须等待这个丢了的包重新传回来。

这是基于TCP传输层的问题,所以HTTP把HTTP下层的TCP改成了UDP!大家都知道 UDP 是不可靠传输的,但基于 UDP 的 QUIC 协议 可以实现类似 TCP 的可靠性传输。

传输层

TCP和UDP是传输层两个最基本的协议,最基本的职责是将两个在端系统间IP的交付服务扩展为在端系统的两个进程之间的交付服务。

6 TCP和UDP的区别

可靠性

TCP是可靠交付:无差错,不丢失,不重复。

UDP是尽最大努力交付,不保证可靠交付。

拥塞控制,流量控制

TCP有拥塞控制和流量控制保证数据传输的安全性,UDP没有拥塞控制,网络拥塞不会影响源主机的发送效率。

报文长度

TCP是动态报文长度,即TCP报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的。

UDP面向报文,不合并,拆分,保留上面串下来的报文边界。

首部开销

TCP首部开销大,占20个字节,UDP占8个字节(源端口,目的端口,数据长度,校验和)

适用场景

若通信数据完整性比实时性重要,比如传送邮件,反之则用UDP,如视频传输,实时通信等。

7 UDP的校验和是怎么计算的

注意:校验和是可选的。(TCP是必选的)。UDP的校验和是药剂算首部和数据部分。首部还包括伪首部。

多了十二字节的伪首部,注意UDP长度被计算了两次,如果校验和有错,则UDP数据报被悄悄丢弃,不产生任何差错报文。

校验方式:发送方对报文段所有16比特的和进行反码运算,加和其间任何其间出现溢出都会回卷。接收方将所有的比特加在一起,如果没有差错,最后的和是全为1的。

8 TCP最大传输单元

TCP最大可以从缓存中取出来并放入报文段中的数量受限于最大报文段长度(MSS maxium segment size)最初确定的由本地发送主机的最大链路帧长度(最大传输单元 maximum Transmission union)MTU一般为1500字节,MSS典型值为1460字节,TCP/IP首部长度一般占40字节。

9 TCP报文段结构

TCP首部一般占20字节

包括

  • 源端口号,目的端口号
  • 32比特序号字段,32比特确认号字段,实现可靠数据传输服务。
    • 序号:序号建立在传送字节流上,序号就是该报文段首个字节编号。
    • 确认号:主机A填写的确认号是主机A希望从主机B收到的确认号。
  • 16比特接收窗口字段,用于流量控制。指示接收方愿意接受的字节数量
  • 4比特首部长度字段,选择字段,协商最大报文长度MSS,或者在告诉网络环境下做窗口调节因子使用。
  • 接收窗口字段,发送字节还剩下多少空间
  • 6比特标志字段,ACK用于指示确认字段中的值是有效的,即该报文包括一个对已被成功接收报文段的确认。RST,SYN和FIN分别用于连接建立和拆除。PSH被设置代表接收方应立即将数据报传输给上层。URG用来指示报文段中存在着被发送端上层实体设置为紧急的任务。紧急数据由最后的紧急数据指针字段指出。

10 流量控制服务和拥塞控制的区别

TCP接收正确、按序到达的字节后,直接放入接收缓存。相关进程会从当前缓存读取数据,但是不一定是立即读取。接收方应用或许在忙其他方面的业务,如果发送方读取数据缓慢,就会很容易的使该连接接受缓存溢出。

  • 流量控制服务:消除发送方使接收方缓存溢出的可能性。流量控制因此是一个速度匹配的服务,即发送方的发送速率和接受方应用程序的读取速率相匹配。

    • 让发送方维护一个称为接受窗口(rwnd Recieve window)的变量来提供流量控制。接受窗口用于给发送方一个提示——该接收方窗口还有多少可用的空间。发送方控制发送出去的字节数小于接受窗口的大小。

    • 小问题:发送方维护的rwnd为0。接收方清空缓存,但是并不向发送方发送带有新值的rwnd段。解决当主机B的接收窗口为0的时候,主机A继续发送只有一个字节的数据报段,这样B就可以返回非零的rwnd的值。

拥塞控制:TCP发送方有可能因为IP网络的拥塞而被抑制:这种形式的发送方控制叫做拥塞控制。

11 简述一下TCP的三次握手四次挥手

  • 握手 (确认号是我想要接受的序号)

1:客户算将报文段首部SYN标志位设置为1,随机生成序号seq = x,发送给服务器,客户端进入Client_sent状态。

2:服务器接收到包含SYN=1的数据包知道了客户端要建立请求连接,客户端将标志位SYN和ACK都置为1,将确认号置为x+1,随机生成一个序号seq=y,将数据包发回客户端确认连接请求,服务器进入SYN_recieved状态。

3 客户端收到数据包检查接受的确认号是否为x+1,ACK是否为1 ,如果正确将确认号设置为y+1发送给服务器,服务器检查确认号是否为y+1,ack是否为1,如果是则建立连接成功,客户端和服务器进入established状态,可以互相发送数据了。

  • 挥手

TCP是全双工连接的,每一方都需要单独关闭,原则是发送FIN来终止任务,首先一方执行主动关闭,另一方执行被动关闭。

1 数据传输结束,客户端应用进行发出连接解释报文段,并停止发送数据,客户端进入FIN_WAIT_1状态,此时客户端依然可以接受数据。

2 服务器接收FIN后,发送一个ACK给客户端,确认号为收到序号加1,服务器进入COLSE_WAIT状态,客户端进入FIN_WAIT2状态。

3 当客户端没有数据据要发送时,服务器向客户端发送一个FIN报文,此时服务器进入LAST_ACK状态。

4 客户端接收到服务器的报文FIN之后,给服务器发送一个ACK报文,确认序号是收到序号+1.此时客户端进入TIME_WAIT状态,等待一段时间(报文最大生存时间)关闭连接。

为什么是3次握手

  • 为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤
    如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认
  • 防止本来因该失效的连接请求报文有突然回到服务器端造成服务器的浪费。例如,客户端发送一个SYN,由于网络堵塞,服务器并没有收到这个数据包。然后客户端有重新传了这个SYN数据报并且正确建立TCP连接了,传送完数据,关闭了TCP连接。这是之前发送的SYN数据包来到服务器,服务器发出应答报文段。如果没采用三次握手连接,此时服务器发送应答报文段表示已经建立起了连接,一直等着发送数据。因为客户端没有发起新的请求,会丢弃服务器的SYN。此时服务器会一直等待客户端发送数据而造成 资源的浪费。

  • SYN洪泛攻击

为什么是4次挥手

为了能够保证数据完成传输,当关闭连接时,当收到FIN报文通知的时候,它仅仅表示对方没有数据发送给你了;但是你的数据未必全部发送给了对方,所以你可以不立刻关闭SOCKET,你还可以继续传输数据给对方。你发送FIN给对方表示你同意可以关闭连接了,所以这里的FIN和ACK都是分开发送的。

12 请你说说传递到IP层怎么知道报文该给哪个应用程序,它怎么区分UDP报文还是TCP报文

根据端口区分;

看ip头中的协议标识字段,17是udp,6是tcp

13 TCP拥塞机制

主要是下面四种机制

  • 慢开始
    • 慢开始指的是TCP开始发送设置拥塞窗口cwnd=1。不要一开始就发送大量数据,先探测一下网络的拥塞程度,经过一个轮次的传输,拥塞窗口cwnd加倍。当cwnd超过慢开始门限,则使用拥塞避免算法,防止cwnd增长过大。
  • 拥塞避免
    • 每经过一个RTT时间按,cwnd就增长1。
    • 在慢开始和拥塞避免的过程中一旦发现网络拥塞,就把慢开始门限设置为当前cwnd的一半,重新设置cwnd为1
  • 快重传
    • 接收方每次接收到一个失序的报文段之后就应该立即发出重复确认,发送方只要连续次收到三个重复确认就立即重传。
  • 快速回复
    • 当发送方接受到三个连续的重复确认时,就是型乘法减小算法,把慢开始门限设置当前cwnd的一半一,将cwnd设置为慢开始门限大小,执行拥塞避免算法。

14 UDP 中一个包的大小最大能多大

  1. 以太网(Ethernet)数据帧的长度必须在 46-1500 字节之间,这是由以太网的物理特性
    决定的.这个 1500 字节被称为链路层的 MTU(最大传输单元).但这并不是指链路层的长度
    被限制在 1500 字节,其实这这个 MTU 指的是链路层的数据区.
  2. 并不包括链路层的首部和尾部的 18 个字节.所以,事实上,这个 1500 字节就是网络层
    IP 数据报的长度限制.因为 IP 数据报的首部为 20 字节,所以 IP 数据报的数据区长度最大为
    1480 字节.
  3. 而这个 1480 字节就是用来放 TCP 传来的 TCP 报文段或 UDP 传来的 UDP 数据报的.
    又因为 UDP 数据报的首部 8 字节,所以 UDP 数据报的数据区最大长度为 1472 字节.这个1472 字节就是我们可以使用的字节数。

15 TCP粘包

发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法
(Nagle 算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,
然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。

TCP 粘包 是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾;

发送方原因
我们知道,TCP 默认会使用 Nagle 算法。而 Nagle 算法主要做两件事:1)只
有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确
认到来时一起发送。所以,正是 Nagle 算法造成了发送方有可能造成粘包现象。

接收方原因

TCP 接收到分组时,并不会立刻送至应用层处理,或者说,应用层并不一定会立即处理;实际上,TCP 将收到的分组保存至接收缓存里,然后应用程序主动从缓
存里读收到的分组。这样一来,如果 TCP 接收分组的速度大于应用程序读分组的速度,多个包就会被存至缓存,应用程序读时,就会读到多个首尾相接粘到一起的包。

  1. 解决方法
    1 发送方
    对于发送方造成的粘包现象,我们可以通过关闭 Nagle 算法来解决,使用TCP_NODELAY 选项来关闭 Nagle 算法。
    2
    接收方

遗憾的是 TCP 并没有处理接收方粘包现象的机制,我们只能在应用层进行
处理。
3
应用层处理
应用层的处理简单易行!并且不仅可以解决接收方造成的粘包问题,还能解决发送方造成的粘包问题。

比如我们进行一个规定,发送每条数据的时候加上独有的开始符和截至符号。这种方法简单可行,就是要保证每条数据内不包含截至符号和开始符号。另一种,就是将将数据的长度一并发送,例如规定数据的前四个是数据的长度,应用层可以根据数据来判断每个分组的起始位置和结束位置。

15.1 UDP会粘包吗

UDP不会发生粘包。接收是面向消息传输的,接收方一次智能接受一个消息,所以不存在粘包问题。

举个例子:有三个数据包,大小为2k,4k,6k,如果我才用UDP发送的话,不管接收方的缓存有多大,我们至少进行三次以上的发送才能将数据包发送完,那么TCP协议发送的话,只需要发送方的接受缓存有12k大小,就可以一次性把这三个数据包一次性的发送完毕。

16 TCP有几大定时器

  • 重传计时器
    • 在一个TCP连接中,TCP没发送一个报文段,就对此报文段设置一个超时重传计时器。若该计时器截至期到了还没有收到应答报文,则重传此报文段,并将计时器复位。
  • 持续计时器
    • 为了应对,零窗口大小通知rwnd = 0;假定服务器发送报文段,我的接收窗口为0.那么客户端就停止发送数据,直到客户端接收到一个服务器发送过来接收窗口部不为零的报文段。若这个报文段传输过程中丢了。双方定时器都会永远等着对方发送数据。为了打破这种死锁,TCP为每一个连接使用一个持续计时器,当发送方将诶收到一个窗口大小为0的确认时,就启动持续计时器。当持续定时器到时间了,发送方就发送一个特殊的报文段,叫探测报文段。这个报文段只有一个字节的数据,特有一个序号,但是他的序号不需要被确认。探测报文段提醒对端若是发生丢失,必须重传。
  • 保活计时器
    • 防止两个TCP连接之间出现长时间的空闲。假定客户打开了到服务器的连接,传送一些数据,然后就保持静默了,。在这个情况下,这个连接就永远处于打开状态。
  • 时间等待计时器
    • 在终止其间使用的。当TCP关闭一个连接时候,他不认为这个连接就真正的关闭了。在时间等待其间中,连接还处于一种中间过度状态。客户端对服务器的FIN报文段,发送ACK后进行确认后进入TIME_WAIT状态。假定ACK丢失,TIME_WAIT状态使得客户端重传最都的确认报文。经过等待后,连接正式关闭,客户端 所有资源被释放。

网络层

17 如何理解不可靠和无连接

不可靠:指的是不能保证数据报成功的到达目的地。

发生错误的时候,丢弃该数据报包,发送ICMP消息给信源端。可靠性由上层提供。

无连接:IP不维护后续数据报的的状态信息。

IP数据可以不安顺序发送和接收。A发送连续的数据报。达到B不一定是连续的,来回的路由选择也可能不一样。路线不一样,达到的先后顺序也不一样。

18 IP报文的格式和各个字段的含义

  • 版本号:IPV4就是4,ipv6就是6.(4)

  • 首部长度:IPV4可以包含一些可变参数选项,说一需要一个参数指示数据部分从哪里开始,一般IP4数据报具有20个字节的首部。(4)

  • 服务类型:不常用(8)

  • 数据报长度:这个IP数据报的总长度,最多可以传送65535字节的IP数据包。一般不超过1500字节。(16)

  • 标识(16) 、标志(3)、片偏移(13):这三个字段与所谓IP分片有关。

  • 生存时间TTL:经过一个路由器减1。字段为0时,数据报被丢弃,并且发送ICMP报文通知源主机。(8)

  • 协议:区分上层协议(8)

  • 首部校验和:将首部字段和的反码存入该字段(16)

  • 源和目的IP地址(64)

  • 选项:平常用不到

19 介绍一下IP分片

IP之所以分片是因为。不是所有链路层都能承载相同长度的网络分组。例如,以太网帧能承载不超过1500字节的数据。而某些广域网链路智能承载不超过576字节的数据。一个链路能承载的最大数据量叫做最大传输单元(Maxmium Transmission Unit)。每种链路可能采用不同的链路层协议,每种协议可能有不同的MTU。

假如在传输的过程中,收到的IP数据报字节数比我要转发出去的那条链路的MTU还要大,这个时候就需要将IP进行分解,逐个传输。

标识:数据报被拆分称片后具有相同的标识。

标志:最后一个片的标志为0,前边的为1。

偏移:偏移指示该片应该放到初始IP分组的哪个位置。8字节为单位

20 子网掩码有什么用?

子网掩码是一种用来指明一个IP地址所标示的主机处于哪个子网当中,子网掩码不能单独存在,它还必须结合IP地址一起使用。子网掩码只有一个作用,就是将某个IP地址划分成网络部分和主机部分。使得路由更方便,先到达子网,在确定是哪个主机。

21 子网掩码的类型

因特网地址分配策略为无类别区间路由选择(CIDR)。寻址将子网哪个寻址的概念一般化了。对于组网寻址。32比特的IP地址划分成两部分,并且也具有点分十进制格式a.b.c.d/x。x最高比特构成IP地址中的网络部分,在外部路由仅仅需要考虑前x个比特。

具有8,16,24比特子网地址的子网哪个称为A,B,C类地址,能容纳2^24个地址。B类可容纳65534台主机。C类可容纳254台主机。

22 IP首部校验和是怎么计算的

与ICMP,IGMP,TCP,UDP的首部校验和有什么区别与共同点。

(1) 先把校验和字段置 0。
(2) 对首部中每个 16 位比特进行二进制反码求和。
(3) 结果存在检验和字段中。
(4) 收到一份 IP 数据包后,同样对首部中每个 16bit 二进制反码求和。
(5) 最后结果全为 1,表示正确,否则表示错误。
(6) 如果是错误的,IP 就丢弃该数据报,但是不生成差错报文,由上层去处理。
共同点:用到的算法都是一样的。
区别**:IP 计算的时候没有将数据包括在内。**
ICMP,IGMP,TCP,UDP 同时覆盖首部和数据检验码。

23 RIP路由协议(因特网自治系统内部路由选择)

弗洛伊德算法

1 网络中的每一个路由器都要维护他自己到其他每一个目标网络的间距离记录

2 距离又称跳数,规定从一个路由器直接连接的网络跳数为1,而且每经过一个路由器,则距离加一。

3 RIP认为好的路由就是通过的路由器最少;

4 RIP默认一条路径上最多有15个路由器,因此规定最大跳数是16;

5 RIP默认每30秒广播一次RIP更新信息。

每个路由器包括三个内容:目的网络、距离、下一跳路由器。

1、对地址为 X 的路由器发过来的路由表 ,先修改此路由表中的所有项目:把 ” 下一
跳 ” 字段中的地址改为 X ,并把所有 ” 距离 ” 字段都加 1。
2、对修改后的路由表中的每一个项目,进行以下步骤:
2.1、将 X 的路由表(修改过的),与 S 的路由表的目的网络进行对比。
若在 X 中出现,在 S 中没出现,则将 X 路由表中的这一条项目添加到 S 的路由表
中。
2.2、对于目的网络在 S 和 X 路由表中都有的项目进行下面步骤
2.2.1、在 S 的路由表中,若下一跳地址是 x
则直接用 X 路由表中这条项目替换 S 路由表中的项目。
2.2.2、在 S 的路由表中,若下一跳地址不是 x
若 X 路由表项目中的距离 d 小于 S 路由表中的距离,则进行更新。
3、若 3 分钟还没有收到相邻路由器的更新表,则把此相邻路由器记为不可到达路由器,即把距离设置为 16。

24 自治系统间的路由选择BGP

25 ICMP报文的分类

ICMP的层次和作用

UDP端口不可达到例子中返回的ICMP报文

ICMP一般被认为是IP的一部分。从体系结构上位于IP之上,ICMP报文是承载在IP分组中的。主要传递差错报文和其他要注意的信息。

ICMP报文的分类

ICMP分为两类ICMP查询报文,另一类是ICMP差错报文

终点不可达什么情况下发出

路由器给主机寻路时,没有找到相应路径,向源IP发回ICMP主机不可达

什么情况下不会产生ICMP差错报文?

目的地址是广播地址或者多播地址的IP数据报

链路广播的数据报

不是IP分片的第一片

源地址不是单个主机的数据报

ICMP 重定向差错报文是怎么来的,在何种场合出现?

1 主机发送IP数据报给R1,因为主机默认路由指向的下一跳是R1。

2 R1收到数据报检查他的路由表。发现下一跳是R2。当它把数据报发送给R2的时候,发现发送接口与接收该数据报的端口是一样的,因此发送一个ICMP重定向报文给主机。

3 主机接收到ICMP重定向报文,接下来将数据报直接发送给R2,不再发送给R1

重定向报文只能是路由器生成,给主机使用。

26 为什么要有MAC地址

事实上并不是所有的主机或者路由器具有链路层地址,而是他们的适配器(网络接口)具有链路层地址。。因此就会有多个网络接口的主机或者路由器将具有多个链路层地址,也就像他具有与之相关联的IP地址一样。

局域网设计是为任意的网络层协议而设计的,而不只是用于IP和因特网。如果适配器指派的是IP地址而不是MAC地址,则适配器不能方便的支持其他的网络层协议。其次网络层地址必须存储在RAM中,在每次适配器移动的时候要从新配置。用MAC地址和IP地址两个地址,用于分别表示物理地址和逻辑地址是有好处的。这样分层可以使网络层与链路层的协议更灵活地替换,网络层不一定非要用『IP』协议,链路层也不一定非用『以太网』协议。

27 ARP协议

ARP为IP地址得到对应的硬件地址提供动态映射

ARP只能为同一个子网上的主机和路由器解析IP地址。

ARP(地址解析协议),当主机要发送一个IP包的时候,会先查自己的ARP高速缓存表,如果查询的IP-MAC的值不存在,主机向网络广播一个ARP请求,这个包里有等待查询的IP地址,而直接收到这份广播的包的所有主机都会查询自己的IP地址,如果收到广播包的某一个主机发现自己符合条件,那就回应一个ARP应答包,源主机拿到ARP应答包后会更新自己的ARP缓存表。源主机根据ARP缓存表准备好数据链路层的数据报发送工作。

点对点链路使用ARP吗?

不使用

ARP高效运行的关键是什么?

关键是每个主机都有一个ARP的告诉缓存。

ARP的各个字段及含义?

帧类型:ARP:0x0806 (2)
ARP 首部:
硬件类型:硬件地址的类型,1 表示以太网地址。(2)
协议类型:协议地址的类型,0x0800 表示 IP 地址。(2)
硬件地址长度:字节为单位 6 (1)
协议地址长度:字节为单位 4 (1)
操作类型:2 个字节。 ARP 请求 1,ARP 回复 2,RARP 请求 3,RARP 应答 4。(2)
发送者硬件地址:6 个字节(6)
发送者 IP 地址:4 个字节(4)
目标硬件地址:6 个字节(6)
目标 IP 地址:4 个字节(4)
CRC 校验:4 个字节 (4)

总结:
arp 总共 28 个字节。
记忆方法: 以太网先目地后源,ARP 先发送端后目地端。先硬件后协议

ARP协议有什么缺点?

1 缓存:主机的地址映射是基于高速缓存的,动态更新的。地址刷新是有时间限制的。以通过次更新之前修改计算机上的地址缓存,造成拒绝服务攻击或者ARP欺骗。

2 广播:可以伪装成ARP应答

3ARP应答没有认证,都是合法的。可以在不接受到请求的时候发出应答包。

ARP代理的概念?

若ARP请求是一个网络主机发送到另一个网络上的主机。那么连接这两个网络的路由器就可以回答该请求,这个过程叫做ARP代理。ARP代理路由器响应ARP请求的MAC地址为路由器的MAC地址而非ARP请求主机的MAC地址。

28 数据链路层MTU的最大值和最小值分别为多少?

1 数据链路层的最小MTU为64字节。最大为1500字节

 要保证以太网的重传,必须保证A收到碰撞信号的时候,数据包没有传完,要实现这一要求,A和B之间的距离很关键,也就是说信号在A和B之间传输的来回时间必须控制在一定范围之内。IEEE定义了这个标准,一个碰撞域内,最远的两台机器之间的round-trip time 要小于512bit time.(来回时间小于512位时,所谓位时就是传输一个比特需要的时间)。

对于 IEEE802.3,两个站点的最远距离不超过 2500m,
由 4 个中继器连接而成,其冲突窗口为 51.2us(2 倍电缆传播延迟加上 4 个中继器的双向延
迟).对于 10Mbps 的 IEEE802.3 来说,这个时间等于发送 64 字节,即 512 位的时间,64 字
节就是由此而来的。如果一个站点已经传输了 512bit,就认为它已经占用了这个信道。

28:知道各个层使用的是哪个数据交换设备。

(交换机、路由器、网关)

网关:应用层,传输层(网关在传输层以上实现网络互联,是最复杂的网络互联设备,仅仅用于两个高层协议不同的网络互联。)

路由器:网络层(路由选择,存储转发)

交换机:数据链路层、网络层(识别数据包的MAC地址信息,根据MAC地址进行转发,并将这些地址与相应的端口记录在一个表中)

网桥:数据链路层,两个LAN连接起来,根据MAC转发

集线器:物理层设备(主要用来连接计算机等网络终端)

中继器:物理层,在比特级对网络信号进行再生和重定时,使得他们能够在网络上传输更长的距离。

29 Web页面请求过程

1 DHCP配置主机信息

  • 假设最开始主机没有IP地址以及其他信息,那么就需要先使用DHCP来获取。
  • 主机生成一个DHCP请求报文,并把这个报文放入具有目的端口67和源端口68的UDP报文段中。
  • 该报文段被放置在一个具有广播IP目的地址(255.255.255.255)和源IP地址(0.0.0.0)的IP数据报中。
  • 该数据报则被放置在MAC帧中,该帧具有目的地址FF:FF:FF:FF:FF:FF中,将广播到与交换机连接的所有设备。
  • 连接在交换机的DHCP服务器收到广播帧之后,不断向上分解得到IP数据报,UDP报文段,DHCP请求报文,之后生成DHCP ACK报文,该报文含有以下信息:IP地址,DNS服务器的IP地址、默认网关路由器的IP地址和子网掩码。该报文被放入在UDP报文段中,UDP报文段被放入IP数据报中,最后放入MAC帧。
  • 该帧的目的治之是请求主机的MAC地址,因为交换解有自主学习能力,之前主机发送了广播帧之后就记录了MAC地址到其转发接口的交换表项,因此现在交换机就可以直到哪个接口发送该帧。
  • 主机接受到该帧之后,不断分解得到DHCP报文。之后配置他的IP地址,子网掩码和DNS服务器的IP地址,并在其IP转发表中安装默认网关。

2 ARP解析MAC地址

  • 主机通过fulani生成一个TCP套解字,套解字向HTTP服务器发送HTTP请求。为了生成该套解字,主机需要直到网站的域名对应的IP地址。
  • 主机生成一个DNS查询报文,该报文具有53号端口,因为DNS服务器的端口号是53。
  • 该IP数据报被放入一个以太网帧中,该帧发送网关到网关路由器。
  • DHCP过程只知道网关路由器的IP地址,为了获取网关路由器的MAC地址,需要使用ARP协议。
  • 主机生成一个包含目的地址的网关路由器ARP查询报文,将该ARP查询报文放入一个具有广播目的地址(FF:FF:FF:FF:FF:FF)的以太网帧中,向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备,包括网关路由器 。
  • 网关路由器接收到该帧之后,不断向上分解得到ARP回答报文,发现其中IP地址与接口的iP 地址相匹配,因此就发送一个ARP回答报文,包含了他的MAC地址,发回给主机。

3 DNS解析域名

  • 知道了网关路由器的MAC地址之后,就可以继续DNS解析过程了。
  • 网关路由器接收到包含DNS查询报文的以太网帧之后,抽取IP数据报,并根据转发表决定IP数据报应该转发的路由器。
  • 因为路由器具有内部网关协议和外部网关协议这两种路由选择协议,因此路由表中已经配置了网关路由器到达DNS路由表项。
  • 找到DNS记录后,发送DNS回答报文,将该回答报文放入UDP报文段中,然后放入IP数据报中,通过路由器反向转发回网关路由器,经过以太网交换机到达主机。

4 http请求页面

  • 有了HTTP服务器的IP地址之后,主机就能生成TCP套接字,该套接字将用与向WEB服务器发送HTTP GET报文。
  • 生成TCP套接字之前与HTTP服务器进行三次握手来建立连接。生成一个具有目的端口号80的TCP SYN报文段,并向HTTP服务器发送该报文。
  • HTTP服务器接收到该报文在之后,生成TCP SYN ACK报文段,发送给主机。
  • 连接建立后,浏览器生成HTTPGET报文,并交付为HTTP服务器。
  • HTTP服务器从TCP套解字读取HTTP GET报文,生成一个HTTP响应报文,将WEB页面内容放入报文主体中,发送给主机。
  • 浏览器接受到HTTP相应报文之后,抽取WEB页面内容,之后进行渲染,显示WEB页面。

30 常用端口号

FTP 21;TELNET 23;SMTP 25;DNS 53;HTTP 80;HTTPS 443

31 请你来说一下socket编程中服务器端和客户端主要用到哪些函数

1)基于TCP的socket:

1、服务器端程序:

1创建一个socket,用函数socket()

2绑定IP地址、端口等信息到socket上,用函数bind()

3开始监听,设定最大连接数用函数listen()

4接收客户端上来的连接,用函数accept()

5收发数据,用函数send()和recv(),或者read()和write()

6关闭网络连接

2、客户端程序:

1创建一个socket,用函数socket()

2设置要连接的对方的IP地址和端口等属性

3连接服务器,用函数connect()

4收发数据,用函数send()和recv(),或read()和write()

5关闭网络连接

2)基于UDP的socket:

1、服务器端流程

1建立套接字文件描述符,使用函数socket(),生成套接字文件描述符。

2设置服务器地址和侦听端口,初始化要绑定的网络地址结构。

3绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定。

4接收客户端的数据,使用recvfrom()函数接收客户端的网络数据。

5向客户端发送数据,使用sendto()函数向服务器主机发送数据。

6关闭套接字,使用close()函数释放资源。UDP协议的客户端流程

2、客户端流程

1建立套接字文件描述符,socket()。

2设置服务器地址和端口,struct sockaddr。

3向服务器发送数据,sendto()。

4接收服务器的数据,recvfrom()。

5关闭套接字,close()。

32 请你说说select,epoll的区别,原理,性能,限制都说一说

IO多路复用出现的场景,是设计一个高性能的网络服务器,能够供多个客户端同时连接并处理客户端传上来的请求。首先想到的是可以利用多线程,但是多线程存在很大弊端,需要上下文切换,尤其是线程多的时候,线程切换耗费时间。考虑单线程的处理方式。

while(1){
    for(Fdx in (DdA-FDE)){
        if(Fdx 有数据){
            读取Fdx;处理
        }
    }
}

IO多路复用就是我们说的select,poll,epoll。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

I/O多路复用和阻塞I/O其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

1 select

#include <sys/socket.h>

sockfd = socket(AF_INET,SOCK_STREAM,0);
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(socketfd,(struct sockaddr*)&addr,sizeof(addr));
//可以接受5个客户端的连接
listen(sockfd,5);

for(i=0;i<5;i++){
    memset(&client,0,sizeof(client));
    addrlen = sizeof(client);
    // fds就是一个数,代表文件的编号,并不是按照顺序的
    fds[i] = accept(sockfd,(struct sockaddr*)&client,&addrlen);
    //求出文件描述符最大的元素
    if(fd[i] > max)
        max = fd[i];
}

while(1){
    FD_ZERO(&rset);
    for(i=0;i <5;i++){
        // rset数据结构为bitmap,默认1024位,在需要监听的位上置1
        FD_SET(fds[i],&rset);
    }
    puts("round again");
    // 最大文件描述符号加1,读文件描述符,写文件描述符,异常文件描述符,阻塞时间
    select(max+1,&rset,NULL,NULL,NULL);
    for(i=0;i < 5;++i){
        if(FD_ISSET(fds[i],&rset)){
            memset(buffer,0,MAXBUF);
            //读fd
            read(fds[i],buffer,MAXBUF);
            //处理
            puts(buffer);
        }
    }
}

select:是最初解决IO阻塞问题的方法。将文件描述符收集起来,用结构体fd_set来告诉内核监听多个文件描述符,该结构体被称为描述符集。由bitmap,默认1024位来维持哪些描述符被置位了。对结构体的操作封装在三个宏定义中。通过轮寻来查找是否有描述符要被处理。

当有数据来的时候select返回:1将相应的fd置位2将select返回

存在的问题:

  1. 内置数组的形式使得select的最大文件数受限与FD_SIZE;

  2. 每次调用select前都要重新初始化描述符集,将fd从用户态拷贝到内核态,每次调用select后,都需要将fd从内核态拷贝到用户态;

  3. 轮寻排查当文件描述符个数很多时O(n),效率很低;

  4. 从用户态到内核态切换有开销

2 poll

工作原理和select相似,将用户态的文件描述符拷贝到内核态,让内核态监听fd上的数据。


struct pollfd{
    //文件描述符
    int fd;
    //在意的事件
    short events;
    //一开始为0,
    short revents;
}
for(i=0;i<5;i++){
    memset(&client,0,sizeof(client));
    addrlen = sizeof(client);
    // fds就是一个数,代表文件的编号,并不是按照顺序的
    pollfds[i].fd = accept(sockfd,(struct sockaddr*)&client,&addrlen);
    //模式为输入
    pollfds[i].events = POLLIN;
}

while(1){
    puts("round again");
    //pollfd的数组,将revent置位为1,数据来了
    poll(pollfds,5,5000);
    for(int i=0; i<5;++i){
        if(pollfds[i].revents & POLLIN){
            pollfds[i].revents=0;
            memset(buffer,0,MAXBUF);
            read(fds[i],buffer,MAXBUF);
            puts(buffer);
        }
    }
}

通过一个可变长度的数组解决了select文件描述符受限的问题。数组中元素是结构体,该结构体保存描述符的信息。每增加一个文件描述符就向数组中加入一个结构体。

poll解决了select重复初始化的问题。每次只要将结构体中的一位revent置位即可。

3 epoll

struct epoll_event events[5];
int epfd = epoll_create(10);

for(i=0;i<5;i++){
    static epoll_event ev;
    memset(&client,0,sizeof(client));
    addrlen = sizeof(client);
    addrlen - sizeof(client);
    ev.data.fd = accept(sockfd,(struct sockaddr*)&client,&addrlen);
    ev.events = EPOLLIN;
    epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd.&ev);
}

while(1){
    puts("round again");
    nfds = epoll_wait(epfd,events,5,1000);
    for(int i=0; i<nfds;++i){
        memset(buffer,0,MAXBUF);
        read(fds[i],buffer,MAXBUF);
        puts(buffer);
    }
}   

节省了数据结构从用户态到内核态的开销

有数据:1“置为”(重排),有数据的fd重排到epfd的最前 2返回相应有数据fd的个数

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式

  1. LT模式

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket**.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。**

  1. ET模式

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

3、LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

33 请你回答一下epoll怎么实现的

Linux epoll机制是通过红黑树和双向链表实现的。 首先通过epoll_create()系统调用在内核中创建一个eventpoll类型的句柄,其中包括红黑树根节点和双向链表头节点。然后通过epoll_ctl()系统调用,向epoll对象的红黑树结构中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。最后通过epoll_wait()系统调用判断双向链表是否为空,如果为空则阻塞。当文件描述符状态改变,fd上的回调函数被调用,该函数将fd加入到双向链表中,此时epoll_wait函数被唤醒,返回就绪好的事件。

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

首先创建一个epoll对象,然后使用epoll_ctl对这个对象进行操作,把需要监控的描述添加进去,这些描述如将会以epoll_event结构体的形式组成一颗红黑树,接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把其对应的结构体放入到一个链表中,返回有事件发生的链表。

34 TCP SYN洪泛攻击的防御

SYN洪泛攻击的基础是依靠TCP建立连接时三次握手的设计。第三个数据包验证连接发起人在第一次请求中使用的源IP地址上具有接受数据包的能力,即其返回是可达的。

TCB(TCP 传输控制块)是一种包含一个连接所有信息的传输协议数据结构(实际上在许多操作系统中它是用于处理进站(inbound)连接请求的一个队列,该队列保存那些处于半开放(half-open)状态的TCP连接项目,和已建立完整连接但仍未由应用程序通过accept()调用提取的项目)。一个单一的TCB所占内存大小取决于连接中所用的TCP选项和其他一些功能的实现。通常一个TCB至少280字节,在某些操作系统中已经超过了1300字节。TCP的SYN-RECEIVED状态用于指出这个连接仅仅是半开连接,请求是否合法仍被质疑。这里值得注意的一个重要方面就是TCB分配空间的大小取决于接收的SYN包——在连接被完全建立或者说连接发起人的返回可达性被证实之前。

这就导致了一个明显潜在的DoS(拒绝服务)攻击,到达的SYN包将被分配过多的TCB而导致主机的内核内存被耗尽。为了避免这种内存耗尽,操作系统通常给监听接口关联了一个”backlog”队列参数,它同时维护连接的TCB上限数量和SYN-RECEIVED状态。尽管这种方案使主机的可用内存免遭攻击,但是backlog队列本身就带来了一个(小的)受攻击源。当backlog中没有空间时,就不可能再响应新的连接请求,除非TCB能被回收或者从SYN-RECIEVE状态中移除。

试图发送足够多的SYN包而耗尽backlog是TCP SYN洪泛的目的。攻击者在SYN包中加入源IP地址,这样就不会导致主机将已分配的TCB从SYN-RECEVIED状态队列中移除(因为主机将响应SYN-ACK)。因为TCP是可靠的,目的主机在断开半开连接并在SYN-RECIEVED队列中移除TCB之前将等待相当长的时间。在此期间,服务器将不能响应其他应用程序合法的新TCP连接请求。

攻击类型

1 直接攻击

如果攻击者用他们自己的没有经过伪装的IP地址快速地发送SYN数据包,这就是所谓的直接攻击。这种攻击非常容易实现,因为它并不涉及攻击者操作系统用户层以下的欺骗或修改数据包。例如,他可以简单地发送很多的TCP连接请求来实现这种攻击。然而,这种攻击要想奏效攻击者还必须阻止他的系统响应SYN-ACK包,因为任何ACK、RST或ICMP(Internet Control Message Protocol)包都将让服务器跳过SYN-RECEIVED状态(进入下一个状态)而移除TCB(因为连接已经建立成功或被回收了)。攻击者可以通过设置防火墙规则来实现,让防火墙阻止一切要到达服务器的数据包(SYN除外),或者让防火墙阻止一切进来的包来使SYN-ACK包在到达本地TCP处理程序之前就被丢弃了。

一旦被检测到,这种攻击非常容易抵御,用一个简单的防火墙规则阻止带有攻击者IP地址的数据包就可以了。这种方法在如今的防火墙软件中通常都是自动执行的。

2 欺骗式攻击

SYN洪泛攻击的另一种方式是IP地址欺骗。它比直接攻击方式更复杂一点,攻击者还必须能够用有效的IP和TCP报文头去替换和重新生成原始IP报文。如今,有很多代码库能够帮助攻击者替换和重新生成原始IP报文。

对于欺骗式攻击,首先需要考虑的就是选择地址。要使攻击成功,位于伪装IP地址上的主机必须不能响应任何发送给它们的SYN-ACK包。攻击者可以用的一个非常简单的方法,就是仅需伪装一个源IP地址,而这个IP地址将不能响应SYN-ACK包,或许因为这个IP地址上根本就没有主机,或许因为对主机的地址或网络属性进行了某些配置。另一种选择是伪装许多源地址,攻击者会假想其中的一些伪装地址上的主机将不会响应SYN-ACK包。要实现这种方法就需要循环使用服务器希望连接的源IP地址列表上的地址,或者对一个子网内主机做相同的修改。

如果一个源地址被重复地伪装,这个地址将很快被检测出来并被过滤掉。在大多数情况下运用许多不同源地址伪装将使防御变得更加困难。在这种情况下最好的防御方法就是尽可能地阻塞源地址相近的欺骗数据包

假设攻击者是在一个“互联”的网络中(例如一个自治系统(Autonomous System)),由其ISP限制攻击者所在网络流量的输入输出过滤将能够制止这种欺骗攻击——如果这种方法能被机构部署到正确位置的话。这种流量输入输出过滤的防御方式将限制一些合法的通信,比如移动IP三角路由运作模式,因此不可能被普遍部署。IP安全协议(IPsec)同样也提供了一种抵御欺骗包的优秀方式,但是这协议因为部署限制还不能被使用。由于对于服务器方通常不可能要求链接发起人的ISP去实施地址过滤或者要求其使用IP安全协议,因此抵御这种用多地址伪装的欺骗攻击还需要更加复杂的解决方案,将在后文讨论到。

3 分布式攻击

对于单个运用欺骗式攻击的攻击者真正的限制因素是如果这些伪装数据包能够以某种方式被回溯到其真正的地址,攻击者将被简单地击败。尽管回溯过程需要一些时间和ISP之间的配合,但它并不是不可能的。但是攻击者运用在网络中主机数量上的优势而发动的分布式SYN洪泛攻击将更加难以被阻止。如图所示,这些主机群可以用直接攻击,也可以更进一步让每台主机都运用欺骗攻击。

如今,分布式攻击才是真正可行的,因为罪犯拥有数以千计的主机供他来进行拒绝服务(DoS)攻击。由于这些大量的主机能够不断地增加或减少,而且能够改变他们的IP地址和其连接,因此要阻止这类攻击目前还是一个挑战。

对策

  • 增加TCP backlog队列

由于其基本攻击原理是依赖于终端主机连接套接字的backlog溢出,因此一个显然的基于终端主机的解决方案是增加backlog队列大小,而且这种方法已经广泛的运用于大多数服务器了。增加backlog队列通常是通过修改应用的listen()函数调用和一个操作系统内核参数SOMAXCONN——它用于设置一个应用程序能够接收的backlog上限值。这种方法本身并不能被完全认为是抵御SYN洪泛的有效方法,即使在一些能够有效支持超大backlog队列分配的操作系统中,因为攻击者能够任意生成比其操作系统支持的backlog还大得多的数据报。

  • 减少SYN-RECEIVED的时间

管理员减少SYN-RECEIVED状态时间多少和攻击者的发包率之间仅仅是一个线性关系而已。基于上述原因,此方案并不建议采用。

  • 设置SYN cookie

对比SYN缓存的方法,SYN Cookies技术做到了接收到一个SYN时完全不需要分配空间。因为构成连接状态的最基本数据都被编码压缩进SYN-ACK的序列号比特位里了。对于一个合法连接,服务器将收到一个带有序列号(其实序列号已经加1)的ACK报文段,然后基本的TCB数据将被重新生成,一个完整的TCB通过解压确认数据将被安全的实例化。这种解压即使在重度攻击下仍然能够成功,因为在主机端根本没有任何存储负载,只有计算编码数据到ACK序列号中的负载。其不足之处就是,不是所有的TCB数据都能添加到32位的序列号段中,所以一些高性能要求的TCP选项将不能被编码。其另一个问题是这样的SYN-ACK报文段将不能被转发(因为转发需要完整的状态数据)。

Andre Oppermann最近的一些研究是运用TCP时间戳选项结合序列号字段编码更多的状态信息,保存那些高性能选项的应用,比如TCP窗口大小,TCP选择性确认选项(TCP Selective Acknowledgment Options )以及TCP MD5摘要对SYN cookies的支持。这使SYN Cookies向前迈进了一步,他消除了之前SYN cooikes实现不能支持这些功能的缺陷。

TCP SYN cookies 的规范格式并不涉及互操作性问题,因为它们仅在本地被处理,对于生成和验证的规范和过程在不同实现中会稍有不同。

6.4.1 SYN cookies的生成

为了在使用SYN cookies时计算出SYN-ACK序列号(就是SYN cookies),主机首先要结合一些本机的密码比特,一个包括IP地址和TCP端口号的数据结构,SYN初始序列号,和一些标识密码比特的索引数据。在所有上述字节之上生成一个MD5摘要,然后一些比特位从hash值里被截断以便将其放入到SYN-ACK序列号中。由于序列号的大小大约是全部hash值的四分之一,因此这种截断是必要的,但是通常验证的时候至少要用3字节大小的hash比特位,这意味着在不知道密码比特位的情况下仍然有将近2^24种可能性去猜测验证cookies。为了将hash值发送出去,cookies的一些比特位将使SYN包含的MSS(最大报文段长度)的上限值变小,并影响在hash值中标识本机密码位的索引位。

6.4.2 SYN cookies的验证

为了验证SYN cookies,首先要将收到的ACK报文段中的确认号减1以便重新生成SYN cookies。对于这些已被截断过的hash位验证值的计算是基于双方IP地址,端口号,序列号减1和与cookie中索引号对应的密码池中的值。如果被计算出来的这些值与cookie中的值相同,此时TCB才初始化并开始建立连接。被编码的MSS上界被用来设置一个不超过最初值的合理大小的MSS值。MSS通常由3个比特位来实现,这三个比特位对应8个由一般链路的MTU(最大传输单元)和以太网头部计算出的MSS值。

  • 设置SYN可疑队列
    根据攻击程序伪造IP地址的方法。将SYN Flood攻击大致分成以下几种情况: (1)攻击者使用单一的虚假IP地址进行攻击。在这种情况下。攻击者向被攻击主机发送的所有攻击包的源IP地址都是同一个虚假的IP地址。 (2)攻击者使用一组虚假的IP地址进行攻击。这种情况比上一种攻击情况有了改进,进行攻击时。攻击者先生成一组IP地址,再将这些IP地址依此添加到成的每个攻击包的源TP地址中。 (3)攻击者使用一个网段范围的地址进行攻击。这是攻击者使用较普遍,也是攻击成功率较高的一种攻击情况。进行攻击时,攻击者精心选择一个地址分配比较稀疏的网段,通常会选择一个B类网段。 针对以上几种情况,可采用对接收的数据按一定规则加以分类,将数据分为攻击数据、可疑数据和正常数据三种类型,然后分别排队处理。
    首先,对同一IP地址在短时间内发送大量重复连接请求时,可将其作为攻击数据直接丢弃,其次,对于一组IP地址在短时间内发送大量重复连接请求时,将其放入可疑数据队列,当某个IP地址发出的连接请求数大于某一阈值时.即作为攻击数据进行丢弃处理:若收到某个IP地址发送的应答,则将其转入正常数据队列。最后,对于使用一个网段范围的地址进行攻击的,即服务器在短时间内收到某个网络地址内不同主机发出大量的连接请求,此时仍将其放入可疑数据队列,对于此网段中正常上网的主机当收到服务器重复的SYN+ACK数据包时。就向服务器发送一个RST数据包,从而结束半连接状态;对于此网段中没有开机或上网的主机。即虚假源IP地址。为防止堆栈溢出崩溃,可不按通常的运行方式为其分配相应的数据结构和存储状态信息,只向源IP发送确认信息,将TCP连接保持一种无状态的握手方式巾,直到超时后将其丢弃或者收到源IP的连接确认,再将其转入正常数据队列,并分配相应的数据结构和存储状态信息。当服务器负载较重时,首先丢弃可疑数据队列中的数据,然后冉随机地丢弃正常数据队列中的数据,确保服务器的正常运行。

  • 使用防火墙
    防火墙位于客户端和服务器之间.因此利用防火墙来阻止DoS攻击能有效地保护内部的服务器。针对SYN Flood,防火墙通常有三种防护方式:SYN网关、被动式SYN网关和SYN中继。
    (1)SYN网关: 防火墙收到客户端的SYN包时,直接转发给服务器:防火墙收到服务器的SYN/ACK包后,一方面将SYN/ACK包转发给客户端,另一方面以客户端的名义给服务器回送一个ACK包.完成TCP的三次握手,让服务器端由半连接状态进入连接状态。当客户端真正的ACK包到达时。有数据则转发给服务器,否则丢弃该包。由于服务器能承受连接状态要比半连接状态高得多。所以这种方法能有效地减轻对服务器的攻击。
    (2)被动式SYN网关: 设置防火墙的SYN请求超时参数,让它远小于服务器的超时期限.防火墙负责转发客户端发往服务器的SYN包,服务器发往客户端的SYN/ACK包、以及客尸,端发往服务器的ACK包。这样,如果客户端在防火墙计时器到期时还没发送ACK包,防火墙则往服务器发送RST包,以使服务器从队列中删去该半连接。由于防火墙的超时参数远小于服务器的超时期限,出此这样能有效防止SYN Flood攻击.
    (3)SYN中继: SYN中继防火墙在收到客户端的SYN包后.并不向服务器转发而是记录该状态信息后主动给客户端回送SYN/ACK包,如果收 到客户端的ACK包。表明是正常访问,由防火墙向服务器发送SYN包并完成三次握手。这样由防火墙作为代理来实现客户端和服务器端的连接。可以完全过滤不可用连接发往服务器。

  • 混合方式

混合方式将上述的两种或更多防御方法联合起来使用。例如,一些终端机操作系统同时实现了一个超大backlog队列和SYN cookies,但是仅仅当backlog的大小超过一定阈值时SYN cookies才被使用,这样就能在不涉及SYN cookies缺点的情况下正常使用,也允许在遭受攻击时转移到SYN-cookies防御。

35 time_wait 状态

tcp断开连接之前要经历time_wait状态。时间为最长报文段生命期(maximum segment lifetime,MSL)的两倍,有时称为2MSL。

大概在1分钟到4分钟之内。MSL是任何IP数据报能在因特网中存活的最长时间。这个时间是有限制的,因为一个数据报中含有一个称为跳限的字段8位,理论上TTL最大值可以设置为255,假设具有最大跳限的分组在网络中存在的时间不可能超过MSL秒。

分组在网络中迷途通常是路由异常的结果。某个路由器崩溃或者,某两个路由器之间的某个链路断开,路由协议可能需要数秒到数分钟时间才能稳定并找出另一条通路。在这段时间内可能发生路由循环。迷途其间,TCP超时重传该分组,而重传分组,重传分组通过候选路径达到目的地,不久后早先迷失的分组也到达目的地,这就皎迷途重复分组或漫游重复分组。TCP必须正确处理这些重复的分组。

Time_WAIT状态的两个存在理由

1 可靠地实现TCP全双工连接的终止

2 允许老的重复分节在网络中消逝

1 主动关闭那端是需要TIME_WAIT,客户端最终的ACK丢失了,服务器将重新发送他的最终的哪个FIN,因此可序必须维护状态信息,以允许重新发送最终那个ACK。要是客户端不维护状态信息,他将响应一个RST,该分节将被服务器解释成一个错误。如果TCP打算执行所有必要的工作以彻底终止某个连接上方向的数据流,那么他必须正确处理连接终止序列的4个分节中任何一个分节丢失的情况。

2 假设服务器和客户端在某个端口建立连接,我们关闭这个连接,过一段时间两个端口之间建立另一个连接。后一个连接称为前一个连接的化身,因为他们的IP和端口号都相同。TCP必须防止来自某个连接的老的重复分组在该连接已经终止之后重复出现。,从而被wurenwi属于统一连接的某个新的化身。time_wait状态的持续时间是MSL的两倍,就足以让某个方向上的分组最多存活MSL秒被丢弃,另一个方向也最多MSL秒。

36 为何Reset报文不需要ACK确认?

因为发送Reset报文的一端,在发送完这个报文之后,和该TCP Session有关的内存结构体瞬间全部释放,无论对方收到或没有收到,关系并不大。

  • 如果对方收到Reset报文,也会释放该TCP Session 的相关内存结构体。
  • 如果对方没有收到Reset 报文,可能会继续发送让接收方弹射出Reset报文的报文,到最后对方一样会收到Reset 报文,并最终释放内存。
  • 37

37,ip地址局域网到外网的转换

假如电脑A想要访问百度,百度的IP我们假设为:172.168.30.3:
我们都知道,电脑A的IP是虚构的,实际上可能并不存在这样一个IP,如果用电脑A的IP去访问百度,那肯定行不通。

  由于百度和电脑A不在一个局域网内,所以A要访问百度,那么必须得经过网关。而网关的这个IP地址,是真实存在的,是可以访问百度的。

  为了让 A 可以访问百度,可以采取这样的方法:让网关去帮助 A 访问,然后百度把结果传递给网关,而网关再把结果传递给 A。

  不过电脑A、B、C都可能拜托网关去帮忙访问百度,而百度返回的结果 的目的IP都是网关的IP=192.168.1.1。那么网关该如何进行区分这结果是A的、B的还是C的呢?

  我们去访问百度的时候,是需要指定一个端口,我们可以把 A的IP + 端口 映射成 网关的IP+端口,就可以唯一确定身份了。

  例如A用端口60去访问百度,网关把 A的IP+端口60 映射成 网关的IP+端口80 。

百度把结果返回给网关的80端口之后,网关再通过映射表,就可以把结果返回给 A的60端口 了。

  如果B也是用60端口去访问百度的话,也是一样,可以把它映射到90端口。

  这种方法地址的映射转换,也称之为网络地址转换,英文为 Network Address Translation,简称NAT。

  像A、B、C这样的IP地址就称之为内网IP;而像网关,百度这样的IP称之为外网IP(即互联网公网IP)。

  为了解决IP地址短缺,技术专家们发明了内网技术,而内网技术的理论支撑就是NAT技术。

数据结构

1 说一下平衡二叉树和红黑AVL树的定义特点,二者区别

  • 平衡二叉树又称AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。

  • 红黑树:一种二叉查找树,在每一个节点内增加一个存储位表示节点颜色。通过对根到叶子节点染色的限制,红黑数确保没有一条路径会比其他路径长出两倍。因此红黑树是一弱平衡的二叉树。对于要求严格的AVL树来说,它在插入和删除时旋转次数少,所以对于插入删除操作较多的情况下。通常使用红黑树。插入最多旋转两次,删除最多三次旋转.

    • 性质:每个节点非红即黑
    • 根节点是黑色的
    • 如果一个节点是红色的它的子节点必须是黑色的
    • 每个叶子节点是黑色的(并定义NULL为黑色的)
    • 对于任何节点而言,从一个节点到NULL指针的每一条路径上都包括相同数目的黑节点

红黑树旋转:

有左旋和右旋两种旋转,通过改变树中某些结点的颜色以及指针结构来保持对红黑树进行插入和删除操作后的红黑性质。

左旋:

对某个结点x做左旋操作时,假设其右孩子为y而不是T.nil:以x到y的链为“支轴”进行。使y成为该子树新的根结点,x成为y的左孩子,y的左孩子成为x的右孩子。

右旋:对某个结点x做右旋操作时,假设其左孩子为y而不是T.nil:以x到y的链为“支轴”进行。使y成为该子树新的根结点,x成为y的右孩子,y的右孩子成为x的左孩子。

2 介绍一下B树

B+树是一种多路搜索树,主要为磁盘存储设备设计的平衡查找树。当数据结构必须放在磁盘上的时候大O模型不再适用,几乎在所有情况下,控制运行时间的都是访问磁盘的次数。想要把磁盘访问次数变小,将树的高度降低即可减少对磁盘的访问次数,一个M阶B+树具有以下特性

  • 数据存储在树叶上
  • 非叶节点内存储至多M-1个关键字指示搜索方向;关键字i表示子树i+1(一整颗树)中的最小关键字
  • 树的根节点,儿子数在2-M。
  • 除了根外,非叶节点的儿子数在M/2-M之间。
  • 所有叶子节点都在相同的深度上
  • 访问次数近似为$\log_{m/2}N$

为什么根节点可以最少两个儿子?

插入节点时,如果叶子节点或者非叶子节点满了,就会发生拆分,如果父节点儿子数量达到限制,就会沿着树向上拆分,如果拆到根节点了,就会产生两个根节点,这是不可以接受的,这个时候就会新建一个节点,将两个树根作为这个根的两个儿子。

删除节点时,不够向邻居借用。

B+树优点,都要查询叶子节点,查询性能稳定,所有叶子节点形成有序列表,便于范围查询。

2.1为什么用B树而不用二叉查找树啊?为什么不用哈希表?

哈希表虽然能够进行O(1)查找到目标数据,不过如果我们要进行模糊查找的话,只能遍历所有的数据,并且如果出现极端情况,哈希冲突的元素过多,也会导致线性查找效率。

为什么不用二叉树呢?

如果是查找效率,即比较次数的话,二差树确实是最快的,但是我们的文件系统是放在磁盘上的,可能我们执行几亿条指令的时间只能进行几次的磁盘加载。所以我们不仅要考虑查找效率,还要考虑磁盘的寻址加载次数,这就是我们要考虑使用B树的原因。

我们知道,在把磁盘里的数据加载到内存中的时候,是以为单位来加载的,而我们也知道,节点与节点之间的数据是不连续的,所以不同的节点,很有可能分布在不同的磁盘页中。

而对于 B 树,由于 B 树的每一个节点,可以存放多个元素,所以磁盘寻址加载的次数会比较少。

2.2 B树和B+树的区别

B+树是对B树进行了改造,他的数据都存放在叶子节点上,同时叶子节点之间还加了指针形成链表。

如果要选择数据库上的多条数据,不是一条一条选择,如果要多条访问,B树就需要做局部的中序遍历,可能要跨层访问而B+树由于所有的数据都在叶子节点上,不用跨层,同时又具有链表结构,只需要找到首尾,通过链表就可以把所有的数据取出来了。

3 请你说一说map和unordered_map的底层实现

map底层是基于红黑树实现的,因此map内部元素排列是有序的。而unordered_map底层则是基于哈希表实现的,因此其元素的排列顺序是杂乱无序的。

对于map,其底层是基于红黑树实现的,优点如下:

1)有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作

2)map的查找、删除、增加等一系列操作时间复杂度稳定,都为logn

缺点如下:

1)查找、删除、增加等操作平均时间复杂度较慢,与n相关

对于unordered_map来说,其底层是一个哈希表,优点如下:

查找、删除、添加的速度快,时间复杂度为常数级O(c)

缺点如下:

因为unordered_map内部基于哈希表,以(key,value)对的形式存储,因此空间占用率高

Unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O(c),取决于哈希函数。极端情况下可能为O(n)

4 TOPK问题

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

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

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

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

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

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

3、最小堆法

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

4、分治法

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

5、Hash法

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

5 hash

hash 用在什么地方,解决 hash 冲突的几种方法?负载因子?

  1. 如何构造哈希函数
    a) 数字分析法;
    b) 平方取中法;
    c) 除留余数法;
  2. 处理冲突
  • 分离链式法,将散列到同一值的元素保留在一个链表中。新元素被插在链表前端。
  • 线性探测法(一般负载因子小于0.5)
    • 发生冲突时,顺序查找下一节点,直到找出一个空单元
    • 问题:占据的位置会形成一个连续的区块,再次散列到这个区块的任何关键字都要试很多单元才能解决冲突。(聚集问题)
  • 平方探测法
    • 解决聚集问题,要求表的大小是素数,冲突函数是二次的f(i) = i^2,冲突发生时,探测1,2,4,9.。。位置上是否冲突(也会产生二次聚集,散列到相同单元探测相同元素)
  • 伪随机探测
    • 令线性探测的步长从常数改为随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。
  • 双散列
    • 就是使用哈希函数取散列一个输入的时候,探测hash2(x),2*hash2(x)…

空间不够了,如超过70%的单元是满的,就新建一个表,将原来的表扩大两倍后的第一个素数。重新插入。

负载因子

散列表元素和散列表大小的比例。

如果负载因子是默认的 0.75,HashMap(16)的时候,占 16 个内存空间,实际
上只用到了 12 个,超过 12 个就扩容。
如果负载因子是 1 的话,HashMap(16)的时候,占 16 个内存空间,实际上会
填满 16 个以后才会扩容。增大负载因子可以减少 hash 表的内存,如果负载因子
是 0.75,hashmap(16)最多可以存储 12 个元素,想存第 16 个就得扩容成
32。如果负载因子是 1,hashmap(16)最多可以存储 16 个元素。同样存 16 个
元素,一个占了 32 个空间,一个占了 16 个空间的内存。

6 哈夫编码的原理和作用

哈夫曼编码是哈夫曼树的一种应用,广泛用于数据文件压缩。哈夫曼编码算法用字符在文件中出现的频率来建立0,1编码最优的表示个字符,使得生成文件的总比特数降低。

各个字符的编码通过建立哈夫曼树来确定,构建哈夫曼树。首先计算每个字符出现的次数。以权值作为根节点构建n棵二叉树,组成森林。在森林中选出两个根节点最小的树合并,作为一棵新树的左右子树,且新树的根节点作为其左右子树根节点的合并。从森林中删除刚选的两颗树,并将新树加入森林,重复上述步骤直到森林中只剩下一棵树为止。

通过从哈夫曼树到根节点的路径经历的路径就可以确定该字符的哈夫曼编码了。根节点向左子树走记0,向右子树走记1。n个权值构建的哈夫曼树有n个叶子节点了,每个哈夫曼编码都不是另一个哈夫曼编码的前缀。这样就保证了在解析哈夫曼编码的过程中不会产生歧义。哈夫曼树是带权路径最短的树(这也就代表了最终哈夫曼编码的总长度最小),树中所有叶子节点的权值乘上其到根节点的路径长度与最终的哈夫曼编码总长度成正比关系。权值小的路径长,权值大的路径短。

数据库问题

1 聊一聊事务

在 MySQL 中,事务其实是一个最小的不可分割的工作单元。事务能够保证一个业务的完整性

事务具有4个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称ACID。

比如我们的银行转账:在数据库中要执行两条语句,一个用户A加钱,一个用户B减钱,这就是一个不可分割的事务,要么两条语句同时成功,要么两条语句同时失败,这就体现了原子性质。如果只有一条执行成功就会出现数据前后不一致的现象。假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。另外事务还有一个性质是隔离性,通常来说,隔离性是当多个用户并发访问数据库的时候,执行自己的事务的时候不能被其它事务执行所干扰。通常来说,一个事务所做的修改在最终提交以前,对其他事务是可不见的。

隔离性也分为各种级别:

Read Uncommitted(读取未提交):最低的隔离级别,什么都不需要做,如果有多个事务,那么任意事务都可以看见其他事务的未提交数据。这样会导致脏读显现,A开启一个事务操作操作转账给B之后未提交,B一方查询到帐了,A回滚,B钱没了。

Read Committed(读取提交内容):只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。但是依然出现问题,一个用户对表多次读的时候,发现读取到的东西不一致,产生困惑,这也叫不可重复读现象。

Repeated Read(可重复读):在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。这就可能产生幻读现象,小张,和小王同时开启事务对表进行操作,小张插入一条数据,无论小张是否执行过 COMMIT ,在小王这边,都不会查询到小张的事务记录,而是只会查询到自己所处事务的记录,小王插入一条主键相同的数据,提示错误,冗余插入,给小张带来很大困惑,我这边查询没有这条数据啊。幻读,一个事务提交的数据,不能被其他事务读取到。**

Serialization(可串行化):事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。串行化的意思就是:假设把所有的事务都放在一个串行的队列中,那么所有的事务都会按照固定顺序执行,执行完一个事务后再继续执行下一个事务的写入操作 ( 这意味着队列中同时只能执行一个事务的写入操作 ) 。小张写数据的时候我这边堵塞,等小张写完数据了,小王才能进行操作。

事务的最后一条性质,持久性,持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

2 数据库的三大范式

第一范式:数据表中的所有字段都是不可分割的原子值。只要字段值还可以继续拆分,就不满足第一范式。

范式设计得越详细,对某些实际操作可能会更好,但并非都有好处,需要对项目的实际情况进行设定。

第2范式:必须在满足第一范式的前提下,其他列都必须完全依赖于主键列。如果出现不完全依赖,只可能发生在联合主键的情况下:

第三范式:必须在满足第二范式的前提下,除了主键列之外,其他列之间不能有传递依赖关系。

我的理解,尽量将字段拆分成原子值,除了与主键字段外,每个字段之间关系都是正交的。减少冗余,新增,修改,删除变少。

对查询不友好了。

3 inner join和left join

left join(左联接) 返回包括左表中的所有记录和右表中联结字段相等的记录 right join(右联接) 返回包括右表中的所有记录和左表中联结字段相等的记录
inner join(等值连接) 只返回两个表中联结字段相等的行

4 索引

数据库索引是为了增加查询速度而对表字段附加的一种标识,是对数据库表中一列或多列的值进行排序的一种结构。首先明白为什么索引会增加速度,DB在执行一条Sql语句的时候,默认的方式是根据搜索条件进行全表扫描,遇到匹配条件的就加入搜索结果集合。如果我们对某一字段增加索引,查询时就会先去索引列表中一次定位到特定值的行数大大减少遍历匹配的行数,所以能明显增加查询的速度。

优点:

通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。

可以大大加快数据的检索速度,这也是创建索引的最主要的原因。

可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。

在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间

缺点:

创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。

索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。

当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。

MyISAM索引实现

MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。主键索引的key不可以重复,而辅助索引的key可以重复。MyISAM 的索引方式也叫做“非聚集索引”

InnoDB索引实现

InnoDB也是使用B+Tree作为索引结构,但是具体实现方式却与MyISAM截然不同。

1 InnoDB的数据文件本身就是索引文件。数据文件本身按照B+Tree组织的一个索引结构,这棵树的叶子节点data域完整保存数据记录。这个索引的key是数据表的主键。这种索引叫做聚集索引。因为 InnoDB 的数据文件本身要按主键聚集。

同时,请尽量在 InnoDB 上采用自增字段做表的主键。因为 InnoDB 数据文件本身是一棵B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持 B+Tree 的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。

2 .第二个与 MyISAM 索引的不同是 InnoDB 的辅助索引 data 域存储相应记录主键的值而不是地址。

聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索**辅助索引获得主键,**然后用主键到主索引中检索获得记录。

因为辅助索引存储的数据是主键索引的key,这样就不建议主键过长,主键过长会导致辅助索引变的过大。

添加索引原则

查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。

只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。

定义为text、image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少。

当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。

使用索引的注意事项

使用索引时,有以下一些技巧和注意事项:

  • 索引不会包含有NULL值的列

只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。

  • 使用短索引

对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的列,如果在前10个或20个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。

  • 索引列排序

MySQL查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。

  • like语句操作

一般情况下不鼓励使用like操作,如果非使用不可,如何使用也是一个问题。like “%aaa%” 不会使用索引而like “aaa%”可以使用索引。

  • 不要在列上进行运算

select * from users where YEAR(adddate)<2007;
将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成

select * from users where adddate<‘2007-01-01’;

  • 不使用NOT IN和<>操作

5 数据库引擎, innodb 和 myisam 的特点与区别

  1. Innodb 引擎提供了对数据库 ACID 事务的支持,并且实现了 SQL 标准的四种隔离级别,该引擎还提供了行级锁和外键约束,**它的设计目标是处理大容量数据库系统*,它本身其实就是基于 MySQL 后台的完整数据库系统,MySQL 运行时 Innodb 会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎不支持 FULLTEXT 类型的索引,而且
    它没有保存表的行数,**当 SELECT COUNT( * ) FROM TABLE 时需要扫描全表。*
    当需要使用数据库事务时,该引擎当然是首选。由于**锁的粒度更小,写操作不会锁定全表,**所以在并发较高时,使用 Innodb 引擎会提升效率。但是使用行级锁也不是绝对的,如果在执行一个 SQL 语句时 MySQL 不能确定要扫描的范围,InnoDB 表同样会锁全表。

  2. MyIASM没有提供对数据库事务的支持,也不支持行级锁和外键,因此当 INSERT(插入)或 UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。不过和 Innodb 不同*,MyIASM 中存储了表的行数**,于是
    SELECT COUNT(
    ) FROM TABLE 时只需要直接读取已经保存好的值而不需要进行全表扫描。如果表的读操作远远多于写操作且不需要数据库事务的支持,那MyIASM 也是很好的选择。

  3. 大尺寸的数据集趋向于选择 InnoDB 引擎,因为它支持事务处理和故障恢复。数据库的大小决定了故障恢复的时间长短,InnoDB 可以利用事务日志进行数据恢复,这会比较快。主键查询在 InnoDB 引擎下也会相当快,不过需要注意的是如果主键太长也会导致性能问题。大批的 INSERT 语句(在每个INSERT 语句中写入多行,批量插入)在 MyISAM 下会快一些,但是 UPDATE 语句在 InnoDB 下则会更快一些,尤其是在并发量大的时候。

InnoDB和Mylsam的区别:

1)事务:MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持,提供事务支持已经外部键等高级数据库功能。

2)性能:MyISAM类型的表强调的是性能,其执行数度比InnoDB类型更快。

3)行数保存:InnoDB 中不保存表的具体行数,也就是说,执行select count() fromtable时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count()语句包含where条件时,两种表的操作是一样的。

4)索引存储:对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。MyISAM支持全文索引(FULLTEXT)、压缩索引,InnoDB不支持。

MyISAM的索引和数据是分开的,并且索引是有压缩的,内存使用率就对应提高了不少。能加载更多索引,而Innodb是索引和数据是紧密捆绑的,没有使用压缩从而会造成Innodb比MyISAM体积庞大不小。

InnoDB存储引擎被完全与MySQL服务器整合,InnoDB存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB存储它的表&索引在一个表空间中,表空间可以包含数个文件(或原始磁盘分区)。这与MyISAM表不同,比如在MyISAM表中每个表被存在分离的文件中。InnoDB 表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上。

5)服务器数据备份:InnoDB必须导出SQL来备份,LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。

MyISAM应对错误编码导致的数据恢复速度快。MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。

InnoDB是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了。

6)锁的支持:MyISAM只支持表锁。InnoDB支持表锁、行锁 行锁大幅度提高了多用户并发操作的新能。但是InnoDB的行锁,只是在WHERE的主键是有效的,非主键的WHERE都会锁全表的。

6 锁及粒度

悲观锁?

当我们要对一个数据库里的一条数据进行修改的时候,为了避免被其他人修改,最好的方法是直接对数据库加锁以防并发。这种借助数据库的锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观锁。

乐观锁?

乐观锁假设一般情况下不会造成冲突,所以在数据提交更新的时候,才会对数据的冲突与否进行检测,如果发生冲突了,则返回给用户错误信息,让用户决定怎么去做。

如何选择?

乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败率较高,容易发生业务失败。

悲观锁效率低,不能并发执行。

  • 共享锁/读锁:互不阻塞,优先级低
  • 排他锁/写锁:阻塞其他锁,优先级高,即确保在一个事务写入时不受其他事务的影响。
  • 锁粒度:锁定的数据量越少(粒度越小),并发程度越高,但相应的加锁、检测锁、释放锁用的系统开销也随之增大。
  • 锁策略:锁开销与数据安全性之间的平衡
    • 表锁:锁住整张表,读锁互不阻塞,写锁阻塞其他所有读写锁(同一张表)。开销最小。
    • 行级锁:对每一行数据(记录)加锁,开销大,并发程度高。

7 SQL优化

索引 ?假设有2000个sql语句

找到最慢的sql来进行分析

sql 程序量 随着数据量变大,测试库和生产库不同,执行语句变慢,有一些语句会变很慢

慢查询

找到值得优化的sql语句,查询慢的日志。

慢查询分析,找到最慢的优化

首先,查询当前mysql数据库是否开启了慢查询日志功能:

show VARIABLES like '%slow%';

还可以查看超过多少秒算是慢查询:

show VARIABLES like 'long_query_time';

long_query_time的默认值是10,意思是运行10S之上的语句。

修改时间默认时间

set global long_query_time=4;

未使用索引的查询被记录到慢查询日志中。如果调优的话,建议开启这个选项。如果开启了这个参数,full index scan的sql也会被记录到慢查询日志中。

show variables like 'log_queries_not_using_indexes

set global log_queries_not_using_indexes=1

查询有多少条慢查询记录

show global status like '%Slow_queries%';


Linux系统下是编辑/etc/my.cnf

注意:[mysqld]下面添加
#mysql慢日志开启
slow_query_log=ON
slow_query_log_file=/var/lib/mysql/mysql-slow.log
long_query_time=1

然后,重启mysql服务使之生效:

service mysqld restart

接下来就可以通过slow_query_log_file指定的日志路径查看慢查询记录了。

如何使用mysqldumpslow工具

-s 按照那种方式排序
    c:访问计数
    l:锁定时间
    r:返回记录
    al:平均锁定时间
    ar:平均访问记录数
    at:平均查询时间
-t 是top n的意思,返回多少条数据。
-g 可以跟上正则匹配模式,大小写不敏感。

记录最多的20个sql

mysqldumpslow -s r -t 20 sqlslow.log

得到平均访问次数最多的20条sql

mysqldumpslow -s ar -t 20 sqlslow.log

得到平均访问次数最多,并且里面含有ttt字符的20条sql

mysqldumpslow -s ar -t 20 -g "ttt" sqldlow.log

使用mysqldumpslow的分析结果不会显示具体完整的sql语句,说明:

SELECT * FROM sms_send WHERE service_id=10 GROUP BY content LIMIT 0, 1000;

mysqldumpslow显示的结果会是:

Count: 1 Time=1.91s (1s) Lock=0.00s (0s) Rows=1000.0 (1000), vgos_dba[vgos_dba]@[10.130.229.196]
SELECT * FROM sms_send WHERE service_id=N GROUP BY content LIMIT N, N;

2:如果我们再执行一条

SELECT * FROM sms_send WHERE service_id=20 GROUP BY content LIMIT 10000, 1000;

mysqldumpslow显示的结果会是:

Count: 2  Time=2.79s (5s)  Lock=0.00s (0s)  Rows=1.0 (2), vgos_dba[vgos_dba]@[10.130.229.196]

SELECT * FROM sms_send WHERE service_id=N GROUP BY content LIMIT N, N;`

虽然这两条语句条件不一样,

1:一个是server_id=10,一个是server_id=20

2:一个是LIMIT 0, 1000,一个是LIMIT 10000, 1000

但是mysqldumpslow分析会认为这是一种类型的语句,会合并显示。这里的time是最长的一条语句执行时间,括号里是总时间

怎么优化

  • 服务器硬件
  • mysql服务器优化
  • sql本身优化:避免子查询

1.子查询
1.1. MySQL从4.1版本开始支持子查询,使用子查询进行SELECT语句嵌套查询,可以一次完成很多逻辑上需要多个步骤才能完成的SQL操作
1.2.子查询虽然很灵活,但是执行效率并不高
1.3.执行子查询时,MYSQL需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会受到一定的影响,这里多了一个创建和销毁临时表的过程

2.连接查询(join)
2.1.可以使用连接查询(JOIN)代替子查询,连接查询不需要建立临时表,因此其速度比子查询快
总结:连接查询效率高于子查询!!!
扩展:多表联查性能优化
优化的本质就是(join on 和where的执行顺序)!!!

  • 反范式设计优化

    适当违反满足三大范式,为了读取效率,允许少量冗余,使用空间换取时间。

  • 索引优化,优化 10 大策略

    • 策略 1.尽量全值匹配,当建立了索引列后,能在 where条件中使用索引的尽量所用。

    • 策略 2.最佳左前缀法则如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。

    • 策略 3**.不在索引列上做任何操作**
      不在索引列上做任何操作(计算、函数、(自动 or 手动)类型转换),会导致索引失效而转向
      全表扫描

    • 策略 4.范围条件放最后,中间有范围查询会导致后面的索引列全部失效

    • 策略 5.覆盖索引尽量用,尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少 select *

    • 策略 6.不等于要甚用mysql 在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描

    • 策略 7.Null/Not 有影响注意 null/not null 对索引的可能影响,在字段为 not null 的情况下,使用 is null 或 is not null 会导致索引失效

      自定义为 NULL 或者不定义,Is not null 的情况会导致索引失效

    • 策略 8.Like 查询要当心like 以通配符开头(‘%abc…’)mysql 索引失效会变成全表扫描的操作

    • 策略 9.字符类型加引号字符串不加单引号索引失效

    • 策略 10.OR 改 UNION 效率高

    EXPLAIN
    select * from staffs where name='July' or name = 'z3'
    EXPLAIN
    select * from staffs where name='July'
    UNION
    select * from staffs where name = 'z3'

索引

是否用到了索引。。执行计划 expain select….

是否充分用到了索引。。查看key_len 字段 索引长度

key 算法 字符类型

1.字符集 utf-8 3

2.字符类型 vchar+,char+0

3.是否为空 null+1 not null +0

4.本身长度

varchar(50) 50*3 = 150+2+0 =152;

组合索引要保证最佳左前缀原则

  • 在经常性的检索列上,建立必要索引,以加快搜索速率,避免全表扫描(索引覆盖扫描);
  • 多次查询同样的数据,可以考虑缓存该组数据
  • 审视select * form tables, 你需要所有列数据吗?
  • 切分查询(大查询切分成为小查询,避免一次性锁住大量数据)
  • 分解关联查询(单表查询,结果在应用程序中进行关联,可以减少处理过程中的锁争用)
  • 尽量先做单表查询

8 为什么索引用B+树

1、 B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。

2、由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只 需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

9 树的高度为 3 的情况下,mysql 能存多少数据

show VARIABLES like ‘innodb_page_size’ //默认为 16K,代表一个节点能存放 16K 数据
假设:主键为 bigint 长度为 8,额外需要 6 的长度记录位置信息(8+6=14)
第一行:那么一个页能存放=16 * 1024/14 =1170.2857
第二行:1170 * 1170=136,89 00
第三行:假设一行数据为 1K, 1170 * 1170 * 16 = 2190,2400 行

10 为什么myISAM插入速度比InnoDB性能差?

MyISAM在读操作占主导的情况下是很高效的。可一旦出现大量的读写并发,同InnoDB相比,MyISAM的效率就会直线下降,而且,MyISAM和InnoDB的数据存储方式也有显著不同:通常,在MyISAM里,新数据会被附加到数据文件的结尾,可如果时常做一些 UPDATE,DELETE操作之后,数据文件就不再是连续的,形象一点来说,就是数据文件里出现了很多洞洞,此时再插入新数据时,按缺省设置会先看这些洞洞的大小是否可以容纳下新数据,如果可以,则直接把新数据保存到洞洞里,反之,则把新数据保存到数据文件的结尾。之所以这样做是为了减少数据文件的大小,降低文件碎片的产生。但InnoDB里则不是这样,在InnoDB里,由于主键是cluster的,所以,数据文件始终是按照主键排序的,如果使用自增ID做主键,则新数据始终是位于数据文件的结尾。

11 数据库连接池的作用

  1. 在内部对象池中,维护一定数量的数据库连接,并对外暴露数据库连接的获取和返回方法,如外部使用者可通过 getConnection 方法获取数据库连接,使用完毕后再通过 releaseConnection 方法将连接返回,注意此时的连接并没有关闭,而是由连接池管理器回收,并为下一次使用做好准备。
  2. 资源重用,由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,增进了系统环境的平稳性(减少内存碎片以级数据库临时进程、线程的数量)
  3. 更快的系统响应速度,**数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池内备用。此时连接池的初始化操作均已完成。对于业务请求处理而言,**直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。
  4. 新的资源分配手段,对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接技术。
  5. 统一的连接管理,避免数据库连接泄露,**较较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用的连接**,从而避免了常规数据库连接操作中可能出现的资源泄露。

12 数据库union join 的区别

  1. join 是两张表做交连后里面条件相同的部分记录产生一个记录集, union 是产生的两个记录集(字段要一样的) 并在一起,成为一个新的记录集 。
  2. union 在数据库运算中会过滤掉重复数据,并且合并之后的是根据行合并的,即:如果 a 表和 b 表中的数据各有五行,且有两行是重复数据,合并之后为 8 行。运用场景:
    适合于需要进行统计的运算
  3. union all 是进行全部合并运算的,即:如果 a 表和 b 表中的数据各有五行,且有两
    行是重复数据,合并之后为 10 行。
  4. join 是进行表关联运算的,两个表要有一定的关系。即:如果 a 表和 b 表中的数据各
    有五行,且有两行是重复数据,根据某一列值进行笛卡尔运算和条件过滤,假如 a 表有 2 列,b 表有 2 列,join 之后是 4 列。

13、请问MySQL的端口号是多少,如何修改这个端口号

使用命令show global variables like ‘port’;查看端口号 ,mysql的默认端口是3306。(补充:sqlserver默认端口号为:1433;oracle默认端口号为:1521;DB2默认端口号为:5000;PostgreSQL默认端口号为:5432)

修改端口号:

修改端口号:编辑/etc/mysql/mysql.conf.d文件

项目相关

Merge会自动根据两个分支的共同祖先和两个分支的最新提交 进行一个三方合并,然后将合并中修改的内容生成一个新的 commit,即merge合并两个分支并生成一个新的提交,并且仍然后保存原来分支的commit记录。

Rebase会从两个分支的共同祖先开始提取当前分支上的修改,然后将当前分支上的所有修改合并到目标分支的最新提交后面,如果提取的修改有多个,那git将依次应用到最新的提交后面。Rebase后只剩下一个分支的commit记录。

测试相关

软件开发的声明周期:产品定义(确定想要做什么)->可行性分析(该问题是否有解决方案)->需求分析(沟通,深入调研了解用户需求)-> 系统设计(设计可实施的方案,设计程序的体系结构)-> 详细设计(具体设计每个功能模块,确定模块所需的算法和数据结构)-> 软件编码 -> 单元测试综合测试 -> 软件运维

测试需要的能力

对商品需求的了解,业务逻辑清晰,站在用户的角度思考产品。

对计算机硬件、操作系统、编程语言和数据库、web体系要有所了解。

软件测试流程,软件测试需求书,软件测试计划书,软件测试缺陷管理。

责任心,沟通能力,协作能力,耐心,细心,严谨的工作态度。

软件测试是干什么的及工作内容

第一、通过测试发现软件中的缺陷或不足

  软件测试是干什么的呢?通过测试发现软件中存在的不足是其中一个内容,测试软件的技术分为两种,一是黑盒测试,二是白盒测试。之后通过黑盒和白盒进行不同类型的测试比如有类弄分法、因果图法以及白盒测试中的分支覆盖等等,通过这些不同的测试可以发现软件中存在的不足,以让软件开发工程师再次进行完善。

第二、软件测试需要把发现的的问题整理成报告

  软件测试的工作还包括把发现的问题整理成报告上交,提交缘分开发工程师,当得到确认后再对软件进行修复。对于软件测试是干什么的问题,大家还需要了解,测试人员在整理报告的时候应使用专业的术语,同时要具备很好的文字表达能力以及较强的语言组织能力,也只有这样才能把发现的缺点或不足详细、清楚的表达出来,让开发人员更好的对软件进行修复。

第三、测试人员需要分析软件的质量好坏

  软件测试是干什么的呢?包括哪些工作内容呢?除了要测试软件的不足,还要分析软件质量的好坏,需要根据测试的结果来分析,计算出软件的缺陷率和缺陷分布的情况,以及提出对软件修复的趋势等。测试工程师需要给出软件各种质量特性的具体度量,比如功能性、可靠性以及易用性等,并得出结论提交给软件开发工程师。

1 软件测试流程:

  • 测试需求分析阶段:阅读需求,理解需求,主要就是对业务的学习,分析需求点,参与需求评审会议
  • 测试计划阶段:主要任务就是编写测试计划,参考软件需求规格说明书,项目总体计划,内容包括测试范围(来自需求文档),进度安排,人力物力的分配,整体测试策略的制定。风险评估与规避措施有一个制定。
  • 测试设计阶段:主要是编写测试用例,会参考需求文档(原型图),概要设计,详细设计等文档,用例编写完成之后会进行评审。
  • 测试执行阶段:搭建环境,执行冒烟测试(预测试)-然后进入正式测试,bug管理直到测试结束
  • 测试评估阶段:出测试报告,确认是否可以上线
  • 测试流程:了解用户需求–>参考需求规格说明书–>测试计划(人力物力时间进度的安排)–>编写测试用例–>评审用例–>搭建环境–>测试包安排预测(冒烟测试)-正式测试-bug-测试结束出报告–>版本上线–>面向用户

2 Android中造成APP闪推的原因总结

  • 弱网络情况下,服务端响应不及时,可能倒是闪退。(网络异常引起的)
  • 应用版本太低,会导致不兼容,造成闪退。(有些API在老版本中有,在新版本中没有,造成对象为空引起闪退)
  • APP的SDK和手机的系统不兼容。
  • 缓存垃圾过多:由于安卓系统的特性,如果长时间不清理垃圾文件。会导致越来越卡,也会出现闪退情况。
  • 设计不合理,1个接口,拉取的数据量太大,请求结果会很慢,且占用大量内存,APP会闪退(比如,我们现在做的记录仪,进入相册列表时候,要拉取所有图片,拉取太慢了,就闪退了)
  • 不同APP间切换,交互测试,可能会出现闪退。
  • 权限问题。

3 网页很卡的原因

http请求次数太多。

接收数据时间过长,如下载资源过大。

JS脚本过大,阻塞了页面的加载。

网页资源过多、接受数据时间长、加载某个资源慢。

DNS解析速度。

4 测试方法分类:单元测试、集成测试、系统测试

  • 单元测试:开发阶段,研发人员负责代码编写,并且自己进行代码检测,对代码的函数,或者类进行单元测试,也被称为组件测试。

  • 冒烟测试:针对每个版本或每次需求变更后,在正式测试前,对产品或系统的一次简单的验证性测试。冒烟测试主要是测试系统的主流程是否可用.为正式测试前,验证是否产品或系统的主要需求或预置条件是否存在bug。 最好的方法,设计出自动化测试脚本,每一次版本更新后都可以去执行脚本验证一下。

  • 集成测试:组装测试,测试接口,测试数据结构

  • 系统测试:测试用例执行过程,准备计算机,相应操作系统,软件运行环境,分配测试人员进行系统测试。

  • 验收测试:啊法测试,删档内测。

    • 贝塔测试,不删档公测。
  • 粒度不同:

    • 单元测试粒度最小,集成测试粒度居中,系统测试粒度最大。
  • 测试方式不同:

    • 单元测试一般由开发小组采用白盒方式来测试,集成测试一般由开发小组采用白盒加黑盒的方式来测试,系统测试一般由独立测试小组采用黑盒方式来测试。
  • 测试内容不同:

    • 单元测试主要测试单元是否符合“设计”,集成测试既验证“设计”,又验证“需求”,系统测试主要测试系统是否符合“需求规格说明书”。
  • 使用阶段不同:

    • 单元测试为开发人员在开发阶段要做的事情,集成测试和系统测试为测试人员在测试周期内级层做的工作。

测试类型5-12

5 什么是黑盒测试和白盒测试

白盒测试:是通过程序的源代码进行测试而不是用户界面。这种类型的测试需要从代码语句发现内部代码在的算法,溢出,路径,条件等等中的缺点或者错误,进而加以改正。

黑盒测试:通过使用整个软件或者莫种软件功能来严格的测试,而并没有通过检查程序的源码或者很清晰的了解软件的源代码程序是怎样设计的。测试人员通过输入他们的数据然后看输出结果从而了解软件怎样工作。在测试其间,把程序看作一个不能打开的黑匣子,完全不考虑内部结构和内部特性,测试者在程序接口进行测试,它只检查程序是否能适当地接受和正确的输出。对页面和功能进行测试,要根据说明书,用户手册进行测试,要求多组数据,多次测试才能得到准确的报告。

灰盒测试:介于白盒测试和黑盒测试之间,根据实际场景,资源,复杂情况等状况进行结合

6 功能测试

功能测试指测试软件各个功能模块是否正确,逻辑是否正确。

  对测试对象的功能测试应侧重于所有可直接追踪到用例或业务功能和业务规则的测试需求。这种测试的目标是核实数据的接受、处理和检索是否正确,以及业务规则的实施是否恰当。此类测试基于黑盒技术,该技术通过图形用户界面(GUI)与应用程序进行交互,并对交互的输出或结果进行分析,以此来核实应用程序及其内部进程。功能测试的主要参考为类似于功能说明书之类的文档。

7 UI测试

  UI测试指测试用户界面的风格是否满足客户要求,文字是否正确,页面美工是否好看,文字,图片组合是否完美,背景是否美观,操作是否友好等等

  用户界面(UI)测试用于核实用户与软件之间的交互。UI测试的目标是确保用户界面会通过测试对象的功能来为用户提供相应的访问或浏览功能。另外,UI测试还可确保UI中的对象按照预期的方式运行,并符合公司或行业的标准。包括用户友好性,人性化,易操作性测试。UI测试比较主观,与测试人员的喜好有关。

8.性能测试

  确定软件在测试环境下的速度,稳定性,和伸缩型等特征,指满足产品的性能目标以及资源利用率等。性能测试主要测试软件测试的性能,包括负载测试,强度测试,数据库容量测试,基准测试。

性能:主观响应时间,软件系统处理时间,网络传输时间,后端测试,前端测试。。

9.安全性和访问控制测试

  安全性和访问控制测试侧重于安全性的两个关键方面:

  应用程序级别的安全性,包括对数据或业务功能的访问

  系统级别的安全性,包括对系统的登录或远程访问。

10.故障转移和恢复测试

  故障转移和恢复测试指当主机软硬件发生灾难时候,备份机器是否能够正常启动,使系统是否可以正常运行,这对于电信,银行等领域的软件是十分重要的。

  故障转移和恢复测试可确保测试对象能成功完成故障转移,并能从导致意外数据损失或数据完整性破坏的各种硬件、软件或网络故障中恢复。

12.兼容性测试

配置测试核实测试对象在不同的软件和硬件配置中的运行情况。在大多数生产环境中,客户机工作站、网络连接和数据库服务器的具体硬件规格会有所不同。客户机工作站可能会安装不同的软件例如,应用程序、驱动程序等而且在任何时候,都可能运行许多不同的软件组合,从而占用不同的资源。

13 什么是测试用例?

测试用例就是一个文档,描述输入、动作、或者时间和一个期望的结果,其目的是确定应用程序的某个特性是否正常的工作。

软件测试的基本要素包括测试用例编号、测试标题、重要级别、测试输入、操作步骤、预期结果。

测试角度

功能性,安全性,压测性,兼容性

测试用例设计原则
1.测试用例的代表性:能够代表并覆盖各种合理的和不合理的、合法的和非法的、边界的和越界的以及极限的输入数据、操作和环境设置等。
2.测试结果的可判定性:即测试执行结果的正确性是可判定的,每一个测试用例都应有相应的期望结果。
3.测试结果的可再现性:即对同样的测试用例,系统的执行结果应当是相同的。

13.聊天功能实际测试用例

  • 登录退出—忘记密码,更换账号

  • 发送对象(普通用户、公众号、群、其他特殊主体)

  • 衍生功能(转发、语音转文字、删除等)

  • 消息发送—单聊、群聊、语音、文字、图片、表情、链接、字符及长度

  • 消息管理—发布通知、接受通知、发文件、消息提醒、通知提醒、声音、震动、好友请求、请求处理

  • 发送内容(空白、正常文字、超长文字、以前曾经引起过崩溃的特殊内容、特殊字符、表情、图片、多媒体、红包、语音等)

  • 消息推送—在线、离线、收发、时序

  • 权限管理—开放群(任何人入群),半开放群(验证入群),验证加好友,不需验证加好友

  • 隐私管理—黑名单,允许好友查看动态,允许陌生人查看动态,允许通过手机号查找,允许真实姓名查找

  • 成员管理—加人,被加,退出,被动退出,编辑,删除

  • 群组管理—创建群,消息设置,申请入群,扫二维码入群,退群,通知提醒,头像编辑,名称编辑,简介编辑,权限编辑,成员编辑

  • 好友管理—扫二维码加人,加好友,查好友,好友推荐,群组推荐,联系人导入,拉黑名单,解除好友,备注名

  • 动态管理—发动态,发投票,点赞,表情,评论,增加,删除,分享,隐藏,编辑

  • 文件管理—接收,离线接收,预览,删除,分享,转存,文件格式,大小

  • 语音聊天—接通/挂断、通话质量、耳机插拔、音量调解、话筒/扬声器切换、打开/关闭麦克风、后台挂起

  • 视频聊天—接通/挂点/切换语音、视频质量、耳机插拔、音量调解、话筒/扬声器切换、前置后置摄像头切换、视频框切换、后台挂起

14 微信红包功能怎么测试

  • 功能
    • 在红包钱数,和红包个数的输入框中只能输入数字
    • 红包里最多和最少可以输入的钱数 200 0.01
    • 拼手气红包最多可以发多少个红包 100、超过最大拼手气红包的个数是否有提醒
    • 当红包钱数超过最大范围是不是有对应的提示
    • 当发送的红包个数超过最大范围是不是有提示
    • 当余额不足时,红包发送失败
    • 在红包描述里是否可以输入汉字,英文,符号,表情,纯数字,汉字英语符号,是否可以输入它们的混合搭配
    • 输入红包钱数是不是只能输入数字
    • 红包描述里许多能有多少个字符 10个
    • 红包描述,金额,红包个数框里是否支持复制粘贴操作
    • 红包描述里的表情可以删除
    • 发送的红包别人是否可以领取、发的红包自己可不可以领取 2人
    • 24小时内没有领取的红包是否可以退回到原来的账户、超过24小时没有领取的红包,是否还可以领取
    • 用户是否可以多次抢一个红包
    • 发红包的人是否还可以抢红包 多人
    • 红包的金额里的小数位数是否有限制
    • 可以按返回键,取消发红包
    • 断网时,无法抢红包
    • 可不可以自己选择支付方式
    • 余额不足时,会不会自动匹配支付方式
    • 在发红包界面能否看到以前的收发红包的记录
    • 红包记录里的信息与实际收发红包记录是否匹配
    • 支付时可以密码支付也可以指纹支付
    • 如果直接输入小数点,那么小数点之前应该有个0
    • 支付成功后,退回聊天界面
    • 发红包金额和收到的红包金额应该匹配
    • 是否可以连续多次发红包
    • 输入钱数为0,”塞钱进红包”置灰

性能

  • 弱网时抢红包,发红包时间
  • 不同网速时抢红包,发红包的时间
  • 发红包和收红包成功后的跳转时间
  • 收发红包的耗电量
  • 退款到账的时间

兼容

  • 苹果,安卓是否都可以发送红包
  • 电脑端可以抢微信红包

界面

  • 发红包界面没有错别字
  • 抢完红包界面没有错别字
  • 发红包和收红包界面排版合理,
  • 发红包和收到红包界面颜色搭配合理

安全

  • 对方微信号异地登录,是否会有提醒 2人
  • 红包被领取以后,发送红包人的金额会减少,收红包金额会增加
  • 发送红包失败,余额和银行卡里的钱数不会少
  • 红包发送成功,是否会收到微信支付的通知

易用性(有点重复)

  • 红包描述,可以通过语音输入
  • 可以指纹支付也可以密码支付

15 朋友圈里的点赞功能

  • 是否可以点赞、取消点赞
  • 多次点赞会出现什么情况
  • 多人点赞时的顺序是否按照时间顺序进行排列
  • 点赞是否显示头像和名称
  • 点赞之后能否进行评论
  • 点赞之后退出该页面,再次进入朋友圈点赞消息是否还存在
  • 多用户点赞,再次打开朋友圈是是否可以按照顺序看到是谁谁谁赞了我
  • 弱网络的情况下点赞能否实时更新
  • 点赞时有短信或电话进来,能否显示点赞情况
  • 点赞的人是否在可见分组里
  • 点赞之后共同好友的点赞和评论是否会提醒你

排名算法(二)–淘宝搜索排序算法分析

淘宝搜索排序的目的是帮助用户快速的找到需要的商品。从技术上来说,就是在用户输入关键词匹配到的商品中,把最符合用户需求的商品排到第一位,其它的依次排在后续相应的位置。为了更好的实现这个目标,算法排序系统基本按三个方面来推进:

一、算法模型

当用户输入关键词进行搜索的时候,系统依据算法模型来给匹配到的每个商品进行实时的计算,并按照分数的大小对商品进行排序。

对于好的算法模型,首先需要考虑我们能够有哪些特征因子可以应用。比如在网页搜索中,算法模型基本就是按网页的重要性和相关性给网页计算一个分数,然后进行排序。这里的相关性,和重要性就是网页排序模型中两个重要的因子。具体来说相关性因子是指搜索关键字在文档中出现的度数,当这个度数越高时,则认为该文档的相关程度越高。重要度因子比如 Google 的 Pagerank,可以理解为一个网页入口超级链接的数目:一个网页被其他网页引用得越多,则该网页就越有价值。特别地,一个网页被越重要的网页所引用,则该网页的重要程度也就越高。

考虑淘宝搜索的时候,有些特征因子是很容易能想到的,比如:

A、文本的相关性:关键词和商品的匹配,匹配的程度,是否重要词的匹配,匹配词之间的距离等,都可能影响相关性。比如搜索“小鸭子洗衣机”的时候,一个商品的中心词是洗衣机的要比卖洗衣机配件商品的相关性高,小鸭子连在一起的相关性要比“小”和“鸭子”分开时候的相关性高等。文本相关性最基本的计算方式可以参考 BM25 等。

B、类目热点:淘宝数据的一个重要特质是每个商品都挂靠在类目属性体系下面,每个商品都做了一个很好的分类。在搜索过程中,同一搜索词的大量用户行为数据很容易聚焦到相应的热点类目,比如“手机”的搜索行为会集中到手机类目,而不是配件类目。

C、图片质量:图片是电子商务网站非常重要的一个数据,图片是否精美吸引人,图片上是否有各种各样的“牛皮癣”,和商品匹配度等都很大程度上影响着用户的点击和购买决策。

D、商品质量:每个商品都有不同的质量,商品的描述真实性,是否物美价廉,受人欢迎的程度等。

E、作弊因子:类似于全网搜索有关键词堆砌,link spam,网页重复等等作弊的问题,电子商务搜索也面临同样的问题,比如商品关键词堆砌,重复铺货,重复开店,广告商品引流等等,也有商品特有的问题如价格作弊,交易作弊等,需要利用统计分析或者机器学习来做异常行为,异常规律的发现和识别并运用到排序中。

F、公平因子:淘宝的商品很丰富,每个搜索词下都有足够多的商品在竞争,需要在相似质量的情况下让更多的商品和卖家有展示的机会,而不是像网页搜索一样是一个基本静态的排序,照成商品点击和展示的马太效应。

类似的商品,卖家,买家,搜索词等方面的特征因子有很多,一个排序模型就是把各种各样不同的特征因子组合起来,给出一个最终的关键词到商品的相关性分数。只用其中的一到两个特征因子,已经可以对商品做一些最基本的排序。如果有更多的特征参与到排序,我们就可能得到一个更好的排序算法。组合的方法可以有简单的人工配置到复杂的类似 Learning to Rank 等的学习模型。

那么如何衡量不同算法之间的优劣呢?

二、线下评估

算法模型的评估一般分为线下的评估和线上的评估,线下的评估很多都体现在搜索中常用的相关性(Relevance)指标。相关性的定义可以分为狭义相关性和广义相关性两方面,狭义相关性一般指检索结果和用户查询的相关程度。而从广义的层面,相关性可以理解为用户查询的综合满意度。当用户在搜索框输入关键词,到需求获得满足,这之间经历的过程越顺畅,越便捷,搜索相关性就越好。

在淘宝搜索衡量狭义相关性的时候,一般是使用 PI(Per Item)测试的方法:

A、抽取具有代表性的查询关键词,组成一个规模适当的关键词集合

B、针对这个关键词集合,从模型的产出结果中查询对应的结果,进行人工标注(人工判断为相关性好、中、差等), 对人工评测的结果数据,使用预定义好的评价计算公式比如 DCG 等,用数值化的方法来评价算法模型的结果和标注的理想结果的接近程度。

利用人工标注数据来计算相关性的分数,来判断模型的好坏;在这个过程中人工不可避免的会有主观的判断,但综合了多人的判断结果还是可以获得一个有统计意义的结果,另一方面标注数据也可以帮助我们找到一个算法表现不理想的地方,有针对性的提升。
广义的相关性线下评测比较困难,受人工主观因素的影响更大,一般使用 SBS(Side by Side)的评测方法,针对一个关键词,把两个不同算法模型产出的结果同时展示在屏幕上,每次新模型和对比模型展示的位置关系都是随即的,人工判断的时候不知道哪一边的数据是新模型的结果,人工判断那一边的搜索结果好,以最终的统计结果综合来衡量新模型和老模型的搜索表现。

线下评测的方法和指标有很多,不同的搜索引擎会关注不同的指标,比如以前 Yahoo 的全网搜索引擎比较关注 RCFP(Relevance,Coverage,Freshness,Perspective)等,淘宝搜索线下评测时候一般统计 DCG 和 SBS 的指标。

线下的评测方法从统计上有一定的指导意义,能从一定程度上区分模型的好坏,但要真正验证算法模型的好坏,还需要接受真实的流量来验证。

三、线上测试

为了真实验证一个算法模型的好坏,需要有一个系统能提供真实的流量来检验。淘宝搜索实现的 BTS(Bucket Testing System)系统就是这样的一个环境,在用户搜索时,由搜索系统根据一定的策略来自动决定用户的分组号(Bucket id),保证自动抽取导入不同分组的流量具有可对比性,然后让不同分组的用户看到的不同算法模型提供的结果。用户在不同模型下的行为将被记录下来,这些行为数据通过数据分析形成一系列指标,而通过这些指标的比较,最后就形成了不同模型之间孰优孰劣的结论。只要分组的流量达到一定的程度,数据指标从统计意义上就具有可比性。

不同的 BTS 系统会关注不同的数据指标,在淘宝搜索,有一些重要的指标是很多算法模型测试的时候关注的:

访问 UV 成交转化率:来淘宝搜索的 UV,最终通过搜索结果成交的用户占比。
IPV-UV 转化率:来淘宝搜索的 UV,有多少比例的用户点击了搜索结果
CTR:搜索产生的点击占搜索产生的 PV 的比例
客单价:每个成交用户在淘宝搜索上产生成交的平均价格
基尼系数:基尼系数是一个经济学名词,考量社会财富的集中度;如果社会财富集中到很少一部分富人手中的时候,基尼系数就会增大,社会的稳定性和可持续发展性就会出现问题;淘宝搜索借用了这个概念来衡量搜索带给卖家的 PV 展示,和点击的集中度,在保证用户体验的前提下,给更多的优质或小小而美的卖家展示的机会。
大部分时候我们都有好几个模型和功能在线上测试,我们用 BTS 的方式来观察测试的情况,如果提升稳定就逐渐开放给所有用户,如果没有提升,我们也能从中获得经验帮助我们更好的理解用户。


评论
  目录