动态内存
之前一篇文章(这篇文章)讲到过,c++内存分为:堆区,栈区,静态区/全局区,代码区和文字常量区五个区。c++通过动态分配的内存位于堆区,堆区的内存需要程序员自动释放,如果没有进行内存释放,就有可能导致内存泄漏。
一、直接管理内存
1、c++通过运算符new在动态内存中分配空间并且返回一个指向该对象的指针;delete接受一个动态对象指针并销毁该对象释放内存。动态分配的对象可以默认初始化,也可以进行值初始化,对于内置类型来说,进行默认初始化其值是未定义的,如:
int* p1=new int; //默认初始化,不确定的值
int* p2=new int(); //值初始化为0
int* p3=new int(10);//值初始化为10
2、也可以动态分配一个const对象,和一般cosnt对象一样,动态分配的const对象必须初始化,如:
const int* p=new const int(10);
3、r如果内存耗尽,我们不能通过new运算符分配要求的内存空间,这时使用new运算符就会抛出一个类型为bad_alloc的异常。如果当使用new运算符不能分配内存时,我们不希望抛出异常而是返回一个空指针,在new后面加上nothrow,如:
int *p=new int(10); //当内存不够时,抛出类型为bad_alloc 的异常。
int *p=new (nothrow) int(10);//当内存不够时,返回一个空指针而不是抛出异常
4、使用new分配得到的动态内存,必须使用delete来手动释放内存(智能指针除外),如果分配的是动态数组,则使用delete[]。使用delete时,需要注意一下几点:
①、delete必须作用的指针必须是指向动态分配的对象或者该指针为空指针;
②、不能对指向同一个内存地址的多个指针释放多次。
③、被delete后的指针称为空悬指针,虽然此时该指针无效了,但是该指针仍然可能保存有动态分配的内存地址,因此使用空悬指针会产生不确定的错误,所以在delete指针后,最好将指针设为空指针(nullptr)。
总结以上几点,可以知道使用new和delete直接管理动态内存容易出现以下三个问题:
①、忘记delete内存,导致内存泄漏;
②、对同一块内存释放多次,比如delete两个指向相同内存地址的指针;
③、使用已经delete后的指针可能产生错误,最好将delete后的指针置为空指针。
由于直接管理动态内存比较麻烦而且容易出错。为了更好的管理动态内存,c++新标准库提供了两种智能指针来管理动态对象:shared_ptr允许多个指针指向同一个对象;unique_ptr则独占所指向的对象。另外还有一个weak_ptr的弱指针,可以指向shared_ptr所管理的对象。
二、shared_ptr指针
1、shared_ptr类为模板类,使用shared_ptr指针时需要指定对象类型。如:
shared_ptr<int> p1;//默认初始化的智能指针为空指针
2、最安全分配和使用动态内存的方法是调用名为:make_shared的标准库函数,make_shared函数动态分配一个对象,并且返回指向该对象的shared_ptr,如:
shared_ptr<int> p1=make_shared<int>(); // 值初始化为0
shared_ptr<int> p2=make_shared<int>(10); //初始化为10
shared_ptr<string> p3=make_shared<string>("HELLO");
3、可以使用指向动态内存的普通指针(注意是指向动态内存)来初始化一个shared_ptr智能指针,但是接受指针参数的智能指针构造函数是exolicit的,因此只能直接初始化,如:
int* p = new int(10); //指向动态内存的普通指针
shared_ptr<int> sp = p; //错误,不能进行隐式转换,explicit
shared_ptr<int> sp(p); //正确,直接初始化,调用智能指针的构造函数
但是注意,如果我们将shared_ptr绑定到一个普通指针上,就将内存管理交给了智能指针,那么智能指针可能会释放掉内存,因此我们不应该再使用原来的普通指针访问原来的对象了,如:
void func(int* p)
{
shared_ptr<int> sp(p); //临时shared_ptr智能指针,
}
int main()
{
int* p = new int(10); //指向动态分配对象的普通指针
func(p);
cout << *p << endl; //输出为定义
return 0;
}
如上述代码所示:当我们将指针p传入到函数func时,p指针初始化了一个shared_ptr指针sp,func函数结束时sp指针就会释放内存,此时p所指向的内存也就被释放了,后续在使用p访问就可能发生错误。
4、同样我们可以使用get函数从shared_ptr智能指针中得到一普通的内置指针,该指针指向与智能指针指向相同的对象。注意我们不能对该内置指针进行delete操作,也不能使用get得到指针初始化另一个智能指针,如:
5、可以对shared_ptr指针进行拷贝和赋值,这样可以使得多个指针指向同一个对象,如:
shared_ptr<int> p1 = make_shared<int>(10);
shared_ptr<int> p2(p1); //拷贝
shared_ptr<int> p3; //空指针
p3 = p1; //赋值,此时p1、p2、p3三个指针都指向同一个 对象。
那么shapre_ptr智能指针是如何实现管理动态内存的呢?
实际上shapre_ptr采用了我们之前进到过的引用计数的方法,在之前进到行为像指针的类时,也采用了引用计数的办法。我们可以认为每一个shapre_ptr都有一个关联的计数器用来记录指向同一个对象的智能指针的数量,当每拷贝一个shared_ptr时,计数器加会加一,单销毁一个shared_ptr时,计数器减一,当计数器为0时,shared_ptr类的析构函数就会执行内存释放的操作,因此不需要我们手动进行delete。引用计数个工作原理可以参考之前的文章(第十三章)
我们可以调用use_count函数查看智能指针中计数器的值,如:
shared_ptr<int> p1 = make_shared<int>(10);
shared_ptr<int> p2(p1);
shared_ptr<int> p3 = p1;
cout << p1.use_count() << endl; //输出为3,表示有3个智能指针指向同一个对象
三、unique_ptr
1、shared_ptr指针可以使多个指针共享同一个对象,而unique_ptr指针则与之相反,某一时刻只能有一个unique_ptr指向同一个对象。unique_ptr没有shared_ptr中的make_shared函数,与shared_ptr类似,unique_ptr只能绑定到new返回来的指针上,并且必须采用直接初始化,如:
unique_ptr<int> p1(new int(10));
//
int* q = new int(100);
unique_ptr<int> p2(q);//将q初始化p2后,,不能对q进行delete操作,内存管理权交给了p2;
2、由于unique_ptr允许同一时刻只能有一个unique_pre指向同一个对象,因此unique_ptr类不支持拷贝和赋值操作。(实际上在unique_ptr类内部将拷贝和赋值构造函数定义为=delete,即删除的),但是有一个例外就是,我们可以拷贝或者赋值一个将要销毁的unique_ptr,最常见的栗子就是从函数返回一个unique_ptr,如:
unique_ptr<int> p1(new int(10));
unique_ptr<int> p2=p1;//错误,不能赋值和拷贝
unique<int> func(int n){
return unque_ptr<int>(new int(n)); //正确,可以拷贝一个将要销毁的unique_ptr
};
3、虽然unique_ptr不能拷贝会在赋值,但是通过调用函数release或者reseat可以将指针的所有权从一个(非const)unique_ptr转到另一个unique_ptr。如:
unique_ptr<int> p1(new int(10));
unique_ptr<int> p2(p1.release()); //将指针P1的所有权交给p2,此时不能再使用p1
四、weak_ptr
1、weak_ptr指向shared_ptr管理的对象,创建weak_ptr时要用shared_ptr来初始化。将一个weak_ptr绑定到一个shared_ptr上,不会改变shared_ptr的计数器。如:
shared_ptr<int> p1 = make_shared<int>(10); //p1的计数器为1
weak_ptr<int> wp(p1); //将p1初始化wp,此时p1的计数器仍然为1
2、由于使用shared_ptr初始化一个weak_ptr不改变shared_ptr的计数器,因此weak_ptr所指向的对象可能被释放掉,因此不能直接使用weak_ptr访问对象,而是通过调用lock函数返回一个shared_ptr指针来间接访问对象,此时计数器会加1,如:
shared_ptr<int> p1 = make_shared<int>(10);
weak_ptr<int> wp(p1);
cout << p1.use_count() << endl; //输出为1
shared_ptr<int> p2 = wp.lock(); //p1的计数器加一
cout<< p1.use_count() << endl; //输出为2
cout << *p2 << endl; //输出为10
五、allocator类
使用new运算符通常是将内存分配和对象构造组合在一起,同样使用delete通常是把对象析构和内存释放组合在一起。如果我们希望将内存分配和对象构造分离、将对象析构和内存释放分离,就可以使用allocator类。allocator类允许我们先分配一块大的内存,然后可以在该内存内按需构造函数,allocator分配的内存是原始的,未构造的。智能指针类与allocator类都放在memory头文件中。
1、allocator类是一个模板类。由于allocator类将内存申请和对象构造分离。将对象析构和内存释放分离,因此当我们使用allocator类时,通常需要进行以下四步操作:
①、内存申请,使用函数allocate():
allocator<string> alloc; //定义一个allocator类对象
string* s = alloc.allocate(10); //调用allocate函数申请内存但不不进行对象构造,返回起始位置指针
②、对象构造,使用函数construct():
string* q= s;
alloc.construct(q++, "IG"); //调用construct函数构造对象,该函数需要传入一个指针和一个用于构造的参数
alloc.construct(q++, "RNG");
alloc.construct(q++, "EDG");
alloc.construct(q++, "FPX");
alloc.construct(q++, "WE");
③、对象析构,调用destroy()函数,注意我们只能对构造的元素进行destroy:
while (q != s)
alloc.destroy(--q);
④内存释放,调用deallocate()函数:
alloc.deallocate(s, 10); //函数第一个参数为分配内存的首地址,第二个参数为元素个数且必须与allocate函数中的参数一致。
看一下完整代码(中间插入了打印的代码):
#include<memory>
//其他头文件
int main()
{
allocator<string> alloc; //定义allocator类对象
string* s = alloc.allocate(10); //内存申请
string* q= s; //为了保持申请内存的首地址s,我们重新定义了一个指针
alloc.construct(q++, "IG"); //构造5个对象
alloc.construct(q++, "RNG");
alloc.construct(q++, "EDG");
alloc.construct(q++, "FPX");
alloc.construct(q++, "WE");
string* p = q;
while (p != s)
cout << *(--p) << endl; ////访问构造的对象
while (q != s)
alloc.destroy(--q); //对象析构
alloc.deallocate(s, 10); //内存释放
return 0;
}