C++ 常面知识点

1.new/delete 与malloc/free 的区别

参考:细说new与malloc的10点区别 - melonstreet - 博客园

共同点:

(1)都可用于申请动态内存和释放内存

不同点:

(1)malloc与free是C++/C 语言的标准库函数new/delete 是C++的运算符。对于非内部数据类的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数, 对象消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符不在编译器控制权限之内不能够把执行构造函数和析构函数的任务强加malloc/free。(本质区别)。由于内部数据类型的“对象”没有构造与析构的过程对它们而言malloc/free和new/delete是等价的。

(2)用法上也有所不同

malloc与free:void * malloc(size_t size); 用malloc 申请一块长度为length 的整数类型的内存;malloc 返回值的类型是void *,所以在调用malloc 时要显式地进行类型转换,将void * 转换成所需要的指针类型int *p = (int *) malloc(sizeof(int) * length); malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。void free( void * memblock ) ,这是因为指针p 的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p 是NULL 指针,那么free对p 无论操作多少次都不会出问题如果p 不是NULL 指针,那么free 对p连续操作两次就会导致程序运行错误。

new/delete :运算符new 使用起来要比函数malloc 简单得多,int *p2 = new int[length];new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。如果类有多个构造函数那么new 的语句也可以有多种形式。如果用new 创建对象数组,那么只能使用类的无参数构造函数,在用delete 释放对象数组时,留意不要丢了符号‘[]’。new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。

使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸

(3)既然new/delete的功能完全覆盖了malloc/free,为什么C++还保留malloc/free呢?

因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差所以new/delete、malloc/free必须配对使用。

(4)new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。

(5)new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符;而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。类型安全很大程度上可以等价于内存安全。

(6)new内存分配失败时会抛出bac_alloc异常,它不会返回NULLmalloc分配内存失败时返回NULL。在使用C语言时,我们习惯在malloc分配内存后判断分配是否成功;

(7)new操作符来分配对象的内存时:第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。第三步:对象构造完成后,返回一个指向该对象的指针。delete运算符释放对象时会经历的步骤:第一步:调用对象的析构函数。第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。malloc/free不会调用构造函数和析构函数,来处理C++的自定义类型不合适,其实不止自定义类型,标准库中凡是需要构造/析构的类型通通不合适

(8)operator new /operator delete的实现可以基于malloc而malloc的实现不可以去调用new

(9)opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本。而malloc/free并不允许重载

(10)使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。new没有这样直观的配套设施来扩充内存。

(11)在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler。对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。

2.c++多态:覆盖和虚函数

C++多态(polymorphism)是通过虚函数来实现的虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override)。最常见的用法就是声明基类的指针利用该指针指向任意一个子类对象,调用相应的虚函数动态绑定。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 。包含纯虚函数的类称为抽象类,由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象

虚函数是C++中用于实现多态的机制。核心理念就是通过基类访问派生类定义的函数如果父类或者祖先类中函数func()为虚函数,则子类及后代类中,函数func()是否加virtual关键字,都将是虚函数。为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。

3. override (覆盖),overload(重载)关键字,overwrite(重写)关键字

override (覆盖)关键字:仅在成员函数声明之后使用时才是区分上下文的且具有特殊含义;否则,它不是保留的关键字使用 override 有助于防止代码中出现意外的继承行为。发生在父类和基类之间。

overload(重载)关键字:将语义相近的几个函数用同一个名字表示,但是函数的参数或者返回值不同。发生在同一个类中,可有virtual 关键字

overwrite(重写)关键字:派生类中屏蔽同名的基类函数不同范围(派生类和基类),参数不同或者相同,无virtual 关键字

4. 指针和引用的区别

(1)指针是一个新的变量存储了另外一个变量的地址,可以通过存储的这个地址修改指针指向的变量;而引用是变量的标签(别名)还是变量本身,任何作用于引用的操作都是作用于变量本身;

(2)指针可以多级指向,也引用只有一级。

(3)指针的传参本质上还是值传递,需要通过解引用对指向对象进行操作,而引用传递传进来是变量本身,可以直接对变量本身进行操作

3.虚函数表、虚函数指针大小

虚函数的实现机制:每个含有虚函数的类,至少有一个虚函数表,存放着该类所有虚函数的指针,派生类会生成一个兼容基类的虚函数表

4.gcc编译过程,c++ 动态联编

预处理:预处理指令;编译:编译成汇编代码;汇编:把汇编代码编译成机器码;链接:链接目标代码生成可执行程序。

5.C++ 11新特性
6. const作用:const int func(const int& A) const , volatile关键字的作用

7. 虚继承的实现原理

C++ 利用虚表和虚指针来实现虚函数;每个类用了一个虚表,每个类的对象用了一个虚指针;

推荐:c++ 虚函数的实现机制:笔记_jiangnanyouzi的专栏-CSDN博客_c++虚机制


8.四种类型转换 C++中static_cast/const_cast/dynamic_cast/reinterpret_cast的区别和使用_网络资源是无限的-CSDN博客

C++深入理解(11)------关于static_cast,dynamic_cast,const_cast,reinterpret_cast(读书笔记)_xiaopengshen的博客-CSDN博客

C++强制类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast - SpartacusIn21 - 博客园

C++类型转换分为隐式类型转换和显示类型转换

  • 隐式类型转换
    • 算术转换:在混合类型的算术表达式中,最宽的数据类型成为目标转换类型;
    • 一种类型表达式赋值给另一种类型的对象:目标类型是被赋值对象的类型
    • 将一个表达式作为实参传递给函数调用,此时形参和实参类型不一致:目标转换类型为形参的类型
    • 从一个函数返回一个表达式,表达式类型与返回类型不一致:目标转换类型为函数的返回类型。
  • 显示类型转换:static_cast、const_cast、dynamic_cast、reinterpret_cast.每一种适用于特定的场合
    • static_cast:static_cast<type-id>(expression) 作用和C语言风格强制转换的效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和C语言风格的强制转换都有安全隐患用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。注意:进行上行转换(把派生类的指针或引用转换成基类表示)是安全的进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性需要开发者来维护static_cast不能转换掉原有类型的const、volatile、或者 __unaligned属性;在c++ primer 中说道:c++ 的任何的隐式转换都是使用 static_cast 来实现。static_cast可以反向执行隐式转换,在这种情况下结果是不确定的。这需要程序员来验证static_cast转换的结果是否安全;static_cast运算符无法转换掉const、volatile或 __unaligned特性。
    • const_cast语法:const_cast<type-id>(expression) 从类中移除const、volatile和__unaligned特性; 不能使用const_cast运算符直接重写常量变量的常量状态。
    • dynamic_cast语法:dynamic_cast<type-id>(expression) dynamic_cast:type-id必须是一个指针或引用到以前已定义的类类型的引用或“指向 void的指针”如果type-id是指针,则expression的类型必须是指针,如果type-id是引用,则为左值。当使用dynamic_cast时,如果expression无法安全地转换成类型type-id,则运行时检查会引起变换失败。dynamic_cast支持运行时类型识别。
    • reinterpret_cast语法:reinterpret_cast<type-id>(expression) 允许将任何指针转换为任何其他指针类型。也允许将任何整数类型转换为任何指针类型以及反向转换。滥用reinterpret_cast运算符可能很容易带来风险。除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。reinterpret_cast运算符不能丢掉const、volatile或__unaligned特性。reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。reinterpret_cast本质上依赖机器。
  • 通俗理解显示类型转换
    • static_cast:相当于传统的C语言里的强制转换,该运算符把expression转换为new_type类型,用来强迫隐式转换,例如non-const对象转为const对象编译时检查用于非多态的转换,可以转换指针及其他,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

      ①用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。

      进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;

      进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。

      用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

      ③把空指针转换成目标类型的空指针。

      把任何类型的表达式转换成void类型。

      注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性

    • dynamic_cast:
      在所有形式中,e的类型必须符合以下三个条件中的任何一个:e的类型是是目标类型type的公有派生类、e的类型是目标type的共有基类或者e的类型就是目标type的的类型。如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个std::bad_cast异常(该异常定义在typeinfo标准库头文件中)。e也可以是一个空指针,结果是所需类型的空指针。
      dynamic_cast主要用于类层次间的上行转换和下行转换;还可以用于类之间的交叉转换;在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。dynamic_cast是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作

    • type必须是一个类类型,
      在第一种形式中,type必须是一个有效的指针
      在第二种形式中,type必须是一个左值
      在第三种形式中,type必须是一个右值
      dynamic_cast<type*>(e)
      dynamic_cast<type&>(e)
      dynamic_cast<type&&>(e)
    • const_cast:用于修改类型的const或volatile属性

      该运算符用来修改类型的const(唯一有此能力的C++-style转型操作符)或volatile属性。除了const 或volatile修饰之外, new_type和expression的类型是一样的。

      常量指针被转化成非常量的指针,并且仍然指向原来的对象;

      常量引用被转换成非常量的引用,并且仍然指向原来的对象;

      const_cast一般用于修改底指针。如const char *p形式。

    • reinterpret_cast:new_type必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编辑器,这也就表示它不可移植

9.智能指针及其原理

参考博客:C++11中智能指针的原理、使用、实现 - wxquare - 博客园

  • 智能指针的作用: C++程序设计中使用堆内存是非常频繁的操作堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等使用智能指针能更好的管理堆内存。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放;这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存
  • 智能指针的理解:
    • 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化的技术对普通的指针进行封装,这使得智能指针实质是一个对象行为表现的却像一个指针
    • 智能指针的作用是防止忘记调用delete释放内存程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决
    • 智能指针还有一个作用是把值语义转换成引用语义。
  • 智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr
    • auto_ptr,有很多问题。 不支持复制(拷贝构造函数)和赋值(operator =),但复制或赋值的时候不会提示出错。因为不能被复制,所以不能被放入容器中。已被启用
    • shared_ptr多个指针指向相同的对象shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1减为0时,自动删除所指向的堆内存shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
      • ​​​​​​​初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的
      • 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1后来指向的对象引用计数加1,指向后来的对象;当计数为0时,自动释放内存。
      • get函数获取原始指针
      • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
      • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放导致内存泄漏。循环引用在weak_ptr中介绍。
    • unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权
      #include <iostream>
      #include <memory>
      
      int main() {
          {
              std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
              //std::unique_ptr<int> uptr2 = uptr;  //不能賦值
              //std::unique_ptr<int> uptr2(uptr);  //不能拷貝
              std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權
              uptr2.release(); //释放所有权
          }
          //超過uptr的作用域,內存釋放
      }
    • weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

11.c++初始化列表, export

C++ 类构造函数初始化列表的异常机制 function-try block,C++ 类构造函数初始化列表的异常机制 function-try block_光宇广贞-CSDN博客

初始化对象成员不会再调用默认构造函数,再调用复制构造函数,而是直接调用复制构造函数C++ 构造函数初始化列表的好处_光宇广贞-CSDN博客_c 初始化列表的好处

12.C++类的析构函数不用虚函数,对继承有什么影响?

13.C++内存管理

堆,栈,自由存储存储(有的说法是代码区),静态存储区,常量区

在函数中,函数的局部变量的存储单元在栈上函数执行结束后栈区自动释放。栈内存分配效率高,由操作系统和编译器自动分配,但存储空间有限。

堆区:堆区中的内存由程序员自己创建并维护,每个new(malloc)都应该对应于一个delete(free),如果程序员忘记释放堆内存则在程序最后结束后会由操作系统完成释放,但在程序运行过程中可能会造成堆区越来越大,从而造成内存溢出。

静态(全局)存储区:这部分内存区存储程序的全局变量和静态变量

常量区:常量区内存空间存储常量(包括字符串,等内容)

代码区:存放函数体的二进制代码。

堆栈的区别:(1)管理方式不同(2)空间大小不同;栈的内存空间是连续的空间大小通常是系统预先规定好的,即栈顶地址和最大空间是确定的;而堆得内存空间是不连续的,由一个记录空间的链表负责管理因此内存空间几乎没有限制,在32位系统下,内存空间大小可达到4G(3)能否产生碎片不同:由于管理方式的不同,所以堆更容易产生碎片。这是由于频繁的调用new/delete而造成内存空间不连续。而对于栈,其由操作系统管理,每次弹出的内存块意味着它上面的内存块也已经弹出,所以几乎不会产生碎片。(4)生长方式不同堆的生长方向是向上的,也就是向着内存地址增加的方向而对于栈,其的生长放下是向下的,向着内存地址减小的方向生长。(5)分配方式不同:堆是动态分配的;而栈其实具有两种分配方式,在栈的静态分配中是由编译器完成的,如局部变量的分配;动态分配的栈是由函数 alloca完成的,虽然是动态分配的栈,但是我们也无需对其进行手工释放,也是由操作系统完成的。(6)分配效率不同: 栈是及其系统提供的数据结构,计算机对其底层提供支持有专门的寄存器存放栈的地址,并有专门的指令执行push/pop等操作。而堆是由库函数提供的,其机制很复杂。

14.explicit, export, mutable关键字的含义

mutable 可变的,易变,类成员加上它,const function 可以修改它

15.深拷贝和浅拷贝的区别 C++细节 深拷贝和浅拷贝(位拷贝)详解_编程爱好者的博客-CSDN博客_c++ 深拷贝和浅拷贝

在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。

拷贝构函数的使用:一个对象以值传递的方式传入函数体;一个对象以值传递的方式从函数返回;一个对象需要通过另外一个对象进行初始化。

c++ 默认的拷贝是欠拷贝,对于指针类型而言如果是浅拷贝,一个被析构(delete),另外一个也就完蛋了,所以要用深拷贝

浅拷贝:位拷贝,拷贝构造函数,赋值重载;多个对象共用同一块资源同一块资源释放多次,崩溃或者内存泄漏

深拷贝:每个对象共同拥有自己的资源,必须显式提供拷贝构造函数和赋值运算符

位拷贝(浅拷贝)举例,a指向b,b的改变其实会影响a的改变,同时a原本指向的空间发生泄漏。

然后这种情况下有了深拷贝

16.右值,右值构造函数,move,右值指针

c++引入右值引用和移动语义可以避免无谓的复制,提高程序的性能;c++中所有的值必然属于左值或者右值中的一种

 左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)

左值是指表达式结束后依然存在的持久对象,而右值是指表达式结束时就不再存在的临时对象T& 指向的是 lvalue,而 const T& 指向的,却可能是 lvalue 或 rvalue,左值引用&与右值引用&&(右值引用是c++11加上的)判断左值还是右值的放法:看能不能对表达式取地址,如果能,则为左值,否则为右值

move函数可以是用于构造函数,也可以用于赋值函数,但都需要手动显示添加。其实move函数用直白点的话来说就是省去拷贝构造和赋值时中间的临时对象,将资源的内存从一个对象移动到(共享也可以)另一个对象。官话是:c++11 中的 move() 是这样一个函数,它接受一个参数,然后返回一个该参数对应的右值引用。

std::forward<T>(u) 有两个参数:T 与 u。当T为左值引用类型时,u将被转换为T类型的左值,否则u将被转换为T类型右值。如此定义std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题。

(1)将亡值则是c++11新增的和右值引用相关的表达式,这样的表达式通常时将要移动的对象T&&函数返回值、std::move()函数的返回值等,c++11增加了右值引用,常说的引用指的是左值引用,右值引用相当于给右值取了别名,延长了右值的生命周期;左值引用只能绑定左值,右值引用只能绑定右值如果绑定的不对,编译就会失败。右值引用其实是个左值,已经可以取地址了,拥有了地址。

(2)常量左值引用却是个奇葩,它可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长缺点是,只能读不能改

(3)总结一下,其中T是一个具体类型:

  1. 左值引用, 使用 T&, 只能绑定左值
  2. 右值引用, 使用 T&&, 只能绑定右值
  3. 常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
  4. 已命名的右值引用,编译器会认为是个左值
  5. 编译器有返回值优化,但不要过于依赖

(4)要实现移动语义就必须增加两个函数:移动构造函数和移动赋值构造函数。

move 避免了没有意义的资源申请和释放操作,以及内存间的拷贝操作;对于左值才调用拷贝构造函数,而对于右值,调用移动构造函数c++11使用std::move将左值(比如可以转换一些局部变量,延长局部变量的生命周期)转换为右值,从而方便应用移动构造函数和移动复制构造函数,它其实就是告诉编译器,虽然我是一个左值但是不要对我用拷贝构造函数,而是用移动构造函数吧。。。

(5)如果编译器找不到移动构造函数,,就会对右值调用拷贝构造函数,而且把变量变成右值,变量不会立刻失效,会在变量对应的作用域之后才失效,所以变量和构造的对象会共享内存空间,产生意想不到的错误;C++11的所有容器都实现了move语义;move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝​​​​​和资源申请;move对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如int和char[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move对含有资源的对象说更有意义。

(6)当右值引用和模板结合的时候,就复杂了。T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用:

template<typename T>
void f( T&& param){
    
}
f(10);  //10是右值
int x = 10; //
f(x); //x是左值

这里的&&是一个未定义的引用类型,称为universal references它必须被初始化它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。

(7)引用叠加规则:

  1. 所有的右值引用叠加到右值引用上仍然使一个右值引用。
  2. 所有的其他引用类型之间的叠加都将变成左值引用。
template<typename T>
void f( T&& param); //这里T的类型需要推导,所以&&是一个 universal references

传递左值进去,就是左值引用,传递右值进去,就是右值引用。如它的名字,这种类型确实很"通用",下面要讲的完美转发,就利用了这个特性。

(8)完美转发

所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。c++中提供了一个std::forward()模板函数解决这个问题;在universal referencesstd::forward的合作下,能够完美的转发。

(9)emplace_back减少内存拷贝和移动

emplace_back()可以直接通过构造函数的参数构造对象,但前提是要有对应的构造函数(使用emplace_back()替换push_back()

(10) 高效的交换

template <typename T>
void swap(T& a, T& b)
{
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

对容器类而言

参考博客:[c++11]我理解的右值引用、移动语义和完美转发 - 简书

17.static 加全局变量,理解局部变量, 全局变量,局部静态变量,全局静态变量的区别

c++ 变量根据作用的位置有着不同的生命周期,具有不同作用域作用域可分为6种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。

从作用域上看:

局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回

全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。

静态局部变量具有局部作用域它只被初始化一次自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见

静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话它作用于定义它的文件里不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

全局变量本身就是静态存储方式静态全局变量当然也是静态存储方式这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误

从分配内存空间看
全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在栈里分配空间。

1)、静态变量会被放在程序的静态数据存储区(数据段)(全局可见)中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
2)、变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。

把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的

函数中必须要使用static变量情况:比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针。

static 全局变量:改变作用范围,不改变存储位置

static 局部变量:改变存储位置,不改变作用范围

静态函数 :在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。

 如果在一个源文件中定义的函数,只能被本文件中的函数调用,而不能被同一程序其它文件中的函数调用,这种函数也称为内部函数。定义一个内部函数,只需在函数类型前再加一个“static”关键字即可。

18.组合和继承的区别以及采用哪一个更好

C++三大特性:封装, 继承,多态

继承是为了实现代码的复用, 如果在逻辑上B是A的一种,即B类的man也是A类的 Hunman的一种我们就可以让B类去继承A类。

组合也是类的一种复用技术, 它遵循的就是如果A类是B类的一部分,则不要让B类去继承A类而是采用组合的形式;

组合的优点:(1)不会破环封装性,父类的任何变化不会引起子类的变化;(2)组合运用复杂的设计,他们的关系是在程序运行的时候才确定,可以支持动态的组合;(3)整体类可以对局部类的接口进行封装,提供新的接口。

组合的缺点:(1)整体类不能自动获得和局部类同样的接口,只有通过创建局部的对象去调用它;(2)创建整体类的时候需要创建局部类的对象。

继承的优点:(1)子类继承了父类能自动获得父类的接口;(2)创建子类对象的时候不用创建父类对象

继承的缺点:(1)破坏了封装父类的改变必定引起子类的改变子类缺乏独立性;(2)支持功能上的扩展,但多重继承往往增加了系统结构的复杂度;​​​​(3) 继承是在静态编译的时候就已经确定了关系不支持动态继承

因此优先考虑组合而不是继承

19.STL map 和vector的实现机制

STL map的原理:内部实现是二叉平衡树(红黑树), 

20.红黑树(近似平衡的二叉搜素树)的算法原理

平衡二叉树最大的作用就是查找,AVL树的查找、插入和删除在平均和最坏情况下都是O(logn)。

(1)AVL树的时间复杂度虽然优于红黑树,但是对于现在的计算机,cpu太快,可以忽略性能差异 ;

(2)红黑树的插入删除比AVL树更便于控制操作

(3)红黑树整体性能略优于AVL树(红黑树旋转情况少于AVL树)

红黑树是一棵二叉搜索树,它在每个节点增加了一个存储位记录节点的颜色,可以是RED,也可以是BLACK;通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡。

性质:

(1)每个节点颜色不是黑色,就是红色

(2)根节点是黑色的

(3)如果一个节点是红色,那么它的两个子节点就是黑色的(没有连续的红节点)

(4)对于每个节点,从该节点到其后代叶节点的简单路径上,均包含相同数目的黑色节点

红黑树的插入过程:

(1)红黑树是二叉搜索树,所以按照二叉搜索树的方法对其进行节点插入

(2)RBTree有颜色约束性质,因此我们在插入新节点之后要进行颜色调整

a)根节点为NULL,直接插入新节点并将其颜色置为黑色

b)根节点不为NULL,找到要插入新节点的位置

c)插入新节点

d)判断新插入节点对全树颜色的影响,更新调整颜色

一共分为三种情况对红黑树进行调整

(1)cur为红,parent为红,pParent为黑,uncle存在且为红 
则将parent,uncle改为黑,pParent改为红,然后把pParent当成cur,继续向上调整。

参考:浅析红黑树(RBTree)原理及实现_芮萌萌的博客-CSDN博客_红黑树

21.平衡二叉树的构建过程(AVL)

平衡二叉树是二叉搜索树的一种:

它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

平衡二叉树中引入了一个概念:平衡二叉树节点的平衡因子,它指的是该节点的两个子树,即左子树和右子树的高度差即用左子树的高度减去右子树的高度如果该节点的某个子树不存在,则该子树的高度为0,如果高度差的绝对值超过1就要根据情况进行调整。

参考博客:平衡二叉树(AVL)图解与实现_小张的专栏-CSDN博客_平衡二叉树

22. C++原子操作,atomic

原子(atomic)本意是”不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作”.

(1)处理器使用总线锁保证原子性,CPU提供了在指令执行期间对总线加锁的手段,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的

(2)使用缓冲锁保证原子性,在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销(代价)比较大。如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定;修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效。

23.extern 的作用,export的作用

extern C C++的编译器会对程序中的符号进行修饰,这个过程在编译器中叫符号修饰(Name Decoration)或者符号改编(Name Mangling),C++是能够兼容C的,如果我们有了一个C语言的头文件和其对于的库,在C++中如何使用它呢?在include该头文件的时候当然要加入extern "C"否则按照C++的符号进行符号修饰,那么在库中就会找不到该符号了。

为了访问其他编译单元(如另一代码文件)中的变量或对象,对普通类型(包括基本数据类、结构和类)可以利用关键字extern,来使用这些变量或对象时;但是对模板类型,则必须在定义这些模板类对象和模板函数时,使用标准C++新增加的关键字export导出/出口/输出)。

24.vector<int>返回值类型的函数,在返回时会出现什么情况?一定会是拷贝构造吗?

常规的是会的:调用相应的构造函数构造function()返回的匿名对象 -> 析构function()中的对象 -> function()结束 -> 主调函数继续执行

考虑编译器的优化,类似右值作为参数,他是不会重复申请内存,而是直接延续右值变量。

参考:c++学习--作为函数返回值时拷贝构造函数的流程解析_u010029439的博客-CSDN博客_拷贝构造函数返回值

25.C++并行与并发,多线程编程

要实现并发有两种方法:多进程和多线程。

使用多进程并发是将一个应用程序划分为多个独立的进程(每个进程只有一个线程)这些独立的进程间可以互相通信共同完成任务。由于操作系统对进程提供了大量的保护机制以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但这也造就了多进程并发的两个缺点:

在进程件的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之

运行多个进程的开销很大操作系统要分配很多的资源来对这些进程进行管理

所以C++选用多线程并发:在同一个进程中执行多个线程。有操作系统相关知识的应该知道,线程是轻量级的进程每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。

++11的标准库中提供了多线程库,使用时需要#include <thread>头文件,该头文件主要包含了对线程的管理类std::thread以及其他管理线程相关的类。

CPU有4核,可以同时执行4个线程这是没有问题了,但是控制台却只有一个,同时只能有一个线程拥有这个唯一的控制台,将数字输出;

共享数据的管理以及线程间的通信,是多线程编程的两大核心。每个应用程序至少有一个进程,而每个进程至少有一个主线程除了主线程外,在一个进程中还可以创建多个线程每个线程都需要一个入口函数入口函数返回退出该线程也会退出主线程就是以main函数作为入口函数的线程。在C++ 11的线程库中,将线程的管理在了类std::thread中,使用std::thread可以创建、启动一个线程,并可以将线程挂起、结束等操作。

线程间通信的三种方式:共享内存、管道通信(Linux)、future通信机制
(1)共享内存:多线程会共享全局变量区,所以可以多个线程去option 这个临界区的XXX;共享内存会引发不安全的结果  ==》所以就有了一些保护机制:互斥锁mutex条件变量cv原子操作和线程局部存储等。

(2)管道通信(Linux):与进程间通信的不同,进程间通信时,子进程会copy父进程的fd,故两端要各关闭一个读写。

(3)future通信机制

26.C++ 优先队列priority_queue 的实现原理以及使用方法

实现原理利用堆实现的

27.hash_map的底层实现,hash的散列方法

新版c++的hash_map都是unordered_map;map和hash_map在运行效率方面:unordered_map最高而map效率较低但 提供了稳定效率和有序的序列。占用内存方面:map内存占用略低unordered_map内存占用略高,而且是线性成比例的。需要无序容器快速查找删除,不担心略高的内存时用unordered_map;有序容器稳定查找删除效率,内存很在意时候用map

hash_map内部是一个hash_table一般是由一个大vector, vector元素节点可挂接链表来解决冲突,来实现.

hash_map其插入过程是:得到key,通过hash函数得到hash值,得到桶号(一般都为hash值对桶数求模),存放key和value在桶内。

其取值过程是:得到key,通过hash函数得到hash值;得到桶号(一般都为hash值对桶数求模);比较桶的内部元素是否与key相等;若都不相等,则没有找到;取出相等的记录的value。
 

28.c++ 常用的设计模式

参考:C++简单实现几种常用的设计模式_Ctrlturtle的博客-CSDN博客

(1)单例模式

作用:保证一个类只有一个实例,并提供一个访问它的全局访问点,使得系统中只有唯一的一个对象实例

应用:常用于管理资源,如日志、线程池

实现要点:在类中,要构造一个实例,就必须调用类的构造函数,并且为了保证全局只有一个实例,需要提供一个全局访问点,就需要在类中定义一个static函数,返回在类内部唯一构造的实例需防止在外部调用类的构造函数而构造实例需要将构造函数的访问权限标记为private。

同时阻止拷贝创建对象时赋值时拷贝对象,因此也将它们声明并权限标记为private

(2)工厂模式:简单工厂模式,工厂方法模式,抽像工厂模式;工厂模式的主要作用是封装对象的创建,分离对象的创建和操作过程,用于批量管理对象的创建过程,便于程序的维护和扩展。

简单工厂模式:工厂模式最简单的一种实现,对于不同产品的创建定义一个工厂类,将产品的类型作为参数传入到工厂的创建函数,根据类型分支选择不同的产品构造函数。

工厂方法模式:工厂方法模式在简单工厂模式的基础上增加对工厂的基类抽象不同的产品创建采用不同的工厂创建(从工厂的抽象基类派生),这样创建不同的产品过程就由不同的工厂分工解决:FactoryA专心负责生产ProductA,FactoryB专心负责生产ProductB,FactoryA和FactoryB之间没有关系;如果到了后期,如果需要生产ProductC时,我们则可以创建一个FactoryC工厂类,该类专心负责生产ProductC类产品。该模式相对于简单工厂模式的优势在于:便于后期产品种类的扩展。

抽像工厂模式:抽象工厂模式对工厂方法模式进行了更加一般化的描述工厂方法模式适用于产品种类结构单一的场合,为一类产品提供创建的接口;而抽象工厂方法适用于产品种类结构多的场合,就是当具有多个抽象产品类型时,抽象工厂便可以派上用场。抽象工厂模式更适合实际情况,受生产线所限,让低端工厂生产不同种类的低端产品,高端工厂生产不同种类的高端产品。

29.c++ public,protected, private 以及对应的继承方式

(1)访问范围

private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问

protected: 可以被该类中的函数子类的函数、以及其友元函数访问,但不能被该类的对象访问。

public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问 。

(2)访问权限

public:可以被任意实体访问

protected:只允许子类及本类的成员函数访问

private:只允许本类的成员函数访问

(3)三种继承方法

基类中 继承方式 子类中

public & public继承 => public

public & protected继承 => protected

public & private继承 = > private
protected & public继承 => protected

protected & protected继承 => protected

protected & private继承 = > private
private & public继承 => 子类无权访问

private & protected继承 => 子类无权访问

private & private继承 = > 子类无权访问

public继承不改变基类成员的访问权限,private继承使得基类所有成员在子类中的访问权限变为private,protected继承将基类中public成员变为子类的protected成员,其它成员的访问 权限不变。基类中的private成员不受继承方式的影响,子类永远无权访问。

30. C++内联函数(行函数)

内联函数是C++中的一种特殊函数,它可以像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是通过将函数体直接插入调用处来实现的这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。一般来说inline用于定义类的成员函数。inline适用的函数有两种,一种是在类内定义的成员函数,另一种是在类内声明,类外定义的成员函数;

(1)类内定义成员函数

这种情况下,我们可以不用在函数头部加inline关键字,因为编译器会自动将类内定义的函数声明为内联函数,编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)设置为内联,具有内联函数调用的性质

(2)类内声明函数,在类外定义函数

根据C++编译器的规则,这种情况下如果想将该函数设置为内联函数,则可以在类内声明时不加inline关键字,而在类外定义函数时加上inline关键字;另外,我们可以在声明函数和定义函数的同时写inline,也可以只在函数声明时加inline,而定义函数时不加inline。只要在调用该函数之前把inline的信息告知编译系统,编译系统就会在处理函数调用时按内联函数处理。

内联函数的优缺点:

优点:(1)inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开);没有了调用的开销,效率也很高。(2)2.很明显,类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。(宏替换不会检查参数类型,安全隐患较大)(3)inline函数可以作为一个类的成员函数,与类的普通成员函数作用相同,可以访问一个类的私有成员和保护成员。内联函数可以用于替代一般的宏定义,最重要的应用在于类的存取函数的定义上面。

缺点:(1)内联函数具有一定的局限性,内联函数的函数体一般来说不能太大,如果内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。(换句话说就是,你使用内联函数,只不过是向编译器提出一个申请,编译器可以拒绝你的申请)这样,内联函数就和普通函数执行效率一样了。因此并不是说把一个函数定义为inline函数就一定会被编译器识别为内联函数具体取决于编译器的实现和函数体的大小。内联函数不能包括复杂的控制语句,如循环语句和switch语句;只将规模很小(一般5个语句一下)而使用频繁的函数声明为内联函数。在函数规模很小的情况下,函数调用的时间开销可能相当于甚至超过执行函数本身的时间,把它定义为内联函数,可大大减少程序运行时间。

内联函数和宏定义的区别

内联函数和宏的区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。内联函数与带参数的宏定义进行下比较,它们的代码效率是一样,但是内联欢函数要优于宏定义,因为内联函数遵循的类型和作用域规则,它与一般函数更相近,在一些编译器中,一旦关联上内联扩展,将与一般函数一样进行调用,比较方便。 

       另外,宏定义在使用时只是简单的文本替换并没有做严格的参数检查也就不能享受C++编译器严格类型检查的好处另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。

       C++的inline的提出就是为了完全取代宏定义,因为inline函数取消了宏定义的缺点,又很好地继承了宏定义的优点,《Effective C++》中就提到了尽量使用Inline替代宏定义的条款,足以说明inline的作用之大。

31.B树/B+树的特点及其实现原理

动态查找树主要包括:二叉搜索树,平衡二叉树,红黑树,B树,B-树时间复杂度O(log2N),通过对树高度的降低可以提升查找效率

B树:就是为了存储设备或者磁盘设计的一种平衡查找树

B树的节点可以有很多孩子节点红黑树是一种近似平衡的二叉搜索树即每个节点只有两个孩子,一颗含有N个节点的B树和红黑树的高度是一样的O(lgn)

m阶B树的定义:(1)每个节点至多有m颗子树;(2)除了根结点和叶子结点其他结点至少有[ceil(m / 2)(代表是取上限的函数)]个孩子;(3)若根结点不是叶子结点时,则至少有两个孩子(除了没有孩子的根结点)(4)所有的叶子结点都出现在同一层中叶子结点不包含任何关键字信息;

B树的插入:

1)若B树中已存在需要插入的键值时,用新的键值替换旧值;

(2)若B树中不存在这个值,则在叶子节点进行插入操作;

对于高度为h的m阶B树,新节点一般插在第h层。    
1)若该节点中关键码个数小于m-1,则直接插入
2)若该节点中关键码个数等于m-1,则节点分裂。以中间的关键码为界,
    将节点一分为二,产生一个新的节点,并将中间关键码插入到父节点中。
重复上述过程,最坏情况一直分裂高根节点,则B树就会增加一层。
B树删除:

首先要查找该值是否在B树中存在,如果存在,判断该元素是否存在左右孩子结点,如果有,则上移孩子结点中的相近结点(左孩子最右边的结点或者有孩子最左边的结点)到父结点中,然后根据移动之后的情况;如果没有,进行直接删除;如果不存在对应的值,则删除失败。

1)如果当前要删除的值位于非叶子结点,则用后继值覆盖要删除的值,再用后继值所在的分支删除该后继值。(该后继值必须位于叶子结点上)

2)该结点值个数不小于Math.ceil(m/2)-1(取上线函数),结束删除操作,否则下一步

3)如果兄弟结点值个数大于Math.ceil(m/2)-1,则父结点中下移到该结点,兄弟的一个值上移,删除操作结束。

将父结点的key下移与当前的结点和他的兄弟姐妹结点key合并,形成一个新的结点,

有些结点可能有左兄弟,也有右兄弟,我们可以任意选择一个兄弟结点即可。

B+树:B+树用于数据库和文件系统中,NTFS等都使用B+树作为数据索引

是B树的一种变形,它把数据都存储在叶结点,而内部结点只存关键字孩子指针;因此简化了内部结点的分支因子B+树遍历也更高效,其中B+树只需所有叶子节点串成链表这样就可以从头到尾遍历,其中内部结点是并不存储信息,而是存储叶子结点的最小值作为索引,下面将讲述到。

(1)有n棵子树的结点含有n个关键字每个关键字都不会保存数据,只会用来索引,并且所有数据都会保存在叶子结点;(2)所有的叶子结点包含所有关键字信息以及指向关键字记录的指针关键字自小到大顺序连接;

B+树的插入:

1)若为空树,直接插入,此时也就是根结点

2)对于叶子结点:根据key找叶子结点,对叶子结点进行插入操作。插入后,如果当前结点key的个数不大于m-1,则插入就结束。反之将这个叶子结点分成左右两个叶子结点进行操作,左叶子结点包含了前m/2个记录,右结点包含剩下的记录key,将第m/2+1个记录的key进位到父结点中(父结点必须是索引类型结点),进位到父结点中的key左孩子指针向左结点,右孩子指针向右结点。

3)针对索引结点:如果当前结点key的个数小于等于m-1,插入结束。反之将这个索引类型结点分成两个索引结点,左索引结点包含前(m-1)/2个数据,右结点包含m-(m-1)/2个数据,然后将第m/2个key父结点中,进位到父结点的key左孩子指向左结点, 父结点的key右孩子指向右结点。

B+树的删除:

为什么说B+树比B树更适合做操作系统的数据库索引和文件索引?

(1)B+树的磁盘读写的代价更低

B+树内部结点没有指向关键字具体信息的指针,这样内部结点相对B树更小。B+通过最后一层可以对所有数据进行访问

(2)B+树的查询更加的稳定

因为非终端结点并不是最终指向文件内容的结点,仅仅是作为叶子结点中关键字的索引。这样所有的关键字的查找都会走一条从根结点到叶子结点的路径。所有的关键字查询长度都是相同的,查询效率相当。

参考链接:

B树与B+详解 - 国孩 - 博客园

B树与B+树简明扼要的区别_Hannah-CSDN博客_b树和b+树有什么区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值