[C++面试] new、delete相关面试点
一、入门
1、说说new与malloc的基本用途
int* p1 = (int*)malloc(sizeof(int)); // C风格
int* p2 = new int(10); // C++风格,初始化为10
new
是 C++ 中的运算符,用于在堆上动态分配内存并调用对象的构造函数,会自动计算所需内存大小。
#include <iostream>
int main() {int* ptr = new int(5);std::cout << *ptr << std::endl;delete ptr;return 0;
}
malloc
是 C 语言中的标准库函数,用于在堆上分配指定大小的内存块,不会调用对象的构造函数,返回的是 void*
类型的指针,需要手动进行类型转换。
int main() {int* ptr = (int*)malloc(sizeof(int));*ptr = 5;std::cout << *ptr << std::endl;free(ptr);return 0;
}
内存分配失败处理:malloc
返回NULL
;new
抛出std::bad_alloc
异常
2、delete
和 free
分别用于什么场景?
delete
是 C++ 中的运算符,用于释放由 new
分配的内存,并调用对象的析构函数。
#include <iostream>
class MyClass {
public:~MyClass() {std::cout << "Destructor called" << std::endl;}
};
int main() {MyClass* obj = new MyClass();delete obj;return 0;
}
free
是 C 语言中的标准库函数,用于释放由 malloc
、calloc
或 realloc
分配的内存,不会调用对象的析构函数。
#include <iostream>
#include <cstdlib>
int main() {int* ptr = (int*)malloc(sizeof(int));free(ptr);return 0;
}
3、new与malloc的关联
new
通过调用operator new
分配内存,而默认的operator new
内部使用malloc
void* operator new(size_t size) { void* p = malloc(size); if (!p) throw std::bad_alloc(); return p;
}
4、delete NULL或nullptr会发生什么?
操作 | 空指针(NULL /nullptr ) | 非空指针 |
---|---|---|
delete / delete[] | 安全(无操作) | 需确保指针有效且未被重复释放 |
free | 安全(无操作) | 需确保内存由 malloc 分配 |
底层逻辑:编译器会检查指针是否为空,若为空则直接跳过析构和内存释放步骤
最佳实践:释放后立即置空指针
delete、free并不会把指针置空。
int* p = new int(10);
delete p; // 第一次释放
delete p; // 危险!重复释放非空指针
安全性:避免程序员在调用 delete
或 free
前必须显式检查指针是否为空(避免冗余检查)。
二、进阶
1、new[]
和 delete[]
的作用是什么?
new[]
用于在堆上动态分配数组内存,并对数组中的每个元素调用构造函数。delete[]
用于释放由 new[]
分配的数组内存,并对数组中的每个元素调用析构函数
include <iostream>
class MyClass {
public:MyClass() {std::cout << "Constructor called" << std::endl;}~MyClass() {std::cout << "Destructor called" << std::endl;}
};
int main() {MyClass* arr = new MyClass[3];delete[] arr;return 0;
}
C++ 编译器在解析代码时会忽略
delete
和[]
之间的所有空白符(包括空格、换行符、制表符等),推荐delete[]
的紧凑写法delete[]arr; delete []arr; delete [] arr; delete[] arr;
2、对于内置数据类型,使用delete、delete[]效果是一样的。这句话对吗?为什么?
内置类型无析构函数:C++ 的内置数据类型(如 int
、long
、指针等)没有析构函数。delete
和 delete[]
的核心差异在于是否调用析构函数,而内置类型无需析构,但内存释放的完整性仍取决于运行时环境。某些编译器(如 MSVC)可能通过内存池机制自动回收整个数组内存,但这属于未定义行为,不可依赖。
int* p1 = new int(10);
delete p1; // 正确释放单个对象
int* p2 = new int[10];
delete[] p2; // 正确释放数组
delete p2; // 未定义行为
分配时的元数据记录:无论是 new
还是 new[]
,内存分配时系统会记录分配的内存大小和对象数量(存储在 _CrtMemBlockHeader
等结构中)。释放时,delete
和 delete[]
均能通过指针获取这些信息,从而正确释放连续内存块 。
int* p = new int[1000];
delete p; // 看似正常,但 Valgrind 报告 3996 字节泄漏(1000 * 4 - 4)
+-------------------+
| 数组长度(1000) | ← 元信息(通常占用 4/8 字节)
+-------------------+
| 元素0(int) | ← 用户可见的指针 `p` 指向此处
+-------------------+
| 元素1(int) |
+-------------------+
| ...(共 1000 个) |
+-------------------+
3、使用 new
分配内存,用 free
释放会怎么样?
如果使用 new
分配内存,却用 free
释放,对象的析构函数不会被调用,可能会导致资源泄漏,例如对象中包含动态分配的资源(如文件句柄、网络连接等)无法正确释放。
#include <iostream>
class MyClass {
public:~MyClass() {std::cout << "Destructor called" << std::endl;}
};
int main() {MyClass* obj = new MyClass();free(obj); // 析构函数不会被调用return 0;
}
4、 使用 malloc
分配内存,用 delete
释放会怎么样?
delete
会尝试调用对象的析构函数。 malloc
分配的内存没有经过构造函数初始化,调用析构函数可能会导致未定义行为。
#include <iostream>
#include <cstdlib>class ResourceHolder {
public:ResourceHolder() {std::cout << "ResourceHolder: Acquiring resource..." << std::endl;// 模拟资源获取,例如打开文件、分配内存等resource = new int[100];}~ResourceHolder() {std::cout << "ResourceHolder: Releasing resource..." << std::endl;// 模拟资源释放,例如关闭文件、释放内存等delete[] resource;}private:int* resource;
};int main() {// 使用 malloc 分配内存ResourceHolder* holder = (ResourceHolder*)malloc(sizeof(ResourceHolder));if (holder == nullptr) {std::cerr << "Memory allocation failed!" << std::endl;return 1;}// 尝试使用 delete 释放内存delete holder;return 0;
}
- 运用
malloc
为ResourceHolder
对象分配内存。malloc
只是单纯地分配指定大小的内存块,不会调用对象的构造函数,所以resource
指针不会被正确初始化。 - 尝试使用
delete
释放内存。delete
会调用对象的析构函数,但是由于resource
指针未被正确初始化,在析构函数中调用delete[] resource
就会引发未定义行为,可能会导致程序崩溃或者出现其他不可预测的问题。- 当
delete[] resource
尝试释放一个未正确初始化的指针时,可能会访问非法内存地址,从而致使程序崩溃。
- 当
5、malloc
+ delete混用问题
注:new
和delete
必须成对使用
- 内存生命周期管理:
new
与delete
通过构造函数/析构函数保证对象完整生命周期 - 混用风险:未调用析构函数(若对象有资源需释放)
class MyClass {int* data; // 未初始化
public:MyClass() { data = new int[100]; } // 构造函数未执行!~MyClass() { delete[] data; } // 析构函数尝试释放野指针
};MyClass* p = (MyClass*)malloc(sizeof(MyClass));
delete p; // 析构函数调用delete[] data,但data未初始化 → 崩溃
delete
确实会调用析构函数,但这一行为是否能正确执行,取决于对象是否被正确构造。通过malloc
分配内存时,MyClass
的构造函数未被调用,但delete p
却尝试调用析构函数。若析构函数中存在对未初始化成员的操作(如delete data
),会导致未定义行为(如访问野指针,引发崩溃)
内置类型(如
int
):
无构造函数和析构函数,因此malloc
+delete
可能不会崩溃(因为没有析构操作),但仍是未定义行为int* p = (int*)malloc(sizeof(int)); delete p; // 可能不崩溃,但不符合规范
6、new
与free混用
未调用析构函数,且可能因内存布局差异导致崩溃(如new[]的头部信息未处理)
当通过new[]
分配数组时,内存布局可能包含头部信息(记录数组长度),例如:
MyClass* arr = new MyClass[5];
// 内存布局:[长度=5][对象1][对象2]...[对象5]
delete[]
会根据头部信息调用5次析构函数,再释放完整内存块。若用free
释放,头部信息未被处理。free
的输入是 arr
(指向第一个对象),但 new[]
分配的实际内存块起始地址是 arr - sizeof(头部)。
正确行为:若用户调用 delete[] arr
,会从头部地址释放完整内存块(包括头部和所有对象)。错误行为:若调用 free(arr)
,free
仅尝试释放从 arr
开始的地址,而实际分配的内存块起始位置未被正确识别,导致部分内存未被释放(内存泄漏)或堆结构破坏(可能崩溃)
free(arr)
无法识别 new[]
的内存布局,会释放不完整的地址范围,导致内存泄漏(头部和部分对象未被释放),甚至因堆管理器元数据损坏而崩溃。
注:不是只释放了[长度=5]
[对象1][对象2]...[对象5]
free
只能释放通过malloc
/calloc
/realloc
分配的内存块,其底层通过内存块的头部元数据(如大小信息)来释放整个内存块。
new[]
分配的头部信息可能格式与malloc
不同,导致free
无法正确解析,最终释放的地址范围是未定义的:
- 可能释放不完整:
free(arr)
可能仅释放从对象0
地址开始的部分内存(如对象0
的存储空间),而头部和其他对象的内存未被释放- 可能破坏堆结构:错误释放地址会导致堆管理器元数据损坏,引发后续内存操作崩溃
若编译器未添加头部信息,free(arr)
可能释放整个数组(因内存块连续),但这是未定义行为,依赖具体实现。(内置类型数组)
- 析构函数未被调用 → 资源泄漏。
- 释放的地址错误(如未回退到头部起始位置)→ 内存布局破坏,可能崩溃
三、高阶
1、如何重载 new
和 delete
运算符?
应用场景:
- 内存池优化(减少碎片)
- 调试内存泄漏(记录分配/释放日志)
#include <iostream>
#include <cstdlib>
class MyClass {
public:static void* operator new(size_t size) {std::cout << "Custom new operator called" << std::endl;return std::malloc(size);}static void operator delete(void* ptr) {std::cout << "Custom delete operator called" << std::endl;std::free(ptr);}~MyClass() {std::cout << "Destructor called" << std::endl;}
};
int main() {MyClass* obj = new MyClass();delete obj;return 0;
}
2、 如何捕获new过程异常并处理
int main() {try {while (true) {int* ptr = new int[1000000];}} catch (const std::bad_alloc& e) {std::cout << "Memory allocation failed: " << e.what() << std::endl;}return 0;
}
3、 delete
和delete[]
有何区别?
delete
释放单个对象;delete[]
释放数组
delete
调用一次析构函数;delete[]
对数组中每个元素调用析构函数
对数组使用delete
会导致内存泄漏或崩溃。
4、new[]
如何知道要调用多少次析构函数?
头部信息存储:当分配自定义类型数组时,new[]
会在内存块头部额外存储数组长度(如4/8字节)
delete[]
根据头部信息确定析构次数,再释放完整内存块
MyClass* arr = new MyClass[5];
// 内存布局:[长度=5][对象1][对象2]...[对象5]
delete[] arr; // 读取长度5,调用5次析构函数
内置类型数组:若数组元素是基本类型(如 int
),某些编译器可能不添加头部信息(因无需调用析构函数),直接分配连续内存
5、设计一个内存泄漏检测工具,如何跟踪new
/delete
的使用?
重载全局operator new/delete
:记录分配/释放的地址和大小。
哈希表跟踪:维护分配记录,检测未配对的new
和delete
std::map<void*, size_t> allocMap;
void* operator new(size_t size) { void* p = malloc(size); allocMap[p] = size; return p;
}
void operator delete(void* p) { if (allocMap.erase(p)) free(p); else logLeak(); // 检测到未记录释放
}