目录
介绍
1.动态内存与智能指针
使用了动态生存期资源的类
定义StrBlob类
1.2直接管理内存
使用new动态分配和初始化对象
动态分配的const对象
内存耗尽
释放动态内存
指针值和delete
不要混合使用普通指针和智能指针
也不要使用get初始化另一个智能指针或为智能指针赋值
1.4智能指针和异常
智能指针和哑类
使用我们自己的释放操作
智能指针陷阱
1.5unique_pte
传递unique_ptr参数和返回unique_pte
向unique_ptr传递删除器
1.6weak_ptr
2.动态数组
2.1new和数组
初始化指针方式:
分配一个数组会得到一个元素类型的指针
初始化动态分配对象的数组
智能指针和动态数组
2.2allocator类
全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开时销毁。局部static对象在第一次使用前分配,在程序结束时销毁。
除了自动和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。
动态对象的释放被证明是编程中极其容易出错的地方。为了更安全地使用动态对象,标准库定义了两个智能指针来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。
静态内存用来保存局部static对象,类static数据成员以及定义在任何函数之外的变量。栈内存用保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在:static对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池。这部分被称作自由空间或堆。程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。
在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化。delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
在确保的正确时间释放内存是及其困难的。如果我们忘记释放内存,就会产生内存泄漏的问题;有时我们会在尚有指针引用内存的情况下我们就释放了它,这种情况就会产生引用非法内存的指针。
为了更容易,更安全地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
类似vector,智能指针也是模板。因此,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字:
shared_ptr p1;//shared_ptr 可以指向string
shared_ptr> p2;//shared_ptr 可以指向int的list
默认初始化的智能指针中保存着一个空指针。
智能指针的使用方式和普通指针类似。解引用一个智能指针返回它所指的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。
举个栗子:
//如果p1不为空 检测它是否指向一个空string
if(p1 && p1->empty())
*p1="hi!"//如果p1指向一个空string 赋予其新值
下面第一张图中式shared_ptr和unique_ptr都支持的操作。只适用于shared_ptr的操作列在第二章表中。
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在头文件memory中。
当要用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后紧跟一个尖括号,在其中给出类型:
//指向一个值为42的int的shared_ptr
shared_ptr p3=make_shared(42);
//p4指向一个值为"9999"的string
shared_ptr p4=make_shared(4,"9");
//p5指向一个值初始化的int 值为0
shared_ptr p5=make_shared();
类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。例如,调用make_shared
当然,我们通常使用auto定义一个对象来保存make_shared的结果,这种方式较为简单:
//p6指向一个动态分配的空vector
auto p6=make_shared>();
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
auto p=make_shared(42);//p指向的对象只有p一个引用者
auto q(p);//p和q指向相同对象 两个引用者
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁时,计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
auto r=make_shared(42);//r指向的int只有一个引用者
r=q;//给r赋值 令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
//r原来指向的对象已没有引用者 会自动释放
此例中我们分配了一个int,将指针保存在r中。接下来,我们将一个新值赋予r。在此情况下,r是唯一指向此int的shared_ptr,把q赋给r的过程中,此int被自动释放。
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过析构函数完成销毁工作的。类似于构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。
析构函数一般用来能释放对象分配的资源。例如,string的构造函数会分配内存来保存构成string的字符。string的析构函数就负责释放这些内存。类似的,vector的若干操作都会分配内存来保存其元素。vector的析构函数负责销毁这些元素,并释放它们所占用的内存。
shared_ptr的析构函数会递减它所指的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。
当我们返回一个shared_ptr时,引用计数会增加。
note:如果将shared_ptr放在一个容器中,而后不再需要全部元素,而只使用其中的一部分,要记得用erase删除那些不再需要的shared_ptr元素
程序使用动态内存出于一下三种原因之一:
1.程序不知道自己需要使用多少对象
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据
第一种情况是最典型的。
这里对第三种情况举个栗子
我们用shared_ptr来管理动态分配的vector:
class StrBlob
{
public:
typedef vector::size_type size_type;
StrBlob(/* args */);
StrBlob(initializer_listi1);
size_type size() const {return data->size();}
bool empty() const {return data->empty();}
//添加和删除元素
void push_back(const string &t) {data->push_back(t);}
void pop_back();
//元素访问
string &front();
string &back();
~StrBlob();
private:
/* data */
shared_ptr> data;
void check(size_type i,const string &msg) const;
};
StrBlob::StrBlob(/* args */) : data(make_shared>()){}
StrBlob::StrBlob(initializer_list i1) : data(make_shared>(i1)){}
StrBlob::~StrBlob()
{
}
void StrBlob::check(size_type i,const string &msg) const
{
if(i>=data->size()) throw out_of_range(msg);
}
string& StrBlob::front()
{
check(0,"front on empty StrBlob");
return data->front();
}
string& StrBlob::back()
{
check(0,"back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back()
{
check(0,"pop_back on empty StrBlob");
data->pop_back();
}
类似Sales_data类,StrBlob使用默认版本的拷贝,赋值和销毁成员函数来对此类型的对象进行这些操作。当我们拷贝,赋值或销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝,赋值或销毁。
C++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。
相对于智能指针,使用这两个运算符管理内存非常容易出错。而且,自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝,赋值和销毁操作的仍和默认定义。因此,使用智能指针的程序更容易编写和调试。
int *pi=new int;
string *ps=new string;
//指定初值
int *pi=new int(1024);
string *ps=new string(10,'9');
vector *pv= new vector{1,2,3}
const int *pci=new const int(1024);
const int *pcs=new const string;
类似于其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是const的,new返回一个指向const的指针。
一旦一个程序用光了它所有可用的内存,new表达式就会失败,默认情况下如果分配失败,new会抛出一个类型为bad_alloc的异常,我们可以改变使用new的方式来阻止它抛出异常。
int *p1=new int;//如果分配失败 new抛出std::bad_alloc
int *p2=new (nothrow) int;//如果分配失败 new返回一个空指针
定位new表达式允许我们向new传递额外的参数。在此例中我们传递的是nothrow对象,我们的意图是告诉它不能抛出异常。如果这种形式的new不能分配所需内存,他会返回一个空指针。bad_alloc和nothrow都定义在头文件new中。
为了防止内存耗尽,在动态内存使用完毕之后,必须将其归还给系统。我们通过delete表达式来将动态内存归还给系统。
delete p;//p必须指向一个动态分配的对象或者是一个空指针
与new类型类似,delete表达式也执行两个操作:销毁给定的指针指向的对象;释放对应的内存。
我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。
动态内存的管理非常容易出错
常见三个问题如下:
1.忘记delete
2.使用已经释放的对象
3.同一块内存释放两次
如前所述,如果我们不初始化一个智能指针,它就会被初始化为一个空指针。我们还可以用new返回的指针来初始化智能指针:
shared_ptr p1;
shared_ptr p2(new int(42));
接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式地转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针。
shared_ptr p1=new int(1024);//错误 必须使用直接初始化方式
shared_ptr p2(new int(1024));//正确 使用了直接初始化方式
出于相同原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来代替delete。
shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝之间(也是shared_ptr),这样,我们就能在分配对象的同时将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。
举个例子:
上面的例子中不会出现错误,但是如果我们强转一个内置指针就很可能出现错误:
这里由于原来是一个内置指针,在函数里边仍然会由shared_ptr来处理,所以这样在出函数之后引用计数为0,内存会被释放。
包括标准库在内的很多C++类都定义了析构函数负责清理对象使用的资源。但是,并不是所有的类都是这样良好定义的。特别是那些为C和C++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。
那些分配了资源,又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误。即忘记释放资源。
与管理动态内存相似,我们通常可以使用类似的计数来管理不具有良好定义的析构函数的类。例如,假定我们正在使用一个C和C++都使用的网络库,使用这个库的代码可能是这样的:
当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection。接下来,end_connection会调用disconnect,从而确保连接被关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,p同样会被瞎弄会,从而连接被关闭。
为了正确使用智能指针,我们必须坚持一些基本规范:
一个unique_ptr拥有它所指向的对象。与shared_pte不同,某个时刻只能有一个unique_pte指向一个给定对象。
与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似于shared_ptr,初始化unique_ptr必须采用直接初始化方式。
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:
unique_ptr的一些操作如下所示:
虽然我们不能拷贝或负责制unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:
调用release会切断unique_pte和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。在本例中,管理内存的责任简单地从一个智能指针赋值。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
不能拷贝unique_ptr有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子时从函数返回一个unique_ptr:
还可以返回一个局部对象的拷贝:
类似shared_ptr,unique_ptr默认情况下使用delete释放它所指的对象。于shared_ptr一样,我们可以重载一个unique_ptr中默认的删除器。
weak_ptr是一种不受所指向对象生存周期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr上不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。
当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:
wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。
由于对象可能不存在,我们不能直接访问对象,要用lock去访问:
new T[]分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。这些函数使用数组维度来返回指向首元素和尾后元素的指针。出于相同原因,也不能用范围for语句来处理动态数组中的元素。
默认初始化:
列表初始化:
动态分配一个空数组时合法的:
释放动态数组:
标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对方括号
我们可以用下标运算符来访问数组中的元素:
与unique_ptr不同,shared_ptr不支持管理动态数组。如果要用它我们要提供删除器:
感觉不大重要的亚子...