前言

之前说的都是运算符重载,new/delete可能谁都不会想到跟运算符相关


正文

看上去是个关键字,但分配内存也是跟运算相关。

至于游戏上的优化,大多都是内存上的优化,比如道具交互,枪械打架,都需要存储,然后取值计算。但是一个子弹类,都有不少因素,就导致计算的时候要考虑很多。那么计算量大了,内存频繁读写,就会导致速度降低了。
当然现在的机器配置基本都挺高了。这种轻量的他还是能处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>

class bullet{
public:
float x;
float y;
float z;

float damage;
};

int main(){

bullet *shota = new bullet();
delete shota;

return 0;
}

最常见的就是这样的,之所以动态分配,因为栈区一般都不大,没必要让这些都堆积在栈区,而且子弹用完就释放,除非换弹夹重新拉满。
还有一个原因,内存碎片,内存释放了,原有的内存就会空白留间隙,如果这个地方不够用,一般就挺难被重用。然后久了就要等大程序释放了。

反正底层实现是比较复杂的。动态分配都是由系统看着来。

那么重载new和delete就是变相的由人为去控制分配到堆区的时候。
new的时候

  1. 先分配内存空间
  2. 然后调用构造
  3. 返回指针

delete的时候

  1. 调用析构函数
  2. 然后释放内存

内存分配在c++中是比较重要的部分,有的时候我们需要重载内存分配和释放,大部分时候是为了解决内存碎片的问题
new的六种形式:

1
2
3
4
void* operator new(size_t size);
void* operator new[](size_t size);
void* operator new(size_t size,const std::nothrow_t&) noexcept;
void* operator new[](size_t size,const std::nothrow_t&) noexcept;

禁止重载的两种形式

1
2
void* operator new(size_t size,void *p)noexcept;
void* operator new[](size_t size,void *p)noexcept;

额带noexcept的是会抛出异常的。暂时先学简单的。

后面两种进制重载的方式则是在特定地址上分配

1
bullet *bl = new ((void *)0x200000) bullet[10];

这个眼下用不到,比较高级的用法。

1
2
3
4
5
6
7
8
9
10
class bullet{
public:
float x;
float y;
float z;

float damage;

void *operator new(size_t size);
};
1
2
3
void *bullet::operator new(size_t size){
return nullptr;
}

因为模板还是return nullptr;就相当于没分配,所以打印出来的是00000000。

所以return的指针决定了分配的内存地址。
改动一下试试

1
2
3
void *bullet::operator new(size_t size){
return new bullet;
}

注意,new自己是一个大毛病

栈溢出了。

因为bullet 重载了new ,然后又调用自己,就是相当于死循环了。

当然因为这个new的作用域在重载本身。你也可以设置全局作用域的new。

1
2
3
void *bullet::operator new(size_t size){
return ::new char[0x100];
}

通过::调用全局的new分配一个内存空间。

1
2
3
4
void *bullet::operator new(size_t size){
std::cout << "size:" << size << std::endl;
return ::new char[size];
}

然后这个size该通过什么传递?

能看到size=16.
这个细心点就看到了,我们的类中有四个float成员。
而我们bullet *shota = new bullet;就是变相的new了一个bullet类的内存。

然后让这个重载的new,分配到我们指定的地方。

1
2
3
4
char *mem = new char[0x1000];
void *bullet::operator new(size_t size){
return mem;
}

通过这种方式,让shota分配到我们指定的mem上。
那么这种自定义的重载就完成了。


new完了,就需要自己delete了。

与new不同,delete不需要返回什么,它只需要知道删除哪块内存空间就行。

1
void operator delete(void *space) noexcept;
1
2
3
4
void bullet::operator delete(void *space) noexcept{
std::cout << "delete" << std::endl;
delete space;
}

直接这样操作。。有点二百五。而且也犯了大毛病。。delete这里调用的是自己重载的,所以跟没发生差不多。

1
2
3
4
5
void bullet::operator delete(void *space) noexcept{
std::cout << "delete" << std::endl;
::delete space;
space = nullptr;
}

但是挺奇怪,执行的时候地址没有变化,顶多编译器提示delete之后这个变量是未初始化的。

按道理来讲我不但释放了,还主动置空了,他应该打印的是00000000才对。

重复delete的时候编译器也会报错,说明我的操作是生效的,但是打印的时候不对就很烦,

delete本质是释放内存,这里纠结地址了。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class bullet{
public:
float x;
float y;
float z;

float damage;

void *operator new(size_t size);
void operator delete(void *space) noexcept;

void Bshow(){
std::cout << "x:" << x << std::endl;
std::cout << "y:" << y << std::endl;
std::cout << "z:" << z << std::endl;
std::cout << "damage:" << damage << std::endl;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void *bullet::operator new(size_t size){
std::cout << "---new---" << std::endl;
return ::new char[size];
}

void bullet::operator delete(void *space) noexcept{
std::cout << "---delete---" << std::endl;

bullet *tmp = (bullet*)space;

if (space == nullptr){
std::cout << "已重置勿重复操作!" << std::endl;
exit(0);
}

::delete space;
space = nullptr;

tmp->x = 0;
tmp->y = 0;
tmp->z = 0;
tmp->damage = 0.0;
tmp->Bshow();
}

内存已经释放了,我们只需要重置对象数据就行。
而且重复delete的时候会报错,就说明那个地方已经没有内存可以释放了。

但其实重置放析构问题也不大,因为delete之前会调用析构然后释放内存


为什么说new和delete的时候是staitc修饰的。

  1. new的时候类还没有分配内存空间,没有内存空间自然不会有this指针
  2. delete阶段本身应该能用this指针,但是它在析构函数里面了,析构函数的目的就是释放内存,所以用不用this都不要紧

重载的目的是为了更灵活,固定的模式太死板不利于创造。

1
2
void *operator new(size_t size, const char *txt);
void operator delete(void *space, size_t size) noexcept;

像这种额外传递参数的时候,就要实现运算符重载的函数重载哈哈。
而且注意,像这种肯定是要配套写的,不能在原有基础上改动,否则编译器很大概率不知道该调用谁合适,或者他就依你这个写的做模板,万一你传递的只是一个普通的也还是会报错。

有的时候为了灵活,我们更加希望有一块内存是我们控制的

1
char *mem = new char[1000*sizeof(bullet)]{};

这块内存,就像子弹的弹夹,发射了之后还可以填充周而复始,就在这个弹夹里操作子弹,

1
2
3
4
5
6
void *bullet::operator new(size_t size, const char *txt){
bullet *dat = (bullet*)mem;
for (int i = 0; i < 1000; i++){
if (!dat[i].flag) return &dat[i];
}
}

内存自然是要去mem里借了,但是如何取决于填充空白部分,那么子弹类应该就有标签一样的属性表示它是否发射。

1
bool flag = true;

这也是flag的由来。那么为什么一开始要等于true,自然是表示未发射,至于改成false的时机

本意是放在delete的,但是delete释放前也会调用析构函数,那么放在析构函数是最为妥当的。

1
2
3
bullet::~bullet(){
flag = false;
}

如此一来基础模板就形成了:

1
2
3
4
5
6
7
8
9
10
11
bullet *shota1 = new ("it's test") bullet;
bullet *shota2 = new ("it's test") bullet;
bullet *shota3 = new ("it's test") bullet;
bullet *shota4 = new ("it's test") bullet;
bullet *shota5 = new ("it's test") bullet;

std::cout << shota1 << std::endl;
std::cout << shota2 << std::endl;
std::cout << shota3 << std::endl;
std::cout << shota4 << std::endl;
std::cout << shota5 << std::endl;

首先地址是连贯的,相差0x14,看内存成员算大小。

然后注意先注释掉之前写的delete的内容不然清除了有点问题。

1
2
3
4
5
6
7
8
9
10
11
12
bullet *shota1 = new ("it's test") bullet;
bullet *shota2 = new ("it's test") bullet;
bullet *shota3 = new ("it's test") bullet;
delete shota1;
bullet *shota4 = new ("it's test") bullet;
bullet *shota5 = new ("it's test") bullet;

std::cout << shota1 << std::endl;
std::cout << shota2 << std::endl;
std::cout << shota3 << std::endl;
std::cout << shota4 << std::endl;
std::cout << shota5 << std::endl;

然后模拟发射了一颗子弹,让其它的子弹补上来。


结语

new和delete是配套的。
当你重载了new,相对应的就要重载delete。
不然你new的方式不同,用默认的delete不一定能释放,而且还会产生bug。

如果条件允许,你重载了一种就要把其他几种都重载改变。如果真的不想或者用不到,最简单的就是把那种方式=delete;