欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C++智能指针

程序员文章站 2022-12-29 07:51:51
直接管理内存 什么时候需要直接管理 简而言之,当内存分配在栈上时,不需要直接管理,而当内存分配在堆上时则需要手动回收,或者等到堆上内存分配满了触发了自动回收机制。 一个由c/c++编译的程序占用的内...

直接管理内存

什么时候需要直接管理

简而言之,当内存分配在栈上时,不需要直接管理,而当内存分配在堆上时则需要手动回收,或者等到堆上内存分配满了触发了自动回收机制。
一个由c/c++编译的程序占用的内存分为以下几个部分

栈区(stack)—— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 堆区(heap)—— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由os回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 全局区(静态区)(static)——全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由释放。 文字常量区——常量字符串就是放在这里的,程序结束后由系统释放 。 程序代码区——存放函数体的二进制代码。

例子程序

这是一个前辈写的,非常详细

//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 sp , unique_ptr up 空智能指针,可以指向类型为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(args) 返回一个shared_ptr,指向一个动态分配的类型为t的对象。使用args初始化此对象
shared_ptrp(q) 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) p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为t*类型
shared_ptr p(u) p从unique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr p(q,d) p接管了内置指针q所指向的对象的所有权。q必须能转换为t*类型。p将使用可调用对象d来代替delete
shared_ptr p(p2,d) 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指向的内存已经被释放了

智能指针和异常

如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。
智能指针和哑类
使用我们自己的释放操作
为了正确使用智能指针,我们必须坚持一些基本规范:

不适用相同的内置指针初始化(或reset)多个智能指针。 不delete get()返回的指针。 不使用get()初始化或reset另一个智能指针。 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。 如果你用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

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 u1,unique_ptr u2,d> 空unique_ptr,可以指向类型为t的对象,u1会使用delete来释放它的指针;u2会使用一个类型为b的可调用对象来释放它的指针
unique_ptr u(d),d> 空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 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() 如果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[]> u可以指向一个动态分配的数组,数组元素类型为t
unique_ptr u(p)[]> 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必须指向足够大的为构造的原始内存,能够容纳给定数量的对象