0

0

如何正确使用new和delete操作符 动态内存分配与释放的最佳实践

P粉602998670

P粉602998670

发布时间:2025-08-13 17:24:02

|

1097人浏览过

|

来源于php中文网

原创

正确使用new和delete操作符的关键在于严格配对并区分单个对象与数组的分配,1. new用于动态内存分配,delete用于释放单个对象;2. new[]用于数组分配,delete[]用于释放数组;3. 释放后应将指针置为nullptr以避免悬空指针;4. 异常安全需特别注意,现代c++++推荐使用智能指针如std::unique_ptr和std::shared_ptr来自动管理资源,从而减少内存泄露和未定义行为的风险。

如何正确使用new和delete操作符 动态内存分配与释放的最佳实践

正确使用

new
delete
操作符,其核心在于理解它们是C++中用于在堆上进行动态内存分配与释放的底层工具。说白了,
new
负责找一块足够大的内存给你用,而
delete
则负责把这块内存还回去,避免浪费。关键是,你拿了就得还,而且要正确地还,不然就会出问题。

如何正确使用new和delete操作符 动态内存分配与释放的最佳实践

动态内存管理,尤其是在C++里,是个挺让人头疼的话题,因为它不像栈内存那样自动管理。你得亲力亲为,每次

new
一个对象或数组,就得记得用
delete
delete[]
来释放。这听起来简单,但实际项目里,一旦逻辑复杂起来,或者有异常抛出,就很容易忘掉,然后内存泄露就悄悄发生了。

解决方案

要正确使用

new
delete
,最基本的原则是:
new
delete
必须配对使用,并且要区分是分配单个对象还是数组。

如何正确使用new和delete操作符 动态内存分配与释放的最佳实践

分配单个对象:

// 分配一个 int 类型的内存,并用 10 初始化
int* p = new int(10); 
// 使用 p 指向的内存...
// 释放 p 指向的内存
delete p; 
p = nullptr; // 良好的习惯,防止悬空指针

分配对象数组:

如何正确使用new和delete操作符 动态内存分配与释放的最佳实践
// 分配一个包含 5 个 int 的数组
int* arr = new int[5]; 
// 使用 arr 指向的数组...
// 释放 arr 指向的数组
delete[] arr; // 注意这里是 delete[]
arr = nullptr;

关键点在于:

  1. 配对使用:
    new
    对应
    delete
    new[]
    对应
    delete[]
    。这是铁律,搞错了会导致未定义行为,轻则内存泄露,重则程序崩溃。
  2. 释放后置空: 这是一个好习惯。当内存被
    delete
    后,指针仍然可能指向那块已经无效的内存区域,成为“悬空指针”。将其置为
    nullptr
    可以有效避免后续误用。
  3. 异常安全: 这是最容易被忽略的。如果在
    new
    delete
    之间发生异常,
    delete
    可能永远不会被调用到,从而导致内存泄露。现代C++更倾向于使用RAII(Resource Acquisition Is Initialization)原则,即通过对象生命周期来管理资源,最典型的就是智能指针。

为什么
new
delete
用不好会出大问题?

这话说起来,其实就是内存管理里的那些老生常谈,但每次看到有经验的开发者在这上面翻车,都觉得有必要再强调一下。用不好

new
delete
,最直接的后果就是内存泄露(memory leak)和各种未定义行为。

内存泄露:这是最常见的问题。你

new
了一块内存,用完了,但是忘了
delete
它。这块内存就一直被你的程序“霸占”着,操作系统以为它还在被使用,所以不会回收。如果你的程序运行时间长,或者频繁进行这种操作,内存就会被慢慢耗尽,最终导致程序变慢,甚至系统崩溃。想象一下,你借了图书馆的书,看完了不还,那书架上的书只会越来越少,其他人就没得借了。

双重释放(Double Free):你对同一块内存调用了两次

delete
。这通常会导致程序崩溃,因为你试图释放一块已经被释放的内存。操作系统会很困惑,或者发现这块内存已经被别人拿走了,就会报错。

int* p = new int;
delete p;
delete p; // 错误!双重释放

悬空指针(Dangling Pointer):内存被

delete
后,指针本身并没有消失,它仍然指向那块现在已经无效的内存区域。如果你之后不小心通过这个悬空指针去访问那块内存,就会导致未定义行为。可能读到垃圾数据,可能写到不该写的地方,结果难以预测。

int* p = new int(10);
delete p;
// 此时 p 是悬空指针
*p = 20; // 错误!访问已释放的内存

数组与非数组的混用

new int
new int[N]
是不同的,它们分配的内存结构可能不一样,
delete
delete[]
也必须严格匹配。
delete[]
知道如何调用数组中每个元素的析构函数,并释放整个数组的内存。而
delete
只知道释放单个对象的内存。如果你用
delete
去释放
new[]
出来的数组,只有第一个元素的析构函数会被调用(如果是非POD类型),并且内存可能无法完全释放,导致部分泄露。反之,用
delete[]
去释放
new
出来的单个对象,也是未定义行为。

墨鱼aigc
墨鱼aigc

一款超好用的Ai写作工具,为用户提供一键生成营销广告、原创文案、写作辅助等文字生成服务。

下载
int* single = new int(5);
delete[] single; // 错误!用 delete[] 释放单个对象

int* arr = new int[5];
delete arr; // 错误!用 delete 释放数组

这些问题,说白了都是因为我们作为程序员,需要手动管理内存的生命周期,而人总有犯错的时候。

智能指针如何彻底改变动态内存管理?

说真的,自从C++11引入了智能指针,我对动态内存管理的看法彻底变了。以前用

new
delete
,总得小心翼翼,生怕漏了哪个
delete
。但智能指针这东西,简直就是内存管理的“救星”,它把RAII原则发挥到了极致。

RAII(Resource Acquisition Is Initialization)的核心思想是:把资源(比如内存)的生命周期和对象的生命周期绑定起来。当对象被创建时,资源被获取;当对象被销毁时(比如超出作用域),资源被自动释放。这样一来,你就不需要手动去调用

delete
了,因为析构函数会自动帮你完成这些事。

C++标准库提供了几种智能指针:

  1. std::unique_ptr
    : 这是最推荐的智能指针,它表示独占所有权。一个
    unique_ptr
    只能指向一个资源,并且不能被复制,但可以被移动。这意味着资源的所有权可以在不同
    unique_ptr
    之间转移。当
    unique_ptr
    超出作用域时,它所管理的内存会自动被释放。

    #include 
    #include 
    
    class MyObject {
    public:
        MyObject() { std::cout << "MyObject created\n"; }
        ~MyObject() { std::cout << "MyObject destroyed\n"; }
        void doSomething() { std::cout << "Doing something...\n"; }
    };
    
    void processUniqueObject() {
        // 使用 std::make_unique 替代 new,更安全高效
        std::unique_ptr objPtr = std::make_unique();
        objPtr->doSomething();
        // objPtr 在这里超出作用域,MyObject 会自动被销毁
    } // 析构函数自动调用,内存自动释放
    
    void transferOwnership() {
        std::unique_ptr p1 = std::make_unique();
        std::unique_ptr p2 = std::move(p1); // 所有权从 p1 转移到 p2
        // 此时 p1 变为空
        if (!p1) {
            std::cout << "p1 is now empty.\n";
        }
        p2->doSomething();
        // p2 超出作用域时,MyObject 销毁
    }

    std::unique_ptr
    是默认首选,因为它没有
    shared_ptr
    的引用计数开销,更轻量。

  2. std::shared_ptr
    : 它表示共享所有权。多个
    shared_ptr
    可以共同管理同一个资源。
    shared_ptr
    内部有一个引用计数器,每当一个
    shared_ptr
    指向资源时,计数器加一;每当一个
    shared_ptr
    不再指向资源时,计数器减一。当引用计数变为零时,资源才会被释放。

    #include 
    #include 
    
    // MyObject 和上面一样
    
    void processSharedObject() {
        std::shared_ptr objPtr1 = std::make_shared(); // 引用计数为 1
        std::cout << "Count after objPtr1 creation: " << objPtr1.use_count() << "\n";
    
        {
            std::shared_ptr objPtr2 = objPtr1; // 引用计数为 2
            std::cout << "Count after objPtr2 copy: " << objPtr1.use_count() << "\n";
            objPtr2->doSomething();
        } // objPtr2 超出作用域,引用计数减 1 (变为 1)
        std::cout << "Count after objPtr2 out of scope: " << objPtr1.use_count() << "\n";
    
        // objPtr1 在这里超出作用域,引用计数减 1 (变为 0),MyObject 会自动被销毁
    } // 析构函数自动调用,内存自动释放

    std::shared_ptr
    适合于需要共享资源所有权的场景,但要注意循环引用问题,这可能导致内存泄露(可以使用
    std::weak_ptr
    来解决)。

智能指针的出现,极大简化了C++的内存管理,让程序员可以更专注于业务逻辑,而不是费心去追踪每一块内存的生命周期。它们几乎消除了手动

delete
的必要性,显著减少了内存泄露和悬空指针的风险。

那么,
new
delete
在现代C++中还有用武之地吗?

这个问题问得好,因为智能指针确实是主流,但

new
delete
并没有完全消失。它们仍然是C++的底层基石,在某些特定场景下,你还是会用到它们。

  1. 实现自定义内存管理: 如果你在开发一个高性能的系统,需要对内存分配有极致的控制,比如实现一个内存池(memory pool)或者自定义的分配器(allocator),那么你就需要直接使用

    new
    delete
    来从操作系统获取和释放原始内存块。智能指针本身也是基于
    new
    delete
    (或者说更底层的
    operator new
    operator delete
    )来实现的。

  2. 与C语言API交互: C语言没有智能指针的概念,很多C库函数会返回通过

    malloc
    分配的内存,或者要求你传入一个指针,由它们来填充数据。在这种情况下,你可能需要使用
    new
    来分配内存,然后把原始指针传给C函数,或者接收C函数返回的原始指针,再手动
    delete
    (或者更常见的,用
    free
    ,这取决于分配方式)。

    // 假设有一个 C 函数返回 malloc 分配的字符串
    // char* get_c_string(); 
    // char* s = get_c_string();
    // // 使用 s
    // free(s); // 注意这里用 free,因为是 malloc 分配的

    或者,你可能需要将

    new
    出来的对象指针传递给一个期望原始指针的C函数。

  3. 在某些特定容器或数据结构中: 虽然标准库容器已经非常强大,但在实现一些非常规的、对内存布局有特殊要求的数据结构时,你可能需要手动控制内存分配。比如,实现一个侵入式链表,或者一个需要紧凑内存布局的图结构。

  4. Placement New: 这是一个比较高级的用法,允许你在已经分配好的内存上构造对象。这意味着你先用

    new char[N]
    malloc
    分配一块原始内存,然后用
    placement new
    在这块内存上“放置”一个对象。这在内存池或者需要精确控制对象构造位置的场景下很有用。

    #include  // 包含 placement new
    #include 
    
    class MyClass {
    public:
        MyClass() { std::cout << "MyClass constructed.\n"; }
        ~MyClass() { std::cout << "MyClass destructed.\n"; }
    };
    
    char buffer[sizeof(MyClass)]; // 预先分配一块内存
    
    int main() {
        MyClass* obj = new (buffer) MyClass(); // 在 buffer 上构造 MyClass 对象
        // 使用 obj...
        obj->~MyClass(); // 手动调用析构函数
        // 不需要 delete buffer,因为 buffer 是栈上的数组
        return 0;
    }

    这里需要注意的是,

    placement new
    只负责构造对象,不负责分配内存。对应的,你也不能用
    delete
    来释放它,而需要手动调用对象的析构函数。

总的来说,在现代C++日常开发中,我们应该优先使用智能指针(

std::unique_ptr
std::shared_ptr
,配合
std::make_unique
std::make_shared
)。它们提供了更安全、更简洁的内存管理方式。只有当你确实需要底层控制,并且清楚自己在做什么时,才应该考虑直接使用
new
delete
。它们是强大的工具,但需要你承担起全部的责任。

相关专题

更多
C语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

400

2023.06.20

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

618

2023.07.25

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

354

2023.08.02

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

259

2023.08.09

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

603

2023.09.05

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

527

2023.09.20

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

642

2023.09.20

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

602

2023.09.22

c++ 根号
c++ 根号

本专题整合了c++根号相关教程,阅读专题下面的文章了解更多详细内容。

17

2026.01.23

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Rust 教程
Rust 教程

共28课时 | 4.8万人学习

Kotlin 教程
Kotlin 教程

共23课时 | 2.8万人学习

Go 教程
Go 教程

共32课时 | 4.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号