深入C基础_new运算符费下载_第1页
深入C基础_new运算符费下载_第2页
深入C基础_new运算符费下载_第3页
深入C基础_new运算符费下载_第4页
深入C基础_new运算符费下载_第5页
已阅读5页,还剩4页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、深入c+的newnew是c+的一个关键字,同时也是操作符关于new的话题非常多,因为它确实比较复杂, 也非常神秘,下而我将把我了解到的与new有关的内容做一个总结new的过程当我们使用关键字new在堆上动态创建一个对彖时,它实际上做了三件事:获得一块 内存空间调用构造函数返回止确的指针当然,如果我们创建的是简单类型的变量,那么第 二步会被省略假如我们定义了如下一个类a:class ainti;public:a(intj) :i(_i*_i) void say() printf(,i=%dnh, i);;调川new:a* pa = new a(3);那么上述动态创建一个对彖的过程大致相当于以下三

2、旬话(只是大致上):虽然从效果上看,这三句话也得到了一个有效的指向堆上的a对彖的指针pa,但区别 在于,当malloc失败时,它不会调用分配内存失败处理程序new.handler,而使用new的 话会的因此我们还是要尽可能的使用new,除非有一些特殊的需求 new的三种形态到冃前为止,本文所提到的new都是指的new operator或称为new expression,但事 实上在c+中一提到new,至少可能代表以下三种含义:new operatoroperator newplacement newnew operator就是我们平时所使川的new,其行为就是前面所说的三个步骤,我们不 能更改

3、它但具体到某一步骤中的行为,如果它不满足我们的具体耍求时,我们是有对能更 改它的三个步骤中最后一步只是简单的做一个指针的类型转换,没什么口j说的,并h.在编 译出的代码中也并不需婆这种转换,只是人为的认识罢了但前两步就有些内容了new operator的笫一步分配内存实际上是通过调川operator new來完成的,这里的 new实际上是像加减乘除一样的操作符,因此也是"j以重载的operator new默认情况卜'首 先调用分配内存的代码,尝试得到一段堆上的空间,如果成功就返回,如果失败,则转而 去调用一个new.hander,然后继续重复前面过程如果我们对这个过程不满意,

4、就町以重 载operator new,來设置我们希望的行为例如:class apublic:void* operator new(size_t size)printf("operator new calledn”);return operator new(size);a* a = new a();这里通过:operatornew调用了原有的全局的new,实现了在分配内存之前输出一句话 全局的operator new也是可以重载的,但这样一来就不能再递归的使用new来分配内存, 而只能使用malloc 了:void* operator new(size_t size)printf(ng

5、lobal newn");return malloc(size);相应的,delete也有delete operator和operator delete之分,后者也是可以重载的并 且,如果重载了 operator new,就应该也相应的重载operator delete,这是良好的编程习 惯new的笫三种形态placement new是丿lj來实现定位构造的,因此町以实现new operator三步操作屮的第二步,也就是在取得了一块可以容纳指定类型对象的内存后,在 这块内存上构造一个对象,这有点类似于前面代码中的p->a:a;这句话,但这并不是一 个标准的写法,正确的写法是使用

6、placement new:#include <new.h>void main()char ssizeof(a);a*p = (a*)s;new(p) a(3); /p->a:a (3);p->say();对头文件vnew>或vnew.h>的引用是必须的,这样才可以使用placement new这里 new(p) a(3)这种奇怪的写法便是placement new 了,它实现了在指定内存地址上用指定类 型的构造函数来构造一个对象的功能,后面a(3)就是对构造函数的显式调用这里不难发现, 这块指定的地址既可以是栈,乂可以是堆,placement对此不加区分但

7、是,除非特别必要, 不要肓接使用placement new ,这毕竟不是用来构造对象的正式写法,只不过是new operator的一个步骤而已使川new operator地编译器会自动牛.成对placement new的调丿ij 的代码,因此也会相应的生成使用delete时调用析构函数的代码如果是像上面那样在栈上 使川了 placement new,则必须手工调川析构函数,这也是显式调川析构函数的唯一情 况:p>a();当我们觉得默认的new operator对内存的管理不能满足我们的需要,而希望自己手工 的管理内存时,placement new就有用了 stl屮的allocator就使

8、用了这种方式,借助 placement new来实现更灵活有效的内存管理处理内存分配异常正如前面所说,operator new的默认行为是请求分配内存,如果成功则返回此内存地 址,如果失败则调用一个new_handler,然后再重复此过程于是,想要从operator new的 执行过程中返回,则必然需要满足下列条件z-:于是,我们可以假设默认情况f operator new的行为是这样的:void* operator new(size_t size)void* p = nullwhile(!(p = malloc(size)if(null = new_handler)throw bad_all

9、oc();trynew_handler();catch(bad_alloc e)throw e;catch()return p;在默认惜况下,new_handler的行为是抛出一个bad_alloc异常,因此上述循环只会执 行一次但如果我们不希望使用默认行为,可以白定义一个new_handler,并使川 std:set_new_handler函数使其生效在自定义的new_handler屮,我们可以抛岀异常,可 以结束程序,也可以运行一些代码使得有可能有内存被空闲出來,从而卜-次分配时也许 会成功,也可以通过set_new_handler来女装另一个可能更有效的new_handler例如: vo

10、id mynewhandler()printf(new handler called!n);throw std:bad_alloc();std:set_ newha ndler(mynewhandler);这里new_handler程序在抛出异常之前会输出一句话应该注意,在new_handler的代 码里应该注意避免再嵌套有对new的调用,因为如果这里调用new再失败的话,可能会再 导致对new_handler的调用,从而导致无限递归调用这是我猜的,并没有尝试过在编程时我们应该注意到对new的调用是有可能有异常被抛出的,因此在new的代码 周围应该注意保持英事务性,即不能因为调用new失败抛出

11、异常来导致不正确的程序逻辑 或数据结构的出现例如:class someclassstatic int count;someclass() public:static someclass* getnewlnstance()coun t+;return new someclass();;静态变量count川于记录此类型主成的实例的个数,在上述代码屮,如果因new分配 内存失败而抛出异常,那么其实例个数并没有增加,但count变量的值却已经多了一个, 从而数据结构被破坏正确的写法是:static someclass* getnewlnstance()someclass* p = new somecl

12、ass();coun t+;return p;这样一来,如果new失败则肓接抛岀异常,count的值不会增加类似的,在处理线程 同步时,也要注意类似的问题:void somefunc()lock(somemutex); 加一个锁delete p;p = new someclass();unlock(somemutex);stl的内存分配与traits技巧在stl原码剖析一书屮详细分析了 sgi stl的内存分配器的行为与肓接使用new operator不同的是,sgi stl并不依赖c+默认的内存分配方式,而是使用一套自行实现 的方案首先sgi stl将可用内存整块的分配,使之成为当前进程可用

13、的内存,当程序屮确 实需耍分配内存时,先从这些己请求好的大内存块屮尝试取得内存,如果失败的话再尝试 整块的分配人内存这种做法有效的避免了人量内存碎片的岀现,提高了内存管理效率为了实现这种方式,stl使mjt placement new,通过在白己管理的内存空间上使用 placement new来构造对象,以达到原有new operator所具有的功能template <class t1, class t2>inline void construct(t 1 * p, const t2& value)此函数接收一个已构造的对象,通过拷贝构造的方式在给定的内存地址p上构造一个

14、新对象,代码中后半截t1 (value)®是placement new语法中调用构造函数的写法,如果传 入的对象value正是所要求的类型t1,那么这里就和当于调用拷贝构造函数类似的,因使 川了 placement new,编译器不会自动产牛调用析构函数的代码,需要手工的实现: template <class t> inline void destory(t* pointer)与此同时,stl'p还有一个接收两个迭代器的destory版本,可将某容器上指定范围内 的对象全部销毁典型的实现方式就是通过一个循环来对此范f貝内的对象逐一调用析构函数 如果所传入的对象是非

15、简单类型,这样做是必要的,但如果传入的是简单类型,或者根本 没有必要调用析构函数的自定义类型(例如只包含数个int成员的结构体),那么再逐一调 丿ij析构函数是没有必要的,也浪费了时间为此,stl使川了一种称为type traits的技巧,在 编译器就判断出所传入的类型是否需要调用析构函数:template <class forwardlterator>in line void destory(forwardlterator first, forwarditerator last)_destory(first, last, value_type(first);其屮value_typ

16、e()用于取出迭代器所指向的对象的类型信息,于是:template<class forward iterator, class t>inline void _destory(forwardlterator first, forward iterator last, t*)typedef typename _type_traits<t>:has_trivial_destructor trivial_destructor;_destory_aux(first, last, trivial_destructor();如果需要调川析构函数:template<class f

17、orward iteratorinline void _destory_aux(forward iterator first, forward iterator last, _false_type) for(; first < last; +first)destory(&*first);因first是迭代器,州rst取出其真正内容,然后再用&取地址/如果不需要,就什么也不做:tempalte<class forward iteratorinline void _destory_aux(forward iterator first, forward iterator

18、last, _true_type) 因上述函数全都是inline的,所以多层的函数调用并不会对性能造成影响,最终编译 的结果根据具体的类型就只是一个for循坏或者什么都没有这里的关键在于_type_traits<t> 这个模板类上,它根据不同的t类型定义出不同的has_trivial_destructor的结果,如果t 是简单类型,就定义为_true_type类型,否则就定义为_false_type类型其屮_truejype_falsejype只不过是两个没冇任何内容的类,对程序的执行结果没冇什么意 义,但在编译器看来它对模板如何特化就具有非常重要的指导意义了,正如上而代码所示 的

19、那样_type_traitsvt>也是特化了的一系列模板类:struct _truejype ;struct _falsejype ;template <class t>struct _type_traitspublic:typedef _false _type has_trivial_destructor;templateo 模板特化struct _type_traits<int>int 的特化版本public:typedef _truejype has_trivial_destructor;;具他简单类型的特化版本如果要把一个自定义的类型myclass也定义为

20、不调用析构函数,只需要相应的定义 _type_traits<t >的一个特化版本即可:templateostruct _type_traits<myclass>public:typedef _truejype has_trivial_destructor;;模板是比较高级的c+编程技巧,模板特化模板偏特化就更是技巧性很强的东西,stl屮的typejraits充分借助模板特化的功能,实现了在程序编译期通过编译器来决定为 每一处调用使用哪个特化版本,于是在不增加编程复杂性的前捉下大人捉高了程序的运行 效率更详细的内容可参考stl源码剖析第二三章中的相关内容带旬的new和de

21、lete我们经常会通过new来动态创建一个数组,例如:严格的说,上述代码是不正确的,因为我们在分配内存时使用的是new,而并不是简 单的new,但释放内存时却用的是delete正确的写法是使用delete: delete s;但是,上述错课的代码似乎也能编译执行,并不会带来什么错谋事实上,new与 newdelete与deleteq是有区别的,特别是当川来操作复杂类型时假如针对一个我们白定 义的类myclass使用new:myclass* p = new myclass10;上述代码的结果是在堆上分配了 10个连续的myclass实例,并且已经对它们依次调用 了构造函数,于是我们得到了 10个

22、可用的对象,这一点与javac#有区别的,javac#屮这 样的结果只是得到了 10个null换句话说,使用这种写法时myclass必须拥冇不带参数的构 造函数,否则会发现编译期错课,因为编译器无法调用有参数的构造函数当这样构造成功后,我们可以再将其轻放,释放时使用delete: deleted p;当我们对动态分配的数组调用delete时,其行为根据所屮请的变量类型会有所不同如 果p指向简单类型,如intchar等,具结果只不过是这块内存被回收,此时使用delete与 delete没有区别,但如果p指向的是复杂类型,delete会针对动态分配得到的每个对象调 丿ij析构函数,然后再释放内存因

23、此,如果我们对上述分配得到的p指针直接使用delete來 i叫收,虽然编译期不报什么错误(因为编译器根木看不出来这个指针p是如何分配的), 但在运行时(debug情况f)会给出一个debug assertion failed提示到这里,我们很容易提出一个问题delete是如何知道要为多少个对象调用析构函数的? 要回答这个问题,我们可以首先看一看new的重载class myclassint a;public:myclass() printf(uctornn);myclass() printf("dtornh);;void* operator new(size_t size)void*

24、p = operator new(size);printf("calling new with size=%d address=%pn", size, p); return p;/主函数myclass* me = new myclass3; printf(haddress of mc=%pn,f, me); delete me;运行此段代码,得到的结果为:(vc2005)ctorctorctordtordtordtor虽然对构造函数和析构函数的调用结果都在预料之屮,但所屮请的内存空间人小以及 地址的数值却出现了问题我们的类myclass的大小显然是4个字节,并r申请的数组中

25、冇 3个元素,那么应该一共屮请12个字节才对,但事实上系统却为我们申请了 16字节,并且 在operator new返后我们得到的内存地址是实际申请得到的内存地址值加4的结果也就是 说,当为复杂类型动态分配数组时,系统白动在最终得到的内存地址前空岀了4个字节, 我们有理由相信这4个字节的内容与动态分配数纟r的长度有关通过单步跟踪,很容易发现 这4个字节对应的int值为0x00000003,也就是说记录的是我们分配的对象的个数改变一 下分配的个数然示再次观察的结果证实了我的想法于是,我们也有理由认为new operator 的行为相当于下面的伪代码:template <class t>

26、; t* new(int count) int size = sizeof(t) * count + 4;void* p = t:operator new(size);*(int*)p = count;t* pt = (t*)(int)p + 4);for(int i = 0; i < count; i+)new(&pti) t();return pt;上述示意性的代码省略了异常处理的部分,只是展示当我们对一个复杂类型使用new 来动态分配数组时其真正的行为是什么,从中可以看到它分配了比预期多4个字节的内存 并川它來保存对象的个数,然后对于后面每一块空间使用placement n

27、ew來调川无参构造 函数,这也就解暫了为什么这种情况下类必须有无参构造函数,最示再将首地址返回类似 的,我们很容易写出相应的delete的实现代码:template <class t> void delete(t* pt) int count = (int*)pt)-1;for(int i = 0; i v count; i+)ptit();void* p = (void*)(int)pt 4);t: operator delete(p);rtl此可见,在默认情况f operator new与operator new的行为是相同的,operator deleted 与 operat

28、or delete 也是,不同的是 new operator 与 newq operatordelete operator 与delete operator当然,我们可以根据不同的需要來选择重载带有和不带有的operator new和delete,以满足不同的具体需求把前面类myclass的代码稍做修改注释掉析构两数,然后再來看看程序的输出:calling new with size=12 address=003a5a58ctorctorctoraddress of mc=003a5a58这一次,new老老实实的屮请了 12个字节的内存,并且屮请的结果与new operator 返回的结果也是

29、相同的,看来,是否在前面添加4个字节,只取决于这个类冇没冇析构函 数,当然,这么说并不确切,正确的说法是这个类是否需要调丿ij构造函数,因为如下两种 情况下虽然这个类没声明析构函数,但还是多屮请了4个字节:一是这个类中拥有需要调 丿ij析构函数的成员,二是这个类继承白需耍调用析构两数的类于是,我们可以递归的定义 需要调用析构函数的类为以下三种情况之一:1显式的声明了析构两数的2拥有需要调用析构函数的类的成员的3继承白需要调用析构函数的类的类似的,动态屮请简单类型的数组时,也不会多屮请4个字节于是在这两种情况下, 释放内存时使川delete或delete都对以,但为养成良好的习惯,我们还是应该注

30、意只要是 动态分配的数组,禅放时就使用delete 释放内存时如何知道长度但这同时又带来了新问题,既然屮请无需调用析构函数的类或简单类型的数纟r时并没 有记录个数信息,那么operator delete,或更直接的说free()是如何來回收这块内存的呢? 这就要研究malloc()返冋的内存的结构了与new类似的是,实际上在malloc()屮请内存时 也多申请了数个字节的内容,只不过这与所申请的变量的类型没有任何关系,我们从调用 malloc时所传入的参数也可以理解这一点它只接收了要屮请的内存的长度,并不关系这块 内存用来保存什么类型下面运行这样一段代码做个实验:char *p = 0;for(int i = 0; i < 40; i += 4)char* s = new chari;printf(”alloc %2d bytes, address=%p distance=%dn”,i, s, s p);我们直

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论