RAII(Resource Acquisition Is Initialization ,资源获得即初始化),是一种利用对象生命周期来控制程序资源的简单技术。在对象构造时获得资源,接着控制对资源的访问在对象的生命周期内始终有效,最后在对象析构时释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。这样的好处在于:
智能指针包含在头文件 < memory >中,标准命名std空间下,有auto_ptr(已经被弃用)、shared_ptr、weak_ptr、unique_ptr。智能指针就是模拟指针动作的类,一般智能指针都会重载 **-> **和 *** **,主要作用时管理动态内存的释放。
智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。
处理资源泄漏;有指针的时候,忘记释放已经不使用的内存,从而导致内存泄漏;
处理空悬指针;虽然释放了申请的内存,但是ptr会变成空悬指针(野指针),会指向垃圾内存,因此需要将内存释放后的指针置空(ptr = nullptr;)
比较隐晦的由异常造成的资源泄漏;new创建对象后因为发生异常而忘记调用delete。
多个指针指向相同的对象,采用引用计数的方式解决赋值与拷贝问题,每个shared_ptr的拷贝都指向同一块内存,每次拷贝内部引用计数+1,每次析构内部引用计数-1,为0时自动删除所指向的堆内存。其内部的引用计数是线程安全的,但是对象读取时需要加锁。
智能指针对象中引用计数是多个智能指针对象共享的,两个线程中的引用计数同时++或者–,这个操作不是原子的,举个例子,应用计数原来是1,++了两次,可能还是2。这样引用计数就错乱了,会导致资源未释放或者程序崩溃的问题,所以其实智能指针是加锁了的,也就是说引用计数的操作是线程安全的。
声明: template < class T > class shared_ptr;
template
class sharedPointer
{
private:
class Implement
{
public:
Implement(T* p) : mPointer(p), mRefs(1) {}
~Implement() { delete mPointer; }
T* mPointer;
size_t mRefs;
};
Implement* mImplPtr;
public:
explicit sharedPointer(T *p) : mImplPtr (new Implement(p)) {} //explicit不能发生隐式类型转换,就是参数类型不匹配,就会进行隐式转换
~sharedPointer() { decrease(); } //计数递减
sharedPointer(const sharedPointer& other) : mImplPtr(other.mImplPtr) { increase(); } //计数递增
sharedPointer operator = (const sharedPointer& other)
{
if (mImplPtr != other.mImplPtr) //避免自赋值
{
decrease();
mImplPtr = other.mImplPtr;
increase();
}
return *this;
}
T* operator -> () const
{
return mImplPtr->mPointer;
}
T* operator *() const
{
return *(mImplPtr->mPointer);
}
private:
void decrease()
{
if (--mImplPtr->mRefs) == 0)
delete mImplPtr;
}
void increase()
{
++(mImplPtr->mRefs);
}
};
**make_shared:**是一种安全分配和使用动态内存的方法,主要功能是在动态内存中分配一个对象并且使用它,返回此对象的shared_ptr。
make_shared生成shared_ptr的优点和缺点:
1)效率更高,原始的 new 表达式分配对象, 然后传递给 shared_ptr (也就是使用 shared_ptr 的构造函数) 的话, shared_ptr 的实现没有办法选择, 而只能单独的分配控制块,但是make_shared内存分配,可以一次性完成,减少了内存分配的次数,所以效率高;
auto p = new widget();
shared_ptr sp1{ p }, sp2{ sp1 };
auto sp1 = make_shared(), sp2{ sp1 };
2)异常安全。比如说可能连续构造两个对象,然后再分配shared_ptr,那我构造一个对象发生可能异常,这个对象就泄漏了,就是说有可能造成shared_ptr 没有立即获得裸指针。这种情况就推荐使用make_shared来代替。
void F(const std::shared_ptr& lhs, const std::shared_ptr& rhs) { /* ... */ }
F(std::shared_ptr(new Lhs("foo")),std::shared_ptr(new Rhs("bar")));
F(std::make_shared("foo"), std::make_shared("bar"));
3)构造函数是保护或者私有的,不能使用make_shared
4)make_shared对象的内存可能没有办法及时回收。
weak_ptr 能访问资源,但不控制其生命周期的指针
weak_ptr是为了配合shared_ptr而引用的一种智能指针,专门用于解决shared_ptr循环引用(class A和class B分别被对方的智能指针所管理[循环引用会导致内存无法正确释放,从而内存泄漏])的问题,没有重载operater *和->,最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。
如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,从而获得资源的观测权。但是weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。它通过lock(),从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。
weak-ptr w;//空weak_ptr可以指向类型为T的对象
weak_ptr w(sp);//与shared_ptr sp指向相同对象的weak_ptr。T必须能够转换为sp指向的类型
w = p;//p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
w.reset();//将w置空
w.use_count();//与w共享对象的shared_ptr的数量
w.expired();//若w.use_count()为0,返回true,否则返回false
w.lock();//如果w.expired()为true,返回一个空shared_ptr,否则返回一个指向w的对象的shared_ptr
weak_ptr提供了expired()与lock()成员函数,前者用于判断weak_ptr指向的对象是否已被销毁,后者返回其所指对象的shared_ptr智能指针(对象销毁时返回”空”shared_ptr)。
template
class weak_ptr: public _Ptr_base<_Ty>
{ // class for pointer to reference counted resource
typedef typename _Ptr_base<_Ty>::_Elem _Elem;
public:
weak_ptr()
{ // construct empty weak_ptr object
}
template
weak_ptr(const shared_ptr<_Ty2>& _Other,
typename enable_if::value,
void *>::type * = 0)
{ // construct weak_ptr object for resource owned by _Other
this->_Resetw(_Other);
}
weak_ptr(const weak_ptr& _Other)
{ // construct weak_ptr object for resource pointed to by _Other
this->_Resetw(_Other);
}
template
weak_ptr(const weak_ptr<_Ty2>& _Other,
typename enable_if::value,
void *>::type * = 0)
{ // construct weak_ptr object for resource pointed to by _Other
this->_Resetw(_Other);
}
~weak_ptr()
{ // release resource
this->_Decwref();
}
weak_ptr& operator=(const weak_ptr& _Right)
{ // assign from _Right
this->_Resetw(_Right);
return (*this);
}
template
weak_ptr& operator=(const weak_ptr<_Ty2>& _Right)
{ // assign from _Right
this->_Resetw(_Right);
return (*this);
}
template
weak_ptr& operator=(shared_ptr<_Ty2>& _Right)
{ // assign from _Right
this->_Resetw(_Right);
return (*this);
}
void reset()
{ // release resource, convert to null weak_ptr object
this->_Resetw();
}
void swap(weak_ptr& _Other)
{ // swap pointers
this->_Swap(_Other);
}
bool expired() const
{ // return true if resource no longer exists
return (this->_Expired());
}
shared_ptr<_Ty> lock() const
{ // convert to shared_ptr
return (shared_ptr<_Elem>(*this, false));
}
};
weak_ptr可以观测被销毁的指针,就直接为NULL就好。不会报错的。
主要是为了解决“有异常抛出时发生内存泄漏”的问题 。因为发生异常而无法正常释放内存。auto_ptr有拷贝语义,拷贝后源对象变得无效,这可能引发很严重的问题;而unique_ptr则无拷贝语义,但提供了移动语义,这样的错误不再可能发生,因为很明显必须使用std::move()进行转移。auto_ptr不支持拷贝和赋值操作,不能用在STL标准容器中。STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,所以不能在STL中使用。
unique_ptr :独占资源所有权指针
unique_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。
需要一个计数器与类指向的对象相关联,需要跟踪该类有多少个对象共享同一指针。每次创建类的新对象是,初始化指针并且将引用计数置为1,当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加对应的引用计数。对一个对象进行赋值时,赋值操作需要将左边的对象引用计数-1(减为0的时候,就会删除对象),并且增加右操作数所指的对象的引用。调用析构函数时,构造函数减少引用计数。
智能指针是一个数据类型,一般用模板实现,模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointer
最高层的shared_ptr就是用户直接使用的类,它提供shared_ptr的构造、复制、重置(reset函数)、解引用、比较、隐式转换为bool等功能。它包含一个指向被管理对象的指针,用来实现解引用操作,并且组合了一个shared_count对象,用来操作引用计数。
但shared_count类还不是引用计数类,它只是包含了一个指向引用计数类sp_counted_base的指针,功能上是对sp_counted_base操作的封装。shared_count对象的创建、复制和删除等操作,包含着对sp_counted_base的增加和减小引用计数的操作。
最后sp_counted_base类才保存了引用计数,并且对引用计数字段提供无锁保护。它也包含了一个指向被管理对象的指针,是用来删除被管理的对象的。sp_counted_base有三个派生类,分别处理用户指定Deleter和Allocator的情况:
sp_counted_impl_p:用户没有指定Deleter和Allocator
sp_counted_impl_pd:用户指定了Deleter,没有指定Allocator
sp_counted_impl_pda:用户指定了Deleter和 Allocator
创建指针P的第一个shared_ptr对象的时候,子对象shared_count同时被建立, shared_count根据用户提供的参数选择创建一个特定的sp_counted_base派生类对象X。之后创建的所有管理P的shared_ptr对象都指向了这个独一无二的X。
然后再看虚线框内的weak_ptr就清楚了。weak_ptr和shared_ptr基本上类似,只不过weak_ptr包含的是weak_count子对象,但weak_count和shared_count也都指向了sp_counted_base。
shared_ptr SP1(new SomeObject());
shared_ptr SP2=SP1;
weak_ptr WP1=SP1;
原文链接:https://blog.csdn.net/jiangfuqiang/article/details/8292906
boost官方文档对shared_ptr线程安全性的正式表述是:shared_ptr对象提供与内置类型相同级别的线程安全性。
第一种情况是对对象的并发读,是线程安全的;第二种情况下,如果两个shared_ptr对象A和B管理的是不同对象的指针,则这两个对象完全不相关,支持并发写也容易理解。但如果A和B管理的是同一个对象P的指针,则A和B需要维护一块共享的内存区域,该区域记录P指针当前的引用计数。对A和B的并发写必然涉及对该引用计数内存区的并发修改,需要做额外的工作。
原文链接:https://blog.csdn.net/jiangfuqiang/article/details/8292906
应该说控制块是线程安全的,指针拷贝不是,c++20有原子智能指针。
线程是否安全是指的对象在内存本身被多个线程更改是否安全!所有要看对象在内存中的结构,会不会被自身的函数成员在不同线程下造成错误修改。
shared_ptr只有两个指针数据成员,sizeof(std::shared_ptr)=16,一个是Ptr裸指针在x64上是8字节,另外一个是_Ref_count_base对象的指针是8字节。两个加起来16字节。该对象有两个成员_Uses和_Weaks。这个对象是在std::make_shared时候创建的,可以在源码中查看到。所以算是考虑shared_ptr和_Ref_count_base两个对象在内存中是否线程安全。
其中__Ptr是不会被改写的,__Uses和__Weaks都是原子操作!所以shared_ptr是线程安全的。但是你解引用指向的对象的线程安全,是不能保证的!
原文链接:https://www.zhihu.com/question/56836057/answer/1472597928
**处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。**首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。Pentium 6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器是不能自动保证其原子性的,比如跨总线宽度、跨多个缓存行和跨页表的访问。但是,处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写操作(i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。举个例子,如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2,如图下图所示。
CPU1 | CPU2 |
---|---|
i = 1 | i = 1 |
i + 1 | i + 1 |
i = 2 | i = 2 |
原因可能是多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入系统内存中。那么,想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
第二个机制是通过缓存锁定来保证原子性。在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在Pentium 6和目前的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。
所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效,在如上图所示的例子中,当CPU1修改缓存行中的时使用了缓存锁定,那么CPU2就不能使用同时缓存 i 的缓存行。
原文链接:https://blog.csdn.net/f110300641/article/details/83510081
1、利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;
2、每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。
3、lambda表达式的语法定义如下:
[capture] (parameters) mutable ->return-type {statement};
4、lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;
capture是捕获列表;params是参数表;opt是函数选项(可以是mutable说明lambda表达式体内的代码可以修改被捕获的变量、exception说明lambda表达式是否抛出异常以及何种异常、attribute声明属性);ret是返回值类型;body是函数体
捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。
class A
{
public:
int i_ = 0;
void func(int x,int y){
auto x1 = [] { return i_; }; //error,没有捕获外部变量
auto x2 = [=] { return i_ + x + y; }; //OK
auto x3 = [&] { return i_ + x + y; }; //OK
auto x4 = [this] { return i_; }; //OK
auto x5 = [this] { return i_ + x + y; }; //error,没有捕获x,y
auto x6 = [this, x, y] { return i_ + x + y; }; //OK
auto x7 = [this] { return i_++; }; //OK
};
int a=0 , b=1;
auto f1 = [] { return a; }; //error,没有捕获外部变量
auto f2 = [&] { return a++ }; //OK
auto f3 = [=] { return a; }; //OK
auto f4 = [=] {return a++; }; //error,a是以复制方式捕获的,无法修改
auto f5 = [a] { return a+b; }; //error,没有捕获变量b
auto f6 = [a, &b] { return a + (b++); }; //OK
auto f7 = [=, &b] { return a + (b++); }; //OK
注意f4,虽然按值捕获的变量值均复制一份存储在lambda表达式变量中,修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。
原因:lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获的任何外部变量,最终会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。
int a = 0;
auto f1 = [=] { return a++; }; //error
auto f2 = [=] () mutable { return a++; }; //OK
lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了**()运算符),我们称为闭包类型**(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };
a = b; // 非法,lambda无法赋值
auto c = a; // 合法,生成一个副本
闭包类型禁用了赋值操作符,但是没有禁用复制构造函数,所以你仍然可以用一个lambda表达式去初始化另外一个lambda表达式而产生副本。
在多种捕获方式中,最好不要使用[=]和[&]默认捕获所有变量。
默认引用捕获所有变量,你有很大可能会出现悬挂引用(Dangling references),因为引用捕获不会延长引用的变量的生命周期.
原文链接:https://blog.csdn.net/jiange_zh/article/details/79356417
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应某种特定的类型说明符(例如 int)不同,auto 让编译器通过初始值来进行类型推演。从而获得定义变量的类型,所以说auto定义的变量必须有初始值。
decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 sizeof 很相似。
有的时候我们还会遇到这种情况,**我们希望从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。**还有可能是函数的返回类型为某表达式的值类型。在这些时候auto显得就无力了,所以C++11又引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进行实际的计算表达式的值。
decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。
C++11 提供了统一的语法来初始化任意的对象。C++11 还把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁。
左值往往有对应的内存地址,右值就是一个数据值。就是左值的生命周期不止于这句话,右值用完这句话就挂了。
左值:表示的是可以获取地址的表达式,它能出现在赋值语句的左边,对该表达式进行赋值。但是修饰符const的出现使得可以声明如下的标识符,它可以取得地址,但是没办法对其进行赋值。
const int& a = 10;
右值:表示无法获取地址的对象,有常量值、函数返回值、lambda表达式等。无法获取地址,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值。
在C++11之前,只有左值才可以被引用,C++11后右值也可以被引用(&&)。右值引用的意义就是为临时变量续命,就是为右值续命,因为以前右值在表达式结束后就消亡了,如果想继续使用右值,就会动用昂贵的拷贝函数。右值引用是用来支持转移语义的,转移语义可以将资源从一个对象转移到另一个对象,这样的好处就是减少不必要的临时对象的创建、拷贝及销毁,能够大幅度提高C++应用程序的性能,就是通过转移语义,临时对象中的资源能够转移到其他对象中。
std::move,将左值强行转化为右值使用。执行一个无条件的转化到右值。它本身并不移动任何东西。
std::forword 完美转发,将一组实参“完美”地传递给形参,完美指的是参数的const属性与左右值属性不变。就是说,函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。
总的来说, C++11正是通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝的问题,通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去,通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)。
左值引用:传统的C++中引用被称为左值引用。
左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
右值引用:C++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。
右值引用用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std:*move()**将左值强制转换为右值。
T&&,T是被推导的类型,那这个变量或者参数就是一个万能引用
在C语言中,NULL被定义为:#define NULL ((void*)0),就是说NULL实际上时一个空指针,在进行赋值给类似于int或者char型的指针时,会发生隐式类型转换,把void转换成相应类型的指针。
在C++中,C++时强类型语言,void*不能隐式转换成其他类型的指针,NULL实际上是0,编译器提供的头文件做了相应的处理,C++中NULL是0,但是在代码中用NULL替代0表示空指针在函数重载时会发生二义性(就是说用NULL输入到函数中,进行重载,会选择Int这个形参,跟我们想要使用的空指针不一样),为了解决这个二义性,采用nullptr来代表空指针。