• C/C++动态分配连续空间,下标越界导致的free():invalid next size问题


    昨天帮导师做的一个程序出了内存泄露的bug(在VS上程序运行一切正常,等return返回后才出错)

    而且是程序运行结束后才出现的错误,在退出前一切代码都顺利执行完了,只是return之后出错。

    之后我在Linux下重新编译运行程序,提示的信息更详细:

    free(): invalid next size (normal)

    然后下面显示Backtrace和Memory map等一大串错误信息。

    最终调试发现问题在于,读取数据格式不对,导致字符串转换后的int小于0,下标越界。我只检查了上限N,没检查下限0。

    那么问题来了,为什么动态分配的内存能访问下标为负的地方呢?来写几个程序测试下。

    #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]仍然可以访问,但是最终结束时会出错。

    再看看下面这份代码

    #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()配对的原因也理解了。每个内存分配器都有自己的申请和释放的策略,比如说记录申请的空间,我可以在一个字节的前几位记录,也可以在一个字节的后几位记录,如果申请和释放的规则不一致的话就会造成错误的后果。

    回顾之前我的两篇类似的博客

    再记录一次delete出错的经历  

    【free() invalid next size】谨慎地在C++的类中存储指针来方便访问其他节点

    第一篇,用cvLoadImage申请内存,却用delete释放内存,两者记录申请内存大小的策略不同,因此释放出错。

    第二篇,记录了vector之前的内部指针p,但是vector重新分配内存后内部指针变了,再访问p指向的位置就物是人非了。和我这次很像的是,之前那篇我自信满满地认为vector不会重新分配内存,即认为push_back的次数小于reserve预留的大小,这篇则是自信满满地认为下标肯定为非负数,因为之前的下标是用字符串转换而成的,比如"0a"对应的就是10,我认为肯定会不小于0,但是这些下标是从1开始的,所以我将字符串转换后的下标都减了1,这样的话错误的输入比如"00"在转换后就是-1,下标越界。

    总结下来,C/C++下标越界确实是个麻烦,有时候像这种“自信满满”的预测会导致运行错误,所以最佳的实践方式是写出便于调试的代码。

    1、尽可能使用STL容器,STL容器在下标越界时会在访问时就出错,不会让程序继续运行;

    2、使用RAII来让申请和释放配对;

    3、调试时若想获得更详细的信息,在所有需要用下标的位置都加上检查语句。

  • 相关阅读:
    python中列表,元组,字典常用操作方法的总结
    python中字符串常用方法总结
    tomcat运行报错Failed to start component [StandardEngine[Catalina].StandardHost[localhost].XXXX
    在Linux Ubuntu16.04中如何修改文件名
    【蓝桥杯训练】第五天1369
    【python3】raise,assert,nonlocal 关键字解读
    【python3】yield 关键字解读
    【蓝桥杯训练】第四天1294、1297
    【蓝桥杯训练】第四天1291、1293
    【蓝桥杯训练】第四天1285、1290
  • 原文地址:https://www.cnblogs.com/Harley-Quinn/p/7661962.html
Copyright © 2020-2023  润新知