1、C++中的三种内存
C++中有三种内存,第一种“静态内存”用来保存局部static对象,类static数据成员以及任何定义在函数体之外的变量,例如:
int count_calls()
{
static int cnt=0;// 局部static变量
return ++cnt;
}
class A
{
static int a;// 类static成员
};
int A::a=1;
int num=10;// 定义在函数体之外的变量
第二种叫做“栈内存”,用来保存定义在函数内的非static变量,例如:
int return_zero()
{
int zero=0;// 定义在函数内的非static变量
return zero;
}
而第三种“自由空间”或者叫“堆”,则用来保存动态分配的对象,例如new出来的东西。
前两种内存中存放的对象,其生命周期是由编译器控制的,而堆中对象的生命周期是由程序员来控制的——程序员通过new得到指向某一对象的指针后,可以通过delete销毁该对象,并释放与之关联的内存。
2、内存泄漏、悬挂指针
对动态内存的管理不合理就会容易造成“内存泄漏”,即该收回的内存没有收回的情况。我们知道,一个new出来的东西最后一般要delete,就是为了防止内存泄漏。一种常见的情况是:
int* ptr=new int[100];
delete ptr;// 此时只是释放了内存空间,并没有将指针置为空,由此产生悬挂指针
写代码的人虽然有管理动态内存的观念,但仍然留下了一个漏洞:ptr“悬空”了,这就导致对ptr的访问可能会带来不可预期的结果,而这个漏洞在编译时根本无法发现。一种解决办法是,在delete ptr后给它赋上nullptr,但是,如果现在的代码是:
int* ptr1=new int[100];
int* ptr2=ptr1;
delete ptr1;
ptr1=nullptr;
ptr2仍然是悬挂指针,仍然有隐患。在实际应用中,把每个指向同一片内存的指针都找出来往往是困难的,可见,管理好动态内存并杜绝可能的漏洞并非易事。
3、智能指针
C++为了解决动态内存不好管理的问题,引入了智能指针,智能指针并不是严格意义上的指针,而是一个模板类,其行为和我们熟悉的指针一样。智能指针主要有两种:shared_ptr和unique_ptr,它们定义在头文件<memory>中。
(1)shared_ptr
shared_ptr允许多个指针指向同一个对象,这也是叫它“shared”的原因。作为一个模板类,shared_ptr的使用方法和其他模板类一样,例如:
shared_ptr<string> p1;// p1可以指向一个string
shared_ptr可以和new混用,也就是说,编译器允许显示地将一个普通指针转换为shared_ptr(注意隐式转换是不允许的),但是智能指针和普通指针混用并不是个好主意,标准库提供了一个叫做make_shared的函数,它会返回某个类型的shared_ptr,make_shared之于shared_ptr就如同new之于普通指针一样:
为了方便,当然可以配合auto使用,例如上面的第一行可以写成:
auto p3=make_shared<int>(42);
这样代码更加清晰、简洁。
每个shared_ptr都有一个引用计数,当每当有一个shared_ptr指向同一个对象,这个计数值就会加1,可以用成员函数use_count()查看这个计数。当给一个shared_ptr赋新值时或是一个shared_ptr被销毁时,这个计数值就会减1,若减为0,shared_ptr会调用析构函数销毁对象,同时释放关联的内存。而shared_ptr本身作为一个对象也会被析构掉,这就避免了前面说的悬挂指针的问题。下面代码展示了多个shared_ptr是如何共享对象的:
auto p1=make_shared<string>("c++ is the best language");
auto p2(p1);
auto p3(p1);
cout<<p1.use_count()<<" "
<<p2.use_count()<<" "
<<p3.use_count()<<endl;// 打印3 3 3
p3=make_shared<string>("no, php is the best");
cout<<p1.use_count()<<" "
<<p2.use_count()<<" "
<<p3.use_count()<<endl;// 打印2 2 1
(2)unique_ptr
只允许自己独占对象:
4、使用动态内存的动机:
C++ Primer中给出了三个原因:第一是预先不知道要用多少对象,二是不知道所需对象的准确类型,三是需要在多个对象间共享数据。第一种原因不难理解,例如vector就解决了这种问题,第二种原因这里不讨论(C++ Primer中在第15章有讲解),下面是第三种原因的一个例子:
vector<string> v1;
{
vector<string> v2={"a","aa","aaa"};
v1=v2;
}
用v2初始化v1后,v2里的数据不见了,加入我们希望:当两个对象共享底层数据时,若其中一个对象被销毁了,底层数据不被销毁,怎么办?答案是给这个类加上数据成员shared_ptr,书中有一个自定义的StrBlob类,在此不再赘述。
附:
下表是一些shared_ptr和unique_ptr的常用操作: