面试题:delete[] 删除数组怎么判断要删除多少个?

旅行   2024-11-06 08:00   广东  

欢迎关注本公众号,专注面试题拆解

分享一套视频课程:《C++实现百万并发服务器》 面试需要项目的可以找我获取,免费分享。 欢迎V:fb964919126


在 C++ 中,内存管理一直是一个既重要又容易出错的话题。当我们使用 delete[] 删除一个数组时,编译器是如何知道要删除多少个元素的?


在 C++ 中,我们经常会看到这样的代码:

class MyClass {public:    MyClass() { std::cout << "Constructor\n"; }    ~MyClass() { std::cout << "Destructor\n"; }};
// 创建对象数组MyClass* arr = new MyClass[5];// 某些操作...delete[] arr; // 如何知道要删除5个对象?

这里就产生了一个有趣的问题:当执行 delete[] arr 时,编译器是如何知道需要调用5次析构函数的?毕竟在删除点,我们并没有显式地传递数组的大小。这个问题的答案,涉及到 C++ 内存管理的一个重要机制,这个机制通常被称为"数组 Cookie"(Array Cookie)或"数组长度前缀"(Array Length Prefix)


01

什么是 Array Cookie?


Array Cookie 是编译器在分配数组内存时,在实际数组数据之前存储的一小块额外信息。这个"Cookie"通常包含了数组的长度信息,使得 delete[] 操作能够正确地清理整个数组。让我们看一个简单的例子:
class MyClass {public:    MyClass() { std::cout << "Constructor\n"; }    ~MyClass() { std::cout << "Destructor\n"; }};
// 创建对象数组时,实际的内存布局:// [Array Cookie][对象1][对象2][对象3][对象4][对象5]MyClass* arr = new MyClass[5];
// delete[] 时会先读取 Cookie 中的信息,// 从而知道需要调用5次析构函数delete[] arr;


02

看标准是如何定义的


这类问题直接看标准也行,cppreference对此有明确的解析:


Array allocation may supply unspecified overhead, which may vary from one call to new to the next, unless the allocation function selected is thestandardnonallocating form. The pointer returned by the new expressionwill be offset by that value from the pointer returned by the allocation function

这段描述揭示了几个重要的特点:

1、数组分配可能包含未指定的开销(overhead)

2、这个开销在不同的 new 调用之间可能会变化

3、返回给用户的指针会相对于实际分配的内存有一个偏移


为什么需要这个开销?

继续看 cppreference 的解释:

Many implementations use the array overhead to store the number of objects in the array which is used by the delete[] expression to call the correct number of destructors.

这解释了这个开销的主要用途:存储数组中对象的数量,以便 delete[] 能正确调用析构函数。


对齐要求

特别值得注意的是对特定类型的特殊处理:

if the new expression is used to allocate an array of char, unsigned char, or std::byte, it may request additional memory from the allocation function if necessary to guarantee correct alignment of objects of all types no larger than requested array size, if one is later placed into allocated array

这说明:

对于 char、unsigned char 或 std::byte 数组

可能会请求额外的内存以确保正确的对齐

这是为了支持后续可能的类型转换


所以分配数组时,实际的内存布局可能如下:

[大小信息][对象1][对象2][对象3]...

// new T[n] 的内存布局[overhead][object1][object2]...[objectN]     ^        ^     |        |     |        用户获得的指针指向这里     包含数组大小信息

当调用 delete[] 时:

1. 首先读取存储的大小信息

2. 按照逆序依次调用每个对象的析构函数

3. 最后释放整个内存块


03

不同类型的处理方式


C++ 对不同类型的数组采用不同的处理策略,这是因为不同类型的对象有不同的生命周期管理需求。

基本类型数组

对于基本类型(如 int, char, double 等),情况相对简单:

void primitive_types() {    // 基本类型数组    int* intArr = new int[10];    // 编译器知道数组的大小和类型大小,可以计算需要释放的内存    delete[] intArr;
char* charArr = new char[20]; delete[] charArr;}

它们没有析构函数需要调用

它们的大小是编译时确定的

它们不需要特殊的清理操作


类对象数组

类对象数组的处理要复杂得多,因为需要考虑对象的生命周期管理:

// 重载 new[] 操作符可以看到分配过程    void* operator new[](size_t size) {        std::cout << "Allocating " << size << " bytes\n";        // size 包含了额外的大小信息存储空间        return ::operator new[](size);    }

需要正确调用每个对象的构造函数

需要按照正确的顺序调用析构函数

需要处理构造函数可能抛出的异常

需要确保内存对齐要求得到满足


04

内部实现机制

我们用伪代码解释一下cppreference所说的overhead可能用法

// 简化的内部实现示意class ArrayManager {private:    // 数组内存布局的简化表示    struct ArrayHeader {        size_t count;  // 数组元素个数        // 可能还有其他信息(如对齐等)    };
template<typename T> static T* allocateArray(size_t n) { // 1. 计算需要的总内存大小 size_t headerSize = sizeof(ArrayHeader); size_t totalSize = headerSize + (n * sizeof(T));
// 2. 分配内存 char* memory = static_cast<char*>(::operator new[](totalSize));
// 3. 在内存开头写入数组大小 ArrayHeader* header = reinterpret_cast<ArrayHeader*>(memory); header->count = n;
// 4. 返回数组起始位置 T* array = reinterpret_cast<T*>(memory + headerSize);
// 5. 构造对象 for(size_t i = 0; i < n; ++i) { new (array + i) T(); // 调用构造函数 }
return array; }
template<typename T> static void deallocateArray(T* array) { // 1. 获取header位置 char* memory = reinterpret_cast<char*>(array) - sizeof(ArrayHeader); ArrayHeader* header = reinterpret_cast<ArrayHeader*>(memory);
// 2. 获取数组大小 size_t n = header->count;
// 3. 调用析构函数 for(size_t i = n; i > 0; --i) { array[i-1].~T(); }
// 4. 释放整块内存 ::operator delete[](memory); }};

这个实现涉及到几个关键点:(上面代码仔细多阅读几遍

内存布局管理

异常安全性保证

对齐要求处理

构造和析构顺序维护


特殊情况:

即使是空数组,也需要存储大小信息

int* arr = new int[0];

这是合法的,但会分配一个最小的内存块

delete[] arr;


05

总结



delete[] 能够正确删除数组是因为:

1、new[] 在分配时存储了必要的大小信息

2、这个信息对用户是不可见的(隐藏的,对用户透明)

3、delete[] 会利用这个隐藏信息确保正确清理

end



CppPlayer 



关注,回复【电子书】珍藏CPP电子书资料赠送

精彩文章合集

专题推荐

【专辑】计算机网络真题拆解
【专辑】大厂最新真题
【专辑】C/C++面试真题拆解

CppPlayer
一个专注面试题拆解的公众号
 最新文章