概述
12.1动态内存与智能指针
C++11新标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,区别是它自动释放所指向的内存。
它们定义在头文件#include <memory>
两种智能指针:
shared_ptr:允许多个指针指向同一个对象。
unique_ptr:独占所指向的对象。
伴随类weak_ptr:弱引用,指向share_ptr所管理的对象。
12.1.1 share_ptr类
shared_ptr和unique_ptr都支持的操作
shared_ptr<string>sp; //空智能指针。可以指向string类型的对象
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 //都是shared_ptr,保存的指针必须能相互转换,操作会递减p的引用计数,增加q的引用计数,p引用计数为0时会释放其管理的内存
p.use_count() //返回与p共享智能指针的数量,可能很慢主要用于调试
p.unique() //当p.use_count()为1时,返回ture,否则返回false。
初始化(构造函数)
shared_ptr<string>sp;//默认构造函数,空指针
shared_ptr<T>p(q) //p是shared_ptr q的拷贝,此操作会递增q中的记数器,q中的指针必须能转换成T*
make_shared<T>(args) //返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化对象
**注意:make_shared<T>(args)中 T类类型时:args必须与T的某个构造函数相匹配。 T为内置类型时:args必须能用来初始化一个T
赋值拷贝p = q ;
swap(sp,sq);
p= make_shared<T>(args);
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
int main()
{
shared_ptr<string> sp;
//cout << sp.use_count;//提示出错。因为没有指向任何对象
make_shared<string>(); //动态分配内存默认初始化,调用默认构造函数必须要有括号
make_shared<string>("a"); //动态分配内存值初始化
shared_ptr<string>sp2 = make_shared<string>();//初始化智能指针
shared_ptr<string>sp3 = make_shared<string>("b");//初始化智能指针
cout << sp3.use_count() << endl;
auto sp4 = sp3;//拷贝一次观察计数器变化
cout << sp3.use_count() << endl;
sp4 = sp2;//被其他赋值,观察计数器变化
cout << sp3.use_count() << endl;
getchar();
}
程序使用动态内存出于以下三种原因
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;//智能指针,指向string的vector
//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和non_const是因为有时候有const StrBlob对象,此时需要用const函数
//调用const版本时对象是const,所以this指针也是const,通过转换this指针才能调用const版本,
std::string& StrBlob::back()
{
check(0, "back on empty StrBlob");
return data->back();
}
std::string& StrBlob::front()
{
check(0, "front on empty StrBlob");
return data->front();
}
//const版本的,当const StrBlob A时,数据成员也是const的
const std::string& StrBlob::back() const
{
check(0, "back on empty StrBlob");
return data->back();
}
const std::string& StrBlob::front() const
{
check(0, "front on empty StrBlob");
return data->front();
}
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
int main()
{
std::shared_ptr<StrBlob> sp;//空指针
std::cout << sp << std::endl;
StrBlob s({ "zhang", "yang"});
StrBlob s2(s);//共享s内的数据
std::string st = "abcd";
s2.push_back(st);
std::cout << s2.front() << std::endl;
std::cout << s2.back() << std::endl;
}
12.1.2直接管理内存
默认情况下,动态分配的内存是默认初始化的,这意味着内置组合或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。
int *p=new int;
直接初始化方式:int *p=new int(1024);
值初始化方式:int *p=new int();
delete p;//p必须指向一个动态分配的对象或是一个空指针
动态内存的管理非常容易出错
1,忘记delete内存
2,使用已经释放掉的对象
3,同一块内存释放两次
坚持只使用智能指针,就可以避免所有的问题,对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会释放它。
delete之后重置指针
delete p;之后,指针值变得无效,但是指针仍然保存着动态内存的地址,此时的指针叫做空悬指针。可以在delete之后将nullptr赋予给指针。
12.1.3shared_ptr和new结合使用
如果我们不初始化一个智能指针,它就会被初始化为一个空指针。
我们可以用new返回的指针来初始化智能指针。接受指针参数的智能指针构造函数是explicit(避免隐式转换)的,我们不能将一个内置指针隐式转换为一个智能指针。
shared_ptr<int>p(new int(10)); //正确,调用了构造函数来初始化的
shared_ptr<int>p=new int(10); //错误,int *隐式转换为share_ptr类型的(构造函数接受int *),然后在初始化
不要混用智能指针和普通指针
当将一个share_ptr绑定到一个普通指针时,我们将内存的管理责任交给了这个share_ptr。一旦这样做了,我们就不应该在使用内置指针来访问share_ptr所指向的内存了。
也不要使用get初始化另一个智能指针或者为智能指针赋值
智能指针定义了一个名为get的函数,返回一个普通类型的指针。eg:p.get() 返回一个int *
目的:向不能使用智能指针的代码传递一个内置指针,使用get返回的指针的代码不能delete此指针。
将另一个智能指针绑定到get返回的指针也是错误的。
永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。
普通指针不能自动转化为智能指针。
p.get() //返回p中的指针,为内置类型
p.reset() //p是唯一指向其对象的share_ptr,reset会释放此对象,置空p
p.reset(q) //若q为内置类型,令p指向q
所以为了正确使用智能指针,我们必须坚持一些基本规范:
1,不使用相同的内置指针初始化(或reset)多个智能指针。
2,不delete get()返回的指针
3,不使用get()初始化或reset另一个智能指针
4,如果使用了get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了。
5,如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
12.1.5unique_ptr
一个unique_ptr 拥有它所指向的对象,和shared_ptr不同,某个时刻只能有一个unique_ptr 指向一个给定对象,当unique_ptr 被销毁时,对象也被销毁。
初始化:只支持new内置类型的初始化
unique_ptr<int>q(new int(p)); //正确
unique_ptr<int>q;//正确
unique_ptr<int>q=new int(p); 错误,不支持隐式初始化
unique没有类似make_shared,必须手动new,将其绑定。由于unique_ptr独占它所指向的对象,因此他不支持普通的拷贝和赋值
但是有种特殊的拷贝可以支持:我们可以拷贝或赋值一个即将要被销毁的unique_ptr。
虽然我们不能拷贝或赋值unique_ptr,但是可以通过调用release()或reset()来将指针的所有权从一个unique_ptr转移给另一个unique:
u.release();//u放弃了对指针的控制权,返回内置指针,并将u清空
u.reset();//释放u指向的对象,u不需要担心,因为程序结束u自动清空。
u.reset(q);//若q为内置指针,令u指向这个对象
#include<iostream>
#include<memory>
using namespace std;
int main()
{
unique_ptr<int> u1(new int(1));
cout << *u1 << endl;
//不能直接赋值,只能转移值
//前提是要先释放,然后给另一个指针赋值
//用到u.release()和u.reset()函数
unique_ptr<int> u2(u1.release());//u1释放自己,赋值给u2
cout << *u2 << endl;
unique_ptr<int> u3;
u3.reset(u2.release());//u2释放自己,赋值给u3
cout << *u3 << endl;
}
注意: p.release()返回一个指针,并将p置空,若没有为对象赋值,则出现错误。
p.release( ); //错误:p被置空,并返回了一个内置指针,但是内存泄漏了
auto q = p.release( ); //q 是int * 类型, 记得delete释放q
12.1.6weak_ptr
weak_ptr 是一种不控制对象生存期的智能指针,它指向由一个shared_ptr 管理的对象。所以当我们创建一个weak_ptr 必须用一个 shared_ptr 初始化。
引入lock和expired是防止在weak_ptr 不知情的情况下,shared_ptr 被释放掉
weak_ptr 不会更改shared_ptr 的引用计数。
std::weak_ptr 是一种智能指针,它对被 std::shared_ptr 管理的对象存在非拥有性(“弱”)引用。在访问所引用的对象前必须先转换为 std::shared_ptr。
std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用std::weak_ptr 来跟踪该对象。需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。
此外,std::weak_ptr 还可以用来避免 std::shared_ptr 的循环引用。
/*
*避免拷贝,多个指针共用一个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();
StrBlobPtr end() ;
private:
void check(size_type sz, std::string msg)const;
std::shared_ptr<std::vector<std::string>>data;
};
std::string& StrBlob::front()
{
check(0, "front on empty vector");
return data->front();
}
std::string& StrBlob::back()
{
check(0, "back on empty vector");
return data->back();
}
//const 版本
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的后面
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,(*p)指向vector第一个元素,与int (*p)[10]一个道理
}
//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;
}
StrBlobPtr StrBlob:: begin() { return StrBlobPtr(*this); }
StrBlobPtr StrBlob::end()
{ auto ret = StrBlobPtr(*this, data->size());
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 it =0 ;it!= S.size(); ++it)
{
std::cout << sp.deref() << std::endl;
sp.incr();
}
}
12.2动态数组
new和delete一次只能分配和释放一个对象,但有时我们需要一次为很多对象分配内存的功能
C++和标准库引入了两种方法,另一种new 和 allocator。
使用allocator 通常会提供更好的性能和更灵活的内存管理能力。
12.2.1new和数组
动态数组不是数组类型的。所以不能调用begin和end,也不能调用范围for语句。而是用数组维度来返回指向首元素和尾后元素的指针。
#include <iostream>
#include <memory>
using namespace std;
typedef int arr[10]; //声明arr为一个int的10元素数组
int main()
{
int *p = new int[10];
int *p2 = new arr; //两个等价,返回指向第一个int的指针
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.2.2allocator类
引入allocator的原因是new类上的缺陷。我们分配单个对象时,希望将内存分配和对象初始化组合在一起。但是分配大块内存时,只有在需要时才真正执行对象创建操作。
new它将内存分配和对象构造结合到了一起,delete将对象析构和内存释放组合一起。就造成了不必要的浪费。
比如:string *p = new string;
new是现在找一块内存分配,不够继续malloc,在分配内存的地址上调用构造函数,delete也一样,在释放内存的时候也会调用析构函数。
内置类型要指定初值。
但是如果我们希望指定它的初值,不让它调用默认构造函数new就不可行了,而且本身调用了一次构造函数,然后我们赋值了一次。
更重要的是,没有默认构造函数的就不能动态分配内存了。
allocator<string> alloc;
auto const p = alloc.allocate(ivec.size()*4);//返回迭代器,指向第一个内存的位置。
allocator<T> a
a.allocate(n) //分配一段原始的未分配的内存,返回迭代器,指向第一个内存位置
a.deallocate(p,n) //释放指针p开始的内存
a.construct(p,args) //在p指向的内存中构造一个对象
a.destroy(p) //对p指向的对象指向析构函数
标准库还为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的元素