shared_ptr类
简述内存的分配:
静态内存用来保存局部的static对象,类static数据成员,以及定义在函数之外的变量。
栈内存用于保存定义在函数内的非static对象。
分配在静态内存或者栈内存中的对象,由编译器自动创建或销毁。
对于栈对象,仅在其定义的程序块允许时才存在;而static对象则在使用之前分配,程序结束时销毁。
另外,
除了静态内存和栈内存,每个程序还拥有一个内存池。
这部分内存被称作自由空间或堆。
程序用堆来存储动态分配的对象。
也就是说,我们在程序运行期间分配的对象,这些对象的生命周期是由程序员来控制的。换句话说,当动态对象不再被使用时,就应该在代码里显示地被销毁掉。
所以,第二篇结尾处的智能指针,以及现在这个地方的shared_ptr类,都是关于那些我们动态分配的对象(或者说内存)的管理的。
在C++当中,
我们使用new这个关键字,在动态内存中为我们的对象分配一块空间,并返回一个指向该对象的指针。
同样的,我们可以通过给delete这个关键字一个动态对象的指针,去销毁这个对象,并释放与之关联的内存。
但是,对于由我们来操控分配的这些内存,有时候却是保证不了在正确的时间内释放掉的。
而这个正确的时间内,可能意味着当我们错过它时,再去释放的时候,就会产生非法内存的指针(假如此时还有指针引用了这块内存),或者,我们直接忘记了释放这块内存,那么很显示就导致了内存泄漏。
智能指针的话,它能帮助我们在正确的时间内,去释放为对应的动态对象分配的内存空间。
而关于C++11标准中的shared_ptr
当指向动态对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此动态对象。
它是通过它的析构函数完成这个销毁工作的。
比如:
比如string,string的构造函数会分配内存用来保存构成string的字符。而string的析构函数就负责释放这些内存。
当动态对象不再被使用时,shared_ptr就会自动释放动态对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//make_shared返回shared_ptr,shared_ptr负责释放内存 shared_ptr<Fun> fact(T arg) { //处理arg return make_shared<Fun>(arg); } //继续 void use_f(T arg) { shared_ptr<Fun> p =fact(arg); //使用p //然后当p离开这个作用域的时候,它就会被释放 } // void use_f_other(T arg) { shared_ptr<Fun> p = fact(arg); //使用完p之后,返回了p return p;//导致p指向的内存被另外一个地方引用到了,所以p的引用计数器进行了递增,那么这块内存就不会被释放了 } |
动态分配对象的列表初始化
1 2 3 4 5 6 7 8 |
//初始化为空串 string * ps = new string; //未初始化 int * pi = new int; //C++11标准可以用列表初始化 int * _pi = new int(1024); string * _ps = new string(10,'*'); vector<int> *_pv = new vector<int>{0,1,2,3,4,5,6,7,8,9}; |
auto和动态分配
1 |
auto _p1 = new auto(object1); |
unique_ptr类
一个unique_ptr拥有它所指向的对象。
(某个时刻只能有一个一个unique_ptr指向一个给定对象)
当unique_ptr被销毁时,它所指向的对象也被销毁。
unique_ptr |
空unique_ptr,可以指向类型为T的对象。u1会使用delete释放它的指针 |
unique_ptr<T,D> u2 | u2会使用一个类型为D的可调用对象来释放它的指针 |
unique_ptr<T,D> u(d) | 空unique_ptr指针,指向类型为T的对象,用类型为D的对象d代替delete |
u = nullptr | 释放u指向的对象,将u置为空 |
u.release() | u放弃对指针的控制权,返回指针,并将u置空 |
u.reset() | 释放u指向的对象 |
u.reset(q) | 如果提供了内置指针q,令u指向这个对象,否则将u置为空 |
u.reset(nullptr) |
1 2 3 4 5 6 7 8 9 10 11 |
unique_ptr<string> p1(new string("sssss")); //错误的语法 unique_ptr<string> p2(p1);//unique_ptr不支持拷贝 unique_ptr<string> p3; p3 = p2;//unique_ptr不支持赋值 //不能拷贝或赋值unique_ptr,使用release或reset将指针的 //所有权从一个unique_ptr转向另一个unique_ptr unique_ptr<string> p2(p1.release()); unique_ptr<string> p3(new string("jisnsis")); p2.reset(p3.release());//reset释放了p2原来指向的内存 |
调用release会切断unique_ptr和它原来管理的对象之间的联系。
注:不能拷贝unique_ptr的规则有一个例外(我们可以拷贝或赋值一个将要被销毁的unique_ptr),如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
//new 返回int*,而我们从int*创建一个unique_ptr对象 unique_ptr<int> clone(int p) { return unique_ptr<int>(new int(p)); } // unique_ptr<int> cloe(int p) { unique_ptr<int> ret(new int (p)); //... return ret; } |
对于这两段代码,编译器都知道返回的对象将要被销毁,在此情况下,编译器执行一种特殊的“拷贝”
weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。
将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使此时还有weak_ptr指向对象,对象也还是会被释放的。
1 2 3 |
//wp若共享p,但不会改变p的引用计数 auto p = shared_ptr<int>(20); weak_ptr<int> wp(p); |
由于对象可能不存在,所以我们不能使用weak_ptr直接访问对象,在使用之前,必须先调用lock,判断一下weak_ptr指向的对象是否仍存在。如果确实存在,lock会返回一个指向共享对象的shared_ptr。
allocator类
new有一些灵活性上的局限,其中一方面的原因是它将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。
而当分配一大块内存时,我们通常希望,这块内存按需求构造对象。在这种情况下,我们希望将内存分配和对象构造分离开来。(也就是说,我们可以分配大块内存,但只在需要的时候才真的执行对象创建操作)
allocator可以帮助我们将内存分配和对象构造分离开来。
它提供的是一种类型感知的内存分配方法,它分配的内存是原始的,未构造的。
allocator |
定义一个名为a的allocator对象,它可以为类型为T的对象分配内存 |
a.allocate(n) | 分配一段原始的,未构造的内存,保存n个类型为T的对象 |
a.deallocate(p,n) | 释放从T*类型指针p中地址开始的内存,这块内存保存了n个类型为T的对象。p必须是一个先前由allocate返回的指针,且n必须是创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destory |
a.construct(p,args) | p必须是一个类型为T*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象。 |
a.destroy(p) | p为T*类型的指针,对p指向的对象执行析构函数 |
allocator分配的内存是未构造的,我们需要在此内存中构造对象。
在C++11标准中,construct成员函数接受一个指针和0个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。
=default
用于拷贝控制成员
我们可以将拷贝成员定义为=default来显示地要求编译器生成合成的版本
注意:我们只能对具有合成版本的成员函数使用=default(默认构造函数或拷贝控制成员)
1 2 3 4 5 6 7 |
class base { base() = default; base(const base &) = default; base& operator=(const base &) = default; ~base() = default; } |
=delete
用于阻止拷贝类对象
在C++11标准下,我们可以将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。
注意:析构函数不能是删除的成员
1 2 3 4 5 6 7 |
struct Nocopy { Nocopy() = default;//使用合成的默认构造函数 Nocopy(const Nocopy &) = delete;//阻止拷贝 Nocopy &operator=(const Nocopy &) = delete;//阻止赋值 ~Nocopy() = default;//使用合成的析构函数 } |
用移动类对象代替拷贝类对象
C++11标准新引入了两种机制,我们可以避免string的拷贝。
移动构造
有一些标准库类,都定义了所谓的“移动构造函数”。
std::move
定义在utility头文件中
- 当reallocate在新内存中构造string时,它必须调用move来表示希望使用string的移动构造函数。如果它漏掉了move,那它会调用string的拷贝构造函数。
- 我们通常不为move提供一个using说明。当我们调用move时,直接调用std::move而不是move。
-
move函数可告诉编译器:我们有一个左值,但我们希望像一个右值一样的处理它。我们必须认识到,调用move就意味着承诺:除了对一个rr1(见下面右值引用代码)赋值或销毁它外,我们将不再使用它。
右值引用
为了支持移动操作,C++11标准引入了一种新的引用类型-右值引用。
所谓右值引用就是必须绑定到右值的引用。
我们通过&&来获取右值引用。
右值引用只能绑定到一个将要销毁的对象,因此,我们可以“自由”的将右值引用的资源“移动到另一个对象中。
一般而言,一个左值表达式表示的是一个对象的身份。而一个右值表达式表示的是对象的值。
左值持久,右值短暂。123int i = 20;int & r = i;//i是一个左值,r是左值引用int && rr = i*21;//i*21的乘法结果属于右值,rr是一个右值引用1234int && rr1 = 23;//右值引用int && rr2 = rr1;//错误,rr1属于左值//正确的方法int && rr3 = std::move(rr1);
移动构造函数&&移动赋值运算符
上面有讲述到,有一些标准库类(诸如string等),都定义了所谓的“移动构造函数”。
如果,我们自己定义的类,也同时支持移动和拷贝,那么,结合这些实现了”移动构造“的标准库类,我们将会从中获益。
为了让我们自己的类支持移动操作,我们需要给它定义移动构造函数和移动赋值运算符。
虽然这两个成员对应类似的”拷贝“操作,但实质上,它们是从给定对象”窃取“资源,而不是真的”拷贝“资源。
类似拷贝构造函数,移动构造函数的第一个参数也是该类类型的一个引用。但是,不同之处,就是,移动构造函数用到这个引用,它必须是一个右值引用。
另外,除了完成资源的移动,移动构造函数还必须确保,源对象在被移动资源后,得处于一种状态,即销毁这个源对象是无害的,这样的一种状态。
接下来一点,就很好理解了,一旦资源完成了被移动,那么,源对象就不能再指向这个被移动的资源了(因为这些资源的所有权已经属于新创建的对象了嘛)。
如下方这个类,根据它的环境实现了移动构造和移动赋值运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
class Aet { public: Aet():elements(nullptr),first_free(nullptr),cap(nullptr) {}//默认初始化 Aet(const Aet &);//拷贝构造函数 Aet & operator=(const Aet &);//拷贝赋值运算符 ~Aet();//析构函数 public: Aet(Aet && aet) noexcept //移动构造函数-移动操作不应该抛出任何异常 :elements(aet.elements),first_free(aet.first_free),cap(aet.cap) { aet.elements = aet.first_free = aet.cap = nullptr; } Aet & operator=(Aet && rhs) noexcept//移动赋值运算符 { if(this != &rhs) { free(); elements = rhs.elements;//释放已有内存 first_free = rhs.first_free; cap = rhs.cap; //将rhs置于可析构状态 rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; } public: void push_back(const string &);//拷贝元素 size_t size() const {return first_free - elements;} size_t capacity() const {return cap - elements;} string * begin() const {return elements;} string * end() const {return first-free;} //... private: static allocator<string> alloc;//分配元素 //被添加的元素所使用 void chk_n_alloc() { if(size() == capacity()) return reallocate(); } //工具函数,被拷贝构造函数,赋值运算符和析构函数所使用 pair<string*,string*> alloc_n_copy(const string*,const string*); void free();//销毁元素并释放内存 void reallocate();//获得更多内存,并拷贝已有元素 private: string * elements;//指向数组首元素的指针 string * first_free;//指向数组第一个空闲元素的指针 string * cap//指向数组尾后位置的指针 } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
//实现 void Aet::push_back(const string & s) { chk_n_alloc();//确保空间容纳新的元素 alloc.construct(first_free++,s);//在first_free指向的元素中构造s的副本 } pair<string*,string*> Aet::alloc_n_copy(const string * lhs,const string * rhs) { auto data = alloc.allocate(rhs-lhs);//分配空间保存给定范围内的元素 return {data,uninitialized_copy(lhs,rhs,data)};//初始化并返回一个pair,该pair由data和uninitialized_copy的返回值构成 } void free() { //不能传递给deallocate一个空指针,如果elements为0,函数什么也不做 if(elments) { //逆序销毁旧元素 for(auto p = first_free; p != elements;/*空*/) alloc.destroy(--p); alloc.deallocate(elments,cap - elements); } } Aet::Aet(const Aet & s) { //调用alloc_n_copy分配空间以容纳和S一样多的元素 auto newdata = alloc_n_copy(s.begin(),s.end()); elements = newedata.first; first_free = cap = newdata.second; } Aet::~Aet() {free();} Aet & Aet::operator=(const Aet & rhs) { //调用alloc_n_copy重新分配内存,大小与rhs中元素占用空间一样多 auto data = alloc_n_copy(rhs.begin(),rhs,end()); free(); elements = data.first; first_free = cap = data.second; return *this; } void Aet::reallocate() { //重新分配当前大小两倍的空间 auto newcapacity = size()?2*size():1; //分配新内存 auto newdata = alloc.allocate(newcapacity); //将数据从旧内存移到新内存 auto dest = newdata;//指向新数组中下一个空闲位置 auto elem = elments;//指向旧数组中下一个元素 for(size_t i = 0;i < size();++i) alloc.construct(dest++,std::move(*elem++)); //一旦移动完元素就释放旧空间 free(); elements = newdata; first_free = dest; cap = elements + newcapacity; } |
移动迭代器
C++11标准定义一种移动迭代器适配器
一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器
我们可以通过标准库的make_move_iterator函数,将也给普通的迭代器转化为一个移动迭代器。
重写上述reallocate函数如下:
1 2 3 4 5 6 7 8 9 10 11 |
void Aet::reallocate() { auto newcapacity = size()?2*size():1; auto first = alloc.allocate(newcapacity); //移动元素 auto last = uninitialized_copy(make_move_iterator(begin()),make_move_iterator(end()),first); free(); elements = first; first_free = last; cap = elements + newcapacity; } |
noexcept
移动构造函数通常应该是noexcept
由于移动构造”窃取“资源,它通常不分配任何资源。
因此,移动操作通常不会抛出任何异常。
当编写一个不抛出任何异常的移动操作时,我们应该将此事通知标准库。
(除非标准库知道我们的移动构造不会抛出异常,否则它会认为移动我们的类对象时可能会抛出异常,这样,为了处理这样一种可能发送的情况,它就会做一些额外的工作。)
noexcept是C++11标准引入的,我们可以用它来承诺一个函数不会抛出异常。
使用方法就是在一个函数的参数列表后面指定noexcept
可见上一条Aet类中移动构造出noexcept的用法。
1 2 |
void recoup(int) noexcept;//不会抛出异常 void alloc(int);//可能会抛出异常 |
使用情况
- 我们确认函数不会抛出异常
- 我们根本不知道该如何处理异常
实参
1 2 |
void recoup(int) noexcept(true);//不会抛出异常 void alloc(int) noexcept(false);//可能会抛出异常 |
运算符
1 2 |
//如果recoup不抛出异常,结果为true,否则为false noexcept(recoup(i)) |
引用限定成员函数
右值和左值引用成员函数
1 2 3 |
string s1 = "a value"; string s2 = "another"; auto n = (s1+s2).find('a'); |
1 2 3 |
//上述代码中我们对一个连接后的string调用了其成员函数 //然后,有时我们可能还会这么做,如下 s1 + s2 = "woca";//S1+S2的结果是一个右值 |
在旧标准中,我们是没办法阻止这种用法的。为了向后兼容,C++11标准仍允许向右值赋值,(但是我们可能希望自己的类中阻止这种行为)同时,我们可以强制左侧运算符是一个左值。
这个时候,我们需要在参数列表后放置一个引用限定符&
1 2 3 4 5 6 7 8 9 10 |
class fun { public: fun & operator=(const fun &rhs) &//只能向可修改的左值赋值 { //执行rhs赋予本对象后所需的工作 //... return *this; } } |
引用限定符,可以是&,也可以是&&。它们分别指向一个左值或右值。
引用限定符只能用于非static成员函数,且必须同时出现在函数的声明和定义中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class fun { public: fun sorted() &&;//可用于改变的右值 fun sorted() const &;//可用于任何类型的fun private: vector<int> data; } fun fun::sorted() && { sort(data.begin(),data.end()); return *this; } fun fun::sorted() const & { fun aet(*this);//拷贝一个副本 sort(aet.data.begin(),aet.data.end()); return aet; } |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Effective C++_第一篇01/10
- ♥ Boost 程序库完全开发指南:函数并发08/25
- ♥ 51CTO:C++语言高级课程二08/08
- ♥ Soui应用 动画二06/27
- ♥ Zlib记述:一09/17
- ♥ C++并发编程 _ 共享数据05/16