四大基本构件
一、new/delete
1.new/delete的调用过程 new的调用过程是:编译器会调用到咱们自己重载的new全局函数,然后调用对象的构造函数。 delete的调用过程:首先是调用对象的析构函数,然后调用重载的delete函数,释放空间 底层还是使用malloc和delete 注意:这就是new/delete与malloc/free的区别,虽然new/delete底层还是调用malloc和free,但是new/delete多出来的操作是调用了对象的构造函数和析构函数。对于内置对象像int、char类型没什么区别,但是对于用户自定义的对象来说,只能使用new和delete。
2.对象不能直接调用类的析构函数和构造函数
二、new[]/delete[]
1.调用过程 new[]分为两种情况:
-
简单数据类型( 包括基本数据类型和不需要析构函数的类型)。new[] 调用的是operator new[],计算出数组总大小之后调用operator new,分配出总的空间,和直接调用malloc没有区别。值得一提的是,可以通过()初始化数组为零值。 -
复杂数据类型, new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小,最后调用三次构造函数。 实际分配的内存块如下: 复杂数据类型必须要记录数组的长度。因为释放内存之前会调用每个对象的析构函数。但是编译器并不知道p实际所指对象的大小。如果没有储存数组大小,编译器如何知道该把p所指的内存分为几次来调用析构函数呢? 对于一个自定义类型。它空间的头部会有一个分配了多少个空间,也就是[]里面的数字。new[]和delete[]两个调用p时,p指向的空间不同。并且只调用delete p会报错 delete[]的调用过程:对象已经分配了多少空间就释放多少空间。
注意: 2.内存泄露的情况 注意:如果分配空间的时候是调用new[],释放空间的时候调用的是delete,这时候会不会产生内存泄露要分为两种情况。
- 分配的对象里面没有指针,比如上面的Complex(复数)类,里面没有指针,那么使用delete和delete[]的效果是一样的,因为尽管只释放了一个空间,但是其它的空间都会被操作系统回收,不会发生内存泄露。
- 分配的对象里面包含指针,那么使用delete会发生内存泄露。因为delete只释放了分配的内存,而分配的内存里面指向的指针没有被全部释放掉,只释放了一个。所以必须调用delete[],它会为每一个分配的对象调用析构函数,将指针指向的空间也释放掉。
3.分配对象和释放对象的顺序 在调用new[]时,首先是调用new[0]、new[1]… 在调用delete[]时,首先调用delete[n]、delete[n-1]…delete[0].
三、placement new
定点放置new,在一个指定的空间上分配内存。 格式是:int *p=new(buf) int;底层是调用重载的new函数,直接返回buf的地址,没有调用malloc。 注意:placement new 没有对应的placement delete,因为它没有调用malloc分配空间
四、重载operator new/operator delete
1.调用过程: operator new/operator delete底层仍然是调用malloc/free。
2.重载 operator new/operator delete是可以被重载的。一般是重载类成员函数,全局函数重载比较少见
-
重载全局的operator new/operator delete -
重载局部的operator new/operator delete 注意:这个时候重载的operator new/operator delete必须是静态成员函数 -
重载局部的operator new[]/operator delete[] -
placement new的重载 重载的示例: -
placement delete的重载 正常情况下不会调用placement delete,只有在构造函数抛出异常时,此时对象未构造完成,无法调用析构函数,但是已经分配了空间,此时使用placement delete释放掉已经分配的空间
内存管理
频繁调用malloc可能会很慢,每一次调用malloc都会在分配的空间的头部加上cookie,内存管理就是为了解决这两个问题。减少调用malloc,使得更快。减少cookie使得消耗的空间变小。
一、设计一
1、这种方法设计的思想是: 如果一个对象可能频繁的需要调用new去分配空间,那么频繁的调用malloc会很慢(实际上malloc并不慢,其实这个因素不是关键的)。还有一个问题是,如果频繁的调用new,那么每一个分配的空间都带有cookie,这样会浪费大量空间。 那么改进的方法就是:可以重载operator new,使得调用这个函数时会给一个比较大的空间。然后只需要调用一次new来分配空间,减少了malloc的调用,增加了速度。并且只有一个cookie,节省了空间。这种设计的实现用到了一个指针,这个指针指向分配空间的首地址,并且将这个空间设计为一个链表。那么每一次调用new时,只有第一次才需要调用new分配一个大的空间,然后调用new时,只需要判断这个空间是否为空就行。不为空,就将p指向的空间分配给调用new的对象,然后让p=p->next;
2.存在的缺陷: 增加了一个next指针,类的大小增加了四个字节。如果类本身就比较小的话,就比如四个字节,那么就相当于将类的大小扩大了一倍。这就是存在的缺陷。
3.实例 这个例子有100个指针,其实前24个指针,只调用了一次new,也只有一个cookie,间隔是8就可以看出来。间隔16表示cookie原来就带了8个字节,在加上对象的8个字节就是16个字节。24就是上面那一张图默认分配的24个对象的空间。
二、设计二
与设计一相比,设计二就是设计了一个union类,共享对象和next指针的内存,这样就节省了空间。一个空间被分配之后它的next将没用,所以空间分配好后,在调用set初始化类的数据,将next指针的空间覆盖掉。 示例
三、设计三
因为操作都比较类似,每一个类就需要重新设计一个operator new,比较繁琐。所以设置一个能重用的类alloctaor。
四、设计四
将上面的操作定义为宏
alloc运行模式
容器的alloc。
一、结构
1.它维护了一个叫free_list[16]的数组,这个数组的每一个元素都指向一个链表。下标为0的元素代表大小为8个字节,1为16个字节,2为24个字节,依次类推,最后一个下标15为128个字节。如果对象的大小不是8的倍数,那么会将它向上调整到最接近的8的倍数。
就比如如果对象大小是6个字节,那么会将它调整到8个字节。虽然这样看起来每一个元素浪费了两个字节,但是这个链表里面的元素是不带cookie的,cookie是8个字节,所以这样设计的空间肯定是节省了的。
对于大小大于128的对象,不会使用alloc。它会直接调用malloc。
使用union的结构,假设这个被分配的空间的地址是p。那么当它被分配完成后,空闲空间就等于p->next。然后这个next将会被要填入的元素覆盖掉。
二、演示
1.初始状态,没有元素。
2.假如现在有一个对象,对象大小为32个字节,申请一个空间。申请空间还是会调用malloc,不过他会申请一个比较大的空间,不会仅申请32个字节。那么这个空间只会携带一个cookie。
这个空间是多大呢? 这个空间的大小是32202+RoundUp(0>>4)=1280。实际上是申请了40个对象的大小,在加上后面的附加量。附加量是已经申请的总内存除以16,附加量会越来越大,这也是源于生活经验,计算机申请的空间会越来越大,所以附加量越来越大。申请的40个对象大小,第1个返回给用户,接下来19个连在链表上。下一次又需要分配内存,就不需要调用malloc了,直接用连接在链表上的空间。剩余的20个空间留作备用。此时备用空间pool的大小是640. 3.又有一个对象是64个字节。那么不会直接调用malloc,因为此时pool空间的大小是640个字节,直接使用pool空间。此时64个字节下的链表有640/10=10个元素。第一个元素被分配给了用户,剩余9个空闲的空间。此时pool空间的大小为640%10=0; 4.又有一个对象,申请的空间是96个字节。pool无余量,所以又需要调用malloc来分配空间。此时pool大小是2000. 5.一个对象又申请88个字节的空间。pool余量充足,给88个字节的链表分配了20个空间之后,还剩余240. 6.连续申请3次88,直接从list#10也就是88字节的链表下取出空间返回给用户。 7.看图 8。
三、源码
源码就是上面思想的具体实现
四、示例
1.下面两种插入方式都没有cookie,我们知道,push_back()底层还是调用placement new,但是因为采用的分配器是alloc,已经有分配好的空间了。 所以每一次的placement new只需要向链表请求空间就行,不需要调用malloc
2.普通的list容器使用的分配器每个元素都带cookie。在默认的情况下,分配器不是使用alloc,那么每一次插入元素都需要调用malloc,所以都会带cookie。 而对于list这个容器来说,它的空间是每插入一个元素就分配一次,那么如果是使用一级分配器的话,每次就要调用malloc,所以它的每一个元素都有cookie。这样二级分配器会好得多。不过默认情况下还是使用一级分配器。
3.关于容器的内存分配------以vector为例 对于一个vector来说,它的内存分配是通过allocator来实现的。 一般而言,我们习惯的C++内存配置操作和释放操作是这样的: Foo* pf=new Foo; //配置内存,然后构造对象 delete pf; //将对象析构,然后释放内存 这里的new包含两段操作,先调用::operator new分配内存,然后调用Foo::Foo()构造对象。 delete也包含两个操作,先调用析构函数,然后调用::operator delete释放内存。 为了精密分工,STL allocator决定将这两个操作分开。内存分配由alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造操作由::construct()负责,对象析构由::destroy()负责。 (1)构造和析构函数 构造函数调用placement new,调用构造函数,placement new没有调用malloc分配空间,是将对象放到了一个已经分配好的空间。 析构函数直接调用 (2)内存空间的分配与释放 考虑到小区块可能造成的内存破碎问题,SGI设计了双层配置器。第一层配置器直接使用malloc和free。第二层配置器使用alloc。
对于vector容器来说,它有着扩容机制,当空间不够时,它会调用allocate来分配双倍的空间。 iterator new_start = data_allocator::allocate(len). 一级分配器 它会使用上面这条语句分配长度为len个对象的空间,一般为原来空间的两倍。默认情况下,vector是直接调用malloc和free的,也就是一级分配器。因为它是一次性分配空间,不需要频繁调用malloc,所以直接使用一级分配器即可。每一个元素插入时,不会调用push_back(),所以每一个元素都不带cookie。
五、缺陷
1.它没有调用free和delete,尽管内存需要释放,但是只会将空闲的空间继续加到链表中。不会还给操作系统。在多进程的任务中,可能会让其它进程无内存可用。
|