目录
在C++编程中,动态内存管理是必须掌握的核心技能之一。new和delete作为C++提供的动态内存管理操作符,承担着在堆内存中创建和销毁对象的重要职责。与C语言的malloc/free不同,new和delete不仅能完成内存分配,还会自动调用对象的构造函数和析构函数,使得它们成为C++面向对象编程中不可或缺的工具。
一、new
和 delete
的基本概念
1.1 静态内存与动态内存
在了解 new
和 delete
之前,我们需要先明确静态内存和动态内存的概念。
- 静态内存:用于存储全局变量、静态变量和函数调用栈中的局部变量。这些变量的生命周期在编译时就已经确定,它们的内存分配和释放由编译器自动处理。例如:
#include <iostream>
// 全局变量,存储在静态内存中
int globalVar = 10;
void func() {
// 局部静态变量,存储在静态内存中
static int staticVar = 20;
std::cout << "Static variable: " << staticVar << std::endl;
}
int main() {
// 局部变量,存储在栈上
int localVar = 30;
std::cout << "Local variable: " << localVar << std::endl;
func();
return 0;
}
- 动态内存:也称为堆内存,程序可以在运行时从堆上分配和释放内存。这种内存分配方式更加灵活,但需要程序员手动管理。
new
和delete
表达式就是用于管理动态内存的工具。
1.2 new 操作符
1.2.1 工作原理
①内存分配
- 当调用
new
操作符时,它首先会调用 C++ 的内存分配函数(通常是operator new
,但用户也可以重载这个函数以提供自定义的内存分配策略)。 operator new
会向堆(heap)请求足够的内存来存储指定类型的一个或多个对象。如果内存分配成功,它会返回一个指向该内存的指针;如果失败,则抛出std::bad_alloc
异常(除非指定了nothrow版本)。
②对象构造
- 在内存分配成功后,
new
操作符接着会调用对象的构造函数来初始化分配的内存。对于单个对象,它直接调用该对象的构造函数;对于对象数组,它调用默认构造函数(无参构造函数)来初始化数组中的每个元素。 - 如果构造函数抛出异常,则
new
操作符会捕获这个异常,并使用operator delete
释放之前分配的内存,然后重新抛出异常。
③ 返回指针:一旦对象被成功构造,new
操作符就会返回指向该对象的指针。
1.2.2 new 操作符的用法
new
操作符用于在堆(heap)上分配内存,并可能调用对象的构造函数来初始化内存。new
有两种形式:为单个对象分配内存和为对象数组分配内存。
①为单个对象分配内存
ClassName* pointer = new ClassName(args);
ClassName
是要创建的对象类型,args
是传递给对象构造函数的参数(如果构造函数不需要参数,则省略)。new
操作符首先为 ClassName
类型的对象分配足够的内存,然后调用该类型的构造函数(如果有的话),并返回指向新分配内存的指针。
②为对象数组分配内存
ClassName* array = new ClassName[size];
size
指定了要分配的数组的元素数量。new
操作符为 size
个 ClassName
类型的对象分配内存,但只调用默认构造函数(无参构造函数)来初始化这些对象。如果没有默认构造函数,则编译失败。
1.3 delete 操作符
1.3.1 工作原理
①对象析构:当调用 delete
或 delete[]
操作符时,它首先会调用指向对象的指针所指向的对象的析构函数(如果有的话)。对于 delete
,它只调用单个对象的析构函数;对于 delete[]
,它会遍历数组中的每个对象并调用它们的析构函数。
②内存释放:
- 析构函数执行完毕后,
delete
或delete[]
操作符会调用 C++ 的内存释放函数(通常是operator delete
或operator delete[]
,但同样可以重载)。 operator delete
或operator delete[]
会将之前分配的内存归还给堆,以便后续的内存分配请求可以使用。
③指针置空(可选):虽然 delete
或 delete[]
操作符本身不会将指针置为 nullptr
,但这是一个良好的编程实践,以避免野指针的出现。应该在 delete
或 delete[]
调用后立即将指针设置为 nullptr
。
1.3.2
delete 操作符的用法
delete
操作符用于释放之前通过 new
分配的内存。与 new
类似,delete
也有两种形式:用于单个对象和对象数组。
①释放单个对象的内存
delete pointer;
pointer
是指向之前通过 new
分配的内存的指针。delete
操作符首先调用对象的析构函数(如果有的话),然后释放与该对象关联的内存。
②释放对象数组的内存
delete[] array;
对于通过 new[]
分配的对象数组,必须使用 delete[]
来释放内存。delete[]
操作符会遍历数组中的每个对象,并调用它们的析构函数(如果有的话),然后释放整个数组的内存。
二、new
和 delete
的基本使用
2.1 分配和释放单个对象
下面是一个简单的示例,演示如何使用 new
和 delete
分配和释放单个对象:
#include <iostream>
int main() {
// 使用 new 分配一个整数对象
int* ptr = new int;
*ptr = 10;
std::cout << "Value: " << *ptr << std::endl;
// 使用 delete 释放内存
delete ptr;
return 0;
}
使用 new
分配了一个整数对象,并将其初始化为 10。然后,使用 delete
释放了该对象所占用的内存。
2.2 分配和释放数组
下面是一个分配和释放数组的示例:
#include <iostream>
int main() {
// 使用 new 分配一个包含 5 个整数的数组
int* arr = new int[5];
for (int i = 0; i < 5; i++) {
arr[i] = i;
}
// 输出数组元素
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// 使用 delete[] 释放数组内存
delete[] arr;
return 0;
}
使用 new
分配了一个包含 5 个整数的数组,并对其进行初始化。然后,使用 delete[]
释放了该数组所占用的内存。
三、new
表达式的初始化
new
表达式可以在分配内存的同时对对象进行初始化。
3.1 初始化单个对象
#include <iostream>
int main() {
// 直接初始化
int* ptr1 = new int(10);
std::cout << "Value of ptr1: " << *ptr1 << std::endl;
// 值初始化
int* ptr2 = new int();
std::cout << "Value of ptr2: " << *ptr2 << std::endl;
delete ptr1;
delete ptr2;
return 0;
}
ptr1
使用直接初始化的方式将分配的整数对象初始化为 10,ptr2
使用值初始化的方式将分配的整数对象初始化为 0。
3.2 初始化数组
#include <iostream>
int main() {
// 初始化数组
int* arr1 = new int[5]{1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
std::cout << arr1[i] << " ";
}
std::cout << std::endl;
// 值初始化数组
int* arr2 = new int[5]();
for (int i = 0; i < 5; i++) {
std::cout << arr2[i] << " ";
}
std::cout << std::endl;
delete[] arr1;
delete[] arr2;
return 0;
}
arr1
使用初始化列表对数组元素进行初始化,arr2
使用值初始化的方式将数组元素初始化为 0。
四、new
和 delete
与类和对象
new
和 delete
也可以用于动态创建和销毁类的对象。
4.1 动态创建和销毁单个对象
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Constructor called" << std::endl;
}
~MyClass() {
std::cout << "Destructor called" << std::endl;
}
void print() {
std::cout << "Hello, World!" << std::endl;
}
};
int main() {
// 使用 new 创建对象
MyClass* obj = new MyClass;
obj->print();
// 使用 delete 销毁对象
delete obj;
return 0;
}
使用 new
创建 MyClass
对象时,会调用类的构造函数;使用 delete
销毁对象时,会调用类的析构函数。
4.2 动态创建和销毁对象数组
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Constructor called" << std::endl;
}
~MyClass() {
std::cout << "Destructor called" << std::endl;
}
};
int main() {
// 使用 new 创建对象数组
MyClass* arr = new MyClass[3];
// 使用 delete[] 销毁对象数组
delete[] arr;
return 0;
}
使用 new
创建对象数组时,会为每个对象调用构造函数;使用 delete[]
销毁对象数组时,会为每个对象调用析构函数。
五、new
和 delete
的异常处理
当 new
表达式无法分配所需的内存时,会抛出 std::bad_alloc
异常。为了避免程序崩溃,可以使用异常处理机制来捕获并处理该异常。
#include <iostream>
#include <new>
int main() {
try {
// 尝试分配大量内存
int* ptr = new int[1000000000];
delete[] ptr;
} catch (const std::bad_alloc& e) {
std::cerr << "Memory allocation failed: " << e.what() << std::endl;
}
return 0;
}
使用 try-catch
块捕获 std::bad_alloc
异常,并输出错误信息。
六、new
和 delete
的替代方案:智能指针
虽然 new
和 delete
提供了动态内存管理的基本功能,但手动管理内存容易出现内存泄漏和悬空指针等问题。C++ 标准库提供了智能指针来自动管理动态内存,避免这些问题。
6.1 std::unique_ptr
std::unique_ptr
是一种独占式智能指针,它确保同一时间只有一个指针可以指向该对象。当 std::unique_ptr
离开作用域时,会自动释放所指向的对象。
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "Constructor called" << std::endl;
}
~MyClass() {
std::cout << "Destructor called" << std::endl;
}
void print() {
std::cout << "Hello, World!" << std::endl;
}
};
int main() {
// 创建 std::unique_ptr
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
ptr->print();
// 当 ptr 离开作用域时,对象会自动销毁
return 0;
}
6.2 std::shared_ptr
std::shared_ptr
是一种共享式智能指针,多个 std::shared_ptr
可以指向同一个对象,并且会维护一个引用计数。当引用计数为 0 时,对象会自动销毁。
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "Constructor called" << std::endl;
}
~MyClass() {
std::cout << "Destructor called" << std::endl;
}
void print() {
std::cout << "Hello, World!" << std::endl;
}
};
int main() {
// 创建 std::shared_ptr
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2 = ptr1;
ptr1->print();
ptr2->print();
// 当 ptr1 和 ptr2 都离开作用域时,对象会自动销毁
return 0;
}
七、底层机制剖析
7.1 operator new/delete函数
内存分配底层实现:
void* operator new(size_t size) {
if (void* mem = malloc(size))
return mem;
else
throw std::bad_alloc();
}
void operator delete(void* mem) noexcept {
free(mem);
}
7.2 自定义内存管理
重载类专属operator new:
class MyClass {
public:
static void* operator new(size_t size) {
std::cout << "Custom new for MyClass\n";
return ::operator new(size);
}
static void operator delete(void* p) {
std::cout << "Custom delete for MyClass\n";
::operator delete(p);
}
};
7.3 与malloc/free的对比
①关键区别
特性 | new/delete | malloc/free |
---|---|---|
类型安全 | 是 | 否 |
构造/析构调用 | 自动 | 手动 |
内存计算 | 自动 | 手动 |
异常处理 | 抛出异常 | 返回NULL |
重载方式 | 类/全局作用域 | 全局函数 |
数组处理 | 专用语法 | 需要手动计算 |
②混用危险示例
// 危险操作!
MyClass* p = (MyClass*)malloc(sizeof(MyClass));
p->MyClass(); // 手动调用构造函数(非常规操作)
// ...使用对象...
p->~MyClass(); // 手动调用析构函数
free(p);
八、常见错误和注意事项
8.1 内存泄漏
如果使用 new
分配了内存,但没有使用 delete
释放,就会导致内存泄漏。例如:
void leaky_function() {
int* p = new int[100];
// 忘记delete[] p;
}
为了避免内存泄漏,应该始终确保在不再需要内存时使用 delete
释放它,或者使用智能指针来自动管理内存。
解决方法:使用智能指针
#include <memory>
void safe_function() {
auto p = std::make_unique<int[]>(100);
// 自动释放内存
}
8.2 悬空指针
当使用 delete
释放内存后,指向该内存的指针就会变成悬空指针。如果继续使用悬空指针,会导致未定义行为。例如:
#include <iostream>
int main() {
int* ptr = new int;
*ptr = 10;
delete ptr;
// 使用悬空指针
std::cout << *ptr << std::endl;
return 0;
}
为了避免悬空指针,在释放内存后,应该将指针置为 nullptr
。
8.3 多次释放内存
如果对同一块内存多次使用 delete
释放,会导致未定义行为。例如:
#include <iostream>
int main() {
int* ptr = new int;
delete ptr;
// 多次释放内存
delete ptr;
return 0;
}
为了避免多次释放内存,应该确保每个 new
只对应一个 delete
,并且在释放内存后将指针置为 nullptr
。
九、最佳实践指南
9.1 优先使用智能指针
#include <memory>
// 独占所有权
std::unique_ptr<MyClass> uptr(new MyClass());
// 共享所有权
std::shared_ptr<MyClass> sptr = std::make_shared<MyClass>();
// 数组支持(C++17)
auto arrPtr = std::make_unique<int[]>(10);
9.2 RAII原则应用
class ResourceHolder {
int* resource;
public:
ResourceHolder(size_t size) : resource(new int[size]) {}
~ResourceHolder() { delete[] resource; }
// 禁用拷贝(C++11)
ResourceHolder(const ResourceHolder&) = delete;
ResourceHolder& operator=(const ResourceHolder&) = delete;
// 移动语义支持(C++11)
ResourceHolder(ResourceHolder&& other) : resource(other.resource) {
other.resource = nullptr;
}
};
十、总结
正确使用new和delete是C++开发者的基本功,需要注意:
-
严格配对使用new/delete和new[]/delete[]
-
优先使用智能指针管理资源
-
对于大块内存分配要考虑异常安全
-
理解底层内存管理机制
-
遵循RAII原则设计资源管理类
现代C++(C++11及后续标准)提供了更安全的内存管理工具,建议在实际开发中优先使用智能指针和标准容器,尽量减少直接使用new/delete的需要。只有深入理解底层机制,才能更好地使用高层抽象工具。
十一、参考资料
- 《C++ Primer(第 5 版)》这本书是 C++ 领域的经典之作,对 C++ 的基础语法和高级特性都有深入讲解。
- 《Effective C++(第 3 版)》书中包含了很多 C++ 编程的实用建议和最佳实践。
- 《C++ Templates: The Complete Guide(第 2 版)》该书聚焦于 C++ 模板编程,而
using
声明在模板编程中有着重要应用,如定义模板类型别名等。 - C++ 官方标准文档:C++ 标准文档是最权威的参考资料,可以查阅最新的 C++ 标准(如 C++11、C++14、C++17、C++20 等)文档。例如,ISO/IEC 14882:2020 是 C++20 标准的文档,可从相关渠道获取其详细内容。
- cppreference.com:这是一个非常全面的 C++ 在线参考网站,提供了详细的 C++ 语言和标准库文档。
- LearnCpp.com:该网站提供了系统的 C++ 教程,配有丰富的示例代码和清晰的解释,适合初学者学习和理解相关知识。