C++面试笔记

C++面试笔记整理


前言

本人双非科班,已拿20k的offer。本文仅记录自己的面试准备过程,不喜勿喷(侵权联删)


一、C++

1、泛型什么意思,有什么优点:
泛型。即通过参bai数化类型来实现在同一份代码上操作多种数据类型。泛型是在C#2.0引入的。泛型(Genericity)的字面意思是指具有在多种数据类型上皆可操作的含意,与模板有些相似。
优点:
泛型类和泛型方法同时具备可重用性、类型安全和效率,这是非泛型类和非泛型方法无法具备的。泛型通常用与集合以及作用于集合的方法一起使用。
泛型是c#2.0的一个新增加的特性,它为使用c#语言编写面向对象程序增加了极大的效力和灵活性。不会强行对值类型进行装箱和拆箱,或对引用类型进行向下强制类型转换,所以性能得到提高。

2、面向过程编程,面向对象编程和面向切面编程理解:
(1)面向过程(Procedure Oriented)是一种以过程为中心的编程思想。这些都是以什么正在发生为主要目标进行编程,不同于面向对象的是谁在受影响。与面向对象明显的不同就是封装、继承、类。
(2)面向对象编程(Object Oriented Programming,OOP,面向对象程序设计)是一种计算机编程架构。OOP 的一条基本原则是计算机程序是由单个能够起到子程序作用的单元或对象组合而成。OOP 达到了软件工程的三个主要目标:重用性、灵活性和扩展性。为了实现整体运算,每个对象都能够接收信息、处理数据和向其它对象发送信息。
(3)面向切面编程(Aspect Oriented Programming(AOP)),是一个比较热门的话题。AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。

3、红黑树:

  • 每个结点不是红色就是黑色
  • 根节点:一定是黑色的
  • 不可能有两个红色的节点连在一起,每个叶子节点都是黑色的空节点(NIl),并且不存储数据
  • 每个节点,从该结点到达其可到达的叶子节点的所有路径,都包含相同树目的黑色节点

红黑树建立的基础就是在二叉查找树的基础之上的.解决了二叉查找树的线性问题。

AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树不是高度平衡的,算是一种折中,插入最多两次旋转,删除最多三次旋转。

所以红黑树在查找,插入删除的性能都是O(logn),且性能稳定,所以STL里面很多结构包括map底层实现都是使用的红黑树。

红黑树详解

4、const:

  • const等常量被定义时必须初始化,且后面不可以修改。
  • const int a 和 int const a一样,而int *const p和const int *p 不一样,前者指针p不能指向其它地址了,地址内容可修改,后者指针p指向的地址内容不可修改。

5、四大类型转换:

  • dynamic_cast 用于多态类型的转换

  • static_cast 用于非多态类型的转换

  • const_cast 用于删除const ,volatile 和 __unaligned 属性

  • reinterpret_cast 用于位的简单重新解释

    详细见

6、C语言申请内存的方式:

  • 从静态存储区域分配.
    内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量、static变量.

  • 在栈上创建
    在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放.栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限.

  • 从堆上分配,亦称动态内存分配.
    程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存.动态内存的生存期由用户决定,使用非常灵活,但问题也最多.

7、placement new:
一般来说,使用new申请空间时,是从系统的“堆”(heap)中分配空间。申请所得的空间的位置是根据当时的内存的实际使用情况决定的。但是,在某些特殊情况下,可能需要在已分配的特定内存创建对象,这就是所谓的“定位放置new”(placement new)操作。
定位放置new操作的语法形式不同于普通的new操作。例如,一般都用如下语句A* p=new A;申请空间,而定位放置new操作则使用如下语句A* p=new (ptr)A;申请空间,其中ptr就是程序员指定的内存首地址。考察如下程序。

8、智能指针 unique_ptr:
unique_ptr 是从 C++ 11 开始,定义在 中的智能指针(smart pointer)。它持有对对象的独有权,即两个 unique_ptr 不能指向一个对象,不能进行复制操作只能进行移动操作。

unique_ptr 之所以叫这个名字,是因为它智能指向一个对象,即当它指向其他对象时,之前所指向的对象会被摧毁。其次,当 unique_ptr 超出作用域时,指向的对象也会被自动摧毁,帮助程序猿实现了自动释放的功能。
unique_ptr 也可能还未指向对象,这时的状态被称为 empty。
详细见

9、三种继承方式:
在这里插入图片描述

10、strcpy,memcpy,memomve:

  • 前两者复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
  • 前两者复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
  • 前两者用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
  • memomve一般情况下与memcpy类似,在内存重叠问题上更具优势

11、stl中的pop_back和 pop函数为什么不带返回值?

  • 为了不减弱 异常安全性
  • 把得到元素和删除元素分开实现,免得做在一起时删除成功而返回失败后,数据丢失。

12、malloc和new的区别?

  • malloc和free是标准库函数,支持覆盖;new和delete是运算符,并且支持重载。
  • malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分
  • 配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函
    数。
  • malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指
    针。内存分配失败时的返回值不同new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL。malloc分配内存失败时返回NULL。

13、C++常用设计模式:

  • 单例模式:这个是必须会的
    单例的实现主要是通过以下两个步骤:
    将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
    在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
    详细见
  • 观察者模式:这个最典型的应用就是mvc模式。
  • flyweight模式:这个也很常用
  • Composite(组合):这个很常见吧,
  • 适配器模式:这个也很常用,比如我们一般会封装一些类库。然后成为我们用起来更方便的类。

14、函数调用的过程是怎么样的:
详细见

15、重载必须要在同一个类中。

16、stl迭代器失效情况:

vector迭代器的几种失效的情况:

  • 当插入(push_back)一个元素后,end操作返回的迭代器肯定失效。
  • 当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效。
  • 当进行删除操作(erase,pop_back)后,指向删除点的迭代器全部失效;指向删除点后面的元素的迭代器也将全部失效。

deque迭代器的失效情况: 在C++Primer一书中是这样限定的:

  • 在deque容器首部或者尾部插入元素不会使得任何迭代器失效。
  • 在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。
  • 在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效。
    详细见

17、<>和“”的区别:

  • <stdio.h>是直接从系统里边找。
  • ""是先在本地找,然后在系统里边找。
  • <>不可以替换"", ""可以提换<> 。

18、指针和引用的区别:

  • 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
  • 指针可以有多级,引用只有一级
  • 指针可以为空,引用不能为NULL且在定义时必须初始化
  • 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
  • sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
  • 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是
    同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
  • 引用只是别名,不占用具体存储空间,只有声明没有定义;指针是具体变量,需要占用存储空间
    (但这也只是一般情况下,具体情况还要具体分析)。
  • 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指
    针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
  • 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);
    指针变量可以重新指向别的变量。
  • 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。

19、STL六大组件:

  • Container(容器) 各种基本数据结构
  • Adapter(适配器) 可改变containers、Iterators或Function object接口的一种组件
  • Algorithm(算法) 各种基本算法如sort、search…等
  • Iterator(迭代器) 连接containers和algorithms
  • Function object(函数对象)
  • Allocator(分配器)

20、auto的一些问题:

  • 不能对值进行修改,只能遍历读取。
  • auto 定义的变量必须有初始值

21、C++中四种线程同步的方法:

  • 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
  • 互斥量:为协调一起对一个共享资源的单独访问而设计的。
  • 信号量:为控制一个具备有限数量用户资源而设计。
  • 事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

22、map与unordered_map的区别:

  • set/map底层实现的机制是红黑树。红黑树是一种近似于平衡的二叉查找树,默认是按升序排序的。在红黑树上做查找、插入、删除操作的时间复杂度为O(logN)。
  • 红黑树的缺点:空间占用率高,每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间。
  • std::unordered_map对应哈希表,哈希表的特点就是查找效率高,时间复杂度为常数级别O(1),而额外空间复杂度则要高出许多。所以对于需要高效率查询的情况,使用std::unordered_map容器。而如果对内存大小比较敏感或者数据存储要求有序的话,则可以用std::map容器。

区别:

  • unordered_map比map执行效率高;但是占用内存也多
  • std::unordered_map对于迭代器遍历效率并不高

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

  • malloc函数用于动态内存管理,使用管理内存池的方式来进行内存分配,申请大块内存作为堆区,分为多个内存块进行管理
  • 采用用隐式链表结构管理使用块,使用显示的管理空闲的内存块。
  • 当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
  • Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。

24、请你回答一下hash表如何rehash,以及怎么处理其中保存的资源?

  • C++的hash表中有一个负载因子loadFactor,当loadFactor<=1时,hash表查找的期望复杂度为O(1).
    因此,每次往hash表中添加元素时,我们必须保证是在loadFactor <1的情况下,才能够添加。
  • 因此,当Hash表中loadFactor==1时,Hash就需要进行rehash。rehash过程中,会模仿C++的vector
    扩容方式,Hash表中每次发现loadFactor ==1时,就开辟一个原来桶数组的两倍空间,称为新桶数组
    ,然后把原来的桶数组中元素全部重新哈希到新的桶数组中。

25、静态变量什么时候初始化?

静态变量存储在虚拟地址空间的数据段和bss段,C语言中其在代码执行之前初始化,属于编译期初始化。而C++中由于引入对象,对象生成必须调用构造函数,因此C++规定全局或局部静态对象当且仅当对象首次用到时进行构造

26、数组和指针的区别:

  • 指针保存数据的地址,数组保存数据
  • 指针间接访问数据,首先获得指针的内容,然后将其作为地址,从该地址中提取数据;数组直接访问数据
  • 指针通常用于动态的数据结构,数组通常用于固定数目且数据类型相同的元素
  • 指针通过Malloc分配内存,free释放内存,数组隐式的分配和删除
  • 指针通常指向匿名数据,操作匿名函数,数组自身即为数据名

27、智能指针有没有内存泄露的情况?该如何解决?

  • 当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
  • 为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

28、请你回答一下为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?

  • 将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
  • C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

29、请你来说一下函数指针:

  • 定义
    函数指针是指向函数的指针变量。
    函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。
    C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。
  • 用途:
    调用函数和做函数的参数,比如回调函数。
  • 示例:
    char * fun(char * p) {…} // 函数fun
    char * (*pf)(char * p); // 函数指针pf
    pf = fun; // 函数指针pf指向函数fun
    pf(/p); // 通过函数指针pf调用函数fun,忽略括号中的 /

30、请你来说一下fork函数:

  • Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用:
    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork(void);
    成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。
    最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。
    在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。
  • fork和vfork的区别:
    fork( )的子进程拷贝父进程的数据段和代码段;vfork( )的子进程与父进程共享数据段
    fork( )的父子进程的执行次序不确定;vfork( )保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。
    vfork( )保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
    当需要改变共享数据段中变量的值,则拷贝父进程。
  • fork,wait,exec函数联系:
    父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork从父进程返回子进程的pid,从子进程返回0.调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1

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

32、请你来介绍一下STL的allocaotr:
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()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

33、请你说一说C++的内存管理是怎样的?
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。

  • 代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
  • 数据段:存储程序中已初始化的全局变量和静态变量
  • bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
  • 堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
  • 映射区:存储动态链接库以及调用mmap函数进行的文件映射
  • 栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值

34、请你回答一下栈和堆的区别,以及为什么栈要快:
堆和栈的区别:

  • 堆是由低地址向高地址扩展;栈是由高地址向低地址扩展
  • 堆中的内存需要手动申请和手动释放;栈中内存是由OS自动申请和自动释放,存放着参数、局部变量等内存
  • 堆中频繁调用malloc和free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会产生内存碎片
  • 堆的分配效率较低,而栈的分配效率较高

栈的效率高的原因:

  • 栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。

35、请你来说一下静态函数和虚函数的区别:

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

36、C++函数栈空间的最大值:

默认是1M,不过可以调整

37、C语言是怎么进行函数调用的?

每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。

38、C语言参数压栈顺序:

从右到左

39、C++中拷贝赋值函数的形参能否进行值传递?

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数…如此循环,无法完成拷贝,栈也会满。

40、STL里resize和reserve的区别:

  • resize():改变当前容器内含有元素的数量(size()),eg: vectorv; v.resize(len);v的size变为len,如果原来v的size小于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;
  • reserve():改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象,如果reserve(len)的值大于当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前v.size()个对象通过copy construtor复制过来,销毁之前的内存;

41、生成器和迭代器:

  • 生成器的另一个优点就是它不要求你事先准备好整个迭代过程中所有的元素,即无须将对象的所有元素都存入内存之后,才开始进行操作。生成器仅在迭代至某个元素时才会将该元素放入内存,而在这之前或之后,元素可以不存在或者被销毁。这个特点使得它特别适合用于遍历一些巨大的或是无限的类序列对象,大文件/大集合/大字典/斐波那契数列等。

  • 迭代器是不具有上述的特性的,不适合去处理一些巨大的类序列对象,所以建议优先考虑使用生成器来处理迭代的场景。

    详细见

42、C++右值引用:

C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。

详细见

43、const对象只能调用const成员函数、不能调用非const成员函数;非const对象可以调用const成员函数(调用成员函数时隐式传参数类型匹配问题)
详细见

44、模板类中可以使用虚函数,模板成员函数不可以是虚函数。

45、怎么限制一个类的对象实例,只能在"堆"上分配,或者只能在"栈"上分配?

只能建立在堆上:

  • 当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。
  • 因此,将析构函数设为私有,类对象就无法建立在栈上了。

只能建立在栈上:

  • 只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。

二、 计算机网络

1、tcp和udp的区别:
在这里插入图片描述
2、IP包的长度:
IP数据包的最大长度是64K字节(65535),因为在IP包头中用2个字节描述报文长度,2个字节所能表达的最大数字就是65535。

3、浏览器输入网址后发生了什么?
第一步:在浏览器中输入www.baidu.com后,应用层会使用DNS解析域名,如果本地存有对应的IP,则使用;如果没有,则会向上级DNS服务器请求帮助,直至获得IP。

第二步:应用层将请求的信息装载入HTTP请求报文,信息包含了请求的方法(GET / POST)、目标url、遵循的协议(http / https / ftp…)等,然后应用层将发起HTTP请求。

第三步:传输层接收到应用层传递下来的数据,并分割成以报文段为单位的数据包进行管理,并为它们编号,方便服务器接收时能准确地还原报文信息。通过三次握手和目标端口建立安全通信。

第四步:网络层接收传输层传递的数据,根据IP通过ARP协议获得目标计算机物理地址—MAC。当通信的双方不在同一个局域网时,需要多次中转才能到达最终的目标,在中转的过程中需要通过下一个中转站的MAC地址来搜索下一个中转目标。

第五步:找到目标MAC地址以后,就将数据发送到数据链路层,这时开始真正的传输请求信息,传输完成以后请求结束。

第六步:服务器接收数据后,从下到上层层将数据解包,直到应用层。

第七步: 服务器接收到客户端发送的HTTP请求后,查找客户端请求的资源,将数据装载入响应报文并返回,响应报文中包括一个重要的信息——状态码,如200,404,500。``

详细见

4、OSI和TCP/IP模型:
在这里插入图片描述
5、TIME_WAIT状态原理:

  • 通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT状态。
  • 客户端主动关闭连接时,会发送最后一个ack后,然后会进入TIME_WAIT状态,再停留2个MSL时间(后有MSL的解释),进入CLOSED状态。
    详细见
    time_wait状态产生的原因,危害,如何避免

6、CLOSE_WAIT:

  • 在被动关闭连接情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态。
  • 通常来讲,CLOSE_WAIT状态的持续时间应该很短,正如SYN_RCVD状态。但是在一些特殊情况下,就会出现连接长时间处于CLOSE_WAIT状态的情况。
  • 出现大量close_wait的现象,主要原因是某种情况下对方关闭了socket链接,但是我方忙于读或者写,没有关闭连接。代码需要判断socket,一旦读到0,断开连接,read返回负,检查一下errno,如果不是AGAIN,就断开连接。
    详细见

7、http的长连接 和短连接:

  • 短连接
    连接->传输数据->关闭连接
    HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。
    也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。
  • 长连接
    连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。
    长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。

长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。

而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。

详细见

8、TCP粘包原理及解决方案:

  • 粘包发生在发送或接收缓冲区中;应用程序从缓冲区中取数据是整个缓冲区中有多少取多少;那么就有可能第一个数据的尾部和第二个数据的头部同时存在缓冲区,而TCP是流式的,数据无边界,这时发生粘包。

  • TCP粘包解决方案:
    目前应用最广泛的是在消息的头部添加数据包长度,接收方根据消息长度进行接收;在一条TCP连接上,数据的流式传输在接收缓冲区里是有序的,其主要的问题就是第一个包的包尾与第二个包的包头共存接收缓冲区,所以根据长度读取是十分合适的;

    1.解决发送方粘包
    (1)发送产生是因为Nagle算法合并小数据包,那么可以禁用掉该算法;
    (2)TCP提供了强制数据立即传送的操作指令push,当填入数据后调用操作指令就可以立即将数据发送,而不必等待发送缓冲区填充自动发送;
    (3)数据包中加头,头部信息为整个数据的长度(最广泛最常用);
    2.解决接收方粘包
    (1)解析数据包头部信息,根据长度来接收;
    (2)自定义数据格式:在数据中放入开始、结束标识;解析时根据格式抓取数据,缺点是数据内不能含有开始或结束标识;
    (3)短连接传输,建立一次连接只传输一次数据就关闭;(不推荐)

9、请回答一下HTTP和HTTPS的区别,以及HTTPS有什么缺点?

安全套接字(Secure Socket Layer,SSL)协议是Web浏览器与Web服务器之间安全交换信息的协议,提供两个基本的安全服务:鉴别与保密。

安全传输层协议(TLS)用于在两个通信应用程序之间提供保密性和数据完整性。
该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。

HTTP协议和HTTPS协议区别如下:

  • HTTP协议是以明文的方式在网络中传输数据,而HTTPS协议传输的数据则是经过TLS加密后的,HTTPS具有更高的安全性
  • HTTPS在TCP三次握手阶段之后,还需要进行SSL 的handshake,协商加密使用的对称加密密钥
  • HTTPS协议需要服务端申请证书,浏览器端安装对应的根证书
  • HTTP协议端口是80,HTTPS协议端口是443

HTTPS优点:

  • HTTPS传输数据过程中使用密钥进行加密,所以安全性更高
  • HTTPS协议可以认证用户和服务器,确保数据发送到正确的用户和服务器

HTTPS缺点:

  • HTTPS握手阶段延时较高:由于在进行HTTP会话之前还需要进行SSL握手,因此HTTPS协议握手阶段延时增加
  • HTTPS部署成本高:一方面HTTPS协议需要使用证书来验证自身的安全性,所以需要购买CA证书;另一方面由于采用HTTPS协议需要进行加解密的计算,占用CPU资源较多,需要的服务器配置或数目高

10、HTTP返回码:

HTTP协议的响应报文由状态行、响应头部和响应包体组成,其响应状态码总体描述如下:

  • 1xx:指示信息–表示请求已接收,继续处理。
  • 2xx:成功–表示请求已被成功接收、理解、接受。
  • 3xx:重定向–要完成请求必须进行更进一步的操作。
  • 4xx:客户端错误–请求有语法错误或请求无法实现。
  • 5xx:服务器端错误–服务器未能实现合法的请求。

常见状态代码、状态描述的详细说明如下:

  • 200 OK:客户端请求成功。
  • 206 partial content服务器已经正确处理部分GET请求,实现断点续传或同时分片下载,该请求必须包含Range请求头来指示客户端期望得到的范围
  • 300 multiple choices(可选重定向):被请求的资源有一系列可供选择的反馈信息,由浏览器/用户自行选择其中一个。
  • 301 moved permanently(永久重定向):该资源已被永久移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一。
  • 302 move temporarily(临时重定向):请求的资源现在临时从不同的URI中获得,
  • 304:not modified :如果客户端发送一个待条件的GET请求并且该请求以经被允许,而文档内容未被改变,则返回304,该响应不包含包体(即可直接使用缓存)。
  • 403 Forbidden:服务器收到请求,但是拒绝提供服务。
  • t Found:请求资源不存在,举个例子:输入了错误的URL。

11、请问tcp握手为什么两次不可以?为什么不用四次?
两次不可以:tcp是全双工通信,两次握手只能确定单向数据链路是可以通信的,并不能保证反向的通信正常
不用四次:
本来握手应该和挥手一样都是需要确认两个方向都能联通的,本来模型应该是:
1.客户端发送syn0给服务器
2.服务器收到syn0,回复ack(syn0+1)
3.服务器发送syn1
4.客户端收到syn1,回复ack(syn1+1)
因为tcp是全双工的,上边的四部确认了数据在两个方向上都是可以正确到达的,但是2,3步没有没有上下的联系,可以将其合并,加快握手效率,所有就变成了3步握手。

12、GET和POST的区别:

概括:

  • 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
  • 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)

区别:

  • get参数通过url传递,post放在request body中。
  • get请求在url中传递的参数是有长度限制的,而post没有。
  • get比post更不安全,因为参数直接暴露在url中,所以不能用来传递敏感信息。
  • get请求只能进行url编码,而post支持多种编码方式。
  • get请求会浏览器主动cache,而post支持多种编码方式。
  • get请求参数会被完整保留在浏览历史记录里,而post中的参数不会被保留。
  • GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
  • GET产生一个TCP数据包;POST产生两个TCP数据包。

13、闭包是什么,有什么作用?

  • 函数执行形成一个不销毁的私有作用域,除了保护私有变量不受干扰以外,还可以存储一些内容,这样的模式叫做 闭包
  • 作用:
    保护:形成私有作用域,保护里面的私有变量不受外界干扰
    保存:函数执行形成一个私有作用域,函数执行完成,当前私有作用域(栈内存)中的某一部分内容被栈内存以外的其他东西(变量/元素的事件)占用了,当前占内存不能被释放掉,也就形成了不销毁的私有作用域(里面的私有变量也不会被销毁)

14、TCP的SYN报文可以携带payload吗?

TCP的SYN报文是不能携带payload的,因为:

  • 序列号还没有协商号,无法确定数据的序列号区间。
  • 接收窗口还没有确定,不知道payload能不能被接收。
    详细见

15、Cookie和Session的作用及区别:

作用:

  • Cookie通过在客户端记录信息确定用户身份
  • Session通过在服务器端记录信息确定用户身份。

区别:

  • cookie数据存放在客户的浏览器上,session数据放在服务器上。
  • cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
  • session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
  • 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
  • 可以考虑将登陆信息等重要信息存放为session,其他信息如果需要保留,可以放在cookie中。

16、udp如何实现可靠性传输?

  • UDP它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
  • 传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。
  • 实现确认机制、重传机制、窗口确认机制。

三、 操作系统

1、僵尸进程和孤儿进程:

  • 僵尸进程:是所有进程都会进入的一种进程状态,子进程退出,而父进程并没有调用 wait() 或 waitpid() 获取子进程的状态信息,那么子进程的 PID 和 进程描述符 等资源仍然保存在系统中,这种进程称之为僵尸进程 。僵尸进程会一直以终止状态(释放了内存等资源)保持在进程表里并会一直等待父进程获取其退出状态,但父进程没有回收(父进程出了问题)。

  • 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(CentOS7 中是 systemd 进程,进程号为 1)所收养,并由 init 进程对它们完成状态收集工作 。

    详细见

2、五种IO模型:

  • 阻塞IO(blocking I/O):
    A拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。

在内核将数据准备好之前,系统调用会一直等待所有的套接字,默认的是阻塞方式。

  • 非阻塞IO(Non-blocking IO):
    B也在河边钓鱼,但是B不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B也在做其他的事情(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但B在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上来。

每次客户询问内核是否有数据准备好,即文件描述符缓冲区是否就绪。当有数据报准备好时,就进行拷贝数据报的操作。当没有数据报准备好时,也不阻塞程序,内核直接返回未准备就绪的信号,等待用户程序的下一个轮寻。

  • 信号驱动IO(signal blocking I/O):
    C也在河边钓鱼,但与A、B不同的是,C比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,C就会将鱼钓上来。

信号驱动IO模型,应用进程告诉内核:当数据报准备好的时候,给我发送一个信号,对SIGIO信号进行捕捉,并且调用我的信号处理函数来获取数据报。

  • IO多路复用(IO Multiplexing):
    D同样也在河边钓鱼,但是D生活水平比较好,D拿了很多的鱼竿,一次性有很多鱼竿在等,D不断的查看每个鱼竿是否有鱼上钩。增加了效率,减少了等待的时间。

IO多路转接是多了一个select函数,select函数有一个参数是文件描述符集合,对这些文件描述符进行循环监听,当某个文件描述符就绪时,就对这个文件描述符进行处理。

  • 异步IO(Asynchronous IO):
    E也想钓鱼,但E有事情,于是他雇来了F,让F帮他等待鱼上钩,一旦有鱼上钩,F就打电话给E,E就会将鱼钓上去。

当应用程序调用aio_read时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。
当内核中有数据报就绪时,由内核将数据报拷贝到应用程序中,返回aio_read中定义好的函数处理程序。

详细见

3、线程安全:
在这里插入图片描述
详细见

4、协程:

  • 协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
  • 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。

协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

  • 极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

5、自旋锁与互斥锁:

  • 从实现原理上来讲,Mutex(互斥锁)属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞,
  • Core0会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其它的任务而不必进行忙等待。而Spin lock(自旋锁)则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
  • 自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋锁”的作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。

自旋锁的不足之处:

  • 自旋锁一直占用着CPU,他在未获得锁的情况下,一直运行(自旋),所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。
  • 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁。

因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

补充:

  • 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
  • RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。

6、微内核与宏内核:

  • 宏内核:
    除了最基本的进程、线程管理、内存管理外,将文件系统,驱动,网络协议等等都集成在内核里面,例如linux内核。
    优点:效率高。
    缺点:稳定性差,开发过程中的bug经常会导致整个系统挂掉。
  • 微内核:内核中只有最基本的调度、内存管理。驱动、文件系统等都是用户态的守护进程去实现的。
    优点:稳定,驱动等的错误只会导致相应进程死掉,不会导致整个系统都崩溃
    缺点:效率低。典型代表QNX,QNX的文件系统是跑在用户态的进程,称为resmgr的东西,是订阅发布机制,文件系统的错误只会导致这个守护进程挂掉。不过数据吞吐量就比较不乐观了。

7、常用的三种IO复用模型:select、poll、epoll

  • select==>时间复杂度O(n)
    它仅仅知道了,有I/O事件发生了,却并不知道是哪几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
  • poll==>时间复杂度O(n)
    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
  • epoll==>时间复杂度O(1)
    epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

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

epoll跟select都能提供多路I/O复用的解决方案。

详细见

8、请自己设计一下如何采用单线程的方式处理高并发:

在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件来

9、操作系统实现锁的方法:

  • 以中断启用与禁止来实现锁
    中断的启用和禁止是原子的,不能被其他代码插入的
  • 以测试和设置指令来实现锁
    原子操作:将内存指定位置对的存储单元读到一个寄存器,将新的值写到刚才的存储单元
  • 以非繁忙等待,中断启用和禁止来实现锁
  • 以最少繁忙等待 测试与设置来实现锁

详细见

10、系统调用和库函数调用的区别:

  • 系统调用是最底层的应用,是面向硬件的。而库函数的调用是面向开发的,相当于应用程序的API(即预先定义好的函数)接口;
  • 各个操作系统的系统调用是不同的,因此系统调用一般是没有跨操作系统的可移植性,而库函数的移植性良好(c库在Windows和Linux环境下都可以操作);
  • 库函数属于过程调用,调用开销小;系统调用需要在用户空间和内核上下文环境切换,开销较大;
  • 库函数调用函数库中的一段程序,这段程序最终还是通过系统调用来实现的;系统调用调用的是系统内核的服务。

详细见

四、数据库

1、数据库索引的原理、分类、优缺点、为什么不用B树、红黑树、Hash?什么时候该用索引?索引什么时候会失效?

详细见

2、事务:一组数据库操作的集合,这些操作是一个不可分割的单位

3、隔离事务级别级实现原理:

  • 读未提交(read-uncommitted):这种隔离级别下、会解决更新丢失的问题、出现脏读、不可重复读和幻读的问题。

  • 读已提交(read-committed):这种隔离级别下会出现不可重复读和幻读的问题。(这是Oracle的默认隔离级别)

  • 可重复读(repeatable read):这种情况下会出现幻读的问题。(这是mysql默认的隔离级别、其实mysql在这种隔离级别下解决了幻读的问题)

  • 串行化(serializable):这种隔离级别最高、也是最慢、它是以串行化的方式执行的。解决所有的并发问题。
    在这里插入图片描述

    详细见

4、mysql引擎:

  • MyISAM
    不支持事物,不支持外健,表级锁,支持全文索引,索引缓存。
  • InnoDB
    mysql5.5后将InnoDB设置为默认存储引擎。
    支持事物,支持索引,行级锁。InnoDB是将数据存储在集群索引中,所以减少了主键查询的I/O**
  • MEMORY
    内存中的表,在RAM中处理数据,所以比在磁盘中处理更快。用于快速查找和引用其他相同数据
  • ARCHIVE
    将数据压缩存储,所以通常用于存储大量数据,只支持insert和select操作,比如数据归档,日志记录等.。

5、数据库ACID:

  • Atomicity原子性
  • Consistency一致性
  • Isolation隔离性(通过用悲观或乐观锁机制实现的)
  • Durability耐久性:一个成功的事务将永久性地改变系统的状态,所以在它结束之前,所有导致状态的变化都记录在一个持久的事务日志中。如果我们的系统突然受到系统崩溃或断电,那么所有未完成已提交的事务可能会重演。

6、MySQL的端口号是多少,如何修改这个端口号?

  • 查看端口号:
    使用命令show global variables like ‘port’;查看端口号 ,mysql的默认端口是3306。(补充:sqlserver默认端口号为:1433;oracle默认端口号为:1521;DB2默认端口号为:5000;PostgreSQL默认端口号为:5432)
  • 修改端口号:
    修改端口号:编辑/etc/my.cnf文件,早期版本有可能是my.conf文件名,增加端口参数,并且设定端口,注意该端口未被使用,保存退出。

7、数据库的三大范式:

  • 第一范式:当关系模式R的所有属性都不能再分解为更基本的数据单位时,称R是满足第一范式,即属性不可分
  • 第二范式:如果关系模式R满足第一范式,并且R得所有非主属性都完全依赖于R的每一个候选关键属性,称R满足第二范式
  • 第三范式:设R是一个满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖(直接依赖)于R的任意一个候选关键字,称R满足第三范式,即非主属性不传递依赖于键码

8、请问SQL优化方法有哪些?

  • 通过建立索引对查询进行优化
  • 对查询进行优化,应尽量避免全表扫描

9、mongodb和redis的区别:

  • 内存管理机制上:Redis 数据全部存在内存,定期写入磁盘,当内存不够时,可以选择指定的 LRU 算法删除数据。MongoDB 数据存在内存,由 linux系统 mmap 实现,当内存不够时,只将热点数据放入内存,其他数据存在磁盘。
  • 支持的数据结构上:Redis 支持的数据结构丰富,包括hash、set、list等。
  • MongoDB 数据结构比较单一,但是支持丰富的数据表达,索引,最类似关系型数据库,支持的查询语言非常丰富

10、请你来说一说Redis的定时机制怎么实现的:

  • Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:文件事件(服务器对套接字操作的抽象)和时间事件(服务器对定时操作的抽象)。Redis的定时机制就是借助时间事件实现的。
  • 一个时间事件主要由以下三个属性组成:id:时间事件标识号;when:记录时间事件的到达时间;timeProc:时间事件处理器,当时间事件到达时,服务器就会调用相应的处理器来处理时间。一个时间事件根据时间事件处理器的返回值来判断是定时事件还是周期性事件

11、请你来说一说Redis是单线程的,但是为什么这么高效呢?

虽然Redis文件事件处理器以单线程方式运行,但是通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程运行的模块进行对接,这保持了Redis内部单线程设计的简单性。

12、请问Redis的数据类型有哪些,底层怎么实现?

  • 字符串:整数值、embstr编码的简单动态字符串、简单动态字符串(SDS)
  • 列表:压缩列表、双端链表
  • 哈希:压缩列表、字典
  • 集合:整数集合、字典
  • 有序集合:压缩列表、跳跃表和字典

13、请问Redis的rehash怎么做的,为什么要渐进rehash,渐进rehash又是怎么实现的?
因为redis是单线程,当K很多时,如果一次性将键值对全部rehash,庞大的计算量会影响服务器性能,甚至可能会导致服务器在一段时间内停止服务。不可能一步完成整个rehash操作,所以redis是分多次、渐进式的rehash。渐进性哈希分为两种:
1)操作redis时,额外做一步rehash
对redis做读取、插入、删除等操作时,会把位于table[dict->rehashidx]位置的链表移动到新的dictht中,然后把rehashidx做加一操作,移动到后面一个槽位。

2)后台定时任务调用rehash
后台定时任务rehash调用链,同时可以通过server.hz控制rehash调用频率

14、MySQL中事务的五种分类:

  • 扁平事务(Flat Transactions)
  • 带有保存点的扁平事务(Flat Transactions with Savepoints)
  • 链事务(Chained Transactions)
  • 嵌套事务(Nested Transactions)
  • 分布式事务(Distributed Transactions)
    详细见

15、mysql的悲观锁和乐观锁:

乐观锁:

  • 乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。
  • 通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。

悲观锁:

  • 与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。
  • 说到这里,由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。

共享锁

五、算法

1、判断一个链表是否为回文链表:
使用栈存储链表前半部分,然后一个个出栈,与后半部分元素比较,如果链表长度未知,可以使用快慢指针的方法,将慢指针指向的元素入栈,然后如果快指针指向了链表尾部,此时慢指针指向了链表中间

2、n个整数的无序数组,找到每个元素后面比它大的第一个数,要求时间复杂度为O(N)
如果我当前的处理对象是第1个元素,如果第2个元素比我小,那么我现在要做的不是去比较第3个元素与第1个元素的关系,而是将处理的对象变成第2个元素。如果第3个元素比第2个元素大,我在回过来比较第1个元素,这样是不是就省去了很多时间?

因此我们需要一个容器来存储未处理的元素,可以看到,元素是后进先出的,比如第2个元素,后后到来却是先得到结果的。因此我们可以用栈(stack) 来存储未处理的元素

详细见

3、给出两个有序的整数数组A和B,请将数组B合并到数组A中,变成一个有序的数组
注意:可以假设A数组有足够的空间存放B数组的元素,A和B中初始的元素数目分别为m和n.

  • 双指针从A和B的m、n尾部遍历
  • 比较A和B的m、n尾部的值,将较大值赋给A整体数组的尾部,指针左移
  • 注意特殊情况处理:A的尾指针已遍历结束,B尾指针还未结束。

4、给出两个字符串S和T,要求在O(n)的时间复杂度内在S中找出最短的包含T中所有字符的子串。

例如:
S =“ADOBECODEBANC”
T =“ABC”
找出的最短子串为"BANC".

注意:
如果S中没有包含T中所有字符的子串,返回空字符串 “”;
满足条件的子串可能有很多,但是题目保证满足条件的最短的子串唯一。

思路:

  • 初始化left right双指针为0 map<char,int> 记录下T中每个字符所需的数目
  • right向右移动(滑动)当遇到T中的字符时,map中相应的所需字符数目–
    (关键点:需要通过map>0 判断该字符是不是有效字符还是多余字符)
  • 若为有效字符 那么count++
  • 当count==T.size 那么组配成功 记录下当前区间长度
    (关键点 需要将left右移动到第一个这样的位置:该位置是T中字符) 确保区间足够小
  • 下一步,舍弃掉区间左边界left的第一个有效字符 同时right继续向右滑动(即过滤掉多余字符)
    在这里插入图片描述
    详细见

5、给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

注意:答案中不可以包含重复的三元组。

思路:

  • 把数组转化为有序数组,从小到大排列
  • 条件a+b+c=0,所以第一个数必须小于0
  • 先循环a,题目条件为不包含重复的三元组,所以要去重,前后两个元素相等时,只循环一遍。
  • 再循环b和c,可以在循环a的方法里面用双指针循环a和b,同理a和b也要去重。
  • 双指针循环时,a+b+c>0时,a<b<c,a固定,c左移,减小数值;
  • a+b+c<0时,a<b<c,a固定,b右移,增大数值
    在这里插入图片描述
    详细见

6、给一个数组,数字内的每个数字的范围都是1到数组长度的,求每个数字出现多少次,返回一个合适的结构
要求时间:O(n),空间O(1)

思路:由于每个元素都是 的1到n的,那让每个数都加上n,然后把原来的取值转为成0到size-1,最后看a[i]有多少个n,就是个数了.唯一的问题就是会长度大了会溢出

vector<int>& sovle(vector<int> &a) {
	int n = a.size();
	for (int i = 0; i < n; i++) {
		a[a[i] - 1] += n;
	}
	for (int i = 0; i < n; i++) {
        a[i] = (a[i] - 1) / n;
    }
    return a;
}

7、快排的优化:
快排的基数是序列的第一个元素,这样的对于有序序列,快排时间复杂度会达到最差的o(n^2)。所以,优化方向就是合理的选择基数。

常见的做法“三数取中”法(序列太短还要结合其他排序法,如插入排序、选择排序等),如下:

①当序列区间长度小于 7 时,采用插入排序;

②当序列区间长度小于 40 时,将区间分成2段,得到左端点、右端点和中点,我们对这三个点取中数作为基数;

③当序列区间大于等于 40 时,将区间分成 8 段,得到左三点、中三点和右三点,分别再得到左三点中的中数、中三点中的中数和右三点中的中数,再将得到的三个中数取中数,然后将该值作为基数。

详细见

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值