C++ new 和 delete

在 C++ 中,如果我们希望动态创建一个对象,让它的存活时间不再受当前函数作用域的限制,就需要使用 new 运算符。当这个动态对象使用完毕后,我们还需要配合 delete 运算符将它销毁并归还内存。

根据操作目标的不同,new 和 delete 分为两种严格配对的形式:一种用于操作 “单个对象”,使用 new 和 delete;另一种用于操作 “动态数组”,使用 new[] 和 delete[]。

创建和销毁单个对象

在操作单个对象时,new 运算符用来在堆内存上分配一块与类型大小相匹配的空间,并返回指向该空间的指针。等到不再需要这个对象时,我们再把该指针交给 delete 运算符来释放内存。

语法:

类型* 指针名 = new 类型(初始值);
delete 指针名;

说明:

  • new 类型(初始值):在堆内存上动态创建一个指定类型的对象并赋初值,最后返回该对象的内存地址。除了圆括号,我们也可以使用花括号 new 类型{初始值} 来指定初值。
  • delete 指针名:销毁指针所指向的那个堆对象,并将这块堆内存归还给系统。

示例 1:动态创建与释放整数

#include <iostream>

int main() {
    int* p = new int(100);    // 在堆上创建 int 对象
    std::cout << "修改前:" << *p << "\n";

    *p = 120;                 // 通过解引用修改值
    std::cout << "修改后:" << *p << "\n";

    delete p;                 // 释放堆内存
    return 0;
}

运行结果如下。

修改前:100
修改后:120

分析:

int* p = new int(100); 这句代码表示在堆上动态创建了一个初值为 100 的整数,并把它的内存地址保存在指针变量 p 中。随后我们通过 *p = 120 修改这块堆内存里的值,用法与普通指针一致。

在动态创建基本类型时,根据是否给出初始值,常见有以下 3 种写法:

// 方式 1
int* p1 = new int;

// 方式 2
int* p2 = new int{};

// 方式 3(推荐)
int* p3 = new int(100);

其中,方式 1 没有初始化,堆内存里保存的是不确定的随机值。方式 2 在类型后面加了一对空花括号,也可以写成 new int(),编译器会自动把整数初始化为 0。

方式 3 则直接给出了具体的初始值。在实际开发中,我们更推荐使用方式 2 或方式 3,养成定义时直接初始化的好习惯。

此外,小伙伴们还要注意一点:最后的 delete p 销毁的是 “指针 p 指向的堆对象”,而不是把栈上的指针变量 p 本身删掉。在离开当前作用域之前,p 依然是一个存在的局部指针变量。

如果我们使用 new 和 delete 来操作自定义的类对象,执行过程会更加直观。通过 new 创建类对象时,程序分配好堆内存后会自动调用类的构造函数;执行 delete 时,则会先自动调用类的析构函数,最后再把堆内存归还给系统。请看下面例子。

示例 2:操作自定义类对象

#include <iostream>
#include <string>

class Student {
private:
    std::string name;

public:
    Student(const std::string& n) : name(n) {
        std::cout << name << ":对象创建\n";
    }

    ~Student() {
        std::cout << name << ":对象销毁\n";
    }

    void study() const {
        std::cout << name << "正在学习 C++\n";
    }
};

int main() {
    Student* stu = new Student("小明");
    stu->study();
    delete stu;
    return 0;
}

运行结果如下。

小明:对象创建
小明正在学习 C++
小明:对象销毁

分析:

在这个例子中,执行 new Student("小明") 时,程序先在堆上开辟内存空间,接着自动调用 Student 类的构造函数,输出 “小明:对象创建”。

由于 stu 是指向类对象的指针,我们可以通过箭头运算符 stu->study() 来调用成员函数,它等价于 (*stu).study()。最后执行 delete stu 时,程序会先调用析构函数输出 “小明:对象销毁”,随后再释放堆内存。

创建和销毁动态数组

前面我们学过普通的栈数组,比如 int scores[4],它的元素个数必须在编译阶段就固定下来。

如果在写代码时还不确定有多少个元素,需要等到程序运行期间根据变量 count 的值来决定数组大小,或者数组体积太大不适合放在栈上,我们就可以使用 new[] 在堆上创建动态数组,用完后再通过 delete[] 释放。

语法:

类型* 指针名 = new 类型[元素个数]{};
delete[] 指针名;

说明:

  • new 类型[元素个数]{}:在堆上分配一段连续的内存来存放指定数量的元素,并返回指向第一个元素的指针。其中 “元素个数” 可以是运行期间才确定的整型变量,末尾的花括号 {} 用于将所有元素初始化为默认值(如整数默认为 0)。
  • delete[] 指针名:销毁动态数组中的全部元素,并释放整段连续的堆内存。注意 delete 后面的中括号 [] 不能省略。

示例 3:使用动态数组

#include <iostream>

int main() {
    int count = 4;
    int* scores = new int[count]{};    // 按变量长度创建动态数组

    for (int i = 0; i < count; i++) {
        scores[i] = 70 + i * 10;
    }

    for (int i = 0; i < count; i++) {
        std::cout << scores[i] << " ";
    }
    std::cout << "\n";

    delete[] scores;                   // 释放整段动态数组
    return 0;
}

运行结果如下。

70 80 90 100 

分析:

new int[count]{} 表示按照变量 count 的长度,在堆上创建了一个包含 4 个整数的动态数组,并将首元素的地址保存在指针 scores 中。随后我们就可以直接使用下标 scores[0] 到 scores[3] 读写每一个元素。

与创建单个对象类似,如果只写 new int[count] 而不加空花括号,数组元素全是不确定的随机值;写成 new int[count]{} 后,所有元素都会统一初始化为 0。如果一开始就确定了具体数值,也可以直接写成 new int[4]{70, 80, 90, 100}。

这里小伙伴们要特别注意:scores 本质上只是一个 int* 指针,不能像栈数组那样通过 sizeof(scores) / sizeof(scores[0]) 计算长度。在 64 位系统下,sizeof(scores) 算出的只是指针本身的 8 个字节,因此需要用变量 count 记住元素个数。

最后释放内存时,代码写的是 delete[] scores。这里的 [] 用来告诉编译器,指针指向的是一整排数组元素而非单个对象。如果数组里存放的是类对象,delete[] 会依次调用每一个元素的析构函数,把整段内存全部清理干净。

提示 在实际开发中,只要需要长度可变的动态数组,我们通常优先使用 std::vector。它不仅能通过 size() 直接获取元素个数,离开作用域时还会自动释放内部的堆内存,比手写 new[] 和 delete[] 省心很多。

new 和 delete 注意事项

手动使用 new 和 delete 管理堆内存时,如果细节处理不当很容易引发程序崩溃。平时写代码时,我们需要重点注意以下 4 点:

  • 创建与释放方式必须配对:通过 new 创建的单个对象,必须使用 delete 释放;通过 new[] 创建的动态数组,必须使用 delete[] 释放,两者不能混用。
  • 只能释放 new 申请的堆内存:delete 和 delete[] 只能用于释放通过 new 分配出来的堆内存地址,千万不要把普通栈变量的地址(如 &a)传给 delete,否则会直接引发程序崩溃。
  • 不要对同一块内存重复释放:同一个堆内存地址只能执行一次 delete。如果对已经释放过的地址再次执行 delete,会产生未定义行为,程序运行结果不可控,通常会直接崩溃。
  • 释放后及时将指针置空:执行 delete 之后,堆上的目标对象已经销毁,原指针就变成了悬空指针,不能再通过它读写数据。我们应该在 delete 之后立即将指针赋值为 nullptr。

对于第一点配对规则,凡是创建时带了中括号 [] 的动态数组,释放时也必须带上 []:

// 错误方式
int* arr = new int[4]{70, 80, 90, 100};
delete arr;

// 正确方式
int* arr = new int[4]{70, 80, 90, 100};
delete[] arr;

此外,C++ 标准规定对空指针 nullptr 执行 delete 是安全的,不会执行任何操作也不会报错。因此在释放完堆内存后,紧接着把指针置为 nullptr,既能避免后续误读无效地址,也能防止不小心重复释放导致崩溃:

int* p = new int(100);
delete p;       // 销毁堆对象
p = nullptr;    // 立即置为空指针

这里还有一点别搞混了:执行 p = nullptr 只会清空 p 这一个指针变量本身。如果程序里还有另一个指针变量(如 q)也保存着刚才那个旧地址,q 并不会跟着自动变成 nullptr,我们同样不能再去解引用 q。

常见问题

1. C++ 既然兼容 C 语言的 malloc() 和 free(),为什么还要专门引入 new 和 delete?

虽然两者都能在堆上申请和释放内存,但 C 语言的 malloc() 和 free() 只是单纯按字节分配和回收原始内存块,既需要手动计算字节大小和强制转换指针类型,也不会调用类的构造函数与析构函数。

而 new 和 delete 是专门面向 C++ 对象设计的运算符:new 会自动计算类型大小、返回对应类型的指针并调用构造函数,delete 则会先调用析构函数清理资源再归还内存。

因此在 C++ 开发中,动态管理对象统一使用 new 和 delete 或智能指针,不要与 malloc() 和 free() 混用。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号