4-1 C++内存管理与智能指针
1.C++内存管理
这是一个老生常谈的话题,内存管理是C++程序员应学习的基础能力,同时也是校招、社招面试中让候选人头疼的问题。掌握内存管理的C++程序员可以从中获得了更好的性能,更大的自由;但稍有不慎则会内存泄漏、core dump。因此,本篇文章从介绍C++内存管理入手,介绍C++程序员应该掌握的内存管理基础知识,进一步的介绍智能指针的概念以及shared_ptr的使用方法。
1.1 C++内存分配(内存分区)
如图所示:C++进程的内存空间被分为:
- 栈:用于维护函数调用的上下文空间,包括:程序临时创建的局部变量,也就是“{}”中定义的变量(不包括static声明的变量); 函数调用时,编译器会将调用处的运行状态压栈,再将调用函数的返回地址、调用函数的参数、调用函数定义的临时变量依次压栈;由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。栈内存分配运算置于处理器的指令集中,效率很高,但是分配的内存容量有限,可用ulimit -s查看。
- 堆:用于存放进程运行中被动态分配的内存段,C++程序中使用malloc申请的空间在堆上,因此它的大小并不固定,可动态扩张或缩减。进程调用malloc函数分配内存时,新分配的内存就被动态添加到堆上;进程调用free函数释放内存时,被释放的内存从堆中被剔除,若未调用free释放,则会导致内存泄漏;因此,堆空间的操作给了C++程序很大的自由。
- .bss段(Block Started by Symbol):用来存放程序中未初始化的全局变量、静态变量(局部+全局)以及所有被初始化为0的全局变量或静态变量。
- .data段:用来存放程序中已初始化的全局变量/静态变量的内存区域。
- .text段:用来存放程序执行代码的一块内存区域,该内存区域通常属于只读。text段也称为代码段,包括只读存储区和文本区,只读存储区存储一些常量(常量字符串和#define定义的常量,这些常量不可以被寻址),文本区存储程序的执行代码。
- mmap:存储动态链接库以及调用mmap方法进行内存映射的存储区域。
1.2 栈区与堆区的区别
至此,已经了解了C++进程的内存分区以及各个分区存放内容和性质。对于C/C++程序来说,栈区和堆区是程序函数调用和变量内存使用的主要区域,那么栈区和堆区有什么区别呢?
- 内存分配方式:栈由编译器自动进行动态分配与释放,是“先进后出”的连续内存存储结构,分配和回收效率较高。堆由malloc/delete系统调用进行动态分配与释放,由程序主动申请并主动释放,是非连续的存储结构,分配和回收效率较低,容易产生内存碎片。
- 内存分配响应:若栈剩余空间大于申请空间,则系统分配,否则抛栈溢出的异常;操作系统将堆空间划分为不连续的内存块,并按链式结构存储所有内存块,收到内存申请时遍历该链表,并返回第一个内存块空间大于所申请空间的堆结点,大于申请大小的多余部分将重新放入空闲链表。
- 内存生长方式:栈是向低地址扩展的数据结构,因为栈顶地址和栈的最大容量是预先设定好的,可获得的空间较为有限;堆是向高地址扩展的数据结构,因为链表的遍历方式是由低到高,堆的大小受限于系统中的虚拟内存。
1.3 new/delete与malloc/free的相同与异同
new/delete operator是C++内建的操作符,不能修改它的行为,new operator只做两件事:(1)调用operator new函数分配一块足够大、原始的、未命名的内存空间;(2) 调用对象的构造函数。 通常情况下,operator new以标准C的malloc()方法实现内存申请,operator delete比标准C的free()方法实现内存释放。但operator new/delete可以看做普通的函数,它可以根据不同的内存申请和释放策略被重载。 new/delete与malloc/free的不同之处可总结为以下几点:
- 参数:new申请内存分配时无需指定内存块的大小,而malloc需显式指定内存块大小。
void *operator new(size_t size);
void operator delete(void *p);
void *malloc(size_t __size);
void free(void *);
-
返回值类型:若分配成功,new返回对象类型的指针,malloc返回void*类型指针;若分配失败,new抛出异常,malloc返回NULL。
-
分配方式:new/delete operator调用参数类型的构造/析构函数;malloc/free不会。
new 操作符的执行过程:1. 调用operator new分配内存 ; 2. 调用构造函数生成类对象; 3. 返回相应类型指针。
- 属性和操作上:new/delete可以对其进行重载以获得更大的内存操作自由度;malloc/free是C语言的系统函数,其本质是调用brk/sbrk系统调用。 一个new/delete和malloc/free的代码示例: 案例中,定义一个类NowCoder,在构造函数和析构函数中打印相应的日志。在main函数中,分别使用new/detele和malloc/free去创建和删除NowCoder对象。
##1.4 重载operator new/delete
对于C++程序员来说,动态的申请和释放堆内存使得程序具有更大的内存自由度;但无论是使用new/delete,还是使用malloc/free,始终是操作系统在堆中寻找内存空间进行内存分配和释放;这也使得堆内存具有分配/回收效率低,存在内存碎片等缺点。C++程序员可以通过重载new/delete操作符,将内存申请和释放的操作由编写的程序控制,而非操作系统去管理,这也是C++内存池的基本思想。因此,本节介绍如何重载operator new/delete,内存池在下一节继续介绍。
如下述代码所示:首先,在类NowCoder中重载了operator new/delete,使其首先打印一句话,再调用malloc申请内存,并返回malloc申请的内存地址。由于类内部定义了operator new,那么在执行new operator时,会调用类内部定义的operator new,而不是全局的。如果重载了全局的operator new/delete,那么该进程在进行new/delete时都将调用程序员定义的operator new/delete, 这也就给予了C++程序员极大的内存管理自由度。
#include <iostream>
#include <string>
using namespace std;
class NowCoder
{
public:
NowCoder() {}
void* operator new(size_t size)
{
cout << "call NowCoder::operator new" << endl;
return malloc(size);
}
void operator delete(void* p)
{
cout << "call NowCoder::operator delete" << endl;
return free(p);
}
private:
int a;
};
int main()
{
NowCoder* example1 = new NowCoder();
delete example1;
}
运行结果为:
2.智能指针概述
内存泄漏:指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。对于堆内存来说,若C++程序员使用new/malloc申请了一段内存后,忘记使用delete/free进行内存释放,那么就会导致申请的内存不被该程序进程控制,从而导致堆内存泄漏。
为了保护堆内存,使得C++程序员避免因疏忽而忘记释放堆内存,C++标准库提供了一套智能指针用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象,这使得智能指针的表现与普通指针类似,并且当智能指针对象的生存周期结束后,会在智能指针的析构函数中释放掉该指针管理的内存,从而防止内存泄漏。
C++标准库提供了4个智能指针,分别是:auto_ptr,unique_ptr,shared_ptr,weak_ptr,其中后三个为C++11的新特性,并且auto_ptr在C11后被弃用。
2.1 unique_ptr
unique_ptr是独占式智能指针,实现了唯一拥有的语义,它保证一个实例对象同一时间只能有一个unique_ptr指向该对象 在C++11的标准库中定义了智能指针unique_ptr,该类是一个模板类,T指得是托管对象的类型,D指得是deleter类型,默认为default_deleter(在unique_ptr析构时调用)。
#include <memory>
#include <string>
template <class T, class D = default_delete<T>>
class unique_ptr;
unique_ptr<string> p1(new string("I am Evila.")); // p1是一个unique_ptr实例,它指向了一个string对象
unique_ptr<string> p2;
p2 = p1; // 不允许,尝试复制p1时在编译时报错
p1将托管对象所有权转移给p2不能进行简单的赋值,因为可能会留下悬挂的p1仍然指向托管对象,从而失去了独占的语义。C++标准库提供了std::move()函数,去转移unique_ptr的所有权。
unique_ptr<string> p1(new string("I am Evila.")); // p1是一个unique_ptr实例,它指向了一个string对象
unique_ptr<string> p2;
p2 = move(p1);
2.2 shared_ptr
shared_ptr是共享式智能指针,实现了共享式的管理概
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> C++工程师面试真题解析! </p> <p> 邀请头部大厂创作者<a href="https://www.nowcoder.com/profile/73627192" target="_blank">@Evila</a> 及牛客教研共同打磨 </p> <p> 助力程序员的求职! </p>