除了自动和static
对象外,C+还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。
静态内存 用来保存局部static
对象、类static
数据成员以及定义在任何函数之外的变量。
栈内存 用来保存定义在函数内的非static对象。
分配在静态或栈内存中的对象由编译器自动创建和销毁。
对于栈对象,仅在其定义的程序块运行时才存在:static
对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作 自由空间(free store) 或 堆(heap)。
程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,代码必须显式地销毁它们。
忘记释放内存会产生内存泄漏,在尚有指针引用内存的情况下释放它会产生非法内存的指针。
动态内存和智能指针
c++ 使用 new
和 delete
管理动态内存
new
:在动态内存中为对象分配空间并返回一个指向该对象的指针
delete
:接受一个动态对象的指针,销毁该对象,并释放与之有关的内存。
新的标准库提供了两种智能指针类型来管理动态对象,都定义在 memory
头文件中。其类似于常规指针,主要区别在于管理底层指针的方式:
1.shared_ptr
允许多个指针指向同一个对象;
2.unique_ptr
则“独占”所指向的对象。
3.weak_ptr
伴随类,是一种弱引用,指向 shared_ptr 所管理的对象。
shared_ptr 类
类似 vector,智能指针也是模板。因此,创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。
shared_ptr<string> pl;// shared_ptr,可以指向string
shared_ptr<list<int>> p2;// shared_ptr,可以指向int的list
默认初始化的智能指针中保存着一个空指针。
智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:
//如果p1不为空,检查它是否指向一个空
string if(p1 && p1->empty())
*pl = "hi";//如果pl指向一个空string,解引用p1,将一个新值赋予string
shared_ptr
和 unique_ptr
都支持的操作
shared_ptr
独有的操作
make_shared 函数
最安全的分配和使用动态内存的方法是调用一个名为 make_shared
的标准库函数,在 memory 头文件中。
在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr
。
//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4指向一个值为"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
// p5指向一个值初始化的int,即,值为0
shared_ptr<int> p5 = make_shared<int>();
类似于顺序容器的 emplace 成员,make_shared
是函数模板,用其参数来构造给定类型的对象。
shared_ptr 的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr
都会记录有多少个其他shared_ptr
指向相同的对象:
auto p = make_shared<int>(42);// p 指向的对象只有 p 一个引用者
auto q(p) ; // p 和 q 指向相同对象,此对象有两个引用者
可以认为每个 shared_ptr
都有一个关联的计数器,通常称其为引用计数。无论何时拷贝一个 shared_ptr
,计数器都会递增。
比如当用一个 shared_ptr
初始化另一个 shared_ptr
,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。
如果 shared_ptr
的计数器变为 0
,就会自动释放管理的对象。给 shared_ptr
赋予一个新值或是shared_ptr
被销毁时,计数器会递减。
auto r = make_shared<int>(42);//r 指向的 int 只有一个引用者
r =q;//给 r 赋值,令它指向另一个地址
//递增 q 指向的对象的引用计数
//递减 r 原来指向的对象的引用计数
//r 原来指向的对象已没有引用者,会自动释放
shared_ptr 自动销毁所管理的对象……
shared_ptr
通过析构函数来完成销毁。
shared_ptr
的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0
,shared_ptr
的析构函数就会销毁对象,并释放它占用的内存。
……shared_ptr 还会自动释放相关联的内存
当动态对象不再被使用时,shared_ptr
类会自动释放动态对象。
最后一个 shared_ptr
销毁前内存都不会释放,所以要保证 shared_ptr
无用之后就不要再保留了。
如果将shared_ptr
存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase
删除不再需要的那些元素,否则会浪费内存。
程序使用动态内存的三种原因
1.程序不知道自己需要使用多少对象;
2.程序不知道所需对象的准确类型;
3.程序需要在多个对象间共享数据。
直接内存管理
运算符 new
分配内存,delete
释放 new
分配的内存。
相比于智能指针,这两个运算符管理内存非常容易出错,而且自己直接管理内存的类与使用智能指针的类不同,它们不依赖于类对象拷贝、赋值和销毁操作的任何默认定义。
使用 new 动态分配和初始化对象
在自由空间分配的内存是无名的,因此 new
无法为其分配的对象命名,而是返回一个指向该对象的指针:
int *pi = new int;//pi指向一个动态分配的、未初始化的无名对象
// 此 new 表达式在自由空间构造一个 int 型对象,并返回指向该对象的指针
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的。而类类型对象将用默认构造函数进行初始化:
int* p = new int; //默认初始化 10
string* sp = new string(10,'9');//直接初始化“9999999999”
vector<int>* vp = new vector<int>{0,1,2,3};//列表初始化
对于定义了自己的构造函数的类类型,不管采用什么形式,对象都会通过默认构造函数来初始化。
对于内置类型,值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值则是未定义的。
类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的。
动态分配 const 对象
用 new
分配 const
对象是合法的:
//分配并初始化一个const int
const int *pci = new const int (1024);
//分配并默认初始化一个const的空 string
const string *pcs = new const string;
类似其他任何const
对象,一个动态分配的const
对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const
动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是const
的,new
返回的指针是一个指向const
的指针。
内存耗尽
一旦一个程序用光了它所有可用的内存,new
表达式就会失败。
默认情况下,如果 new
不能分配所要求的内存空间,会抛出一个类型为 bad_alloc
的异常。
可以改变使用 new 的方式来阻止它抛出异常:
//如果分配失败,new返回一个空指针
int *pl = new int; //如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow) int;//如果分配失败,new返回一个空指针
释放动态内存
delete
执行两个动作:销毁给定的指针指向的对象;释放对应的内存。
delete p;// p必须指向一个动态分配的对象或是一个空指针
指针值和 delete
传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非 new 分配的内存,或者将相同的指针释放多次,其行为是未定义的:
int i, *pi1 = &i, *pi2=nullptr;
double *pd = new double(33),*pd2 = pd;
delete i;//错误:i不是一个指针
delete pi1;//未定义:pi1指向一个局部变量
delete pd;//正确
delete pd2;//未定义:pd2指向的内存已经被释放了
delete pi2;//正确:释放一个空指针总是没有错误的
const
动态对象的值不可改变,但是可以通过 delete
指向它的指针进行销毁。
动态对象的生存期知道释放时为止
由 shared_ptr
管理的内存在最后一个 shared_ptr
销毁时会被自动释放;
对于通过内置指针类型来管理的内存(动态对象),需要显式释放。
通常编译器不能分辨 delete 的对象是动态还是静态分配的对象,也不能分辨一个指针所指的内存是否已被释放。
动态内存的管理容易出现的问题
1.忘记 delete 内存。产生“内存泄漏”:内存无法归还给自由空间;
2.使用已释放掉的对象;
3.同一块内存释放两次:自由空间可能会被破坏。
使用智能指针可以避免这些问题。对于一块内存,只有在由任何智能指针指向它的情况下,智能指针才会自动释放它。
delete 之后重置指针值
delete
一个指针后,其指针值变为无效,但仍然保存着(已经释放掉的)动态内存的地址。
在 delete
之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的指针。
未初始化指针的所有缺点空悬指针也都有。
避免空悬指针的问题: 在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以在 delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。
shared_ptr 和 new 结合使用
如果不初始化一个智能指针,它就会被初始化为一个空指针。
还可以用 new 返回的指针来初始化智能指针:
shared_ptr<double> pl;// shared_ptr 可以指向一个double
shared_ptr<int> p2(new int (42));// p2指向一个值为42的int
接受指针参数的智能指针构造函数是explicit
的。explicit
一般用来防止隐式转换。
必须使用直接初始化形式来初始化一个智能指针:
shared_ptr<int> pl = new int(1024);//错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));//正确:使用了直接初始化形式
定义和改变 shared_ptr 的其他方法:
shared_ptr<T> p(q);// p 管理内置指针 q 所指向的对象;q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u)// p从 unique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr<T> p(a,d)//p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
shared ptr<T> p(p2,d)//p是shared ptrp2的拷贝,唯一的区别是p将用可调用对象d来代替delete
p.reset()
p.reset (q)
p.reset(q,d)//若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空。若还传递了参数d,将会调用d而不是delete来释放q
不要混合使用普通指针和智能指针
shared_ptr
可以协调对象的析构,但这仅限于其自身的据贝(也是shared_ptr
)之间。这也是为什么推荐使用make_ shared
而不是new的原因。
make_ shared
能在分配对象的同时就将 shared_ptr 与之绑定,从而避免将同一块内存绑定到多个独立创建的 shared_ptr 上。
使用一个内置指针来访问一个智能指针非常危险,因为无法知道对象何时会被销毁。
也不要使用 get 初始化另一个智能指针或为智能指针赋值
智能指针类型定义了一个名为 get
的函数,它返回一个内置指针指向智能指针管理的对象。此函数设计目的:需要向不能使用智能指针的代码传递一个内置指针。
使用get返回的指针的代码不能delete此指针。
sharedptr<int>p(newint(42));//引用计数为1
int*q=p.get();//正确:但使用q时要注意,不要让它管理的指针被释放
{//新程序块
//未定义:两个独立的sharedptr指向相同的内存
shared ptr<int>(q);
)//程序块结束,q被销毁,它指向的内存被释放
int foo=*p;//未定义:p指向的内存已经被释放了
get 用来将指针的访问权限传递给代码,只有在确定代码不会 delete 指针的情况下,才能使用 get。
特别是,永远不要用 get 初始化另一个智能指针或者为另一个智能指针赋值。
智能指针和异常
使用异常处理的程序能在异常发生后令程序流程继续,需要确保在异常发生后资源能被正确地释放,一种简单的确保资源被释放的方法是使用智能指针。
如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。内置指针管理内存时,在 new
之后在对应的 delete
之前发生了异常,则内存不会被释放。
智能指针和哑类
C++ 中很多类都定义了析构函数,负责清理对象使用的资源,但是那些为 C 和 C++ 两种语言设计的类,通常要求用户显式地释放所使用地任何资源。
那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误——忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。
使用自己的释放操作
默认情况下,shared_ptr
假定它们指向的是动态内存。因此,当一个shared_ptr
被销毁时,它默认地对它管理的指针进行 delete
操作。可以使用 shared_ptr
管理其他对象,但需要定义一个相应的删除器函数来代替 delete
,完成对 shared_ptr
中保存地指针进行释放地操作。
//删除器接受单个类型位 connection* 的参数:
//void end_connection (connection *p){disconnect(*p);}
void f(destination &d/*其他参数*/){
connection c = connect(&d);
shared_ptr<connection> p(&c,end_connection);
//使用连接
//当f退出时(即使是由于异常而退出),connection会被正确关闭
}
当 p
被销毁时,它不会对自己保存的指针执行delete
,而是调用end_connection
。接下来,end_connection
会调用disconnect
,从而确保连接被关闭。如果f正常退出,那么 p
的销毁会作为结束处理的一部分。如果发生了异常,p
同样会被销毁,从而连接被关闭。
正确使用智能指针的一些基本规范:
- 不使用相同的内置指针值初始化(或reset)多个智能指针。
- 不delete get()返回的指针。
- 不使用get()初始化或reset另一个智能指针。
- 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,指针就变为无效了。
- 如果使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
unqie_ptr
一个 unique_ptr
"拥有"它所指向的对象。与shared_ptr
不同,某个时刻只能有一个unique_ptr
指向一个给定对象。当unique_ptr
被销毁时,它所指向的对象也被销毁。
unique_ptr
独有它指向的对象,所有它不支持拷贝和赋值操作。但可以"拷贝"或"赋值"一个将要被销毁的unique_ptr
。
//从函数返回一个`unique_ptr`
unique_ptr<int> clone(int p){
//正确:从int*创建一个unique_ptr<int>
return unique_ptr<int>(new int (p));
}
//返回一个局部对象的拷贝
unique ptr<int> clone(int p){
unique_ptr<int> ret(new int (P))// ...
return ret;
}
auto_ptr
标准库的较早版本包含了一个名为auto_ptr
的类,它具有unique_ptr
的部分特性,但不是全部。特别是,不能在容器中保存auto_ptr
,也不能从函数中返回auto_ptr
。
虽然auto_ptr
仍是标准库的一部分,但编写程序时应该使用unique_ptr
.
weak_ptr
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr
管理的对象。
将一个 weak_ptr
绑定到一个 shared_ptr
不会改变shared_ptr
的引用计数。一旦最后一个指向对象的shared_ptr
被销毁,对象就会被释放。即使有weak_ptr
指向对象,对象也还是会被释放。
weak_ptr 定义和初始化
]创建一个weak_ptr
时,要用一个shared_ptr
来初始化它:
auto p= make shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p;p的引用计数未改变
//wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。
由于对象可能不存在,我们不能使用weak_ptr
直接访问对象,而必须调用lock
。此函数检查weak_ptr
指向的对象是否仍存在。如果存在,lock
返回一个指向共享对象的shared_ptr
。与任何其他shared_ptr
类似,只要此shared_ptr
存在,它所指向的底层对象也就会一直存在。
if (shared_ptr<int> np - wp.1ock()){ //如果np 不为空则条件成立
//在if中,np与p共享对象
}
动态数组
new
和 delete
运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。例如,vector 和 string 都是在连续内存中保存它们的元素,因此,当容器需要重新分配内存时,必须一次性为很多元素分配内存。
C++ 语言和标准库提供了两种一次分配一个对象数组的方法:1.表达式语法new
;2. 类allocator
,允许将分配和初始化分离。
new 和数组
new 分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针。
// 调用 get_size 确定分配多少个 int
int *pia = new int [get_size()];// pia指向第一个int
//方括号中的大小必须是整型,但不必是常量。
也可以用一个表示数组类型的类型别名来分配一个数组:
// new 表达式不再需要方括号
typedef int arrT [42]; // arrT表示42个int的数组类型
int *p = new arrT; //分配一个42个int的数组;p指向第一个int
//new分配一个int数组,并返回指向第一个int的指针。
//即使代码中没有方括号,编译器执行这个表达式时还是会用 new[]。
//等价于执行:
int *P = new int[42];
分配一个数组会得到一个元素类型的指针
当用 new
分配一个数组时,并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。
即使使用类型别名定义了一个数组类型,new
也不会分配一个数组类型的对象。
由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin
或 end
。也不能用范围 for 语句来处理(所谓的)动态数组中的元素。
通常所说的动态数组并不是一种数组类型。
初始化动态分配对象的数组
默认情况下,new
分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。也可以进行列表初始化。
int *pia = new int [10];// 10 个未初始化的 int
int *pia2 = new int[10]();// 10个值初始化为0的int
string *psa = new string[10];// 10个空string
string *psa2 = new string[10]();// 10个空string
// 10个int分别用列表中对应的初始化器初始化
int *pia3 = new int [10] {0,1,2,3,4,5,6,7,8,9};
// 10个string,前4个用给定的初始化器初始化,剩余的进行值初始化
string *psa3 = new string[10] {"a", "an","the",string(3, 'x')};
与内置数组对象的列表初始化一样,初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数目,剩余元素将进行值初始化。
如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。
虽然用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto
分配数组。
动态分配一个空数组是合法的
虽然不能创建一个大小为 0
的静态数组对象,但当 n
等于 0
时,调用 new[n]
是合法的。
char arr[0];//错误:不能定义长度为0的数组
char *cp = new char[0]; //正确:但cp不能解引用
用new
分配一个大小为0的数组时,new
返回一个合法的非空指针。此指针保证与new
返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,可以像使用尾后迭代器一样使用这个指针。
可以用此指针进行比较操作,可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到0。但此指针不能解引用——它不指向任何元素。
释放动态数组
使用 delete []
来释放动态数组
delete p;//p必须指向一个动态分配的对象或为空
delete [] pa;// pa必须指向一个动态分配的数组或为空
数组中的元素按逆序销毁即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。
delete[]
中 []
是必需的:如果在 delete
一个指向动态数组的指针时忽略了方括号,行为是未定义的。
智能指针和动态数组
标准库提供了一个可以管理 new
分配的数组的 unique_ptr
版本。
// up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up (new int [10]);
up.release();//自动用 delete[]销毁其指针
类型说明符中的方括号(<int[]>
)指出up
指向一个int
数组而不是一个 int
。由于up
指向一个数组,当up
销毁它管理的指针时,会自动使用delete[]
。
指向数组的 unique_ptr:
//指向数组的unique ptr不支持成员访问运算符(点和箭头运算符)。
//其他unique ptr操作不变。
unique_ptr<T[]> u // u 可以指向一个动态分配的数组,数组元素类型为 T
unique_ptr<T[]> u(p) // u 指向内置指针 p 所指向的动态分配的数组,p 必须能转换为类型 T*
u[i] // 返回 u 拥有的数组中位置 i 处的对象
shared_ptr 不直接支持管理动态数组。如果希望使用 shared_ptr 管理一个动态数组 ,必须提供自己定义的删除器。
//为了使用shared_ptr,必须提供一个删除器
shared ptr<int> sp(new int[10],[](int*p){ delete[] p;});
sp.reset();//使用我们提供的 lambda释放数组,它使用delete[]
如果未提供删除器,shared_ptr 默认使用 delete 来销毁动态数组,是未定义的。
shared_ptr 访问数组中的元素需要使用 get()。
allocator 类
灵活性上的局限,new 将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。
分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,几乎肯定知道对象应有什么值。
当分配一大块内存时,通常计划在这块内存上按需构造对象。这种情况下,希望将内存分配和对象构造分离。意味着可以分配大块内存,但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。
allocator
标准库allocator
类定义在头文件memory
中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
类似vector
,allocator
是一个模板。为了定义一个allocator
对象,必须指明这个allocator
可以分配的对象类型。
当一个allocator
对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:
allocator<string> alloc;//可以分配string 的allocator对象
auto const p = alloc.allocate(n);//分配n个未初始化的string
// 这个 allocate 调用为 n 个 string 分配了内存
标准库 allocator 类及其算法
allocator 分配未构造的内存
allocator分配的内存是未构造的。按需要在此内存中构造对象。
在新标准库中,construct
成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器:
auto q = p; //q指向最后构造的元素之后的位置
alloc.construct(q++);//*q为空字符串
alloc.construct(q++,10,'c');//*q为ccccccccc
calloc.construct(q++, "hi");//*q为hi!
为了使用allocate
返回的内存,必须用 construct
构造对。使用未构造的内存,其行为是未定义的
用完对象后,必须对每个构造的元素调用destroy来销毁它们:
while (q != p)
alloc.destroy(--q);//释放真正构造的string
只能对真正构造了的元素进行destroy操作。
元素被销毁后可以重新在这块内存构造对象也可以释放掉内存。
construct
和 destory
一次都只能构造或销毁一个对象,要想完成对所有元素的操作,需要通过指针来遍历对每个元素进行操作。
拷贝和填充未初始化内存的算法
定义在头文件 memory
中,两个伴随算法:uninitialized_copy
和 uninitialized_fill
,可以在未初始化内存中创建对象。
将分配一块比vector
中元素所占用空间大一倍的动态内存,然后将原vector
中的元素拷贝到前一半空间,对后一半空间用一个给定值进行填充:
//分配比vi中元素所占用空间大一倍的动态内存
auto p = alloc.allocate(vi.size()*2);
//通过拷贝vi中的元素来构造从p开始的元素
auto q = uninitialized_copy(vi.begin(), vi.end(),p);
//将剩余元素初始化为42
uninitialized_fill_n(g, vi.size(),42);
重要术语
allocator :标准库类,用来分配未构造的内存。
空悬指针:一个指针,指向曾经保存一个对象但现在已释放的内存。众所周知,空悬指针引起的程序错误非常难以调试。
释放器:传递给智能指针的函数,用来代替delete释放指针绑定的对象。
动态分配的:在自由空间中分配的对象。在自由空间中分配的对象直到被显式释放或程序结束才会销毁。
自由空间:程序可用的内存池,保存动态分配的对象。
堆:自由空间的同义词。
定位new:一种new表达式形式,接受一些额外的参数,在 new
关键字后面的括号中给出。例如,new(nothrow) int
告诉new
不要抛出异常。
引用计数:一个计数器,记录有多少用户共享一个对象。智能指针用它来判断什么时候释放所指向的对象是安全的。