--优化内存分配[续1]
三、operator new函数和operator delete 函数
– 分配但不初始化内存
首先,需要对new和delete表达式怎样工作有更多的理解。当使用new表达式
- string *sp = new string("initialized");
的时候,实际上发生三个步骤:
1)首先,表达式调用名为operator new 的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;
2)接下来,运行该类型的一个构造函数,用指定初始化式构造对象;
3)最后,返回指向新分配并构造的对象的指针。
- delete sp;
删除动态分配对象的时候,发生两个步骤:
1)首先,对sp指向的对象运行适当的析构函数;
2)然后,通过调用名为operator delete 的标准库函数释放该对象所用内存。
【术语对比:new 表达式和operator new函数】
与其他operator函数(如operator=)不同,operator new 和 operator delete 函数没有重载new或 delete表达式,实际上,我们不能重定义new和delete表达式的行为。
new表达式通过调用operator new 函数获得内存,并接着在该内存中构造一个对象;通过撤销一个对象执行delete表达式,并接着调用operator delete函数,以释放该对象使用的内存。
因此:operator new/delete 的主要工作就是申请/释放内存!而new/delete表达式将申请/释放内存与构造/撤销对象的工作一并都做了!
【小心:】因为new(或delete)表达式与标准库函数同名,所以二者容易混淆。
1、operator new和 operator delete接口
operator new 和 operator delete 函数有两个重载版本,每个版本支持相关的new表达式和delete表达式:
- void *operator new(size_t);
- void *operator new[](size_t);
- void *operator delete(void *); //G++编译器要求该函数必须返回void!
- void *operator delete[](void *);
2、使用分配操作符函数
虽然operator new 和operator delete 函数的设计意图是供new表达式使用,但它们通常是标准库中的可用函数。可以使用它们获得未构造内存,它们有点类似allocator类的allocate和deallocate成员。例如,代替使用allocator对象,可以在Vector类中使用operator new 和 operator delete 函数。在分配新空间时我们曾编写
- T *newelements = alloc.allocate(newcapacity);
可以重写为:
- T *newelements = static_cast<T *>
- (operator new[](newcapacity * sizeof(T)));
类似地,在重新分配由Vector成员elements指向的旧空间的时候,我们曾经编写
- alloc.deallocate(elements,end – elements);
可以重写为:
- operator delete[](elements);
这些函数的表现与allocator类的allocate和deallocate成员类似。但是,它们在一个重要方面有不同:它们在void*指针而不是类型化的指针上进行操作。
【最佳实践】
一般而言,使用allocator比直接使用operator new 和 operator delete 函数更为类型安全。
1)allocator 的 allocate成员分配类型化的内存,所以使用它的程序可以不必计算以字节为单位的所需内存量.
2)它们也可以避免对operator new 的返回值进行强制类型转换。类似地,deallocate释放特定类型的内存,也不必转换为void *。
四、定位new表达式
标准库函数operator new 和operator delete 是allocator的allocate和deallocate成员的低级版本,它们都分配但不初始化内存。
allocator的成员construct和 destroy也有两个低级选择,这些成员在由 allocator对象分配的空间中初始化和撤销对象。
类似于construct成员,有第三种new表达式,称为定位new。定位new 表达式在已分配的原始内存中初始化一个对象,它与new的其他版本的不同之处在于,它不分配内存。相反,它接受指向已分配但未构造内存的指针,并在该内存中初始化一个对象。实际上,定位new表达式使我们能够在特定的、预分配的内存地址构造一个对象。
定位new表达式的形式是:
- new (place_address) type
- new (place_address) type (initializer-list)
其中place_address必须是一个指针,而initializer-list提供了(可能为空的)初始化列表,以便在构造新分配的对象时使用。
可以使用定位new表达式代替Vector实现中的construct调用。原来的代码
- alloc.construct(first_free,item);
可用等价的定位new表达式代替
- new (first_free) T(item);
定位new表达式比allocator类的construct成员更灵活。定位new表达式初始化一个对象的时候,可以使用任何构造函数,并直接建立对象。construct函数总是使用复制构造函数。
- allocator<string> alloc;
- string *sp = alloc.allcate(2);
- new (sp) string(b,e);
- alloc.construct(sp + 1,string(b,e));
定位new表达式使用了接受一对迭代器的string构造函数,在sp指向的空间直接构造string对象。当调用construct函数的时候,必须首先从迭代器构造一个string对象,以获得传递给construct的string对象,然后,该函数使用string的复制构造函数,将那个未命名的临时string对象复制到 sp指向的对象中。
通常,这些区别是不相干的:对值型类而言,在适当的位置直接构造对象与构造临时对象并进行复制之间没有可观察到的区别,而且性能差别基本没有意义。但对某些类而言,使用复制构造函数是不可能的(因为复制构造函数是私有的),或者是应该避免的,在这种情况下,也许有必要使用定位new表达式。
- //P639 习题18.4
- //你认为为什么限制construct函数只能使用元素类型的复制构造函数?
- /*
- allocator类提供的是可感知类型的内存分配,限制construct函数只能使用元素类型的复制构造函数,可以获得更高的类型安全性!
- */
五、显式析构函数的调用
正如定位new表达式是使用allocator类的construct成员的低级选择,我们可以使用析构函数的显式调用作为调用destroy函数的低级选择。
在使用allocator对象的 Vector版本中,通过调用destroy函数清除每个元素:
- for (T *p = first_free; p != elements;)
- {
- alloc.destroy(--p);
- }
对于使用定位new表达式构造对象的程序,显式调用析构函数:
- for (T *p = first_free; p != elements;)
- {
- (--p) -> ~T();
- }
【声明:我一直认为原书上这段程序写错了,我自己认为它应该有循环控制的部分,不知道是我自己错了,还是Lippman先生忘记写了,特注于此!】
显式调用析构函数的效果是适当地清除对象本身。但是并没有释放对象所占的内存,如果需要,可以重用该内存空间。
【注释】
调用operator delete函数不会运行析构函数,它只是释放指定的内存!
六、类特定的new和delete
前几节介绍了类怎样能够接管自己的内部数据结构的内存管理,另一种优化内存分配的方法涉及优化new表达式的行为。考虑Queue类。该类不直接保存它的元素,相反,它使用new表达式分配QueueItem类型的对象。
通过预先分配一块原始内存以保存QueueItem对象,也许有可能改善Queue的性能。创建新QueueItem对象的时候,可以在这个预先分配的空间中构造对象。释放QueueItem对象的时候,将它们放回预先分配对象的块中,而不是将内存真正返回给系统。
这个问题与Vector的实现之间的区别在于,在这种情况下,我们希望在应用于特定类型的时候优化 new和 delete表达式的行为。默认情况下,new表达式通过调用由标准库定义的operatornew 版本分配内存。通过定义自己的名为 operator new 和operator delete 的成员,类可以管理用于自身类型的内存。
编译器看到类类型的new或delete表达式的时候,它查看该类是否有operator new 或operator delete 成员,如果类定义(或继承)了自己的成员new和delete函数,则使用那些函数为对象分配和释放内存;否则,调用这些函数的标准库版本。
优化new和 delete的行为的时候,只需要定义operator new 和operator delete 的新版本,new和delete表达式自己照管对象的构造和撤销。[即:只需要自己管理内存的分配/释放,至于对象的构造/撤销,则由原来的new/delete表达式自己管理]
1、成员new/delete函数
【小心地雷】
如果类定义了这两个成员中的一个,它也应该定义另一个。
类成员operator new 函数必须具有返回类型void *并接受size_t类型的形参。由new表达式用以字节计算的分配内存量初始化函数的size_t形参。
类成员operator delete 函数必须具有返回类型void[而不是void*]。它可以定义为接受单个void*类型形参,也可以定义为接受两个形参,即void*和size_t类型。由delete表达式用被delete的指针初始化void *形参,该指针可以是空指针。如果提供了size_t形参,就由编译器用第一个形参所指对象的字节大小自动初始化size_t形参(?)。
除非类是某继承层次的一部分,否则形参size_t不是必需的。当delete指向继承层次中类型的指针时,指针可以指向基类对象,也可以指向派生类对象。派生类对象的大小一般比基类对象大。如果基类有virtual析构函数,则传给operator delete 的大小将根据被删除指针所指对象的动态类型而变化;如果基类没有virtual析构函数,那么,通过基类指针删除指向派生类对象的指针的行为,跟往常一样是未定义的。
这些函数隐式地为静态函数,不必显式地将它们声明为static,虽然这样做是合法的。成员new和 delete函数必须是静态的,因为它们要么在构造对象之前使用(operator new),要么在撤销对象之后使用(operator delete),因此,这些函数没有成员数据可操纵。像任意其他静态成员函数一样,new和delete只能直接访问所属类的静态成员。
2、数组操作符new[]和操作符delete[]
也可以定义成员operator new[] 和 operator delete[] 来管理类类型的数组。如果这些operator函数存在,编译器就使用它们代替全局版本。
类成员operator new[]必须具有返回类型void*,并且接受的第一个形参类型为size_t.用表示存储特定类型给定数目元素的数组的字节数值自动初始化操作符的size_t形参。
成员操作符operator delete[] 必须具有返回类型void,并且第一个形参为void* 类型。用表示数组存储起始位置的值自动初始化操作符的void* 形参。
类的操作符delete[]也可以有两个形参,第二个形参为size_t。如果提供了附加形参,由编译器用数组所需存储量的字节数自动初始化这个形参。
3、覆盖类特定的内存分配
如果类定义了自己的成员new和delete,类的用户就可以通过使用全局作用域确定操作符,强制new或 delete表达式使用全局的库函数。如果用户编写
- Type *p = ::new Type;
- ::delete p;
那么,即使类定义了自己的类特定的operatornew,也调用全局的operator new;delete类似。
【小心地雷】
如果用new表达式调用全局operatornew 函数分配内存,则 delete表达式也应该调用全局operatordelete 函数。
- //P641 习题18.9
- template <class Type> class QueueItem
- {
- friend class Queue<Type>;
- public:
- QueueItem(const Type &t):item(t),next(0) {}
- Type item;
- QueueItem *next;
- void *operator new(size_t);
- void operator delete(void *,size_t);
- };