为什么使用动态内存
程序不知道自己需要多少对象;
程序不知道对象的准确类型;
程序需要在多个对象之间共享数据;
动态内存在哪里
程序有静态内存、栈内存。静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建或销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间或堆。程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式的销毁它们。(c++ primer P400)
自由存储区和堆
自由存储是c++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区
堆是操作系统维护的一块内存
虽然c++编译器默认使用堆来实现自由存储。但两者不能等价
动态内存与智能指针
我们知道c++需要注意的地方之一就是对内存的管理,动态内存的使用经常会出现内存泄漏,或者产生引用非法内存的指针
新的标准库提供了两种智能指针类型来管理动态对象:
(1)shared_ptr 允许多个指针指向同一个对象
(2)unique_ptr 独占所指向的对象
定义在memory头文件中,他们的作用在于会自动释放所指向的对象
智能指针的本质
智能指针的实质是一个对象,行为却表现的像一个指针
unique_ptr的“独占”?
先说说为什么shared_ptr允许多个指针指向同一对象吧
因为 动态对象的所有权不确定。对象可以在多个作用域中共享,又不能像栈对象一样自由地值拷贝。只要有一个对象\作用域还持有这个动态对象,他就不能销毁,当他没有用时,自动销毁,这个机制后面再讲。
unique_ptr的“独占”是指:不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。例如:
std::unique_ptr<int> p (new int);
std::unique_ptr<int> q = p; //error
但是unique_ptr允许通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,注意,这时它本身就不再拥有原来指针的所有权了。
std::unique_ptr<int> p (new int);
std::unique_ptr<int> q = std::move(p); //ok
shared_ptr基操
shared_ptr也是一个模板,所以我们在创建的时候,需要提供指针指向的类型。
shared_ptr<string> p1; //指向string
shared_ptr<list<int>> p2; //指向int的list
默认初始化的智能指针为一个空指针,智能指针的使用类似于普通指针:
if (p1 &&p1->empty()) //如果p1不为空且p1指向一个空string
{
*p1 = "lvbai"; //赋值
}
- shared_ptr和unique_ptr都支持的一些操作
shared_ptr<T> p1; //空智能指针,可以指向类型为T的对象
unique_ptr<T> p2;
if (p1) //将p1用作一个条件判断,若p指向一个对象,为true
{...}
*p1; //解引用p1,获得他指向的对象
p->member; //等价于*(p1).member
p1.get(); //返回p1中保存的指针。若智能指针释放了对象,则这个指针指向的对象也消失了
swap(p1, q); //交换p和q中的指针即p1.swap(q);
- 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.unique() //若p.use_count()为1,返回true
p.use_count() //返回与p共享对象的智能指针的数量,可能很慢,主要用于调试
make_shared函数
什么是make_shared函数,他的作用是什么呢?
make_shared函数是一个安全的分配和使用动态内存的方法。他会在动态内存中分配一个对象并初始化它,返回值是一个指定类型的shared_ptr,同样也定义在memory的头中
//指向一个值为42的int的shared_ptr
shared_ptr<int> p = make_shared<int> (42);
//指向一个值为"999"的string
shared_ptr<string> q = make_shared<string> (3, '9');
//指向一个值初始化的int,即值为0
shared_ptr<int> w = make_shared<int> ();
make_shared怎么初始化
make_shared用其参数来构造给定类型的对象,也就是说,我们的参数,必须符合给定类型的某一构造函数,不传参就默认初始化与shared_ptr的区别,为什么使用make_shared更好
他相比shared_ptr减少了内存分配的次数,而内存分配的代价较高;他会立即获得申请的裸指针,不会造成内存泄漏make_shared的缺点
当构造函数是保护或私有的时,无法使用make_shared;对象调额内存可能无法及时回收
shared_pt的强引用、弱引用
关于智能指针怎么实现自动释放对象,简单来说,智能指针的内部有一个引用计数,当有指针指向其对象,引用计数就会++,相反–,当减为0时,自动释放该对象
到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。关键是智能指针类能记录有多少个shared_ptr指向相同的对象,并能在恰当的时候自动释放
强引用和弱引用就是shared_ptr用来维护引用计数的信息
- 强引用
用来记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个强引用离开的时候销毁( 也可能释放). 弱引用
用来记录当前有多少个正在观察该对象的 weak_ptrs. 当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话).当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他的shared_ptr指向相同的对象
当指向一个对象的最后一个shared_ptr被销毁,shared_ptr类会自通过调用对应的析构函数销毁此对象
shared_ptr会自动释放相关联的内存
//该函数返回一个T类型的动态分配的对象,对象是通过一个类型为Q的参数来进行初始化的
shared_ptr<T> Fun(Q arg)
{
//对arg进行处理
//shared_ptr负责释放内存
return make_shared<T>(arg);
//由于返回的是shared_ptr,我们可以保证他分配的对象会在恰当的时候释放
}
void use_Fun(Q arg)
{
shared_ptr<T> p = Fun(arg);
//使用p
}
//函数结束,p离开了作用域,他指向的内存被自动释放
shared_ptr<T> use_Fun(Q arg)
{
shared_ptr<T> p = Fun(arg);
//使用p
return p; //返回p时,引用计数递增
}
//p离开了作用域,但他不会释放指向的内存
shared_ptr和new结合使用
shared_ptr<int> p1(new int(1));//直接初始化
shared_ptr<int> p2 = new int(1);//错误,int是内置类型
接受指针参数的智能指针构造函数是explicit的,必须使用直接初始化,不能做隐式类型转换
shared_ptr<int> FUn(int p)
{
return new int(p); //error
return shared_ptr<int>(new int(p)); //right
}
定义自己的释放操作
shared_ptr<T> p(q) //p管理内置指针q,q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u) //p从unique_ptr u那里接管了对象的所有权,并将u置空
shared_ptr<T> p(q, d) //p接管了内置指针q所指向的对象的所有权,q必须能够转换为T*类型。p将使用可调用对象d来代替delete
shared_ptr<T> p(p2, d) //p是shared_ptr p2的拷贝,调用d来代替delete
//删除器必须接受所指定类型的参数,如,上述中就要接受一个类型为T*的参数
p.reset()
p.reset(q)
p.reset(q, d)
//若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置空。若还传递了参数d,则会调用d而不是delete来释放q
//例如:
void del(int* p)
{...}
shared_ptr<int> p(new int(1), del);
shared_ptr<int> p(new int, del);
智能指针的陷阱和缺陷
- 不要混合使用智能指针和普通指针
void process(shared_ptr<int> ptr)
{
...
}
//ptr离开作用域,被销毁
int* x(new int(1024));
process(x); //error
process(shared_ptr<int>(x)); //合法,但内存会被释放
int j = *x; //未定义,x是一个空悬指针
//使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时被销毁
- 永远不要使用get初始化另一个智能指针或为智能指针赋值
智能指针定义的get函数返回值是一个内置指针,指向智能指针管理的对象,所以结合我们上文所述,这样有可能导致产生空悬指针,从而发生未定义的行为。
此函数是为了:需要向不能使用智能指针的代码传递一个内置指针。
void f(int* q)
{
shared_ptr<int> tmp(q);
}//释放了q指向的内容
int main()
{
shared_ptr<int> p(new int(1));
int* q = p.get();
f(q);
int qq = *q; //未定义的行为
}
//对上述代码编译器并不会给出错误信息
get用来将指针的访问权限传递给代码,只有在确定代码不会delete指针的情况下,才能使用get
unique_ptr
unique_ptr的直观认知应该就是“独占”、“拥有”了吧。它的意思就是某个时刻只能有一个unique_ptr指向一个给定的对象,即不能拷贝和赋值
int* p (new int(3));
shared_ptr<int> p1(p);
auto p2 = p1; //ok
unique_ptr<int> p3(p);
unique_ptr<int> p4(p); //ok,智能指针毕竟只是一个帮助你管理的一个工具,它并不能知道有多少初始化的动作
unique_ptr<int> p5 = p3; //error
- 初始化
unique_ptr没有类似make_shared的操作,只能直接初始化
unique_ptr<int> p(new int(1024)); //ok
unique_ptr<int> p1 = new int; //error
unique_ptr<int> p2(p); //error
- unique_ptr基操
unique_pre<T> p1; //空unique_ptr,可以指向类型为T的对象,p1会使用delete来释放它的指针,p2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr<T, D> p2;
unique_ptr<T, D> p(d); //空unique_ptr,指向类型为T的对象,用类型为D的对象d来代替delete
p = nullptr; //释放p指向的对象,将p置为空
p.release(); //p放弃对指针的控制权,返回指针,并将p置空
p.reset(); //释放p所指向的对象
p.reset(q); //如果提供了内置指针q,令p指向这个对象;
p.reset(nullptr);
- 虽然我们不能拷贝和赋值,但我们可以调用上述所说的reset和release将指针的所有权从一个(非const)unqiue_ptr转移给另一个unique_ptr
//将所有权从p1转移给p2
(1)unique_ptr<string> p2(p1.release());
(2)p2.reset(p1.release());
- 单纯调用release是错误的
p.release(); //错误,release放弃了控制权不会释放内存,丢失了指针
auto q = p.release(); //正确,记得delete掉q
- 特殊的版本
unique_ptr针对new出来的数组特殊化,是一个特殊化的版本
unique_ptr<int []> q(new int[10]);
q.release();
//自动用delete[]销毁其指针释放内存
- unique_ptr作为参数传递和返回值
unique_ptr的不能拷贝有一个例外:
unique_ptr<int> Fun(int p)
{
return unique_ptr<int>(new int(p));
}
//返回一个局部对象的拷贝
unique_ptr<int> Fun(int p)
{
unique_ptr<int> ret(new int(p));
//...
return ret;
}
编译器知道要返回的对象将要被销毁,执行了一种特殊而“拷贝”(移动操作)