当前位置:嗨网首页>书籍在线阅读

13-存储方案和动态分配

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

9.2.10 存储方案和动态分配

前面介绍C++用来为变量(包括数组和结构)分配内存的5种方案(线程内存除外),它们不适用于使用C++运算符new(或C函数malloc())分配的内存,这种内存被称为动态内存。第4章介绍过,动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。与自动内存不同,动态内存不是LIFO,其分配和释放顺序要取决于new和delete在何时以何种方式被使用。通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储。

虽然存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量。例如,假设在一个函数中包含下面的语句:

float * p_fees = new float [20];

由new分配的80个字节(假设float为4个字节)的内存将一直保留在内存中,直到使用delete运算符将其释放。但当包含该声明的语句块执行完毕时,p_fees指针将消失。如果希望另一个函数能够使用这80个字节中的内容,则必须将其地址传递或返回给该函数。另一方面,如果将p_fees的链接性声明为外部的,则文件中位于该声明后面的所有函数都可以使用它。另外,通过在另一个文件中使用下述声明,便可在其中使用该指针:

extern float * p_fees;

注意: 在程序结束时,由new分配的内存通常都将被释放,不过情况也并不总是这样。例如,在不那么健壮的操作系统中,在某些情况下,请求大型内存块将导致该代码块在程序结束不会被自动释放。最佳的做法是,使用delete来释放new分配的内存。

1.使用new运算符初始化

如果要初始化动态分配的变量,该如何办呢?在C++98中,有时候可以这样做,C++11增加了其他可能性。下面先来看看C++98提供的可能性。

如果要为内置的标量类型(如int或double)分配存储空间并初始化,可在类型名后面加上初始值,并将其用括号括起:

int *pi = new int (6);            // *pi set to 6
double * pd = new double (99.99); // *pd set to 99.99

这种括号语法也可用于有合适构造函数的类,这将在本书后面介绍。

然而,要初始化常规结构或数组,需要使用大括号的列表初始化,这要求编译器支持C++11。C++11允许您这样做:

struct where {double x; double y; double z;};
where * one = new where {2.5, 5.3, 7.2}; // C++11
int * ar = new int [4] {2,4,6,7};        // C++11

在C++11中,还可将列表初始化用于单值变量:

int *pin = new int {});            // *pi set to 6
double * pdo = new double {99.99}; // *pd set to 99.99

2.new失败时

new可能找不到请求的内存量。在最初的10年中,C++在这种情况下让new返回空指针,但现在将引发异常std::bad_alloc。第15章通过一些简单的示例演示了这两种方法的工作原理。

3.new:运算符、函数和替换函数

运算符new和new []分别调用如下函数:

void * operator new(std::size_t);   // used by new
void * operator new[](std::size_t); // used by new[]

这些函数被称为分配函数(alloction function),它们位于全局名称空间中。同样,也有由delete和delete []调用的释放函数(deallocation function):

void operator delete(void *);
void operator delete[](void *);

它们使用第11章将讨论的运算符重载语法。std::size_t是一个typedef,对应于合适的整型。对于下面这样的基本语句:

int * pi = new int;

将被转换为下面这样:

int * pi = new(sizeof(int));

而下面的语句:

int * pa = new int{40];

将被转换为下面这样:

int * pa = new(40 * sizeof(int));

正如您知道的,使用运算符new的语句也可包含初始值,因此,使用new运算符时,可能不仅仅是调用new()函数。

同样,下面的语句:

delete pi;

将转换为如下函数调用:

delete (pi);

有趣的是,C++将这些函数称为可替换的(replaceable)。这意味着如果您有足够的知识和意愿,可为new和delete提供替换函数,并根据需要对其进行定制。例如,可定义作用域为类的替换函数,并对其进行定制,以满足该类的内存分配需求。在代码中,仍将使用new运算符,但它将调用您定义的new()函数。

4.定位new运算符

通常,new负责在堆(heap)中找到一个足以能够满足要求的内存块。new运算符还有另一种变体,被称为定位(placement)new运算符,它让您能够指定要使用的位置。程序员可能使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。

要使用定位new特性,首先需要包含头文件new,它提供了这种版本的new运算符的原型;然后将new运算符用于提供了所需地址的参数。除需要指定参数外,句法与常规new运算符相同。具体地说,使用定位new运算符时,变量后面可以有方括号,也可以没有。下面的代码段演示了new运算符的4种用法:

#include <new>
struct chaff
{
    char dross[20];
    int slag;
};
char buffer1[50];
char buffer2[500];
int main()
{
    chaff *p1, *p2;
    int *p3, *p4;
// first, the regular forms of new
    p1 = new chaff; // place structure in heap
    p3 = new int[20]; // place int array in heap
// now, the two forms of placement new
    p2 = new (buffer1) chaff; // place structure in buffer1
    p4 = new (buffer2) int[20]; // place int array in buffer2
...

出于简化的目的,这个示例使用两个静态数组来为定位new运算符提供内存空间。因此,上述代码从buffer1中分配空间给结构chaff,从buffer2中分配空间给一个包含20个元素的int数组。

熟悉定位new运算符后,来看一个示例程序。程序清单9.10使用常规new运算符和定位new运算符创建动态分配的数组。该程序说明了常规new运算符和定位new运算符之间的一些重要差别,在查看该程序的输出后,将对此进行讨论。

程序清单9.10 newplace.cpp

// newplace.cpp -- using placement new
#include <iostream>
#include <new>     // for placement new
const int BUF = 512;
const int N = 5;
char buffer[BUF]; // chunk of memory
int main()
{
    using namespace std;
    double *pd1, *pd2;
    int i;
    cout << "Calling new and placement new:\n";
    pd1 = new double[N]; // use heap
    pd2 = new (buffer) double[N]; // use buffer array
    for (i = 0; i < N; i++)
        pd2[i] = pd1[i] = 1000 + 20.0 * i;
    cout << "Memory addresses:\n" << " heap: " << pd1
        << " static: " << (void *) buffer <<endl;
    cout << "Memory contents:\n";
    for (i = 0; i < N; i++)
    {
        cout << pd1[i] << " at " << &pd1[i] << "; ";
        cout << pd2[i] << " at " << &pd2[i] << endl;
    }
    cout << "\nCalling new and placement new a second time:\n";
    double *pd3, *pd4;
    pd3= new double[N]; // find new address
    pd4 = new (buffer) double[N]; // overwrite old data
    for (i = 0; i < N; i++)
        pd4[i] = pd3[i] = 1000 + 40.0 * i;
    cout << "Memory contents:\n";
    for (i = 0; i < N; i++)
    {
        cout << pd3[i] << " at " << &pd3[i] << "; ";
        cout << pd4[i] << " at " << &pd4[i] << endl;
    }
    cout << "\nCalling new and placement new a third time:\n";
    delete [] pd1;
    pd1= new double[N];
    pd2 = new (buffer + N * sizeof(double)) double[N];
    for (i = 0; i < N; i++)
        pd2[i] = pd1[i] = 1000 + 60.0 * i;
    cout << "Memory contents:\n";
    for (i = 0; i < N; i++)
    {
        cout << pd1[i] << " at " << &pd1[i] << "; ";
        cout << pd2[i] << " at " << &pd2[i] << endl;
    }
    delete [] pd1;
    delete [] pd3;
    return 0;
}

下面是该程序在某个系统上运行时的输出:

Calling new and placement new:
Memory addresses:
  heap: 006E4AB0 static: 00FD9138
Memory contents:
1000 at 006E4AB0; 1000 at 00FD9138
1020 at 006E4AB8; 1020 at 00FD9140
1040 at 006E4AC0; 1040 at 00FD9148
1060 at 006E4AC8; 1060 at 00FD9150
1080 at 006E4AD0; 1080 at 00FD9158
Calling new and placement new a second time:
Memory contents:
1000 at 006E4B68; 1000 at 00FD9138
1040 at 006E4B70; 1040 at 00FD9140
1080 at 006E4B78; 1080 at 00FD9148
1120 at 006E4B80; 1120 at 00FD9150
1160 at 006E4B88; 1160 at 00FD9158
Calling new and placement new a third time:
Memory contents:
1000 at 006E4AB0; 1000 at 00FD9160
1060 at 006E4AB8; 1060 at 00FD9168
1120 at 006E4AC0; 1120 at 00FD9170
1180 at 006E4AC8; 1180 at 00FD9178
1240 at 006E4AD0; 1240 at 00FD9180

5.程序说明

有关程序清单9.10,首先要指出的一点是,定位new运算符确实将数组p2放在了数组buffer中,p2和buffer的地址都是00FD9138。然而,它们的类型不同,p1是double指针,而buffer是char指针(顺便说一句,这也是程序使用(void *)对buffer进行强制转换的原因,如果不这样做,cout将显示一个字符串)同时,常规new将数组p1放在很远的地方,其地址为006E4AB0,位于动态管理的堆中。

需要指出的第二点是,第二个常规new运算符查找一个新的内存块,其起始地址为006E4B68;但第二个定位new运算符分配与以前相同的内存块:起始地址为00FD9138的内存块。定位new运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。这将一些内存管理的负担交给了程序员。例如,在第三次调用定位new运算符时,提供了一个从数组buffer开头算起的偏移量,因此将分配新的内存:

pd2 = new (buffer + N * sizeof(double)) double[N]; // offset of 40 bytes

第三点差别是,是否使用delete来释放内存。对于常规new运算符,下面的语句释放起始地址为006E4AB0的内存块,因此接下来再次调用new运算符时,该内存块是可用的:

delete [] pd1;

然而,程序清单9.10中的程序没有使用delete来释放使用定位new运算符分配的内存。事实上,在这个例子中不能这样做。buffer指定的内存是静态内存,而delete只能用于这样的指针:指向常规new运算符分配的堆内存。也就是说,数组buffer位于delete的管辖区域之外,下面的语句将引发运行阶段错误:

delete [] pd2; // won't work

另外,如果buffer是使用常规new运算符创建的,便可以使用常规delete运算符来释放整个内存块。

定位new运算符的另一种用法是,将其与初始化结合使用,从而将信息放在特定的硬件地址处。

您可能想知道定位new运算符的工作原理。基本上,它只是返回传递给它的地址,并将其强制转换为void *,以便能够赋给任何指针类型。但这说的是默认定位new函数,C++允许程序员重载定位new函数。

将定位new运算符用于类对象时,情况将更复杂,这将在第12章介绍。

6.定位new的其他形式

就像常规new调用一个接收一个参数的new()函数一样,标准定位new调用一个接收两个参数的new()函数:

int * pi = new int;             // invokes new(sizeof(int))
int * p2 = new(buffer) int;     // invokes new(sizeof(int), buffer)
int * p3 = new(buffer) int[40]; // invokes new(40*sizeof(int), buffer)

定位new函数不可替换,但可重载。它至少需要接收两个参数,其中第一个总是std::size_t,指定了请求的字节数。这样的重载函数都被称为定义new,即使额外的参数没有指定位置。