第 8 章 动态内存管理
1. 动态内存基础
● 栈内存 V.S. 堆内存
– 栈内存的特点:更好的局部性,用于语言的固有类型,对象自动销毁,由低到高开辟
– 堆内存的特点:运行期动态扩展,需要显式释放,由高到低开辟
● 在 C++ 中通常使用 new 与 delete 来构造、销毁对象
● 对象的构造分成两步:分配内存与在所分配的内存上构造对象;对象的销毁与之类似
● new 的几种常见形式
分配失败是不继续执行下面的代码的,所以在下面判断是否分配成功是检测不出分配失败的
– 构造单一对象 / 对象数组
– nothrow new //分配失败了不抛出异常
– placement new //已有内存,不需要额外分配,直接使用即可,谨慎使用
– new auto //
new 分配失败会抛出异常,
● new 与对象对齐
● delete 的常见用法
– 销毁单一对象 / 对象数组
– placement delete //vector,只销毁地址,不返还给系统
● 使用 new 与 delete 的注意事项
– 根据分配的是单一对象还是数组,采用相应的方式销毁,怎么分配怎么销毁
– delete nullptr
– 不能 delete 一个非 new 返回的内存
– 同一块内存不能 delete 多次 // 释放完他还是那块内存,只是不能再用了
● 调整系统自身的 new / delete 行为
– 不要轻易使用
cppreference : operator new, operator new[]
补充:常见的指针错误使用情况:
-
空指针引用(Null Pointer Dereference):当一个指针被解引用(即访问指针指向的内存)时,但指针的值为null或未初始化时,就会发生空指针引用错误。这种错误通常会导致程序崩溃或异常终止。
-
内存泄漏(Memory Leaks):内存泄漏指的是程序在动态分配内存后,没有释放该内存而导致内存无法再被使用。如果重复发生内存泄漏,程序的内存消耗会逐渐增加,最终可能导致系统资源耗尽。
-
误用释放的内存(Incorrect Use of Freed Memory):当一个内存块被释放后,如果程序继续使用该内存块或者对其进行写操作,就会发生误用释放的内存错误。这可能导致数据损坏、崩溃或安全漏洞。
-
缓冲区溢出(Buffer Overflow):缓冲区溢出指的是向一个缓冲区写入超过其容量的数据,导致数据溢出到相邻的内存区域。这种错误可能会破坏程序的内存结构,导致未定义的行为和安全漏洞。
-
重复释放内存(Double Free):当同一个内存块被多次释放时,就会发生重复释放内存错误。这可能导致程序访问无效的内存或崩溃。
-
悬挂指针(Dangling Pointer):悬挂指针是指指针仍然指向有效内存,但该内存已经被其他操作或释放修改。使用悬挂指针可能导致访问无效数据或出现未定义行为。
-
野指针(Dangling Pointer):野指针指的是指向已释放或无效内存的指针。当程序中的指针指向一个已经释放的内存块或者指向一个无效的地址时,就称之为野指针。野指针可能产生的原因包括指针没有被正确初始化、指针指向的对象已经被释放、指针超出了其作用域等。当使用野指针时,程序可能会出现未定义行为,包括崩溃、数据损坏或安全漏洞等问题。
-
僵尸指针(Zombie Pointer):僵尸指针是指指向已经被释放的内存的指针,但程序仍然继续使用它。当一个指针指向的内存块被释放后,指针没有被及时置为 null 或重新分配,导致指针称为僵尸指针。使用僵尸指针可能会导致类似野指针的问题,因为程序可能会尝试访问已释放的内存。
这些指针错误都是常见的编程错误,可能导致程序的不稳定性、内存泄漏、安全漏洞或数据损坏。在编写程序时,应该注意避免这些错误,并采取适当的指针使用和内存管理策略。
2. 智能指针
● 使用 new 与 delete 的问题:内存所有权不清晰,容易产生不销毁,多销毁的情况
● C++ 的解决方案:智能指针
– auto_ptr ( C++17 删除)
– shared_ptr / uniuqe_ptr / weak_ptr ( C++11 )
A、shared_ptr——基于引用计数的共享内存解决方案、类模板
Int* x(new int(3));
std::shared_ptr<int> x (new int(3));
std::shared_ptr<int> x(new int(3)); // 引用计数1
std::shared_ptr<int> y = x; // 引用计数 2
- 基本用法:先删除y,计数-1;再删除x,计数-1 ,引用计数为0时候,调用delete 收回地址。
std::shared ptr<int> fun() {
std::shared_ptr<int> res(new int(3)); return res;
}
int main()
{
std::shared_ptr<int>x=fun();
std::cout<< *x << endl;
}
- 通过智能指针获取普通指针使用:
.get() :
void fun2(int*x)
{
std::cout << *x << Std::endl:
}
int main()
{
std::shared_ptr<int>x = fun();
fun2(x.get());
}
.reset():
重新设置:如果已有关联,delete销毁后重新开辟空间;否则直接重新开辟空间
x.reset(new int(4));
void dummy(int*) {}
std::shared_ptr<int> fun()
{
static int res =3;
return std::shared_ptr<int> (&res, dummy);
}
int main()
{
std::shared_ptr<int> x = fun();
}
- 指定内存回收逻辑: new和delete相对较慢;内存池较快:从内存中先分割一块出来,使用后在还给内存池,减少new和delete提升性能。:
std::shared_ptr<类型>(&名字, 自定义delete);
- std::make_shared :
std::shared_ptr<int> x =std::make_share<int> (3);
引用计数和对象的内存会开辟到的尽量近一些
– 支持数组:
std: : shared_ptreint> x(new int[5]); // 删除的时候可能有问题
std::shared_ptr<T[]> // C++17 支持
auto x = std::make_shared<int[5]>(); // C++20 支持
– 注意: shared_ptr 管理的对象不要调用 delete 销毁! 因为它会自动销毁的,delete会导致内存销毁两次。
B、 unique_ptr——独占内存的解决方案,不可以拷贝,可以移动
- 基本用法:
std : :unique ptreint> x( new int(3));
std: :unique ptreint> y=x; // 不可以拷贝
std: :unique_ptr<int> y = std: :move(×); //可以移动
- 为 unique_ptr 指定内存回收逻辑
std:: unique ptr<int> funo)
std::unique_ptr<int> res (new int( 3));
return res;
)
int main(){
std:: unique_ptr<int> x = fun();
)
** **——防止循环引用而引入的智能指针
– 基于 shared_ptr 构造
– lock 方法
struct str
std: : shared_ptreStr> nei;-str{)
{
std: :cout << "~Str is calledin";
}
};
int main()
{
std::shared_ptr<Str> x(new Str);
std::shared ptr<Str> y(new Str);
x->nei = y;
y->nei = x; // 循环引用,产生 环形,无法释放
}
3. 动态内存的相关问题
● sizeof 不会返回动态分配的内存大小,只返回编译器相关大小
● 使用分配器( allocator )来分配内存,只分配,不构造!
std: :allocatorcint> al;
int* ptr = al.allocate(3);
deallocate // 内存回收
● 使用 malloc / free 来管理内存,属于C的方式,只分配不构造!不分配对其的内存。
需要参数输入分配大小,
● 使用 aligned_alloc 来分配对齐内存
● 动态内存与异常安全:
delete
之前异常可能会产生内存泄漏。解决方式:智能指针 shared_ptr
● C++ 对于垃圾回收的支持 :没有内置的垃圾回收机制,有6个相关函数:
补充:malloc / free和new/delete区别?
malloc
和free
是C语言中的内存分配和释放函数,而new
和delete
是C++语言中的内存分配和释放操作符。它们之间存在以下区别:
-
语法差异:
malloc
和free
是函数,需要使用函数调用的语法,而new
和delete
是操作符,使用类似于关键字的语法。// C语言中的malloc和free void* malloc(size_t size); void free(void* ptr); // C++语言中的new和delete type* new type; delete ptr;
-
类型安全:
malloc
返回void*
指针,需要手动进行类型转换,而new
操作符在分配内存时会自动进行类型推断,并返回正确类型的指针。同样,delete
操作符也会根据指针的类型自动释放内存。 -
构造函数和析构函数调用:
new
操作符在分配内存后会自动调用对象的构造函数,而delete
操作符在释放内存前会自动调用对象的析构函数。这使得使用new
和delete
的对象能够自动执行构造和析构的操作,方便对象的初始化和清理。// 使用new和delete创建和销毁对象 MyObject* obj = new MyObject(); // 调用MyObject的构造函数 delete obj; // 调用MyObject的析构函数 // 使用malloc和free分配和释放内存 MyObject* obj = (MyObject*)malloc(sizeof(MyObject)); free(obj);
-
数组分配:
new
操作符可以用于分配动态数组,而malloc
无法直接分配动态数组。当需要分配数组时,使用new[]
,并使用delete[]
进行释放。// 使用new[]和delete[]创建和销毁动态数组 int* arr = new int[5]; delete[] arr; // 使用malloc和free分配和释放内存 int* arr = (int*)malloc(5 * sizeof(int)); free(arr);
当内存分配失败时,可以采取以下处理方式:
-
检查返回值: 在使用
malloc
或new
进行内存分配后,应该检查返回的指针是否为NULL
,这表示内存分配失败。如果返回的指针为NULL
,则说明系统无法满足所需的内存空间,此时应该采取相应的错误处理措施。// 使用malloc进行内存分配 int* ptr = (int*)malloc(sizeof(int)); if (ptr == NULL) { // 内存分配失败,进行错误处理 } // 使用new进行内存分配 int* ptr = new int; if (ptr == nullptr) { // 内存分配失败,进行错误处理 }
-
抛出异常: 在C++中,可以使用异常机制来处理内存分配失败的情况。当
new
操作符无法分配所需的内存时,会抛出std::bad_alloc
异常,我们可以通过捕获该异常并进行相应的错误处理。try { // 使用new进行内存分配 int* ptr = new int; // 内存分配成功 } catch (const std::bad_alloc& e) { // 内存分配失败,进行错误处理 }
-
释放已分配的内存: 如果内存分配失败后,已经分配了一部分内存,但无法继续分配所需的额外内存,可以通过手动释放已分配的内存来回收资源。
int* ptr1 = new int; int* ptr2 = new int; // ... int* ptr3 = new int; if (ptr3 == nullptr) { // 内存分配失败,释放已分配的内存 delete ptr1; delete ptr2; // 进行错误处理 }
无论采取哪种处理方式,当内存分配失败时,必须确保对已分配的内存进行适当的释放,以避免内存泄漏和其他潜在的问题。
总的来说,new
和delete
是C++中更高级、更安全的内存分配和释放方式,它们提供了更多的语法特性和类型安全,并且可以自动处理对象的构造和析构函数。然而,在C语言中只能使用malloc
和free
进行内存分配和释放。如果在C++中使用malloc
和free
,则需要手动调用构造和析构函数,而且类型安全性较差。