嵌入式软件工程师面经C/C++篇—new/delete vs malloc/free 对比
在 C++ 开发中,动态内存分配是核心需求之一,new
/delete
和 malloc
/free
是实现该需求的两套核心机制。前者是 C++ 专属的 “对象级管家”,后者是源自 C 语言的 “内存块搬运工”,二者在底层逻辑、使用方式和功能特性上存在本质区别。本文将通过概念解析 + 代码示例的方式,系统梳理两者的差异,帮助你在实际开发中精准选型。
一、基础概念与使用语法
首先明确两者的核心定位:malloc
/free
是 C 标准库函数,仅负责 “分配 / 释放内存块”;new
/delete
是 C++ 运算符,不仅管理内存,还能自动处理对象的 “构造 / 析构”,契合面向对象编程需求。
1. malloc
与 free
(C 库函数)
核心特性
- 函数声明:
void* malloc(size_t size)
,需传入 “字节数” 作为参数; - 返回值:
void*
(无类型指针),必须强制转换为目标类型才能使用; - 初始化:仅分配内存,不初始化内容(内存中是随机垃圾值);
- 释放:通过
free(void* ptr)
释放内存,仅回收空间,不处理对象逻辑。
基础使用示例
#include <cstdlib> // 必须包含头文件 #include <iostream> int main() { // 1. 分配 1 个 int 大小的内存(int 占 4 字节,故传入 sizeof(int)) // 注意:必须强制转换为 int*,否则编译报错 int* p_int = (int*)malloc(sizeof(int)); if (p_int == NULL) { // 必须检查返回值,NULL 表示分配失败 std::cout << "malloc 分配 int 内存失败!" << std::endl; return 1; } // 2. 手动初始化(否则内存是随机值) *p_int = 10; std::cout << "malloc 分配的 int 值:" << *p_int << std::endl; // 输出:10 // 3. 分配数组(需计算总字节数:元素个数 × 单个元素大小) int* p_arr = (int*)malloc(5 * sizeof(int)); // 5 个 int 的数组 if (p_arr == NULL) { std::cout << "malloc 分配数组内存失败!" << std::endl; free(p_int); // 先释放已分配的内存,避免泄漏 return 1; } // 手动初始化数组(否则每个元素都是随机值) for (int i = 0; i < 5; i++) { p_arr[i] = i + 1; // 赋值为 1,2,3,4,5 } std::cout << "malloc 分配的数组第 3 个元素:" << p_arr[2] << std::endl; // 输出:3 // 4. 释放内存(顺序无关,但必须成对释放) free(p_int); // 释放单个 int 内存 free(p_arr); // 释放数组内存 // 注意:释放后指针本身仍存在(成为“野指针”),建议置空 p_int = NULL; p_arr = NULL; return 0; }
关键注意点
- 内存越界风险:若传入的
size
不足(如malloc(1)
存放int
),会非法占用后续内存,导致程序崩溃; - 指针未置空:
free
仅释放内存,指针本身不会销毁,若后续误操作该指针,会触发 “野指针访问” 错误; - 不支持对象:若用
malloc
分配类对象内存,不会调用构造函数,对象内成员(如string
)会处于未初始化状态,调用成员函数会触发未定义行为。
2. new
与 delete
(C++ 运算符)
核心特性
- 语法简洁:无需指定字节数,编译器自动根据类型计算大小;
- 返回值安全:直接返回目标类型指针,无需强制转换;
- 自动初始化: 基本类型:可显式初始化(如 new int(5)),未显式初始化则为随机值(C++11 后可 new int{} 初始化为 0);类对象:自动调用构造函数初始化;
- 释放逻辑:
delete
会先调用析构函数(清理对象资源),再释放内存; - 数组支持:用
new[]
分配数组、delete[]
释放,会自动调用数组中每个对象的构造 / 析构函数。
基础使用示例
#include <iostream> #include <string> // 定义一个类,用于演示对象的构造/析构 class Person { public: // 构造函数:初始化姓名和年龄 Person(const std::string& name, int age) : m_name(name), m_age(age) { std::cout << "Person 构造函数调用:" << m_name << "(" << m_age << "岁)" << std::endl; } // 析构函数:释放对象资源(此处无动态资源,仅作演示) ~Person() { std::cout << "Person 析构函数调用:" << m_name << "已释放" << std::endl; } // 成员函数:打印信息 void showInfo() { std::cout << "姓名:" << m_name << ",年龄:" << m_age << std::endl; } private: std::string m_name; int m_age; }; int main() { // 1. 分配单个基本类型(int) int* p_int1 = new int; // 未初始化(值随机) int* p_int2 = new int(20); // 显式初始化为 20 int* p_int3 = new int{}; // C++11 后,空初始化列表置为 0 std::cout << "new int(未初始化):" << *p_int1 << std::endl; // 输出随机值 std::cout << "new int(20):" << *p_int2 << std::endl; // 输出:20 std::cout << "new int{}:" << *p_int3 << std::endl; // 输出:0 // 2. 分配类对象(自动调用构造函数) Person* p_person = new Person("张三", 25); // 直接传入构造函数参数 p_person->showInfo(); // 调用成员函数,输出:姓名:张三,年龄:25 // 3. 分配对象数组(new[] + 初始化列表,自动调用每个对象的构造函数) Person* p_person_arr = new Person[2]{ Person("李四", 30), // 第 1 个元素 Person("王五", 35) // 第 2 个元素 }; p_person_arr[0].showInfo(); // 输出:姓名:李四,年龄:30 p_person_arr[1].showInfo(); // 输出:姓名:王五,年龄:35 // 4. 释放内存(必须成对,数组用 delete[]) delete p_int1; delete p_int2; delete p_int3; delete p_person; // 先调用析构函数,再释放内存 delete[] p_person_arr; // 调用数组中每个对象的析构函数,再释放内存 return 0; }
输出结果(重点看构造 / 析构顺序)
new int(未初始化):-842150451 // 随机值 new int(20):20 new int{}:0 Person 构造函数调用:张三(25岁) 姓名:张三,年龄:25 Person 构造函数调用:李四(30岁) Person 构造函数调用:王五(35岁) 姓名:李四,年龄:30 姓名:王五,年龄:35 Person 析构函数调用:张三已释放 // delete p_person 触发 Person 析构函数调用:王五已释放 // delete[] 先析构最后一个元素 Person 析构函数调用:李四已释放 // 再析构第一个元素
关键注意点
- 数组释放必须用
delete[]
:若用delete
释放new[]
分配的数组,会导致 “部分析构”(仅第一个对象调用析构函数),造成内存泄漏; - 异常处理:
new
分配失败时会抛出std::bad_alloc
异常,需用try-catch
捕获(C++11 也支持new(nothrow)
返回nullptr
,类似malloc
); cpp运行
二、new
/delete
与 malloc
/free
核心区别(对比表 + 示例)
为了更清晰地梳理差异,我们通过对比表总结核心特性,并结合示例强化理解。
语言属性 | C++ 关键字,编译器原生支持 | C 标准库函数,需包含
头文件 |
内存大小计算 | 自动根据类型计算(如
无需传 4) | 需手动计算字节数(如
) |
返回值类型 | 目标类型指针(如
),无需强制转换 |
,必须强制转换为目标类型 |
对象构造 / 析构 | 自动调用构造 / 析构函数(对象级管理) | 仅分配 / 释放内存,不调用任何构造 / 析构(内存块级管理) |
数组支持 | 专用
/
,自动处理数组元素构造 / 析构 | 需手动计算总字节数,无数组专属逻辑 |
分配失败处理 | 抛出
异常(或
返回
) | 返回
,需手动检查 |
重载支持 | 可重载
/
(自定义内存分配) | 不可重载 |
内存区域 | 从 “自由存储区”(Free Store)分配(通常基于堆,可自定义) | 从 “堆”(Heap)分配(操作系统管理的固定内存区域) |
关键区别示例验证
1. 构造 / 析构调用差异(最核心区别)
#include <iostream> #include <cstdlib> class Test { public: Test() { std::cout << "Test 构造函数" << std::endl; } ~Test() { std::cout << "Test 析构函数" << std::endl; } }; int main() { std::cout << "=== 使用 new/delete ===" << std::endl; Test* t1 = new Test(); // 输出:Test 构造函数 delete t1; // 输出:Test 析构函数 std::cout << "\n=== 使用 malloc/free ===" << std::endl; Test* t2 = (Test*)malloc(sizeof(Test)); // 无输出(不调用构造) free(t2); // 无输出(不调用析构) return 0; }
结论:new
/delete
是 “对象级” 管理,malloc
/free
是 “内存块级” 管理,后者无法处理对象的初始化和资源清理。
2. 重载 operator new
/operator delete
(new
专属特性)
通过重载,可以自定义内存分配逻辑(如内存池、日志记录),malloc
无法实现:
#include <iostream> #include <cstdlib> class MyClass { public: // 重载 operator new:自定义内存分配(加日志) void* operator new(size_t size) { std::cout << "自定义 operator new:分配 " << size << " 字节" << std::endl; void* ptr = std::malloc(size); // 底层仍可调用 malloc if (!ptr) throw std::bad_alloc(); return ptr; } // 重载 operator delete:自定义内存释放(加日志) void operator delete(void* ptr) { std::cout << "自定义 operator delete:释放内存" << std::endl; std::free(ptr); // 底层仍可调用 free } MyClass() { std::cout << "MyClass 构造函数" << std::endl; } ~MyClass() { std::cout << "MyClass 析构函数" << std::endl; } }; int main() { MyClass* obj = new MyClass(); // 输出顺序: // 1. 自定义 operator new:分配 1 字节(MyClass 无成员,占 1 字节) // 2. MyClass 构造函数 delete obj; // 输出顺序: // 1. MyClass 析构函数 // 2. 自定义 operator delete:释放内存 return 0; }
三、使用建议与避坑指南
1. 核心选型原则
- C++ 开发优先用
new
/delete
:尤其是处理类对象时,必须用new
/delete
保证构造 / 析构调用,避免内存泄漏和未定义行为; - 仅在特定场景用
malloc
/free
: 与 C 代码交互(如调用 C 库函数,需传入 malloc 分配的内存);需要用 realloc 动态调整内存大小(new 不支持内存扩充); - 绝对禁止混用:用
new
分配的内存必须用delete
释放,用malloc
分配的必须用free
释放,混用会导致内存 corruption(如new
分配的内存用free
释放,会跳过析构函数)。
2. 进阶优化:用智能指针替代手动管理
C++11 引入的智能指针(std::unique_ptr
/std::shared_ptr
)可以自动管理内存,避免手动调用 delete
,从根源上解决内存泄漏问题。例如:
#include <memory> // 智能指针头文件 #include <iostream> class Person { public: Person(const std::string& name) : m_name(name) { std::cout << "Person 构造:" << m_name << std::endl; } ~Person() { std::cout << "Person 析构:" << m_name << std::endl; } private: std::string m_name; }; int main() { // 用 std::unique_ptr 管理内存,无需手动 delete std::unique_ptr<Person> p1 = std::make_unique<Person>("赵六"); // 构造 std::shared_ptr<Person> p2 = std::make_shared<Person>("孙七"); // 构造 // 函数结束时,智能指针自动调用析构函数并释放内存 return 0; }
输出:
Person 构造:赵六 Person 构造:孙七 Person 析构:孙七 Person 析构:赵六
四、总结
new
/delete
和 malloc
/free
的本质差异,是 C++ 面向对象思想与 C 过程式思想的体现:
malloc
/free
是 “低级工具”,仅负责内存块的分配与回收,无对象概念;new
/delete
是 “高级管家”,围绕对象生命周期设计,自动处理构造 / 析构,支持重载和类型安全。
在 C++ 开发中,应优先使用 new
/delete
,并结合智能指针减少手动内存管理;仅在与 C 代码交互或需要特殊内存调整时,才考虑 malloc
/free
。掌握两者的区别,是写出安全、高效 C++ 代码的基础。
嵌入式岗位热门但面试难,考点繁杂。本专栏聚焦实战,助求职者突破壁垒。 内容覆盖面试全流程:从简历优化突出项目能力,到拆解多方向(物联网、汽车电子等)高频题,含 C 语言、RTOS、驱动开发等核心考点解析与拓展。另有面试模拟、薪资谈判、大厂流程揭秘等实用内容。