C++中内存错误通常属于运行时错误,只有在程序运行时才能发现,编译器无法自动检测到内存错误。多数情况下是程序逻辑或者参数存在某些错误。下面总结一下C++常见的内存错误:
1. 内存泄露
内存泄露是指应用程序未释放动态申请的且不再使用的内存,原因可能是程序员疏忽或者错误造成程序异常。
在C/C++中,动态申请的内存是在堆上的。内存管理器也不会自动回收不再使用的内存,也就是说如果忘记释放动态申请的内存,该内存区域是不允许重用的。如果发送此类的内存泄露,函数每执行一次就丢失一块内存。长时间运行该程序可能引起系统"内存耗尽"。
这个问题本身没有很好的解决思路。只能从编程习惯上入手,也就是说动态申请内存与释放内存必须匹配,亦即new和delete的调用次数必须相同。
2. 野指针
未初始化的指针称为野指针(另一种说法是指向不可用内存区域的指针,不过笔者认为前者更合适)。通常对野指针进行写操作会发生不可预知的错误。
通常的避免方法就是在指针定义的时候就初始化,初始为NULL或者一个有意义的内存地址。对于动态申请的内存地址,在该内存释放之后,对应指针最好立即赋值为NULL。并在具体使用指针的时候判断指针的值是否有效(通常检测是否为NULL)。
3. 内存越界访问
内存越界访问通常发生在数组、字符串或者连续内存的访问。有两种情况:
读越界,即读了非有效的数据。如果所读的内存地址是无效的,程序会立即崩溃。如果所读内存地址是有效的,读入的时候不会有错误,但是读入的数据是随机的,可能会产生不可控制的后果。举个简单的例子,字符串的输出,如果没有结束符,会输出一堆乱码也可能输出正常,也就是说结果是不可控的。
写越界,亦称为缓冲区溢出,通常写越界会发生错误。内存写越界造成的后果是非常严重的。例如访问数组越界可能会修改访问数组的循环变量,造成死循环。另一个比较经典的例子就是缓冲区溢出攻击,试试上就是利用越界修改程序的栈空间,达到控制操作系统或者执行某些特定任务的目的。
这类问题几乎没有很有效的解决思路,只能由程序员控制好内存的访问,小心处理各种内存有关的操作。
4. 返回指向临时变量的指针
最经典的例子是一道面试题中关于字符串指针的返回函数,代码如下:
char * getString(){char b[] = "Hello, Tocy!"; return b;}
栈里面的变量都是临时的,函数执行完成之后,相关的临时变量和参数都会被清除。这也是程序不允许返回指向这些临时变量的指针的原因,因为函数执行结束后这些指针指向的数据是随机的,给程序造成不可预知的后果。
通常此类错误编译器会给出警告。解决思路很简单,在任何情况下不要返回函数的局部变量的任何指针,如果需要传递参数可以考虑使用返回值、参数或者全局变量(不推荐)。
5. 试图修改常量
在普通变量前面加上const修饰符,只是给编译器做类型检查用的。编译器禁止修改这样的变量,但这并不是强制的,完全可以用强制类型转换来处理,一般不会出错。例如下面代码:
int func(void){ const int IMAX = 3; int * pi = (int *)(&IMAX); *pi = 4; cout << IMAX << endl;}
笔者在vc6和vs2008下运行该函数,输出都是3,编译运行没有任何错误和警告。至于有没有修改常量的值,有兴趣的读者可以自己看看反汇编的代码,相信你就会明白是否修改了常量的值(最终结果是修改了,只是编译器做了某些优化所以输出依然不变。)。
而对于全局常量和字符串使用强制类型转来处理在运行时仍然会出错,这是因为它们是存放在只读内存区("rodata"),只读内存区是不允许修改的。试图对其修改,会引发内存错误。
所以针对这种类型错误,笔者建议最好不要修改常量,除非万不得已。
6. 内存未分配成功,但已经使用
通常是由于程序员认为动态内存分配不会失败。解决思路很简单,在使用动态申请的内存时,首先检测指针是否为NULL(通常动态内存分配失败会返回空指针)。
7. 内存分配成功,但没有初始化
犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
当然发生这种情况还有另外一个可能就是在动态创建类对象时类构造函数抛出异常(即申请动态内存成功,但是调用构造函数失败)。
另外如果出现下图
基本可以断定是内存问题,极有可能是指针异常引起的。当然还有其他原因,需要视具体情况而定。