C++ Primer笔记12---chapter12 动态内存

1. 动态内存与智能指针

在C++中,动态内存(堆)管理通过new和delete运算符实现;新的标准库还提供了两种智能指针类型来管理动态内存,其行为类似常规指针,重要区别在于负责自动释放所指的对象。

新标准提供的两种智能指针区别在于管理底层指针的方式不同,都定义在头文件memory中

shared_ptr允许多个指针指向同一个对象
unique_ptr“独占”所指向的对象
weak_ptr伴随类,一种弱引用,指向shared_ptr所管理的对象

2. shared_ptr类

2.1 make_shared函数

此为最安全的分配和使用动态内存的方法

//如果显示说明指针类型,需要使用模板的方式补全类型
shared_ptr<int> p1 = make_shared<int>(12);
//使用auto更为方便,p2指向一个值为0的int类型变量
auto p2 = make_shared<int>();

2.2 shared_ptr的拷贝和赋值

当进行拷贝或者赋值操作时,每个shared_ptr都会记录有多少个其他的share_ptr指向相同的对象,当一个shared_ptr所指对象的计数器为0,他就会自动释放自己所管理的对象。(这里的对象都是我们申请的动态内存,都是由指针管理的,没有指针指向这块内存了,也就意味着我们用不了/不再需要用这块内存,那么就应该释放)

计数器递增:拷贝,用一个shared_ptr初始化另一个shared_ptr,作为参数传递给一个函数,作为函数返回值

计数器递减:shared_ptr被赋予新值或者被销毁

//目前r指向的int只有一个引用者,此时r中的计数器为1
auto r = make_shared<int>(42);
//给r赋新值,令他指向另一个地址
//q中的对象引用计数递增,r中原来指向对象的对象计数递减
//r原来指向的对象已经没有引用者,会自动释放
r = q;

shared_ptr还会自动释放相关联的内存

//注意,这个函数可不是返回只想局部变量的指针,返回的指针是指向的是传进来的参数
shared_ptr<int> factory(int a)
{
    auto p1=make_shared<int>(a);
    return p1;
}

//这里当这个函数结束时,p的声明周期结束,p会被销毁,那么他指向的对象也会被销毁,所占用的内存会被释放
//如果在这里,这个函数将p返回出现,那么计数多加1,也就还不到销毁对象的时机
void use_factory(int a){
    auto p= factory(a);
    cout<<*p<<endl;
}

如果忘记销毁shared_ptr,就会出现内存浪费。一种可能情况是将shared_ptr放进一个容器中,随后重排了容器,从而不需要某些元素,这种情况下应该确保用erase删除那些不再需要的shared_ptr元素

3.直接管理内存

3.1 使用new分配内存

//使用new动态分配和初始化对象
string *ps1 = new string;    //ps指向一个空string
string *ps2 = new string(10,'0');    //ps指向一个值为“0000000000”的string对象
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

//如果提供了一个括号包围的初始化器,就可以使用auto自动推断
auto p1 = new auto(obj);
//错误:括号中只能有单个初始化器
auto p2 = new auto{a,b,c};

//动态分配的const对象,必须进行初始化
const int *pci = new const int(1024);

//如果内存耗尽,会抛出一个类型为bad_alloc的异常
//可以使用定位new,向其传递 nothrow 参数,避免其抛出异常而返回一个空指针
//二者都定义在文件new中
int *p1 = new int ;    //如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow) int;    //如果分配失败,new返回一个空指针



3.2 使用delete释放动态内存

传递给delete的指针必须指向动态分配的内存 或者 是一个空指针;释放一块并非new分配的内存或者将相同的指针值释放多次的行为是未定义的。

注意局部变量的指针离开他的作用域之前,我们务必要先释放他所指向的动态内存。

忘记delete内存、使用已释放掉的对象、同一块内存释放两次等问题常常会出现,应坚持使用智能指针。

Foo* factory(int a)
{
    return new Foo(arg);    //这里是动态内存,调用者最终需要释放他
}

void use_factory(int a){
    auto p= factory(a);
    //使用p但是不delete他
}//p离开了他的作用域,但是他所指向的内存并内有被释放

我们在delete指针后需要将指针重置为nullptr,但是这也只能提供有限的保护

int *p(new int(42));
auto q=p;
delete p;
p=nullptr;
//在上述操作完成后,内存释放了,p也被值为空了,但是此时q也是无效了,而仍然是空悬指针的状态
//在实际系统中查找指向相同内存的所有指针异常困难

4. shared_ptr和new结合使用

4.1 不可隐式转换

接受指针参数的智能指针构造函数是explicit的,因此不能将一个内置指针隐式转换为一个智能指针;默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存

shared_ptr<int> p1 = new int(1024);    //错误:必须使用直接初始化形式
shared_ptr<int> p2 (new int(2014));    //正确:使用了直接初始化
//类似的,如果作为函数返回值,也是这么回事

4.2 不要混合使用普通指针和智能指针

使用内置指针来访问智能指针所管理的对象很危险,因为我们不知道对象何时被销毁

void process(shared_ptr<int> ptr){
    //使用ptr
}//离开作用域,被销毁

shared_ptr<int> p(new int(1024));    //引用计数为1
process(p);            //拷贝p会增加计数,在process中计数为2;结束调用后,引用数变为1
int i=*p;              //正确:此时引用计数为1,此对象可用


int *x(new int(1024));    //危险:x是一个普通指针,不是智能指针
process(x);               //错误:不能将内置指针隐式转换为智能指针
//合法,创建一个临时变量的shared_ptr,计数为1,但是此表达式结束时,临时变量会被销毁,计数为0,此时指针所指向的内存会被释放,x已经是一个空悬指针了
process(shared_ptr<int>(x));    
int j=*x;    //未定义的:x是一个空悬指针

也不要使用get初始化另一个智能指针或者为智能指针赋值(这样你可能得到两个指向同一对象的智能指针,对象可能随时就因一个智能指针而被销毁了,但你还在开开心心的用另一个悬浮指针):智能指针的get函数返回一个指向同样对象的内置指针,此函数是为了这么一种情形设计,即我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能使用delete此指针。

4.3其他操作

智能指针还有rest,unique等成员函数

reset经常与unique一起使用,来控制多个shared_ptr所共享的对象。在改变底层对象之前,我们检查自己是否是当前对象的仅有用户;如果不是,在改变之前要制作一份新的拷贝

if(!p.unique())
    p.reset(new string(*p));    //我们不是唯一用户,分配新的拷贝;p指向了具有相同值的另一个对象
*p+=newVal;                     //现在我们知道知道自己是唯一用户了,可以改变对象值

 4.4 智能指针和异常

https://blog.csdn.net/CLZHIT/article/details/104046579

5. unique_ptr

没有类似make_shared的标准库函数,当我们定义时,必须将其绑定到一个new返回的指针上。由于unique_ptr独占的拥有他所指的对象,因此unique_ptr不支持普通的拷贝或者赋值操作。

类似的,他也有reset、构造函数参数能够指定一个可调用对象(删除器)代替delete;另外还有release函数;当unique_ptr被置为nullptr时会自动释放对象

unique_ptr<string> p(new string("hello"));
p.release();    //错误:p不会释放内存,而且我们丢失了指针
auto p1 = p.ralease();    //正确,但之后必须记得delete p1

虽然我们不能拷贝或者赋值unique_ptr,但是可以通过调用release和rest将指针所有权从一个(非const)unique_ptr转移到另一个unique

//将所有权从p1转移给p2
unique_ptr<string> p2(p1.release());    //relese将p1置空并返回指针

unique_ptr<string> p3(new string("Trex"));
//将所有权从p2转移给p2
p2.reset(p3.release());    //reset释放了p2原来指向的内存

不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr,比如从函数中返回一个unique_ptr

//这两种情况下,编译器会执行一种特殊的拷贝


//正确:此时返回的指针指向的对象是我们参数传进来的
unique_ptr<int> clone(int p){
    return unique_ptr<int>(new int(p));
}

//还可以返回一个局部对象的拷贝
unique_ptr<int> clone(int p){
    return unique_ptr<int> ret(new int(p));
    return ret;
}

 unique_ptr的构造函数中若要传递删除器,注意与shared_ptr不同的是,需要像重载关联容器的比较操作一样,需要在尖括号内提供可调用类型(函数指针)

6. weak_ptr

不控制所指对象的生存周期,指向一个由shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不改变shared_ptr的计数,当最后一个指向对象的shared_ptr被销毁时,即使仍有weak_ptr指向该对象,该对象也仍然会被销毁

由于对象可能不存在,我们不能直接使用weak_ptr访问对象,而必须调用lock。此函数检查weak_ptr所指的对象是否仍然存在

weak_ptr作用:对象存在与否,weak_ptr始终存在,我们可以先通过weak_ptr的lock函数判断对象存在与否,若存在,再拿着lock返回的shared_ptr去访问对象,从而组织用户访问不存在对象的企图

https://blog.csdn.net/u014786409/article/details/101767227

if(shared_ptr<int> np = wp.lock()){    //如果np不为空,则条件成立
    //在if中,np与wp共享对象
}
operator=()重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。
reset()将当前 weak_ptr 指针置为空指针。
use_count()查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
expired()判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。
lock()如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。

 7. 动态数组

大多数应用应该使用标准库容器而不是动态分配的数组。分配的动态数组的类必须自己定义自己版本的操作,在拷贝、赋值以及销毁对象时管理所关联的内存。

7.1 new和数组

使用new分配一个‘“数组对象”,得到的是指向第一个元素的指针,而非数组类型的指针。

所分配的内存并不是数组类型,因此不能对动态数组调用begin或者end,也不能使用范围for循环。

int *pia = new int[get_size()];    //pia指向第一个int,方括号中必须是整形,不必要是常量

//可以以使用类型别名
typedef int arrT[42];    //arrT表示有42个int的数组
int *p = new arrT;


//可以对数组中的元素进行初始化
int *p1 = new int[10];     //10个未初始化的int
int *p2 = new int[10]();   //10个默认初始化为0的int
int *p3 = new int[10]{0,1,2,3,4,5,6,7,8,9};     //可以使用花括号初始化

//可以申请空数组,但是返回的指针不能解引用
char arr[0];    //错误:不能定义长度为0的数组
char *cp = new char[0];    //正确:但是cp不能解引用

//释放动态数组时,必须在指针前面加上一个[]
//否则,很可能编译器没报错,但是程序行为肯定会异常
delete [] p;

//也可以用智能指针来管理动态数组
unique_ptr<int []> up(new int[10]);
up.release();  //自动会调用delete[]来销毁指针并释放内存,与之前我们要手动存下返回指针并delete不同


当一个unique_ptr指向数组时,我们不能使用点和箭头成员运算符,而可以使用下标运算

shared_ptr不支持管理动态数组,如果需要,则得提供自己定义的删除器

//使用lambda作为一个删除器,有了自定义删除器才能用shared_ptr管理动态数组
shared_ptr<int> sp(new int[10], [](int *p){delete[] p;});
sp.reset();


//shared_ptr未定义下标运算,而且智能指针不支持指针算数运算符(加减),为了访问数组中的元素,必须使用get来获取内置指针,然后用它来访问元素
for(size_t i=0;i!=10;++i)
    *(sp.get()+i) = i;    //使用get获得内置指针

7.2 allocator类

定义在头文件memory中,allocator类可以帮我们吧内存分配和对象构造分离开来。(使用new是无法分离的,那些没有默认构造函数的类就不能动态分配数组了)

allocator a定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存
a.allocate(n)分配一段原始的、未构造的内存,保存n个类型为T的对象
a.deallocate(p, n)释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象,调用deallocate前,用户必须对每个在这块内存中创建的对象调用destroy
a.construct(p, args)args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象
a.destory(p)  p为T*类型的指针,此算法对p指向的对象执行析构函数
//使用allocator分配内存,此时的内存是未构造的
allocator<string> alloc;    //可以分配string的alloctor对象
auto const p = alloc.allocate(n);    //分配n个未初始化的string类型

//构造,注意是对每个元素一个一个的构造
auto q = p;    
alloc.construct(q++);    //*q为空字符串
alloc.construct(q++,10,'c');    //*q为cccccccccc
alloc.construct(q++,"hi");     //*q为hi

//最后,q指向最后构造的元素之后的位置
//还未构造对象的情况下就使用原始内存是错误的:
cout << *p <<endl;    //正确:使用string的输出运算符
cout << *q <<endl;    //灾难:q指向未构造的内存

//当我们用完对象,必须对每个构造的元素调用destroy来销毁他们
while(q != p)
    alloc.destroy(--q);

//当所有元素被销毁,我们才能释放这片内存;这里的n必须和我们调用allocate时传入的n大小一致
alloc.deallocate(p,n);

标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象

//例子:将一个int的vector拷贝到动态内存中,且动态内存比vector大一倍,后一半的空间用给定值填充
auto p = alloc.allocate(vec.size()*2);
auto q = uninitialized_copy(vec.begin(),vec.end(),p);
uninitialized_fill(q,vec.size(),42);

这些函数在给定目的位置创建元素,而不是由系统分配内存给他们(定义在memory中)

操作解释
uninitialized_copy(b, e, b2)从迭代器be给定的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能够容纳输入序列中元素的拷贝。
uninitialized_copy_n(b, n, b2)从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中。
uninitialized_fill(b, e, t)在迭代器be执行的原始内存范围中创建对象,对象的值均为t的拷贝。
uninitialized_fill_n(b, n, t)从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值