C++开发面试基础知识点

1. 语言基础

1.1 const的用法

1)在定义的时候必须进行初始化

2)指针可以是const  指针,也可以是指向const对象的指针

3)定义为const的形参,即在函数内部是不能被修改的

4)类的成员函数可以被声明为常成员函数,不能修改类的成员变量

5)类的成员函数可以返回的是常对象,即被const声明的对象

6)类的成员变量是常成员变量不能在声明时初始化,必须在构造函数的列表里进行初始化

note: const如何做到只读?

这些在编译期间完成,对于内置类型,如int, 编译器可能使用常数直接替换掉对此变量的引用。而对于结构体不一定。

1.2 static的用法

1)在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
3)在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用

4)类内的static成员变量属于整个类所拥有,不能在类内进行定义,只能在类的作用域内进行定义

5)类内的static成员函数属于整个类所拥有,不能包含this指针,只能调用static成员函数

static全局变量与普通的全局变量有什么区别:

static全局变量与普通的全局变量有什么区别:static全局变量只初使化一次,防止在其他文件单元中被引用;
static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值;
static函数与普通函数有什么区别:static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝

1.3 extern c 作用

告诉编译器该段代码以C语言进行编译

1.4 指针和引用的区别

1)引用是直接访问,指针是间接访问

2)引用是变量的别名,本身不单独分配自己的内存空间,而指针有自己的独立内存空间

3)引用绑定内存空间(必须赋初值),是一个变量别名不能更改绑定,可以改变对象的值。

总的来说:引用既具有指针的效率,又具有变量使用的方便性和直观性

1.4 关于静态内存分配和动态内存分配的区别及过程

1) 静态内存分配是在编译时完成的,不占用CPU资源;动态分配内存运行时完成,分配与释放需要占用CPU资源;

2)静态内存分配是在栈上分配的,动态内存是堆上分配的;

3)动态内存分配需要指针或引用数据类型的支持,而静态内存分配不需要;

4)静态内存分配是按计划分配,在编译前确定内存块的大小,动态内存分配运行时按需分配。

5)静态分配内存是把内存的控制权交给了编译器,动态内存把内存的控制权交给了程序员;

6)静态分配内存的运行效率要比动态分配内存的效率要高,因为动态内存分配与释放需要额外的开销;动态内存管理水平严重依赖于程序员的水平,处理不当容易造成内存泄漏。

volatile(必须将cpu的寄存器缓存机制回答的很透彻)

1访问寄存器比访问内存单元要快,编译器会优化减少内存的读取,可能会读脏数据。声明变量为volatile,编译器不再对访问该变量的代码优化,仍然从内存读取,使访问稳定。

总结:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不再编译优化,以免出错。

2)使用实例如下(区分C程序员和嵌入式系统程序员的最基本的问题。)

并行设备的硬件寄存器(如:状态寄存器)
一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
多线程应用中被几个任务共享的变量
3)一个参数既可以是const还可以是volatile吗?解释为什么。

可以。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
4)一个指针可以是volatile 吗?解释为什么。
可以。尽管这并不很常见。一个例子当中断服务子程序修该一个指向一个buffer的指针时。

下面的函数有什么错误:

int square(volatile int *ptr) {
return *ptr * *ptr;
}

下面是答案:
这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr){
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr){
int a;
a = *ptr;
return a * a;
}

1.5 string实现

class String{public://普通构造函数String(const char *str = NULL);//拷贝构造函数String(const String &other);//赋值函数String & operator=(String &other) ;//析构函数~String(void);private:char* m_str;
};分别实现以上四个函数//普通构造函数String::String(const char* str){if(str==NULL) //如果str为NULL,存空字符串{m_str = new char[1]; //分配一个字节*m_str = ‘\0′; //赋一个’\0′
}else{str = new char[strlen(str) + 1];//分配空间容纳str内容strcpy(m_str, str); //复制str到私有成员m_str中}}//析构函数
String::~String(){if(m_str!=NULL) //如果m_str不为NULL,释放堆内存{delete [] m_str;m_str = NULL;
}
}//拷贝构造函数
String::String(const String &other){m_str = new char[strlen(other.m_str)+1]; //分配空间容纳str内容strcpy(m_str, other.m_str); //复制other.m_str到私有成员m_str中
}//赋值函数
String & String::operator=(String &other){if(this == &other) //若对象与other是同一个对象,直接返回本{return *this
}delete [] m_str; //否则,先释放当前对象堆内存m_str = new char[strlen(other.m_str)+1]; //分配空间容纳str内容strcpy(m_str, other.m_str); //复制other.m_str到私有成员m_str中return *this;
}

1.6 用struct关键字与class关键定义类以及继承的区别

(1)定义类差别

(2)继承差别

note: 主要点就两个:默认的访问级别和默认的继承级别 class都是private

1.7 C++多态性与虚函数表

多态分为静态多态和动态多态。静态多态是通过重载模板技术实现,在编译的时候确定。动态多态通过虚函数继承关系来实现,执行动态绑定,在运行的时候确定。
动态多态实现有几个条件:(1) 虚函数; (2) 一个基类的指针或引用指向派生类的对象

基类指针在调用成员函数(虚函数)时,就会去查找该对象的虚函数表。虚函数表的地址在每个对象的首地址。查找该虚函数表中该函数的指针进行调用。每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的都指向这同一个虚函数表。虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。

编译器为每一个类维护一个虚函数表,每个对象的首地址保存着该虚函数表的指针,同一个类的不同对象实际上指向同一张虚函数表。

静态多态是指通过模板技术或者函数重载技术实现的多态,其在编译器确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术。

纯虚函数如何定义,为什么对于存在虚函数的类中析构函数要定义成虚函数?

为了实现多态进行动态绑定,将派生类对象指针绑定到基类指针上,对象销毁时,如果析构函数没有定义为析构函数,则会调用基类的析构函数,显然只能销毁部分数据。如果要调用对象的析构函数,就需要将该对象的析构函数定义为虚函数,销毁时通过虚函数表找到对应的析构函数。

析构函数能抛出异常吗?

(1) 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题

(2) 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题

面向对象的三个基本特征,并简单叙述之?

  • 封装:将客观事物抽象成类,每个类对自身的数据和方法实行protection(private, protected,public)
  • 继承:广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。
  • 多态:系统能够在运行时,能够根据其类型确定调用哪个重载的成员函数的能力,称为多态性。

重载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?

  • 从定义上来说:重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。重写:是指子类重新定义父类虚函数的方法。
  • 从实现原理上来说:重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。

 多态的作用?

  • 隐藏实现细节,使得代码能够模块化;扩展代码模块,实现代码重用;
  • 接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用

虚函数与纯虚函数区别

  • 虚函数在子类里面也可以不重载的;但纯虚必须在子类去实现
  • 带纯虚函数的类叫虚基类也叫抽象类,这种基类不能直接生成对象,只能被继承,重写虚函数后才能使用,运行时动态动态绑定!

纯虚函数如何定义?含有纯虚函数的类称为什么?为什么析构函数要定义成虚函数?

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。纯虚函数是虚函数再加上= 0。virtual void fun ()=0。含有纯虚函数的类称为抽象类在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。如果析构函数不是虚函数,那么释放内存时候,编译器会使用静态联编,认为p就是一个基类指针,调用基类析构函数,这样子类对象的内存没有释放,造成内存泄漏。定义成虚函数以后,就会动态联编,先调用子类析构函数,再基类。

C++中哪些不能是虚函数

  • 普通函数只能重载,不能被重写,因此编译器会在编译时绑定函数。
  • 构造函数是知道全部信息才能创建对象,然而虚函数允许只知道部分信息。
  • 内联函数在编译时被展开,虚函数在运行时才能动态绑定函数。
  • 友元函数 因为不可以被继承。
  • 静态成员函数 只有一个实体,不能被继承。父类和子类共有。

C++虚函数是如何实现的?
使用虚函数表。 C++对象使用虚表, 如果是基类的实例,对应位置存放的是基类的函数指针;如果是继承类,对应位置存放的是继承类的函数指针(如果在继承类有实现)。所以 ,当使用基类指针调用对象方法时,也会根据具体的实例,调用到继承类的方法

深拷贝与浅拷贝

  • 浅拷贝:
  • char ori[]=“hello”;char *copy=ori;
  • 深拷贝:
  • char ori[]="hello";  char *copy=new char[];  copy=ori;
  • 浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

浅拷贝可能出现的问题:

  • 浅拷贝只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃。
  • 浅拷贝使得两个指针都指向同一块内存,任何一方的变动都会影响到另一方。
  • 同一个空间,第二次释放失败,导致无法操作该空间,造成内存泄漏。

1.8 智能指针

1. 如何实现智能指针

  1. 构造函数中计数初始化为1;
  2. 拷贝构造函数中计数值加1;
  3. 赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一;
  4. 析构函数中引用计数减一;
  5. 在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象。

2. share_prt与weak_ptr的区别

share_ptr可能出现循环引用,从而导致内存泄露

weak_ptr是一种弱引用指针,其存在不会影响引用计数,从而解决循环引用的问题

1.9 C++ 转换

  1. const_cast用于将const变量转为非const
  2. static_cast用的最多,对于各种隐式转换,非const转constvoid*转指针等, static_cast能用于多态想上转化,如果向下转能成功但是不安全,结果未知;
  3. dynamic_cast用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
  4. reinterpret_cast几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
  5. 为什么不使用C的强制转换?C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

1.10 内存对齐的原则

  1. 从0位置开始存储;
  2. 变量存储的起始位置是该变量大小的整数倍;
  3. 结构体总的大小是其最大元素的整数倍,不足的后面要补齐;
  4. 结构体中包含结构体,从结构体中最大元素的整数倍开始存;
  5. 如果加入pragma pack(n) ,取n和变量自身大小较小的一个。

1.11 内联函数有什么优点?内联函数与宏定义的区别?

  1. 宏定义在预编译的时候就会进行宏替换
  2. 内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译。
  3. 内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义。
  4. 使用宏定义函数要特别注意给所有单元都加上括号,#define MUL(a, b) a * b,这很危险,正确写法:#define MUL(a, b) ((a) * (b))

1.12 C++内存管理

1. 内存管理

  • malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。
  • free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。

内存分布图:

内存分布图

从上图可以看到,栈至顶向下扩展,堆至底向上扩展, mmap 映射区域至顶向下扩展。 mmap 映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域,这种结构便于 C 运行时库使用 mmap 映射区域和堆进行内存分配。

局部变量、函数参数、返回地址等

动态分配的内存

BSS段

未初始化初值为0的全局变量和静态局部变量

数据段

已初始化且初值非0的全局变量和静态局部变量

代码段

可执行代码、字符串字面值、只读变量

在将应用程序加载到内存空间执行时,操作系统负责代码段数据段BSS段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;由程序员自己管理,即显式地申请和释放空间BSS段、数据段和代码段是可执行程序编译时的分段,运行时还需要栈和堆。具体详见Linux虚拟内存布局

深入谈谈堆和栈

1).分配和管理方式不同

  • 堆是动态分配的,其空间的分配和释放都由程序员控制。
  • 栈由编译器自动管理。栈有两种分配方式:静态分配和动态分配。静态分配由编译器完成,比如局部变量的分配。动态分配由alloca()函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无须手工控制。

2).产生碎片不同

  • 对堆来说,频繁的new/delete或者malloc/free势必会造成内存空间的不连续,造成大量的碎片,使程序效率降低。
  • 对栈而言,则不存在碎片问题,因为栈是先进后出的队列,永远不可能有一个内存块从栈中间弹出。

3).生长方向不同

  • 堆是向着内存地址增加的方向增长的,从内存的低地址向高地址方向增长。
  • 栈是向着内存地址减小的方向增长,由内存的高地址向低地址方向增长。

内存的静态分配和动态分配的区别

  • 时间不同。静态分配发生在程序编译和连接时。动态分配则发生在程序调入和执行时。
  • 空间不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。alloca,可以从栈里动态分配内存,不用担心内存泄露问题,当函数返回时,通过alloca申请的内存就会被自动释放掉。

基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用:

  • brk: brk() 是一个非常简单的系统调用。该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。
  • mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 munmap() 所做的事情与 mmap() 相反。

2. 内存池

一次性分配一大块内存, 多次使用,管理这所谓的一大块内存的数据结构, 也就是今天我们要说的内存池. 内存池可以认为由上面的一个指针数组和下面的自由链表两部分组成, 指针数组中第一个指针指向的是存放内存大小为8bytes的节点串接而成的自由链表, 之后依次是内存而16bytes, 24bytes直到128bytes.

(1). 如果用户分配的内存大于128bytes, 直接用malloc, 否则的话找出适合的自由链表, 从其上摘下一个节点将其头指针返回给用户.

(2). 释放过程则正好与分配相对应, 如果用户分配的内存大于128bytes, 直接用free, 否则找出适当的自由链表, 将指针所指的该段内存重新连接到自由链表中(注意此

时并不返回给操作系统, 因为之后还可以再重复利用). 

因为brk、sbrk、mmap都属于系统调用,若每次申请内存,都调用这三个,那么每次都会产生系统调用,影响性能;其次,这样申请的内存容易产生碎片,因为堆是从低地址到高地址,如果高地址的内存没有被释放,低地址的内存就不能被回收。

Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。

详情见ptmalloc和slab

Linux对内核空间和用户空间是分别管理的,因为进程要么运行在用户态,要么运行在内核态,进程通过系统调用陷入内核态参考文章操作系统管理

附加: STL和redis的内存管理

  1. STL的内存池实现:STL内存分配分为一级分配器二级分配器,一级分配器就是采用malloc分配内存(内存分配),二级分配器采用内存池(构造函数初始化内存)。详见分配算法。二级分配器设计的非常巧妙,分别给8k,16k,…, 128k等比较小的内存片都维持一个空闲链表,每个链表的头节点由一个数组来维护。需要分配内存时从合适大小的链表中取一块下来。假设需要分配一块10K的内存,那么就找到最小的大于等于10k的块,也就是16K,从16K的空闲链表里取出一个用于分配。释放该块内存时,将内存节点归还给链表。如果要分配的内存大于128K则直接调用一级分配器。为了节省维持链表的开销,采用了一个union结构体,分配器使用union里的next指针来指向下一个节点,而用户则使用union的空指针来表示该节点的地址
  2. Redis的内存池实现:Redis为了方便内存的管理,在分配一块内存之后,会将这块内存的大小存入内存块的头部。如图所示,real_ptr是redis调用malloc后返回的指针。redis将内存块的大小size存入头部,size所占据的内存大小是已知的,为size_t类型的长度,然后返回ret_ptr。当需要释放内存的时候,ret_ptr被传给内存管理程序。通过ret_ptr,程序可以很容易的算出real_ptr的值,然后将real_ptr传给free释放内存。

3. 定位内存泄露

  1. 在windows平台下通过CRT中的库函数进行检测;
  2. 在可能泄漏的调用前后生成块的快照,比较前后的状态,定位泄漏的位置
  3. Linux下通过工具valgrind检测

2. 基本数据结构

2.1 STL里set和map是基于什么实现的。红黑树的特点?

  1. set和map都是基于红黑树实现的。
  2. 红黑树是一种平衡二叉查找树,与AVL树的区别是什么?AVL树是完全平衡的,红黑树基本上是平衡的。
  3. 为什么选用红黑数呢?因为红黑数是平衡二叉树,其插入和删除的效率都是N(logN),与AVL相比红黑数插入和删除最多只需要3次旋转,而AVL树为了维持其完全平衡性,在坏的情况下要旋转的次数太多。
    红黑树的定义:
    (1) 节点是红色或者黑色;
    (2) 父节点是红色的话,子节点就不能为红色;
    (3) 从根节点到每个页子节点路径上黑色节点的数量相同;
    (4) 根是黑色的,NULL节点被认为是黑色的。

2.2 必须在构造函数初始化式里进行初始化的数据成员有哪些?

  1. 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
  2. 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
  3. 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化

2.3  模板特化

  1. 模板特化分为全特化和偏特化,模板特化的目的就是对于某一种变量类型具有不同的实现,因此需要特化版本。例如,在STL里迭代器为了适应原生指针就将原生指针进行特化

2.4 hash表

  • Hash表实现(拉链和分散地址)
  • Hash策略常见的有哪些?
  • STL中hash_map扩容发生什么?
    (1) 创建一个新桶,该桶是原来桶两倍大最接近的质数(判断n是不是质数的方法:用n除2到<script type="math/tex" id="MathJax-Element-1">sqrt(n)</script>范围内的数) ;
    (2) 将原来桶里的数通过指针的转换,插入到新桶中(注意STL这里做的很精细,没有直接将数据从旧桶遍历拷贝数据插入到新桶,而是通过指针转换)
    (3) 通过swap函数将新桶和旧桶交换,销毁新桶。

2.5 红黑树

  • 节点为红色或者黑色;
  • 根节点为黑色;
  • 从根节点到每个叶子节点经过的黑色节点个数的和相同;
  • 如果父节点为红色,那么其子节点就不能为红色。

红黑树如何实现:?详见手写红黑树

红黑树与AVL树的区别

  • 红黑树与AVL树都是平衡树,但是AVL是完全平衡的(平衡就是值树中任意节点的左子树和右子树高度差不超过1);
  • 红黑树效率更高,因为AVL为了保证其完全平衡,插入和删除的时候在最坏的情况下要旋转logN次,而红黑树插入和删除的旋转次数要比AVL少

Trie树(字典树)

  • 每个节点保存一个字符
  • 根节点不保存字符
  • 每个节点最多有n个子节点(n是所有可能出现字符的个数)
  • 查询的复杂父为O(k),k为查询字符串长度

海量数据问题

十亿整数(随机生成,可重复)中前K最大的数
类似问题的解决方法思路:首先哈希将数据分成N个文件,然后对每个文件建立K个元素最小/大堆(根据要求来选择)。最后将文件中剩余的数插入堆中,并维持K个元素的堆。最后将N个堆中的元素合起来分析。可以采用归并的方式来合并。在归并的时候为了提高效率还需要建一个N个元素构成的最大堆,先用N个堆中的最大值填充这个堆,然后就是弹出最大值,指针后移的操作了。当然这种问题在现在的互联网技术中,一般就用map-reduce框架来做了。
大数据排序相同的思路:先哈希(哈希是好处是分布均匀,相同的数在同一个文件中),然后小文件装入内存快排,排序结果输出到文件。最后建堆归并。

几十亿个数经常要查找某一个数在不在里面,使用布隆过滤器,布隆过滤器的原理。布隆过滤器可能出现误判,怎么保证无误差?

100万个32位整数,如何最快找到中位数。能保证每个数是唯一的,如何实现O(N)算法

  • 内存足够时:快排
  • 内存不足时:分桶法:化大为小,把所有数划分到各个小区间,把每个数映射到对应的区间里,对每个区间中数的个数进行计数,数一遍各个区间,看看中位数落在哪个区间,若够小,使用基于内存的算法,否则 继续划分

排序算法

  • 排序算法当然是基础内容了,必须至少能快速写出,快排,建堆,和归并
  • 每种算法的时间空间复杂度,最好最差平均情况

3. Linux网络编程IO模型

3.1 进程与线程

(1) 进程与线程区别?

  • 进程是资源分配的基本单位,线程是cpu调度,或者说是程序执行的最小单位
  • 进程有独立的地址空间,而同一进程中的线程共享该进程的地址空间。
  • 线程之间的通信比较方便,而进程之间的通信只能通过进程通信的方式进行。在一个线程中分配的堆在各个线程中均可以使用,在一个线程中打开的文件各个线程均可用,当然指同一进程中的线程。
  • 多进程比多线程程序要健壮。一个线程死掉整个进程就死掉了
  • 线程的执行与进程是有区别的。
  • linux中进程具有父子关系,形成进程树,但是线程是平等的没有父子关系

(2) 线程比进程具有哪些优势?
(3) 什么时候用多进程?什么时候用多线程?

  1. 多进程程序,一个进程崩溃不会影响其他进程,但是进程之间的切换和通信代价较大;
  2. 多线程程序,一个线程崩溃会导致整个进程死掉,其他线程也不能正常工作,但是线程之前数据共享和通信更加方便。
  3. 进程需要开辟独立的地址空间,多进程对资源的消耗很大,而线程则是“轻量级进程”,对资源的消耗更小,对于大并发的情况,只有线程加上IO复用技术才能适应。

因此:对于需要频繁交互数据的,频繁的对同一个对象进行不同的处理,选择多线程合适,对于一些并发编程,不需要很多数据交互的采用多进程。

(4) LINUX中进程和线程使用的几个函数?
(5) 线程同步?

1、进程

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

2、线程

线程是指进程内的一个执行单元,也是进程内的可调度实体。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

3、协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。从技术的角度来说,“协程就是你可以暂停执行的函数”。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

进程间通信需要解决的问题有三个:数据传递,关键部位不会交叉,顺序
在Windows下线程同步的方式有:互斥量,信号量,事件,关键代码段
在Linux下线程同步的方式有:互斥锁,自旋锁,读写锁,屏障(并发完成同一项任务时,屏障的作用特别好使)
知道这些锁之间的区别,使用场景?

协程与线程区别:

1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程

2) 线程进程都是同步机制,而协程则是异步

3) 协程能保留上一次调用时的状态(有自己的寄存器和栈),每次过程重入时,就相当于进入上一次调用的状态。

4)线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力

5)协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。

6)线程是协程的资源。协程通过Interceptor来间接使用线程这个资源。

同步方法有哪些?

  1. 互斥锁,自旋锁,信号量,读写锁,屏障
  2. 互斥锁与自旋锁的区别:互斥锁得不到资源的时候阻塞,不占用cpu资源。自旋锁得不到资源的时候,不停的查询,而然占用cpu资源。
  3. 死锁

 多线程锁的种类有哪些?

a.互斥锁(mutex)b.递归锁 c.自旋锁 d.读写锁

什么是原子操作,gcc提供的原子操作原语,使用这些原语如何实现读写锁

原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch。

如何实现守护进程?

  • 创建子进程,父进程退出
  • 在子进程中创建新会话
  • 改变当前目录为根目
  • 重设文件权限掩码
  • 关闭文件描述符
  • 守护进程退出处理

当用户需要外部停止守护进程运行时,往往会使用 kill命令停止该守护进程。所以,守护进程中需要编码来实现kill发出的signal信号处理,达到进程的正常退出。

3.2 进程间通讯方式

  • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  • 命名管道 (FIFO) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 信号量:信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据,有XSI信号量和POSIX信号量,POSIX信号量更加完善。
  • 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。(原理一定要清楚,常考)
  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生,常见的信号。
  • 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  • 匿名管道与命名管道的区别:匿名管道只能在具有公共祖先的两个进程间使用。
  • 共享文件映射mmap
    mmap建立进程空间到文件的映射,在建立的时候并不直接将文件拷贝到物理内存,同样采用缺页终端。mmap映射一个具体的文件可以实现任意进程间共享内存,映射一个匿名文件,可以实现父子进程间共享内存。

  • 常见的信号有哪些?:SIGINT,SIGKILL(不能被捕获),SIGTERM(可以被捕获),SIGSEGV,SIGCHLD,SIGALRM

3.3 进程调度

  1. Linux进程分为两种,实时进程和非实时进程;
  2. 优先级分为静态优先级和动态优先级,优先级的范围;
  3. 调度策略,FIFO,LRU,时间片轮转
  4. 交互进程通过平均睡眠时间而被奖励;

fork()一子进程程后父进程的全局变量能不能使用?

fork后子进程将会拥有父进程的几乎一切资源,父子进程的都各自有自己的全局变量。不能通用,不同于线程。对于线程,各个线程共享全局变量。

3.4 死锁

  1. 死锁产生的条件;几个进程申请资源,出现了循环等待的情况!
  2. 死锁的避免;1).资源是互斥的 2).不可抢占 3)占有且申请 4).循环等待

3.5 linux的相关命令

  • Linux命令 在一个文件中,倒序打印第二行前100个大写字母
cat filename | head -n 2 | tail -n 1 | grep '[[:upper:]]' -o | tr -d '\n'| cut -c 1-100 | rev
  • 与CPU,内存,磁盘相关的命令(top,free, df, fdisk)
  • 网络相关的命令netstat,tcpdump等

  • sed, awk, grep三个超强大的命名,分别用与格式化修改,统计,和正则查找

  • ipcs和ipcrm命令

  • 查找当前目录以及字母下以.c结尾的文件,且文件中包含”hello world”的文件的路径

  • 创建定时任务

  • kill 命令

kill -9发送SIGKILL信号将其终止,但是以下两种情况不起作用:

a、该进程处于"Zombie"状态(使用ps命令返回defunct的进程)。此时进程已经释放所有资源,但还未得到其父进程的确认。"Zombie"进程要等到下次重启时才会消失,但它的存在不会影响系统性能。

b、 该进程处于"kernel mode"(核心态)且在等待不可获得的资源。处于核心态的进程忽略所有信号处理,因此对于这些一直处于核心态的进程只能通过重启系统实现。进程在AIX 中会处于两种状态,即用户态和核心态。只有处于用户态的进程才可以用“kill”命令将其终止

可以通过ps -ax|grep "pid"(要杀死的进程),找到父进程,杀死父进程就好用了

用top命令查看发现zombie进程数是0,看来这三个进程不属于僵尸进程,应该是b这中情况,就是这些进程进入核心态等待磁盘资源时出现磁盘空间不足的故障,这时我强制关闭了数据库,所以这几个进程就一直处于核心态无法被杀除,看来只能重启了。

3.6 IO模型

  • 五种IO模型:阻塞IO,非阻塞IO,IO复用,信号驱动式IO,异步IO

  • select,poll,epoll的区别,区别见三者区别:Select和poll缺点:(1)每次调用select都需要将fd集合从用户态拷贝到内核态(2)每一次调用select都需要在内核中遍历所有的fd(3)select支持的文件描述符太小,默认1024,poll没有限制。Epoll:使用红黑树来存储fd,同时每一次通过epoll__ctl来将fd加入内核中,同时通过双向列表来返回已经出发某一个事件的fd

  • epoll源码解析:epoll内核解析

  • Netty

  • Reactor:主线程往epoll内核上注册socket读事件,主线程调用epoll_wait等待socket上有数据可读,当socket上有数据可读的时候,主线程把socket可读事件放入请求队列。睡眠在请求队列上的某个工作线程被唤醒,处理客户请求,然后往epoll内核上注册socket写请求事件。主线程调用epoll_wait等待写请求事件,当有事件可写的时候,主线程把socket可写事件放入请求队列。睡眠在请求队列上的工作线程被唤醒,处理客户请求。

epoll哪些触发模式,有啥区别?

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。

  • LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,
  • 在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。

所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值。

也就是说在LT模式的情况下一定要确认收发的数据包的buffer是不是足够大如果收发数据包大小大于buffer的大小的时候就可能会出现数据丢失的情况。

熟练netstat tcpdump ipcs ipcrm

netstat:检查网络状态,tcpdump:截获数据包,ipcs:检查共享内存,ipcrm:解除共享内存

共享内存段被映射进进程空间之后,存在于进程空间的什么位置?共享内存段最大限制是多少?

  • 将一块内存映射到两个或者多个进程地址空间。通过指针访问该共享内存区。一般通过mmap将文件映射到进程地址共享区。
  • 存在于进程数据段,最大限制是0x2000000Byte

i++ 是否原子操作?并解释为什么?

答案肯定不是原子操作,i++主要看三个步骤

首先把数据从内存放到寄存器上,在寄存器上进行自增处理,放回到寄存器上,每个步骤都可能会被中断分离开!

3.7 Linux的API

  • fork与vfork区别
    fork和vfork都用于创建子进程。但是vfork创建子进程后,父进程阻塞,直到子进程调用exit()或者excle()。
    对于内核中过程fork通过调用clone函数,然后clone函数调用do_fork()。do_fork()中调用copy_process()函数先复制task_struct结构体,然后复制其他关于内存,文件,寄存器等信息。fork采用写时拷贝技术,因此子进程和父进程的页表指向相同的页框。但是vfork不需要拷贝页表,因为父进程会一直阻塞,直接使用父进程页表。

  • exit()与_exit()区别
    exit()清理后进入内核,_exit()直接陷入内核。

  • 孤儿进程与僵死进程详见

    1. 孤儿进程是怎么产生的?:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
    2. 僵死进程是怎么产生的?:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
    3. 僵死进程的危害?:如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
    4. 如何避免僵死进程的产生?:僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。
  • Linux是如何避免内存碎片的

    1. 伙伴算法,用于管理物理内存,避免内存碎片;
    2. 高速缓存Slab层用于管理内核分配内存,避免碎片。
  • 共享内存的实现原理?
    共享内存实现分为两种方式一种是采用mmap,另一种是采用XSI机制中的共享内存方法。mmap是内存文件映射,将一个文件映射到进程的地址空间,用户进程的地址空间的管理是通过vm_area_struct结构体进行管理的。mmap通过映射一个相同的文件到两个不同的进程,就能实现这两个进程的通信,采用该方法可以实现任意进程之间的通信。mmap也可以采用匿名映射,不指定映射的文件,但是只能在父子进程间通信。XSI的内存共享实际上也是通过映射文件实现,只是其映射的是一种特殊文件系统下的文件,该文件是不能通过read和write访问的。

注:最简单面经

4. 数据库相关

4.1 ACID 相关

事务:所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。

ACID,是指在可靠数据库管理系统(DBMS)中,事务(transaction)所应该具有的四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability).在事务处理的ACID属性中,一致性是最基本的属性,其它的三个属性都为了保证一致性而存在的。

原子性(Atomicity):指事务是一个不可再分割的工作单位,事务中的操作要么都发生,要么都不发生。为了实现原子性,需要通过日志:将所有对数据的更新操作都写入日志,如果一个事务中的一部分操作已经成功,但以后的操作,由于断电/系统崩溃/其它的软硬件错误而无法继续,则通过回溯日志,将已经执行成功的操作撤销,从而达到“全部操作失败”的目的。

一致性(Consistency):一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。保障事务的一致性,可以从以下两个层面入手:(1)数据库机制层面,在一个事务执行之前和之后,数据会符合你设置的约束(唯一约束,外键约束,Check约束等)和触发器设置。这一点是由SQL SERVER进行保证的。比如转账,则可以使用CHECK约束两个账户之和等于2000来达到一致性目的。(2)业务层面,对于业务层面来说,一致性是保持业务的一致性。这个业务一致性需要由开发人员进行保证。当然,很多业务方面的一致性,也可以通过转移到数据库机制层面进行保证。

隔离性(Isolation):多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。事务之间的相互影响分别为:脏读不可重复读幻读丢失更新

脏读:一个事务读取了另一个事务未提交的数据,而这个数据是有可能回滚的。

不可重复读:一个事务范围内两个相同的查询却返回了不同数据。这是由于查询时系统中其他事务修改的提交而引起的。

幻读(虚读):当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样.

丢失更新:两个事务同时读取同一条记录,A先修改记录,B也修改记录(B是不知道A修改过),B提交数据后B的修改结果覆盖了A的修改结果。

怎样实现隔离性,原则上无非是两种类型的锁:一种是悲观锁,即当前事务将所有涉及操作的对象加锁,操作完成后释放给其它对象使用。为了尽可能提高性能,发明了各种粒度(数据库级/表级/行级……)/各种性质(共享锁/排他锁/共享意向锁/排他意向锁/共享排他意向锁……)的锁。为了解决死锁问题,又发明了两阶段锁协议/死锁检测等一系列的技术。一种是乐观锁,即不同的事务可以同时看到同一对象(一般是数据行)的不同历史版本。如果有两个事务同时修改了同一数据行,那么在较晚的事务提交时进行冲突检测。实现也有两种,一种是通过日志UNDO的方式来获取数据行的历史版本,一种是简单地在内存中保存同一数据行的多个历史版本,通过时间戳来区分。

理解数据库隔离级别:

  • 1读未提交:(Read Uncommitted),在读数据时不会检查或使用任何锁。因此,在这种隔离级别中可能读取到没有提交的数据。
  • 2读已提交(Read Committed) 大多数数据库默认的隔离级别,只读取提交的数据并等待其他事务释放排他锁。读数据的共享锁在读操作完成后立即释放。已提交读是SQL Server的默认隔离级别。
  • 3可重复读(Repeatable-Read) mysql数据库所默认的级别,像已提交读级别那样读数据,但会保持共享锁直到事务结束。
  • 4序列化(serializable),工作方式类似于可重复读。但它不仅会锁定受影响的数据,还会锁定这个范围。这就阻止了新数据插入查询所涉及的范围。

隔离级别

脏读

丢失更新

不可重复读

幻读

并发模型

更新冲突检测

未提交读:Read Uncommited

悲观

已提交读:Read commited

悲观

可重复读:Repeatable Read

悲观

可串行读:Serializable

悲观

持久性(Durability):意味着在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。即使出现了任何事故比如断电等,事务一旦提交,则持久化保存在数据库中。

关于锁模式:

  • 共享锁:(读取)操作创建的锁。其他用户可以并发读取数据,但任何事物都不能获取数据上的排它锁,直到已释放所有共享锁。
  • 排他锁(X锁):对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
  • 更新锁(U) :更新 (U) 锁可以防止通常形式的死锁。如果两个事务获得了资源上的共享模式锁,然后试图同时更新数据,则两个事务需都要转换共享锁为排它 (X) 锁,并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。若要避免这种潜 在的死锁问题,请使用更新 (U) 锁。一次只有一个事务可以获得资源的更新 (U) 锁。如果事务修改资源,则更新 (U) 锁转换为排它 (X) 锁。否则,锁转换为共享锁。

锁的粒度主要有以下几种类型:

  • 行锁: 粒度最小,并发性最高
  • 页锁:一次锁定一页。25个行锁可升级为一个页锁。
  • 表锁:粒度大,并发性低
  • 数据库锁:控制整个数据库操作

4.2 三大范式

  • 第一范式(确保每列保持原子性),所有字段值都是不可分解的原子值
  • 第二范式(确保表中的每列都和主键相关),确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)
  • 第三范式(确保每列都和主键列直接相关,而不是间接相关)

4.3 索引

MyISAM、InnoDB区别:

  • MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持。
  • MyISAM表不支持外键,InnoDB支持
  • MyISAM锁的粒度是表级,而InnoDB支持行级锁定。mysql的表锁见表锁
  • MyISAM支持全文类型索引,而InnoDB不支持全文索引。(mysql 5.6后innodb支持全文索引)
  • MyISAM相对简单,所以在效率上要优于InnoDB,小型应用可以考虑使用MyISAM。当你的数据库有大量的写入、更新操作而查询比较少或者数据完整性要求比较高的时候就选择innodb表。当你的数据库主要以查询为主,相比较而言更新和写 入比较少,并且业务方面数据完整性要求不那么严格,就选择mysiam表。

MyISAM和InnoDB索引实现:

MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。

(1)主索引

MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。

(2) 辅助索引,在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。

MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分

InnoDB索引实现

(1)主索引,InnoDB表数据文件本身就是主索引。

InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。

(2)辅助索引,InnoDB的所有辅助索引都引用主键作为data域。

聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白

  • 为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,
  • 用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。

InnoDB索引和MyISAM索引的区别:

  • 一是主索引的区别,InnoDB的数据文件本身就是索引文件。而MyISAM的索引和数据是分开的。
  • 二是辅助索引的区别:InnoDB的辅助索引data域存储相应记录主键的值而不是地址。而MyISAM的辅助索引和主索引没有多大区别。

4.4 分布式锁

zookeeper分布式锁

  • 实现原理:基于zookeeper瞬时有序节点实现的分布式锁,其主要逻辑如下(该图来自于IBM网站)。大致思想即为:每个客户端对某个功能加锁时,在zookeeper上的与该功能对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
  • 优点:锁安全性高,zk可持久化
  • 缺点:性能开销比较高。因为其需要动态产生、销毁瞬时节点来实现锁功能。

memcached分布式锁

  • 实现原理:memcached带有add函数,利用add函数的特性即可实现分布式锁。add和set的区别在于:如果多线程并发set,则每个set都会成功,但最后存储的值以最后的set的线程为准。而add的话则相反,add会添加第一个到达的值,并返回true,后续的添加则都会返回false。利用该点即可很轻松地实现分布式锁。
  • 优点:并发高效。
  • 缺点:(1)memcached采用列入LRU置换策略,所以如果内存不够,可能导致缓存中的锁信息丢失。(2)memcached无法持久化,一旦重启,将导致信息丢失。

redis分布式锁

redis分布式锁即可以结合zk分布式锁锁高度安全和memcached并发场景下效率很好的优点,可以利用redis客户端实现,详见Redis实现分布式锁,基于redis分布式锁实现“秒杀”

mysql底层索引为什么用b+树而不是二叉树、b树?

1.数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。所以我们要减少IO次数,对于树来说,IO次数就是树的高度,而“矮胖”就是b树和b+树的特征之(二叉树不够矮胖,淘汰

2.b+树相对于b树的优点:

  • b+树中间节点不保存数据,磁盘页能容纳更多节点,树更矮胖
  • b+树查询必须查到叶子结点,b+树查找速度更稳定
  • 对于区间查找,b+树只需遍历叶子结点练成的有序链表即可,b树需要在树中反复查找

mysql索引失效的情况

  • 条件中有or(要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引)
  • 多列索引时不满足最左前缀原则
  • like查询是以%开头
  • 如果查询列类型是字符串,但在条件中未将数据使用引号引用起来
  • mysql估计使用全表扫描要比使用索引快

Mysql 主键为什么建议自增

同一个叶子节点内的各条数据记录按主键顺序存放,当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置(不用频繁地移动数据),当一页写满,就会自动开辟一个新的页。如果使用非自增主键,而主键采用的聚集索引,此时MySQL不得不为了将新记录插到合适位置而移动数据

mysql 主键能不能太大,为什么

对非主键的列建立的索引叫做二级索引,二级索引的叶节点里存的是对应记录的主键的值(再根据主键的索引回表查对应的记录), 而二级索引的缓冲区是有限的,主键过长那么缓冲区里的二级索引的节点就很少, 查一次二级索引会读取更多次的IO,性能差

Mysql Mvcc机制

Mvcc(Multi-Version Concurrency Control),多版本并发控制, 可以事务并发的很多情况下避免加锁操作,因此开销更低。

一个支持MVCC的数据库,在更新某些数据时,并非使用新数据覆盖旧数据,而是标记旧数据是过时的,同时在其他地方新增一个数据版本。因此,同一份数据有多个版本存储,但只有一个是最新的。

Innodb维护了一个系统版本号,每开始一个新事务,系统版本号就会递增,当前事务Id也就是当前系统版本号。然后mvcc机制在每条数据后面加了隐藏的两列(创建事务Id和删除事务Id)

  • SELECT:InnoDB会根据以下两个条件检查每行记录: a) InnoDB只会查找创建事务Id小于等于当前事务Id的数据行,来确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的. b)行的删除事务Id要么未定义,要么大于当前事务Id,来确保事务读取到的行,在事务开始之前未被删除. 只有a,b同时满足的记录,才能返回作为查询结果.
  • INSERT:InnoDB将新插入的每一行的创建事务Id置为当前事务Id,并将其删除事务Id置为“未定义”
  • DELETE:InnoDB将删除的每一行的删除事务Id置为当前事务Id
  • UPDATE:InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建事务Id为当前事务Id,同时将被更新的行的删除事务Id置为当前事务Id

Mysql GroupBy底层原理

group by实现方式有三种,松散索引,紧凑索引,临时表(文件排序)

对group by操作优化的原理就是让mysql利用索引,而避免进行建立临时表,进而进行文件排序

对于group by引用的多个字段,需满足于所建立索引的最左前缀索引,否则进行group by操作时,无法利用索引。在利用索引时,group by可根据索引,即可对数据分组,这种实现方式就是利用松散索引

当group by引用的字段无法构成所建索引的最左前缀索引时,也就是说group by不能直接利用索引时。如果有where语句,比如:group by引用的字段为(c2,c3),而索引为(c1,c2,c3)。此时如果where语句限定了c1=a(某一个值),那么此时mysql的执行过程为先根据where语句进行一次选择,

对选出来的结果集,可以利用索引。这种方式,从整体上来说,group by并没有利用索引,但是从过程来说,在选出的结果中利用了索引,这种方式就是紧凑索引

这种方式,mysql的执行计划为using where,use index。而松散索引的执行计划为using index for group by。

如果mysql如论如何都不能利用索引时,此时mysql将读取所有的数据建立临时表,进行排序,完成分组操作,这就采用了第三种方式

MySQL Join的底层实现原理

1. Simple Nested-Loop Join
如下图,r为驱动表,s为匹配表,最简单的join,拿出r的每一行去和s的所有行匹配,开销大

2. Index Nested-Loop Join

这要求非驱动表(匹配表s)上有索引,可以通过索引来减少比较,加速查询。
在查询时,驱动表(r)会根据关联字段的索引进行查找,在s的索引上找到符合的值后,再回表进行查询,进行数据拼接
如果非驱动表(s)的关联键是主键的话,性能会非常高,如果不是主键,要根据二级索引的主键值进行回表再查一次,性能上比索引是主键要慢。

3. Block Nested-Loop Join:
如果关联字段有索引,会选取第二种方式进行join,否则就会采用Block Nested-Loop Join。可以看到中间有个join buffer缓冲区,是将驱动表的所有join相关的列都先缓存到join buffer中,然后批量与匹配表进行匹配,将第一种多次比较合并为一次,降低了对非驱动表s的访问。默认情况下join_buffer_size=256K,在查找的时候MySQL会将所有的需要的列缓存到join buffer当中,包括要select出的列,而不只缓存关联列。在一个有N个JOIN关联的SQL当中会在执行时候分配N-1个join buffer。

详情见数据库

数据库索引失败,有的不符合索引的操作规范会造成全表扫描

  • 前导模糊查询不能利用索引(like '%XX'或者like '%XX%')
  • 如果是组合索引的话,如果不按照索引的顺序进行查找,比如直接使用第三个位置上的索引而忽略第一二个位置上的索引时,则会进行全表查询
  • 条件中有or, 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描
  • 索引无法存储null值,所以where的判断条件如果对字段进行了null值判断,将导致数据库放弃索引而进行全表查询,可以在num上设置默认值0,确保表中num列没有null值,然后这样查询。a.单列索引无法储null值,复合索引无法储全为null的值。b.查询时,采用is null条件时,不能利用到索引,只能全表扫描。
  • 为什么索引列无法存储Null值? 索引是有序的。NULL值进入索引时,无法确定其应该放在哪里。(将索引列值进行建树,其中必然涉及到诸多的比较操作,null 值是不确定值无法比较,无法确定null出现在索引树的叶子节点位置。) 
  • 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描
  • in 和 not in 也要慎用,否则会导致全表扫描,对于连续的数值,能用 between 就不要用 in 了
  • 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描
  • 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描

SQL索引失败的原因

5. IP/TCP

C++面试基础知识点相关推荐

  1. 【Android 面试基础知识点整理】

    针对Android面试中常见的一些知识点整理,Max 仅仅是个搬运工.感谢本文中引用文章的各位作者,给大家分享了这么多优秀文章.对于当中的解析,是原作者个人见解,有错误和不准确的地方,也请大家积极指正 ...

  2. Java学习---面试基础知识点总结

    Java中sleep和wait的区别 ① 这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类.sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程 ...

  3. Java面试基础知识点简要总结

    目录 Java基础 1. 为什么Java代码可以一次编译,到处运行 2. Java的基本数据类型以及它们的范围 3. 自动装箱和自动拆箱 4. Object类中的方法 5. 说一说hashcode() ...

  4. 操作系统面试基础知识点

    操作系统的几大模块 处理机管理:主要控制和管理CPU的工作.(CPU管理) 存储管理:主要进行内存的分配和管理 (内存管理) 内存分配.地址映射.内存保护与共享.虚拟内存等. 设备管理:主要管理基本的 ...

  5. 自然语言处理算法工程师历史最全资料汇总-基础知识点、面试经验

    2019年秋招已过,零星的招聘任然在继续.本资源适用于NLP算法工程师面试,也适用于算法相关的其他岗位.整理了算法面试需要数学基础知识.编程语言.深度学习.机器学习.计算机理论.统计学习.自然语言处理 ...

  6. mysql 存储引擎 面试_搞定PHP面试 - MySQL基础知识点整理 - 存储引擎

    MySQL基础知识点整理 - 存储引擎 0. 查看 MySQL 支持的存储引擎 可以在 mysql 客户端中,使用 show engines; 命令可以查看MySQL支持的引擎: mysql> ...

  7. 高级 Java 面试通关知识点整理

    转载自 高级 Java 面试通关知识点整理 1.常用设计模式 单例模式:懒汉式.饿汉式.双重校验锁.静态加载,内部类加载.枚举类加载.保证一个类仅有一个实例,并提供一个访问它的全局访问点. 代理模式: ...

  8. 短小精悍-机器学习核心概念、模型、基础知识点简明手册-免费分享

    该手册只有130页,整理了几乎所有关机机器学习的概念.模型.基础知识点,它将帮助读者快速回顾关于机器学习相关的核心知识点和重要公式.模型.概念.涉及概率模型.处理离散数据的生成模型.高斯模型.贝叶斯模 ...

  9. python需要的基础_推荐收藏!小白不要怕!一周学全Python面试基础(2)

    Python是一个广泛的领域,因此有必要保持最新状态.通过列出30个python面试问题和答案,本文涵盖在Python面试中经常问到的问题.如果您是该行业的新手,本基础篇将极大地帮助您.我们衷心希望这 ...

最新文章

  1. 设计所需的各种输出格式(包括整数、实数、字符串等),用一个文件名format.h把这些信息都包括到此文件内,另编写一个文件,用文件包含命令验证可以使用这些格式
  2. 机械爪的带有压力反馈的控制实验
  3. 郑州5月份的windows phone7小聚
  4. 网易考拉没有了,网易严选还会远吗?
  5. C#中面向对象初使用-实现问好窗体程序
  6. solr 启动时指定 solr.home
  7. RabbitMq、ActiveMq、ZeroMq、kafka之间的比较,资料汇总
  8. 7 Statistical estimation
  9. Codeforces Round #697 (Div.3) A~G解题报告与解法证明
  10. 2018阿里云双12年终大促主会场全攻略
  11. python list tuple 消耗_Python list 和 tuple 使用小记
  12. [error] eclipse编写spring等xml配置文件时只有部分提示,tx无提示
  13. 判断当前是什么版本浏览器
  14. 环形电流计算公式_辨析!环形差模电感饱和电流的计算公式是什么?
  15. 一代测序:又称Sanger测序(多分子,单克隆)
  16. 怎么取消工作组计算机,windows10系统如何退出workgroup工作组 windows10系统退出workgroup工作组的操作方法...
  17. rrpp协议如何修改_【网安学术】基于NQA策略的RRPP优化机制
  18. 工具及方法 - Excel插件XLTools
  19. 2018年中国500强排行榜
  20. Random Walk(随机游走)

热门文章

  1. 程序媛人生——专访“龙书”《编译原理》联合作者 Monica S. Lam
  2. 《动手学ROS2》10.7 Nav2导航框架介绍与安装
  3. Android 上架腾讯应用宝
  4. 测试Python软件是否安装成功
  5. 正则表达式实现不包含某个特定字符串
  6. 【LC3】无重复字符的最长子串
  7. swan怎么在linux编译,[转载]总结一下常用的 Linux 命令
  8. 超简单的通用Mapper快速入门
  9. Ubuntu18.04 工控机接USB无线网卡无法连网问题解决办法
  10. B2N给互联网商业模式注入新活力