嵌入式软件工程师面经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、驱动开发等核心考点解析与拓展。另有面试模拟、薪资谈判、大厂流程揭秘等实用内容。