1 内存管理基础
C++ 的内存管理主要涉及如何分配、使用和释放计算机内存,以确保程序的正确运行和性能优化。其重要任务是避免内存泄漏和野指针等问题。 C++ 的内存管理是一个涉及多个方面和复杂概念的重要主题。正确地管理内存对于编写高效、稳定和安全的程序至关重要。
1.1 什么是内存管理
C++ 的内存管理是指程序在运行时如何分配、使用和释放计算机内存的过程。内存管理对于任何编程语言来说都是非常重要的,因为它直接影响到程序的性能、稳定性和安全性。在C++中,内存管理主要涉及以下几个方面:
(1)内存分区
C++ 程序中的内存主要分为几个区域,包括堆(heap)、栈(stack)、全局/静态存储区(global/static storage)、常量存储区(constant storage)和自由存储区(free store,也叫动态内存区)。不同的区域有不同的用途和生命周期。
(2)动态内存分配
在 C++ 中,可以使用 new 关键字在堆上动态分配内存,使用 delete 关键字释放这些内存。这种分配方式允许程序在运行时根据需要分配和释放内存。
(3)资源管理
资源管理主要关注如何确保在不再需要资源时正确地释放它们,以防止内存泄漏。这通常通过智能指针(如 std::unique_ptr 、 std::shared_ptr 等)和RAII(资源获取即初始化,Resource Acquisition Is Initialization)技术来实现。
(4)内存泄漏
内存泄漏是指程序在动态分配内存后未能释放这些内存,导致可用内存逐渐减少。内存泄漏如果不加以控制,最终可能导致程序崩溃或系统资源耗尽。
(5)内存优化
内存优化是指通过合理的内存管理策略来减少内存使用,提高程序的运行效率。这可能包括使用更紧凑的数据结构、减少不必要的内存分配和复制、使用内存池等技术。
(6)多线程内存管理
在多线程环境中,内存管理变得更为复杂,需要处理线程间的数据共享和同步问题。这通常需要使用互斥锁、原子操作等同步机制来确保内存访问的安全性和一致性。
C++ 的内存管理是一个复杂的主题,需要程序员具备一定的专业知识和经验。计算机的内存是有限的资源,必须有效地分配和释放。如果不进行管理,程序可能会消耗所有可用内存(动态分配的内存如果不手动释放,会导致内存泄漏),导致系统资源耗尽,甚至程序崩溃。内存管理允许程序员显式地控制何时分配和释放内存,确保资源得到合理利用。另外,不正确的指针操作可能导致野指针问题,访问野指针会引起未定义行为,也是程序崩溃的常见原因。内存管理可以帮助程序员避免这些问题。因此,熟悉和掌握 C++ 的内存管理技术是编写高效、稳定和安全程序的关键。
1.2 内存分区
C++ 中的内存分区主要包括以下几个部分:
(1)栈存储区(Stack)
- 特点:栈内存是连续的内存空间,遵循后进先出(LIFO)的原则。
- 使用场景:局部变量的存储、函数调用的参数传递、返回地址的保存等。
- 分配与释放:栈内存的分配和释放是自动的,由编译器控制。当函数被调用时,局部变量和参数会被压入栈中;函数返回时,这些局部变量和参数会被自动弹出并释放。
- 大小限制:栈的大小通常受到编译器和操作系统的限制,超出限制可能导致栈溢出。
(2)堆存储区(Heap)
- 特点:堆内存是由 new 关键字动态分配的,大小在运行时确定,且不受栈大小的限制。
- 使用场景:动态数据结构(如动态数组、链表等)、需要生命周期超过当前函数调用的对象等。
- 分配与释放:通过 new 关键字分配堆内存,程序员需要显式地通过 delete 关键字释放这些内存。如果不释放,会造成内存泄漏。
- 管理复杂性:由于堆内存的分配和释放需要程序员手动管理,因此相对复杂,容易出现内存泄漏或野指针等问题。
(3)全局/静态存储区(Global/Static Storage)
- 特点:全局变量和静态变量在程序开始执行时分配内存,并在程序结束时释放。这些变量的生命周期与整个程序的执行周期相同。
- 使用场景:全局变量、静态变量、常量等。
- 分配与释放:由编译器自动分配和释放。在C++中,全局变量和静态变量不再区分为初始化的和未初始化的,它们都存储在同一个内存区域。
- 注意事项:由于全局变量和静态变量的生命周期很长,过度使用可能导致内存浪费或增加程序的启动时间。
(4)代码区(Code Segment)
- 特点:存储程序的二进制代码,包括函数体、常量等。
- 访问权限:该区域的内容是只读的,以防止程序意外地修改了其指令。
- 分配与释放:由编译器和操作系统管理,程序员通常不需要关心。
(5)自由存储区(Free Store)
- 特点:与堆存储区类似,但内存是通过 malloc 函数申请,通过 free 函数释放的。
- 使用场景:在一些 C 风格的编程场景中,可能会使用 malloc 和 free 来管理内存,尤其是在与 C 语言混合编程时。
- 管理复杂性:与堆存储区相似,需要程序员手动管理内存的申请和释放。
1.3 堆与栈的区别
堆(Heap)和栈(Stack)两种不同的内存区域,它们之间有着显著的区别:
(1)管理方式
栈:由编译器自动管理,无需程序员手工控制。当函数被调用时,局部变量和参数会被自动压入栈中;函数返回时,这些局部变量和参数会被自动弹出并释放。
堆:产生和释放由程序员控制。程序员需要通过 new 关键字动态地申请堆内存,并在适当的时机通过 delete 关键字释放这些内存。如果忘记释放,会造成内存泄漏。
(2)空间大小
栈:空间有限,大小通常受到编译器和操作系统的限制。超出限制可能导致栈溢出。
堆:空间相对较大,理论上只受限于系统的虚拟内存大小。因此,堆内存几乎没有限制。
(3)能否产生碎片
栈:不会产生碎片。因为栈是种先进后出的队列,内存分配和释放都是连续的。
堆:容易产生碎片。多次的 new/delete 操作会造成内存的不连续,从而形成大量的碎片。碎片过多会降低程序的性能。
(4)生长方向
堆:生长方向是向上的,即地址越来越大。
栈:生长方向是向下的,即地址越来越小。
(5)分配方式
堆:都是动态分配的。
栈:可以是静态分配和动态分配两种。但栈的动态分配由编译器进行释放,无需程序员手工实现。
(6)分配效率
栈:由于栈是机器系统提供的数据结构,计算机底层对栈提供支持,因此分配效率较高。分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令。
堆:分配效率相对较低。堆是由C/C++函数库提供的,其分配机制较为复杂。例如,为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间。如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间。
2 资源管理与所有权
在 C++ 中,资源管理和所有权是两个紧密相关的概念,它们共同决定了如何有效地管理程序中的资源,如内存、文件句柄、网络连接等。资源管理是指程序如何分配、使用和释放这些资源,而所有权则涉及哪个对象或代码块负责这些资源的生命周期。
2.1 资源管理与所有权的基础概念
资源管理
资源管理主要是负责管理各类资源的生命周期(资源的生命周期指的是从资源被创建或获取到资源被释放或销毁的整个过程),在 C++ 中资源管理主要涉及到以下两个方面:
(1)内存管理
内存管理包括动态内存分配(通过 new 和 delete )和栈内存管理(通过作用域和自动变量)。正确的内存管理可以防止内存泄漏和野指针等问题。
(2)其他资源管理
除了内存之外,资源管理还包括文件操作、网络连接、线程管理、锁和其他系统资源。这些资源也需要被正确地创建、使用和释放。
资源的所有权
资源的所有权指的是哪个实体(如对象或函数)负责资源的管理和释放。在 C++ 中,资源的所有权可以通过多种方式来表达,包括使用智能指针、自定义析构函数、RAII(Resource Acquisition Is Initialization)等。
资源的所有权可以是显式的或隐式的。显式所有权通常意味着通过智能指针或其他资源管理机制来明确指定哪个对象或实体负责资源的释放。隐式所有权则意味着资源的管理和释放不是通过明确的机制来指定的,而是依赖于对象的生命周期或程序的执行流程。
(1)显式所有权
- 智能指针: C++11 及其后续版本提供了智能指针,如 std::unique_ptr、std::shared_ptr 和 std::weak_ptr,来提供显式的所有权模型。这些智能指针负责自动管理资源的生命周期,确保在智能指针被销毁时,其所指向的资源也被正确地释放。
自定义析构函数:通过为类定义析构函数,程序员可以显式地指定在对象生命周期结束时如何释放资源。这是一种显式的所有权表达方式。 - RAII:资源获取即初始化(RAII)是一种将资源的获取和释放与对象的构造和析构过程紧密结合的技术。这也是一种显式的所有权模型,因为它明确地将资源的管理与对象的生命周期绑定在一起。
(2)隐式所有权
- 裸指针:使用裸指针(raw pointers)时,资源的所有权是隐式的。程序员需要手动管理资源的生命周期,包括分配和释放。这增加了出错的可能性,如资源泄漏或野指针。
- 局部对象:局部对象的生命周期是隐式的,它们在定义它们的代码块内有效。当局部对象离开作用域时,它们的析构函数会被自动调用,从而释放资源。然而,这仍然是一种显式的所有权形式,因为析构函数的调用是明确的。
- 全局对象和静态对象:全局对象和静态对象的生命周期也是隐式的,它们在程序的整个执行期间都存在。这些对象的资源释放通常是在程序结束时进行的,这也是一种隐式的所有权形式。
总体而言,显式所有权通常更安全,因为它减少了出错的可能性,并提供了更清晰的资源管理语义。智能指针和RAII是C++中常用的显式所有权管理机制。然而,隐式所有权在某些情况下可能更方便或更符合特定的编程需求。在使用隐式所有权时,程序员需要格外小心,以确保资源的正确管理和释放,避免资源泄漏和野指针等问题。
2.2 构造函数与析构函数中的资源管理
在 C++ 中,构造函数和析构函数是用于管理资源的重要机制,尤其是在涉及到动态内存分配、文件句柄、网络连接或其他需要显式释放的资源时。
构造函数中的资源管理
构造函数通常用于初始化对象的成员变量,并分配必要的资源。这些资源可能包括动态内存、文件句柄、网络连接等。如下为样例代码:
struct Resource{ };class MyResourceClass
{
public:// 构造函数,初始化资源 MyResourceClass() {// 分配动态内存 resource = new Resource();// 打开文件 file = fopen("test.txt", "r");// 其他资源初始化... }private:Resource* resource;FILE* file;// 其他资源...
};
在上面代码中,MyResourceClass 的构造函数分配了动态内存并打开了一个文件。这些都是需要在对象的生命周期内管理的资源。
析构函数中的资源管理
析构函数则用于释放构造函数中分配的资源。当对象被销毁时(例如,该对象超出了其作用域),析构函数会自动被调用。如下为样例代码:
struct Resource{ };class MyResourceClass {
public:// ...构造函数... // 析构函数,释放资源 ~MyResourceClass() {// 关闭文件 if (file) {fclose(file);file = nullptr;}// 释放动态内存 if (resource) {delete resource;resource = nullptr;}// 释放其他资源... }private:Resource* resource;FILE* file;// 其他资源...
};
在上面代码中,MyResourceClass 的析构函数关闭了文件并释放了动态内存。这是确保资源不会被泄露的关键步骤。
2.3 动态申请的资源管理
在 C++ 中,动态申请的资源管理主要涉及到动态内存分配,这通常是通过使用 new、delete、new[] 和 delete[] 操作符来完成的。为了确保这些资源在不再需要时能够被正确地释放,避免内存泄漏,可以采取以下几种策略:
(1)手动管理
最基础的方式是手动管理动态分配的资源。该管理方式可以使用上面提到的 “构造函数与析构函数中的资源管理” 来实现。这意味着在构造函数中使用 new(或 new[] )来分配资源,并在析构函数中使用 delete(或 delete[] )来释放这些资源。如下为样例代码:
class MyClass
{
public: MyClass() { // 动态分配内存 m_vals = new int[10]; } ~MyClass() { // 释放内存 delete[] m_vals; } private: int* m_vals;
};
(2)智能指针
使用智能指针可以自动管理动态分配的内存,从而避免手动管理内存的繁琐和错误。智能指针是 RAII(资源获取即初始化)原则的一个应用,它们会在适当的时候自动释放所管理的资源。
C++11 标准库提供了 3 种智能指针:
(1)std::unique_ptr:独占所有权的智能指针,当 unique_ptr 被销毁时,它所指向的对象也会被自动删除。
(2)std::shared_ptr:共享所有权的智能指针,允许多个 shared_ptr 指向同一个对象。当最后一个 shared_ptr 被销毁时,它所指向的对象才会被删除。
(3)std::weak_ptr:弱引用智能指针,它指向一个由 shared_ptr 管理的对象,但不增加对象的引用计数。主要用于解决 shared_ptr 之间的循环引用问题。
以 std::unique_ptr 为例,如下为样例代码:
#include <memory> class MyClass
{
public: MyClass() : m_vals(std::make_unique<int[]>(10)) { // 使用智能指针自动管理内存 } // 析构函数不需要显式定义,因为智能指针会自动释放内存 private: std::unique_ptr<int[]> m_vals;
};
在上面代码中,std::unique_ptr<int[]> 会负责在 MyClass 对象销毁时自动释放动态分配的内存。
(3)容器和算法
对于动态分配的资源,尤其是数组和集合,C++标准库提供了许多容器(如 std::vector、std::list、std::map 等)和算法(如 std::sort、std::find 等),它们内部已经实现了资源的自动管理。使用这些容器和算法可以减少手动管理资源的需要。
以 std::vector 为例,如下为样例代码:
#include <vector> class MyClass
{
public: MyClass() : m_vals(10) { // vector会自动管理其内部的动态数组 } // 析构函数不需要显式定义,因为vector会自动释放内存 private: std::vector<int> m_vals;
};
在上面代码中,std::vector<int> 会自动管理其内部动态分配的数组,包括内存的分配和释放。
(4)自定义资源管理类
对于更复杂的资源管理需求,也可以使用上面提到的 “构造函数与析构函数中的资源管理” 来实现。可以创建自定义的资源管理类,该类可以在构造函数中获取资源,并在析构函数中释放资源。这可以适用于不仅仅是内存管理的其他资源,比如文件句柄、网络连接等。
以自定义资源管理类来管理文件句柄为例,如下为样例代码:
class FileResource
{
public: FileResource(const std::string& filename) { // 打开文件获取句柄 file = fopen(filename.c_str(), "r"); if (!file) { // 处理文件打开失败的情况 } } ~FileResource() { // 关闭文件句柄 if (file) { fclose(file); } } private: FILE* file;
};
在上面代码中,FileResource 类在构造函数中打开文件并获取文件句柄,然后在析构函数中关闭文件句柄。使用这样的资源管理类可以确保文件句柄在不再需要时被正确地关闭。
总体而言,在 C++ 中管理动态申请的资源时,应该优先考虑使用智能指针、容器和算法等 RAII 技术,以简化资源管理并减少出错的可能性。在特殊情况下,如果需要更精细的控制,可以自定义资源管理类。
2.4 最高级所有者
在 C++ 中,最高级所有者通常与资源管理和智能指针相关。这个概念指的是负责释放资源的对象或实体,通常是最后一个持有资源引用的对象。在资源生命周期管理的上下文中,最高级所有者负责在不再需要资源时释放它,从而防止资源泄露。
在 C++ 中,有几种智能指针可以用来实现最高级所有者的概念:
(1)std::unique_ptr
这是一个独占所有权的智能指针,它负责释放其指向的对象。当 std::unique_ptr 离开其作用域或被重新赋值时,它所指向的对象会被自动删除。 std::unique_ptr 是实现最高级所有者概念的常用工具。
(2)std::shared_ptr
这是一个共享所有权的智能指针,允许多个 std::shared_ptr 实例共享同一个对象的所有权。当最后一个 std::shared_ptr 离开其作用域或被重新赋值时,它所指向的对象会被自动删除。在某些情况下, std::shared_ptr 也可以作为最高级所有者,尤其是在多个对象需要共享所有权的情况下。
(3)自定义删除器
智能指针允许你指定一个自定义删除器,它是一个可调用对象,用于释放智能指针所持有的资源。通过提供自定义删除器,可以控制资源的释放方式,实现更复杂的资源管理策略。
在最高级所有者的概念中,重要的是确保只有一个智能指针(或类似机制)在任何给定时间持有资源的所有权。这有助于避免资源泄露和重复释放的问题。通过合理地使用智能指针和其他 RAII 技术,可以确保资源在不再需要时被正确地释放,从而保持程序的健壮性和稳定性。
3 指针与内存管理
C++ 指针在内存管理中扮演着至关重要的角色,它们提供了直接操作内存的能力,使得程序员能够灵活地管理内存资源。然而,这种灵活性也带来了更高的责任和风险,因此深入理解与掌握指针的用法,对于代码的正确性和稳定性非常重要。
3.1 指针的基本概念
C++ 指针是存储内存地址的变量。在 C++ 中,每个变量都有一个与之关联的内存地址,而指针就是用来存储这些内存地址的变量。以下是 C++ 指针的基本概念:
指针的定义
指针变量的声明方式是在变量类型前加上一个星号(*)。例如,一个指向整数的指针可以声明如下:
int *ptr; // ptr 是一个指向整数的指针
这里,int 是指针所指向的数据类型,* 表示这是一个指针,而 ptr 是指针变量的名称。
指针的初始化
在定义指针之后,通常需要将其初始化为一个有效的内存地址。这通常是通过将某个变量的地址赋给指针来完成的。例如:
int a = 1;
int *ptr = &a; // ptr 指向 a 的内存地址
这里,&a 获取变量 a 的地址,并将其赋给指针 ptr。
指针的解引用
指针的解引用操作是通过在指针变量前使用星号(*)来完成的,它返回指针指向的值。例如:
int b = *ptr; // b 的值现在是 1,因为 ptr 指向 a,a 的值是 1
空指针
空指针是一个不指向任何有效内存地址的指针。在 C++11 及更高版本中,可以使用 nullptr 来表示空指针。
int *ptr2 = nullptr; // ptr2 是一个空指针
指针的运算
指针可以进行一些特定的算术运算,如加法和减法。这些运算通常用于遍历数组或操作内存中的连续区域。
int vals[5] = {1, 2, 3, 4, 5};
int *ptrVals = vals; // ptrVals 指向 vals 的第一个元素
int nextVal = *(ptrVals + 1); // nextVal 的值是 2
指针的类型
指针的类型必须与它所指向的变量的类型匹配。例如,一个 int 类型的指针不能用来指向一个 double 类型的变量。
指针的指针
指针的指针是一个指向指针的指针。例如:
int **pptr; // pptr 是一个指向 int 类型指针的指针
指针与数组
在 C++ 中,数组名可以被解释为指向数组第一个元素的指针。因此,可以使用指针来遍历和操作数组。
int vals[] = {1, 2, 3, 4, 5};
int *ptr = vals; // ptr 指向 vals 的第一个元素
for (int i = 0; i < 5; i++)
{std::cout << *(ptr + i) << " "; // 输出数组的每个元素
}
3.2 指针的加减运算
在 C++ 中,指针的加减运算是一种特殊的操作,用于在内存中移动指针的位置。这种运算通常用于遍历数组、操作连续的内存区域,或者在更底层的内存管理中。
指针加法
指针加法是将指针与一个整数相加,结果是一个新的指针,它指向原指针位置之后的第 N 个元素。这里的整数通常表示的是元素的数量,而不是字节的数量。如下为样例代码:
int vals[] = {1, 2, 3, 4, 5};
int *ptr = vals; // ptr 指向 vals 的第一个元素 // 指针加 1,指向下一个元素
ptr = ptr + 1; // 或者使用 ptr++
// 现在 ptr 指向 vals 的第二个元素,即值为 2 的元素 // 指针加 2,指向下两个元素之后的位置
ptr = ptr + 2; // 或者使用 p += 2
// 现在 ptr 指向 vals 的第四个元素,即值为 4 的元素
在指针加法中,编译器会自动考虑元素的大小。例如,如果 ptr 是一个指向 int 的指针,那么 ptr + 1 实际上是将 ptr 的值增加了 sizeof(int) 字节,而不是简单地增加 1。这样确保了指针移动到正确的位置。
指针减法
指针减法是将指针与一个整数相减,或者将两个指针相减。结果通常是一个整数,表示两个指针之间的距离。如下为样例代码:
int vals[] = {1, 2, 3, 4, 5};
int *ptr1 = vals; // ptr1 指向 vals 的第一个元素
int *ptr2 = vals + 3; // ptr2 指向 vals 的第四个元素 // 计算两个指针之间的距离
int offset = ptr2 - ptr1;
// offset 的值是 3,因为 ptr2 和 ptr1 之间相隔 3 个元素 // 指针与整数相减
ptr1 = ptr2 - 2; // 或者使用 ptr1 -= 2
// 现在 ptr1 指向 vals 的第二个元素,即值为 2 的元素
当两个指针指向同一个数组(或者更精确地说,指向同一块连续的内存区域)时,它们之间的减法运算结果是它们之间相隔的元素数量。如果指针不指向同一数组,或者指向了非连续的内存区域,这种减法运算的结果是未定义的。
指针运算和数组
指针运算在处理数组时尤其有用,因为数组名可以被解释为指向其第一个元素的指针。通过指针运算,可以轻松地遍历数组中的元素。如下为样例代码:
int vals[] = {1, 2, 3, 4, 5};
int *ptr = vals; // ptr 指向 vals 的第一个元素 // 使用指针运算遍历数组
for (int i = 0; i < 5; i++)
{ std::cout << *(ptr + i) << " "; // 输出数组的每个元素
}
在上面代码中,*(ptr + i) 等价于 vals[i],因为 vals[i] 实际上就是指针运算 vals + i 的解引用。
注意事项
(1)指针加减运算的结果必须指向有效的内存区域,否则可能会导致未定义行为,如访问违规内存或程序崩溃。
(2)指针加减运算不能用于空指针(nullptr)。
(3)指针的加减运算不会进行越界检查,因此程序员需要确保运算结果指向有效的内存地址。
3.3 指针的数组与数组的指针
在 C++ 中,指针和数组之间有着紧密的联系,但它们是不同的概念。理解它们之间的关系和差异对于掌握 C++ 的内存管理和指针操作至关重要。
指针的数组
指针的数组是指一个数组,其元素是指针类型。换句话说,它是一个包含多个指针的数组。每个指针可以指向不同类型的数据。如下为样例代码:
int *ptr1; // 一个指向整数的指针
double *ptr2; // 一个指向双精度浮点数的指针 // 指针的数组:包含两个指针的数组
void *ptrs[2];
ptrs[0] = ptr1; // 将第一个元素初始化为指向整数的指针 ptr1
ptrs[1] = ptr2; // 将第二个元素初始化为指向双精度浮点数的指针 ptr2
在上面代码中,ptrs 是一个包含两个指针的数组。第一个元素 ptrs[0] 是一个指向整数的指针,而第二个元素 ptrs[1] 是一个指向双精度浮点数的指针。
数组的指针
数组的指针是指一个指针,它指向一个数组的首个元素。在大多数情况下,数组的指针和指向数组首个元素的指针是等价的。数组的指针通常用于传递数组到函数中,或者用于操作数组。如下为样例代码:
int vals[5]; // 一个包含5个整数的数组
int (*pVals)[5]; // 一个指向包含5个整数的数组的指针 pVals = &vals; // 将 pVals 初始化为指向 vals 的指针
在上面代码中,pVals 是一个指向包含5个整数的数组的指针。它等价于 int *pVals = vals; ,因为数组名 vals 在大多数情况下会退化为指向数组首元素的指针。然而,数组指针 pVals 的类型明确表示了它指向的是一个具有特定大小的数组,而不仅仅是单个元素的指针。
3.4 动态内存分配与指针
动态内存分配是 C++ 中一种重要的内存管理技术,它允许程序在运行时根据需要分配和释放内存。与静态内存分配(如数组)不同,动态内存分配允许程序在运行时确定需要多少内存,并在不再需要时释放这些内存。这种灵活性使得动态内存分配在处理大型数据集、构建可变大小的数据结构以及实现某些算法时非常有用。
动态内存分配函数
C++ 提供了如下四个操作符来支持动态内存分配:
(1)new:用于分配内存并初始化对象。
(2)delete:用于释放new分配的内存。
(3)new[]:用于分配数组的内存并初始化对象。
(4)delete[]:用于释放new[]分配的内存。
动态内存分配与指针
在动态内存分配中,指针扮演着关键角色。当使用 new 或 new[] 分配内存时,它们会返回一个指向新分配内存的指针。随后便可以使用这个指针来访问和操作分配的内存。同样,当使用 delete 或 delete[] 释放内存时,需要传递一个指向要释放内存的指针。
(1)使用 new 和 delete 的样例代码:
// 使用new分配内存
int* ptr = new int; // 使用分配的内存
*ptr = 1; // 输出内存中的值
std::cout << "value: " << *ptr << std::endl; // 使用delete释放内存
delete ptr;
ptr = nullptr; // 将指针设置为nullptr,避免悬挂指针
在上面代码中,new int 分配了足够存储一个 int 类型值的内存,并返回指向这块内存的指针。 delete 函数则负责释放这块内存。
(2)使用 new[] 和 delete[] 的样例代码:
// 使用new[]分配数组内存
int* vals = new int[10]; // 使用分配的内存
for (int i = 0; i < 10; i++)
{ vals[i] = i + 1;
} // 输出数组中的值
for (int i = 0; i < 10; i++)
{ std::cout << vals[i] << " ";
}
std::cout << std::endl; // 使用delete[]释放数组内存
delete[] vals;
vals = nullptr; // 将指针设置为nullptr,避免悬挂指针
在上面代码中,new int[10] 分配了足够存储 10 个 int 类型值的数组内存,并返回指向数组首元素的指针。 delete[] 函数则负责释放整个数组的内存。
注意事项
(1)内存泄漏:如果忘记释放使用 new 或 new[] 分配的内存,会导致内存泄漏。内存泄漏会消耗程序可用的内存资源,可能导致程序性能下降或崩溃。
(2)悬挂指针:释放内存后,如果不将指针设置为nullptr,它将成为悬挂指针。悬挂指针指向的内存已经被释放,但指针仍然保留原来的地址。解引用悬挂指针是未定义行为,通常会导致程序崩溃。
(3)初始化和析构:使用 new 分配的内存时,会调用对象的构造函数进行初始化。使用 delete 释放内存时,会调用对象的析构函数进行清理。
(4)类型匹配:释放内存时,必须使用与分配内存时相同的 delete 或 delete[] 。不匹配的释放操作(例如,使用 delete 释放 new[] 分配的内存)可能导致未定义行为。
(5)异常安全性:在分配内存后,如果在使用内存之前发生异常,必须确保在异常处理程序中释放内存,以避免内存泄漏。这通常可以通过智能指针(如 std::unique_ptr 或 std::shared_ptr)来实现,它们可以在作用域结束时自动释放内存。
3.5 内存泄漏与野指针
C++ 内存泄漏和野指针是两种常见的内存管理问题,它们可能导致程序不稳定、性能下降,甚至崩溃。
(1)内存泄漏(Memory Leak)
定义:
内存泄漏是指程序在动态分配内存后,未能正确释放这些内存,导致内存占用持续增长,最终可能耗尽系统资源。
产生原因:
- 忘记释放内存:使用 new 或 new[] 分配的内存必须使用 delete 或 delete[] 释放。如果忘记释放,这些内存将永远不会被操作系统回收。
- 动态分配的内存丢失引用:如果一个指针指向动态分配的内存,但之后这个指针被覆盖或丢失,那么这块内存就无法被释放,因为它已经无法被访问。
- 循环引用:两个或多个对象相互引用,且它们的析构函数都没有正确解除这些引用,导致它们无法被释放。
影响:
内存泄漏会导致程序占用的内存不断增长,最终可能耗尽可用内存,导致程序崩溃或系统不稳定。
避免方法:
- 及时释放内存:确保每次使用 new 或 new[] 分配内存后,都在合适的时机使用 delete 或 delete[] 释放内存。
- 使用智能指针:智能指针(如 std::unique_ptr、std::shared_ptr)可以自动管理内存,当智能指针离开作用域时,它们会自动释放内存。
- 使用检测工具:使用内存泄漏检测工具,如Valgrind,可以帮助发现潜在的内存泄漏问题。
(2)野指针(Wild Pointer)
定义:
野指针是指指向无效内存区域的指针。这些指针可能指向已经被释放的内存,或者从未被初始化,或者指向了非法的内存区域。
产生原因:
- 指针未初始化:指针在使用前必须被初始化,否则它将指向一个随机的内存地址。
- 释放内存后继续使用:如果一个指针指向的内存被释放,但指针没有被置为 nullptr ,那么这个指针就变成了野指针。
- 越界访问:如果指针访问了数组之外的内存,那么这个指针也会变成野指针。
影响:
野指针的使用通常会导致程序崩溃或未定义行为,因为它们可能指向已经被操作系统分配给其他程序的内存,或者指向不可访问的内存区域。
避免方法:
- 初始化指针:在使用指针之前,确保将其初始化为 nullptr 或有效的内存地址。
- 释放内存后重置指针:每次释放内存后,都将指针设置为 nullptr ,防止误用。
- 使用智能指针:智能指针在释放内存后会自动置为 nullptr ,可以有效避免野指针问题。
- 边界检查:在访问数组或指针时,始终确保它们在有效范围内。
总体而言,理解和管理好内存是编写健壮和高效的 C++ 程序的关键。通过避免内存泄漏和野指针,可以确保程序的稳定性和可靠性。