【C++ Primer 第5版 笔记】第12章 动态内存与智能指针
2017-03-01 10:31
295 查看
转载:http://blog.csdn.net/wwh578867817/article/details/41866315
第 12 章 动态内存 与 智能指针
静态内存用来保存:(1)局部static对象。
(2)类的static数据成员。
(3)定义在任何函数之外的变量。
栈内存用来保存:(1)定义在函数内的非static对象。
堆用来存储动态分配的对象。当动态对象不再使用时,必须显式的销毁它们。
在c++中,动态内存的管理是通过一对运算符来完成的:
new:在动态内存中为对象分配空间并返回一个指向该对象的指针。我们可以选择对对象进行初始化。
delete:接受一个动态对象的指针,销毁该对象,并释放与之相关的内存。
动态分配内存带来了许多问题,比如忘记释放的内存泄漏,提前释放的指针非法访问内存。
C++11新标准库提供了两种智能指针类型来管理动态对象,智能指针的行为类似常规指针,区别是它自动释放所指向的内存。
头文件
#include <memory>
两种智能指针:
(1)shared_ptr:允许多个指针指向同一个对象。
(2)unique_ptr:独占所指向的对象。
伴随类
weak_ptr:一种弱引用,指向share_ptr所管理的对象。
1.share_ptr类:
智能指针也是模板。
默认初始化的智能指针中保存着一个空指针。
解引用一个智能指针返回它指向的对象。
注意:
智能指针比较所指对象是否相同,只能通过get( )返回指针,然后比较指针的地址是否相等来判断。
<1. make_shared函数
#include <memory>
make_shared是一个非成员函数,具有给共享对象分配内存,并且只分配一次内存的优点,和显式通过构造函数初始化(new)的shared_ptr相比较,后者需要至少两次分配内存。这些额外的开销有可能会导致内存溢出的问题。
最安全的分配和使用动态内存的方法是使用一个 make_shared 的标准库函数。
此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
当使用 make_shared时,必须指定想要创建的对象的类型。
例如:
通常也可以用auto来定义一个对象,用来保存make_shared的结果。
<2. shared_ptr的拷贝和赋值
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。
当我们拷贝一个shared_ptr,计数器都会递增。当我们给一个shared_ptr赋值或者shared_ptr被销毁,计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会释放自己所管理的对象。
!注意:标准库是用计数器还是其他数据结构来记录有多少个指针共享对象由标准库来决定,关键是智能指针类能记录有多少个shared_ptr指向相同的对象,并能在恰当的时候自动释放对象。
<3. 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时,它所指向的对象会被自动销毁。
<4. 使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因
(1)程序不知道自己需要使用多少对象
(2)程序不知道所需对象的准确类型
(3)程序需要在多个对象间共享数据
使用动态内存的一个常见的原因是允许多个对象共享相同的状态。
重点例子!!
我们希望定义一个Blob类,保存一组元素,与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。既当我们拷贝一个Blob时,
原Blob对象及其拷贝应该引用相同的底层元素。
定义一个管理string的类,命名为StrBlob。
可以看出s1和s2共享同一个对象。
2.直接管理内存
<1. 使用 new 动态分配和初始化对象
<2. delete注意:
delete p;释放p所指向的对象的那块内存区域,释放后p仍然指向那块区域(测试时输出的地址仍然相同),但是释放后输出的对象已无效。
一般我们可以在释放delete p后, p = nullptr。这样明确指针不指向其他的区域。
空悬指针:指向一块曾经保存数据现在已经无效的内存的指针。
坚持只使用智能指针,就可以避免所有的问题,对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会释放它。
3.shared_ptr 和 new 结合使用
<<1. 可以用new来初始化智能指针
接受指针参数的智能指针构造函数是explicit(避免隐式转换)的,我们不能将一个内置指针隐式转换为一个智能指针。
必须使用直接初始化形式,来初始化一个智能指针。
默认情况下,一个用来初始化智能指针的普通指针必须指向一块动态分配的内存,因为智能指针默认使用delete释放它所关联的对象。
如果将智能指针绑定到其他类型的指针上,我们必须自己定义自己的释放操作。
<<2. 不要混用智能指针和普通指针
shared_ptr 可以协调对象的析构(也就是计数为0释放),仅限于自身的拷贝,所以推荐使用make_shared而不是new。
在分配对象时就将对象绑定在shared_ptr上面。
当将一个shared_ptr 绑定到一个普通指针时,我们就将内存管理交给了shared_ptr,之后我们就不应该使用内置指针来访问shared_ptr指向的内存了。
使用内置指针来访问智能指针所附则的对象是非常危险的,我们不知道对象何时被销毁。
<<3. 也不要使用get初始化另一个智能指针或者为智能指针赋值
智能指针定义了一个名为get的函数,返回一个普通类型的指针。
目的:向不能使用智能指针的代码传递一个内置指针,使用get返回的指针的代码不能delete此指针。
将另一个智能指针绑定到get返回的指针也是错误的。
永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。
普通指针不能自动转化为智能指针。
为什么使用get返回的指针不能使用delete
p是一个智能指针,在函数块结束后会自动调用内部delete释放动态申请的空间,然而我们又delete了一次,等于释放了两次空间。
*** Error in `./a.out': double
free or corruption (out): 0x00007fff21931150 ***
<<3. reset和unique搭配使用
reset和unique一起使用,来控制多个shared_ptr共享的对象。
4.智能指针和异常
使用智能指针,即使程序块过早结束,智能指针类也能确保内存不再需要时将其释放。
但是普通指针就不会。
<<1.智能指针和哑类
标准很多都定义了析构函数,负责清理对象使用的资源,但是一些同时满足c和c++的设计的类,通常都要求我们自己来释放资源。
通过智能指针可以很好的解决这个问题
举个网络连接的例子:
定义删除器
//使用智能指针优化,等于自己定义了delete代替本身的delete
demo, 用string代替connection类型。
这样做即使我们忘记写断开连接或者中间发生了异常都会保证执行断开连接的代码。
!注意:智能指针陷阱
*不使用相同的内置指针值初始化(或reset)多个智能指针
//多个智能指针还是单独的指向内置指针的内存,use_count分别为1
*不delete get( )返回的指针 //两次delete释放,智能指针内部也会delete
*不使用get( )初始化或reset另一个智能指针
//free( ): invalid pointer:也是多次释放
*如果你使用get( )返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了
*如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(删除函数像上面的disconnect(
))。
课后题12.15
使用lambda改写connect函数。
unique_ptr 的删除器是函数模板(function template), 所以需要在模板类型传递删除器的类型(即函数指针(function pointer)), 再在参数中添加具体删除器;
5.unique_ptr
介绍:一个unique_ptr 拥有它所指向的对象,和shared_ptr不同,某个时刻只能有一个unique_ptr 指向一个给定对象,当unique_ptr 被销毁时,对象也被销毁。
<1.
unique没有类似make_shared,必须手动new,将其绑定。
由于unique_ptr独占它所指向的对象,因此它不支持普通的拷贝和赋值。
但是有种特殊的拷贝可以支持:我们可以拷贝或赋值一个即将要被销毁的unique_ptr。
<2. 向unique_ptr 传递 删除器
类似shared_ptr, unique_ptr 默认情况下用delete释放它指向的对象,和shared_ptr 一样我们可以重载一个unique_ptr 中默认的删除器类型。
重载一个unique_ptr 中的删除器会影响到unique_ptr 类型及如何构造该类型的对象,我们必须在尖括号中unique_ptr 指向类型之后提供删除器的类型,在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)。
//p指向一个类型为objT的对形象,并使用一个类型为delT的对象释放objT对象。
//它会调用一个名为fcn的delT类型对象。
unique_ptr<objT, delT> p (new objT, fcn);
c++11
6.weak_ptr
weak_ptr 是一种不控制所指向对象的生存期的智能指针,它指向由一个shared_ptr 管理的对象。
将一个weak_ptr 绑定到一个shared_ptr 不会改变shared_ptr 的引用计数。
!注意:
当我们创建一个weak_ptr 必须用一个 shared_ptr 初始化。
weak_ptr 不会更改shared_ptr 的引用计数。
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,必须调用lock,进行检查。
例子:
定义一个StrBlobPtr(内部weak_ptr)类打印StrBlob中的元素
上面的StrBlobPtr 例子的改进。
StrBlobPtr内部是weak_ptr实现的,它实际上就是一个助手类,作用就是类似一个旁观者,一直观测StrBlob的资源使用情况
4.动态数组
new和delete一次只能分配和释放一个对象,但有时我们需要一次为很多对象分配内存的功能
C++和标准库引入了两种方法,另一种new 和 allocator。
使用allocator 通常会提供更好的性能和更灵活的内存管理能力。
<1.new和数组
动态数组不是数组类型。
[cpp] view
plain copy
#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
[cpp] view
plain copy
#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就不可行了,而且本身调用了一次构造函数,然后我们赋值了一次。
更重要的是,没有默认构造函数的就不能动态分配内存了。
[cpp] view
plain copy
#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
[cpp] view
plain copy
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的元素
[cpp] view
plain copy
#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;
}
第 12 章 动态内存 与 智能指针
静态内存用来保存:(1)局部static对象。
(2)类的static数据成员。
(3)定义在任何函数之外的变量。
栈内存用来保存:(1)定义在函数内的非static对象。
堆用来存储动态分配的对象。当动态对象不再使用时,必须显式的销毁它们。
在c++中,动态内存的管理是通过一对运算符来完成的:
new:在动态内存中为对象分配空间并返回一个指向该对象的指针。我们可以选择对对象进行初始化。
delete:接受一个动态对象的指针,销毁该对象,并释放与之相关的内存。
动态分配内存带来了许多问题,比如忘记释放的内存泄漏,提前释放的指针非法访问内存。
C++11新标准库提供了两种智能指针类型来管理动态对象,智能指针的行为类似常规指针,区别是它自动释放所指向的内存。
头文件
#include <memory>
两种智能指针:
(1)shared_ptr:允许多个指针指向同一个对象。
(2)unique_ptr:独占所指向的对象。
伴随类
weak_ptr:一种弱引用,指向share_ptr所管理的对象。
1.share_ptr类:
智能指针也是模板。
默认初始化的智能指针中保存着一个空指针。
解引用一个智能指针返回它指向的对象。
注意:
智能指针比较所指对象是否相同,只能通过get( )返回指针,然后比较指针的地址是否相等来判断。
<1. make_shared函数
#include <memory>
make_shared是一个非成员函数,具有给共享对象分配内存,并且只分配一次内存的优点,和显式通过构造函数初始化(new)的shared_ptr相比较,后者需要至少两次分配内存。这些额外的开销有可能会导致内存溢出的问题。
最安全的分配和使用动态内存的方法是使用一个 make_shared 的标准库函数。
此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
当使用 make_shared时,必须指定想要创建的对象的类型。
例如:
shared_ptr<int> pi = make_shared<int>(25);
通常也可以用auto来定义一个对象,用来保存make_shared的结果。
auto ps = make_shared<string>(10, '9');
<2. shared_ptr的拷贝和赋值
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。
当我们拷贝一个shared_ptr,计数器都会递增。当我们给一个shared_ptr赋值或者shared_ptr被销毁,计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会释放自己所管理的对象。
auto r = make_shared<int>(42); //r指向的int只有一个引用者 r = q; //给赋值,r指向另一个地址 //递增q指向的对象的引用计数 //递减r原来指向的对象的引用计数 // r原来指向的对象已经没有引用者,会自动释放
!注意:标准库是用计数器还是其他数据结构来记录有多少个指针共享对象由标准库来决定,关键是智能指针类能记录有多少个shared_ptr指向相同的对象,并能在恰当的时候自动释放对象。
<3. 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时,它所指向的对象会被自动销毁。
<4. 使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因
(1)程序不知道自己需要使用多少对象
(2)程序不知道所需对象的准确类型
(3)程序需要在多个对象间共享数据
使用动态内存的一个常见的原因是允许多个对象共享相同的状态。
重点例子!!
我们希望定义一个Blob类,保存一组元素,与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。既当我们拷贝一个Blob时,
原Blob对象及其拷贝应该引用相同的底层元素。
定义一个管理string的类,命名为StrBlob。
#include <iostream> #include <string> #include <vector> #include <memory> #include <stdexcept> using namespace std; class StrBlob { public: typedef vector<string>::size_type size_type; StrBlob(); StrBlob(initializer_list<string> il); 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(); const string &front() const; const string &back() const; private: shared_ptr<vector<string>> data; //如果data[i]不合法,抛出一个异常 void check(size_type index, const string &msg) const; }; //默认构造函数 StrBlob::StrBlob() : data(make_shared<vector<string>>()) {} //拷贝构造函数 StrBlob::StrBlob(initializer_list<string> il) : data(make_shared<vector<string>>()) {} void StrBlob::check(size_type index, const string &msg) const { if(index >= data->size()) throw out_of_range(msg); } void StrBlob::pop_back() { check(0, "pop_back on empty StrBlob"); data->pop_back(); } const string &StrBlob::front() const { check(0, "front on empty StrBlob"); return data->front(); } string &StrBlob::front() { const auto &s = static_cast<const StrBlob*>(this)->front(); return const_cast<string &>(s); } const string &StrBlob::back() const { check(0, "back on empty StrBlob"); return data->back(); } string &StrBlob::back() { const auto &s = static_cast<const StrBlob*>(this)->back(); return const_cast<string &>(s); } int main() { std::shared_ptr<StrBlob> sp; StrBlob s1({"wang","wei","hao"}); StrBlob s2(s1);//共享s1内的数据 std::string st = "asd"; s2.push_back(st); //s2.front(); std::cout << s2.front() << std::endl; std::cout << s2.back() << std::endl; std::cout << s1.front() << std::endl; std::cout << s1.back() << std::endl; }结果:
可以看出s1和s2共享同一个对象。
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"); //p8指向一个与obj类型相同的对象 auto p9 = new auto{1,2,3}; //error:括号中只能有单个初始化器 //const //一个动态分配的const对象必须进行初始化。 const string *p10 = new const string("aha"); const int pci = new const int(1024); //内存耗尽 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(避免隐式转换)的,我们不能将一个内置指针隐式转换为一个智能指针。
必须使用直接初始化形式,来初始化一个智能指针。
shared_ptr<int> p1 = new int(1024); //错误:必须使用直接初始化形式 shared_ptr<int> p2(new int(1024)); //正确:使用了直接初始化形式
默认情况下,一个用来初始化智能指针的普通指针必须指向一块动态分配的内存,因为智能指针默认使用delete释放它所关联的对象。
如果将智能指针绑定到其他类型的指针上,我们必须自己定义自己的释放操作。
shared_ptr<int> clone(int p) { return new int(1024); //错误:不能进行普通指针到智能指针的隐式转换 } shared_ptr<int> clone(int p) { return shared_ptr<int>(new int(1024)); //正确:需要显示绑定到智能指针 }
<<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 ***
<<3. reset和unique搭配使用
reset和unique一起使用,来控制多个shared_ptr共享的对象。
if(!p.unique()) { p.reset(new string(*p)); *p += newVal; }
4.智能指针和异常
使用智能指针,即使程序块过早结束,智能指针类也能确保内存不再需要时将其释放。
但是普通指针就不会。
void f() { shared_ptr<int> sp(new int(42)); //分配一个新对象 //这段代码抛出一个异常,且在f中未被捕获 //在函数结束时,shared_ptr能自动释放内存 }
<<1.智能指针和哑类
标准很多都定义了析构函数,负责清理对象使用的资源,但是一些同时满足c和c++的设计的类,通常都要求我们自己来释放资源。
通过智能指针可以很好的解决这个问题
举个网络连接的例子:
connection connect(destination *); void disconnect(connection); void f(destination &d) { connection c = connect(&d); //使用连接... disconnect(d);//如果没有调用disconnect,那么永远不会断开连接。 }
定义删除器
//使用智能指针优化,等于自己定义了delete代替本身的delete
connection connect(destination *); void disconnect(connection); void end_connection(connection *p) {disconnect(*p);} void f(destination &d) { connection c = connect(&d); shared_ptr<connection> p(&c, end_connection); //使用连接 //f退出时,会自动调用end_connection。 }
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 *dis; p = connect(dis); shared_ptr<connection> sp(&p, disconnect); }
这样做即使我们忘记写断开连接或者中间发生了异常都会保证执行断开连接的代码。
!注意:智能指针陷阱
*不使用相同的内置指针值初始化(或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); }); }shared_ptr 的传递删除器(deleter)方式比较简单, 只需要在参数中添加具体的删除器函数名, 即可; 注意是单参数函数;
unique_ptr 的删除器是函数模板(function template), 所以需要在模板类型传递删除器的类型(即函数指针(function pointer)), 再在参数中添加具体删除器;
5.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)); //手动new,并绑定 return q; } 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类型的对象时,必须提供一个指定类型的可调用对象(删除器)。
//p指向一个类型为objT的对形象,并使用一个类型为delT的对象释放objT对象。
//它会调用一个名为fcn的delT类型对象。
unique_ptr<objT, delT> p (new objT, fcn);
#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()关键字返回一个函数类型,所以必须添加一个*号来指出我们使用的是一个指针 }
c++11
6.weak_ptr
weak_ptr 是一种不控制所指向对象的生存期的智能指针,它指向由一个shared_ptr 管理的对象。
将一个weak_ptr 绑定到一个shared_ptr 不会改变shared_ptr 的引用计数。
!注意:
当我们创建一个weak_ptr 必须用一个 shared_ptr 初始化。
weak_ptr 不会更改shared_ptr 的引用计数。
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,必须调用lock,进行检查。
auto p = make_shared<int>(42); weak_ptr<int> wp(p); //wp若共享p:p的引用计数未改变 if(shared_ptr<int> np = wp.lock()) { //如果np不为空,则成立 //在if中,np和p共享对象 }
例子:
定义一个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和数组
动态数组不是数组类型。
[cpp] view
plain copy
#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
[cpp] view
plain copy
#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就不可行了,而且本身调用了一次构造函数,然后我们赋值了一次。
更重要的是,没有默认构造函数的就不能动态分配内存了。
[cpp] view
plain copy
#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
[cpp] view
plain copy
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的元素
[cpp] view
plain copy
#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;
}
相关文章推荐
- C++ Primer 学习笔记——动态内存与智能指针(1)
- C++Primer新笔记之----第12章 动态内存与智能指针
- c++ primer(第五版)学习笔记及习题答案代码版(第十二章)动态内存与智能指针
- 《c++ primer》 第12章 动态内存 学习笔记
- 《C++ Primer》读书笔记第十二章-1-动态内存与智能指针
- 《c++ primer》 第12章 动态内存 学习笔记
- C++ Primer : 第十二章 : 动态内存之shared_ptr与new的结合使用、智能指针异常
- C++ Primer 笔记——智能指针
- 【C】【笔记】《C和指针》第10章 结构和联合 第11章 动态内存分配 第12章 使用结构和指针 第13章 高级指针话题 第14章预处理器
- C++ 学习笔记(12)动态内存、智能指针、new和delete、动态数组、allocator
- 智能指针学习笔记
- 智能指针基础std::auto_ptr与new、delete的重载学习笔记
- [C++ Primer] : 第12章: 动态内存
- 动态内存与智能指针
- C++学习笔记之一智能指针
- C++ Primer学习笔记之第12章-泛型-踩在巨人的脚背上-prog12.cpp程序
- 【足迹C++primer】39、动态内存与智能指针(3)
- C++primer学习笔记----智能指针
- C++ Primer 学习笔记_11_指针
- C++ Primer 学习笔记_24_函数(续3) --重载函数、指向函数的指针