内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++程序中都会发生,因此要想成为C++高手,内存管理一关是必须要过的,除非放弃C++,转到Java或者.NET,他们的内存管理基本是自动的,当然你也放弃了自由和对内存的支配权,还放弃了C++超绝的性能。
内存分配方式
在C++中,内存分为内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
(1)堆
就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
(2)栈
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3)自由存储区
就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
(4)全局/静态存储区
全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
(5)常量存储区
这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
1. 堆和栈的区别
(1)管理方式
对于栈来讲,是由编译器自动管理,无需我们手工控制;
对于堆来说,释放工作由程序员控制,容易产生memory leak;
(2)空间大小
一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的;
但是对于栈来讲,一般都是有一定的空间大小的,我们可以修改;
(3)生长方向
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;
对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长;
(4)分配方式
堆都是动态分配的,没有静态分配的堆;
栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。
动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
(5)分配效率
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高;
堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低
(6)能否产生碎片
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低;
对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列;
控制C++内存分配
在嵌入式系统中使用C++的一个常见问题是内存分配,即对new 和 delete 操作符的失控。
具有讽刺意味的是,问题的根源却是C++对内存的管理非常的容易而且安全。具体地说,当一个对象被消除时,它的析构函数能够安全的释放所分配的内存。
这当然是个好事情,但是这种使用的简单性使得程序员们过度使用new 和 delete,而不注意在嵌入式C++环境中的因果关系。并且,在嵌入式系统中,由于内存的限制,频繁的动态分配不定大小的内存会引起很大的问题以及堆破碎的风险。
作为忠告,保守的使用内存分配是嵌入式环境中的第一原则。
但当你必须要使用new 和delete时,你不得不控制C++中的内存分配。你需要用一个全局的new 和delete来代替系统的内存分配符,并且一个类一个类的重载new 和delete。
一个防止堆破碎的通用方法是从不同固定大小的内存持中分配不同类型的对象。对每个类重载new 和delete就提供了这样的控制。
1. 为单个类重载new和delete操作符
class TestClass { public: TestClass() { cout << "TestClass()" << endl; } ~TestClass() { cout << "~TestClass()" << endl; } void * operator new(size_t size); void operator delete(void *p); }; void *TestClass::operator new(size_t size) { cout << "operator new" << endl; void *p = malloc(size); return (p); } void TestClass::operator delete(void *p) { cout << "operator delete" << endl; free(p); } void main() { TestClass *pTest = new TestClass; delete pTest; pTest = NULL; }
2. 为单个类重载new[]和delete[]操作符
class TestClass { public: TestClass() { cout << "TestClass()" << endl; } ~TestClass() { cout << "~TestClass()" << endl; } void * operator new[](size_t size); void operator delete[](void *p); }; void *TestClass::operator new[](size_t size) { cout << "operator new" << endl; void *p = malloc(size); return (p); } void TestClass::operator delete[](void *p) { cout << "operator delete" << endl; free(p); } void main() { TestClass *pTest = new TestClass[5]; delete[] pTest; pTest = NULL; return; }
特别特别要注意size_t nSize的大小!
C++new和delete实现原理:https://blog.csdn.net/passion_wu128/article/details/38966581
常见的内存错误及其对策
1. 常见内存错误
(1) 内存分配未成功,却使用了它
(2)内存分配虽然成功,但是尚未初始化就引用它
(3)内存分配成功并且已经初始化,但操作越过了内存的边界
(4)忘记了释放内存,造成内存泄露
(5) 释放了内存却继续使用它
2. 对策
(1)用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存
(2)不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用
(3)避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作
(4)动态内存的申请与释放必须配对,防止内存泄漏
(5)用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”