参考:https://www.cnblogs.com/QG-whz/p/5140930.html
共同点:
(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异常,它不会返回NULL;malloc分配内存失败时返回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。
C++多态(polymorphism)是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override)。最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,动态绑定。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 。包含纯虚函数的类称为抽象类,由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。
虚函数是C++中用于实现多态的机制。核心理念就是通过基类访问派生类定义的函数。如果父类或者祖先类中函数func()为虚函数,则子类及后代类中,函数func()是否加virtual关键字,都将是虚函数。为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。
override (覆盖)关键字:仅在成员函数声明之后使用时才是区分上下文的且具有特殊含义;否则,它不是保留的关键字。使用 override 有助于防止代码中出现意外的继承行为。发生在父类和基类之间。
overload(重载)关键字:将语义相近的几个函数用同一个名字表示,但是函数的参数或者返回值不同。发生在同一个类中,可有virtual 关键字
overwrite(重写)关键字:派生类中屏蔽同名的基类函数,不同范围(派生类和基类),参数不同或者相同,无virtual 关键字
(1)指针是一个新的变量,存储了另外一个变量的地址,可以通过存储的这个地址修改指针指向的变量;而引用是变量的标签(别名),还是变量本身,任何作用于引用的操作都是作用于变量本身;
(2)指针可以多级指向,也引用只有一级。
(3)指针的传参本质上还是值传递,需要通过解引用对指向对象进行操作,而引用传递传进来是变量本身,可以直接对变量本身进行操作
虚函数的实现机制:每个含有虚函数的类,至少有一个虚函数表,存放着该类所有虚函数的指针,派生类会生成一个兼容基类的虚函数表
预处理:预处理指令;编译:编译成汇编代码;汇编:把汇编代码编译成机器码;链接:链接目标代码生成可执行程序。
C++ 利用虚表和虚指针来实现虚函数;每个类用了一个虚表,每个类的对象用了一个虚指针;
推荐:https://blog.csdn.net/jiangnanyouzi/article/details/3720807
const_cast , static_cast , dynamic_cast , reinterpret_cast;
c语言风格的类型转换:TypeName b = (TypeName)a;C++也是支持C风格的强制转换,但是C风格的强制转换可能带来一些隐患
(1)const_cast:常量指针被转化成非常量的指针, 并且仍然指向原来的对象;常量引用被转换成非常量的引用,并且仍然指向原来的对象;const_cast一般用于修改指针。如const char *p形式
(2)static_cast :作用和C语言风格强制转换的效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和C语言风格的强制转换都有安全隐患。用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。注意:进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性需要开发者来维护static_cast不能转换掉原有类型的const、volatile、或者 __unaligned属性;在c++ primer 中说道:c++ 的任何的隐式转换都是使用 static_cast 来实现。
(3)dynamic_cast:使用 dynami_cast 转换时检查的运行期类型,dynamic_cast强制转换,应该是这四种中最特殊的一个,因为他涉及到面向对象的多态性和程序运行时的状态,也与编译器的属性设置有关.
(4)reinterpret_cast:用来处理无关类型转换的,通常为操作数的位模式提供较低层次的重新解释!他仅仅是重新解释了给出的对象的比特模型,并没有进行二进制的转换!常常使用的地方:指针转向足够大的整数类型; 从整型或者enum枚举类型转换为指针;从指向函数的指针转向另一个不同类型的指向函数的指针; 从一个指向对象的指针转向另一个不同类型的指向对象的指针; 从一个指向成员的指针转向另一个指向类成员的指针!或者是类型,如果类型的成员和函数都是函数类型或者对象类型。
c++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被c++11弃用。
智能指针本质是存放在栈的模板对象,只是在栈内部包了一层指针。而栈在其生命周期结束时,其中的指针指向的堆内存也自然被释放了。因而实现了智能管理的效果,不需要考虑内存问题了。
C++ 11之前auto_ptr 在c++11之后逐渐将它弃用,而加入了 shared_ptr, weak_ptr, unique_ptr ;包含的头文件#include
auto_ptr:控制权可以随便转换(把赋值智能指针的内存交给被赋值智能指针),但是只有一个在用,用起来会受到诸多限制,所以有了下面的智能指针。
auto_ptr
unique_ptr:中的拷贝构造和赋值操作符delete了,所以也就意味着,他和auto_ptr有区别,控制权唯一,不能随意转换。它俩用法都差不多;但是如果想切换控制权的话也不是没有办法,我们可以看到还有个这样的函数:move函数;
shared_ptr:前两者控制权唯一,切换的时候把前面的清除。而shared_ptr不会,可以共享多个使用;有个地方需要注意,当删除一个智能指针时,并不影响其它两个智能指针的继续使用。因为该片内存添加了一个引用计数,每shared_ptr一次,引用计数+1;每次调用析构函数,引用计数减一。直到最后一个智能指针删除,才会释放内存。其实就是和unique_ptr一样可以通过move来切换控制权,这个时候是切换,不是共享了。auto_ptr和unique_ptr都可以通过move函数转换成shared_ptr类型,当然,一样是切换控制权的形式,即旧的置空。
weak_ptr:(1)他不像其余三种,可以通过构造函数直接分配对象内存;他必须通过shared_ptr来共享内存。(2)没有重载opreator*和->操作符,没法使用该对象 (3)不主动参与引用计数,即,share_ptr释放了,那么weak_ptr所存的对象也释放了。(4)使用成员函数use_count()可以查看当前引用计数,expired()判断引用计数是否为空。(5)lock()函数,返回一个shared_ptr智能指针;
weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况
shared_ptr不是线程安全的:
参考博客:https://blog.csdn.net/zy19940906/article/details/50470087
C++ 类构造函数初始化列表的异常机制 function-try block,https://blog.csdn.net/hikaliv/article/details/4457090;
初始化对象成员不会再调用默认构造函数,再调用复制构造函数,而是直接调用复制构造函数https://blog.csdn.net/hikaliv/article/details/4456862
堆,栈,自由存储存储(有的说法是代码区),静态存储区,常量区
在函数中,函数的局部变量的存储单元在栈上,函数执行结束后栈区自动释放。栈内存分配效率高,由操作系统和编译器自动分配,但存储空间有限。
堆区:堆区中的内存由程序员自己创建并维护,每个new(malloc)
都应该对应于一个delete(free)
,如果程序员忘记释放堆内存,则在程序最后结束后会由操作系统完成释放,但在程序运行过程中可能会造成堆区越来越大,从而造成内存溢出。
静态(全局)存储区:这部分内存区存储程序的全局变量和静态变量
常量区:常量区内存空间存储常量(包括字符串,等内容)
代码区:存放函数体的二进制代码。
堆栈的区别:(1)管理方式不同(2)空间大小不同;栈的内存空间是连续的,空间大小通常是系统预先规定好的,即栈顶地址和最大空间是确定的;而堆得内存空间是不连续的,由一个记录空间的链表负责管理,因此内存空间几乎没有限制,在32位系统下,内存空间大小可达到4G(3)能否产生碎片不同:由于管理方式的不同,所以堆更容易产生碎片。这是由于频繁的调用new/delete而造成内存空间不连续。而对于栈,其由操作系统管理,每次弹出的内存块意味着它上面的内存块也已经弹出,所以几乎不会产生碎片。(4)生长方式不同:堆的生长方向是向上的,也就是向着内存地址增加的方向;而对于栈,其的生长放下是向下的,向着内存地址减小的方向生长。(5)分配方式不同:堆是动态分配的;而栈其实具有两种分配方式,在栈的静态分配中是由编译器完成的,如局部变量的分配;动态分配的栈是由函数 alloca
完成的,虽然是动态分配的栈,但是我们也无需对其进行手工释放,也是由操作系统完成的。(6)分配效率不同: 栈是及其系统提供的数据结构,计算机对其底层提供支持,有专门的寄存器存放栈的地址,并有专门的指令执行push/pop等操作。而堆是由库函数提供的,其机制很复杂。
14.explicit, export, mutable关键字的含义
mutable 可变的,易变,类成员加上它,const function 可以修改它
c++ 默认的拷贝是欠拷贝,对于指针类型而言如果是浅拷贝,一个被析构(delete),另外一个也就完蛋了,所以要用深拷贝
c++引入右值引用和移动语义可以避免无谓的复制,提高程序的性能;c++中所有的值必然属于左值或者右值中的一种
左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)
左值是指表达式结束后依然存在的持久对象,而右值是指表达式结束时就不再存在的临时对象。T& 指向的是 lvalue,而 const T& 指向的,却可能是 lvalue 或 rvalue,左值引用&与右值引用&&(右值引用是c++11加上的)。判断左值还是右值的放法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
move函数可以是用于构造函数,也可以用于赋值函数,但都需要手动显示添加。其实move函数用直白点的话来说就是省去拷贝构造和赋值时中间的临时对象,将资源的内存从一个对象移动到(共享也可以)另一个对象。官话是:c++11 中的 move() 是这样一个函数,它接受一个参数,然后返回一个该参数对应的右值引用。
std::forward
(1)将亡值则是c++11
新增的和右值引用相关的表达式,这样的表达式通常时将要移动的对象、T&&
函数返回值、std::move()
函数的返回值等,c++11增加了右值引用,常说的引用指的是左值引用,右值引用相当于给右值取了别名,延长了右值的生命周期;左值引用只能绑定左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。右值引用其实是个左值,已经可以取地址了,拥有了地址。
(2)常量左值引用却是个奇葩,它可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。
(3)总结一下,其中T
是一个具体类型:
T&
, 只能绑定左值T&&
, 只能绑定右值const T&
, 既可以绑定左值又可以绑定右值(4)要实现移动语义就必须增加两个函数:移动构造函数和移动赋值构造函数。
move 避免了没有意义的资源申请和释放操作,以及内存间的拷贝操作;对于左值才调用拷贝构造函数,而对于右值,调用移动构造函数;c++11使用std::move将左值(比如可以转换一些局部变量,延长局部变量的生命周期)转换为右值,从而方便应用移动构造函数和移动复制构造函数,它其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是用移动构造函数吧。。。。
(5)如果编译器找不到移动构造函数,,就会对右值调用拷贝构造函数,而且把变量变成右值,变量不会立刻失效,会在变量对应的作用域之后才失效,所以变量和构造的对象会共享内存空间,产生意想不到的错误;C++11的所有容器都实现了move语义;move
只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝和资源申请;move
对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如int和char[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move
对含有资源的对象说更有意义。
(6)当右值引用和模板结合的时候,就复杂了。T&&
并不一定表示右值引用,它可能是个左值引用又可能是个右值引用:
template
void f( T&& param){
}
f(10); //10是右值
int x = 10; //
f(x); //x是左值
这里的&&
是一个未定义的引用类型,称为universal references
,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。
(7)引用叠加规则:
template
void f( T&& param); //这里T的类型需要推导,所以&&是一个 universal references
传递左值进去,就是左值引用,传递右值进去,就是右值引用。如它的名字,这种类型确实很"通用",下面要讲的完美转发,就利用了这个特性。
(8)完美转发
所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。c++中提供了一个std::forward()
模板函数解决这个问题;在universal references
和std::forward
的合作下,能够完美的转发。
(9)emplace_back减少内存拷贝和移动
emplace_back()
可以直接通过构造函数的参数构造对象,但前提是要有对应的构造函数(使用emplace_back()
替换push_back()
)。
(10) 高效的交换
template
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
对容器类而言
参考博客:https://www.jianshu.com/p/d19fc8447eaa
c++ 变量根据作用的位置有着不同的生命周期,具有不同作用域,作用域可分为6种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
从作用域上看:
局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。
静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。
从分配内存空间看:
全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在栈里分配空间。
1)、静态变量会被放在程序的静态数据存储区(数据段)(全局可见)中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
2)、变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。
把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。
函数中必须要使用static变量情况:比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针。
static 全局变量:改变作用范围,不改变存储位置
static 局部变量:改变存储位置,不改变作用范围
静态函数 :在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。
如果在一个源文件中定义的函数,只能被本文件中的函数调用,而不能被同一程序其它文件中的函数调用,这种函数也称为内部函数。定义一个内部函数,只需在函数类型前再加一个“static”关键字即可。
C++三大特性:封装, 继承,多态;
继承是为了实现代码的复用, 如果在逻辑上B是A的一种,即B类的man也是A类的 Hunman的一种我们就可以让B类去继承A类。
组合也是类的一种复用技术, 它遵循的就是如果A类是B类的一部分,则不要让B类去继承A类,而是采用组合的形式;
组合的优点:(1)不会破环封装性,父类的任何变化不会引起子类的变化;(2)组合运用复杂的设计,他们的关系是在程序运行的时候才确定,可以支持动态的组合;(3)整体类可以对局部类的接口进行封装,提供新的接口。
组合的缺点:(1)整体类不能自动获得和局部类同样的接口,只有通过创建局部的对象去调用它;(2)创建整体类的时候需要创建局部类的对象。
继承的优点:(1)子类继承了父类能自动获得父类的接口;(2)创建子类对象的时候不用创建父类对象
继承的缺点:(1)破坏了封装,父类的改变必定引起子类的改变,子类缺乏独立性;(2)支持功能上的扩展,但多重继承往往增加了系统结构的复杂度;(3) 继承是在静态编译的时候就已经确定了关系,不支持动态继承。
因此优先考虑组合而不是继承
STL map的原理:内部实现是二叉平衡树(红黑树),
平衡二叉树最大的作用就是查找,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,继续向上调整。
参考:https://blog.csdn.net/tanrui519521/article/details/80980135
平衡二叉树是二叉搜索树的一种:
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
平衡二叉树中引入了一个概念:平衡二叉树节点的平衡因子,它指的是该节点的两个子树,即左子树和右子树的高度差,即用左子树的高度减去右子树的高度,如果该节点的某个子树不存在,则该子树的高度为0,如果高度差的绝对值超过1就要根据情况进行调整。
参考博客:https://blog.csdn.net/u014634338/article/details/42465089
原子(atomic)本意是”不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作”.
(1)处理器使用总线锁保证原子性,CPU提供了在指令执行期间对总线加锁的手段,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的。
(2)使用缓冲锁保证原子性,在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销(代价)比较大。如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定;修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效。
extern C C++的编译器会对程序中的符号进行修饰,这个过程在编译器中叫符号修饰(Name Decoration)或者符号改编(Name Mangling),C++是能够兼容C的,如果我们有了一个C语言的头文件和其对于的库,在C++中如何使用它呢?在include该头文件的时候当然要加入extern "C",否则按照C++的符号进行符号修饰,那么在库中就会找不到该符号了。
为了访问其他编译单元(如另一代码文件)中的变量或对象,对普通类型(包括基本数据类、结构和类),可以利用关键字extern,来使用这些变量或对象时;但是对模板类型,则必须在定义这些模板类对象和模板函数时,使用标准C++新增加的关键字export(导出/出口/输出)。
常规的是会的:调用相应的构造函数构造function()返回的匿名对象 -> 析构function()中的对象 -> function()结束 -> 主调函数继续执行
考虑编译器的优化,类似右值作为参数,他是不会重复申请内存,而是直接延续右值变量。
参考:https://blog.csdn.net/u010029439/article/details/80808425
要实现并发有两种方法:多进程和多线程。
使用多进程并发是将一个应用程序划分为多个独立的进程(每个进程只有一个线程),这些独立的进程间可以互相通信,共同完成任务。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但这也造就了多进程并发的两个缺点:
在进程件的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。
运行多个进程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。
所以C++选用多线程并发:在同一个进程中执行多个线程。有操作系统相关知识的应该知道,线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。
++11的标准库中提供了多线程库,使用时需要#include
头文件,该头文件主要包含了对线程的管理类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通信机制
实现原理利用堆实现的
新版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。
参考:https://blog.csdn.net/hechao3225/article/details/71366058
(1)单例模式
作用:保证一个类只有一个实例,并提供一个访问它的全局访问点,使得系统中只有唯一的一个对象实例。
应用:常用于管理资源,如日志、线程池
实现要点:在类中,要构造一个实例,就必须调用类的构造函数,并且为了保证全局只有一个实例,需要提供一个全局访问点,就需要在类中定义一个static函数,返回在类内部唯一构造的实例。需防止在外部调用类的构造函数而构造实例,需要将构造函数的访问权限标记为private。
同时阻止拷贝创建对象时赋值时拷贝对象,因此也将它们声明并权限标记为private
(2)工厂模式:简单工厂模式,工厂方法模式,抽像工厂模式;工厂模式的主要作用是封装对象的创建,分离对象的创建和操作过程,用于批量管理对象的创建过程,便于程序的维护和扩展。
简单工厂模式:工厂模式最简单的一种实现,对于不同产品的创建定义一个工厂类,将产品的类型作为参数传入到工厂的创建函数,根据类型分支选择不同的产品构造函数。
工厂方法模式:工厂方法模式在简单工厂模式的基础上增加对工厂的基类抽象,不同的产品创建采用不同的工厂创建(从工厂的抽象基类派生),这样创建不同的产品过程就由不同的工厂分工解决:FactoryA专心负责生产ProductA,FactoryB专心负责生产ProductB,FactoryA和FactoryB之间没有关系;如果到了后期,如果需要生产ProductC时,我们则可以创建一个FactoryC工厂类,该类专心负责生产ProductC类产品。该模式相对于简单工厂模式的优势在于:便于后期产品种类的扩展。
抽像工厂模式:抽象工厂模式对工厂方法模式进行了更加一般化的描述。工厂方法模式适用于产品种类结构单一的场合,为一类产品提供创建的接口;而抽象工厂方法适用于产品种类结构多的场合,就是当具有多个抽象产品类型时,抽象工厂便可以派上用场。抽象工厂模式更适合实际情况,受生产线所限,让低端工厂生产不同种类的低端产品,高端工厂生产不同种类的高端产品。
(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成员不受继承方式的影响,子类永远无权访问。
内联函数是C++中的一种特殊函数,它可以像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是通过将函数体直接插入调用处来实现的,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。一般来说inline用于定义类的成员函数。inline适用的函数有两种,一种是在类内定义的成员函数,另一种是在类内声明,类外定义的成员函数;
这种情况下,我们可以不用在函数头部加inline关键字,因为编译器会自动将类内定义的函数声明为内联函数,编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)设置为内联,具有内联函数调用的性质。
根据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的作用之大。
动态查找树主要包括:二叉搜索树,平衡二叉树,红黑树,B树,B-树时间复杂度O(log2N),通过对树高度的降低可以提升查找效率
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+树遍历也更高效,其中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+树的删除:
(1)B+树的磁盘读写的代价更低
B+树内部结点没有指向关键字具体信息的指针,这样内部结点相对B树更小。B+通过最后一层可以对所有数据进行访问
(2)B+树的查询更加的稳定
因为非终端结点并不是最终指向文件内容的结点,仅仅是作为叶子结点中关键字的索引。这样所有的关键字的查找都会走一条从根结点到叶子结点的路径。所有的关键字查询长度都是相同的,查询效率相当。
参考链接:
https://www.cnblogs.com/guohai-stronger/p/9225057.html
https://blog.csdn.net/zhuanzhe117/article/details/78039692