《c语言newC++的new.pdf》由会员分享,可在线阅读,更多相关《c语言newC++的new.pdf(15页珍藏版)》请在三一文库上搜索。
1、c 语言 new:C+ 的 new “new ”是 C+ 的一个关键字,同时也是操作符。关于new的话题非常多,因为 它确实比较复杂,也非常神秘,下面我将把我了解到的与new有关的内容做一 个总结。 new的过程 当我们使用关键字 new在堆上动态创建一个对象时,它实际上做了三件事:获 得一块内存空间、调用构造函数、返回正确的指针。当然,如果我们创建的是 简单类型的变量,那么第二步会被省略。假如我们定义了如下一个类A: class A int i; public: A(int _i) :i(_i*_i) void Say() printf(“i=%dn“, i); ; / 调用 new : A
2、* pa = new A(3); 那么上述动态创建一个对象的过程大致相当于以下三句话(只是大致上): A* pa = (A*)malloc(sizeof(A); pa-A:A(3); return pa; 虽然从效果上看,这三句话也得到了一个有效的指向堆上的A对象的指针 pa, 但区别在于,当 malloc 失败时,它不会调用分配内存失败处理程序 new_handler,而使用 new的话会的。因此我们还是要尽可能的使用new ,除非 有一些特殊的需求。 new的三种形态 到目前为止,本文所提到的new都是指的“ new operator ”或称为“ new expression ”,但事实上
3、在C+ 中一提到 new ,至少可能代表以下三种含义: new operator 、operator new 、placement new 。 new operator就是我们平时所使用的new ,其行为就是前面所说的三个步骤, 我们不能更改它。但具体到某一步骤中的行为,如果它不满足我们的具体要求 时,我们是有可能更改它的。三个步骤中最后一步只是简单的做一个指针的类 型转换,没什么可说的,并且在编译出的代码中也并不需要这种转换,只是人 为的认识罢了。但前两步就有些内容了。 new operator的第一步分配内存实际上是通过调用operator new来完成的, 这里的 new实际上是像加减乘
4、除一样的操作符,因此也是可以重载的。 operator new默认情况下首先调用分配内存的代码,尝试得到一段堆上的空间, 如果成功就返回,如果失败,则转而去调用一个new_hander,然后继续重复前 面 过程。如果我们对这个过程不满意,就可以重载operator new ,来设置我 们希望的行为。例如: class A public: void* operator new(size_t size) printf(“operator new calledn“); return :operator new(size); ; A* a = new A(); 这里通过 :operator new调用
5、了原有的全局的new ,实现了在分配内存之前输 出一句话。全局的operator new也是可以重载的,但这样一来就不能再递归的 使用 new来分配内存,而只能使用malloc 了: void* operator new(size_t size) printf(“global newn“); return malloc(size); 相应的, delete 也有 delete operator和 operator delete之分,后者也是可 以重载的。并且,如果重载了operator new ,就应该也相应的重载operator delete ,这是良好的编程习惯。 new的第三种形态 pl
6、acement new 是用来实现定位构造的,因此可以实现 new operator三步操作中的第二步,也就是在取得了一块可以容纳指定类型对 象的内存后,在这块内存上构造一个对象,这有点类似于前面代码中的“p- A:A(3);”这句话,但这并不是一个标准的写法,正确的写法是使用 placement new : #include void main() char ssizeof(A); A* p = (A*)s; new(p) A(3); /p-A:A(3); p-Say(); 对头文件 或的引用是必须的,这样才可以使用 placement new 。 这里“new(p) A(3) ”这种奇怪的
7、写法便是placement new 了,它实现了在指定 内存地址上用指定类型的构造函数来构造一个对象的功能,后面A(3) 就是对构 造函数的显式调用。这里不难发现,这块指定的地址既可以是栈,又可以是堆, placement 对此不加区分。但是,除非特别必要,不要直接使用placement new ,这毕竟不是用来构造对象的正式写法,只不过是new operator的一个步 骤而已。使用 new operator地编译器会自动生成对placement new 的调用的代 码,因此也会相应的生成使用delete 时调用析构函数的代码。如果是像上面那 样在栈上使用了 placement new ,则
8、必须手工调用析构函数,这也是显式调用 析构函数的唯一情况: p-A(); 当我们觉得默认的new operator对内存的管理不能满足我们的需要,而希望自 己手工的管理内存时, placement new 就有用了。 STL中的 allocator就使用了 这种方式,借助 placement new 来实现更灵活有效的内存管理。 处理内存分配异常 正如前面所说, operator new的默认行为是请求分配内存,如果成功则返回此 内存地址,如果失败则调用一个new_handler,然后再重复此过程。于是,想 要从 operator new的执行过程中返回,则必然需要满足下列条件之一: l 分配
9、内存成功 l new_handler中抛出 bad_alloc异常 l new_handler中调用 exit()或类似的函数,使程序结束 于是,我们可以假设默认情况下operator new的行为是这样的: void* operator new(size_t size) void* p = null while(!(p = malloc(size) if(null = new_handler) throw bad_alloc(); try new_handler(); catch(bad_alloc e) throw e; catch(,) return p; 在默认情况下, new_han
10、dler 的行为是抛出一个bad_alloc 异常,因此上述循 环只会执行一次。但如果我们不希望使用默认行为,可以自定义一个 new_handler,并使用 std:set_new_handler函数使其生效。在自定义的 new_handler 中,我们可以抛出异常,可以结束程序,也可以运行一些代码使 得有可能有内存被空闲出来,从而下一次分配时也许会成功,也可以通过 set_new_handler 来安装另一个可能更有效的new_handler。例如: void MyNewHandler() printf(“New handler called!n”); throw std:bad_alloc
11、(); std:set_new_handler(MyNewHandler); 这里 new_handler 程序在抛出异常之前会输出一句话。应该注意,在 new_handler 的代码里应该注意避免再嵌套有对new的调用,因为如果这里调 用 new再失败的话,可能会再导致对new_handler 的调用,从而导致无限递归 调用。这是我猜的,并没有尝试过。 在编程时我们应该注意到对new的调用是有可能有异常被抛出的,因此在new 的代码周围应该注意保持其事务性,即不能因为调用new失败抛出异常来导致 不正确的程序逻辑或数据结构的出现。例如: class SomeClass static int
12、count; SomeClass() public: static SomeClass* GetNewInstance() count+; return new SomeClass(); ; 静态变量 count 用于记录此类型生成的实例的个数,在上述代码中,如果因 new分配内存失败而抛出异常,那么其实例个数并没有增加,但count 变量的 值却已经多了一个,从而数据结构被破坏。正确的写法是: static SomeClass* GetNewInstance() SomeClass* p = new SomeClass(); count+; return p; 这样一来,如果 new失败则直
13、接抛出异常, count 的值不会增加。类似的,在 处理线程同步时,也要注意类似的问题: void SomeFunc() lock(someMutex); /加一个锁 delete p; p = new SomeClass(); unlock(someMutex); 此时,如果 new失败, unlock 将不会被执行,于是不仅造成了一个指向不正确 地址的指针 p 的存在,还将导致 someMutex永远不会被解锁。这种情况是要注 意避免的。(参考: C+ 箴言:争取异常安全的代码) STL的内存分配与 traits技巧 在STL原码剖析一书中详细分析了SGI STL 的内存分配器的行为。与直
14、接 使用 new operator不同的是, SGI STL 并不依赖 C+ 默认的内存分配方式,而 是使用一套自行实现的方案。首先SGI STL将可用内存整块的分配,使之成为 当前进程可用的内存,当程序中确实需要分配内存时,先从这些已请求好的大 内存块中尝试取得内存,如果失败的话再尝试整块的分配大内存。这种做法有 效的避免了大量内存碎片的出现,提高了内存管理效率。 为了实现这种方式, STL使用了 placement new ,通过在自己管理的内存空间上 使用 placement new 来构造对象,以达到原有new operator所具有的功能。 template inline void
15、construct(T1* p, const T2 此函数接收一个已构造的对象,通过拷贝构造的方式在给定的内存地址p 上构 造一个新对象,代码中后半截T1(value) 便是 placement new 语法中调用构造 函数的写法,如果传入的对象value 正是所要求的类型 T1,那么这里就相当于 调用拷贝构造函数。类似的,因使用了 placement new ,编译器不会自动产生 调用析构函数的代码,需要手工的实现: template inline void destory(T* pointer) pointer-T(); 与此同时, STL中还有一个接收两个迭代器的destory 版本,可
16、将某容器上指 定范 围内的对象全部销毁。典型的实现方式就是通过一个循环来对此范围内的 对象逐一调用析构函数。如果所传入的对象是非简单类型,这样做是必要的, 但如果传入的是简单类型,或者根本没有必要调用析构函数的自定义类型(例 如只包含数个 int成员的结构体),那么再逐一调用析构函数是没有必要的, 也浪费了时间。为此,STL使用了一种称为“ type traits”的技巧,在编译 器就判断出所传入的类型是否需要调用析构函数: template inline void destory(ForwardIterator first, ForwardIterator last) _destory(fi
17、rst, last, value_type(first); 其中 value_type()用于取出迭代器所指向的对象的类型信息,于是: template inline void _destory(ForwardIterator first, ForwardIterator last, T*) typedef typename _type_traits:has_trivial_destructor trivial_destructor; _destory_aux(first, last, trivial_destructor(); / 如果需要调用析构函数: template inline vo
18、id _destory_aux(ForwardIterator first, ForwardIterator last, _false_type) for(; first inline void _destory_aux(ForwardIterator first, ForwardIterator last, _true_type) 因上述函数全都是inline的,所以多层的函数调用并不会对性能造成影响,最 终编译 的结果根据具体的类型就只是一个for 循环或者什么都没有。这里的关 键在于 _type_traits这个模板类上,它根据不同的T 类 型定义出不同的 has_trivial_des
19、tructor的结果,如果 T 是简单类型,就定义为 _true_type 类型,否则就定义为 _false_type类型。其中 _true_type 、_false_type只 不过是两个没有任何内容的类,对程序的执行结果没有什么意义,但在编译器 看来它对模板如何特化就具有非常重要的指导意义了,正如上面代码所示的那 样。_type_traits也是特化了的一系列模板类: struct _true_type ; struct _false_type ; template struct _type_traits public: typedef _false _type has_trivial_d
20、estructor; , ; template struct _type_traits public: typedef _true_type has_trivial_destructor; , ; 模板是比较高级的C+ 编程技巧,模板特化、模板偏特化就更是技巧性很强的 东西, STL 中的 type_traits充分借助模板特化的功能,实现了在程序编译期 通过编译器来决定为每一处调用使用哪个特化版本,于是在不增加编程复杂性 的 前提下大大提高了程序的运行效率。更详细的内容可参考STL源码剖析 第二、三章中的相关内容。 带有“ ”的 new和 delete 我们经常会通过 new来动态创建一个数
21、组,例如: char* s = new char100; , delete s; 严格的说,上述代码是不正确的,因为我们在分配内存时使用的是new ,而 并不是简单的 new ,但释放内存时却用的是delete 。正确的写法是使用 delete: delete s; 但是,上述错误的代码似乎也能编译执行,并不会带来什么错误。事实上,new 与 new 、delete 与 delete是有区别的,特别是当用来操作复杂类型时。假 如针对一个我们自定义的类MyClass使用 new : MyClass* p = new MyClass10; 上述代码的结果是在堆上分配了10 个连续的 MyClass
22、 实例,并且已经对它们依 次调 用了构造函数,于是我们得到了10 个可用的对象,这一点与Java、C# 有 区别的, Java、C# 中这样的结果只是得到了10 个 null 。换句话说,使用这种 写法时 MyClass 必须拥有不带参数的构造函数,否则会发现编译期错误,因为 编译器无法调用有参数的构造函数。 当这样构造成功后,我们可以再将其释放,释放时使用delete: delete p; 当我们对动态分配的数组调用delete时,其行为根据所申请的变量类型会有 所不 同。如果 p 指向简单类型,如int 、char 等,其结果只不过是这块内存被 回收,此时使用 delete与 delete
23、 没有区别,但如果p 指向的是复杂类型, delete会针对动态分配得到的每个对象调用析构函数,然后再释放内存。因 此,如果我们对上述分配得到的p 指针直接使用 delete 来回收,虽然编译期 不报什么错误(因为编译器根本看不出来这个指针p 是如何分配的),但在运 行时( DEBUG 情况下)会给出一个Debug assertion failed提示。 到这里,我们很容易提出一个问题delete是如何知道要为多少个对象调 用析构函数的?要回答这个问题,我们可以首先看一看new 的重载。 class MyClass int a; public: MyClass() printf(“ctorn“
24、); MyClass() printf(“dtorn“); ; void* operator new(size_t size) void* p = operator new(size); printf(“calling new with size=%d address=%pn“, size, p); return p; / 主函数 MyClass* mc = new MyClass3; printf(“address of mc=%pn“, mc); delete mc; 运行此段代码,得到的结果为:(VC2005 ) calling new with size=16 address= 003
25、A5A58 ctor ctor ctor address of mc= 003A5A5C dtor dtor dtor 虽然对构造函数和析构函数的调用结果都在预料之中,但所申请的内存空间大 小以及地址的数值却出现了问题。我们的类MyClass 的大小显然是 4 个字节, 并且申请的数组中有3 个元素,那么应该一共申请12 个字节才对,但事实上系 统却为我们申请了 16 字节,并且在 operator new返后我们得到的内存地址 是实际申请得到的内存地址值加4 的结果。也就是说,当为复杂类型动态分配 数组时,系统自动在最终得到的内存地址前空出了 4 个字节,我们有理由相信 这 4 个字节的内容
26、与动态分配数组的长度有关。通过单步跟踪,很容易发现这 4 个字节对应的 int值为 0x00000003,也就是说记录的是我们分配的对象的个 数。改变一下分配的个数然后再次观察的结果证实了我的想法。于是,我们也 有理由认为 new operator的行为相当于下面的伪代码: template 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 void Dele
27、te(T* pt) int count = (int*)pt)-1; for(int i = 0; i count; i+) pt.T(); void* p = (void*)(int)pt 4); T:operator delete(p); 由此可见,在默认情况下operator new与 operator new的行为是相同的, operator delete与 operator delete也是,不同的是new operator与 new operator 、delete operator与 delete operator。当然,我们可以根据不同 的需要来选择重载带有和不带有“ ”的 o
28、perator new和 delete ,以满足不 同的具体需求。 把前面类 MyClass 的代码稍做修改注释掉析构函数,然后再来看看程序的 输出: calling new with size=12 address=003A5A58 ctor ctor ctor address of mc=003A5A58 这一次, new 老老实实的申请了12 个字节的内存,并且申请的结果与new operator 返回的结果也是相同的,看来,是否在前面添加4 个字节,只取决于 这个类有没有析构函数,当然,这么说并不确切,正确的说法是这个类是否需 要调用构造函数,因为如下两种情况下虽然这个类没声明析构函数
29、,但还是多 申请了 4 个字节:一是这个类中拥有需要调用析构函数的成员,二是这个类继 承自需要调用析构函数的类。于是,我们可以递归的定义“需要调用析构函数 的类”为以下三种情况之一: 1 显式的声明了析构函数的 2 拥有需要调用析构函数的类的成员的 3 继承自需要调用析构函数的类的 类似的,动态申请简单类型的数组时,也不会多申请4 个字节。于是在这两种 情况下,释放内存时使用delete 或 delete都可以,但为养成良好的习惯, 我们还是应该注意只要是动态分配的数组,释放时就使用delete。 释放内存时如何知道长度 但这同时又带来了新问题,既然申请无需调用析构函数的类或简单类型的数组 时
30、并没有记录个数信息,那么 operator delete,或更直接的说free()是如何 来回收这块内存的呢?这就要研究malloc() 返回的内存的结构了。与new 类 似的是,实际上在 malloc()申请内存时也多申请了数个字节的内容,只不过这 与所申请的变量的类型没有任何关系,我们从调用malloc 时所传入的参数也可 以理解这一点它只接收了要申请的内存的长度,并不关系这块内存用来保 存什么类型。下面运行这样一段代码做个实验: char *p = 0; for(int i = 0; i 40; i += 4) char* s = new char; printf(“alloc %2d
31、bytes, address=%p distance=%dn“, i, s, s - p); p = s; 我们直接来看 VC2005下 Release 版本的运行结果, DEBUG 版因包含了较多的调 试信息,这里就不分析了: alloc 0 bytes, address=003A36F0 distance=3815152 alloc 4 bytes, address=003A3700 distance=16 alloc 8 bytes, address=003A3710 distance=16 alloc 12 bytes, address=003A3720 distance=16 all
32、oc 16 bytes, address=003A3738 distance=24 alloc 20 bytes, address=003A84C0 distance=19848 alloc 24 bytes, address=003A84E0 distance=32 alloc 28 bytes, address=003A8500 distance=32 alloc 32 bytes, address=003A8528 distance=40 alloc 36 bytes, address=003A8550 distance=40 每一次分配的字节数都比上一次多4,distance值记录着与
33、上一次分配的差值, 第 一个差值没有实际意义,中间有一个较大的差值,可能是这块内存已经被分 配了,于是也忽略它。结果中最小的差值为16 字节,直到我们申请16 字节时, 这个差 值变成了 24,后面也有类似的规律,那么我们可以认为申请所得的内 存结构是如下这样的: 从图中不难看出,当我们要分配一段内存时,所得的内存地址和上一次的尾地 址至少要相距 8 个字节(在 DEBUG 版中还要更多),那么我们可以猜想,这8 个字节中应该记录着与这段所分配的内存有关的信息。观察这8 个节内的内容, 得到结果如下: 图中右边为每次分配所得的地址之前8 个字节的内容的 16 进制表示,从图中红 线所表示可以看到,这 8 个字节中的第一个字节乘以8 即得到相临两次分配时 的距离,经过试验一次性分配更大的长度可知,第二个字节也是这个意义,并 且代表高 8 位,也就说前面空的这 8 个字节中的前两个字节记录了一次分配内 存的长度信息,后面的六个字节可能与空闲内存链表的信息有关,在翻译内存 时用来提供必要的信息。这就解答了前面提出的问题,原来C/C+ 在分配内存 时已经记录了足够充分的信息用于回收内半
链接地址:https://www.31doc.com/p-5106316.html