一、内存分配方式
1、程序运行前(编译后)
代码区 存放CPU执行的机器指令,即编写的所有代码
代码区是共享的,其原因是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,放置程序意外第修改了它的指令
全局区(包含常量存储区、静态存储区)
(1) 静态存储区:存放静态变量(static)、全局变量
(2)常量存储区:字符串常量、const修饰的全局常量
2、程序运行后
栈区 由编译器自动分配,存放函数的参数值、局部变量等;函数结束时,系统自动回收。
栈内存分配运算内置于处理器的指令集中,效率很高,但分配的内存空间有限。
堆区 由程序员分配释放,程序员不释放,程序结束时由操作系统回收。
C++中主要利用new在堆区开辟内存(该块内存又称为自由存储区)
堆和栈的区别参考:c++ 内存管理_AITBOOK-CSDN博客
二、C++中可能出现的内存问题及解决办法
1 缓冲区溢出
由程序员自己编写Buffer类来管理缓冲区,自动记住使用缓冲区的大小,通过成员函数(不是裸指针)来修改缓冲区。
2 野指针
野指针:指针变量指向非法的内存空间,如:使用没有申请的内存,或者已经释放的内存
解决办法:(1)在使用内存前,检查指针是否为空指针NULL
(2)一个指针指向一个对象,当对象销毁时,该指针应该自动置为NULL,而不需要 对象通知重置。C++11提供shared_ptr/weak_ptr(下面内存泄漏部分详细阐述)可以解 决,我们在编写程序时,尽量使用智能指针,避免野指针。
空指针:定义 int *p = NULL 表示指针变量指向内存中编号为0的空间
空指针指向的内存是不可访问的,0~255号内存被系统占用
NULL本质是宏#define NULL 0,这里的0既可以表示整型常量,又可以表示指针常量。
这带 来一些问题,例如:无法区分void fun(int a) 与void func(int* a)。
C++11 中提供nullptr表示空指针,nullptr是指针类型,不能转换成整型类型,建议使用
3 重复释放
C++中一个指针被重复delete,编译时不会报错,在运行时才报错。
浅拷贝:一个类中有属性m_A(指针数据)开辟在堆区,对象P2通过对象P1进行拷贝构造创建,P1和P2的属性的属性指向同一块内存。在函数运行结束后,P1和P2都调用析构函数释放时(释放顺序:先进后出),先释放P2(此时堆区内存已经释放),再释放P1(堆区内存又释放一次)
可以利用深拷贝来解决浅拷贝带来的问题,我们只需重载拷贝构造函数,通过P1构造P2时,在堆区重新开辟空间,放P2的属性m_A。
重载拷贝构造时需要注意的问题:形参一般是常量引用,如 const Person &p
设置为常量,防止出错,只读 (拷贝操作,值必然不能改变)
必须设置为引用, 否则传参时,自动调用拷贝构造,拷贝一个临时副本,导致拷贝构造函数无限的递归,最终使栈溢出。
4 内存泄漏
未能及时释放不再使用的内存(借东西不还,程序功能),失去对内存的控制(并不是物理内存消失),常指堆区内存泄漏,因为堆是动态分配的,由程序员控制的,使用不当就会出现内存泄漏问题。
解决办法:(1)动态内存的申请和释放必须配对,即new-delete 和 malloc-free
(2)shared_ptr指针 管理动态申请new的对象的销毁。
shared_ptr共享指针原理:若某块内存被多个指针共享,使用计数器表示该对象被几个指针调用。一般通过use_count()函数查看有几个指针;通过release()释放内存,每当一个指针释放,计数减1,当计数减为0时,自动释放该块内存,从而避免内存泄漏。
5 不配对的new和delete
一般把new[]换成vector或array开辟连续存储空间
6 内存碎片
对于堆来讲,频繁的new/delete会造成内存空间的不连续,产生大量碎片,使程序效率降低。
对于栈来讲,不会存在这个问题,因为栈是先进后出的,压根不会存在某个内存块从栈中间弹出。
解决办法:(1) 内存池技术,相同大小或者大小接近的内存都放到同一个内存池,这样从内存池的 每个内存块之间浪费的内存会非常小或者没有浪费。
优点:一次性先从操作系统OS申请一大块内存,所以,在为每个对象分配内存时 候,就不 用再跟OS交互了,能够提升效率。
(2)内存池向系统申请内存时都以页为单位进行申请大块内存,内存池对外分配内存也 以优先使用使用率最高的内存块,这样会有更高的效率将整个内存块归还系统