C++之内存管理
这里只是记录了一些简单的内存管理知识,详细的成体系的内容建议观看书本和侯捷老师的视屏,末尾给出视屏和书本链接。
内存分区
在C++中,内存分为5个区。分别是栈区,堆区,自由存储区,全局/静态存储区,常量存储和代码区。
- 栈区:存放局部变量,函数的参数,返回值等,由编译器自动分配释放。分配效率高,但是分配的内存容量有限。是存在于某作用域的一快内存空间,例如当你调用函数,函数本身即会形成一个stack来放置它所接受的参数,以及返回地址。函数本体内声明的任何变量,其所使用的内存都来自己栈。
- 堆区:OS提供的一块全局内存空间,动态分配内存,由程序员分配释放,如果程序员不是放,程序结束OS回收
- 自由存储区:由malloc等分配的内存块,和堆相似,不过使用free来结束生命周期
- 全局/静态存储区:存放全局变量和静态变量
- 常量存储和代码区:存放代码和常量字符串,程序结束后由系统释放
类中数据成员和成员函数的内存分布
先从下面程序开始:
class A {
public:
void printAge() {
cout << this->age << endl;
}
public:
int age;
};
int main() {
A a;
cout << sizeof(A) << endl;
cout << &a << endl;
cout << &(a.age) << endl;
cout << sizeof(a) << endl;
cout << sizeof(a.age) << endl;
return 0;
}
类创建一个对象就会为这个对象分配内存空间,而每个对象占用的内存空间只是该对象的数据部分(指向虚函数表的指针,虚函数指针和虚基类指针也属于数据部分)所占用的存储空间,不包括函数代码所占用的内存空间。函数代码是存放在代码区,类的成员函数和非成员函数代码都存访在代码区,而运行函数分配的局部变量,函数参数,返回数据和返回地址都存放在栈区。
当含有虚函数的时候,会为类对象分配一个指向虚函数表的指针,无论虚函数有多少个,类对象都只分配一个虚函数指针,如下程序:
class A {
public:
void printAge() {
cout << this->age << endl;
}
virtual void abc() {//两个虚函数也只是虚函数表增大,而为对象分配的指向虚函数表的指针始终只有一个。
cout << this->num << endl;
}
virtual void abc1() {
cout << this->num << endl;
}
//static void printAge1() {
// cout << name << endl;
//}
public:
//static int name;
int age;
int num;
};
//int A :: name = 1;
int main() {
A a;
cout << &a << endl;
cout << &(a.age) << endl;
cout << &(a.num) << endl;
cout << sizeof(a) << endl;
cout << sizeof(a.age) << endl;
cout << sizeof(a.num) << endl;
return 0;
}
可以看出,对象a包含的内存包括vptr和两个数据成员占用的内存。
new和delete
先来看一个程序
class Complex{...}
{
Complex*p=new Coomplex;
...
//delete p;//不写这个就会造成内存泄漏。内存泄漏:已经分配的堆内存我们对他失去了控制,导致无法正常释放内存。
}
new和delete必须同时出现,如果没有delete就会出现内存泄漏。当作用域结束,p指向的堆对象仍存在,但是指针p本身已经结束了,作用域之外看不到p了。
下面说下new和delete的分配内存的机制,侯捷老师的PPT太完美了,我直接拿来用的。
先调用析构函数将申请的内存中的东西消除,然后再释放指针本身,必须是这个顺序,不然还是会造成内存泄漏。
malloc和free
malloc原型
void*malloc(size_t size);
用malloc申请内存,重点是所申请的内存的总字节数。
int *a=(int*)malloc(sizeof(int)*length)//用malloc申请一块长度为length的整数型内存
这也是为什么在调用malloc的时候要进行类型转换,因为要将void*转成所需要的指针类型。
free的原型
void free(void*memblock);
和new和delete不同的是,malloc和free只分配和释放内存。
内存池
- new,malloc申请内存的方式缺点:申请的内存块大小不定,当频繁使用时会造成大量的内存碎片进而降低性能
- 内存池原理:一种内存分配方式,在真正使用内存之前,先预先申请分配一定数量的,大小相等的内存块留作备用,当有新的内存需求的时候,从内存池中取出一部分内存块,如果内存池中的内存块不够取得,就再继续申请新的内存。
内存池实现机制:
参考STL源码刨析中内存池实现:
首先要知道allocator是用来包装内存的,最重要的两个函数allocate和deallocate,其中alllocate包装malloc,deallocate包装free。而且申请内存的时候永远先看内存池是否有剩余,有的话就用上,然后挂在0-15号某一条链表上,要不然就重新申请。
- 首先客户端会调用malloc配置一定数量的内存块,通常是8的倍数,一半给程序实际使用,一半留给内存池。
- 客户端之后如果还有内存需求,就先从内存池中取出内存块,知道内存池空了
- 之后如果客户端还有内存需求,就必须再次调用malloc配置空间,此时新申请的区块数量会增加一个随着配置次数越来越多的附加量,同样一半给程序实际使用,一半留给内存池。
- 如果整个堆的空间都不够用了,就会在原先已经分配的区块中寻找能满足当前需求的区块数量,能满足就返回,否则就报bad_alloc异常。
内存泄漏
只是一些浅显的知识点记录,更深层的东西建议看书本。
什么是内存泄漏
是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
常见情况
- 指针指向更改:这种情况和做题的时候(链表和树之类的题目)很像,动态申请了两块内存A,B,指向A的指针a,指向B的指针b,现在a=b,而内存A没有释放并且A已经没有指针能够找到它了,那就导致了内存泄漏。所以说有new一定要有delete,有malloc一定要跟着free。
- 释放结构化的元素:有一个指针变量a,指向6字节的内存位置,该内存位置中的某个字节b又指向某个动态分配的8字节的内存位置,C++中**new type []一定和delete[]**同时出现,而C中,free(a)之后一定free(a->b),
- 返回值的不正确处理:其实就一句话,一块内存一定要能够有指针指向它,一定要能够找到他,不能让它失去控制。
- 基类析构函数未声明为虚函数:基类析构函数一定要声明为虚函数,不然就可能会导致只释放子类对象中基类部分的内存,而没有释放子类对象中子类部分的内存。
内存泄漏如何避免:
- new和delete一定同时出现,malloc和free也一定同时出现
- new type []和delete []一定同时出现
- 基类的析构函数一定声明为虚函数
如何检测内存泄漏:
Linunx:
- ccmalloc
- Valgrind
- Leaky等库
windows:
VS下使用如下程序所示:
#define CRTDBG_MAP_ALLOC//CRT库放在程序最前面
...
int main(){
int *a=new int;
int *b=new int [10];
_CrtDumpMemoryLeaks();
return 0;
}
可以看到,发生了内存泄漏,所以说使用了new一定要使用delete。
参考:
侯捷老师面向对象讲解
大佬博客
内存泄漏的总结
书本:《STL源码刨析》
<c++ Primer plus>