C++ Primer学习笔记——$20 内存分配

    技术2023-03-29  15

    题记:本系列学习笔记(C++ Primer学习笔记)主要目的是讨论一些容易被大家忽略或者容易形成错误认识的内容。只适合于有了一定的C++基础的读者(至少学完一本C++教程)。

      作者: tyc611, 2007-03-03
       本文主要讨论C++的内存分配机制,operator new和operator delete函数的重载等内容。    如果文中有错误或遗漏之处,敬请指出,谢谢!
       C++中,内存分配和对象构造紧密相关,就像对象析构和内存回收一样。使用new表达式的时候,分配内存,并在该内存中构造一个对象;使用delete表达式的时候,调用析构函数撤销对象,并将对象所用内存还给系统。      如用户程序要接管内存分配,就必须处理这两个任务。分配原始内存时,必须在内存中构造对象;在释放内存之前,必须保证适当地撤销这些对象。对未构造的内存中的对象进行赋值而不是初始化(严格地说,此时对象根本就不存在),其行为是未定义的。对许多类而言,这样做引起运行时崩溃。赋值涉及删除现存对象,如果没有现存对象,赋值操作符中的动作就会有灾难性效果。   C++中的内存分配      C++提供下面两种方法分配和释放未构造的原始内存:    1)allocator类,它提供可感知类型的内存分配。这个类支持一个抽象接口,以分配内存并随后使用该内存保存对象;    2)标准库中的 operator new 和 operator delete ,它们分配和释放需要大小的原始的、未类型化的内存。      C++还提供不同的方法在原始内存中构造和撤销对象:    1)allocator类定义了名为construct和destroy的成员,其操作正如它们名字所指示的那样:construct成员在未构造内存中初始化对象,destroy成员在对象上运行适当的析构函数;    2) 定位new表达式(placement new expression)接受指向未构造内存的指针,并在该空间中初始化一个对象或一个数组;    3)可以直接调用对象的析构函数来撤销对象,运行析构函数并不释放对象所在的内存;    4)算法uninitialized_fill和uninitialized_copy像fill和copy算法一样执行,除了它们在目的地构造对象而不是给对象赋值之外。      注:现代的C++程序一般应该使用allocator类来分配内存,它更安全更灵活。但是,在构造对象的时候,用new表达式比allocator::construct成员更灵活。有几种情况下必须使用new。   allocator类      allocator类是一个模板,它提供类型化的内存分配以及对象构造与撤销,头文件为<memory>。相关操作如下:    allocator<T> a; 定义名为a的allocator对象,可以分配内存或构造T类型对象 a.allocate(n) 分配原始的未构造内存以保存T类型的n个对象,返回指向首地址的指针 a.deallocate(p, n) 释放内存,这段内存的起始地址为T*指针p,共有n个T类型的对象。在调用deallocate之前,调用在该内存中构造的任意对象的destroy是用户的责任 a.construct(p, t) 在T*指针p所指内存中构造一个新元素。调用T类型的拷贝构造函数初始化该对象为t的副本 a.destroy(p) 运行T*指针p所指对象的析构函数 uninitialized_copy(b, e, b2) 从迭代器b和e标识的输入范围将元素拷贝到从迭代器b2开始的未构造的原始内存中。该函数在目的地构造元素,而不是给它们赋值。假定由b2指出的目的地足以保存输入范围中元素的副本 uninitialized_fill(b, e, t) 将由迭代器b和e指出的范围中的对象初始化为t的副本。假定该范围是未构造的原始内存,使用拷贝构造函数构造对象 uninitialized_fill(b, e, t, n) 将由迭代器b和e指出的范围中至多n个对象初始化为t的副本。假定范围内至少为n个元素大小,使用拷贝构造函数构造对象      allocator类将内存分配和对象构造分开。当allocator对象分配内存的时候,它分配适当大小并排列成保存给定类型对象的空间。但是,它分配的内存是未构造的,allocator用户必须分别construct和destroy放置在该内存中的对象。      我们拿vector类举例。为了获得可接受的性能,vector预先分配比所需元素空间更大的元素空间。每个将元素加到容器中的vector成员检查是否有可用空间以容纳另一元素。如果有,该成员在预分配内存中下一个可用位置初始化一个对象;如果没有空的元素空间可用,就重新分配vector;vector获取新的空间,将现存元素拷贝到该空间,增加新元素,并释放旧空间。vector所用存储开始是未构造内存,它还没有保存任何对象。将元素拷贝或增加到这个预分配空间的时候,必须使用allocator类的construct成员构造元素。      为了更详细地说明,我们试着实现一个小型的vector demo,将之命名为Vector,以区别于标准类vector:    template <typename T> class Vector {    public:       Vector(): elements(0), first_free(0), end(0) { }       void push_back(const T&);       // ...    private:       static std::allocator<T> alloc;  // object to get raw memory       void reallocate();               // get more space and copy existing elements       T* elements;                     // pointer to first element in the array       T* first_free;                   // pointer to first free element in the array       T* end;                          // pointer to one past the end of the array    };       注意,上面的alloc成员是static,因为我们只需要使用它的一些成员函数,其内部并不保存我们的数据。它唯一拥有的信息是它的分配类型T,而这与Vector模板的任何特定实例化类型都是一一对应的。      类Vector中三个指针的含义如下图所示:      成员push_back实现如下:    template <typename T>    void Vector<T>::push_back(const T& t)    {       if (first_free == end)          reallocate();  // gets more space and copies existing elements ot it       alloc.construct(first_free, t);       ++first_free;    }      成员reallocate实现如下,这是Vector类最重要的一个成员:    template <typename T>    void Vector<T>::reallocate()    {       // compute size of current array and allocate space for twice as many elements       std::ptrdiff_t size = first_free - elements;       std::ptrdiff_t newcapacity = 2 * max(size, 1);       // allocate space to hold newcapacity number of elements of type T       T* newelements = alloc.allocate(newcapacity);         // construct copies of the existing elements in the new space       uninitialized_copy(elements, first_free, newelements);         // destroy the old elements in reverse order       for (T *p = first_free; p != elements; /* empty */ )          alloc.destroy(--p);       // deallocate cannot be called on 0 pointer       if (elements)          // deallocate the memory that held the elements          alloc.deallocate(elements, end - elements);         // make our data structure point to the new elements       elements = newelements;       first_free = elements + size;       end = elements + newcapacity;    }   operator new 函数 和 operator delete 函数      首先,需要对new和delete表达式怎样工作有清楚的理解。当使用 new表达式:    string *sp = new string("initialized");  // 注意这里的new是操作符,不是函数 的时候,实际上发生三个步骤:首先,该表达式调用名为 operator new的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;接下来,运行该类型的一个构造函数,用指定初始化式构造对象;最后,返回指向新分配并构造的对象的指针。    当使用 delete表达式:    delete sp;    // 注意这里的delete是操作符,不是函数 删除动态分配对象的时候,发生两个步骤:首先,对sp指向的对象运行适当的析构函数;然后,通过调用名为 operator delete的标准库函数释放该对象所用内存。   注意:标准库函数operator new和operator delete的命名容易让人误解。与其他operator函数不同,这些函数没有重载new或delete表达式(操作符),实际上,我们不能重定义new和delete表达式的行为。通过调用operator new函数执行new表达式获得内存,并接着在该内存中构造一个对象,通过执行delete表达式撤销一个对象,并接着调用operator delete函数,以释放内存。即,标准库函数operator new只是分配空间而不负责构造对象,operator delete函数只是释放空间而不负责撤销对象。      operator new 和 operator delete函数有两个重载版本,每个版本支持相关的new表达式和delete表达式:    void* operator new (size_t);        // allocate an object    void* operator new [] (size_t);     // allocate an array       void operator delete (void*);       // free an oject    void operator delete [] (void*);    // free an array      虽然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函数更为类型安全。allocate成员分配类型化的内存,所以使用它的程序可以不必计算以字节为单位的所需内存量,它们也可以避免对operator new的返回值进行强制类型转换。类似地,deallocate释放特定类型的内存,也不必转换为void*。   定位new表达式和显式析构函数的调用      标准库函数operator new 和 operator delete是allocator的allocate和deallocate成员的低级版本,它们都只分配但不初始化内存。      allocator的成员construct和destroy也有两个低级选择,这些成员在由allocator对象分配的空间中初始化和撤销对象。      类似于construct成员,有第三种new表达式,称为 定位new(placement 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, t); 可以改写为等价的定位new表达式代替:    new (first_free) T(t);      定位new表达式比allocator类的construct成员更灵活。定位new表达式初始化一个对象的时候,它可以使用任何构造函数,并直接建立对象。construct函数总是使用拷贝构造函数。      正如定位new表达式是调用allocator类的construct成员的低级选择,我们可以使用析构函数的显式调用作为调用destroy函数的低级选择。      在使用allocator对象的Vector版本中,通过使用destroy函数清除每个元素:    for (T *p = first_free; p != elements; /* empty */ )       alloc.destroy(--p); 可以改写为:    for (T *p = first_free; p != elements; /* empty */ )       (--p)->~T(); // call the destructor   类特定的new和delete      默认情况下,new和delete表达式通过调用标准库定义的operator new和operator delete版本分析内存和释放内存。类也可以通过定义自己的名为operator new和operator delete成员来优化管理自身类型的内存分配和释放。      编译器看到类类型的new或delete表达式的时候,它查看该类是否有operator new和operator delete成员,如果类定义(或继承)了自己的成员new和delete函数,则使用那些函数为对象分配和释放内存;否则,调用这些函数的标准库版本。      当通过这种方式来优化new和delete的行为时,只需要定义operator new和operator delete的新版本,new和delete表达式自己负责对象的构造和撤销。如果类定义了这两个函数中的一个,它也应该定义另一个。      类成员operator new函数必须具有返回类型为void*,并接受size_t类型的参数。在new表达式中调用operator new函数时,new表达式用以字节计算的分配内存量初始化函数的size_t参数。    类成员operator delete函数必须具有返回类型void。它可以定义为接受单个void*类型形参,也可以定义为接受两个形参,即void*和size_t类型。在delete表达式中调用operator delete函数时,delete表达式用被delete的指针初始化void*形参,该指针可以为空指针。如果提供了size_t形参,就由编译器用第一个形参所指对象的字节大小自动初始化size_t形参。    除非类是某继承层次的一部分,否则形参size_t不是必需的。当delete指向继承层次中类型的指针时,指针可以指向某基类对象,也可以指向派生类对象。派生类对象的大小一般比基类对象大。如果基类有virtual析构函数,则传给operator delete的大小将根据被删除指针所指对象的动态类型而变化;如果基类没有virtual析构函数,那么,通过基类指针删除指向派生类对象的指针的行为,跟往常一样是未定义的。      operator new和operator delete函数隐式地为静态函数,不必显式地将它们声明为static,虽然这样做是合法的。成员new和delete函数必须是静态的,因为它们要么在构造对象之前使用(operator new),要么在撤销对象之后使用(operator delete),因此,这些函数没有成员数据可操纵。像任意其他静态成员函数一样,new和delete只能直接访问所属类的静态成员。      也可以定义成员operator new[]和operator delete[]来管理类类型的数组。如果这些operator函数存在,编译器就使用它们代替全局版本。      类成员operator new[]必须具有返回类型void*,并且接受第一个形参类型为size_t。new表达式用存储该数组所需的字节数自动初始化operatore new[]的size_t形参。    类成员operator delete[]必须具有返回类型为void,并且第一个参数为void*类型。delete表达式用表示该数组的起始地址自动初始化operator delete[]的void*形参。类的operator delete[]也可以有两个形参,第二个形参为size_t类型。如果提供了这个附加形参,由编译器用数组所需存储量的字节数自动初始化这个形参。      如果类定义了自己的成员new和delete,类的用户仍可以通过使用全局作用域操作符强制new或delete表达式使用全局的库函数。例如:    Type *p = ::new Type;    // uses global operator new    ::delete p;              // uses global operator delete 注意:在定义或调用new和delete时,它们始终应该配对出现。比如,要么都调用全局的,要么都调用类成员;定义一个应当同时定义另一个。      示例代码如下:  

    #include <iostream>class Foo {public:    void* operator new (std::size_t size) {        std::cout << "Foo::operator new" << std::endl;        return ::operator new(size);    }    void* operator new[] (std::size_t size) {        std::cout << "Foo::operator new[]" << std::endl;        return ::operator new[](size);    }    void operator delete (void* p) {        std::cout << "Foo::operator delete" << std::endl;        ::operator delete(p);    }    void operator delete[] (void* p) {        std::cout << "Foo::operator delete[]" << std::endl;        ::operator delete[](p);    }    };int main () {    std::cout << "> Class member" << std::endl;    Foo *p = new Foo;    delete p;    Foo *pa = new Foo[5];    delete [] pa;    std::cout << "> Global" << std::endl;    Foo *p2 = ::new Foo;    ::delete p2;    Foo *pa2 = ::new Foo[5];    ::delete [] pa2;        return 0;}

    运行结果为:> Class memberFoo::operator newFoo::operator deleteFoo::operator new[]Foo::operator delete[]> GlobalTerminated with return code 0Press any key to continue ...

     
       如果文中有错误或遗漏之处,敬请指出,谢谢!
    参考文献: [1] C++ Primer(Edition 4) [2] Thinking in C++(Volume Two, Edition 2) [3] International Standard:ISO/IEC 14882:1998
    最新回复(0)