定位new运算符

通常,new 从堆中分配内存,但它还有另一种称为 定位(placement)new 运算符,它可以让我们指定要使用的位置。可以通过这个特性来设置内存管
理规程,处理需要通过特定地址进行访问的硬件或在特定位置创建对象。

要使用定位 new 特性,需要包含头文件 new。使用定位 new 运算符时,变量后面可以有方括号,也可以没有。下面代码中同时使用了传统的 new 运算
符和定位 new 运算符:

// newplace.cpp -- using placement new
#include <iostream>
#include <new> // 使用定位 new 所需要的头文件
const int BUF = 512;
const int N = 5;
char buffer[BUF];      // 数组缓冲区
int main()
{
    using namespace std;
 
    double *pd1, *pd2;
    int i;
    cout << "Calling new and placement new:\n";
    pd1 = new double[N];           // 从堆中分配
    pd2 = new (buffer) double[N];  // 使用缓冲数组
    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];            // 从堆中分配一块新的内存
    pd4 = new (buffer) double[N];  // 覆盖了原来的数据
    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;
    // cin.get();
    return 0;
}

运行输出:

Calling new and placement new:
Memory addresses:
  heap: 001D7E98  static: 0037A138
Memory contents:
1000 at 001D7E98; 1000 at 0037A138
1020 at 001D7EA0; 1020 at 0037A140
1040 at 001D7EA8; 1040 at 0037A148
1060 at 001D7EB0; 1060 at 0037A150
1080 at 001D7EB8; 1080 at 0037A158

Calling new and placement new a second time:
Memory contents:
1000 at 001D8840; 1000 at 0037A138
1040 at 001D8848; 1040 at 0037A140
1080 at 001D8850; 1080 at 0037A148
1120 at 001D8858; 1120 at 0037A150
1160 at 001D8860; 1160 at 0037A158

Calling new and placement new a third time:
Memory contents:
1000 at 001D7E98; 1000 at 0037A160
1060 at 001D7EA0; 1060 at 0037A168
1120 at 001D7EA8; 1120 at 0037A170
1180 at 001D7EB0; 1180 at 0037A178
1240 at 001D7EB8; 1240 at 0037A180
定位new 的一个主要特征就是在先分配好的缓冲区中划分出一部分用作我们自己的内存规划。如同上面,先分配好一段共 512 字节大小的 buffer 缓冲区,然后将 5*double (40个字节)大小的区域用来存放 double 数组。这个 buffer 缓冲区域是由全局数组变量 buffer[512] 定义出来的,这块区域位于静态数据区中,当然这不是必须的,这块缓冲区域同样可以位于栈中,堆中都没问题,关键是它是事先被划分好的,然后我们再在里边划分出一块为己用,这就达到了”定位“的目的。

需要注意 定位new 的使用语法, new (buffer) double[N]; 表示将拥有 N 个 double 元素的数组放在 buffer 缓冲区中,并且是从开始存放,这也是上面程序中第 1 种情况所演示的。在第 2 种情况里,再次存放了另外一个数组,也是从开始处存放,这会覆盖原来的数据。第 3 种情况,并不是从开始处存放的,而是从一个偏移处开始存放,这样可以避免覆盖掉之前的数据。


定位new 应用于对象

上面的定位 new 运算符应用于内置的数据类型(如 double),它也可以应用于对象,此时情况有些不同:

#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF = 512;
 
class JustTesting
{
private:
    string words;
    int number;
public:
    JustTesting(const string & s = "Just Testing", int n = 0) 
    {words = s; number = n; cout << words << " constructed\n"; }
    ~JustTesting() { cout << words << " destroyed\n";}
    void Show() const { cout << words << ", " << number << endl;}
};
int main()
{
    char * buffer = new char[BUF];       // 分配一块缓冲区
 
    JustTesting *pc1, *pc2;
 
    pc1 = new (buffer) JustTesting;      // 将对象放入 buffer[] 中
    pc2 = new JustTesting("Heap1", 20);  // 将对象放到堆分配的内存中
 
    cout << "Memory block addresses:\n" << "buffer: "
        << (void *) buffer << "    heap: " << pc2 <<endl;
    cout << "Memory contents:\n";
    cout << pc1 << ": ";
    pc1->Show();
    cout << pc2 << ": ";
    pc2->Show();
 
    delete pc1;
 
 
    JustTesting *pc3, *pc4;
    pc3 = new (buffer) JustTesting("Bad Idea", 6);  //之前的被覆盖了
    pc4 = new JustTesting("Heap2", 10);
    cout << "Memory contents:\n";
    cout << pc3 << ": ";
    pc3->Show();
    cout << pc4 << ": ";
    pc4->Show();
 
    delete pc2;                          // 释放 Heap1         
    delete pc4;                          // 释放 Heap2
    delete [] buffer;                    // 释放 buffer
    cout << "Done\n";
    // std::cin.get();
    return 0;
}

运行输出:

Just Testing constructed
Heap1 constructed
Memory block addresses:
buffer: 004D8800    heap: 00658EF0
Memory contents:
004D8800: Just Testing, 0
00658EF0: Heap1, 20
Bad Idea constructed
Heap2 constructed
Memory contents:
004D8800: Bad Idea, 6
004D8AA0: Heap2, 10
Heap1 destroyed
Heap2 destroyed
Done
上面程序中,定位 new 运算符创建的第 2 个对象覆盖了第 1 个对象的内存单元。因此必须自己管理好缓冲区的内存单元分配,将不同的对象放在缓冲区的不同地址上,以确保两个内存单元不重叠,如:

pc1 = new (buffer) JustTesting;
pc3 = new (buffer + sizeof (JustTesting)) JustTesting("Better Idea", 6);

另外,delete 删除 pc2 和 pc4 时,自动调用了 pc2 和 pc4 指向对象的析构函数;然而,在将 delete [] 用于 buffer 时,确实释放了由 new 分配的数组内存块,但这并不会为定位 new 运算符创建的对象调用析构函数。这个可以从函数中的输出可以看到,析构函数只宣布了 "Heap1" 和 "Heap2" 的死亡,却没有宣布 "Just Testing" 和 "Bad Idea" 的死亡。

解决上述没有调用析构函数问题的方法是,显示为定位 new 运算符创建的对象调用析构函数,这也是显示调用析构函数的少数几种情形之一。显示调用析构函数,必须指定要销毁的对象。由于有指向对象的指针,因此可以如下调用析构函数:

pc3->~JustTesting();
pc1->~JustTesting();


加入合适的delete和显式的析构函数调用,能够解决这样的问题,但是需要注意的一点是正确的删除顺序。对于使用定位new运算符创建的对象,应以与创建顺序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。

#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF = 512;
 
class JustTesting
{
private:
    string words;
    int number;
public:
    JustTesting(const string & s = "Just Testing", int n = 0) 
    {words = s; number = n; cout << words << " constructed\n"; }
    ~JustTesting() { cout << words << " destroyed\n";}
    void Show() const { cout << words << ", " << number << endl;}
};
int main()
{
    char * buffer = new char[BUF];       // 分配一块缓冲区
 
    JustTesting *pc1, *pc2;
 
    pc1 = new (buffer) JustTesting;      // 将对象放入 buffer[] 中
    pc2 = new JustTesting("Heap1", 20);  // 将对象放到堆分配的内存中
     
    cout << "Memory block addresses:\n" << "buffer: "
        << (void *) buffer << "    heap: " << pc2 <<endl;
    cout << "Memory contents:\n";
    cout << pc1 << ": ";
    pc1->Show();
    cout << pc2 << ": ";
    pc2->Show();
 
    JustTesting *pc3, *pc4;
 
// 修正重叠问题
    pc3 = new (buffer + sizeof (JustTesting))
                JustTesting("Better Idea", 6);
    pc4 = new JustTesting("Heap2", 10);
     
    cout << "Memory contents:\n";
    cout << pc3 << ": ";
    pc3->Show();
    cout << pc4 << ": ";
    pc4->Show();
     
    delete pc2;           // 释放 Heap1         
    delete pc4;           // 释放 Heap2
 
// 显示销毁由定位 new 创建的对象,注意调用的顺序
    pc3->~JustTesting();  // 销毁由 pc3 指向的对象
    pc1->~JustTesting();  //  销毁由 pc1 指向的对象
    delete [] buffer;     // 释放 buffer
     
    return 0;
}

还需要注意的是,调用析构函数的正确顺序。对于使用定位 new 运算符创建的对象,应该和创建的顺序相反。因为,晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。



版权声明:本文为u012317833原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。