重载new/delete
前言
之前说的都是运算符重载,new/delete可能谁都不会想到跟运算符相关
正文
看上去是个关键字,但分配内存也是跟运算相关。
至于游戏上的优化,大多都是内存上的优化,比如道具交互,枪械打架,都需要存储,然后取值计算。但是一个子弹类,都有不少因素,就导致计算的时候要考虑很多。那么计算量大了,内存频繁读写,就会导致速度降低了。
当然现在的机器配置基本都挺高了。这种轻量的他还是能处理的。
1 |
|
最常见的就是这样的,之所以动态分配,因为栈区一般都不大,没必要让这些都堆积在栈区,而且子弹用完就释放,除非换弹夹重新拉满。
还有一个原因,内存碎片,内存释放了,原有的内存就会空白留间隙,如果这个地方不够用,一般就挺难被重用。然后久了就要等大程序释放了。
反正底层实现是比较复杂的。动态分配都是由系统看着来。
那么重载new和delete就是变相的由人为去控制分配到堆区的时候。
new的时候
- 先分配内存空间
- 然后调用构造
- 返回指针
delete的时候
- 调用析构函数
- 然后释放内存
内存分配在c++中是比较重要的部分,有的时候我们需要重载内存分配和释放,大部分时候是为了解决内存碎片的问题
new的六种形式:
1 | void* operator new(size_t size); |
禁止重载的两种形式
1 | void* operator new(size_t size,void *p)noexcept; |
额带noexcept的是会抛出异常的。暂时先学简单的。
后面两种进制重载的方式则是在特定地址上分配
1 | bullet *bl = new ((void *)0x200000) bullet[10]; |
这个眼下用不到,比较高级的用法。
1 | class bullet{ |
1 | void *bullet::operator new(size_t size){ |
因为模板还是return nullptr;就相当于没分配,所以打印出来的是00000000。
所以return的指针决定了分配的内存地址。
改动一下试试
1 | void *bullet::operator new(size_t size){ |
注意,new自己是一个大毛病
栈溢出了。
因为bullet 重载了new ,然后又调用自己,就是相当于死循环了。
当然因为这个new的作用域在重载本身。你也可以设置全局作用域的new。
1 | void *bullet::operator new(size_t size){ |
通过::
调用全局的new分配一个内存空间。
1 | void *bullet::operator new(size_t size){ |
然后这个size该通过什么传递?
能看到size=16.
这个细心点就看到了,我们的类中有四个float成员。
而我们bullet *shota = new bullet;
就是变相的new了一个bullet类的内存。
然后让这个重载的new,分配到我们指定的地方。
1 | char *mem = new char[0x1000]; |
通过这种方式,让shota分配到我们指定的mem上。
那么这种自定义的重载就完成了。
new完了,就需要自己delete了。
与new不同,delete不需要返回什么,它只需要知道删除哪块内存空间就行。
1 | void operator delete(void *space) noexcept; |
1 | void bullet::operator delete(void *space) noexcept{ |
直接这样操作。。有点二百五。而且也犯了大毛病。。delete这里调用的是自己重载的,所以跟没发生差不多。
1 | void bullet::operator delete(void *space) noexcept{ |
但是挺奇怪,执行的时候地址没有变化,顶多编译器提示delete之后这个变量是未初始化的。
按道理来讲我不但释放了,还主动置空了,他应该打印的是00000000才对。
重复delete的时候编译器也会报错,说明我的操作是生效的,但是打印的时候不对就很烦,
delete本质是释放内存,这里纠结地址了。。
1 | class bullet{ |
1 | void *bullet::operator new(size_t size){ |
内存已经释放了,我们只需要重置对象数据就行。
而且重复delete的时候会报错,就说明那个地方已经没有内存可以释放了。
但其实重置放析构问题也不大,因为delete之前会调用析构然后释放内存
为什么说new和delete的时候是staitc修饰的。
- new的时候类还没有分配内存空间,没有内存空间自然不会有this指针
- delete阶段本身应该能用this指针,但是它在析构函数里面了,析构函数的目的就是释放内存,所以用不用this都不要紧
重载的目的是为了更灵活,固定的模式太死板不利于创造。
1 | void *operator new(size_t size, const char *txt); |
像这种额外传递参数的时候,就要实现运算符重载的函数重载哈哈。
而且注意,像这种肯定是要配套写的,不能在原有基础上改动,否则编译器很大概率不知道该调用谁合适,或者他就依你这个写的做模板,万一你传递的只是一个普通的也还是会报错。
有的时候为了灵活,我们更加希望有一块内存是我们控制的
1 | char *mem = new char[1000*sizeof(bullet)]{}; |
这块内存,就像子弹的弹夹,发射了之后还可以填充周而复始,就在这个弹夹里操作子弹,
1 | void *bullet::operator new(size_t size, const char *txt){ |
内存自然是要去mem里借了,但是如何取决于填充空白部分,那么子弹类应该就有标签一样的属性表示它是否发射。
1 | bool flag = true; |
这也是flag的由来。那么为什么一开始要等于true,自然是表示未发射,至于改成false的时机
本意是放在delete的,但是delete释放前也会调用析构函数,那么放在析构函数是最为妥当的。
1 | bullet::~bullet(){ |
如此一来基础模板就形成了:
1 | bullet *shota1 = new ("it's test") bullet; |
首先地址是连贯的,相差0x14,看内存成员算大小。
然后注意先注释掉之前写的delete的内容不然清除了有点问题。
1 | bullet *shota1 = new ("it's test") bullet; |
然后模拟发射了一颗子弹,让其它的子弹补上来。
结语
new和delete是配套的。
当你重载了new,相对应的就要重载delete。
不然你new的方式不同,用默认的delete不一定能释放,而且还会产生bug。
如果条件允许,你重载了一种就要把其他几种都重载改变。如果真的不想或者用不到,最简单的就是把那种方式=delete;