MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ delete []操作的正确使用场景

2024-03-141.1k 阅读

C++ delete []操作的基本概念

在C++ 中,delete [] 操作符用于释放通过 new [] 分配的动态数组内存。当我们使用 new [] 为一个数组分配内存时,C++ 运行时会在内存中为这个数组预留一块连续的空间,并返回指向这块空间首地址的指针。之后,当我们不再需要这个数组时,就必须使用 delete [] 来正确地释放这块内存,以避免内存泄漏。

例如,下面是一个简单的代码示例:

#include <iostream>

int main() {
    int* arr = new int[5];
    // 使用数组
    for (int i = 0; i < 5; i++) {
        arr[i] = i;
    }
    // 释放数组内存
    delete [] arr;
    return 0;
}

在上述代码中,首先使用 new int[5] 分配了一个包含5个 int 类型元素的数组,并将返回的指针赋值给 arr。之后,对数组元素进行了初始化操作。最后,使用 delete [] arr 释放了 arr 指向的动态数组内存。

delete []delete 的区别

单个对象与数组对象的释放

delete 用于释放单个动态分配的对象,而 delete [] 用于释放动态分配的数组对象。这两者的区别不仅仅在于语法形式,更重要的是它们在内存管理上的不同行为。

考虑如下代码:

#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor" << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destructor" << std::endl;
    }
};

int main() {
    MyClass* singleObj = new MyClass();
    MyClass* arrObj = new MyClass[3];

    delete singleObj;
    // 错误示范,应该使用delete [] arrObj;
    delete arrObj; 

    return 0;
}

在这个例子中,MyClass 类有一个构造函数和一个析构函数,用于在对象创建和销毁时输出相应的信息。singleObj 是通过 new MyClass() 分配的单个对象,应该使用 delete singleObj 来释放。而 arrObj 是通过 new MyClass[3] 分配的包含3个 MyClass 对象的数组,正确的释放方式是 delete [] arrObj。如果像上述代码中错误地使用 delete arrObj,会导致只有数组中第一个对象的析构函数被调用,其余两个对象的析构函数不会被调用,从而造成内存泄漏和未定义行为。

内存布局与释放机制

从内存布局的角度来看,当使用 new 分配单个对象时,内存中只包含该对象的实例数据。而使用 new [] 分配数组时,除了数组元素本身占用的内存空间外,C++ 运行时还会在数组头部存储一些额外的信息,例如数组的大小。

delete 操作符在释放单个对象时,直接根据对象的地址释放对应的内存块。而 delete [] 操作符在释放数组时,会先从数组头部获取数组的大小信息,然后按照数组元素的顺序依次调用每个元素的析构函数(如果元素类型是类类型),最后释放整个数组占用的内存块。

例如,假设我们有一个简单的 int 数组:

int* intArr = new int[10];

在内存中,intArr 指向的内存布局大致如下(简化示意): [数组大小信息(隐藏)][int元素1][int元素2]...[int元素10]

当执行 delete [] intArr 时,运行时会首先读取数组大小信息,然后不调用 int 类型的析构函数(因为 int 是基本类型,没有析构函数),直接释放整个内存块。

delete [] 的正确使用场景

动态数组的释放

最常见的使用场景就是释放通过 new [] 分配的动态数组。无论是基本数据类型数组还是类类型数组,只要是通过 new [] 分配的,就必须使用 delete [] 来释放。

#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name) : m_name(name) {
        std::cout << "Person constructor: " << m_name << std::endl;
    }
    ~Person() {
        std::cout << "Person destructor: " << m_name << std::endl;
    }
private:
    std::string m_name;
};

int main() {
    // 基本类型数组
    int* intArray = new int[5];
    for (int i = 0; i < 5; i++) {
        intArray[i] = i * 2;
    }
    delete [] intArray;

    // 类类型数组
    Person* people = new Person[3] {
        Person("Alice"),
        Person("Bob"),
        Person("Charlie")
    };
    delete [] people;

    return 0;
}

在上述代码中,intArrayint 类型的动态数组,peoplePerson 类类型的动态数组。对于这两个数组,都使用了 delete [] 进行正确的内存释放。在释放 people 数组时,会依次调用每个 Person 对象的析构函数。

与智能指针的配合使用

在现代C++ 编程中,智能指针被广泛用于自动管理动态内存,以避免手动内存管理带来的错误。std::unique_ptrstd::shared_ptr 都有针对数组的特化版本,它们在内部会正确地使用 delete [] 来释放数组内存。

#include <iostream>
#include <memory>

class MyData {
public:
    MyData() {
        std::cout << "MyData constructor" << std::endl;
    }
    ~MyData() {
        std::cout << "MyData destructor" << std::endl;
    }
};

int main() {
    // 使用std::unique_ptr管理数组
    std::unique_ptr<MyData[]> dataArray1(new MyData[3]);

    // 使用std::shared_ptr管理数组
    std::shared_ptr<MyData> dataArray2(new MyData[5], [](MyData* ptr) {
        delete [] ptr;
    });

    return 0;
}

在这个例子中,std::unique_ptr<MyData[]> 是专门用于管理 MyData 数组的智能指针类型,当 dataArray1 离开其作用域时,会自动调用 delete [] 来释放数组内存。对于 std::shared_ptr,由于其默认的删除器是针对单个对象的 delete,所以在管理数组时,需要自定义删除器,在自定义删除器中使用 delete [] 来确保数组内存的正确释放。

函数参数与返回值中的动态数组

当函数接受动态数组作为参数或者返回动态数组时,正确使用 delete [] 就显得尤为重要。

#include <iostream>

int* createIntArray(int size) {
    int* arr = new int[size];
    for (int i = 0; i < size; i++) {
        arr[i] = i;
    }
    return arr;
}

void processArray(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

void deleteArray(int* arr) {
    delete [] arr;
}

int main() {
    int* myArray = createIntArray(5);
    processArray(myArray, 5);
    deleteArray(myArray);

    return 0;
}

在上述代码中,createIntArray 函数分配并返回一个动态数组,processArray 函数对数组进行处理,deleteArray 函数负责释放数组内存。在 main 函数中,调用 createIntArray 获得数组指针,调用 processArray 处理数组,最后调用 deleteArray 使用 delete [] 释放数组内存,确保了内存的正确管理。

错误使用 delete [] 的情况及后果

使用 delete 代替 delete []

正如前面提到的,如果使用 delete 来释放通过 new [] 分配的数组,会导致只有数组中第一个元素(如果是类类型)的析构函数被调用,其余元素的析构函数不会被调用,从而造成内存泄漏。

#include <iostream>

class Resource {
public:
    Resource() {
        std::cout << "Resource constructor" << std::endl;
    }
    ~Resource() {
        std::cout << "Resource destructor" << std::endl;
    }
};

int main() {
    Resource* resources = new Resource[3];
    // 错误:使用delete代替delete []
    delete resources; 

    return 0;
}

在这个例子中,Resource 类的析构函数只会被调用一次(针对数组的第一个元素),其余两个 Resource 对象的析构函数不会被调用,导致内存泄漏。

使用 delete [] 释放非数组指针

如果使用 delete [] 来释放不是通过 new [] 分配的指针,同样会导致未定义行为。

#include <iostream>

class MyObject {
public:
    MyObject() {
        std::cout << "MyObject constructor" << std::cout;
    }
    ~MyObject() {
        std::cout << "MyObject destructor" << std::cout;
    }
};

int main() {
    MyObject* obj = new MyObject();
    // 错误:对单个对象使用delete []
    delete [] obj; 

    return 0;
}

在这个例子中,obj 是通过 new MyObject() 分配的单个对象,使用 delete [] obj 会导致未定义行为,可能会破坏内存布局,引发程序崩溃等问题。

多次释放同一数组指针

多次使用 delete [] 释放同一个数组指针也是错误的行为,这会导致双重释放错误,同样会引发未定义行为。

#include <iostream>

int main() {
    int* arr = new int[5];
    delete [] arr;
    // 错误:再次释放已释放的指针
    delete [] arr; 

    return 0;
}

在上述代码中,第一次 delete [] arr 已经释放了数组内存,第二次再调用 delete [] arr 会导致未定义行为,可能会使程序崩溃。

确保 delete [] 正确使用的方法

代码审查

在团队开发中,代码审查是发现和避免 delete [] 使用错误的重要手段。通过代码审查,其他开发者可以检查代码中动态内存分配和释放的地方,确保 new []delete [] 正确配对使用。例如,审查代码时关注函数参数和返回值中动态数组的处理,以及对象生命周期内动态数组的管理。

使用智能指针

如前文所述,使用智能指针(如 std::unique_ptrstd::shared_ptr)可以有效地避免手动内存管理错误,包括 delete [] 使用不当的问题。智能指针在对象销毁时会自动调用正确的删除操作,无论是单个对象还是数组对象。在现代C++ 编程中,应尽量优先使用智能指针来管理动态内存。

遵循命名和编码规范

制定并遵循良好的命名和编码规范也有助于确保 delete [] 的正确使用。例如,可以采用特定的命名约定来标识动态数组指针,如在指针变量名中包含 “Array” 或 “Arr” 等关键字,这样在阅读和编写代码时可以更清晰地识别哪些指针指向动态数组,从而正确地使用 delete [] 进行释放。

同时,在编码规范中明确规定动态内存分配和释放的操作流程,例如要求在函数内部,动态分配的内存必须在函数结束前释放,并且要正确匹配 new []delete [],这样可以减少开发过程中因不规范操作导致的错误。

总结 delete [] 操作的要点

  1. 匹配使用delete [] 必须与 new [] 配对使用,用于释放通过 new [] 分配的动态数组内存。
  2. 类类型数组:对于类类型的动态数组,delete [] 会依次调用每个数组元素的析构函数,确保资源的正确释放。
  3. 智能指针辅助:在现代C++ 中,优先使用智能指针(如 std::unique_ptrstd::shared_ptr 的数组特化版本)来管理动态数组内存,避免手动使用 delete [] 带来的错误。
  4. 避免错误操作:避免使用 delete 代替 delete [] 释放数组,避免使用 delete [] 释放非数组指针,以及避免多次释放同一数组指针。

正确使用 delete [] 操作对于C++ 程序的内存管理至关重要,能够有效避免内存泄漏和未定义行为,提高程序的稳定性和可靠性。在实际编程中,开发者应深入理解 delete [] 的使用规则,并结合智能指针等工具,确保动态内存的正确管理。