`
美丽的小岛
  • 浏览: 308150 次
  • 性别: Icon_minigender_1
  • 来自: 大连
社区版块
存档分类
最新评论

operator new在C++中的各种写法

    博客分类:
  • c
  • c++
 
阅读更多

http://blog.sina.com.cn/s/blog_3c6889fe0100tqe8.html

乍一看,在C++中动态分配内存很简单:new是分配,delete是释放,就这么简单。然而,这篇文章讲得要复杂一点,并且要考虑到自定义层次。这也许对简单的程序并不重要,但对你在代码中控制内存却是十分必要的,是否能写一个自定义的分配器,某种高级内存管理表或一个特定的垃圾回收机制。

这篇文章并不是一个综合的手册,而是一个C++中各种内存分配方法的概述。它面向已经很熟悉C++语言的读者。

 

原生operator new

我们先从原生operator new开始。考虑如下代码,它用来分配5int型的空间并返回指向他们的指针[1]

int* v = static_cast<int*>(::operator new(5 * sizeof(*v)));

当像如上的调用,operator new扮演原生的内存分配角色,类似malloc。上面等价于:

int* v = static_cast<int*>(malloc(5 * sizeof(*v)));

释放用operator new分配的内存用operator delete

::operator delete(v);

你愿意永远用原生newdelete函数吗?是,只在极少数不用,我在下面的文章中会论证的。为什么用它们而不用原来的可信的mallocfree呢?一个很充分的原因就是你想保持代码在C++领域的完整性。混合使用newfree(或mallocdelete)是很不可取的(big NO NO)。用newdelete的另一个原因是你可以重载(overload)或重写(override)这些函数,只要你需要。下面是个例子:

 

voidoperator new(size_t sz) throw (std::bad_alloc)
{
    cerr << "allocating " << sz << " bytesn";
    void* mem = malloc(sz);
    if (mem)
        return mem;
    else
        throw std::bad_alloc();
}

void operator delete(void* ptr) throw()
{
    cerr << "deallocating at " << ptr << endl;
    free(ptr);

通常,注意到new被用来给内置类型,不包含用户自定义new函数的类的对象,和任意类型的数组分配空间,使用的都是全局的运算符new。当new被用来为已经被重定义new的类实例化时,用的就是那个类的new函数。

下面来看下带new函数的类。

 

特定类的operator new

大家有时很好奇"operator new""new operator"的区别。前者可以是一个重载的operator new,全局的或者特定类或者原生的operator new。后者是你经常用来分配内存的C++内置的new operator,就像:

Car* mycar = new Car;

C++支持操作符重载,并且我们可以重载的其中一个就是new

下面是个例子:

class Base
{
public:
    voidoperator new(size_t sz)
    {
        cerr << "new " << sz << " bytesn";
        return ::operator new(sz);
    }

    void operator delete(void* p)
    {
        cerr << "deleten";
        ::operator delete(p);
    }
private:
    int m_data;
};

class Derived : public Base
{
private:
    int m_derived_data;
    vector<int> z, y, x, w;
};

int main()
{
    Base* b = new Base;
    delete b;

    Derived* d = new Derived;
    delete d;
    return 0;
}

打印结果:

new 4 bytes
delete
new 56 bytes
delete

在基类被重载的operator newoperator delete也同样被子类继承。如你所见,operator new得到了两个类的正确大小。注意实际分配内存时使用了::operator new,这是前面所描述过的原生new。在调用前面的两个冒号很关键,是为了避免进行无限递归(没有它函数将一直调用自己下去)。

为什么你要为一个类重载operator new?这里有许多理由。

 

性能:默认的内存分配算符被设计成通用的。有时你想分配给一个非常特殊的对象,通过自定义分配方式可以明显地提高内存管理。许多书和文章都讨论了这种情况。尤其是"Modern C++ Design"的第4章展示了一个为较小的对象的非常好的设计并实现了自定义的分配算符。

调试 & 统计:完全掌握内存的分配和释放为调试提供了很好的灵活性,统计信息和性能分析。你可将你的分配算符插入进专门用来探测缓冲区溢出的守卫,通过分配算符和释放算符(deallocations)的比较来检测内存泄漏,为统计和性能分析积累各种指标,等等。

个性化:对于非标准的内存分配方式。一个很好的例子是内存池或arenas,它们都使得内存管理变得更简单。另一个例子是某个对象的完善的垃圾回收系统,可以通过为一个类或整个层面写你自己的operators newdelete

 

研究在C++new运算符是很有帮助的。分配是分两步进行:

1.  首先,用全局operator new指导系统请求原生内存。

2.  一旦请求内存被分配,一个新的对象就在其中开始构造。

The C++ FAQ给出一个很好的例子,我很愿意在这里这出来:

当你写下这段代码:

Foo* p = new Foo();

编译器会生成类似这种功能的代码:

Foo* p;

 // don't catch exceptions thrown by the allocator itself

//不用捕捉分配器自己抛出的异常

 void* raw = operator new(sizeof(Foo));

 // catch any exceptions thrown by the ctor

//捕捉ctor抛出的任何异常

 try {
   p = new(raw) Foo();  // call the ctor with raw as this 
像这样用raw调用ctor分配内存
 }
 catch (...) {
   // oops, ctor threw an exception 
啊哦,ctor抛出了异常
   operator delete(raw);
   throw;  // rethrow the ctor's exception 
重新抛出ctor的异常
 }

其中在try中很有趣的一段语法被称为"placement new",我们马上就会讨论到。为了使讨论完整,我们来看下用delete来释放一个对象时一个相似的情况,它也是分两步进行:

1.  首先,将要被删除对象的析构函数被调用。

2.  然后,被对象占用的内存通过全局operator delete函数返还给系统。

所以:

delete p;

等价于[2]:

if (p != NULL) {
  p->~Foo();
  operator delete(p);
}

这时正适合我重复这篇文章第一段提到的,如果一个类有它自己的operator new operator delete,这些函数将被调用,而不是调用全局的函数来分配和收回内存。

 

Placement new

现在,回来我们上面看到样例代码中的"placement new"问题。它恰好真的能用在C++代码中的语法。首先,我想简单地解释它如何工作。然后,我们将看到它在什么时候有用。

直接调用 placement new会跳过对象分配的第一步。也就是说我们不会向操作系统请求内存。而是告诉它有一块内存用来构造对象[3]。下面的代码表明了这点:

int main(int argc, const char* argv[])
{
    // A "normal" allocation. Asks the OS for memory, so we
    // don't actually know where this ends up pointing.
    //
一个正常的分配。向操作系统请求内存,所以我们并不知道它指向哪里
    int* iptr = new int;
    cerr << "Addr of iptr = " << iptr << endl;

    // Create a buffer large enough to hold an integer, and
    // note its address.
    //
创建一块足够大的缓冲区来保存一个整型,请注意它的地址
    char mem[sizeof(int)];
    cerr << "Addr of mem = " << (void*) mem << endl;

    // Construct the new integer inside the buffer 'mem'.
    // The address is going to be mem's.
    //
在缓冲区mem中构造新的整型,地址将变成mem的地址
    int* iptr2 = new (mem) int;
    cerr << "Addr of iptr2 = " << iptr2 << endl;

    return 0;
}

在我的机器上输出如下:

Addr of iptr = 0x8679008
Addr of mem = 0xbfdd73d8
Addr of iptr2 = 0xbfdd73d8

如你所见,placement new的结构很简单。而有趣的问题是,为什么我需要用这种东西?以下显示了placement new在一些场景确实很有用:

·         自定义非侵入式内存管理。当为一个类重载 operator new 同时也允许自定义内存管理,这里关键概念是非侵入式。重载一个类的 operator new需要你改变一个类的源代码。但假设我们有一个类的代码不想或者不能更改。我们如何仍能控制它的分配呢? Placement new就是答案。这种用 Placement new达到这个目的的通用编程技术叫做内存池,有时候也叫arenas[4]

·         在一些程序中,在指定内存区域的分配对象是很必要的。一个例子是共享内存。另一个例子是嵌入式程序或使用内存映射的周边驱动程序,这些都可以很方便地在它们的“领地”分配对象。

·         许多容器库预先分配很大一块内存空间。当一个对象被添加,它们就必须在这里构造,因此就用上了placement new。典型的例子就是标准vector容器。

 

删除用placement new 分配的对象

一条C++箴言就是一个用new创建的对象应该用delete来释放。这个对placement new 同样适用吗?不完全是:

int main(int argc, const char* argv[])
{
    char mem[sizeof(int)];
    int* iptr2 = new (mem) int;

    delete iptr2;       // Whoops, segmentation fault! 
呜啊,段错误啦!

    return 0;
}

为了理解上面代码片段为什么delete iptr2会引起段错误(或某种内存异常,这个因操作系统而异),让我们回想下delete iptr2实际干了什么:

1.  First, the destructor of the object thats being deleted is called.

首先,调用将要被删除的对象的析构函数。

2.  Then, the memory occupied by the object is returned to the OS, represented by the global operator delete function.

然后,这个对象在操作系统中占用的内存用全局operator delete函数收回。

对于用placement new分配的对象,第一步是没有问题的,但第二步就可疑了。尝试释放一段没有被分配算符实际分配的内存就不对了,但上面的代码确实这么做了。iptr2指向了一段并没有用全局operator new分配的栈中的一段位置。然而,delete iptr2将尝试用全局operator delete来释放内存。当然会段错误啦。

那么我们应该怎么办?我们应该怎样正确地删除iptr2?当然,我们肯定不会认为编译器怎么会解决怎么翻译内存,毕竟,我们只是传了一个指针给placement new,那个指针可能是从栈里拿,从内存池里或者别的地方。所以必须手动根据实际情况来释放。

事实上,上面的placement new用法只是C++new指定额外参数的广义placement new语法的一种特例。它在标准头文件中定义如下:

inline voidoperator new(std::size_t, void* __p) throw()
{
    return __p;
}

C++一个对应的带有相同参数的delete也被找到,它用来释放一个对象。它在头文件中定义如下:

inline void  operator delete  (void*, void*) throw()
{
}

的确,C++运行并不知道怎么释放一个对象,所以delete函数没有操作。

怎么析构呢?对于一个int,并不真的需要一个析构函数,但假设代码是这样的:

char mem[sizeof(Foo)];
Foo* fooptr = new (mem) Foo;

对于某个有意义的类Foo。我们一旦不需要fooptr了,应该怎么析构它呢?我们必须显式调用它的析构函数:

fooptr->~Foo();

对,显式调用析构函数在C++中是合法的,并且这也是唯一一种正确的做法[5]

 

结论

这是一个复杂的主题,并且这篇文章只起到一个介绍的作用,对C++的多种内存分配方法给出了一种“尝鲜”。一旦你研究一些细节会发现还有许多有趣的编程技巧(例如,实现一个内存池分配)。这些问题最好是在有上下文的情况下提出,而不是作为一个普通的介绍性文章的一部分。如果你想知道得更多,请查阅下面的资源列表。

 

资源

·         C++ FAQ Lite, especially items 11.14 and 16.9

·         "The C++ Programming Language, 3rd edition" by Bjarne Stroustrup  10.4.11

·         "Effective C++, 3rd edition" by Scott Myers  item 52

·         "Modern C++ Design" by Andrei Alexandrescu  chapter 4

·         Several StackOverflow discussions. Start with this one and browse as long as your patience lasts.

 

 

[1]

我仍会在operator new前面显式地写::(双冒号),虽然这里并不是必须的。恕我直言,这是一个很好的做法,特别当在重载operator new的类中,可以避免二义性。

[2]

注意到这里是检查是否为NULL。这样做使delete p 很安全,即使pNULL

[3]

对传给placement new的指针确保有足够的内存分配给对象,并且确保它们正确地对齐,这都是你的应该做的。

[4]

内存池本身是一个很大且迷人的话题。我并不打算在这里扩展,所以我鼓励你自己上网找些信息,WIKI如往常一样是个好地方(good start)

[5]

事实上,标准的vector容器用这种方法去析构它保存的数据。

 

http://www.blogjava.net/dongwq/archive/2010/04/20/318874.html

C++ placement new 用法举例zz
2009-12-17 16:16

在处理内存分配的时候,C++程序员会用new操作符(operator new)来分配内存,并用delete操作符(operator delete)来释放内存。这是一个new操作符的例子。

class CTest
{
     
/* 成员函数和成员数据 */
};

// . . . 代码

// 
分配一个对象
CTest * pTest = new Test;
// 分配一个有十个对象的数组 (CTest 要有缺省构造函数(default constuctor)
CTest * p10Tests = new Test[ 10];

虽然这种写法在大多数时候都工作得很好,但还是有些情况下使用new是很烦人的,比如当你想重新分配一个数组或者当你想在预分配的内存上构造一个对象的时候。

比如第一种情况,重新分配一个数组效率是很低的:

// 分配一个有10个对象的数组
CTest * pTests = new Test[ 10];
// . . .
// 
假设现在我们需要11个对象
CTest * pNewTests = new Test[ 11];
// . . . 我们必须把原来的对象拷贝到新分配的内存中
for ( int i = 0; i < 10; i++)
     pNewTests[ i] = pTests[ i];
delete pTests;
pTests = pNewTests;

如果你想在预分配的内存上创建对象,用缺省的new操作符是行不通的。要解决这个问题,你可以用placement new构造。它允许你构造一个新对象到预分配的内存上:

// buffer 是一个void指针 (void *)
// 
用方括号[] 括起来的部分是可选的
[CYourClass * pValue = ] new( buffer) CYourClass[( parameters)];

下面是一些例子:

#include <new>

class CTest
{
public:
     CTest()
     {}
     CTest( int)
     {}
    
/* 代码*/
};

int main(int argc, char* argv[])
{

     // 
由于这个例子的目的,我们不考虑内存对齐问题
     char strBuff[ sizeof( CTest) * 10 + 100];
     CTest * pBuffer = ( CTest *)strBuff;

    
// 缺省构造
     CTest * pFirst = new(pBuffer) CTest;

    
// 缺省构造
     CTest * pSecond = new(pBuffer + 1) CTest;
    
    
// 带参数的构造;
     // 
不理会返回的指针
     new(pBuffer + 2) CTest( 5);

    
// 带参数的构造
     CTest * pFourth = new( pBuffer + 3) CTest( 10);

    
// 缺省构造
     CTest * pFifth = new(pBuffer + 4) CTest();

    
// 构造多个元素(缺省构造)
     CTest * pMultipleElements = new(pBuffer + 5) CTest[ 5];
     return 0;
}

当你有自己的内存缓冲区或者在你实现自己的内存分配策略的时候,placement new会很有用。事实上在STL中广泛使用了placement new来给容器分配内存;每个容器类都有一个模版参数说明了构造/析构对象时所用的分配器(allocator)。

在使用placement new的时候,你要记住以下几点:

  • 加上头文件#include <new>
  • 你可以用placement new构造一个数组中的元素。
  • 要析构一个用placement new分配的对象,你应该手工调用析构函数(并不存在一个“placement delete”)。它的语法如下:

pFirst->~CTest();
pSecond->~CTest();

前段事件,我问过关于placement new的问题,一位仁兄讲了一些道理,他说道:

::栈上的对象(注意,是类对象,char类型就无需了,后面还会提到)保证放在对齐地址上. 

但是,个人实验了一下,发现并不是这样

例如:
int main()
{
char c1 = 'A' ;
char c2 = 'B' ;
char c3 = 'C' ;
char c4 = 'D' ;
char c5 = 'E' ;

//-------- 验证这四个地址是否是 4 的倍数 --------------//
if ( ((int)(&c1)) % 4 == 0 )
cout << "c1:Yes" << endl ;

if ( ((int)(&c2)) % 4 == 0 )
cout << "c2:Yes" << endl ;

if ( ((int)(&c3)) % 4 == 0 )
cout << "c3:Yes" << endl ;

if ( ((int)(&c4)) % 4 == 0 )
cout << "c4:Yes" << endl ;

if ( ((int)(&c5)) % 4 == 0 )
cout << "c5:Yes" << endl ;

cout << (int)(&c1) << endl // 输出四个字符所在的地址(输出结果都是 4 的倍数)
 << (int)(&c2) << endl 
 << (int)(&c3) << endl 
 << (int)(&c4) << endl 
 << (int)(&c5) << endl ;
}
-----------------------------
上面的执行结果在VC下运行都是 4 的倍数
--------------

--> 问题1:连栈上分配的空间地址都是 4 的倍数,那就说明系统分配的空间都是 4 的倍数吧???

--> 问题2:如果万一,如果放一个对象的地址不是4的倍数,那么会出现什么情况??可以给简单说一下吗?

--> 问题3:地址对齐的通用性???
   -------------
   程序1:
Class C1
{
int i ;
char c ;
} ;
cout << sizeof(C1) << endl ;// 输出结果: 8 (是 4 的倍数)
   程序2:
class C2
{
char c1 ;
char c2 ;
} ;
cout << sizeof(C2) << endl ;// 输出结果:2 ( 上一个中char类型也给了4个字节,怎么这个地方都给了一个字节??)
--> 问题4:由上面的程序2 引出下面的程序
class C2// sizeof(C2) =2 ,在VC实验下的结果,不是 4
{
char c1 ;
char c2 ;
} ;
//----------用placement new方法建立对象----------------
void *ptr = operator new(100) ;// 分配内存
C2 *POINTER = (C2*)ptr ;// 类型转换
String *str1 = new (POINTER) C2() ;// 建立一C2对象
String *str2 = new (POINTER+1) C2() ;// 再建立一个对象
String *str3 = new (POINTER+2) C2() ;// 再建立一个对象

cout << (int)(str1) << endl// 结果:3608720(  是4的倍数)
      << (int)(str2) << endl // 结果:3608722(不是4的倍数)!!
                     << (int)(str3) << endl ;// 结果:3608724(不是4的倍数)!!
分享到:
评论

相关推荐

    operator new在C++中的各种写法总结

    这篇文章并不是一个综合的手册,而是一个C++中各种内存分配方法的概述。它面向已经很熟悉C++语言的读者。 原生operator new 我们先从原生operator new开始。考虑如下代码,它用来分配5个int型的空间并返回指向他们的...

    c语言中new_C++的new归纳详解

    在 C++ 中,new 可以分为三种形态:new operator、operator new、placement new。 new operator new operator 是我们平时所使用的 new,其行为就是前面所说的三个步骤,我们不能更改它。但具体到某一步骤中的行为...

    关于new和delete的详细用法

    8. placement new可以实现new operator三步操作中的第二步,也就是在取得了一块可以容纳指定类型对象的内存后,在这块内存上构造一个对象。例如,#include &lt;new.h&gt;void main(){ char s[sizeof(A)]; A* p = (A*)s; ...

    C++面试题集C++面试题集C++面试题集C++面试题集

    在C++面试中,面试官可能会关注各种技术点,包括语言基础、编程技巧、模板、STL、内存管理、多线程、异常处理、设计模式等。以下是一些可能的面试知识点,根据提供的部分内容进行详细说明: 1. **预处理器...

    C++语言入门[借鉴].pdf

    C++是C的超集,这意味着所有的C语言程序都可以在C++环境中编译,但由于C++增加了一些新的关键字和特性,有些C语言的写法在C++中可能会导致问题。 C++的关键字扩展包括typename、bool、dynamic_cast、mutable、...

    分享C++面试中string类的一种正确写法

    在提供的代码示例中,`main()`函数测试了`String`类的各种用法,包括直接初始化、赋值、作为函数参数和返回值,以及存储在`std::vector`中。所有这些场景在我们的`String`类实现下都能够正确工作,表明了这个设计的...

    C++基础考试.doc

    在C++中,与“零值”进行比较是非常常见的操作,通常用来判断一个变量是否为特定的“零值”,如0、0.0、FALSE或空指针。 - **BOOL类型变量与“零值”的比较** ```cpp BOOL flag = FALSE; if(flag == FALSE) { /...

    C++首创()

    在C++中,不同类型的变量与零值进行比较时有不同的写法: - 对于BOOL类型的flag,如果零值为FALSE,比较语句可以写为:`if (flag == FALSE)`或`if (!flag)`。 - 对于float类型的x,与零值比较通常使用`if (x == 0.0)...

    C++语言程序设计阶段作业一、二.docx

    题目中提到`new`、`char`、`try`都是C++的关键字,而`get`不是。 2. 字符型变量判断:在C++中,使用逻辑与`&&`来判断字符`ch`是否在'A'到'Z'之间,即`ch&gt;='A' && ch。 3. 自增运算符:`++a`先自增再进行乘法运算,...

    08级C++(下)上机试卷D.doc

    - `void insertOrder(int)`是插入排序函数,但在`if`语句中,应该使用`==`而不是`-`,即`if (last == -1)`。另外,`while`循环中的条件应该是`key [i]`,而不是`key [i]`。 - `void print()`函数的错误在于它没有...

    中兴通讯C++C笔试题(1912制作)

    - 在C/C++中,`BOOL` 类型实际上是预处理器宏,通常定义为 `#define BOOL int` 或者在某些情况下定义为一个枚举类型。对于 `BOOL flag` 的变量,它与零值比较的 if 语句应该这样写: ```c if(flag == 0) // 判断 ...

    程序设计(下)机试模拟卷1.doc

    - 在构造函数`Array::Array(int max)`中,动态分配内存时遗漏了数组长度`max`,正确写法是`p = new int[max]`。 - 析构函数`Array::~Array()`的内存释放语句有误,应为`delete[] p;`。 - 主函数`main()`中,`...

    深入浅出ES6

    - Promise的实现参考了C++项目Xanadu、E语言、Python中的实现以及JavaScript框架Dojo中的Deferred对象。 3. **for-of循环**: - 类似于传统的`for`循环,但专门用于遍历可迭代对象。 - 受到C++、Java、C#等语言...

Global site tag (gtag.js) - Google Analytics