在 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() 混用。
