C++ Primer 学习笔记_99_特殊工具与技术 --优化内存分配[续1]

特殊工具与技术

--优化内存分配[续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表达式,实际上,我们不能重定义newdelete表达式的行为。

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 newoperator delete 函数的设计意图是供new表达式使用,但它们通常是标准库中的可用函数。可以使用它们获得未构造内存,它们有点类似allocator类的allocatedeallocate成员。例如,代替使用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 + n;)
        {
            (--p) -> ~T();
        }

【声明:我一直认为原书上这段程序写错了,我自己认为它应该有循环控制的部分,不知道是我自己错了,还是Lippman先生忘记写了,特注于此!】

显式调用析构函数的效果是适当地清除对象本身。但是并没有释放对象所占的内存,如果需要,可以重用该内存空间

【注释】

调用operator delete函数不会运行析构函数,它只是释放指定的内存!

六、类特定的newdelete

前几节介绍了类怎样能够接管自己的内部数据结构的内存管理,另一种优化内存分配的方法涉及优化new表达式的行为。考虑Queue类。该类不直接保存它的元素,相反,它使用new表达式分配QueueItem类型的对象。

通过预先分配一块原始内存以保存QueueItem对象,也许有可能改善Queue的性能。创建新QueueItem对象的时候,可以在这个预先分配的空间中构造对象。释放QueueItem对象的时候,将它们放回预先分配对象的块中,而不是将内存真正返回给系统。

这个问题与Vector的实现之间的区别在于,在这种情况下,我们希望在应用于特定类型的时候优化 newdelete表达式的行为。默认情况下,new表达式通过调用由标准库定义的operatornew 版本分配内存。通过定义自己的名为 operator new 和operator delete 的成员,类可以管理用于自身类型的内存。

编译器看到类类型的new或delete表达式的时候,它查看该类是否有operator new operator delete 成员,如果类定义(或继承)了自己的成员new和 delete函数,则使用那些函数为对象分配和释放内存;否则,调用这些函数的标准库版本

优化new和 delete的行为的时候,只需要定义operator newoperator 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),因此,这些函数没有成员数据可操纵。像任意其他静态成员函数一样,newdelete只能直接访问所属类的静态成员

2、数组操作符new[]和操作符delete[]

也可以定义成员operator new[] 和 operator delete[] 来管理类类型的数组。如果这些operator函数存在,编译器就使用它们代替全局版本

类成员operator new[]必须具有返回类型void*,并且接受的第一个形参类型为size_t.用表示存储特定类型给定数目元素的数组的字节数值自动初始化操作符的size_t形参。

成员操作符operator delete[] 必须具有返回类型void,并且第一个形参为void* 类型。用表示数组存储起始位置的值自动初始化操作符的void* 形参。

类的操作符delete[]也可以有两个形参,第二个形参为size_t。如果提供了附加形参,由编译器用数组所需存储量的字节数自动初始化这个形参

3、覆盖类特定的内存分配

如果类定义了自己的成员newdelete,类的用户就可以通过使用全局作用域确定操作符,强制newdelete表达式使用全局的库函数。如果用户编写

	 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);
};

C++ Primer 学习笔记_99_特殊工具与技术 --优化内存分配[续1],布布扣,bubuko.com

时间: 2024-10-27 13:01:30

C++ Primer 学习笔记_99_特殊工具与技术 --优化内存分配[续1]的相关文章

C++ Primer 学习笔记_100_特殊工具与技术 --优化内存分配[续2]

特殊工具与技术 --优化内存分配[续2] 七.一个内存分配器基类 预先分配一块原始内存来保存未构造的对象,创建新元素的时候,可以在一个预先分配的对象中构造:释放元素的时候,将它们放回预先分配对象的块中,而不是将内存实际返还给系统.这种策略常被称为维持一个自由列表.可以将自由列表实现为已分配但未构造的对象的链表. 我们将定义一个名为 CachedObj 的新类来处理自由列表.像 QueueItem 这样希望优化其对象分配的类可以使用 CachedObj 类,而不用直接实现自己的 new 和 del

C++ Primer 学习笔记_98_特殊工具与技术 --优化内存分配

特殊工具与技术 --优化内存分配 引言: C++的内存分配是一种类型化操作:new为特定类型分配内存,并在新分配的内存中构造该类型的一个对象.new表达式自动运行合适的构造函数来初始化每个动态分配的类类型对象. new基于每个对象分配内存的事实可能会对某些类强加不可接受的运行时开销,这样的类可能需要使用用户级的类类型对象分配能够更快一些.这样的类使用的通用策略是,预先分配用于创建新对象的内存,需要时在预先分配的内存中构造每个新对象. 另外一些类希望按最小尺寸为自己的数据成员分配需要的内存.例如,

C++ Primer 学习笔记_104_特殊工具与技术 --嵌套类

特殊工具与技术 --嵌套类 可以在另一个类内部(与后面所讲述的局部类不同,嵌套类是在类内部)定义一个类,这样的类是嵌套类,也称为嵌套类型.嵌套类最常用于定义执行类. 嵌套类是独立的类,基本上与它们的外围类不相关,因此,外围类和嵌套类的对象是互相独立的.嵌套类型的对象不具备外围类所定义的成员,同样,外围类的成员也不具备嵌套类所定义的成员. 嵌套类的名字在其外围类的作用域中可见,但在其他类作用域或定义外围类的作用域中不可见.嵌套类的名字将不会与另一作用域中声明的名字冲突 嵌套类可以具有与非嵌套类相同

C++ Primer 学习笔记_102_特殊工具与技术 --运行时类型识别[续]

特殊工具与技术 --运行时类型识别[续] 三.RTTI的使用 当比较两个派生类对象的时候,我们希望比较可能特定于派生类的数据成员.如果形参是基类引用,就只能比较基类中出现的成员,我们不能访问在派生类中但不在基类中出现的成员. 因此我们可以使用RTTI,在试图比较不同类型的对象时返回假(false). 我们将定义单个相等操作符.每个类定义一个虚函数 equal,该函数首先将操作数强制转换为正确的类型.如果转换成功,就进行真正的比较:如果转换失败,equal 操作就返回 false. 1.类层次 c

C++ Primer 学习笔记_105_特殊工具与技术 --联合:节省空间的类

特殊工具与技术 --联合:节省空间的类 联合是一种特殊的类.一个 union 对象可以有多个数据成员,但在任何时刻,只有一个成员可以有值.当将一个值赋给 union 对象的一个成员的时候,其他所有都变为未定义的. 为 union 对象分配的存储的量至少与包含其最大数据成员的一样多.联合提供了便利的办法表示一组相互排斥的值,这些值可以是不同类型的. 1.定义联合 作为例子,我们可能有一个处理不同各类数值或字符数据的过程.该过程可以定义一个 union 来保存这些值: union ToKenValu

C++ Primer 学习笔记_106_特殊工具与技术 --局部类

特殊工具与技术 --局部类 可以在函数体内部定义类,这样的类称为局部类.一个局部类定义了一个类型,该类型只在定义它的局部作用域中可见.与嵌套类不同,局部类的成员是严格受限的. 局部类的所有成员(包括函数)必须完全定义在类定义体内部,因此,局部类远不如嵌套类有用. 实际上,成员完全定义在类中的要求限制了局部类成员函数的复杂性.局部类中的函数很少超过数行代码,超过的话,阅读者会难以理解代码. 类似地,不允许局部类声明 static 数据成员,没有办法定义它们. 1.局部类不能使用函数作用域中的变量

C++ Primer 学习笔记_107_特殊工具与技术 --固有的不可移植的特征[上]

特殊工具与技术 --固有的不可移植的特征[上] C++从 C 语言继承来的不可移植特征:位域和 volatile 限定符.这些特征可使与硬件接口的直接通信更容易. C++ 还增加了另一个不可移植特征(从 C 语言继承来的):链接指示,它使得可以链接到用其他语言编写的程序. 一.位域 可以声明一种特殊的类数据成员,称为位域,来保存特定的位数.当程序需要将二进制数据传递给另一程序或硬件设备的时候,通常使用位域. 位域在内存中的布局是机器相关的. 位域必须是整型数据类型,可以是 signed 或 un

C++ Primer 学习笔记_103_特殊工具与技术 --类成员指针

特殊工具与技术 --类成员指针 成员指针可以做到:获得特定成员的指针,然后从一个对象或别的对象获得该成员.成员指针应该包含类的类型以及成员的类型. 一.声明成员指针 测试类: class Screen { public: typedef std::string::size_type index; char get() const; char get(index ht,index wd) const; private: std::string contents; index cursor; ind

C++ Primer 学习笔记_101_特殊工具与技术 --运行时类型识别

h2.western { font-family: "Liberation Sans",sans-serif; font-size: 16pt; }h2.cjk { font-family: "微软雅黑"; font-size: 16pt; }h2.ctl { font-family: "AR PL UMing CN"; font-size: 16pt; }h1 { margin-bottom: 0.21cm; }h1.western { fon