作为C/C++
的开发者,内存泄漏问题大概是最不愿意碰到,但是却不得不去面对的一个问题。自从C/C++
诞生的那一天起,内存和指针就好比天上的两朵乌云,笼罩在无数C/C++
开发者的头顶,如跗骨之蛆,挥之不去。它一方面以其无与伦比的高效率以及便捷性让人可以在浩如烟海的硬件地址之间纵横驰骋,如入无人之境;另一方面却又像一个娇滴滴的小姑娘,稍不顺心遂意,就会给你致命的下马威。就如同你永远猜不透女朋友为什么会生气一样,你永远也想不到在什么地方存在着看不见摸不着的内存泄漏问题。
高收益必然伴随着高风险,古人诚不欺我。
那么,什么是内存泄漏呢?
所谓内存泄漏,就是由于程序主动申请了内存,但是又没有主动释放它,那么这块内存可能就会一直驻留在进程中,只要进程不结束,内存就一直不释放。如果这是一个长期运行的server
或者daemon
类的守护进程,一旦发生内存泄漏,那么后果是不可设想,因为在长时间的运行过程中,进程一点点蚕食系统内存,最终可能导致系统内存耗尽,从而导致系统崩溃。
内存泄漏问题为什么会存在呢?
对于一个进程来说,其内存占用可根据其地址范围及生命周期分为四个区域,这就是大名鼎鼎的内存四区。分别为代码区、数据区、堆区和栈区。
其中代码区主要存放二进制代码,由操作系统进行管理;数据区主要存放全局变量、静态变量以及常量,这些变量 或常量会贯穿整个进程生命周期,在程序结束后会由操作系统进行释放。栈区的空间是由编译器自动分配和释放的 ,其生命周期一般以大括号{}
作为分界点,比如常见的局部变量,函数参数等,大括号结束,变量空间也就自动释放了。以上三区的内存基本上都不会造成内存泄漏。
而真正会造成内存泄漏的罪魁祸首只有堆区内存。堆区的内存有个特点,就是必须由程序员手动去申请以及释放。比如常见的malloc
、calloc
、realloc
函数用来在堆上申请内存,free
函数用来释放内存。C++
中也用new
/delete
运算符来管理申请和释放内存。通常来说,有申请就必须要有释放,也就是说,malloc/free
、new/delete
必然是成对出现的,一旦发生了不匹配 ,那么就极有可能存在内存泄漏问题。
void foo(){ int *p = (void*)malloc(sizeof(int)); //do something with pointer p free(p); p = NULL; }
比如上面就是一段非常简单的在堆上分配内存的例子,我们在第2行使用malloc
函数在堆上申请了一个大小为 sizeof(int)
的内存块,在第4
行对这块内存进行了释放。
如果我们把第4行注释掉,代码如下所示:
void foo(){ int *p = (void*)malloc(sizeof(int)); //do something with pointer p //free(p); p = NULL; }
当执行完 p = NULL
语句后,原来申请的那一块内存也就没有指针可以指向它了,但是这块内存又还没有释放,这样就会导致这块内存再也无法释放掉,除非整个进程停止。如果这样的代码在整个程序中有很多,那么也就会有很多内存申请后指针丢失,无法释放,导致内存占用越来越大,从而形成很严重的内存泄露问题。
那么,有人问,既然堆区的内存管理这么麻烦,而栈区的内存可以由编译器自动分配和回收。我们直接用栈区的内存不就行了吗?干嘛自讨苦吃,去招惹堆区的魔鬼呢?
事实上,每个进程所能使用栈空间是极其有限的。在Linux
系统上,我们可以通过ulimit -s
命令查看可支配的stack
空间大小,这个大小一般只有几兆,甚至kb
级别。对于一个大型的应用程序来说,这点可怜的栈区内存未免显得捉襟见肘。只有对那些占用内存比较小的变量,对执行效率要求比较高的地方,可能直接使用栈上的内容,对于一些比较占用内存的结构,比如结构体、字符串等,通常的做法,都是在堆上申请内存。
这也就意味着,只要我们使用C/C++
进行编程,内存泄漏的问题就是我们不得不去面对并要解决的问题。
本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对C/C++课程感兴趣的读者,可以点击链接,查看详细的服务:C/C++Linux服务器开发/高级架构师