昨天帮导师作的一个程序出了内存泄露的bug(在VS上程序运行一切正常,等return返回后才出错)html
并且是程序运行结束后才出现的错误,在退出前一切代码都顺利执行完了,只是return以后出错。java
以后我在Linux下从新编译运行程序,提示的信息更详细:ios
free(): invalid next size (normal)函数
而后下面显示Backtrace和Memory map等一大串错误信息。post
最终调试发现问题在于,读取数据格式不对,致使字符串转换后的int小于0,下标越界。我只检查了上限N,没检查下限0。测试
那么问题来了,为何动态分配的内存能访问下标为负的地方呢?来写几个程序测试下。ui
#include <iostream> using namespace std; int main() { const int N = 5; int* p = new int[N]; p[-1] = 1; p[-2] = 3; for (int i = -4; i < 0; i++) { cout << p[i] << " "; } cout << endl; delete[] p; return 0; }
0 0 3 1 *** Error in `./a.out': munmap_chunk(): invalid pointer: 0x0000000000ed6c20 ***
能够发如今n<0时,p[n]仍然能够访问,可是最终结束时会出错。url
再看看下面这份代码spa
#include <iostream> using namespace std; int main() { const int N = 5; int* p = new int[N]; p[N] = 100; cout << p[N] << endl; delete[] p; return 0; }
运行结果是100,而且没任何问题。指针
也就是说,C/C++能够访问显式申请的内存以外的内存空间,它们多是库函数隐式申请的,好比之因此上面一份代码正常运行,可是异常退出,下面一份代码正常运行、正常退出。缘由是,new(会调用内置的allocator)动态申请一片内存时,会在返回的指针p以前记录下申请的内存大小,这样以后用delete释放new申请的内存时会隐式查找记录的内存大小,从而知道该释放多少内存。因此才能够用delete[]而不是delete[N]。
同理,使用malloc()时也会在返回的指针以前的某个地址记录申请内存大小,这样free()就会在释放内存时找到这个记录分配大小的地址,而后知道释放多少。
C/C++不会像java同样在编译层面检查下标是否越界,因此若是不在代码里手动检查,下标越界可能会致使库函数须要用到的内存地址被咱们误修改,从而使库函数出错。
明白了这一点后,new和delete配对,new[]和delete[]配对,malloc()和free()配对的缘由也理解了。每一个内存分配器都有本身的申请和释放的策略,好比说记录申请的空间,我能够在一个字节的前几位记录,也能够在一个字节的后几位记录,若是申请和释放的规则不一致的话就会形成错误的后果。
回顾以前个人两篇相似的博客
【free() invalid next size】谨慎地在C++的类中存储指针来方便访问其余节点
第一篇,用cvLoadImage申请内存,却用delete释放内存,二者记录申请内存大小的策略不一样,所以释放出错。
第二篇,记录了vector以前的内部指针p,可是vector从新分配内存后内部指针变了,再访问p指向的位置就物是人非了。和我此次很像的是,以前那篇我自信满满地认为vector不会从新分配内存,即认为push_back的次数小于reserve预留的大小,这篇则是自信满满地认为下标确定为非负数,由于以前的下标是用字符串转换而成的,好比"0a"对应的就是10,我认为确定会不小于0,可是这些下标是从1开始的,因此我将字符串转换后的下标都减了1,这样的话错误的输入好比"00"在转换后就是-1,下标越界。
总结下来,C/C++下标越界确实是个麻烦,有时候像这种“自信满满”的预测会致使运行错误,因此最佳的实践方式是写出便于调试的代码。
一、尽量使用STL容器,STL容器在下标越界时会在访问时就出错,不会让程序继续运行;
二、使用RAII来让申请和释放配对;
三、调试时若想得到更详细的信息,在全部须要用下标的位置都加上检查语句。