【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. 永远配对使用 :new 配 delete,new[] 配 delete[],malloc 配 free

  2. 优先使用智能指针 (unique_ptr、shared_ptr)→ 后续章节会讲

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

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

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


九、这一篇的收获

你现在应该理解:

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

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

  • 必须配对使用 :new 配 delete,new[] 配 delete[]

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

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

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


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

相关推荐
weixin_460443564 小时前
2026年新修订《网络安全法》实施:Java私有化考试系统如何做好SBOM、漏洞治理与安全升级?
java·开发语言·在线考试系统·企业培训考试系统·宏远培训考试系统·国企培训考试系统·国产化培训系统
Cc.Y7 小时前
Java零基础入门:可变字符串与包装类:StringBuilder、StringBuffer 与 通讯录管理系统实战
java·开发语言·python
启观川8 小时前
数据结构与算法 -第 3 章 常用算法-动态规划
数据结构·笔记·python·算法
码爸8 小时前
排序算法介绍
python·算法·排序算法
sm_926787058 小时前
RFID 标签打印的技术实现要点与二次开发实践
java·大数据·前端·c++·编辑器
阿森_dev8 小时前
2027 届毕设选题怎么选:30 个 Java 方向选题的实现难度排序
java·开发语言·spring boot·毕业设计·课程设计
by209998 小时前
C++ 多态:同一个基类接口,调用不同的函数
开发语言·c++·经验分享·多态
笔墨登场说说8 小时前
flink bin/start-cluster.sh 帮我做成开机启动
开发语言·python
Sarvartha8 小时前
Lambda 表达式和函数式接口
java·开发语言
小白学大数据8 小时前
拼多多反爬对抗实战:Scrapy 中间件化采集架构解析
开发语言·scrapy·中间件·架构