new和delete操作一定要谨慎再谨慎

  今天调试程序,本来一直运行的好好的,由于换了一批数据,在运行过程中突然崩溃,并弹出如下崩溃信息

heap_error

  出错的函数代码如下:

01 inline T* _resize(T *ptr, size_t size, size_t new_size, T val)
02 {
03     T *tmp = new T[new_size];
04     for (register size_t i = 0; i < size; ++i)
05     {
06         tmp[i] = ptr[i];
07     }
08     for (register size_t i = size; i < new_size; ++i)
09     {
10         tmp[i] = val;
11     }
12     delete [] ptr;
13     return tmp;
14 }

  我百思不得其解,从来写程序没遇到这种问题,不停的调试,整个流程都没看出什么问题,后来才发现,由于其他代码的逻辑不健全,导致调用该函数时,size 不为0,而new_size 的值为0,因此出现了出错的代码情景

1 T *tmp = new T[new_size];

  从而导致运行到下面代码时,便出现堆崩溃问题

1 delete [] ptr;

  问题就在于new_size为0是,出现

1 T *tmp = new T[0];

  以上恶劣的行为导致的后果就是堆上内存没有申请不成功,为了防止 tmp 指向下一相临的内存,系统会让 tmp 指向一个最小的内存,也就是1个字节,也就是说,当申请字节数为0时,编译器把它置为1,申请一个字节,但是这个字节是不能用的。同时由于对这个不合法的地址数据进行了赋值,tmp[i] = ptr[i],于是 delete 一个不存在的内存块地址就会让系统产生崩溃。

  因此,总结教训,对于在编程中经常遇到 int *p = new int[n]; 这种代码时,尽管我们对 n 的情况心里比较有数,但是为了程序鲁棒性,需要在申请内存前确认 n > 0,或者添加异常捕捉代码,以防止程序崩溃还要调试好久。

new和delete操作一定要谨慎再谨慎》上有4条评论

  1. 我觉得不是new和delete的原因。直接写下面的代码没问题:
    int main(int argc, char* argv[])
    {
    int* p = new int[0];
    delete [] p;
    p = NULL;
    return 0;
    }

    我觉得上面代码的问题在于没有判断size和new_size的大小关系,当new_size < size的时候,第6行赋值tmp[i] = ptr[i]; 就越界了。

    BTW: (1) 尽量不用new和delete,STL的容器已经设计挺好的了,加上boost之类的库提供的功能,基本new和delete可以消失了; (2) 特殊情况检查,像这个函数,我会在入口的时候判断如果ptr是NULL的话要怎么处理(assert(ptr)),还有像你说的当new_size==0的时候应该怎么处理; (3) 数组赋值之前最好加上assert(index < size)这样的语句; (4) 指针delete之后最好马上赋值为NULL,即使后面没有代码了,因为可能将来修改代码的时候可能自己都忘记那个指针是已经被delete掉的; (5) 另外那两个循环也是不需要的,可以分别用copy和fill来替代。

    • 师兄的建议很好,有些我也这么做的,有些还需要学习,有时可能为了效率或者内存考虑,会避免一些STL,所以管理起内存比较麻烦,另外这个问题确实不是
      int* p = new int[0];
      delete [] p;
      p = NULL;
      就出错的,出错的原因在于在我的函数里面对p进行赋值了,比如p[0] =1,从而产生了这个问题

  2. 我的意思是说这个错误在逻辑上其实还是没判断 new_size 和 size的大小造成的,错误检查应该使得程序在错误发生的地方就断掉,而并非遗留到后面出错。那个new的问题,并非是new[0]的问题,而是越界的问题,类似于 int* p = new int[10]; 然后写一个 p[100] = 0; ,呵呵。

    另外stl一般会比手写循环快,比如像这里用copy函数,copy函数有可能会实例化一个copy或针对random iterator实例化一个特殊的版本,然后针对它们进行优化(例如用memcpy实现而不用循环),如果我们手写循环就丧失了这个可能进行优化的机会。所以尽量用库函数代替手写循环,除非你确定知道底层库的实现有什么样的问题,或者自己的代码有什么特殊之处底层库无法满足。

发表评论

电子邮件地址不会被公开。 必填项已用*标注