嵌入式软件工程师面经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 标准库函数,需包含 

<cstdlib>

 头文件

内存大小计算

自动根据类型计算(如 

new int

 无需传 4)

需手动计算字节数(如 

malloc(5*sizeof(int))

返回值类型

目标类型指针(如 

int*

),无需强制转换

void*

,必须强制转换为目标类型

对象构造 / 析构

自动调用构造 / 析构函数(对象级管理)

仅分配 / 释放内存,不调用任何构造 / 析构(内存块级管理)

数组支持

专用 

new[]

/

delete[]

,自动处理数组元素构造 / 析构

需手动计算总字节数,无数组专属逻辑

分配失败处理

抛出 

std::bad_alloc

 异常(或 

new(nothrow)

 返回 

nullptr

返回 

NULL

,需手动检查

重载支持

可重载 

operator new

/

operator delete

(自定义内存分配)

不可重载

内存区域

从 “自由存储区”(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 deletenew 专属特性)

通过重载,可以自定义内存分配逻辑(如内存池、日志记录),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、驱动开发等核心考点解析与拓展。另有面试模拟、薪资谈判、大厂流程揭秘等实用内容。

全部评论

相关推荐

评论
2
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务