C++智能指针
直接管理内存
什么时候需要直接管理
简而言之,当内存分配在栈上时,不需要直接管理,而当内存分配在堆上时则需要手动回收,或者等到堆上内存分配满了触发了自动回收机制。
一个由c/c++编译的程序占用的内存分为以下几个部分
例子程序
这是一个前辈写的,非常详细
//main.cpp int a = 0; 全局初始化区 char *p1; 全局未初始化区 main() { int b; 栈 char s[] = "abc"; 栈 char *p2; 栈 char *p3 = "123456"; 123456/0在常量区,p3在栈上。 static int c =0; 全局(静态)初始化区 p1 = (char *)malloc(10); p2 = (char *)malloc(20); 分配得来得10和20字节的区域就在堆区。 strcpy(p1, "123456"); 123456/0放在常量区,编译器可能会将它与p3所指向的"123456" 优化成一个地方。 }
注意,除了上文的malloc,new分配的内存也在堆中需要手动销毁。
动态内存
由上文看出,分配在堆上的内存需要手动进行动态分配和释放,我们将之称为动态内存。c++中,动态内存是通过new和delete来进行分配和释放的。
new:在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化。
delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
在*空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针。
int *pi=new int; //pi指向一个动态分配的,未初始化的无名对象
可以是使用直接初始化方式来初始化一个动态分配的对象。
int *pi=new int(1024); string *ps=new string(10,’9’);
也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:
string *ps1=new string; //默认初始化为空string string *ps=new string(); //值初始化为空string
动态分配const对象
类似于其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。
内存耗尽
一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,他会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止他抛出异常:
int *p1=new int; //如果分配失败,new抛出std::bad_alloc int *p2=new (nothrow) int //如果分配失败,new返回一个空指针
释放动态内存
delete p; //p必须指向一个动态分配的对象或是一个空指针
释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。
int i,*pil=&i,*pi2=nullptr; double *pd=new double(33),*pd2=pd; delete i; //错误:i不是一个指针 delete pil; //未定义:pil指向一个局部变量 delete pd; //正确 delete pd2; //未定义:pd2指向的内存已经被释放了 delete pi2; //正确:释放一个空指针总是没有错误的
对于通过内置指针类型来管理的动态对象,直到被显式释放之前他都是存在的。
在delete之后,指针就变成了空悬指针,即,指向一块曾经保存数据对象但现在已经无效的内存的指针。
如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清除地指出指针不指向任何对象。
智能指针
智能指针与常规指针的重要区别是它负责自动释放所指向的对象,两种智能指针的区别在于管理底层指针的方式:
shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为week_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
shared_ptr和unique_ptr都支持的操作
操作 | 说明 |
---|---|
shared_ptr , unique_ptr
|
空智能指针,可以指向类型为t的对象 |
p | 将p用作一个条件判断,若p指向一个对象,则为true |
*p | 解引用p,获得它指向的对象 |
p->mem | 等价于(*p).mem |
p.get() | 返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了 |
swap(p,q) ,p.swap(q) | 交换p和q中的指针 |
shared_ptr类
创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。
shared_ptr p1; shared_ptr> p2;
shared_ptr独有的操作
操作 | 说明 |
---|---|
make_shared |
返回一个shared_ptr,指向一个动态分配的类型为t的对象。使用args初始化此对象 |
shared_ptr |
p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为t* |
p=q | p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放 |
p.unique() | 若p.use_count()为1,返回true;否则返回false |
p.use_count() | 返回与p共享对象的智能指针数量;可能很慢,主要用于调试 |
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
shared_ptr p3=make_shared(42); auto p6=make_shared>();
shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。
shared_ptr自动销毁所管理的对象
shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
shared_ptr还会自动释放相关联的内存
return会对shared_ptr指针的引用次数进行递增操作。
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
1. 程序不知道自己需要使用多少对象
2. 程序不知道所需对象的准确类型
3. 程序需要在多个对象间共享数据
shared_ptr和new结合使用
接受指针参数的智能指针构造函数是explicit的,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
shared_ptr p1=new int(1024); //错误 shared_ptr p2(new int(1024)); //正确 shared_ptr clone(int p){ return new int(p); //错误 } shared_ptr clone(int p){ return shared_ptr(new int(p)); //正确 }
定义和改变shared_ptr的其他方法
操作 | 说明 |
---|---|
shared_ptr |
p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为t*类型 |
shared_ptr |
p从unique_ptr u那里接管了对象的所有权;将u置为空 |
shared_ptr |
p接管了内置指针q所指向的对象的所有权。q必须能转换为t*类型。p将使用可调用对象d来代替delete |
shared_ptr |
p是shared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来代替delete |
p.reset()p,reset(q)p,reset(q,d) | 若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则将p置为空。若还传递了参数d,将会调用d而不是delete来释放q |
不要混合使用普通指针和智能指针
void process(shared_ptr ptr){}
process的参数是传值方式传递的,因此实参会被拷贝到ptr中。拷贝一个shared_ptr会递增其引用计数,因此,在process运行过程中,引用计数值至少为2.当process结束时,ptr的引用计数会递减,但不会变为0.因此当局部变量ptr被销毁时,ptr指向的内存不会被释放。
正确方式是传递给它一个shared_ptr:
shared_ptr p(new int(42)); //引用计数为1 process(p); //
虽然不能传递给process一个内置指针,但可以传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显式构造的。但是,这样做很可能会导致错误:
int *x(new int(1024)); //危险:x是一个普通指针,不是一个智能指针 process(x); //错误 process(shared_ptr(x)); //合法的,但内存会被释放 int j=*x; //未定义的:x是一个空悬指针
不要使用get初始化另一个智能指针或为智能指针赋值
智能指针类型顶一个了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况儿设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。
shared_ptr p(new int(42)); //引用计数为1 int *q=p.get(); //正确:但使用q时要注意,不要让它管理的指针被释放 { //未定义:两个独立的shared_ptr指向相同的内存 shared_ptr(q); }//程序块结束,q被销毁,它指向的内存被释放 int foo=*p; //未定义:p指向的内存已经被释放了
智能指针和异常
如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。
智能指针和哑类
使用我们自己的释放操作
为了正确使用智能指针,我们必须坚持一些基本规范:
unique_ptr
一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。尅死shared_ptr,初始化unique_ptr必须采用直接初始化形式:
unique_ptr p1; //可以指向一个double的unique_ptr unique_ptr p2(new int(42));//p2指向一个值为42的int
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作
unique_ptr操作
操作 | 说明 |
---|---|
unique_ptr ,unique_ptr
|
空unique_ptr,可以指向类型为t的对象,u1会使用delete来释放它的指针;u2会使用一个类型为b的可调用对象来释放它的指针 |
unique_ptr |
空unique_ptr,指向类型为t的对象,用类型为d的对象d代替delete |
u=nullptr | 释放u指向的对象,将u置为空 |
u.release() | u放弃对指针的控制权,返回指针,并将u置为空 |
u.reset() | 释放u指向的对象 |
u.reset(q) ,u.reset(nullptr) | 如果提供了内置指针q,令u指向这个对象;否则将u置为空 |
//将所有权从p1转移给p2 unique_ptr p2(p1.release()); //release将p1置为空 unique_ptr p3(new string(“trex”)); //将所有权从p3转移给p2 p2.reset(p3.release());; //reset释放了p2原来指向的内存
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr。
向unique_ptr传递删除器
weak_ptr
weak_ptr指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
weak_ptr
操作 | 说明 |
---|---|
weak_ptr |
空weak_ptr可以指向类型为t的对象 |
weak_ptr |
与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() | 如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr |
动态数组
new和delete运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。例如,vector和string都是在连续内存中保存它们的元素,因此,当容器需要重新分配内存时,必须一次性为很多元素分配内存。
为了支持这种需求,c++语言和标准库提供了两种一次分配一个对象数组的方法:c++语言定义了动态数组的new方式;标准库中包含了一个名为allocator的类。
new和数组
int *pia=new int[get_size()]; //pia指向第一个int
分配一个数组会得到一个元素类型的指针
虽然我们通常称new t[]分配的内存为“动态数组”,但我们用new分配一个数组时,并未得到一个数组类型的对象,而是一个数组元素类型的指针。
由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。这些函数使用数组维度来返回指向首元素和尾后元素的指针。出于相同的原因,也不能用范围for语句来处理动态数组中的元素。
初始化动态分配对象的数组
可以对数组中的元素进行值初始化,方法是在大笑之后跟一对空括号:
int *pia=new int[10]; //10个未初始化的int int *pia2=new int[10](); //10个值初始化为0的int
动态分配一个空数组是合法的
虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new[n]是合法的:
char arr[0]; //错误 char *cp=new char[0]; //正确
释放动态数组
为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对:
delete p; //p必须指向一个动态分配的对象或为空 delete [] pa; //pa必须指向一个动态分配的数组或为空
数组中的元素按逆序被销毁。
智能指针和动态数组
为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:
//up指向一个包含10个未初始化int的数组 unique_ptr up(new int[10]); up.release(); //自动用delete[]销毁其指针
指向数组的unique_ptr
指向数组的unique_ptr不支持成员访问运算符(点和箭头运算符)
其他unique_ptr操作不便
操作 | 说明 |
---|---|
unique_ptr |
u可以指向一个动态分配的数组,数组元素类型为t |
unique_ptr |
u指向内置指针p所指向的动态分配的数组。p必须能转换为类型t* |
u[i] | 返回u拥有的数组中的位置i处的对象,u必须指向一个数组 |
allocator类
allocator类
标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
类似vector,allocator是一个模板。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,他会根据给定的对象类型来确定恰当的内存大小和对齐位置:
allocator alloc; //可以分配string的allocator对象 auto const p=alloc.allocate(n); //分配n个未初始化的string
标准库allocator类及其算法
操作 | 说明 |
---|---|
allocator a | 定义了一个名为a的allocator对象,它可以为类型为t的对象分配内存 |
a.allocate(n) | 分配一段原始的、为构造的内存,保存n个类型为t的对象 |
a.deallocate(p,n) | 释放从t*指针p中地址开始的内存,这块内存保存了n个类型为t的对象;p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy |
a.construct(p,args) | p必须是一个类型为t*的指针,指向一块原始内存;arg被传递给类型为t的构造函数,用来在p指向的内存中构造一个对象 |
a.destroy(p) | p为t*类型的指针,此算法对p指向的对象执行西沟函数 |
allocator分配为构造的内存
allocator分配的内存是未构造的,我们按需要在此内存中构造对象。
auto q=p; //q指向最后构造的元素之后的位置 alloc.construct(q++); //*q为空字符串 alloc.construct(q++,10,’c’); //*q为cccccccccc alloc.construct(q++,”hi”); //*q位hi!
为了使用allocate返回的内存,我们必须用construct构造对象。使用为构造的内存,其行为是未定义的。
我们只能对真正构造了的元素进行destroy操作
拷贝和填充未初始化内存的算法
它们都定义在头文件memory中
allocator算法
这些函数在给定目的位置创建元素,而不是由系统分配内存给它们。
操作 | 说明 |
---|---|
uninitialized_copy(b,e,b2) | 从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的为构造的原始内存中。b2指向的内存必须足够大,能容纳输入序列中元素的拷贝 |
uninitialized_copy(b,n,b2) | 从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中 |
uninitialized_fill(b,e,t) | 在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝 |
uninitialized_fill_n(b,n,t) | 从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的为构造的原始内存,能够容纳给定数量的对象 |