本文的内容将专门对付内存管理,培养起有借有还的好习惯,方可消除资源管理的问题。程序员
所谓的资源就是,一旦用了它,未来必须还给系统。若是不是这样,糟糕的事情就会发生。数据库
C++ 程序内常见的资源:数组
不管哪种资源,重要的是,当你再也不使用它时,必须将它还给系统,有借有仍是个好习惯。安全
把资源放在析构函数,交给析构函数释放资源服务器
假设某个 class 含有个工厂函数,该函数获取了对象的指针:网络
A* createA(); // 返回指针,指向的是动态分配对象。 // 调用者有责任删除它。
如上述注释所言,createA 的调用端使用了函数返回的对象后,有责任删除它。如今考虑有个f
函数履行了这个责任:app
void f() { A *pa = createA(); // 调用工厂函数 ... // 其余代码 delete pa; // 释放资源 }
这看起来稳妥,但存在若干状况f
函数可能没法执行到delete pa
语句,也就会形成资源泄漏,例如以下状况:socket
固然能够经过谨慎地编写程序能够防止这一类错误,但你必须想一想,代码可能会在时间渐渐过去后被修改,若是是一个新手没有注意这一类状况,那必然又会再次有内存泄漏的可能性。函数
为确保 A 返回的资源都是被回收,咱们须要将资源放进对象内,当对象离开做用域时,该对象的析构函数会自动释放资源。布局
「智能指针」是个好帮手,交给它去管理指针对象。
对因而由动态分配(new)于堆内存的对象,指针对象离开了做用域并不会自动调用析构函数(需手动delete),为了让指针对象能像普通对象同样,离开做用域自动调用析构函数回收资源,咱们须要借助「智能指针」的特性。
经常使用的「智能指针」有以下三个:
下面示范如何使用 std::auto_ptr 以免 f
函数潜在的资源泄漏可能性:
void f() { std::auto_ptr<A> pa (createA()); // 调用工厂函数 ... // 一如既往的使用pa } // 离开做用域后,经由 auto_ptr 的析构函数自动删除pa;
这个简单的例子示范「以对象管理资源」的两个关键想法:
为何在 C++11 建议弃用 auto_ptr 吗?固然是 auto_ptr 存在缺陷,因此后续不被建议使用。
auto_ptr 有一个不寻常的特质:若经过「复制构造函数或赋值操做符函数」 copy 它们,它们会变成 null ,而复制所得的指针将获取资源的惟一拥有权!
见以下例子说明:
std::auto_ptr<A> pa1(createA()); // pa1 指向 createA 返回物 std::auto_ptr<A> pa2(pa1); // 如今 pa2 指向对象,pa1将被设置为 null pa1 = pa2; // 如今 pa1 指向对象,pa2 将被设置为 null
这一诡异的复制行为,若是再次使用指向为 null 的指针,那必然会致使程序奔溃。
意味着 auto_ptr 并不是管理动态分配资源的神兵利器。
unique_ptr 也采用全部权模型,可是在使用时,是直接禁止经过复制构造函数或赋值操做符函数 copy 指针对象,以下例子在编译时,会出错:
std::unique_ptr<A> pa1(createA()); // pa1 指向 createA 返回物 std::unique_ptr<A> pa2(pa1); // 编译出错! pa1 = pa2; // 编译出错!
shared_ptr 在使用复制构造函数或赋值操做符函数后,引用计会数累加而且两个指针对象指向的都是同一个块内存,这就与 unique_ptr、auto_ptr 不一样之处。
void f() { std::shared_ptr<A> pa1(createA()); // pa1 指向 createA 返回物 std::shared_ptr<A> pa2(pa1); // 引用计数+1,pa2和pa1指向同一个内存 pa1 = pa2; // 引用计数+1,pa2和pa1指向同一个内存 }
当一个对象离开做用域,shared_ptr 会把引用计数值 -1 ,直到引用计数值为 0 时,才会进行删除对象。
因为 shared_ptr 释放空间时会事先要判断引用计数值的大小,所以不会出现屡次删除一个对象的错误。
小结 - 请记住
- 为防止资源泄漏,请使用 RAII(Resource Acquisition Is Initaliaztion - 资源取得时机即是初始化时机) 对象,它们在构造函数中获取资源,并在析构函数中是释放资源
- 两个建议使用的 RAII classes 分别是 std::unique_ptr 和 std::shared_ptr。前者不容许 copy 动做,后者容许 copy 动做。可是不建议用 std::auto_ptr,若选 auto_ptr,复制动做会使它(被复制物)指向 null 。
假设,咱们使用 C 语音的 API 函数处理类型为 Mutex 的互斥对象,共有 lock 和 unlock 两函数可用:
void locak(Mutex *pm); // 锁定 pm 所指的互斥器 void unlock(Mutex* pm); // 将互斥器解除锁定
为确保毫不会忘记一个被锁住的 Mutex 解锁,咱们可能会但愿创立一个 class 来管理锁资源。这样的 class 要遵照 RAII 守则,也就是「资源在构造期间得到,在析构释放期间释放」:
class Lock { public: explicit Lock(Mutex *pm) // 构造函数 : pMutex(pm) { lock(pMutex); } ~Lock() // 析构函数 { unlock(pMutex); } private: Mutex* pMutex; };
这样定义的 Lock,用法符合 RAII 方式:
Mutex m; //定义你须要的互斥锁 ... { // 创建一个局部区块做用域 Lock m1(&m); // 锁定互斥器 ... } // 在离开区块做用域,自动解除互斥器锁定
这很好,但若是 Lock 对象被复制,会发生什么事情?
Lock m1(&m); // 锁定m Lock m2(&m1); // 将 m1 复制到 m2身上,这会发生什么?
这是咱们须要思考和面对的:「当一个 RAII 对象被复制,会发生什么事情?」大多数时候你会选择如下两种可能:
若是前述的 Lock 打算使用使用引用计数法,它可使用 std::shared_ptr 来管理 pMutex 指针,而后很不幸 std::shared_ptr 的默认行为是「当引用次数为 0 时删除其所指物」那不是咱们想要的行为,由于要对 Mutex 释放动做是解锁而非删除。
幸运的是 std::shared_ptr 容许指定自定义的删除方式,那是一个函数或函数对象。以下:
class Lock { public: explicit Lock(Mutex *pm) : pMutex(pm, unlock) // 以某个 Mutex 初始化 shared_ptr, // 并以 unlock 函数为删除器。 { lock(pMutex.get()); // get 获取指针地址 } private: std::shared_ptr<Mutex> pMutex; // 使用 shared_ptr };
请注意,本例的 Lock class 再也不声明析构函数。由于编译器会自动创立默认的析构函数,来自动调用其 non-static 成员变量(本例为 pMutex )的析构函数。
而 pMutex 的析构函数会在互斥器的引用次数为 0 时,自动调用 std::shared_ptr 的删除器(本例为 unlock )。
小结 - 请记住
- 复制 RAII 对象必须一并复制它的所管理的资源(深拷贝),因此资源的 copying 行为决定 RAII 对象的 copying 行为。
- 普通而常见的 RAII class copying 行为是:禁止 copying、施行引用计数法。
智能指针「显式」转换,也就是经过 get 成员函数的方式转换为原始指针对象。
上面提到的「智能指针」分别是:std::auto_ptr、std::unique_ptr、std::shared_ptr。它们都有访问原始资源的办法,都提供了一个 get 成员函数,用来执行显式转换,也就是它会返回智能指针内部的原始指针(的复件)。
举个例子,使用智能指针如 std::shared_ptr 保存 createA() 返回的指针对象 :
std::shared_ptr<A> pA(createA());
假设你但愿以某个函数处理 A 对象,像这样:
int getInfo(const A* pA);
你想这么调用它:
std::shared_ptr<A> pA(createA()); getInfo(pA); // 错误!!
会编译错误,由于 getInfo 须要的是 A
指针对象,而不是类型为 std::shared_ptr<A>
的对象。
这时候就须要用 std::shared_ptr 智能指针提供的 get
成员函数访问原始的资源:
std::shared_ptr<A> pA(createA()); getInfo(pA.get()); // 很好,将 pA 内的原始指针传递给 getInfo
智能指针「隐式」转换的方式,是经过指针取值操做符。
智能指针都重载了指针取值操做符(operator->和operator*),它们容许隐式转换至底部原始指针:
class A { public: bool isExist() const; ... }; A* createA(); // 工厂函数,建立指针对象 std::shared_ptr<A> pA(createA()); // 令 shared_ptr 管理对象资源 bool exist = pA->isExist(); // 经由 operator-> 访问资源 bool exist2 = (*pA).isExist(); // 经由 operator* 访问资源
多数设计良好的 classes 同样,它隐藏了程序员不须要看到的部分,可是有程序员须要的全部东西。
因此对于自身设计 RAII classes 咱们也要提供一个「取得其所管理的资源」的办法。
小结 - 请记住
- APIs 每每要求访问原始资源,因此每个 RAII class 应该提供一个「取得其所管理的资源」的办法。
- 对原始资源的访问可能经由显式转换或隐式转换。通常而言显式转换比较安全,但隐式转换比较方便。
如下动做有什么错?
std::string* strArray = new std::string[100]; ... delete strArray;
每件事情看起来都井井有理。使用了 new,也搭配了对应的 delete。但仍是有某样东西彻底错误。strArray 所含的 100 个 string 对象中的 99 个不太可能被适当删除,由于它们的析构函数极可能没有被调用。
当使用 new ,有两件事发生:
当使用 delete,也会有两件事情:
delete 的最大问题在于:即将被删除的内存以内究竟有多少对象?这个答案决定了须要执行多少个析构函数。
对象数组所用的内存一般还包括「数组大小」的记录,以便 delete 知道须要调用多少次析构函数。单一对象的内存则没有这笔记录。你能够把二者不一样的内存布局想象以下,其中 n 是数组大小:
当你对着一个指针使用 delete,惟一可以让 delete 知道内存中是否存在一个「数组大小记录」的办法就是:由你告诉它。若是你使用 delete 时加上中括号[],delete 便认定指针指向一个数组,不然它便认定指针指向一个单一对象:
std::string* strArray = new std::string[100]; std::string* strPtr = new std::strin; ... delete [] strArray; // 删除一个对象 delete strPtr; // 删除一个由对象组成的数组
游戏规则很简单:
小结 - 请记住
- 若是你在 new 表达式中使用[],必须在相应的 delete 表达式也使用[]。若是你在 new 表达式中不使用[],必定不要在相应的 delete 表达式使用[]。
假设咱们有个如下示范的函数:
int getNum(); void fun(std::shared_ptr<A> pA, int num);
如今考虑调用 fun:
fun(new A(), getNum());
它不能经过编译,由于 std::shared_ptr
构造函数须要一个原始指针,并且该构造函数是个 explicit
构造函数,没法进行隐式转换。若是写成这样就能够编译经过:
fun(std::shared_ptr<A>(new A), getNum());
使人想不到吧,上述调用却可能泄露资源。接下来咱们来一步一步的分析为何存在内存泄漏的可能性。
在进入 fun
函数以前,确定会先执行各个实参。上述第二个实参只是单纯的对 getNum
函数的调用,但第一个实参 std::shared_ptr<A>(new A)
由两部分组成:
new A
表达式std::shared_ptr
构造函数因而在调用 fun
函数以前,先必须作如下三件事:
getNum
函数new A
表达式std::shared_ptr
构造函数那么他们的执行次序是必定如上述那样的吗?能够肯定的是 new A
必定比 std::shared_ptr
构造函数先被执行。但对 getNum 调用能够排在第一或第二或第三执行。
若是编译器选择以第二顺位执行它:
new A
表达式getNum
函数std::shared_ptr
构造函数getNum
函数发生了异常,会发生什么事情?在此状况下 new A
返回的指针将不会置入 std::shared_ptr
智能指针里,就存在内存泄漏的现象。避免这类问题的办法很简单:使用分离语句。
分别写出:
fun
函数。std::shared_ptr<A> pA(new A); // 先构造智能指针对象 fun(pA, getNum()); // 这个调用动做毫不至于形成泄漏。
以上的方式,就能避免本来因为次序致使内存泄漏发生。
小结 - 请记住
- 以独立语句将 newed (已 new 过) 对象存储于智能指针内。若是不这样作,一旦异常被抛出,有可能致使难以察觉的资源泄漏。
本文部份内容参考了《Effective C++ (第3版本)》第三章节内容,前两章节的内容可看旧文
《学过 C++ 的你,不得不知的这 10 条细节!》
关注公众号,后台回复「我要学习」,便可免费获取精心整理「服务器 Linux C/C++ 」成长路程(书籍资料 + 思惟导图)