参考: 《Boost 程序库完全开发指南:深入 C++ 准标准库 》 第三版
1、为什么需要使用智能指针
内存泄漏
C++在堆上申请内存后,需要手动对内存进行释放。代码的初创者可能会注意内存的释放,但随着代码协作者加入,或者随着代码日趋复杂,很难保证内存都被正确释放。尤其是一些代码分支在开发中没有被完全测试覆盖的时候,就算是内存泄漏检查工具也不一定能检查到内存泄漏。
多线程下对象析构问题
多线程遇上对象析构,是一个很难的问题,稍有不慎就会导致程序崩溃。因此在对于 C++开发者而言,经常会使用静态单例来使得对象常驻内存,避免析构带来的问题。这势必会造成内存泄露,当单例对象比较大,或者程序对内存非常敏感的时候,就必须面对这个问题了。
智能指针
智能指针在 C++11 标准中被引入真正标准库(C++98 中引入的 auto_ptr 存在较多问题),但还有很多 C++开发者仍习惯用原生指针,视智能指针为洪水猛兽。但很多实际场景下,智能指针却是解决问题的神器,尤其是一些涉及多线程的场景下。
RAII (Resource Acquisition Is Initialization)
RAII 就是 "资源获得即初始化"。
-
智能指针的基本哲学是 RAII,RAII 的理念是将资源的获取放在类的构造函数里,资源的释放放在类的析构函数里。在类的生存期结束的时候,析构函数会被自动调用,对应的资源将会释放。RAII 的思想还应用到例如
std::lock_guard
之类的用于对锁、线程、socket、handle 等资源的管理
资源是什么
-
资源: 程序中需要获取后才能用,然后需要隐式(implictly)或者显式(explicitly)释放的东西。例如内存、文件句柄、socket 和锁等。如果没有释放,可能会造成资源泄漏或者程序出错。资源泄漏在简单调试中可能无法发现,但是在长期运行的时候可能会吃尽系统的可获得的资源,导致程序崩溃
C/C++面试:什么是智能指针?智能指针有什么作用?分为哪几种?各自有什么样的特点?
:https://blog.csdn.net/zhizhengguan/article/details/112302192
因为 C++ 使用内存的时候很容易出现野指针、悬空指针、内存泄露的问题。所以C++11引入了智能指针来管理内存。有四种:
- auto_ptr:已经不用了
- unique_ptr:独占式指针,同一时刻只能有一个指针指向同一个对象
- shared_ptr:共享式指针,同一时刻可以有多个指针指向同一个对象
- weak_ptr:用来解决shared_ptr相互引用导致的死锁问题
unique_ptr 能否被另一个 unique_ptr 拷贝呢?
- 不能,因为它把它的拷贝构造函数private了。但是它提供了一个移动构造函数,所以可以通过std::move将指针指向的对象交给另一个unique_ptr,转交之后自己就失去了这个指针对象的所有权,除非被显示交回
unique_ptr 和 shared_ptr 的区别
(1)
- unique_ptr代表的是专属所有权,不支持复制和赋值。但是可以移动
- shared_ptr 代表的是共享所有权,shared_ptr 是支持复制的和赋值以及移动的
(2)资源消耗上
- unique_ptr 在默认情况下和裸指针的大小是一样的。
所以 内存上没有任何的额外消耗,性能是最优的,我们大多数场景下用到的应该都是 unique_ptr。 - shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。因此相比于 unique_ptr, shared_ptr 的内存占用更高。在使用 shared_ptr 之前应该考虑,是否真的需要使用 shared_ptr, 而非 unique_ptr。
unique_ptr = unique_ptr和shared_ptr=shared_ptr这两个操作有什么后果呢?
先来说下unique_ptr :
- 这个操作是不允许的,因为unique_ptr它的原理是将拷贝构造和拷贝赋值私有化,但是它提供了移动构造和移动赋值。所以如果你想要使用=赋值,必须先把右边的用std::move包裹一下,这样右边的unique_ptr就会失去所有权,左边的unique_ptr就会得到对应对象的所有权
至于shared_ptr:
- 对于左边的指针,它会将自己的引用计数减一,然后检测一下是不是减到了0,如果是,那么delete所管理的对象
- 然后将右边的引用计数和管理对象赋值给左边,此时两边指向同一个对象,共享同一个引用计数,然后引用计数++
shared_ptr 的移动赋值时发生了什么事情
- 首先它会检查本指针和参数指针是不是同一个对象,如果是,直接返回
- 然后,先把本指针的引用变量–,如果发现减到了0,就把参数指针和参数引用变量析构掉并置NULL
- 最后,本指针和参数指针指向同一个对象以及引用计数,然后引用计数自增1
shared_ptr 什么时候会被释放掉,就是什么时候引用次数为0?
- 在一个shared_ptr析构时,或者被赋值时,强引用计数会减1。减到0就delete资源
shared_ptr 你是怎么实现的
shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。当引用计数为 0 的时候,则 delete 内存。
关键在于多个对象持有同一个引用对象,第一次创建指针的时候可以new一个int,然后大家都有一个int指针就可以共用了
shared_ptr应该要有三个成员:
- 裸指针:指向所要管理的对象
- 强引用计数:就是一个int指针,记录了有多少个shared_ptr指向裸指针
- 弱引用计数:也是一个int指针,记录了有多少个weak_ptr指向裸指针
shared_ptr是不是线程安全
- 不是
- 引用计数的增减是原子操作没问题,但是shared_pytr的读写本身不只包括引用计数操作,还包括资源所有权的操作,这两个操作合起来不是原子的
- 如果要求线程安全必须加锁
sharedPtr在64位操作系统下大小有多大
- 两个指针,64字节
shared_ptr和weak_ptr之间是什么关系?
- weak_ptr是用来辅助shared_ptr的,每一个weak_ptr它指向weak_ptr而不是实际的操作函数
为什么推荐用makeshared创建指针
优点是:
- 它分配的时候,只分配一次,而shared_ptr的构造函数需要分配两次。效率更高更安全
缺点是: - 如果有weak_ptr不推荐使用,否则会出现知道最后一个weak_ptr被释放了才真正去释放管理的对象
为什么要用 shared_from_this?
我们往往会需要在类内部使用自身的 shared_ptr,例如:
class Widget
{
public:
void do_something(A& a)
{
a.widget = 该对象的 shared_ptr;
}
}
我们需要把当前 shared_ptr 对象同时交由对象 a 进行管理。意味着,当前对象的生命周期的结束不能早于对象 a。因为对象 a 在析构之前还是有可能会使用到 a.widget。
如果我们直接 a.widget = this;, 那肯定不行, 因为这样并没有增加当前 shared_ptr 的引用计数。shared_ptr 还是有可能早于对象 a 释放。
如果我们使用 a.widget = std::make_shared(this);,肯定也不行,因为这个新创建的 shared_ptr,跟当前对象的 shared_ptr 毫无关系。当前对象的 shared_ptr 生命周期结束后,依然会释放掉当前内存,那么之后 a.widget 依然是不合法的。
对于这种,需要在对象内部获取该对象自身的 shared_ptr, 那么该类必须继承 std::enable_shared_from_this。代码如下:
class Widget : public std::enable_shared_from_this<Widget>
{
public:
void do_something(A& a)
{
a.widget = shared_from_this();
}
}
这样才是合法的做法。
weak_ptr的原理?
- shared_ptr中有一个成员时,弱引用计数。往weak_ptr被赋值时,弱引用计数自增1
weak_ptr的使用场景
- 用来解决悬空指针问题。通过std::shared_ptr管理数据并将std::weak_ptr提供给数据用户,用户可以通过expired()或者lock()来检测数据的有效性
- 打破shared_ptr相互引用导致死锁的问题。方法:将任意一个改为weak_ptr
- 有时候我们需要“如果对象还活着,就调用它的成员函数,否则忽略之”的语意
- 缓存对象
关于shared_ptr相互引用具体讲一下
weak_ptr 是为了解决 shared_ptr 双向引用的问题。即:
class B;
struct A{
shared_ptr<B> b;
};
struct B{
shared_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
对于这种情况, 我们可以使用 weak_ptr:
class B;
struct A{
shared_ptr<B> b;
};
struct B{
weak_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
weak_ptr 不会增加引用计数,因此可以打破 shared_ptr 的循环引用。
通常做法是 parent 类持有 child 的 shared_ptr, child 持有指向 parent 的 weak_ptr。这样也更符合语义。
weak_ptr如何检测指针是否被销毁
expired():
- 判断强引用计数是否为0
- 如果返回true,那么被观测的对象(也就是shared_ptr管理的资源)已经不存在了
如何将weak_ptr转换为shared_ptr
用lock():
- 如果expired()为true,返回一个空shared_ptr,否则返回非空shared_ptr。
lambda有哪几种捕获方式
用lambda捕获uniquePtr应该怎么做
引用捕获,因为uniquePtr没有拷贝构造
- 我们在lambda表达式中使用的是按值传递; 按值传递就会产生副本,就会产生一个unique_ptr的copy; 但是我们知道的,这显然是错误。
- 解决方法很简单,就是按引用传递替代按指针传递:
auto str = std::make_unique<std::string>("my string");
auto lambda = [capturedStr = std::move(str)]{
std::cout << *capturedStr.get() << std::endl;
};
lambda();
如果我们想要修改捕获的值呢?
lambda表达式默认是const的,我们当然不能std::move一个const对象。
解决方法也很简单,就是加入关键字mutable
auto str = std::make_unique<std::string>("my string");
auto lambda = [capturedStr = std::move(str)] ()mutable {
std::cout << *capturedStr.get() << std::endl;
auto str2 = std::move(capturedStr);
std::cout << *str2 << std::endl;
};
lambda();
用lambda捕获sharedPtr要怎么做
值捕获,避免调用时sharedPtr对象已销毁
捕获sharedPtr时如果不想延长对象生命周期怎么做
先赋值给weakPtr,再捕获
如何将 weak_ptr 转换成 shared_ptr 类型
如何将 unique_ptr 转换成 shared_ptr 类型
转换前提:如果unique_ptr为右值时,可以赋值给shared_ptr
原因:因为shared_ptr包含一个显示构造函数,可以用于将右值unique_ptr转换为shared_ptr,shared_ptr将接管原来归unique_ptr所拥有的对象
auto myfunc()
{
return unique_ptr<string>(new string("good luck")); //这是一个右值(临时对象都是右值)
}
void test18()
{
shared_ptr<string> ps1 = myfunc(); //可以
unique_ptr<int> pi1(new int(1));
shared_ptr<int> pi2 = std::move(pi1); //可以,移动后pi1被置空
// unique_ptr<int> pi3(new int(100));
// shared_ptr<int> pi4(pi3);//不可以,原因pi4的参数不是右值
}
总结
因为C++的内存管理很容易出现问题:
- 野指针:一些内存单元已经释放,但是之前指向它的指针还在被使用
- 重复释放:试图去释放已经释放的
- 内存泄露:应该释放的没有被释放
所以C++引入了智能指针。智能指针可以让对象退出作用域时,自动delete构造函数
智能指针是一个RAII类模板,用于动态分配内存,其设计思想是将基本类型指针封装为(模板)类对象指针,并在离开作用域时调用析构函数,使用delete删除指针所执行的内存空间。
分为auto_ptr、unique_ptr、shared_ptr和weak_ptr四种,各自的特点:
auto_ptr
,实现了独占式拥有的概念,同一时间只能由一个只能指针可以指向该对象;但auto_ptr已经被C++11放弃,其主要问题在于:- 对象所有权的转移,比如在函数传参的过程中,对象所有权不会返还,从而存在潜在的内存崩溃的问题
- 不能指向数组,也不能作为STL容器的成员
unique_ptr
:- 独占式指针,与所指对象的内存绑定紧密,禁止其他智能指针与其他共享同一个对象。也就是同一时间只能有一个智能指针可以指向该对象
- 独占的意思是不可以复制(拷贝构造和拷贝复制),但是我们可以利用std::move将其转移给其他unique_ptr(可以移动构造和移动赋值)。一旦转移,这个所有权就会失去,除非被显示归还
- 从实现上来讲,unique_ptr是一个删除了拷贝构造函数,保留了移动构造函数的指针类型。可以使用右值对unique_ptr进行构造
shared_ptr
:- 实现了共享式拥有的概念,即多个智能指针可以指向相同的对象,该对象以及相关资源会在其所指对象不再使用之后,自动释放与对象相关的资源
- 它是使用计数机制来表明资源被几个指针共享
- 可以通过成员函数 use_count() 来查看资源的所有者个数,除了可以通过 new 来构造,还可以通过传⼊auto_ptr,unique_ptr,weak_ptr 来构造。当我们调⽤ release() 时,当前指针会释放资源所有权,计数减⼀。当计数等于 0时,资源会被释放。
- shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的),在使⽤引⽤计数的机制上提供了可以共享所有权的智能指针。
- 因为使用shard_ptr仍然需要new来调用,这使得代码出现了不对称,std::make_shared_ptr就可以显示消除这个new
weak_ptr
,解决shared_ptr相互引用的问题,两个指针的引用计数永远不会下降为0,从而导致死锁问题。而weak_ptr是对对象的一种弱引用,可以绑定到shared_ptr,但不会增加对象的引用计数。- weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能智能,因为它没有普通指针的行为,没有重置*和→。它得最大作用是协助shared_ptr工作,像是旁观者那样观测资源的使用情况
- weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。进行该对象的内存管理的是那个强引用的shared_ptr
- weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr设计的目的是为了配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它只可以从一个 shared_ptr 或另⼀个 weak_ptr 对象构造,,它的构造和析构不会引起引用计数的增加或者减少
- weak_ptr 是⽤来解决 shared_ptr 相互引⽤时的死锁问题,如果说两个 shared_ptr 相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的⼀种弱引⽤,不会增加对象的引⽤计数,和shared_ptr 之间可以相互转化,shared_ptr 可以直接赋值给它,它可以通过调⽤ lock 函数来获得shared_ptr。
- 当两个智能指针都是 shared_ptr 类型的时候,析构时两个资源引⽤计数会减⼀,但是两者引⽤计数还是为 1,导致跳出函数时资源没有被释放(的析构函数没有被调⽤),解决办法:把其中⼀个改为weak_ptr就可以。
- weak_ptr可以从一个shared_ptr和另一个weak_ptr对象构造,它是一种弱引用(shared_ptr是一种强引用),弱引用不会引起引用计数的增加,它提供了一个expired()方法用来判断当前weak_ptr是否为空或者执行的堆内存是否已经被释放了。提供了一个lock(),如果当前weak_ptr已经过期,那么就会返回一个空的shared_ptr指针,否则,会返回实际的shared_ptr指针
shared_ptr VS unique_ptr
- 尽可能用unique_ptr
- shared_ptr应该仅在绝对需要共享时使用
- 避免使用任何其他指针,如weakptr、raw指针等。
从技术上讲,所有unique_ptr都可以被shared_ptr替换而不会出现编译错误,但我们仍然使用unique_ptr,原因如下:
- unique_ptr具有与C++原始指针“几乎”相同的效率,包括大小分配、引用/取消引用(假设没有自定义构造函数)。
- unique_ptr明确了对象的所有权和生存时间。
- shared_ptr的大小是原始指针的两倍,堆栈更大。
- 最重要的是,shared_ptr需要维护引用计数,并且该操作必须是原子的(即线程安全的),这与unique_ptr相比增加了恒定的开销。
因此,我们不应该仅仅因为方便而使用shared_ptr:对于shared_pcr的每次使用,我们都需要明确解释为什么这应该是一个共享指针,以及需要共享这个对象的模块/函数是什么。
避免使用所有其他指针,如weak_ptr或raw指针,因为在大多数情况下,它们可以被unique_ptr/shared_ptr替换。然而,显然也有例外,例如,所有QT应用程序都建议使用原始指针来管理UI组件。
2、智能指针使用
关键字:c++ 智能指针 使用场景
C++ 智能指针最佳实践&源码分析:https://zhuanlan.zhihu.com/p/436290273
C++11 智能指针实战应用:https://zhuanlan.zhihu.com/p/440653774
关于c++ 智能指针的使用场景。智能指针能完全替代new\delete吗?https://www.zhihu.com/question/501350105
C++ 智能指针 使用场景:https://xie.infoq.cn/article/d7850479aa075a82126099de6
auto_ptr 是 C++98 提出的,C++11 已将其摒弃,并提出了 unique_ptr 替代 auto_ptr。但在实际项目中仍可使用 auto_ptr ,但建议使用更加安全的 unique_ptr。shared_ptr 和 weak_ptr 则是 C+11 从准标准库 Boost 中引入的两种智能指针。此外,Boost 库还提出了 boost::scoped_ptr、boost::scoped_array、boost::intrusive_ptr 等智能指针,虽然尚未得到 C++ 标准采纳,但是在开发实践中可以使用。
一个智能指针就是一个C++的对象,这个对象的行为像一个指针,但是它却可以在其不需要的时候自动删除。注意这个“其不需要的时候”, 这可不是一个精确的定义。这个不需要的时候可以指好多方面:局部变量退出函数作用域、类的对象被析构……。
示例代码:
#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
A(){ cout<<"A constructor"<<endl; }
~A(){ cout<<"A destructor"<<endl;}
void dis(){ cout<<"Are you MI fans"<<endl; }
private:
int data;
};
//自定义智能指针
class SPA
{
public:
SPA(A* p)
:_p(p)
{
cout<<"SPA"<<endl;
}
~SPA()
{
cout<<"~SPA"<<endl;
delete _p;
}
A* operator ->() //重载 -> 运算符,返回值是一个指针
{
return _p;
}
A& operator *() //重载 解引用 * 运算符,
{
return *_p;
}
private:
A * _p;
};
void foo()
{
SPA a(new A); //自定义的智能指针,因为没有使用模板,所以只能指向A的对象
cout<<"---------------------------------"<<endl;
SPA b((A*)23);
a->dis(); // (a.operator ->())->dis();
(*a).dis(); //(a.operator *()).dis();;
//“auto_ptr”是系统提供的智能指针,使用模本,通用编程
auto_ptr<A> p(new A);
p->dis();
}
int main()
{
foo();
return 0;
}
使用模版实现代码:
#include<iostream>
using namespace std;
template<typename T>
class My_Smart_Ptr
{
public:
My_Smart_Ptr(T *data)
{
if(NULL == data) this->data = NULL;
else this->data = data;
}
~My_Smart_Ptr()
{
if(NULL != this->data) delete this->data;
this->data = NULL;
}
T& operator *() const
{
return *(this->data);
}
T* operator &() const
{
return this->data;
}
private:
T *data;
};
int main()
{
int *pInt = new int(10);
cout<<*pInt<<endl;
My_Smart_Ptr<int> mp1(pInt);
cout<<*mp1<<endl;
*mp1 = 20;
cout<<*mp1<<endl;
cout<<*pInt<<endl;
My_Smart_Ptr<int> mp2(new int);
*mp2 = 30;
cout<< *mp2 <<endl;
cout<<"pInt: "<<pInt<<endl; // 输出16进地址
// mp1 是一个对象,使用取地址符,调用的是 operator &() const 这个重载函数,输出的地址和 pInt 地址一样
cout<<"&mp1: "<<&mp1<<endl;
return 0;
}
运行截图:
boost定义了多个不同的智能指针来管理不同的场景。
shared_ptr<T> | 内部维护一个引用计数器来判断此指针是不是需要被释放。是boost中最常用的智能指针了。 |
scoped_ptr<t> | 当这个指针的作用域消失之后自动释放 |
intrusive_ptr<T> | 也维护一个引用计数器,比shared_ptr有更好的性能。但是要求T自己提供这个计数器。 |
weak_ptr<T> | 弱指针,要和shared_ptr 结合使用 |
shared_array<T> | 和shared_ptr相似,但是访问的是数组 |
scoped_array<T> | 和scoped_ptr相似,但是访问的是数组 |
简介
由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete。程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见。
用智能指针便可以有效缓解这类问题,本文主要讲解参见的智能指针的用法。包括:std::auto_ptr、boost::scoped_ptr、boost::shared_ptr、boost::scoped_array、boost::shared_array、boost::weak_ptr、boost:: intrusive_ptr。你可能会想,如此多的智能指针就为了解决new、delete匹配问题,真的有必要吗?看完这篇文章后,我想你心里自然会有答案。
下面就按照顺序讲解如上 7 种智能指针(smart_ptr)。
具体使用
对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放有它管理的堆内存。所有智能指针都重载了“operator->”操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用“.”操作符。
访问智能指针包含的裸指针则可以用 get() 函数。由于智能指针是一个对象,所以if (my_smart_object)永远为真,要判断智能指针的裸指针是否为空,需要这样判断:if (my_smart_object.get())。
智能指针包含了 reset() 方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。
我们编写一个测试类来辅助分析:
class Simple
{
public:
Simple(int param = 0)
{
number = param;
std::cout << "Simple: " << number << std::endl;
}
~Simple()
{
std::cout << "~Simple: " << number << std::endl;
}
void PrintSomething()
{
std::cout << "PrintSomething: " << info_extend.c_str() << std::endl;
}
std::string info_extend;
int number;
};
std::auto_ptr
std::auto_ptr 属于 STL,当然在 namespace std 中,包含头文件 #include<memory> 便可以使用。std::auto_ptr 能够方便的管理单个堆内存对象。
我们从代码开始分析:
void TestAutoPtr()
{
std::auto_ptr<Simple> my_memory(new Simple(1)); // 创建对象,输出:Simple:1
if (my_memory.get()) { // 判断智能指针是否为空
my_memory->PrintSomething(); // 使用 operator-> 调用智能指针对象中的函数
my_memory.get()->info_extend = "Addition"; // 使用 get() 返回裸指针,然后给内部对象赋值
my_memory->PrintSomething(); // 再次打印,表明上述赋值成功
(*my_memory).info_extend += " other"; // 使用 operator* 返回智能指针内部对象,然后用“.”调用智能指针对象中的函数
my_memory->PrintSomething(); // 再次打印,表明上述赋值成功
}
} // my_memory 栈对象即将结束生命期,析构堆对象 Simple(1)
/*
执行结果为:
Simple: 1
PrintSomething:
PrintSomething: Addition
PrintSomething: Addition other
~Simple: 1
上述为正常使用 std::auto_ptr 的代码,一切似乎都良好,无论如何不用我们显示使用该死的 delete 了。
其实好景不长,我们看看如下的另一个例子:
*/
void TestAutoPtr2()
{
std::auto_ptr<Simple> my_memory(new Simple(1));
if (my_memory.get())
{
std::auto_ptr<Simple> my_memory2; // 创建一个新的 my_memory2 对象
my_memory2 = my_memory; // 复制旧的 my_memory 给 my_memory2
my_memory2->PrintSomething(); // 输出信息,复制成功
my_memory->PrintSomething(); // 崩溃
}
}
/*
最终如上代码导致崩溃,如上代码时绝对符合 C++ 编程思想的,居然崩溃了,
跟进 std::auto_ptr 的源码后,我们看到,罪魁祸首是“my_memory2 = my_memory”,这行代码,
my_memory2 完全夺取了 my_memory 的内存管理所有权,导致 my_memory 悬空,最后使用时导致崩溃。
所以,使用 std::auto_ptr 时,绝对不能使用“operator=”操作符。
作为一个库,不允许用户使用,确没有明确拒绝[1],多少会觉得有点出乎预料。
看完 std::auto_ptr 好景不长的第一个例子后,让我们再来看一个:
*/
void TestAutoPtr3()
{
std::auto_ptr<Simple> my_memory(new Simple(1));
if (my_memory.get())
{
my_memory.release();
}
}
/*
执行结果为:
Simple: 1
看到什么异常了吗?我们创建出来的对象没有被析构,没有输出“~Simple: 1”,导致内存泄露。
当我们不想让 my_memory 继续生存下去,我们调用 release() 函数释放内存,
结果却导致内存泄露(在内存受限系统中,如果my_memory占用太多内存,我们会考虑在使用完成后,立刻归还,
而不是等到 my_memory 结束生命期后才归还)。
正确的代码应该为:
*/
void TestAutoPtr3()
{
std::auto_ptr<Simple> my_memory(new Simple(1));
if (my_memory.get()) {
Simple* temp_memory = my_memory.release(); //让出所有权
delete temp_memory;
}
}
//或
void TestAutoPtr3()
{
std::auto_ptr<Simple> my_memory(new Simple(1));
if (my_memory.get())
{
my_memory.reset(); // 释放 my_memory 内部管理的内存
}
}
/*
原来 std::auto_ptr 的 release() 函数只是让出内存所有权,这显然也不符合 C++ 编程思想。
总结:std::auto_ptr 可用来管理单个对象的对内存,但是,请注意如下几点:
(1)尽量不要使用“operator=”。如果使用了,请不要再使用先前对象。
(2)记住 release() 函数不会释放对象,仅仅归还所有权。
(3)std::auto_ptr 最好不要当成参数传递(读者可以自行写代码确定为什么不能)。
(4)由于 std::auto_ptr 的“operator=”问题,有其管理的对象不能放入 std::vector 等容器中。
(5) ……
使用一个 std::auto_ptr 的限制还真多,还不能用来管理堆内存数组,这应该是你目前在想的事情吧,我也觉得限制挺多的,哪天一个不小心,就导致问题了。
由于 std::auto_ptr 引发了诸多问题,一些设计并不是非常符合 C++ 编程思想,所以引发了下面 boost 的智能指针,boost 智能指针可以解决如上问题。
让我们继续向下看。
*/
boost::scoped_ptr
scoped_ptr
boost::scoped_ptr是一个简单的智能指针,它能够保证在离开作用域后对象被自动释放。
boost::scoped_ptr的实现是利用了一个栈上的对象去管理一个堆上的对象,从而使得堆上的对象随着栈上的对象销毁时自动删除。
boost::scoped_ptr 特点
- 不能转换所有权
scoped_ptr所管理的对象生命周期仅仅局限于一个区间(该指针所在的"{}"之间),无法传到区间之外,这就意味着scoped_ptr对象是不能作为函数的返回值的。 - 不能共享所有权
这个特点一方面使得该指针简单易用。另一方面也造成了功能的薄弱:不能用于STL的容器中。(用shared_ptr代替) - 不能用于管理数组对象
由于scoped_ptr是通过delete来删除所管理对象的,而数组对象必须通过deletep[]来删除,因此scoped_ptr是不能管理数组对象的,如果要管理数组对象需要使用boost::scoped_array类。scoped_ptr 不能指向一块能够动态增长的内存区域(用scoped_array代替)
boost::scoped_ptr 属于 boost 库,定义在 namespace boost 中,包含头文件 #include<boost/smart_ptr.hpp> 便可以使用。boost::scoped_ptr 跟 std::auto_ptr 一样,可以方便的管理单个堆内存对象,特别的是,boost::scoped_ptr 独享所有权,避免了 std::auto_ptr 恼人的几个问题。我们还是从代码开始分析:
void TestScopedPtr()
{
boost::scoped_ptr<Simple> my_memory(new Simple(1));
if (my_memory.get())
{
my_memory->PrintSomething();
my_memory.get()->info_extend = "Addition";
my_memory->PrintSomething();
(*my_memory).info_extend += " other";
my_memory->PrintSomething();
my_memory.release(); // 编译 error: scoped_ptr 没有 release 函数
std::auto_ptr<Simple> my_memory2;
my_memory2 = my_memory; // 编译 error: scoped_ptr 没有重载 operator=,不会导致所有权转移
}
}
/*
首先,我们可以看到,boost::scoped_ptr 也可以像 auto_ptr 一样正常使用。但其没有 release() 函数,不会导致先前的内存泄露问题。
其次,由于 boost::scoped_ptr 是独享所有权的,所以明确拒绝用户写“my_memory2 = my_memory”之类的语句,可以缓解 std::auto_ptr 几个恼人的问题。
由于 boost::scoped_ptr 独享所有权,当我们真真需要复制智能指针时,需求便满足不了了,如此我们再引入一个智能指针,专门用于处理复制,参数传递的情况,这便是如下的 boost::shared_ptr。
*/
boost::shared_ptr
shared_ptr
share_ptr 高级议题(参看)Boost 程序库完全开发指南:深入 C++ 准标准库 》
boost::scoped_ptr虽然简单易用,但它不能共享所有权的特性却大大限制了其使用范围,shared_ptr可以解决这一局限。在标准C++中为 std::shared_ptr ,在Boost C++库里,这个智能指针命名为boost::shared_ptr。
shared_ptr的管理机制其实并不复杂,就是对所管理的对象进行了引用计数,当新增一个shared_ptr对该对象进行管理时,就将该对象的引用计数加一;减少一个shared_ptr对该对象进行管理时,就将该对象的引用计数减一,如果该对象的引用计数为0的时候,说明没有任何指针对其管理,才调用delete释放其所占的内存。
boost::shared_ptr 特点
- 可以共享对象的所有权
和scoped_ptr相比shared_ptr可以共享对象的所有权,所以保存在容器中的拷贝(包括容器在需要时额外创建的拷贝)都是和原件相同的,可以在标准容器中安全的使用动态分配的对象。 - 线程安全的
shared_ptr 对象提供与内建类型一样的线程安全级别。一个 shared_ptr 实例可以同时被多个线程“读”(仅使用不变操作进行访问)。 不同的 shared_ptr 实例可以同时被多个线程“写入”(使用类似 operator= 或 reset 这样的可变操作进行访问)(即使这些实例是拷贝,而且共享下层的引用计数)。 任何其它的同时访问的结果会导致未定义行为。”- 同一个shared_ptr被多个线程“读”是安全的。
- 同一个shared_ptr被多个线程“写”是不安全的。
- 共享引用计数的不同的shared_ptr被多个线程”写“是安全的。
- 支持自定义的deleter
- 不能指向一块动态增长的内存(用share_array代替)
boost::shared_ptr 注意事项
- shared_ptr可以当做函数的参数,也可以当做函数的返回值,这时候相当于使用复制构造
- shared_ptr可以被用于标准容器,复制时相当于使用复制构造
- 要注意不要循环引用,那样会造成对象不会被释放
使用boost::shared_ptr的一大陷阱就是用一个raw pointer多次创建boost::shared_ptr,这将导致该raw pointer被多次销毁当boost::shared_ptr析构时。即不能如下使用:
int *a = new int(5);
boost::shared_ptr ptr1(a);
boost::shared_ptr ptr2(a); //错误!
boost::shared_ptr 属于 boost 库,定义在 namespace boost 中,包含头文件 #include<boost/smart_ptr.hpp> 便可以使用。在上面我们看到 boost::scoped_ptr 独享所有权,不允许赋值、拷贝,boost::shared_ptr 是专门用于共享所有权的,由于要共享所有权,其在内部使用了引用计数。boost::shared_ptr 也是用于管理单个堆内存对象的。
我们还是从代码开始分析:
void TestSharedPtr(boost::shared_ptr<Simple> memory) { // 注意:无需使用 reference (或 const reference)
memory->PrintSomething();
std::cout << "TestSharedPtr UseCount: " << memory.use_count() << std::endl;
}
void TestSharedPtr2()
{
boost::shared_ptr<Simple> my_memory(new Simple(1));
if (my_memory.get())
{
my_memory->PrintSomething();
my_memory.get()->info_extend = "Addition";
my_memory->PrintSomething();
(*my_memory).info_extend += " other";
my_memory->PrintSomething();
}
std::cout << "TestSharedPtr2 UseCount: " << my_memory.use_count() << std::endl;
TestSharedPtr(my_memory);
std::cout << "TestSharedPtr2 UseCount: " << my_memory.use_count() << std::endl;
//my_memory.release();// 编译 error: 同样,shared_ptr 也没有 release 函数
}
/*
执行结果为:
Simple: 1
PrintSomething:
PrintSomething: Addition
PrintSomething: Addition other
TestSharedPtr2 UseCount: 1
PrintSomething: Addition other
TestSharedPtr UseCount: 2
TestSharedPtr2 UseCount: 1
~Simple: 1
boost::shared_ptr 也可以很方便的使用。并且没有 release() 函数。
关键的一点,boost::shared_ptr 内部维护了一个引用计数,由此可以支持复制、参数传递等。
boost::shared_ptr 提供了一个函数 use_count() ,此函数返回 boost::shared_ptr 内部的引用计数。
查看执行结果,我们可以看到在 TestSharedPtr2 函数中,引用计数为 1,传递参数后(此处进行了一次复制),
在函数TestSharedPtr 内部,引用计数为2,在 TestSharedPtr 返回后,引用计数又降低为 1。
当我们需要使用一个共享对象的时候,boost::shared_ptr 是再好不过的了。
在此,我们已经看完单个对象的智能指针管理,关于智能指针管理数组,我们接下来讲到。
*/
boost::scoped_array
boost::scoped_array 属于 boost 库,定义在 namespace boost 中,包含头文件 #include<boost/smart_ptr.hpp> 便可以使用。
boost::scoped_array 便是用于管理动态数组的。跟 boost::scoped_ptr 一样,也是独享所有权的。我们还是从代码开始分析:
void TestScopedArray()
{
boost::scoped_array<Simple> my_memory(new Simple[2]); // 使用内存数组来初始化
if (my_memory.get())
{
my_memory[0].PrintSomething();
my_memory.get()[0].info_extend = "Addition";
my_memory[0].PrintSomething();
(*my_memory)[0].info_extend += " other"; // 编译 error,scoped_ptr 没有重载 operator*
my_memory[0].release(); // 同上,没有 release 函数
boost::scoped_array<Simple> my_memory2;
my_memory2 = my_memory; // 编译 error,同上,没有重载 operator=
}
}
/*
boost::scoped_array 的使用跟 boost::scoped_ptr 差不多,不支持复制,并且初始化的时候需要使用动态数组。
另外,boost::scoped_array 没有重载“operator*”,其实这并无大碍,一般情况下,我们使用 get() 函数更明确些。
下面肯定应该讲 boost::shared_array 了,一个用引用计数解决复制、参数传递的智能指针类。
*/
boost::shared_array
boost::shared_array 属于 boost 库,定义在 namespace boost 中,包含头文件 #include<boost/smart_ptr.hpp> 便可以使用。
由于 boost::scoped_array 独享所有权,显然在很多情况下(参数传递、对象赋值等)不满足需求,由此我们引入 boost::shared_array。跟 boost::shared_ptr 一样,内部使用了引用计数。我们还是从代码开始分析:
void TestSharedArray(boost::shared_array<Simple> memory) { // 注意:无需使用 reference (或 const reference)
std::cout << "TestSharedArray UseCount: " << memory.use_count() << std::endl;
}
void TestSharedArray2()
{
boost::shared_array<Simple> my_memory(new Simple[2]);
if (my_memory.get())
{
my_memory[0].PrintSomething();
my_memory.get()[0].info_extend = "Addition 00";
my_memory[0].PrintSomething();
my_memory[1].PrintSomething();
my_memory.get()[1].info_extend = "Addition 11";
my_memory[1].PrintSomething();
//(*my_memory)[0].info_extend += " other"; // 编译 error,scoped_ptr 没有重载 operator*
}
std::cout << "TestSharedArray2 UseCount: " << my_memory.use_count() << std::endl;
TestSharedArray(my_memory);
std::cout << "TestSharedArray2 UseCount: " << my_memory.use_count() << std::endl;
}
/*
执行结果为:
Simple: 0
Simple: 0
PrintSomething:
PrintSomething: Addition 00
PrintSomething:
PrintSomething: Addition 11
TestSharedArray2 UseCount: 1
TestSharedArray UseCount: 2
TestSharedArray2 UseCount: 1
~Simple: 0
~Simple: 0
跟 boost::shared_ptr 一样,使用了引用计数,可以复制,通过参数来传递。
至此,我们讲过的智能指针有 std::auto_ptr、boost::scoped_ptr、boost::shared_ptr、boost::scoped_array、boost::shared_array。这几个智能指针已经基本够我们使用了,90% 的使用过标准智能指针的代码就这 5 种。
可如下还有两种智能指针,它们肯定有用,但有什么用处呢,一起看看吧。
*/
boost::weak_ptr
weak_ptr
weak_ptr 就是一个弱指针。weak_ptr 被shared_ptr控制, 它可以通过share_ptr的构造函数或者lock成员函数转化为share_ptr。
weak_ptr的一个最大特点就是它共享一个share_ptr的内存,但是无论是构造还是析构一个weak_ptr 都不会影响引用计数器。
boost::weak_ptr 属于 boost 库,定义在 namespace boost 中,包含头文件 #include<boost/smart_ptr.hpp> 便可以使用。
在讲 boost::weak_ptr 之前,让我们先回顾一下前面讲解的内容。似乎 boost::scoped_ptr、boost::shared_ptr 这两个智能指针就可以解决所有单个对象内存的管理了,这儿还多出一个 boost::weak_ptr,是否还有某些情况我们没纳入考虑呢?
回答:有。首先 boost::weak_ptr 是专门为 boost::shared_ptr 而准备的。有时候,我们只关心能否使用对象,并不关心内部的引用计数。boost::weak_ptr 是 boost::shared_ptr 的观察者(Observer)对象,观察者意味着 boost::weak_ptr 只对 boost::shared_ptr 进行引用,而不改变其引用计数,当被观察的 boost::shared_ptr 失效后,相应的 boost::weak_ptr 也相应失效。
我们还是从代码开始分析:
void TestWeakPtr()
{
boost::weak_ptr<Simple> my_memory_weak;
boost::shared_ptr<Simple> my_memory(new Simple(1));
std::cout << "TestWeakPtr boost::shared_ptr UseCount: " << my_memory.use_count() << std::endl;
my_memory_weak = my_memory;
std::cout << "TestWeakPtr boost::shared_ptr UseCount: " << my_memory.use_count() << std::endl;
}
/*
执行结果为:
Simple: 1
TestWeakPtr boost::shared_ptr UseCount: 1
TestWeakPtr boost::shared_ptr UseCount: 1
~Simple: 1
我们看到,尽管被赋值了,内部的引用计数并没有什么变化,当然,读者也可以试试传递参数等其他情况。
现在要说的问题是,boost::weak_ptr 到底有什么作用呢?从上面那个例子看来,似乎没有任何作用,
其实 boost::weak_ptr 主要用在软件架构设计中,可以在基类(此处的基类并非抽象基类,而是指继承于抽象基类的虚基类)中定义一个 boost::weak_ptr,
用于指向子类的 boost::shared_ptr,这样基类仅仅观察自己的 boost::weak_ptr 是否为空就知道子类有没对自己赋值了,而不用影响子类 boost::shared_ptr 的引用计数,用以降低复杂度,更好的管理对象。
*/
shared_ptr引用计数是一种便利的内存管理机制,但它有一个很大的缺点,那就是不能管理循环引用的对象。例如:
#include <string>
#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
class parent;
class children;
typedef boost::shared_ptr<parent> parent_ptr;
typedef boost::shared_ptr<children> children_ptr;
class parent
{
public:
~parent() { std::cout <<"destroying parent\n"; }
public:
children_ptr children;
};
class children
{
public:
~children() { std::cout <<"destroying children\n"; }
public:
parent_ptr parent;
};
void test()
{
parent_ptr father(new parent());
children_ptr son(new children);
father->children = son;
son->parent = father;
}
void main()
{
std::cout<<"begin test...\n";
test();
std::cout<<"end test.\n";
}
运行该程序可以看到,即使退出了test函数后,由于parent和children对象互相引用,它们的引用计数都是1,不能自动释放,并且此时这两个对象再无法访问到,这就引起了内存泄漏。
一般来讲,解除这种循环引用有下面有三种可行的方法:
- 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
- 当parent的生存期超过children的生存期的时候,children改为使用一个普通指针指向parent。
- 使用弱引用的智能指针打破这种循环引用。在父对子引用时使用强引用,子对父引用时使用弱引用,从而避免了循环引用。
虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。这里主要介绍一下第三种方法和boost中的弱引用的智能指针boost::weak_ptr。
强引用
一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。boost::share_ptr就是强引用。
弱引用
它仅仅是对象存在时候的引用,当对象不存在时弱引用能够检测到,从而避免非法访问,弱引用也不会修改对象的引用计数。这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
在C++中,普通指针可看做弱引用,智能指针可看做强引用,尽管指针不能算"真正"的弱引用,因为弱引用应该能知道何时对象变成不可访问的了。
打破循环引用
在父对子引用时使用强引用,子对父引用时使用弱引用,从而避免了循环引用。
//详细例子见 Boost智能指针-weak_ptr
class children
{
public:
~children() { std::cout <<"destroying children\n"; }
public:
boost::weak_ptr<parent> parent;
};
虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在程序员能预见会出现循环引用的情况下才能使用,也可以是说这个仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。
boost::intrusive_ptr
boost::intrusive_ptr属于 boost 库,定义在 namespace boost 中,包含头文件 #include<boost/smart_ptr.hpp> 便可以使用。
讲完如上 6 种智能指针后,对于一般程序来说 C++ 堆内存管理就够用了,现在有多了一种 boost::intrusive_ptr,这是一种插入式的智能指针,内部不含有引用计数,需要程序员自己加入引用计数,不然编译不过(⊙﹏⊙b汗)。
intrusive_ptr
在多数情况下,我们不使用 boost::intrusive_ptr, 因为共享所有权的功能已在 boost::shared_ptr中提供,而且非插入式智能指针比插入式智能指针更灵活。
但是,有时候也会需要插入式的引用计数,可能是由于旧的代码,或者是为了与第三方的类进行集成。当有这种需要时,可以用 intrusive_ptr ,它具有与其它Boost智能指针相同的语义。如果你使用过其它的Boost智能指针,你就会发现不论是否插入式的,所有智能指针都有一致的接口。
使用intrusive_ptr的类必须可以提供引用计数。intrusive_ptr 通过调用两个函数,intrusive_ptr_add_ref 和 intrusive_ptr_release来管理引用计数;这两个函数必须正确地操作插入式的引用计数,以保证 intrusive_ptr正确工作。在使用intrusive_ptr的类中已经内置有引用计数的情况下,实现对intrusive_ptr的支持就是实现这两个函数。有些情况下,可以创建这两个函数的参数化版本,然后对所有带插入式引用计数的类型使用相同的实现。多数时候,声明这两个函数的最好的地方就是它们所支持的类型所在的名字空间。 在以下情况时使用 intrusive_ptr
- 需要把 this 当作智能指针来使用。
- 已有代码使用或提供了插入式的引用计数。
- 智能指针的大小必须与裸指针的大小相等。
对比boost::shared_ptr
使用boost::shared_ptr用户类本省不需要具有引用计数功能,而是由boost::shared_ptr来提供;使用boost::shared_ptr的一大陷阱就是用一个raw pointer多次创建boost::shared_ptr,这将导致该raw pointer被多次销毁当boost::shared_ptr析构时。即不能如下使用:
int *a = new int(5);
boost::shared_ptr ptr1(a);
boost::shared_ptr ptr2(a); //错误!
boost::intrusive_ptr完全具备boost::shared_ptr的功能,且不存在shared_ptr的问题,即可以利用raw pointer创建多个intrusive _ptr,其原因就在于引用计数的ref_count对象,shared_ptr是放在shared_ptr结构里,而目标对象T通过继承intrusive_ptr_base将引用计数作为T对象的内部成员变量,就不会出现同一个对象有两个引用计数器情况。
那么为什么通常鼓励大家使用shared_ptr,而不是intrusive_ptr呢, 在于shared_ptr不是侵入性的,可以指向任意类型的对象; 而intrusive_ptr所要指向的对象,需要继承intrusive_ptr_base,即使不需要,引用计数成员也会被创建。
shared_ptr比普通指针提供了更完善的功能。有一个小小的代价,那就是一个共享指针比普通指针占用更多的空间,每一个对象都有一个共享指针,这个指针有引用计数器以便于释放。但对于大多数实际情况,这些都是可以忽略不计的。
intrusive_ptr 提供了一个折中的解决方案。它提供了一个轻量级的引用计数器,但必须对象本身已经有了一个对象引用计数器。这并不是坏的想法,当你自己的设计的类中实现智能指针相同的工作,那么一定已经定义了一个引用计数器,这样只需要更少的内存,而且可以提高执行性能。
如果你要使用intrusive_ptr 指向类型T,那么你就需要定义两个函数:intrusive_ptr_add_ref 和intrusive_ptr_release。下面是一个简单的例子解释如何在自己的类中实现:
#include "boost/intrusive_ptr.hpp"
// forward declarations(前向声明)
class CRefCounted;
namespace boost
{
void intrusive_ptr_add_ref(CRefCounted * p);
void intrusive_ptr_release(CRefCounted * p);
}
// My Class
class CRefCounted
{
private:
long references;
friend void ::boost::intrusive_ptr_add_ref(CRefCounted * p);
friend void ::boost::intrusive_ptr_release(CRefCounted * p);
public:
CRefCounted() : references(0) {} // initialize references to 0
};
// class specific addref/release implementation
// the two function overloads must be in the boost namespace on most compilers:
namespace boost
{
inline void intrusive_ptr_add_ref(CRefCounted * p)
{
// increment reference count of object *p
++(p->references);
}
inline void intrusive_ptr_release(CRefCounted * p)
{
// decrement reference count, and delete object when reference count reaches 0
if (--(p->references) == 0)
delete p;
}
} // namespace boost
/*
这是一个最简单的(非线程安全)实现操作。
但作为一种通用的操作,如果提供一种基类来完成这种操作或许很有使用价值,也许在其他地方会介绍到。
*/
总结
如上讲了这么多智能指针,有必要对这些智能指针做个总结:
1、在可以使用 boost 库的场合下,拒绝使用 std::auto_ptr,因为其不仅不符合 C++ 编程思想,而且极容易出错[2]。
2、在确定对象无需共享的情况下,使用 boost::scoped_ptr(当然动态数组使用 boost::scoped_array)。
3、在对象需要共享的情况下,使用 boost::shared_ptr(当然动态数组使用 boost::shared_array)。
4、在需要访问 boost::shared_ptr 对象,而又不想改变其引用计数的情况下,使用 boost::weak_ptr,一般常用于软件框架设计中。
5、最后一点,也是要求最苛刻一点:在你的代码中,不要出现 delete 关键字(或 C 语言的 free 函数),因为可以用智能指针去管理。
下面是几个使用智能指针需要注意的地方:
- 声明一个智能指针的时候要立即给它实例化, 而且一定不能手动释放它。
- …_ptr<T> 不是T* 类型。所以:
a: 声明的时候要…_ptr<T> 而不是….._ptr<T*>
b:不能把T* 型的指针赋值给它
c: 不能写ptr=NULl, 而用ptr.reset()代替。
- 不能循环引用。
- 不要声明临时的share_ptr, 然后把这个指针传递给一个函数
---------------------------------------
[1]参见《effective C++(3rd)》,条款06 。
[2]关于 boost 库的使用,可本博客另外一篇文章:《在 Windows 中编译 boost1.42.0》。
[3]读者应该看到了,在我所有的名字前,都加了命名空间标识符std::(或boost::),这不是我不想写 using namespace XXX 之类的语句,在大型项目中,有可能会用到 N 个第三方库,如果把命名空间全放出来,命名污染(Naming conflicts)问题很难避免,到时要改回来是极端麻烦的事情。当然,如果你只是写 Demo,可以例外。