【c++面向对象编程】第28篇:new/delete vs malloc/free:C++中正确动态内存管理

目录

一、一个会崩溃的程序

[二、new/delete 与 malloc/free 的对比](#二、new/delete 与 malloc/free 的对比)

正确用法示例

[三、new 和 delete 的工作原理](#三、new 和 delete 的工作原理)

[new 做了两件事](#new 做了两件事)

[delete 做了两件事](#delete 做了两件事)

[四、new[] 和 delete[]:数组的配对](#四、new[] 和 delete[]:数组的配对)

为什么不能混用?

五、完整例子:正确与错误的对比

[六、placement new:在已分配内存上构造对象](#六、placement new:在已分配内存上构造对象)

七、常见错误总结

[错误1:混用 malloc/free 和 new/delete](#错误1:混用 malloc/free 和 new/delete)

[错误2:忘记 delete 导致内存泄漏](#错误2:忘记 delete 导致内存泄漏)

[错误3:重复 delete](#错误3:重复 delete)

[错误4:delete 后继续使用指针](#错误4:delete 后继续使用指针)

八、最佳实践建议

九、这一篇的收获


一、一个会崩溃的程序

先看这段代码,猜猜会发生什么:

cpp

复制代码
#include <iostream>
#include <cstdlib>
#include <string>
using namespace std;

class Student {
private:
    string name;
    int age;
public:
    Student(const string& n, int a) : name(n), age(a) {
        cout << "构造函数: " << name << endl;
    }
    ~Student() {
        cout << "析构函数: " << name << endl;
    }
    void speak() {
        cout << "我是" << name << ", " << age << "岁" << endl;
    }
};

int main() {
    // 错误示范1:malloc 分配对象,不调用构造函数
    Student* s1 = (Student*)malloc(sizeof(Student));
    s1->speak();   // 未定义行为!name 和 age 未初始化
    
    // 错误示范2:new 分配的对象用 free 释放
    Student* s2 = new Student("张三", 20);
    free(s2);      // 未定义行为!没有调用析构函数
    
    return 0;
}

运行结果:可能输出乱码、崩溃,或者看起来"正常"但内存泄漏。

问题

  • malloc 只分配原始内存,不调用构造函数 → 对象处于未初始化状态

  • free 只释放内存,不调用析构函数 → string 的内部资源泄漏


二、new/delete 与 malloc/free 的对比

特性 malloc / free new / delete
头文件 <cstdlib> 不需要(C++ 关键字)
返回值 void*(需要强转) 类型化指针(无需强转)
内存大小 手动计算 sizeof 编译器自动计算
构造函数 ❌ 不调用 ✅ 调用
析构函数 ❌ 不调用 ✅ 调用
失败时 返回 NULL 抛出 bad_alloc 异常
配对要求 必须 malloc/free 配对 必须 new/delete 配对

正确用法示例

cpp

复制代码
// ✅ 正确:new/delete 配对
Student* s1 = new Student("李四", 21);
delete s1;

// ✅ 正确:malloc/free 配对(用于 POD 类型尚可,但不推荐)
int* arr = (int*)malloc(10 * sizeof(int));
free(arr);

// ❌ 错误:混用
Student* s2 = (Student*)malloc(sizeof(Student));
delete s2;    // 未定义行为

// ❌ 错误:混用
Student* s3 = new Student("王五", 22);
free(s3);     // 未定义行为

三、new 和 delete 的工作原理

new 做了两件事

  1. 分配内存 (类似 malloc

  2. 调用构造函数(在分配的内存上构造对象)

cpp

复制代码
Student* s = new Student("赵六", 23);
// 编译器大致转换成:
// 1. void* mem = operator new(sizeof(Student));  // 分配内存
// 2. Student* s = new(mem) Student("赵六", 23);  // placement new 调用构造

delete 做了两件事

  1. 调用析构函数

  2. 释放内存 (类似 free

cpp

复制代码
delete s;
// 编译器大致转换成:
// 1. s->~Student();      // 调用析构
// 2. operator delete(s); // 释放内存

四、new[] 和 delete[]:数组的配对

为数组分配内存时,必须使用 new[]delete[] 配对。

cpp

复制代码
// ✅ 正确:new[] / delete[] 配对
Student* arr = new Student[3] { {"A", 1}, {"B", 2}, {"C", 3} };
delete[] arr;   // 调用 3 次析构函数

// ❌ 错误:new[] 配 delete(只调用一次析构)
Student* arr2 = new Student[3];
delete arr2;    // 未定义行为!只析构第一个元素,且释放内存错误

为什么不能混用?

new[] 分配时会在内存块开头(通常是前 4-8 字节)存储数组元素个数,以便 delete[] 知道要调用多少次析构函数。

cpp

复制代码
// new[] 分配的内存布局(示意)
// [元素个数][元素0][元素1][元素2]...
//   4字节    N字节   N字节   N字节

如果用 delete(而不是 delete[]):

  • 编译器以为只有一个对象

  • 只调用一次析构函数 → 剩余对象的资源泄漏

  • 释放内存时地址计算错误 → 崩溃


五、完整例子:正确与错误的对比

cpp

复制代码
#include <iostream>
#include <cstring>
using namespace std;

class StringHolder {
private:
    char* data;
    size_t len;
public:
    StringHolder(const char* s) {
        len = strlen(s);
        data = new char[len + 1];
        strcpy(data, s);
        cout << "构造: " << data << " (分配内存)" << endl;
    }
    
    ~StringHolder() {
        cout << "析构: " << data << " (释放内存)" << endl;
        delete[] data;
    }
    
    void print() const {
        cout << "内容: " << data << endl;
    }
};

int main() {
    cout << "=== 正确示范:new/delete ===" << endl;
    StringHolder* s1 = new StringHolder("Hello");
    s1->print();
    delete s1;   // ✅ 调用析构,释放内存
    
    cout << "\n=== 正确示范:new[]/delete[] ===" << endl;
    StringHolder* arr = new StringHolder[2] { {"First"}, {"Second"} };
    arr[0].print();
    arr[1].print();
    delete[] arr;  // ✅ 调用两次析构
    
    cout << "\n=== 错误示范:new[] 配 delete ===" << endl;
    StringHolder* arr2 = new StringHolder[2] { {"A"}, {"B"} };
    delete arr2;   // ❌ 只调用一次析构!第二个对象内存泄漏
    
    // 程序可能崩溃,或者看似正常但内存泄漏
    
    return 0;
}

输出(正确部分):

text

复制代码
=== 正确示范:new/delete ===
构造: Hello (分配内存)
内容: Hello
析构: Hello (释放内存)

=== 正确示范:new[]/delete[] ===
构造: First (分配内存)
构造: Second (分配内存)
内容: First
内容: Second
析构: Second (释放内存)
析构: First (释放内存)

错误示范部分的行为是未定义的。


六、placement new:在已分配内存上构造对象

有一种特殊情况:placement new 允许在已有的内存上构造对象(不分配新内存)。

cpp

复制代码
#include <iostream>
#include <new>  // placement new 需要这个头文件
using namespace std;

class Point {
public:
    int x, y;
    Point(int a, int b) : x(a), y(b) {
        cout << "Point 构造: (" << x << "," << y << ")" << endl;
    }
    ~Point() {
        cout << "Point 析构" << endl;
    }
};

int main() {
    // 分配原始内存(不构造对象)
    void* mem = operator new(sizeof(Point));
    cout << "分配了原始内存,地址: " << mem << endl;
    
    // 在已有内存上构造对象
    Point* p = new(mem) Point(10, 20);
    
    // 使用对象
    cout << "p->x = " << p->x << ", p->y = " << p->y << endl;
    
    // 手动调用析构
    p->~Point();
    
    // 释放原始内存
    operator delete(mem);
    
    return 0;
}

使用场景

  • 内存池实现(预分配一大块内存,重复使用)

  • 共享内存中的对象构造

  • 高性能场景(避免重复分配/释放)


七、常见错误总结

错误1:混用 malloc/free 和 new/delete

cpp

复制代码
// ❌ 各种混用都是未定义行为
Student* s1 = (Student*)malloc(sizeof(Student));
delete s1;

Student* s2 = new Student("A", 1);
free(s2);

Student* s3 = new Student[10];
delete s3;      // 应该是 delete[]

错误2:忘记 delete 导致内存泄漏

cpp

复制代码
void leak() {
    Student* s = new Student("临时", 0);
    // 忘记 delete s; → 内存泄漏
}

错误3:重复 delete

cpp

复制代码
Student* s = new Student("A", 1);
delete s;
delete s;   // ❌ 未定义行为(double free)

错误4:delete 后继续使用指针

cpp

复制代码
Student* s = new Student("A", 1);
delete s;
s->speak();   // ❌ 悬空指针,未定义行为
// 应该 s = nullptr;

八、最佳实践建议

  1. 永远配对使用newdeletenew[]delete[]mallocfree

  2. 优先使用智能指针unique_ptrshared_ptr)→ 后续章节会讲

  3. delete 后立即置空delete p; p = nullptr; 防止二次释放

  4. 避免手动 new/delete :能用 vectorstring 就别自己管理内存

  5. 类管理资源时遵循五法则:析构、拷贝构造、拷贝赋值、移动构造、移动赋值


九、这一篇的收获

你现在应该理解:

  • new 调构造函数 + 分配内存delete 调析构函数 + 释放内存

  • malloc/free 只处理内存,不处理构造/析构

  • 必须配对使用newdeletenew[]delete[]

  • 混用是未定义行为:可能崩溃或资源泄漏

  • placement new:在已有内存上构造对象,用于内存池等高级场景

💡 小作业:写一个 Timer 类,构造函数中记录开始时间,析构函数中输出"对象存活了 X 毫秒"。用 new 创建对象,故意忘记 delete,观察析构函数是否被调用。然后用 delete 正确释放,对比行为。


下一篇预告:第29篇《定位new(placement new):在指定内存上构造对象》------深入 placement new 的用法,结合内存池技术,展示如何手动控制对象的构造位置和生命周期。

相关推荐
qeen874 小时前
【算法笔记】各种常见排序算法详细解析(下)
c语言·数据结构·c++·笔记·学习·算法·排序算法
TechWayfarer4 小时前
IP归属地API实战指南:用IP数据云解析日志挖掘用户地域分布
大数据·开发语言·网络·python·tcp/ip
之歆4 小时前
DAY_13DOM操作完全指南DOM基础API与节点操作(上)
开发语言·前端·javascript·ecmascript
lsx2024064 小时前
Vue3 表单深度解析
开发语言
欢璃5 小时前
笔试强训练习
java·开发语言·jvm·数据结构·算法·贪心算法·动态规划
400分5 小时前
# LangChain v0.2+ 与 Ollama 三大核心模型实战指南
算法
花开·莫之弃5 小时前
Mac安装多版本jdk(jenv)
java·开发语言·macos
计算机安禾5 小时前
【c++面向对象编程】第32篇:移动语义与右值引用:现代C++性能优化核心
java·c++·性能优化
qq_401700415 小时前
Qt 自定义无边框窗口:标题栏、拖拽移动与缩放
开发语言·qt