现在的位置: 首页 > 综合 > 正文

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

2018年03月30日 ⁄ 综合 ⁄ 共 9223字 ⁄ 字号 评论关闭

题记:本系列学习笔记(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 member
Foo::operator new
Foo::operator delete
Foo::operator new[]
Foo::operator delete[]
> Global

Terminated with return code 0
Press any key to continue ...

 

   如果文中有错误或遗漏之处,敬请指出,谢谢!


参考文献:
[1] C++ Primer(Edition 4)
[2] Thinking in C++(Volume Two, Edition 2)
[3] International Standard:ISO/IEC 14882:1998

抱歉!评论已关闭.