C++ Primer笔记12---chapter12 动态内存
Posted Ston.V
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 智能指针和异常
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)
从迭代器 b
和e
给定的输入范围中拷贝元素到迭代器b2
指定的未构造的原始内存中。b2
指向的内存必须足够大,能够容纳输入序列中元素的拷贝。uninitialized_copy_n(b, n, b2)
从迭代器 b
指向的元素开始,拷贝n
个元素到b2
开始的内存中。uninitialized_fill(b, e, t)
在迭代器 b
和e
执行的原始内存范围中创建对象,对象的值均为t
的拷贝。uninitialized_fill_n(b, n, t)
从迭代器 b
指向的内存地址开始创建n
个对象。b
必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。
以上是关于C++ Primer笔记12---chapter12 动态内存的主要内容,如果未能解决你的问题,请参考以下文章
C++ Primer笔记16---chapter13 代码实例
C++ Primer笔记15---chapter13 拷贝控制2