【C++】new 和 delete

目录

一、new 操作符

1. 工作原理

1.1. 内存分配

1.2. 对象构造

1.3. 返回指针

2. new 操作符的用法

2.1. 为单个对象分配内存

2.2. 为对象数组分配内存

二、delete 操作符

1. 工作原理

1.1. 对象析构

1.2. 内存释放

1.3. 指针置空(可选)

2. delete 操作符的用法

2.1. 释放单个对象的内存

2.2. 释放对象数组的内存

三、注意事项

3.1. 匹配使用

3.2. 异常安全

3.3. 内存泄漏

3.4. 野指针

3.5. 重复释放

3.6. 内存分配失败

3.7. 自定义内存管理

3.8. 性能考虑

四、使用示例

4.1. 为单个对象分配和释放内存

4.2. 为对象数组分配和释放内存


在C++中,new 和 delete 是用于动态内存管理的两个关键操作符。它们允许程序在运行时请求和释放内存,这对于创建大小在编译时未知的对象或数组特别有用。下面详细介绍 new 和 delete 的工作原理、用法以及注意事项。

一、new 操作符

1. 工作原理

1.1. 内存分配

  • 当调用 new 操作符时,它首先会调用 C++ 的内存分配函数(通常是 operator new,但用户也可以重载这个函数以提供自定义的内存分配策略)。
  • operator new 会向堆(heap)请求足够的内存来存储指定类型的一个或多个对象。如果内存分配成功,它会返回一个指向该内存的指针;如果失败,则抛出 std::bad_alloc 异常(除非指定了nothrow版本)。

1.2. 对象构造

  • 在内存分配成功后,new 操作符接着会调用对象的构造函数来初始化分配的内存。对于单个对象,它直接调用该对象的构造函数;对于对象数组,它调用默认构造函数(无参构造函数)来初始化数组中的每个元素。
  • 如果构造函数抛出异常,则 new 操作符会捕获这个异常,并使用 operator delete 释放之前分配的内存,然后重新抛出异常。

1.3. 返回指针

  • 一旦对象被成功构造,new 操作符就会返回指向该对象的指针。

2. new 操作符的用法

new 操作符用于在堆(heap)上分配内存,并可能调用对象的构造函数来初始化内存。new 有两种形式:为单个对象分配内存和为对象数组分配内存。

2.1. 为单个对象分配内存

ClassName* pointer = new ClassName(args);

这里,ClassName 是要创建的对象类型,args 是传递给对象构造函数的参数(如果构造函数不需要参数,则省略)。new 操作符首先为 ClassName 类型的对象分配足够的内存,然后调用该类型的构造函数(如果有的话),并返回指向新分配内存的指针。

2.2. 为对象数组分配内存

ClassName* array = new ClassName[size];

这里,size 指定了要分配的数组的元素数量。new 操作符为 size 个 ClassName 类型的对象分配内存,但只调用默认构造函数(无参构造函数)来初始化这些对象。如果没有默认构造函数,则编译失败。

二、delete 操作符

1. 工作原理

1.1. 对象析构

  • 当调用 delete 或 delete[] 操作符时,它首先会调用指向对象的指针所指向的对象的析构函数(如果有的话)。对于 delete,它只调用单个对象的析构函数;对于 delete[],它会遍历数组中的每个对象并调用它们的析构函数。

1.2. 内存释放

  • 析构函数执行完毕后,delete 或 delete[] 操作符会调用 C++ 的内存释放函数(通常是 operator delete 或 operator delete[],但同样可以重载)。
  • operator delete 或 operator delete[] 会将之前分配的内存归还给堆,以便后续的内存分配请求可以使用。

1.3. 指针置空(可选)

  • 虽然 delete 或 delete[] 操作符本身不会将指针置为 nullptr,但这是一个良好的编程实践,以避免野指针的出现。程序员应该在 delete 或 delete[] 调用后立即将指针设置为 nullptr

2. delete 操作符的用法

delete 操作符用于释放之前通过 new 分配的内存。与 new 类似,delete 也有两种形式:用于单个对象和对象数组。

2.1. 释放单个对象的内存

delete pointer;

这里,pointer 是指向之前通过 new 分配的内存的指针。delete 操作符首先调用对象的析构函数(如果有的话),然后释放与该对象关联的内存。

2.2. 释放对象数组的内存

delete[] array;

注意,对于通过 new[] 分配的对象数组,必须使用 delete[] 来释放内存。delete[] 操作符会遍历数组中的每个对象,并调用它们的析构函数(如果有的话),然后释放整个数组的内存。

三、注意事项

在C++中使用newdelete进行动态内存管理时,有几个重要的注意事项需要牢记,以避免内存泄漏、野指针、重复释放等常见问题。以下是使用newdelete时的一些关键注意事项。

3.1. 匹配使用

  • 确保为每个new表达式(无论是单个对象还是对象数组)都使用相应的deletedelete[]表达式来释放内存。不匹配使用(如使用delete来释放new[]分配的内存)会导致未定义行为,通常表现为程序崩溃或数据损坏。

3.2. 异常安全

  • 当在构造函数中抛出异常时,new表达式会自动调用operator delete来释放已分配但尚未初始化的内存。然而,如果你在new表达式之后但在对象完全构造之前执行了其他可能抛出异常的代码,你需要确保这些异常被妥善处理,以避免内存泄漏。
  • 使用智能指针(如std::unique_ptrstd::shared_ptr)可以自动管理内存,并减少因异常而导致的内存泄漏风险。

3.3. 内存泄漏

  • 忘记释放通过new分配的内存会导致内存泄漏。确保每个new表达式都有一个对应的deletedelete[]表达式,并且在程序退出之前释放所有分配的内存。
  • 使用RAII(Resource Acquisition Is Initialization)原则,通过对象生命周期来管理资源(包括内存)。当对象被销毁时,其析构函数可以自动释放分配的资源。

3.4. 野指针

  • 释放内存后,指针仍然指向原来的内存地址,此时该指针变为野指针。访问野指针是未定义行为,可能导致程序崩溃或数据损坏。在释放内存后,将指针设置为nullptr是一个好习惯,这有助于避免野指针问题。

3.5. 重复释放

  • 尝试对已经释放的内存再次调用deletedelete[]会导致未定义行为。这通常发生在复杂的指针操作或错误的内存管理策略中。确保每个内存块只被释放一次。

3.6. 内存分配失败

  • new表达式可能会因为内存不足而失败,此时它会抛出std::bad_alloc异常(除非使用了nothrow版本)。确保你的代码能够妥善处理这种异常,避免程序因未捕获的异常而异常终止。

3.7. 自定义内存管理

  • 如果你需要自定义内存分配和释放策略,可以通过重载operator newoperator new[]operator deleteoperator delete[]来实现。然而,这需要非常小心,以确保你的自定义实现不会引入新的内存管理问题。

3.8. 性能考虑

  • 虽然newdelete提供了灵活的内存管理能力,但它们也可能对性能产生影响。频繁地分配和释放小对象可能会导致内存碎片和性能下降。在性能敏感的应用中,考虑使用内存池或其他优化技术来减少内存分配和释放的开销。

通过遵循这些注意事项,你可以更安全、更有效地使用C++中的newdelete操作符进行动态内存管理。然而,在可能的情况下,推荐使用智能指针和容器来自动管理内存,以减少内存泄漏和异常安全问题的风险。

四、使用示例

在C++中,new 和 delete 是用于动态内存分配和释放的基本操作符。下面我将给出一些使用 new 和 delete 的示例,包括为单个对象分配和释放内存,以及为对象数组分配和释放内存。

4.1. 为单个对象分配和释放内存

#include <iostream>  
  
class MyClass {  
public:  
    MyClass(int value) : value_(value) {} // 构造函数  
    ~MyClass() { // 析构函数  
        std::cout << "Destructor called for MyClass with value " << value_ << std::endl;  
    }  
  
    void display() const {  
        std::cout << "Value: " << value_ << std::endl;  
    }  
  
private:  
    int value_;  
};  
  
int main() {  
    // 使用 new 为 MyClass 的一个实例分配内存  
    MyClass* myObject = new MyClass(10);  
  
    // 使用对象  
    myObject->display();  
  
    // 释放内存  
    delete myObject;  
  
    // 注意:此时 myObject 变成了悬垂指针,应该将其设置为 nullptr  
    myObject = nullptr;  
  
    // 尝试访问已释放的对象(不推荐,仅用于说明)  
    // myObject->display(); // 这将导致未定义行为  
  
    return 0;  
}

4.2. 为对象数组分配和释放内存

#include <iostream>  
  
class MyClass {  
public:  
    MyClass(int value) : value_(value) {} // 构造函数  
    ~MyClass() { // 析构函数  
        std::cout << "Destructor called for MyClass with value " << value_ << std::endl;  
    }  
  
    void display() const {  
        std::cout << "Value: " << value_ << std::endl;  
    }  
  
private:  
    int value_;  
};  
  
int main() {  
    // 使用 new[] 为 MyClass 的一个数组分配内存  
    MyClass* myArray = new MyClass[5]{1, 2, 3, 4, 5};  
  
    // 遍历数组并使用对象  
    for (int i = 0; i < 5; ++i) {  
        myArray[i].display();  
    }  
  
    // 释放内存  
    delete[] myArray;  
  
    // 注意:此时 myArray 变成了悬垂指针,应该将其设置为 nullptr  
    myArray = nullptr;  
  
    // 尝试访问已释放的数组(不推荐,仅用于说明)  
    // myArray[0].display(); // 这将导致未定义行为  
  
    return 0;  
}

在这两个示例中,我们定义了一个简单的 MyClass 类,它有一个构造函数、一个析构函数和一个 display 方法。在 main 函数中,我们首先展示了如何为 MyClass 的一个实例分配内存,如何使用它,然后如何释放它。接着,我们展示了如何为 MyClass 的一个数组分配内存,如何遍历数组并使用其中的对象,然后如何释放整个数组的内存。

请注意,在释放内存后,我们将指针设置为 nullptr,这是一个好习惯,可以防止悬垂指针问题。然而,在 main 函数的末尾,由于 main 函数即将返回,所以实际上这一步是可选的,因为程序即将结束,所有局部变量(包括指针)都将被销毁。但在更复杂的程序中,特别是在长时间运行或包含多个返回点的函数中,将不再使用的指针设置为 nullptr 是一个很好的做法。

  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值