C++学习:六个月从基础到就业——内存管理:new/delete操作符
C++学习:六个月从基础到就业——内存管理:new/delete操作符
本文是我C++学习之旅系列的第十七篇技术文章,也是第二阶段"C++进阶特性"的第二篇,主要介绍C++中动态内存管理的核心操作符——new和delete。查看完整系列目录了解更多内容。
引言
在上一篇文章中,我们深入探讨了堆和栈的概念以及它们在内存管理中的作用。本文将聚焦于C++中用于动态内存分配和释放的基本工具——new
和delete
操作符。与许多高级语言不同,C++允许程序员直接控制内存的分配和释放,这提供了极大的灵活性,但同时也带来了更多的责任和潜在的陷阱。
动态内存管理是现代软件开发中不可或缺的部分,尤其是当处理大型数据结构、运行时大小未知的数据或需要在程序执行过程中持久存在的对象时。理解new
和delete
的工作原理及正确使用方法,对于编写高效、稳定的C++程序至关重要。
new和delete基础
new操作符概述
new
操作符用于动态分配内存,它执行三个主要步骤:
- 分配足够大的未初始化内存来存储指定类型的对象
- 调用构造函数初始化对象(如果是类类型)
- 返回指向新创建对象的指针
基本语法:
Type* ptr = new Type; // 分配单个对象
Type* arr = new Type[size]; // 分配对象数组
delete操作符概述
delete
操作符用于释放动态分配的内存,它执行两个主要步骤:
- 调用对象的析构函数(如果是类类型)
- 释放内存
基本语法:
delete ptr; // 释放单个对象的内存
delete[] arr; // 释放数组的内存
简单示例
下面是一个使用new
和delete
的基本示例:
#include <iostream>class Simple {
public:Simple() {std::cout << "Simple constructor called" << std::endl;}~Simple() {std::cout << "Simple destructor called" << std::endl;}void sayHello() {std::cout << "Hello from Simple object!" << std::endl;}
};int main() {// 分配单个对象Simple* obj = new Simple;obj->sayHello();delete obj; // 释放单个对象std::cout << "-------------------" << std::endl;// 分配对象数组Simple* objArray = new Simple[3];objArray[0].sayHello();objArray[1].sayHello();objArray[2].sayHello();delete[] objArray; // 释放数组return 0;
}
输出结果:
Simple constructor called
Hello from Simple object!
Simple destructor called
-------------------
Simple constructor called
Simple constructor called
Simple constructor called
Hello from Simple object!
Hello from Simple object!
Hello from Simple object!
Simple destructor called
Simple destructor called
Simple destructor called
new操作符详解
分配过程详解
当我们使用new
操作符时,它实际执行以下操作:
- 调用底层的内存分配函数(通常是
operator new
)分配足够的原始内存 - 将原始内存转换为适当的类型指针
- 使用适当的构造函数初始化对象(对于非POD类型)
new的变体
带初始化的new
int* p1 = new int; // 未初始化值
int* p2 = new int(); // 初始化为0
int* p3 = new int(42); // 初始化为42// C++11后,可以使用统一初始化语法
int* p4 = new int{42}; // 初始化为42
带位置的new (placement new)
Placement new允许在预先分配的内存位置构造对象,而不分配新内存:
#include <iostream>
#include <new> // 为placement new包含此头文件class Complex {
private:double real;double imag;public:Complex(double r, double i) : real(r), imag(i) {std::cout << "Constructor called." << std::endl;}~Complex() {std::cout << "Destructor called." << std::endl;}void print() const {std::cout << real << " + " << imag << "i" << std::endl;}
};int main() {// 分配一块足够大的内存,但不构造对象char memory[sizeof(Complex)];// 在预先分配的内存上构造对象Complex* obj = new(memory) Complex(3.0, 4.0);obj->print();// 显式调用析构函数(不要使用delete,因为内存不是通过new分配的)obj->~Complex();return 0;
}
Placement new主要用于:
- 优化内存分配(避免多次分配/释放)
- 内存池实现
- 对象的精确放置(如硬件通信缓冲区)
nothrow new
默认情况下,当new
无法分配内存时会抛出std::bad_alloc
异常。但我们也可以使用nothrow
形式,失败时返回nullptr
而不是抛出异常:
#include <iostream>
#include <new>int main() {// 尝试分配巨大的内存(可能失败)int* hugeArray = new(std::nothrow) int[1000000000000];if (hugeArray == nullptr) {std::cout << "Memory allocation failed." << std::endl;} else {std::cout << "Memory allocation succeeded." << std::endl;delete[] hugeArray;}return 0;
}
分配数组
分配数组时,new
会记录数组的大小,以便delete[]
能正确释放全部内存:
int* array = new int[10]; // 分配10个整数的数组// 使用数组...delete[] array; // 释放整个数组
注意:当分配数组时必须使用delete[]
而非delete
来释放内存,否则可能导致未定义行为。
二维数组的分配
有几种方式可以分配二维数组:
// 方法1:使用连续内存(推荐)
int rows = 3, cols = 4;
int* matrix = new int[rows * cols];
matrix[row * cols + col] = value; // 访问元素// 方法2:数组的数组(指针数组)
int** matrix = new int*[rows];
for (int i = 0; i < rows; i++) {matrix[i] = new int[cols];
}// 使用...// 释放方法2的内存
for (int i = 0; i < rows; i++) {delete[] matrix[i];
}
delete[] matrix;
使用std::vector替代动态数组
通常,使用std::vector
比原始动态数组更安全:
#include <vector>// 一维vector
std::vector<int> vec(10); // 大小为10的向量// 二维vector
std::vector<std::vector<int>> matrix(rows, std::vector<int>(cols));
matrix[row][col] = value; // 访问元素
std::vector
会自动管理内存分配和释放,减少了内存泄漏和错误的风险。
delete操作符详解
释放过程详解
当我们使用delete
操作符时,它执行以下操作:
- 如果指针非空,调用对象的析构函数(对于非POD类型)
- 调用底层的内存释放函数(通常是
operator delete
)来释放内存
delete与析构函数
delete
的关键功能之一是调用对象的析构函数。对于类对象,析构函数负责清理资源(如关闭文件、释放其他动态分配的内存等)。
class ResourceManager {
private:int* data;public:ResourceManager() {data = new int[1000];std::cout << "Resource acquired" << std::endl;}~ResourceManager() {delete[] data;std::cout << "Resource released" << std::endl;}
};int main() {ResourceManager* rm = new ResourceManager();delete rm; // 调用析构函数,确保data被正确释放return 0;
}
如果不调用delete
,析构函数不会被执行,导致data
指向的内存泄漏。
delete[]与数组
delete[]
操作符用于释放通过new[]
分配的数组。它确保数组中的每个对象都正确调用其析构函数。
class MyClass {
public:MyClass() {std::cout << "Constructor called" << std::endl;}~MyClass() {std::cout << "Destructor called" << std::endl;}
};int main() {MyClass* array = new MyClass[3];// 输出: Constructor called (3次)delete[] array; // 对数组中的每个对象调用析构函数// 输出: Destructor called (3次)return 0;
}
使用错误的删除形式可能导致严重问题:
- 使用
delete
释放通过new[]
分配的内存:可能只调用第一个对象的析构函数,导致其他对象的资源泄漏 - 使用
delete[]
释放通过new
分配的单个对象:未定义行为,可能导致崩溃
new和delete的内部机制
operator new与operator delete函数
new
和delete
操作符实际上调用了底层的函数operator new
和operator delete
来执行内存分配和释放:
void* operator new(std::size_t size);
void operator delete(void* ptr) noexcept;void* operator new[](std::size_t size);
void operator delete[](void* ptr) noexcept;
这些函数可以被重载,以实现自定义内存管理策略:
#include <iostream>
#include <cstdlib>// 重载全局operator new
void* operator new(std::size_t size) {std::cout << "Custom global operator new: " << size << " bytes" << std::endl;void* ptr = std::malloc(size);if (!ptr) throw std::bad_alloc();return ptr;
}// 重载全局operator delete
void operator delete(void* ptr) noexcept {std::cout << "Custom global operator delete" << std::endl;std::free(ptr);
}class MyClass {
public:MyClass() {std::cout << "MyClass constructor" << std::endl;}~MyClass() {std::cout << "MyClass destructor" << std::endl;}// 类特定的operator new重载void* operator new(std::size_t size) {std::cout << "MyClass::operator new: " << size << " bytes" << std::endl;void* ptr = std::malloc(size);if (!ptr) throw std::bad_alloc();return ptr;}// 类特定的operator delete重载void operator delete(void* ptr) noexcept {std::cout << "MyClass::operator delete" << std::endl;std::free(ptr);}
};int main() {// 使用全局operator newint* pi = new int;delete pi;std::cout << "-------------------" << std::endl;// 使用类特定的operator newMyClass* obj = new MyClass;delete obj;return 0;
}
输出:
Custom global operator new: 4 bytes
Custom global operator delete
-------------------
MyClass::operator new: 1 bytes
MyClass constructor
MyClass destructor
MyClass::operator delete
内存对齐与内存布局
new
操作符确保分配的内存满足对象的对齐要求。不同类型有不同的对齐要求:
std::cout << "alignof(char): " << alignof(char) << std::endl;
std::cout << "alignof(int): " << alignof(int) << std::endl;
std::cout << "alignof(double): " << alignof(double) << std::endl;
一个复杂对象的内存布局可能受到填充(padding)和对齐(alignment)的影响:
struct Padded {char c; // 1字节// 可能有填充int i; // 4字节// 可能有填充double d; // 8字节
};std::cout << "sizeof(Padded): " << sizeof(Padded) << std::endl;
// 可能大于13(1+4+8)字节,由于内存对齐
常见的new/delete问题与陷阱
内存泄漏
内存泄漏发生在动态分配的内存未被释放时:
void memoryLeak() {int* ptr = new int[100];// 无匹配的delete[],内存泄漏
}
悬挂指针(野指针)
悬挂指针指向已释放的内存:
int* createAndDestroy() {int* ptr = new int(42);delete ptr; // 内存已释放return ptr; // 返回悬挂指针
}int main() {int* dangling = createAndDestroy();*dangling = 10; // 未定义行为:写入已释放的内存return 0;
}
重复释放
重复释放同一内存会导致未定义行为:
int* ptr = new int;
delete ptr; // 第一次释放,正确
delete ptr; // 第二次释放,未定义行为
使用错误的释放形式
不匹配的分配和释放形式会导致未定义行为:
int* single = new int;
delete[] single; // 错误:应使用deleteint* array = new int[10];
delete array; // 错误:应使用delete[]
自定义析构函数前提下的释放问题
如果基类没有虚析构函数,通过基类指针删除派生类对象会导致未定义行为:
class Base {
public:~Base() { // 非虚析构函数std::cout << "Base destructor" << std::endl;}
};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor" << std::endl;}
};int main() {Base* ptr = new Derived;delete ptr; // 只调用Base析构函数,不调用Derived析构函数return 0;
}
std::nothrow的限制
std::nothrow
只影响new
的内存分配部分,构造函数仍然可能抛出异常:
class ThrowingCtor {
public:ThrowingCtor() {throw std::runtime_error("Constructor failed");}
};int main() {try {// nothrow只对内存分配部分起作用,构造函数异常仍会传播ThrowingCtor* obj = new(std::nothrow) ThrowingCtor;} catch (const std::exception& e) {std::cout << "Caught exception: " << e.what() << std::endl;}return 0;
}
替代方案与现代C++实践
智能指针
现代C++提供了智能指针,自动管理动态内存的生命周期:
#include <memory>// 独占所有权
std::unique_ptr<int> p1(new int(42));
// 或更好的方式
std::unique_ptr<int> p2 = std::make_unique<int>(42); // C++14// 共享所有权
std::shared_ptr<int> p3 = std::make_shared<int>(42);
std::shared_ptr<int> p4 = p3; // 现在p3和p4共享同一个对象
智能指针会在超出作用域时自动释放其管理的内存,无需显式调用delete
。
容器
标准库容器如std::vector
、std::list
等自动管理内存:
std::vector<int> vec;
for (int i = 0; i < 1000; ++i) {vec.push_back(i); // 自动扩容,无需手动内存管理
}
RAII(资源获取即初始化)原则
使用RAII原则设计类,在构造函数中获取资源,在析构函数中释放资源:
class ResourceWrapper {
private:Resource* resource;public:ResourceWrapper(const std::string& resourceName) {resource = acquireResource(resourceName);}~ResourceWrapper() {releaseResource(resource);}// 禁止复制ResourceWrapper(const ResourceWrapper&) = delete;ResourceWrapper& operator=(const ResourceWrapper&) = delete;// 允许移动ResourceWrapper(ResourceWrapper&& other) noexcept : resource(other.resource) {other.resource = nullptr;}ResourceWrapper& operator=(ResourceWrapper&& other) noexcept {if (this != &other) {releaseResource(resource);resource = other.resource;other.resource = nullptr;}return *this;}// 使用资源的方法...
};
内存池和分配器
对于性能关键的应用,可以使用自定义内存池来优化频繁的小内存分配:
#include <cstddef>
#include <new>class SimpleMemoryPool {
private:struct Block {Block* next;};Block* freeList;static const size_t BLOCK_SIZE = 1024;public:SimpleMemoryPool() : freeList(nullptr) {}~SimpleMemoryPool() {Block* block = freeList;while (block) {Block* next = block->next;std::free(block);block = next;}}void* allocate(size_t size) {if (size > BLOCK_SIZE - sizeof(Block*)) {return std::malloc(size);}if (!freeList) {// 分配一批新块char* memory = reinterpret_cast<char*>(std::malloc(BLOCK_SIZE * 10));if (!memory) return nullptr;// 将新内存块链接到自由列表for (int i = 0; i < 10; ++i) {Block* block = reinterpret_cast<Block*>(memory + i * BLOCK_SIZE);block->next = freeList;freeList = block;}}// 使用自由列表的第一个块Block* block = freeList;freeList = block->next;return block;}void deallocate(void* ptr, size_t size) {if (size > BLOCK_SIZE - sizeof(Block*)) {std::free(ptr);return;}// 将块添加回自由列表Block* block = reinterpret_cast<Block*>(ptr);block->next = freeList;freeList = block;}
};// 为特定类型实现自定义new和delete
class MyObject {
private:static SimpleMemoryPool pool;public:void* operator new(size_t size) {return pool.allocate(size);}void operator delete(void* ptr, size_t size) {pool.deallocate(ptr, size);}// 类的其他成员...
};SimpleMemoryPool MyObject::pool;
性能考量
new/delete的开销
动态内存分配涉及多种开销:
- 调用操作系统分配内存:通常需要系统调用,这是昂贵的操作
- 查找合适的内存块:内存管理器需要查找足够大的空闲块
- 记录分配信息:存储元数据(如分配大小)
- 对齐处理:确保内存对齐
- 构造和析构:调用构造函数和析构函数
对于频繁的小内存分配和释放,这些开销可能会显著影响性能。
性能优化策略
可以采用以下策略优化动态内存管理:
- 减少分配次数:预分配、重用对象
- 批量分配:一次分配多个对象
- 使用内存池:为特定大小的对象预分配内存
- 避免碎片化:使用适当的分配策略
- 考虑替代方案:使用栈内存(当对象较小且生命周期有限时)
栈与堆的性能对比
栈分配通常比堆分配快得多:
#include <iostream>
#include <chrono>
#include <vector>const int ITERATIONS = 1000000;void stackAllocation() {for (int i = 0; i < ITERATIONS; ++i) {int array[10]; // 栈分配array[0] = i;}
}void heapAllocation() {for (int i = 0; i < ITERATIONS; ++i) {int* array = new int[10]; // 堆分配array[0] = i;delete[] array;}
}void vectorAllocation() {for (int i = 0; i < ITERATIONS; ++i) {std::vector<int> vec(10); // 使用std::vectorvec[0] = i;}
}template<typename Func>
double measureTime(Func func) {auto start = std::chrono::high_resolution_clock::now();func();auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> duration = end - start;return duration.count();
}int main() {std::cout << "Stack allocation: " << measureTime(stackAllocation) << " ms" << std::endl;std::cout << "Heap allocation: " << measureTime(heapAllocation) << " ms" << std::endl;std::cout << "Vector allocation: " << measureTime(vectorAllocation) << " ms" << std::endl;return 0;
}
运行此代码会明显看出栈分配比堆分配快得多,而std::vector
通常介于两者之间。
实际应用案例
实现简单的字符串类
下面是使用动态内存管理实现简单字符串类的示例:
#include <iostream>
#include <cstring>
#include <algorithm>class SimpleString {
private:char* data;size_t length;// 确保有足够的空间void ensureCapacity(size_t newLength) {if (newLength > length) {// 分配新内存(添加一些额外空间以减少重新分配次数)size_t newCapacity = std::max(newLength, length * 2);char* newData = new char[newCapacity + 1]; // +1 用于空终止符// 复制现有数据if (data) {std::strcpy(newData, data);delete[] data;}data = newData;length = newCapacity;}}public:// 默认构造函数SimpleString() : data(nullptr), length(0) {data = new char[1];data[0] = '\0';}// 从C风格字符串构造SimpleString(const char* str) : data(nullptr), length(0) {if (!str) {data = new char[1];data[0] = '\0';} else {length = std::strlen(str);data = new char[length + 1];std::strcpy(data, str);}}// 复制构造函数SimpleString(const SimpleString& other) : data(nullptr), length(0) {length = other.length;data = new char[length + 1];std::strcpy(data, other.data);}// 移动构造函数SimpleString(SimpleString&& other) noexcept : data(other.data), length(other.length) {other.data = nullptr;other.length = 0;}// 析构函数~SimpleString() {delete[] data;}// 复制赋值SimpleString& operator=(const SimpleString& other) {if (this != &other) {delete[] data;length = other.length;data = new char[length + 1];std::strcpy(data, other.data);}return *this;}// 移动赋值SimpleString& operator=(SimpleString&& other) noexcept {if (this != &other) {delete[] data;data = other.data;length = other.length;other.data = nullptr;other.length = 0;}return *this;}// 连接操作符SimpleString operator+(const SimpleString& other) const {SimpleString result;result.ensureCapacity(length + other.length);std::strcpy(result.data, data);std::strcat(result.data, other.data);return result;}// 获取C风格字符串const char* c_str() const {return data;}// 获取长度size_t size() const {return std::strlen(data);}// 访问元素char& operator[](size_t index) {return data[index];}const char& operator[](size_t index) const {return data[index];}// 设置新值void assign(const char* str) {if (!str) return;size_t newLength = std::strlen(str);ensureCapacity(newLength);std::strcpy(data, str);}// 追加字符串void append(const char* str) {if (!str) return;size_t currentLength = std::strlen(data);size_t appendLength = std::strlen(str);ensureCapacity(currentLength + appendLength);std::strcat(data, str);}
};int main() {// 测试构造函数SimpleString s1("Hello");SimpleString s2(" World");// 测试连接SimpleString s3 = s1 + s2;std::cout << s3.c_str() << std::endl; // 输出:Hello World// 测试复制和赋值SimpleString s4 = s1;std::cout << s4.c_str() << std::endl; // 输出:Hellos4 = s2;std::cout << s4.c_str() << std::endl; // 输出:World// 测试修改s4[0] = 'w'; // 改为小写std::cout << s4.c_str() << std::endl; // 输出:world// 测试appends4.append("!");std::cout << s4.c_str() << std::endl; // 输出:world!return 0;
}
这个例子展示了如何使用new
和delete
在堆上管理字符串数据,包括深拷贝、移动语义和内存重分配逻辑。
自定义对象池
对于需要频繁创建和销毁的小对象,可以实现对象池来提高性能:
#include <iostream>
#include <vector>
#include <memory>template<typename T, size_t BlockSize = 100>
class ObjectPool {
private:// 表示内存块的结构struct Block {union {T value;Block* next;};// 禁用构造和析构函数以允许在union中使用TBlock() : next(nullptr) {}~Block() {}};Block* freeList;std::vector<std::unique_ptr<Block[]>> blocks;public:ObjectPool() : freeList(nullptr) {}~ObjectPool() {// blocks的vector会自动清理分配的内存}// 分配对象template<typename... Args>T* allocate(Args&&... args) {if (freeList == nullptr) {// 分配新的内存块auto newBlock = std::make_unique<Block[]>(BlockSize);// 初始化自由列表for (size_t i = 0; i < BlockSize - 1; ++i) {newBlock[i].next = &newBlock[i + 1];}newBlock[BlockSize - 1].next = nullptr;freeList = &newBlock[0];blocks.push_back(std::move(newBlock));}// 使用自由列表的第一个块Block* block = freeList;freeList = block->next;// 使用placement new构造对象return new (&block->value) T(std::forward<Args>(args)...);}// 释放对象void deallocate(T* object) {if (!object) return;// 调用析构函数object->~T();// 将块添加回自由列表Block* block = reinterpret_cast<Block*>(object);block->next = freeList;freeList = block;}
};// 测试用的类
class TestObject {
private:int id;public:TestObject(int i) : id(i) {std::cout << "TestObject " << id << " constructed." << std::endl;}~TestObject() {std::cout << "TestObject " << id << " destructed." << std::endl;}int getId() const { return id; }
};int main() {// 创建对象池ObjectPool<TestObject, 5> pool;// 分配一些对象std::vector<TestObject*> objects;for (int i = 0; i < 10; ++i) {objects.push_back(pool.allocate(i));}// 使用对象for (auto obj : objects) {std::cout << "Object ID: " << obj->getId() << std::endl;}// 释放一些对象for (int i = 0; i < 5; ++i) {pool.deallocate(objects[i]);objects[i] = nullptr;}// 分配更多对象(会重用已释放的内存)for (int i = 0; i < 5; ++i) {objects[i] = pool.allocate(i + 100);}// 清理所有对象for (auto obj : objects) {if (obj) {pool.deallocate(obj);}}return 0;
}
对象池可以显著减少内存分配和释放的开销,特别是对于频繁创建和销毁的小对象。
最佳实践
总结一些使用new
和delete
的最佳实践:
-
避免裸指针:尽可能使用智能指针(
std::unique_ptr
、std::shared_ptr
)// 不推荐 Resource* res = new Resource(); // 使用后... delete res;// 推荐 std::unique_ptr<Resource> res = std::make_unique<Resource>(); // 自动管理生命周期
-
优先使用标准库容器:它们已经在内部处理好内存管理
// 不推荐 int* array = new int[size]; // 使用后... delete[] array;// 推荐 std::vector<int> array(size);
-
确保匹配
new
与delete
、new[]
与delete[]
:// 正确 int* single = new int; delete single;int* array = new int[10]; delete[] array;
-
遵循RAII原则:在构造函数中获取资源,在析构函数中释放资源
-
检查内存分配失败:使用异常处理或
std::nothrow
// 使用异常 try {int* array = new int[1000000000]; } catch (const std::bad_alloc& e) {std::cerr << "Memory allocation failed: " << e.what() << std::endl; }// 使用nothrow int* array = new(std::nothrow) int[1000000000]; if (!array) {std::cerr << "Memory allocation failed" << std::endl; }
-
基类使用虚析构函数:确保通过基类指针正确删除派生类对象
class Base { public:virtual ~Base() = default; };
-
考虑移动语义:当涉及大量数据转移时
class MovableResource { public:// 移动构造函数MovableResource(MovableResource&& other) noexcept: data(other.data) {other.data = nullptr;}// 移动赋值运算符MovableResource& operator=(MovableResource&& other) noexcept {if (this != &other) {delete[] data;data = other.data;other.data = nullptr;}return *this;}private:int* data; };
-
使用工具检测内存问题:Valgrind、AddressSanitizer等
总结
new
和delete
操作符是C++中动态内存管理的基本工具,它们允许程序员在运行时分配和释放内存。理解它们的工作原理、正确用法和潜在问题对于编写健壮的C++程序至关重要。
然而,在现代C++中,我们通常应该倾向于使用更高级的抽象(如智能指针、容器和RAII类)来自动管理内存,这有助于减少常见错误如内存泄漏、悬空指针和重复释放。
关键记忆点:
new
分配内存并调用构造函数,delete
调用析构函数并释放内存- 数组使用
new[]
分配,delete[]
释放 - 裸指针管理的内存需要显式释放,否则会导致内存泄漏
- 现代C++提供更安全的替代方案:智能指针、容器和RAII
在下一篇文章中,我们将讨论内存泄漏的检测与避免,这是C++内存管理中的一个重要主题。
这是我C++学习之旅系列的第十七篇技术文章。查看完整系列目录了解更多内容。