全局对象在程序启动时分配,结束时销毁。局部自动对象进入其定义的所在程序块中分配在离开时销毁。局部static对象在第一次使用前分配,在程序结束时销毁。
C++还支持动态内存。动态分配的对象的生存期与它们在哪里创建时无关的,只有当显式的释放时这些对象才会销毁。但是动态内存是最容器出错的地方。为了更安全的使用动态内存,标准库定义了两个智能指针来管理动态分配对象。当一个对象应该被释放时,指向它的智能指针可以确保自动释放它。
静态内存用来保存局部static对象、类static数据成员以及定义在任何函数外的变量。栈内存用来保存定义在函数内的非static对象。分配在栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块内运行才存在。
除了静态内存和栈内存每个程序还拥有一个内存池。这部分内存空间叫做自由空间或者堆。程序用堆进行存储动态分配的对象。
动态内存与智能指针
C++中动态内存的管理通过一对运算符来完成的:new,在动态内存中为对象分配空间空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象并释放与之关联的内存。
有两种智能指针来管理动态对象。智能指针的行为类似常规指针,重要的却别是它负责自动释放分配的对象。智能指针定义在头文件memory中
- shared_ptr允许多个指针指向同一个对象。
- unique_ptr则是独占所指的对象。
- weak_ptr是一种弱引用指向shared_ptr所指的对象
shared_ptr类
智能指针也是模板,需要提供额外的信息。指针可以指向的类型与vector一样,我们在尖括号内给出类型,之后给出需要定义智能指针的名字。
shared_ptr<string> p1;
shareed_ptr<list<int>> p2;
默认初始化保存的是空指针。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mw6ouVMf-1641812444884)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210902105202492.png)]
make_shared函数在动态内存中非配一个对象并初始化它。返回指向此对象的shared_ptr。make_shared是一个模板函数也需要给额外信息指定对象类型。
shared_ptr<int> p3 = make_shared<int>(42); //auto p = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10,'9');
shared_ptr<int> p5 = make_shared<int>();
auto p6 = make_shared<int>(p3); //这时候p3中保存的对象就有两个引用者引用计数为2,只有引用计数清零后智能指针就会自动销毁对象
当用一个shared_ptr初始化另一个shared_ptr时,或者将它作为一个参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给shared_ptr赋一个新值(比方说主动赋空指针)或者是shared_ptr被销毁(局部变量离开作用域)计数器就会递减。一旦减少为0就会自动释放自己管理的对象。
auto r = make_shared<int>(42);
r = q; //给r赋值,令他指向另外一个地址。递增q指向的对象的引用计数。递减r原来指向的对象的引用计数。r原来指向的对象已没有引用者会自动释放。
shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动的销毁此对象。它是通过另一个特殊的成员函数——析构函数来完成的。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr就会销毁对象并释放它们占用的内存。
//定义strBlob类。这个类用智能指针拷贝。 从而实现了很多浅拷贝的功能
class StrBolod{
public std::vector<std::string>::size_type size_type;
StrBolo();
StrBlod(std::initializer_list<std::string>i1);
size_type sieze() cosnt{return data->empty();}
//添加和删除元素
void push_back(cosnt std::string &t) {data->push_back(t)}
void pop_back();
//元素访问
std::string &front();
std::string &back();
private:
std::shared_ptr<std::vector<std:;string>> data;
void check(size_type i,const std::string &msg) const; //如果data[i]不合法,抛出一个异常
}
//构造函数实现
strBlol::strBlob():data(make_shared<vector<string>>()){}
strBolo::strBlod(initializer_list<string> il):data(make_shared<vector<string>>(il)){}//该函数接受一个initializer_list构造函数并将其参数传递给对应的vector构造函数。此构造函数通过拷贝列表中的值来初始化vector的元素
//元素访问成员函数
//p_back、front、和back操作访问vector的元素,这些操作在试图访问元素之前必须检查元素是否存在所以先定义了一个检查函数
void StrBlob::check(size_type i,const string &msg) cosnt{
if (i>=data->size())
throw out_of_range(msg);
}
//p_back和元素访问成员函数首先调用check.如果check成功再完成自己的工作
string & StrBlob::front(){ //应该对cosnt做一次重载版本
//如果vector为空,check会抛出一个异常
check(0,"front on empty StrBlob");
return data->front();
}
string & StrBlob::back(){ //同上做一下重载版本
//如果vector为空,check会抛出一个异常
check(0,"back on empty StrBlob");
return data->back();
}
string & StrBlob::pop_back(){
check(0,"pop_back on empty StrBlob");
data->pop_back();
}
//对于拷贝 会自动拷贝,因为使用了智能指针所以自动拷贝是完全正确的。
直接管理内存
C++定义了new和delete来直接分配和释放内存。相对于智能指针,使用这两个运算符容易出错。
使用new动态分配和初始化对象
int *pi = new int;//pi指向一个动态分配的未初始化的对象 等价于 new int();
string *ps = new string;//初始化为空的string 等价于 new string();
in *pi = new int(1024);
string * ps = new string(10,'9');
vector<int> *pv = new vector<int>{0,1,2,3,45};
auto p = new auto(obj); //obj是一个对象,这样就可以用
动态分配const对象
用new分配const对象时合法的 const int *pci = new const int(1023);
内存耗尽
当内存耗时new会返回一个bad_alloc异常,我们可以用new(nothrow)来阻止这种异常。int *p2 = new(nothrow) int;
这种new的形式叫做placement new。允许向new传递额外的参数。
分配内存之后需要尽快释放内存使用 delete p;//p是一个空指针或者一个动态分配的对象
和类类型不同的是局部指针离开作用域时不会自动释放掉内存。
记得重置指针
delete指针使用后,指针值就变得无效了,但是很多机制上仍然保存着一个地址(称为空悬指针 ),可以再delete后变成空指针处理。
share_ptr和new结合使用
如果我们不初始化一个智能指针,就会被初始化为一个空指针。我们可以使用new返回的指针来初始化智能指针,接受指针参数的智能指针是explicit的,所以不能将普通指针显式的转化为智能指针。
shared_ptr<double> p1;
shared_ptr<int> p2(new int(42)):
shared_ptr<int> p1 = new int(1024); //错误,必须直接使用初始化形式来初始化一个只能只恨 不能用赋值的形式 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXCRPIj5-1641812444885)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210902164409080.png)]
不要混合使用普通指针和智能指针,shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间,这也是为什么我们推荐使用make_shared而不是new的原因。
智能指针的get可以得到对应对象的内置指针格式,但是不要用这个得到的内置指针绑定在另一个智能指针上(用来初始化另一个智能指针或者为另一个智能指针赋值),这是错误的。
智能指针与异常
使用异常处理流程能在异常发生时令程序流程继续,这种程序需要让异常发生后的资源能够正确的释放。一个简单的确保资源释放的方法是使用智能指针。
void f(){
shared_ptr<int> sp(new int(42)); //分配一个对象
//代码结束时抛出异常,且在f中未被捕获
//在函数结束时自动释放内存
}
void f(){
int *ip = new int(42);
//这段代码抛出一个异常,切没有在f中被捕获。并且f外没有指针指向这个分配的区域,内存永远不会被释放。
delete ip;
}
智能指针与哑类
标准库中很多类型都定义了析构函数。负责清理对象使用的资源。但是不是所有类都是定义良好的。特别是为c和c++两种语言设计的一些类,通常都是要求用户显式的释放所使用的资源。
那些分配了资源但是没有定义析构函数来释放这些资源的类可能会遇到与使用动态内存相同的错误——程序员非常容器忘记释放资源。类似的如果在资源分配和释放之间发生了异常,程序也会发生资源泄露。
与管理动态内存类似,我们通常可以使用类似的技术管理不具有良好定义的析构函数的类。这些可以用智能指针解决,但是要注意有些没有分配动态内存的可以自己定义一个delete操作传入智能指针来进行类似析构函数一样的后处理。
智能指针陷阱:
- 不使用相同的内置指针初始化(或reset)多个智能指针。
- 不delete get()返回的指针
- 不使用get()初始化或reset另一个指针
- 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就无效了
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
unique_ptr
一个unique_ptr拥有它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unqiue_ptr被销毁时,它所指向的对象也被销毁。
与shared_ptr不同没有类似make_shared的标准库函数返回一个unque_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似于shared_ptr,初始化unique_ptr必须采用直接初始化的方式。
unique_ptr<double> p1;
unique_ptr<int> p2(new int(42));
unique_ptr<int> p3(3);
//由于一个unique_str拥有它指向的对象因此unique_str不具备拷贝和赋值操作:
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1); //错误unique不支持拷贝
unique_ptr<string> p3;
p3 = p2; //错误,unique_ptr不支持赋值操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qONeSgM7-1641812444886)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210902201846244.png)]
虽然不能拷贝或者赋值unique_ptr,但是可以通过release或reset将指针的所有权从一个(非const) unique_ptr转移给另一个unique:
unique_ptr<string> p2(p1.release()); //将所有权从p1转移给p2
unique_ptr<string> p3(new string("Trex")); //release将p1置为空
p2.reset(p3.release()); //reset释放了p2所指向的内存
传递和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p)); //从int*创建一个unique_ptr<int>
}
unique_ptr<int> clone(int p){
unique_ptr<int> ret(new int(p));
return ret;
}
auto p = clone(3); //正确,将要销毁的unique_ptr是可以被return返回然后赋值的。因为编译器知道返回的对象需要被销毁,在此情况下会执行一种特殊的拷贝
向auto_ptr传递删除器
类似于shared_ptr,unqiue_ptr默认情况下用delete释放所指的对象。与shared_ptr一样我们可以重载一个unique_ptr中默认的删除其。但是unique_ptr管理删除器的方式与shared_ptr也有一些区别。
重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,我们需要在尖括号中unique_ptr指向类型之后提供删除其类型。在创建一个reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)。
unique_ptr<objT,delT> p(new objT,fcn); //p指向一个类型为object的对象并使用一个类型为delT的对象释放objT对象。
weak_ptr
weiak_ptr是一种不能控制所指对象生命周期的智能指针,它指向一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr 的引用计数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ByJifR9p-1641812444886)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210902205950170.png)]
当我们创建一个weak_ptr的对象,要用一个shared_ptr来初始化它。
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); //wp弱共享p;p的引用计数不会改变
if(shared_ptr<int> np = wp.lock()){ // wp.lock()返回一个指向wp对象的shared_ptr ,使用这种方式可以避免智能指针意外的在其他地方被释放掉
//使用np //在if中 np和p共享对象。
}
动态数组
new和delete运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。例如vector和string都是在连续内存中保存其元素,因此当容器需要重新分配内存时,必须一次性分配很多内存。
为了支持这种c++提供了两种一次分配一个对象数组的方法。c++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含一个名为allocator的类,允许我们将分配和初始化。使用allocator通常会提供更好的性能和更灵活的内存管理能力。
当然书中建议使用容器。
new和数组
为了让new分配一个对象数组,我们要在类型名之后跟上一对方括号,在其中指明要分配的对象的数目。
int *pia = new int[get_size()];//pia指向第一个int
,方括号中不必是常量
可以用一个表示数组类型的类型别名来分配一个数组,这样在new中就不需要方括号了
typedef int arrT[42];
int *p = new arrT; //分配一个42个int的数组。p指向第一个int
分配一个数组会得到一个元素类型的指针。即使我们用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象。new返回的是一个元素类型的指针。并且由于分配的数组并不是一个数组类型,因此不能对动态数组调用begin或者end。这些函数使用数组维度来指向首元素和尾后元素的指针。处于相同原因,也不能用范围for来处理(所谓的)动态数组中的元素。即动态数组不是数组类型。
初始化动态分配对象的数组
默认情况下,new分配的对象不管是单个分配的还是数组中的都是默认初始化的。可以对数组中的元素进行值初始化。方法是在大小后面跟上一对空括号。还可以提供一个元素初始化器的花括号列表:
int *pia = new int[10];
int *pia2 = new int[10](); //这里括号不支持传入任何的值。
int *pia3 = new int[10](){0,1,2,3,4,5,6,7,8}; //使用花括号的初始化列表进行初始化,初始化器后不够的会进行值初始化
string *psa = new string[10]; //10个空string
string *psa2 = new string[10](); //10个空string
虽然我们可以用空括号对其中的元素进行值初始化但不能在括号中给出初始化器,这意味着不能使用auto分配数组auto p = new auto(pia);//是不行的
。
动态分配一个空数组是合法的,可以用任意表达式确定要分配的对象的数目:
size_t n = get_size();
int * p = new int[10];
for(int *q=p;,q!=p+n;++q){
//处理数组
}
//有一个有意思的问题,当get_size返回0,会发生什么?
答案是代码能正常运行但是。虽然我们不能创建一个大小为0的数组,但是可以创建 new[0],当我们用new分配一个大小为0的数组时。new会返回一个合法的非空指针。辞职真保证与new返回的其他任何指针都不同。对于0长度的数组来说此指针就想尾后指针一样。我们可以像尾后指针一样使用这个指针。可以向此指针加上或者减去0,也可以从此指针减去自身得到0。但是此指针不能解引用因为其不指向任何元素
释放动态数组
为了释放动态数组,我们使用一种特殊形式的delte——在指针的前面加上一个[]对:
delete p; //p必须指向一个动态分配的对象或为空
delete [] p;//p必须指向一个动态分配的数组或者为空
智能数组和动态数组
标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组。必须在对象类型后面跟上一对空的方括号。
unique_ptr<int []> up(new int[10]); //up指向一个具有10个int元素的数组
up.release(); //自动调用delete[]销毁其指针
unique指向数组的时候毕竟不是指向元素,所以不能使用点和箭头运算符,但是可以使用下表运算符来访问数组中的元素
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8AngJfF4-1641812444886)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210903102643717.png)]
和unqiue_ptr不同shared_ptr不直接支持管理动态数组,同样也不支持下标运算符。但是可以自己提供删除其来管理一个动态数组。
shared_ptr<int> sp(new int[10],[](int* p){delete [] p;});
sp.reset();
//使用shared_ptr访问可以用如下的方式
for(size_t i = 0; i!=10;++i)
*(sp.get()+i) = i; //get得到一个内置和指针
allocator类
new有一些灵活性上的局限,其中一方面表现为将内存分配和对象构造组合在了一起。类似的delete将对象析构和内存释放组合到了一起。但是当分配一块大内存时我们往往希望按需构造对象。这样我们就可以分配大内存。
一般情况下,将内存分配和对象构造放到一起可能导致不必要的浪费例如:
标准库allocator类定义在头文件memory中帮助我们将内存分配和兑现构造分开。allocator仍然是模板类,其需要传入参数从而为这些对象确定恰当的内存大小和对齐位置。
allocator<string> alloc; //分配string的allocator对象
auto const p = alloc.allocate(n); //分配n个未初始化的string
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5inz8g9T-1641812444886)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210903120059792.png)]
allocator分配未构造的内存
allocator分配的内存是未构造的。我们按照需要在此内存中构造对象。在新标准中,conostruct成员函数接受一个指针和零个或多个额外参数。在给定的位置构造一个元素。额外参数用来初始化对象。
为了使用allocate返回的内存,我们必须调用construct构造对象,还未构造的对象使用原始内存是错误的。
auto q = p;
alloc.construct(q++);
alloc.construct(q++,10,'c');
当我们用完对象后,必须对每个构造的元素调用destory来销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数。
while(q!=p){
alloc.destroy(--q); //释放我们真正构建的string 我们只能对真正构造了的元素使用destroy操作
}
一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统。释放内存通过调用deallocate完成。
alloc.deallocate(p,n);
//释放从p开始的内存,n必须是p创建时所要求的的大小。在调用这个之前要确保这段内存已经执行过destroy销毁了所有构造了的元素。
拷贝和填充未初始化内存的算法
标准库为allocator类定义了两个伴随算法,可以再未初始化的内存中创建对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cnlncelo-1641812444886)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210903122424144.png)]
注意上面的函数都返回构造了的最后一个元素下一个内存所在的位置。
//例子,一个int型的vector,分配了一块两倍vector大小的内存然后将vector拷贝到这块内存然后对后一半用一个给定值填充。
auto p = alloc.allocate(vi.size()*2);
auto q = uninitialized_copy(vi.begin(),vo.end(),p);
uninitialized_fill_n(q,vi.size(),42);