[C++游戏开发底子]:基于new和delete的动态内存分配
讨论动态内存分配之前,先看看C++中前面两种种根本的内存分配范例:[*]静态内存分配用于静态和全局变量。这类变量的内存是在步伐运行时一次性分配的,并一连整个步伐的生命周期。
[*]主动内存分配用于函数参数和局部变量,这类变量的内存是在进入相干作用域时实验分配的,并在退出作用域时开释,根据需求可以多次举行。
[*]至于第三点,就是本文即将讨论的主题:动态内存的分配。
上面两种分配方式有两个共同点:
[*]变量/数组的巨细必须在编译时已知
[*]内存分配和开释是主动举行的
在大多数情况下,如许的逻辑大概没什么大标题。但是存在一些情况比力破例,好比,假设我们须要用一个字符串来生存某个人的名字,但是在输入竣事之前,我们不知道它们的名字叫具体会有多长,同样的原理,好比我们从磁盘读取未知巨细的数据记载时,这个记载在读取竣事之前也是未知数。
在游戏开发中,好比游戏的中的怪物数量大概会随着时间厘革,如许的数据对我们来说也是动态厘革的,这就导致了 编译时已知这个条件很难满意。
你大概会想到使用提前预分配指定巨细的容量来办理这些标题,也不是说不可行,但是存在空间浪费和内存溢出的风险。
好比说,假如我们为每个名字分配 25 个字符的空间,但匀称名字长度只有 12 个字符,我们就使用了现实所需的两倍以上的空间。
大多数通例变量(包罗固定命组)都是在称为堆栈的内存部门中分配的。步伐可用的堆栈内存通常相称小,好比作为游戏开发中工具的Visual Studio 默认的堆栈巨细为 1MB。假如你凌驾这个数字,就会发生堆栈溢出,使用体系大概会关闭步伐。
对于许多步伐来说,将内存限定在1MB的范围内是一个值得思索的标题,尤其是在图形开发范畴。
单个变量的动态内存分配
要动态的分配单个变量,使用new运算符来实现:
new int;
new 运算符使用该内存创建对象,然后返回一个包罗已分配内存所在的指针。通常,我们会将返回值赋给我们的指针变量,以便以后访问分配的内存。
int* ptr {new int};
然后使用解引用来访问内存,举行值的分配。
*ptr = 9;
注意,由于堆分配对象须要先从指针获取对象的所在,然后再通过所在获取对象的值。这比访问栈分配的对象要慢一些,使用栈举行分配时,编译器知道栈分配对象的所在,可以直接访问该所在以获取值。
动态内存分配的根本工作原理
盘算机拥有肯定的内存供应用步伐使用。当你运行一个应用步伐时,使用体系会将该应用步伐加载到部门内存中。
应用步伐使用的这部门内存被分别为差异的地区,每个地区负担者差异的任务,此中一个地区存放的便是你的步伐代码。另一个地区用来包管步伐正常的运作,好比,记载函数调用的情况,创建和销魂全局和局部变量等等。
只管云云,一样平常情况下照旧会有肯定的内存处于空闲状态,等候着步伐的使用哀求。 当你举举措态内存分配时,就是在哀求使用体系这部门空闲的内存,渴望可以从中哀求一部门用来给你的步伐使用.假如体系内存满意要求,那么它就会根据你的哀求返回一个内存所在给你的步伐。
从现在开始,你的步伐便拥有并可以随意使用这部门内存了,当步伐不在须要时,还可以将这部门内存归还给使用体系以便随时待命,等候其他步伐的使用哀求。
与静态大概主动内存差异,动态分配的内存是由步伐本身负责申请和开释的,也就是说,动态内存的管理权不再由使用体系控制,而是转交给了步伐。
https://i-blog.csdnimg.cn/img_convert/3ef1baef061b61bfb9ad0cca9158740e.gif
核心小结
栈上对象的分配和开释是主动举行的。
我们不须要手动处理处罚内存所在 —— 编译器天生的代码会主动完成这些工作。
堆上对象的分配和开释则不是主动完成的。
我们必须手动加入。也就是说,我们须要一种明白的方法来引用某个特定的堆对象,以便在恰当的时间手动烧毁它。
这种引用堆对象的方式,就是通过内存所在。
当我们使用 new 使用符时,它会返回一个指针,指向新分配对象的内存所在。
我们通常会把这个返回值生存在一个指针变量中,如许之后就可以通过这个所在访问对象,并在须要时烧毁它。
当你动态分配一个变量时,可以通过直接初始化大概同一初始化来初始化这个变量:
int* ptr1 {new int(6)};
int* ptr2 {new int {6}};
当我们动态分配的变量使用竣事后,须要明白的告诉C++开释这部门内存以便于内存重用。对于单个变量,可以使用delete关键字的非数组情势来完成开释。
delete ptr;
ptr = nullptr;
注意:
[*]delete运算符现实上并没有删除任何东西,它只是将内存归还给使用体系。使用体系随后可以自由地将这些内存重新分配给另一个应用步伐。
[*]固然语法上看起来像是在删除一个变量,但现实上并不是如许!指针变量仍然具有之前的作用域,而且可以像其他变量一样被重新赋值(比方赋值为 nullptr)。
[*]delete 只能用来开释通过 new(或 new[])动态分配的内存。假如你对一个平常变量的指针或已经被开释的指针使用 delete,大概会导致步伐瓦解、未界说活动或内存粉碎等严峻结果。
C++ 并不包管被开释的内存内容会发生什么厘革,也不包管被删除的指针值会酿成什么样。
在大多数情况下,被归还给使用体系的内存中仍然生存着原来的数据,而谁人指针仍然指向已经被开释的内存所在。
指向已开释内存的指针被称为 悬空指针(dangling pointer)。对悬空指针举行解引用或再次删除,会导致未界说活动(undefined behavior)。
#include <iostream>
int main()
{
int* ptr{ new int }; // 动态分配一个整数的内存
*ptr = 7; // 将一个值写入这块内存
delete ptr;
// 将这块内存归还给操作系统。此时 ptr 成为悬空指针(dangling pointer)
std::cout << *ptr; // 解引用一个悬空指针会导致未定义行为(undefined behavior)
delete ptr;
// 尝试再次释放这块已释放的内存也会导致未定义行为
return 0;
}
在上面的步伐中,之前分配给内存的值 7 大概仍然存在,但也有大概该内存所在的值已经发生厘革。
别的,也有大概这块内存已经被分配给了其他应用步伐(或使用体系自身使用),假如你实验访问这块内存,使用体系大概会因此制止步伐。
假如多个指针指向同一块已开释的内存(即多个悬空指针),任何一个指针的错误访问都会导致 未界说活动。
#include <iostream>
int main()
{
int* ptr{ new int{} }; // 动态分配一个整数的内存
int* otherPtr{ ptr }; // otherPtr 现在指向与 ptr 相同的内存地址
delete ptr;
// 将这块内存归还给操作系统。此时 ptr 和 otherPtr 都是悬空指针。
ptr = nullptr; // ptr 现在是一个 nullptr
// 然而,otherPtr 仍然是一个悬空指针!
return 0;
}
针对上面的标题,下面是几个规避发起:
[*]只管制止多个指针指向同一块动态分配的内存。假如无法制止,须要明白哪个指针拥有这块内存(方便后续的开释处理处罚),和哪些指针只能访问这块内存。
[*]当删除一个指针时,假如该指针在删除后没有立即超出作用域,应该将该指针设置为nullptr。
new使用符分配失败的情况
[*]在向使用体系哀求内存时,少少数情况下,使用体系大概没有富足的内存来满意哀求。
[*]默认情况下,假如 new 分配内存失败,会抛出一个 bad_alloc 非常。假如这个非常没有被精确处理处罚,步伐会由于未处理处罚非常而直接瓦解。
[*]许多情况下,抛出非常(大概让步伐瓦解)并不是我们渴望的效果,因此有一种 new 的替换情势,可以用来告诉 new 在无法分配内存时返回一个空指针。
这时间,可以通过在 new 关键字和分配范例之间添加常量 std::nothrow 来实现出现分配非常时返回一个空指针。
int* val {new(std::nothrow)int};
在上面的例子中,假如 new 无法分配内存,它将返回一个空指针,而不是已分配内存的所在。
请注意,假如你随后实验解引用这个指针,会导致未界说活动(最有大概的情况是,步伐瓦解)。因此,最佳实践是在使用分配的内存之前举行空指针的判定,确保它们现实上乐成了。
int* val { new (std::nothrow) int{} }; // 请求分配一个整数大小的内存
if (!val) // 处理 new 返回空指针的情况
{
// 在这里进行错误处理
std::cerr << "无法分配内存\n";
}
空指针和动态内存分配
空指针(设置为 nullptr 的指针)在处理处罚动态内存分配时特别有效。在动态内存分配的上下文中,空指针根本上体现“此指针没有分配到内存”。
这使得我们可以大概像如许有条件地分配内存:
// 如果 ptr 尚未分配内存,进行分配
if (!ptr)
ptr = new int;
[*]删除空指针不会产生任何影响,因此没有须要先查抄指针是否为空再举行 delete 使用。delete ptr;
会安全地忽略空指针,只有当指针指向有效内存时,才会实验内存开释。
if (ptr) // 如果 ptr 不是空指针
delete ptr;
// 删除它
// 否则什么都不做
上面的写法是多余的,可以像下面如许写:
delete ptr;
最佳实践:
对空指针使用delete是允许的,而且不须要对delete语句举行条件判定。
内存走漏
[*]当你使用 new 动态分配内存时,这些内存会不绝保持分配状态,直到你显式地使用 delete 开释它。纵然函数实验完毕,内存也不会主动开释,直到你手动开释它大概步伐竣事时,使用体系接纳内存。
[*]指针作为局部变量,它的作用域是有限的,即它会随着函数的实验竣事而失效。但是,指针指向的动态内存(假如没有开释)会继承存在,直到你显式开释它。这种指针生命周期与动态内存生命周期的不匹配大概导致标题,好比内存走漏或悬空指针。
思量下面函数:
void doSomething()
{
int* ptr{ new int{} };
}
这个函数动态分配了一个整数的内存,但从未使用 delete 开释它。由于指针变量就像平常变量一样,当函数竣事时,ptr 将超出作用域。
而且由于 ptr 是唯一持有动态分配整数所在的变量,当 ptr 被烧毁时,动态分配的内存的所在就没有了。也就是说,步伐现在“丢失”了指向动态分配内存的所在。
因此,这个动态分配的整数不能被删除。
这就叫做 内存走漏。
内存走漏发生在你的步伐在没有开释内存的情况下失去了某个动态分配内存的所在。当发生这种情况时,步伐无法删除动态分配的内存,由于它已经不知道内存在那边了。
使用体系也无法使用这块内存,由于它被以为仍然在步伐中使用。
内存走漏在步伐运行时会占用可用内存,导致此步伐以及其他步伐都可用的内存镌汰。具有严峻内存走漏标题的步伐大概会斲丧全部可用内存,导致整个呆板运行迟钝乃至瓦解。
只有当步伐制止时,使用体系才气整理并“接纳”全部走漏的内存。
内存走漏在步伐运行时会占用可用内存,导致此步伐以及其他步伐都可用的内存镌汰。具有严峻内存走漏标题的步伐大概会斲丧全部可用内存,导致整个呆板运行迟钝乃至瓦解。
只有当步伐制止时,使用体系才气整理并“接纳”全部走漏的内存。
int value = 5;
int* ptr{ new int{} }; // 分配内存
ptr = &value; // 旧地址丢失,导致内存泄漏
可以通过在重新赋值之前删除指针来修复此标题:
int value{ 5 };int* ptr{ new int{} }; // 分配内存delete ptr;
// 将内存返回给使用体系ptr = &value; // 将指针重新赋值为 value 的所在通过第二次使用 new 分配内存并将所在赋给同一个指针 ptr,会覆盖掉原先存储的所在,从而丢失对第一次分配内存的引用,导致内存走漏。
int* ptr{ new int{} };
ptr = new int{}; // 旧地址丢失,导致内存泄漏
在重新分配内存之前,确保先开释掉原来的内存(使用 delete),以防止丢失原始内存所在并引发走漏。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]