More Effective C++----(8)理解各种不同含义的new和delete

Item M8:理解各种不同含义的new和delete

人们有时好像喜欢故意使C++语言的术语难以理解。比如说new操作符(new operator)和new操作(operator new)的区别。

new操作符和new操作的相关知识请参考<http://blog.csdn.net/qianqin_2014/article/details/51320775>

当你写这样的代码:

string *ps = new string("Memory Management");

使用的new是new操作符。这个操作符就象sizeof一样是语言内置的,你不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new操作符总是做这两件事情,你不能以任何方式改变它的行为。(C++
 Primer 中指出,new要做三件事!!!

你所能改变的是如何为对象分配内存new操作符调用一个函数来完成必需的内存分配,你能够重写或重载这个函数来改变它的行为。new操作符为分配内存所调用函数的名字是operator new。

函数operator new 通常这样声明:

void * operator new(size_t size);

返回值类型是void*,因为这个函数返回一个未经处理(raw)的指针,未初始化的内存。(如果你喜欢,你能写一种operator new函数,在返回一个指针之前能够初始化内存以存储一些数值,但是一般不这么做。)参数size_t确定分配多少内存你能增加额外的参数重载函数operator
new,但是第一个参数类型必须是size_t。
(有关operator new更多的信息参见Effective C++ 条款8至条款10。)

你一般不会直接调用operator new,但是一旦这么做,你可以象调用其它函数一样调用它:

void *rawMemory = operator new(sizeof(string));

操作符operator new将返回一个指针,指向一块足够容纳一个string类型对象的内存。

就象malloc一样,operator new的职责只是分配内存。它对构造函数一无所知。operator new所了解的是内存分配。把operator new 返回的未经处理的指针传递给一个对象是new操作符的工作。当你的编译器遇见这样的语句:

string *ps = new string("Memory Management");

它生成的代码或多或少与下面的代码相似(更多的细节见Effective C++条款8和条款10,还有我的文章Counting object里的注释。):

void *memory = // 得到未经处理的内存
operator new(sizeof(string)); // 为String对象
call string::string("Memory Management") //初始化
on *memory; // 内存中的对象
string *ps = // 是ps指针指向
static_cast<string*>(memory); // 新的对象

注意第二步包含了构造函数的调用,你做为一个程序员被禁止这样去做。你的编译器则没有这个约束,它可以做它想做的一切。因此如果你想建立一个堆对象就必须用new操作符,不能直接调用构造函数来初始化对象。

 placement new

有时你确实想直接调用构造函数。在一个已存在的对象上调用构造函数是没有意义的,因为构造函数用来初始化对象,而一个对象仅仅能在给它初值时被初始化一次。但是有时你有一些已经被分配但是尚未处理的(raw)内存,你需要在这些内存中构造一个对象。你可以使用一个特殊的operator
new ,它被称为placement new。

下面的例子是placement new如何使用,考虑一下:

class Widget {
public:
  Widget(int widgetSize);
  ...
};
Widget * constructWidgetInBuffer(void *buffer,
                                 int widgetSize)
{
  return new (buffer) Widget(widgetSize);
}

这个函数返回一个指针,指向一个Widget对象,对象在转递给函数的buffer里分配。当程序使用共享内存或memory-mapped I/O时这个函数可能有用,因为在这样程序里对象必须被放置在一个确定地址上或一块被例程分配的内存里。(参见条款M4,一个如何使用placement
new的一个不同例子。)

在constructWidgetInBuffer里面,返回的表达式是:

new (buffer) Widget(widgetSize)

这初看上去有些陌生,但是它是new操作符的一个用法,需要使用一个额外的变量(buffer)(请参考定位new表达式),当new操作符隐含调用operator
new函数时,把这个变量传递给它。被调用的operator new函数除了待有强制的参数size_t外,还必须接受void*指针参数,指向构造对象占用的内存空间。这个operator new就是placement new,它看上去象这样:

void * operator new(size_t, void *location)
{
  return location;
}

(上述函数不能重载)

这可能比你期望的要简单,但是这就是placement new需要做的事情。毕竟operator new的目的是为对象分配内存然后返回指向该内存的指针。在使用placement new的情况下,调用者已经获得了指向内存的指针,因为调用者知道对象应该放在哪里。placement
new必须做的就是返回转递给它的指针。
没有用的(但是强制的)参数size_t没有名字,以防止编译器发出警告说它没有被使用;见条款M6。) placement new是标准C++库的一部分(见Effective C++ 条款49)。为了使用placement new,你必须使用语句#include <new>(或者如果你的编译器还不支持这新风格的头文件名(再参见Effective
C++ 条款49),<new.h>)。

让我们从placement new回来片刻,看看new操作符(new operator)与operator new的关系(1)你想在堆上建立一个对象,应该用new操作符。它既分配内存又为对象调用构造函数。(2)如果你仅仅想分配内存,就应该调用operator
new函数;它不会调用构造函数。(3)如果你想定制自己的在堆对象被建立时的内存分配过程,你应该写你自己的operator new函数,然后使用new操作符,new操作符会调用你定制的operator new。(4)如果你想在一块已经获得指针的内存里建立一个对象,应该用placement
new。

(有关更多的不同的new与delete的观点参见Effective C++ 条款7和我的文章Counting objects。)

 Deletion and Memory Deallocation

为了避免内存泄漏,每个动态内存分配必须与一个等同相反的deallocation对应。函数operator delete与delete操作符的关系与operator new与new操作符的关系一样。当你看到这些代码:

有关allocator的内存机制请参考<http://blog.csdn.net/qianqin_2014/article/details/51323555>

string *ps;
...
delete ps; // 使用delete 操作符

你的编译器会生成代码来析构对象并释放对象占有的内存。

Operator delete用来释放内存,它被这样声明:

void operator delete(void *memoryToBeDeallocated);

因此,

delete ps;  //使用delete操作符

导致编译器生成类似于这样的代码:

ps->~string();                      // call the object's dtor
operator delete(ps);                // deallocate the memory
                                    // the object occupied

这有一个隐含的意思是如果你只想处理未被初始化的内存,你应该绕过new和delete操作符,而调用operator new 获得内存和operator delete释放内存给系统:

void *buffer =                      // 分配足够的
operator new(50*sizeof(char));      // 内存以容纳50个char
                                 //没有调用构造函数
...
operator delete(buffer);              // 释放内存
                                 // 没有调用析构函数

(因为没有建立对象,怎么能够销毁对象呢...)

这与在C中调用malloc和free等同。

如果你用placement new在内存中建立对象,你应该避免在该内存中用delete操作符。因为delete操作符调用operator delete来释放内存,但是包含对象的内存最初不是被operator new分配的,placement new只是返回转递给它的指针。谁知道这个指针来自何方?而你应该显式调用对象的析构函数来解除构造函数的影响:

// 在共享内存中分配和释放内存的函数
void * mallocShared(size_t size);
void freeShared(void *memory);
void *sharedMemory = mallocShared(sizeof(Widget));
Widget *pw =                                   // 如上所示,
  constructWidgetInBuffer(sharedMemory, 10);   // 使用
                                               // placement new
...
delete pw;            // 结果不确定! 共享内存来自
                      // mallocShared, 而不是operator new
pw->~Widget();        // 正确。 析构 pw指向的Widget,
                      // 但是没有释放
                      //包含Widget的内存
freeShared(pw);       // 正确。 释放pw指向的共享内存
                      // 但是没有调用析构函数

如上例所示,如果传递给placement new的raw内存是自己动态分配的(通过一些不常用的方法),如果你希望避免内存泄漏,你必须释放它。(参见我的文章Counting objects里面关于placement delete的注释。)

 Arrays

到目前为止一切顺利,但是还得接着走。到目前为止我们所测试的都是一次建立一个对象。怎样分配数组?会发生什么?

string *ps = new string[10];          // allocate an array of
                                // objects

被使用的new仍然是new操作符,但是建立数组时new操作符的行为与单个对象建立有少许不同。第一是内存不再用operator new分配,代替以等同的数组分配函数,叫做operator new[](经常被称为array new)。它与operator new一样能被重载。这就允许你控制数组的内存分配,就象你能控制单个对象内存分配一样(但是有一些限制性说明,参见Effective
C++ 条款8)。

(operator new[]对于C++来说是一个比较新的东西,所以你的编译器可能不支持它。如果它不支持,无论在数组中的对象类型是什么,全局operator new将被用来给每个数组分配内存。在这样的编译器下定制数组内存分配是困难的,因为它需要重写全局operator new。这可不是一个能轻易接受的任务。缺省情况下,全局operator new处理程序中所有的动态内存分配,所以它行为的任何改变都将有深入和普遍的影响。而且全局operator
new有一个正常的签名(normal signature)(也就单一的参数size_t,参见Effective C++条款9),所以如果你决定用自己的方法声明它,你立刻使你的程序与其它库不兼容(参见条款M27)基于这些考虑,在缺乏operator new[]支持的编译器里为数组定制内存管理不是一个合理的设计。)

第二个不同是new操作符调用构造函数的数量。对于数组,在数组里的每一个对象的构造函数都必须被调用:

string *ps =               // 调用operator new[]为10个
new string[10];            // string对象分配内存,
                           // 然后对每个数组元素调用
                           // string对象的缺省构造函数。

同样当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。

就象你能替换或重载operator delete一样,你也替换或重载operator delete[]。在它们重载的方法上有一些限制。请参考优秀的C++教材。

new和delete操作符是内置的,其行为不受你的控制,凡是它们调用的内存分配和释放函数则可以控制。当你想定制new和delete操作符的行为时,请记住你不能真的做到这一点。你只能改变它们为完成它们的功能所采取的方法,而它们所完成的功能则被语言固定下来,不能改变。(You can modify how they do what they
do, but what they do is fixed by the language)

时间: 2024-10-12 15:30:12

More Effective C++----(8)理解各种不同含义的new和delete的相关文章

理解各种不同含义的 new 和 delete

new operator new操作符 operator new 操作符new placement new 定位new string *ps = new string("Memory Management"); 这里的new是new 操作符.这里共有两个步骤的工作要做: ①为对象申请空间 ②调用构造函数初始化内存中的对象 new 操作符总是做这两件事,不可以任何方式改变其行为. 但是你可以改变步骤①如何为对象申请空间. new 操作符是通过 operator new这个函数为对象申请空

Effective Java通俗理解(持续更新)

这篇博客是Java经典书籍<Effective Java(第二版)>的读书笔记,此书共有78条关于编写高质量Java代码的建议,我会试着逐一对其进行更为通俗易懂地讲解,故此篇博客的更新大约会持续1个月左右. 第1条:考虑用静态工厂方法代替构造器 通常情况下我们会利用类的构造器对其进行实例化,这似乎毫无疑问.但“静态工厂方法”也需要引起我们的高度注意. 什么是“静态工厂方法”?这不同于设计模式中的工厂方法,我们可以理解它为“在一个类中用一个静态方法来返回这个类的实例”,例如: public st

深入理解html5标签含义

文章简介: 关于html5相信大家早已经耳熟能详,但是他真正的意义在具体的开发中会有什么作用呢?相对于html,他又有怎样的新的定义与新理念在里面呢?为什么一些专家认为html5完全完成后,所有的工作都可以达到真正的云方式呢?这一系列的问题你是否已经想明白了呢? 本系列文章将为您一一解答你所不知道的关于html5与html中的那些事;具体会包括如:html5新的理念与想法,html5的新标签的用意与具体开发中场景应用,html5与css3的感情经历(用法搭配),包括html5的父亲html的一些

More Effective C++----(12)理解&quot;抛出一个异常&quot;与&quot;传递一个参数&quot;或&quot;调用一个虚函数&quot;间的差异

Item M12:理解"抛出一个异常"与"传递一个参数"或"调用一个虚函数"间的差异 从语法上看,在函数里声明参数与在catch子句中声明参数几乎没有什么差别: class Widget { ... }; //一个类,具体是什么类 // 在这里并不重要 void f1(Widget w); // 一些函数,其参数分别为 void f2(Widget& w); // Widget, Widget&,或 void f3(const W

More Effective C++----(19)理解临时对象的来源

Item M19:理解临时对象的来源 当程序员之间进行交谈时,他们经常把仅仅需要一小段时间的变量称为临时变量.例如在下面这段swap(交换)例程里: template<class T> void swap(T& object1, T& object2) { T temp = object1; object1 = object2; object2 = temp; } 通常把temp叫做临时变量.不过就C++而言,temp根本不是临时变量,它只是一个函数的局部对象.(一切事物皆对象

More Effective C++----(24)理解虚拟函数、多继承、虚继承和RTTI所需的代价

Item M24:理解虚拟函数.多继承.虚继承和RTTI所需的代价 C++编译器们必须实现语言的每一个特性.这些实现的细节当然是由编译器来决定的,并且不同的编译器有不同的方法实现语言的特性.在多数情况下,你不用关心这些事情.然而有些特性的实现对对象大小和其成员函数执行速度有很大的影响,所以对于这些特性有一个基本的了解,知道编译器可能在背后做了些什么,就显得很重要.这种特性中最重要的例子是虚拟函数. 当调用一个虚拟函数时,被执行的代码必须与调用函数的对象的动态类型相一致:指向对象的指针或引用的类型

[OpenCV学习]彻底理解卷积的含义

看了很多关于卷积的介绍,一直感觉不怎么理解,今天彻底研究下: 我们知道图像是由像素构成的,图像中的行和列一起构成了图像矩阵,比如一个分辨率800*400的图像,同时也是一个大矩阵,这个矩阵有着400行和800列.假设有一个3*3的滤波小矩阵(卷积核), 在进行卷积运算的时候,我们便利整个图像大矩阵中的每一个像素,先取一个像素,然后取这个像素周围的一圈像素,构成一组3*3的矩阵,与卷积核对应位置的值相乘,把相乘的结果在相加,把相加的结果作为新的值存入结果.概括如下: 卷积就是对图像大矩阵和小矩阵对

理解闭包的含义以及他的使用

1.什么是闭包, 闭包就是能够读取其它函数内部的变量. 首先我们来看一段代码:function a(){ var n = 0; function inc() { n++; console.log(n); } inc(); inc(); } a(); //控制台输出1,再输出2 再来看一段代码: function a(){ var n = 0; this.inc = function () { n++; console.log(n); }; } var c = new a(); c.inc();

《Effective C++》:条款51:编写new和delete时需固守常规

条款 50已经说明为什么要写自己的operator new和operator delete,本条款解释在编写时遵循什么守则. 从operator new开始.operator new必须返回正确的值,内存不足时必须调用new-handling函数,要有对付零内存需求的准备,避免不慎掩盖正常形式的new–这比较偏近class接口的要求而非实现要求.正常形式的new描述与条款 52. operator new如果申请内存成功,就返回指向那块内存的指针,失败则遵循条款 49描述,抛出bad_alloc