第 12 章 动态内存
概述. 动态内存和智能指针
在c++中,动态内存的管理是通过一对运算符来完成的:
new,在动态内存中为对象分配空间并返回一个指向该对象的指针。我们可以选择对对象进行初始化
delete,接受一个动态对象的指针,销毁该对象,并释放与之相关的内存。
动态分配内存带来了许多问题,比如忘记释放的内存泄漏,提前释放的指针非法访问内存。
c++11新标准库提供了两种智能指针类型来管理动态对象,只能指针的行为类似常规指针,区别是它自动释放所指向的内存。
头文件#include <memory>
两种智能指针:
shared_ptr:允许多个指针指向同一个对象。
unique_ptr:独占所指向的对象。
伴随类weak_ptr:指向share_ptr所管理的对象。
1.share_ptr类:
//shared_ptr和unique_ptr都支持的操作 //空智能指针。可以指向string类型的对象 shared_ptr<string>sp; unique_ptr<string>up; sp //sp可以作为条件判断sp是否指向一个对象 *sp //解引用sp,获得它指向的对象 sp->mem //等价于(*sp).mem sp.get() //返回sp中所报存的指针。要小心使用,所智能指针释放了对象,则返回的指针所指向的对象也不存在了。 swap(sp,sq) sp.swap(sq) //交换sp和sq中的指针 //shared_ptr支持的操作 make_shared<T>(args) //返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化对象 shared_ptr<T>p(q) //p是shared_ptr q的拷贝,此操作会递增q中的记数器,q中的指针必须能转换成T* p = q //p和q都是shared_ptr,所保存的指针必须能相互转换,此操作会递减p的引用计数,增加q的引用计数,p引用计数为0时会释放其管理的内存。 p.use_count() //返回与p共享智能指针的数量,可能很慢主要用于调试 p.unique() //当p.use_count()为1时,返回ture,否则返回false。
注意:
智能指针比较所指对象是否相同,只能通过get( )返回指针,比较指针的地址是否相等来判断。
<1.make_shared函数#include <memory>
make_shared是一个非成员函数,具有给共享对象分配内存,并且只分配一次内存的优点,和显式通过构造函数初始化(new)的shared_ptr相比较,后者需要至少两次分配内存。这些额外的开销有可能会导致内存溢出的问题
最安全的使用动态内存的方法是使用一个make_shared的函数。
此函数在动态内存中分配一个对象并初始化,返回指向此对象的shared_ptr。
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论我们拷贝一个share_ptr,计数器都会递增。
当我们给一个shared_ptr赋值或者shared被销毁,计数器就会递减。
当用一个shared_ptr初始化另外一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值(赋值给其他的),计数器都会递增
一旦一个share_ptr的计数器变为0,它就会释放自己所管理的对象。
!注意标准库是用计数器还是其他数据结构来记录有多少个指针共享对象由标准库来决定,关键是智能指针类能记录有多少个shared_ptr指向相同的对象,
并能在恰当的时候自动释放对象。
补充:智能指针和make_shared分配内存初始化
#include <iostream> #include <string> #include <vector> #include <memory> using namespace std; int main() { shared_ptr<string>sp; make_shared<string>(); //动态分配内存默认初始化,必须要有括号 //make_shared<string>ms; //error:动态分配内存!!联想c语言malloc也没有起名字 - -。 make_shared<string>("a"); //动态分配内存值初始化 shared_ptr<string>sp2 = make_shared<string>(); //初始化智能指针 shared_ptr<string>sp3 = make_shared<string>("b");//初始化智能指针 }
例子:
#include <iostream> #include <memory> #include <string> using namespace std; shared_ptr<string> fun1(shared_ptr<string> sp5)//传递参数会构造一个,计数器递增,函数运行结束后释放 { auto sp6 = sp5; //创建临时并赋值,计数器递增。 cout << "sp5 use_count:" << sp5.use_count() << endl; cout << "sp5 is_unique:" << sp5.unique() << endl; return sp6; } int main() { shared_ptr<string>sp = make_shared<string>("aa"); auto sp3 = make_shared<string>(10,'a');//通常使用auto来简化定义一个对象来保存make_shared的结果,这种方式比较简单。 cout << "sp use_count:" << sp.use_count() << endl; auto sp2(sp); //拷贝sp,count计数会增加 cout << "sp use_count:" << sp.use_count() << endl; cout << "sp is_unique:" << sp.unique() << endl; sp2 = sp3; //赋值sp2,计数会减少 cout << "sp use_count:" << sp.use_count() << endl; cout << "sp is_unique:" << sp.unique() << endl; auto sp4(sp3); cout << "sp3 use_count:" << sp3.use_count() << endl; cout << "sp3 is_unique:" << sp3.unique() << endl; sp = sp3;//sp指向sp3指向的,sp指向的被销毁。 cout << "sp use_count:" << sp.use_count() << endl; cout << "sp is_unique:" << sp.unique() << endl; auto sp7 = fun1(sp); cout << "sp7 use_count:" << sp.use_count() << endl; cout << "sp7 is_unique:" << sp.unique() << endl; }
注意! sp1 = sp2; sp2计数器值增加,右值的计数器增加,左值指向的对象的计数器减少,减少为0时自动释放对象
<2.shared_ptr 自动销毁所管理的对象
当指向对象的最后一个shared_ptr 被销毁时,shared_ptr 类会自动销毁此对象。它是通过特殊的成员函数析构函数来控制对象销毁时做什么操作。
shared_ptr 的析构函数会递减它所指向的对象的引用计数,如果引用计数变为0,shared_ptr 的函数就会销毁对象,并释放它占用的资源。
对于一块内存,shared_ptr 类保证只要有任何shared_ptr 对象引用它,它就不会被释放。
如果我们忘记了销毁程序不再需要的shared_ptr,程序仍然会正确运行,但会浪费内存
注意!:如果你将shared_ptr存放于一个容器中,而后不在需要全部元素,而只使用其中的一部分,要记得调用erase删除不再需要的那些元素。
注意!:将一个shared_ptr 赋予另一个shared_ptr 会递增赋值号右侧的shared_ptr 的引用计数,而递减左侧shared_ptr 的引用计数,如果一个shared_ptr 引用技术
变为0时,它所指向的对象会被自动销毁。
<3.使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因
1.程序不知道自己需要使用多少对象
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据。
使用动态内存的一个常见的原因是允许多个对象共享相同的状态。
重点例子!!
我们希望定义一个Blob类,保存一组元素,与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。既当我们拷贝一个Blob时,
原Blob对象及其拷贝应该引用相同的底层元素。
定义一个管理string的类,命名为StrBlob。
#include <iostream> #include <string> #include <memory> //智能指针和动态分配内存 #include <vector> #include <initializer_list> //初始值列表 #include <stdexcept> class StrBlob { public: typedef std::vector<std::string>::size_type size_type; StrBlob(); StrBlob(std::initializer_list<std::string>il); size_type size()const{ return data->size(); } bool empty() { return data->empty(); } //添加删除元素 void push_back(const std::string &s){ data->push_back(s); } void pop_back(); //访问元素 std::string& front(); std::string& back(); const std::string& front()const; const std::string& back() const; private: std::shared_ptr<std::vector<std::string>> data; //private 检查函数。 void check(size_type i, const std::string &msg)const; }; //默认构造函数 StrBlob::StrBlob(): data(std::make_shared<std::vector<std::string>>()) { } //拷贝构造函数 StrBlob::StrBlob(std::initializer_list<std::string>il): data(std::make_shared<std::vector<std::string>>(il)) { } void StrBlob::check(size_type i, const std::string &msg)const { if(i >= data->size()) throw std::out_of_range(msg); } const std::string& StrBlob::back()const { check(0, "back on empty StrBlob"); return data->back(); } <span style="color:#FF0000;"> //避免代码重复和编译时间问题,用non-const版本调用const版本 //在函数中必须先调用const版本,然后去除const特性 //在调用const版本时,必须将this指针转换为const,注意转换的是this指针,所以<>里面是const StrBlob* 是const的类的指针。 //调用const版本时对象是const,所以this指针也是const,通过转换this指针才能调用const版本,否则调用的是non-const版本,non-const调用non-const会引起无限递归。 //return时,const_cast抛出去除const特性。</span> std::string& StrBlob::back() { const auto &s = static_cast<const StrBlob*>(this)->back(); //<span style="color:#FF0000;">auto前面要加const,因为auto推倒不出来const。</span> return const_cast<std::string&>(s); } const std::string& StrBlob::front()const { check(0, "front on empty StrBlob"); return data->front(); } std::string& StrBlob::front() { const auto &s = static_cast<const StrBlob*>(this)->front(); return const_cast<std::string&>(s); } void StrBlob::pop_back() { check(0, "pop_back on empty StrBlob"); data->pop_back(); } int main() { std::shared_ptr<StrBlob>sp; StrBlob s({"wang","wei","hao"}); StrBlob s2(s);//共享s内的数据 std::string st = "asd"; s2.push_back(st); //s2.front(); std::cout << s2.front() << std::endl; std::cout << s2.back() << std::endl; }
可以输出s和s2的size( )是相等的证明他们共享的是同一块内存。
2.直接管理内存
<1.使用new动态分配和初始化对象
#include <iostream> #include <string> #include <vector> using namespace std; int main() { //在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回该指向该对象的一个指针。 int *p = new int; int *p2 = new int(10); string *p3 = new string(10,'a'); vector<string> *p4 = new vector<string>{"a","b","c"}; //也可以对动态分配的对象进行值初始化,只需在类型名之后加一对空括号 string *p5 = new string(); //值初始化 string *p6 = new string; //默认初始化 int *p7 = new int; //但是对于内置类型是未定义的。*p7值未定义 //对动态分配的对象进行初始化通常是个好主意。 //auto auto p8 = new auto("abc"); auto p9 = new auto{1,2,3}; //error ?? //const //一个动态分配的const对象必须进行初始化。 const string *p10 = new const string("aha"); //内存耗尽 int *p11 = new int; //如果分配失败,new会抛出一个std::bad_alloc int *p12 = new (nothrow) int; //如果分配失败返回一个空指针。 bad_alloc和nothrow定义在#include <new> }
<2.delete注意:
delete p;释放p所指向的对象的那块内存区域,释放后p仍然指向那块区域(测试时输出的地址仍然相同),但是释放后输出的对象已无效。
一般我们可以在释放delete p后, p = nullptr。这样明确指针不指向其他的区域。
空悬指针:指向一块曾经保存数据现在已经无效的内存的指针。
坚持只使用智能指针,就可以避免所有的问题,对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会释放它。
#include <memory> #include <iostream> using namespace std; int main() { //p也指向p2指向的内存,那么p原来所指向的内存区域就没有其他指针指向了,也没有释放,内存泄漏 int *p = new int(20); int *p2 = new int(40); p = p2; //p3也指向p4所指向的内存,但是p3原先指向的内存区域计数器变为0时,内存自动会释放。 auto p3 = make_shared<int>(20); auto p4 = make_shared<int>(30); p3 = p4; }
<3.shared_ptr和new结合使用。
<<1.
可以用new来初始化智能指针
接受指针参数的智能指针构造函数是explicit(避免隐式转换)的,我们不能将一个内置指针隐式转换为一个智能指针。
默认情况下,一个用来初始化智能指针的普通指针必须指向一块动态分配的内存,因为智能指针默认使用delete释放它所关联的对象。
如果将智能指针绑定到其他类型的指针上,我们必须自己定义自己的释放操作。
#include <iostream> #include <memory> using namespace std; shared_ptr<int> clone(int p) { return shared_ptr<int>(new int(p)); //return new int(p); error } int main() { shared_ptr<int>p(new int(10)); cout << "p:" << p << endl; cout << "p:" << p.unique() << endl; cout << "p:" << p.use_count() << endl; //error: not int* transfrom shared_ptr<int> //shared_ptr<int>p2 = new int(10); //定义和改变shared_ptr的其他方法 int *p2 = new int(20); shared_ptr<int>p3(p2); cout << "p3:" << *p3 << endl; cout << "p3:" << p3.unique() << endl; cout << "p3:" << p3.use_count() << endl; unique_ptr<int>p4(new int(40)); //shared_ptr<int>p5(move(p4));//要添加move将p4转换为右值。左值不行。 //cout << "p5:" << p5.unique() << endl; //cout << "p5:" << p5.use_count() << endl; p3.reset();//重置p3 int *p6 = new int(50); p3.reset(p6);//重置p3并将p3绑定到p6。 cout << "p3:" << *p3 << endl; cout << "p3:" << p3.unique() << endl; cout << "p3:" << p3.use_count() << endl; //shared_ptr<T> p(q,d); d自己定义delete释放内存 //shared_ptr<T> p(p2,d); //p.reset(); 释放p对象,use_count()减了1。 //p.reset(q); 释放p对象,获得q,q必须是内置类型动态分配的! //p.reset(q,d); 用d函数释放p对象,获得q, (q必须是内置类型动态分配的!)。 }
<<2.不要混用智能指针和普通指针。
shared_ptr 可以协调对象的析构(也就是计数为0释放),仅限于自身的拷贝,所以推荐使用make_shared而不是new。
在分配对象时就将对象绑定在shared_ptr上面。
当将一个shared_ptr 绑定到一个普通指针时,我们就将内存管理交给了shared_ptr,之后我们就不应该使用内置指针来访问shared_ptr指向的内存了。
使用内置指针来访问智能指针所附则的对象是非常危险的,我们不知道对象何时被销毁。
<<3.也不要使用get初始化另一个智能指针或者为智能指针赋值
智能指针定义了一个名为get的函数,返回一个普通类型的指针。
目的:向不能使用智能指针的代码传递一个内置指针,使用get返回的指针的代码不能delete此指针。
将另一个智能指针绑定到get返回的指针也是错误的。
永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。
普通指针不能自动转化为智能指针。
为什么使用get返回的指针不能使用delete
{ auto p = make_shared<int>(20); auto q = p.get(); delete q; }
p是一个智能指针,在函数块结束后会自动调用内部delete释放动态申请的空间,然而我们又delete了一次,等于释放了两次空间。
*** Error in `./a.out': double free or corruption
(out): 0x00007fff21931150 ***
书上的一个例子,有点问题记录下
#include <memory> #include <iostream> using namespace std; int main() { shared_ptr<int>p(new int(42)); int *q = p.get(); cout << "count:" << p.use_count() << endl; //delete q; error: { auto t = shared_ptr<int>(q); //转换 cout << "count:" << t.use_count() << endl; } int foo = *p; cout << foo << endl; }
书上没有auto t,按照书上foo可以正常使用,估计是编译器优化。
书上意思是智能指针t也指向q所指向的内存,但是引用计数都是1,函数块结束后,t被释放,内存也被delete,那么p指向的未定义了。
<<4.智能指针和异常
使用智能指针,即使程序块过早结束,智能指针类也能确保内存不再需要时将其释放。
但是普通指针就不会
void fun( ) { int *p = new int(42); //如果这时抛出一个异常且未捕获,内粗不会被释放,但是智能指针就可以释放。 delete p; }
<<5.智能指针和哑类
标准很多都定义了析构函数,负责清理对象使用的资源,但是一些同时满足c和c++的设计的类,通常都要求我们自己来释放资源。
通过智能指针可以很好的解决这个问题
举个网络连接的例子:
connection connect(*destination); void disconnect(connect); void f(destination &d) { connection c = connect(&d); disconnect(d);//如果没有调用disconnect,那么永远不会断开连接。 }
//使用智能指针优化,等于自己定义了delete代替本身的delete connection connect(*destination); void end_disconnect(connection*p) {disconnect(p);} void f(destination &d) { connection c = connect(&d); shared_ptr<connection>p(&d, end_connect); //f退出时,会自动调用end_connect。 }
demo,用string代替connection类型。
#include <iostream> #include <string> #include <memory> using namespace std; typedef string connection; connection& connect(connection *s) { cout << "正在连接..." << endl; s = new connection("connect"); return *s; } void disconnect(connection *s) { cout << "正在断开连接..." << endl; } int main() { connection p; connection *d; p = connect(d); shared_ptr<connection>sp(&p,disconnect);//&p }
这样做即使我们忘记写断开连接或者中间发生了异常都会保证执行断开连接的代码。
!注意:智能指针陷阱
*不使用相同的内置指针值初始化(或reset)多个智能指针 //多个智能指针还是单独的指向内置指针的内存,use_count分别为1
*不delete get( )返回的指针 //两次delete释放,智能指针内部也会delete
*不使用get( )初始化或reset另一个智能指针
//free( ): invalid pointer:也是多次释放
*如果你使用get( )返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了
*如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(删除函数向上面的disconnect( ))。
课后题12.15
使用lambda改写connect函数。
#include <iostream> #include <string> #include <memory> #include <functional> #include <algorithm> using namespace std; typedef string connection; connection& connect(connection *s) { cout << "正在连接..." << endl; s = new connection("connect"); return *s; } void disconnect(connection *s) { cout << "正在断开连接..." << endl; } int main() { connection p; connection *d; p = connect(d); //shared_ptr<connection>sp(&p,disconnect); //error:lambda代表了删除函数。那么参数列表也要和删除函数一致,因为delete内部是free(p)。 //shared_ptr<connection>sp(&p, [&p] { disconnect(&p); }); shared_ptr<connection>sp(&p, [](connection *s) { disconnect(s); }); }
2.unique_ptr
介绍:一个unique_ptr 拥有它所指向的对象,和shared_ptr不同,某个时刻只能有一个unique_ptr 指向一个给定对象,当unique_ptr 被销毁时,对象也被销毁
<1.
unique没有类似make_shared,必须手动new,将其绑定
由于unique_ptr独占它所指向的对象,因此他不支持普通的拷贝和赋值
但是有种特殊的拷贝可以支持:我们可以拷贝或赋值一个即将要被销毁的unique_ptr。
#include <memory> #include <iostream> #include <string> using namespace std; unique_ptr<int> clone(int p) { unique_ptr<int>q(new int(p)); return q; //return unique<int>q(new int(p)); } int main() { unique_ptr<string>p(new string("aaa")); shared_ptr<string>p2(new string("aaa")); //unique_ptr<string>p3(p); error:不能拷贝 //unique_ptr<string>p4 = p; error:不能赋值 unique_ptr<string>p5; string s = "a"; //p5.reset(&s); error:两次释放 //两种转移所有权的方法 unique_ptr<string>p6(p.release());//p.release(),释放p对指针对象的控制权,返回指针并将p置空,并不会释放内存 unique_ptr<string>p7; p7.reset(p6.release()); //p6释放控制权,p7指向这个对象。 //特殊的拷贝和赋值 int i = 10; clone(i); }
在早的版本中提供了auto_ptr的类,它有unique_ptr 的部分特性,但是不能在容器中保存auto_ptr, 也不能在函数中返回 auto_ptr, 编写程序时应该使用unique_ptr.
<2.向unique_ptr 传递删除器
类似shared_ptr, unique_ptr 默认情况下用delete释放它指向的对象,和shared_ptr 一样我们可以重载一个unique_ptr 中默认的删除器类型。
重载一个unique_ptr 中的删除器会影响到unique_ptr 类型及如何构造该类型的对象,
我们必须在尖括号中unique_ptr 指向类型之后提供删除器的类型,在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)
#include <memory> #include <iostream> using namespace std; typedef int connection; connection* connect(connection *d) { cout << "正在连接..." << endl; d = new connection(40); return d; } void disconnect(connection *p) { cout << "断开连接..." << endl; } int main() { connection *p,*p2; p2 = connect(p); cout << p << endl; cout << *p2 << endl; unique_ptr<connection, decltype(disconnect)*>q(p2,disconnect); //在尖括号中提供类型,圆括号内提供尖括号中的类型的对象。 //使用decltype()关键字返回一个函数类型,所以必须添加一个*号来指出我们使用的是一个指针 }
注意:
p.release( ); //error:p2不会释放内存,而且丢失了指针
auto q = p.release( ); //q 是int * 类型, 记得delete释放q
c++11
3.weak_ptr
weak_ptr 是一种不控制对象生存期的智能指针,它指向由一个shared_ptr 管理的对象。
#include <memory> #include <iostream> using namespace std; int main() { shared_ptr<int>p0 = make_shared<int>(42); auto p1 = p0; cout << "p1 count:" << p1.use_count() << endl; weak_ptr<int>p2; weak_ptr<int>p3(p1); //与p1指向相同的对象 p3 = p0; //与p0指向相同的对象 p2 = p3; cout << "p2 count:" << p2.use_count() << endl; //w.reset() 将w置空 p2.reset(); //将p2置空 cout << "p2 count:" << p2.use_count() << endl; cout << "p3 count:" << p3.use_count() << endl; //w.expired() 若w.use_count()为0,返回true,否则返回false expired(过期的) //w.expried() 也就是判断shared_ptr的count为0吗。 cout << "p2 expired:" << p2.expired() << endl; cout << "p3 expired:" << p3.expired() << endl; //w.lock() 如果expired为true,返回一个空的shared_ptr,否则返回一个指向w的对象的shared_ptr //w.lock() 也就是如果shared_ptr的count为0,返回空,不为0,返回shared_ptr。 auto p4 = p2.lock(); cout << "p4 count:" << p4.use_count() << endl; auto p5 = p3.lock();//引用计数加1 cout << "p5 count:" << p5.use_count() << endl; }
!注意:
当我们创建一个weak_ptr 必须用一个 shared_ptr 初始化。
引入lock和expired是防止在weak_ptr 不知情的情况下,shared_ptr 被释放掉
weak_ptr 不会更改shared_ptr 的引用计数。
例子:
定义一个StrBlobPtr(内部weak_ptr)类打印StrBlob中的元素
上面的StrBlobPtr 例子的改进。
StrBlobPtr内部是weak_ptr实现的,它实际上就是一个助手类,作用就是类似一个旁观者,一直观测StrBlob的资源使用情况
/* *避免拷贝,多个指针共用一个vector<string> *使用weak_ptr访问共享的对象 * */ #include <iostream> #include <vector> #include <string> #include <initializer_list> #include <memory> #include <stdexcept> #include <fstream> #include <sstream> class StrBlob; class StrBlobPtr; class StrBlob { public: friend class StrBlobPtr; typedef std::vector<std::string>::size_type size_type; StrBlob(); //默认构造函数 StrBlob(std::initializer_list<std::string>il); //拷贝构造函数 size_type size() { return data->size(); } //对data进行解引用就是对vector<string>操作 std::string& front(); std::string& back(); const std::string& front()const; const std::string& back()const; void push_back(const std::string &s) { data->push_back(s); } void pop_back(); //StrBlobPtr begin() { return StrBlobPtr(*this); } //StrBlobPtr end() { auto ret = StrBlobPtr(*this, data->size()); // return ret; } private: void check(size_type sz, std::string msg)const; std::shared_ptr<std::vector<std::string>>data; }; std::string& StrBlob::front() { const auto &s = static_cast<const StrBlob*>(this)->front(); return const_cast<std::string&>(s); } std::string& StrBlob::back() { const auto &s = static_cast<const StrBlob*>(this)->back(); return const_cast<std::string&>(s); } const std::string& StrBlob::front()const { check(0, "front on empty vector"); return data->front(); } const std::string& StrBlob::back()const { check(0, "back on empty vector"); return data->back(); } void StrBlob::check(size_type sz, std::string msg)const { if(sz >= data->size()) throw std::out_of_range(msg); } StrBlob::StrBlob(): data(std::make_shared<std::vector<std::string>>()) { } StrBlob::StrBlob(std::initializer_list<std::string> il): data(std::make_shared<std::vector<std::string>>(il)) { } /* --------------------------------------------------------------------------------- */ //必须定义在StrBlobPtr的后面 //否则error: invalid use of incomplete type ‘class StrBlob’ class StrBlobPtr { public: friend StrBlob; StrBlobPtr():curr(0){ } StrBlobPtr(StrBlob &s, std::size_t sz = 0): wptr(s.data), curr(sz){ } std::string& deref()const;//返回当前string StrBlobPtr& incr(); //递增 private: std::shared_ptr<std::vector<std::string>> check(std::size_t i, const std::string &msg)const; std::weak_ptr<std::vector<std::string>> wptr; std::size_t curr; //当前下标 }; StrBlobPtr& StrBlobPtr::incr() { check(curr, "increment past end of StrBlobPtr"); ++curr; //推进当前位置。 return *this; //为什么要return *this, 如果再次自加可以重复,举个例子就像赋值一样 a = b = c; 如果不返回对象不能继续赋值。 } //return *this是一份拷贝。 return this是地址。 std::string& StrBlobPtr::deref()const { auto p = check(curr, "dereference past end"); //shared_ptr引用计数会增加,但是作用域结束后,引用计数又会减1 return (*p)[curr]; //p是所指的vector } //check检查是否存在shared_ptr和大小 std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string &msg)const { auto ret = wptr.lock(); //检查是否存在,存在返回shared_ptr,不存在返回空的shared_ptr. if(!ret) throw std::runtime_error("unbound StrBlobPtr"); if(i >= ret->size()) throw std::out_of_range(msg); return ret; } int main(int argc, char*argv[]) { std::fstream is(argv[1]); std::string s; StrBlob S; while(std::getline(is, s)) { std::string temp; std::istringstream ist(s); while(!ist.eof()) { ist >> temp; S.push_back(temp); } } std::cout << "size:" << S.size() << std::endl; StrBlobPtr sp(S); for(auto i = 0; i < S.size(); ++i) { std::cout << sp.deref() << std::endl; sp.incr(); } }
4.动态数组
new和delete一次只能分配和释放一个对象,但有时我们需要一次为很多对象分配内存的功能
C++和标准库引入了两种方法,另一种new 和 allocator。
使用allocator 通常会提供更好的性能和更灵活的内存管理能力。
<1.new和数组
动态数组不是数组类型。
#include <iostream> #include <memory> using namespace std; typedef int arr[10]; int main() { int *p = new int[10]; int *p2 = new arr; for(int i = 0; i < 10; i++) { p[i] = i; } for(int i = 0; i < 10; i++) cout << p[i] << " "; cout << endl; //for(const int i : p); //error:动态分配数组返回的不是数组类型,而是数组元素的指针。所以不能用范围for /*---------------------------------- */ //初始化动态数组 int *pi = new int[10]; //未初始化 int *pi2 = new int[10](); //初始化为0,且有括号必须为空 string *ps = new string[10]; //10个空string string *ps2 = new string[10](); //10个空string //可以使用列表初始化 int *pi3 = new int[10]{1,2,3,4,5,6,7,8,9,0};//初始值列表里的值不能多于容量,否则new失败,不会分配内存 string *ps3 = new string[10]{"a","b","c","d","e","f","g","h","i",string(3,'x')}; //释放动态数组 delete []pi3; //必须要加[]括号,且释放动态数字时是逆序释放。如果delete动态数组不加[],行为是未定义的。 /*----------------------------------- */ //智能指针和动态数组,标准库定义了特别的unique_ptr来管理,当uo销毁它管理的指针时,会自动调用delete []; int *p5 = new int[10]; //unique_ptr<int[]> up; unique_ptr<int[]> up(p5); for(int i = 0; i < 10; ++i) cout << up[i] << " "; cout << endl; //如果使用shared_ptr的话我们必须自己定义delete函数 shared_ptr<int>sp(new int[10], [](int *p) { delete []p;}); //智能指针不支持算数类型,如果要访问数组中的元素,必须使用get函数返回一个内置指针。 cout << (*sp.get()) << endl; }
课后题12.24
#include <iostream> #include <string> using namespace std; int main() { string s1; cin >> s1; string *p = new string(s1); const char *p1 = new char[s1.size()]; p1= (*p).c_str(); //转换为一个c风格的字符串,但是是const类型的 cout << "*p1 " << *p1 << endl; //只会输出第一个字母,说明new创建返回的不是数组类型的指针,而是元素类型 cout << "p1 "; for(int i = 0; i < s1.size(); ++i) cout << p1[i] << " "; cout << endl; const char *p2 = new char[10]; string ss; ss = "aaaaaaaaaaaaaaaaaaaaaa"; //超出了动态分配的内存空间 p2 = ss.c_str(); cout << "p2 "; for(int i = 0; i < ss.size(); ++i) //结果还是正常输出了! cout << p2[i] << " "; cout << endl; }
<2.使用allocator类
引入allocator的原因是new类上的缺陷
new它将内存分配和对象构造结合到了一起
比如:string *p = new string;
new是现在找一块内存分配,不够继续malloc,在分配内存的地址上调用构造函数,delete也一样,在释放内存的时候也会调用析构函数。
内置类型要指定初值。
但是如果我们希望指定它的初值,不让它调用默认构造函数new就不可行了,而且本身调用了一次构造函数,然后我们赋值了一次。
更重要的是,没有默认构造函数的就不能动态分配内存了。
#include <iostream> #include <memory> #include <string> using namespace std; int main() { //allocator<T>a; 定义了一个名为a的allocator对象,可以为T对象分配空间 allocator<string>alloc; //a.allocate(n); 为T分配n个空间 string *const p = alloc.allocate(10); //为10个string分配了内存,且内存是原始的,未构造的 cout << sizeof(p) << endl; //释放p中地址的内存,这快内存保存了n个T对象,p必须是allocte返回的指针,n必须是allocate(n)的n //且在调用deallocate时必须先毁坏这块内存中创建的对象 alloc.deallocate(p,10); //p是allocate返回的指针,construction是传递给类型T的构造函数,在p指向的内存中构造一个对象 //alloc.construct(p, construction) //对p指向的对象进行析构函数。 //alloc.destroy(p); allocator<string>alloc2; auto const p2 = alloc2.allocate(10); auto q = p2; auto q2 = p2; //为了使用我们allocate的内存,必须用construct构造对象,使用未定义的内存,其行为是未定义的。 alloc2.construct(q++, "sssss"); cout << *q2++ << endl; alloc2.construct(q++, "he"); cout << *q2++ << endl; alloc2.construct(q++, 10, 'x'); cout << *q2 << endl; //对使用过的内存进行释放,调用string的析构函数,注意不能destory未使用的内存。 while(q2 != p2) alloc2.destroy(q2--); //元素被销毁后,我们可以重新使用这块内存,也可以归还给系统 alloc2.deallocate(p2, 10); //deallocate的指针不能为空,必须指向allocate分配的内存,且deallocate和allocate的大小相同。 }
标准库还为allocator定义了两个伴随算法
在未初始化的内存中创建对象,都定义在头文件memory
uninitialized_copy(b,e,b2) b,2是输入容器的迭代器,b2是内存的起始地址,要保证空间足够 uninitialized_copy_n(b,n,b2) b是输入容器的起始迭代器,复制n个,复制到以b2为起始地址的动态内存中 uninitialized_fill(b,e,t) b,e是动态内存的起始和终止位置,t是要fill的元素 uninitialized_fill_n(b,n,t) b是动态内存的起始,fill n个,t是要fill的元素
#include <iostream> #include <string> #include <vector> #include <memory> using namespace std; int main() { //copy返回的是最后一个元素的下一个位置,fill返回void vector<string>ivec(10,"a"); allocator<string>alloc; auto const p = alloc.allocate(ivec.size()*4); auto q = uninitialized_copy(ivec.begin(),ivec.end(), p); auto q2 = q; while(q-- != p) cout << *q << " "; cout << endl; uninitialized_fill_n(q2, ivec.size(), "b"); for(auto i = 0; i < ivec.size(); ++i) cout << *q2++ << " "; cout << endl; vector<string>ivec2(10,"c"); auto q3 = uninitialized_copy_n(ivec2.begin(),10,q2); for(auto i = 0; i < ivec2.size(); ++i) cout << *q2++ << " "; cout << endl; uninitialized_fill(q3,q3+10, "d"); for(auto i = 0; i < ivec2.size(); ++i) cout << *q3++ << " "; cout << endl; }