new和delete的内部实现
C++中如果要在堆内存中创建和销毁对象需要借助关键字new和delete来完成。比如下面的代码
class CA { public: CA()m_a(0){} CA(int a):m_a(a){} virtual void foo(){ cout<<m_a<<endl;} int m_a; }; void main() { CA *p1 = new CA; CA *p2 = new CA(10); CA *p3 = new CA[20]; delete p1; delete p2; delete[] p3; }
new和delete既是C++中的关键字也是一种特殊的运算符。
void* operator new(size_t size); void* operator new[](size_t size); void operator delete(void *p); void operator delete[](void *p);
new和delete不仅承载着内存分配的功能还承载着对象构造函数的调用功能,因此上面的对象创建代码其实在编译时会转化为如下的实现:
CA *p1 = operator new(sizeof(CA)); //分配堆内存 CA::CA(p1); //调用构造函数 CA *p2 = operator new(sizeof(CA)); //分配堆内存 CA::CA(p2, 10); //调用构造函数 CA *p3 = operator new[](20 * sizeof(CA)); CA *pt = p3; for (int i = 0; i < 20; i++) { CA::CA(pt); pt += 1; } CA::~CA(p1); operator delete(p1); CA::~CA(p2); operator delete(p2); CA *pt = p3; for (int i = 0; i < 20; i++) { CA::~CA(pt); pt += 1; } operator delete[](p3);
看到上面的代码也许你会感到疑惑,怎么在编译时怎么会在源代码的基础上插入这么多的代码。这也是很多C程序员吐槽C++语言的原因:C++编译器会偷偷插入很多未知的代码或者对源代码进行修改和处理,而这些插入和修改动作对于程序员来说是完全不可知的!
言归正传,我们还能从上面的代码中看出new和delete操作其实是分别进行了2步操作:1.内存的分配,2.构造函数的调用;3.析构函数的调用,4.内存的销毁。所以当对象是从堆内存分配时,构造函数执前内存就已经完成分配,同样当析构函数执行完成后内存才会被销毁。
这里面一个有意思的问题就是当我们分配或者销毁的是数组对象时,系统又是如何知道应该调用多少次构造函数以及调用多少次析构函数的呢?答案就是在内存分配里面。当我们调用operator new[]来分配数组对象时,编译器时系统内部会增加4或者8字节的分配空间用来保存所分配的数组对象的数量。当对数组对象调用构造和析构函数时就可以根据这个数量值来进行循环处理了。因此上面对数组对象的分配和销毁的真实代码其实是按如下方式处理的:
// CA *p3 = new CA[20]; 这句代码在编译时其实会转化为如下的代码片段 unsigned long *p = operator new[](20 * sizeof(CA) + sizeof(unsigned long)); //64位系统多分配8字节 *p = 20; //这里保存分配的对象的数量。 CA *p3 = (CA*)(p + 1); CA *pt = p3; for (int i = 0; i < *p; i++) { CA::CA(pt); pt += 1; } // delete[] p3; 这句代码在编译时其实会转化为如下的代码片段 unsigned long *p = ((unsigned long*)p3) - 1; CA *pt = p3; for (int i = 0; i < *p; i++) { CA::~CA(pt); pt += 1; } operator delete[](p);
可见C++中为我们隐藏了多少细节啊!既然new和delete操作默认是从堆中进行内存分配,而且new和delete又是一个普通的运算符函数,那么他内部是如何实现呢?其实也很简单。我们知道C语言中堆内存分配和销毁的函数是malloc/free。因此C++中对系统默认的new和delete运算符函数就可以按如下的方法实现:
void * operator new(size_t size) { return malloc(size); } void * operator new[](size_t size) { return malloc(size); } void operator delete(void *p) { free(p); } void operator delete[](void *p) { free(p); }
这里需要注意的是你在代码里面使用new关键字和使用operator new操作符所产生的效果是不一样的。如果你在代码里面使用的是new关键字那么系统内部除了会调用operator new操作符来分配内存还会调用构造函数,而如果你直接使用operator new时则只会进行内存分配而不会执行任何构造就比如下面的代码:
CA *p1 = new CA; //这里会分配内存和执行构造函数 CA *p2 = operator new(sizeof(CA)); //这里只是执行了普通的堆内存分配而不会调用构造函数
上述的伪代码都是在运行时通过查看汇编语言而得出的结论,我是在XCODE编译器上查看运行的结果,有可能不同的编译器会有一些实现的差异,但是不管如何要想真实的了解内部实现原理还是要懂一些汇编的知识为最好。
placement技术
系统默认的new关键字除了分配堆内存外还进行构造函数的调用。而实际中我们可能有一些已经预先分配好的内存区域,我们想在这些已经分配好的内存中来构建一个对象。还有一种情况是不希望进行频繁的堆内存分配和释放而只是对同一块内存进行重复的对象构建和销毁。就如下面的代码:
char buf1[100]; CA *p1 = (CA*)buf1; CA::CA(p1); p1->foo(); p1->m_a = 10; char *buf2 = new char[sizeof(CA)]; CA *p2 = (CA*)buf2; CA::CA(p2); p2->foo(); p2->m_a = 20; p1->~CA(); p2->~CA(); delete[] buf2;
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/c-cdevelopment/482206.html