C++从入门到入土 (5):.C/C++内存管理

一、C/C++内存分布

先让我们先看一看以下的代码

cpp 复制代码
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
	static int staticVar = 1;
	int localVar = 1;
	int num1[10] = { 1, 2, 3, 4 };
	char char2[] = "abcd";
	const char* pChar3 = "abcd";
	int* ptr1 = (int*)malloc(sizeof(int) * 4);
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
	free(ptr1);
	free(ptr3);
}
//1. 选择题:
//选项 : A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
// 
//globalVar在哪里?____		    C 全局变量,未被 static 修饰但作用域是整个程序,存放在数据段(静态区)
//staticGlobalVar在哪里?____   C static 修饰的全局变量,作用域限制在当前文件,但存储位置仍在数据段
//staticVar在哪里?____         C 函数内 static 变量,生命周期全局,存储在数据段,仅作用域在函数内
//localVar在哪里?____          A 函数内普通局部变量,栈帧分配,函数结束后销毁
//num1 在哪里?____             A 函数内数组,属于局部变量,数组本身(存储空间)在栈上
//char2在哪里?____             A 函数内字符数组,数组名是栈上的地址,存储在栈
//* char2在哪里?___            A char2 是数组首地址,*char2 是数组第一个元素 'a',元素都存在栈上的数组空间里
//pChar3在哪里?____            A 函数内的指针变量本身(存储地址的变量),在栈上
//* pChar3在哪里?____          D 指针指向字符串常量 "abcd",常量存储在只读的代码段(常量区)
//ptr1在哪里?____              A 指针变量本身(存储 malloc 返回地址的变量),在栈上
//* ptr1在哪里?____            B ptr1 指向 malloc 申请的内存,动态内存分配在堆上

说明:

  1. 栈又叫堆栈------非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux)
  3. 用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段------存储全局数据和静态数据。
  5. 代码段------可执行的代码/只读常量。

注意:calloc/realloc 和 malloc 一样,申请的内存都在堆区;

二、C语言中的动态内存管理方式:calloc/realloc/malloc/free

calloc / realloc / malloc的区别:

  1. malloc 主要用于分配指定字节大小的内存块 ,它仅完成内存分配操作 ,不会对分配的内存做任何初始化处理,内存中的内容是随机的垃圾值,使用时只需传入一个表示总字节数的参数即可,适合对性能要求高、无需初始化的小块内存分配场景。
  2. calloc 同样是分配内存 ,但它会在分配后自动将内存内容初始化为 0,安全性更高,使用时需要传入两个参数,分别是元素个数和单个元素的大小,更适合需要内存清零、对数据安全性有要求的场景。
  3. realloc 核心作用是调整已通过 malloc/calloc 分配的内存块大小 ,它的参数包含原内存指针和调整后的总大小,调整后的新增内存部分不会被初始化;当 realloc 执行时,若原内存块后方有足够连续空间,会直接扩展内存(原地扩容),若空间不足则会申请新的内存块、拷贝旧数据,并自动释放原内存块(异地扩容),若 realloc 调用失败会返回 NULL,此时原内存块依然有效,不会被释放。

malloc的实现原理?

glibc中的malloc实现原理(Linux)

三、C++内存管理方式

注意:C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。

new/delete操作内置类型

cpp 复制代码
void Test()
{
	// 动态申请一个int类型的空间
	int* ptr1 = new int;
	int* ptr2 = new int[10];
	delete ptr1;
	delete[] ptr2;

	// 支持初始化
	int* ptr3 = new int(0);
	int* ptr4 = new int[10]{1,2,3,4};
	delete ptr3;
	delete[] ptr4;
}

总结:

  1. new/delete是 C++ 面向对象的内存管理方式,支持初始化和构造 / 析构函数调用,malloc/free是 C 的底层内存函数,仅做内存分配释放;
  2. 使用规则:new配delete、new[]配delete[ ],不可混用,避免内存泄漏或析构不全;
  3. 内存分布:new/malloc分配的内存在堆区,指针变量本身(如 ptr4/ptr5/ptr6)在栈区。

new和delete操作自定义类型

cpp 复制代码
int main()
{
	// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A;  // 调用默认构造
	A* p3 = new A(10);
	A* p4 = new A[5];

	free(p1);
	delete p2;
	delete p3;
	delete[] p4;
	return 0;
}
new/delete 与 malloc/free 核心区别
  1. malloc/free 仅做内存操作,不会调用A的构造函数,
  2. new/delete 完整管理对象生命周期
    new A:先调用operator new(底层封装malloc)分配内存 ,再自动调用A的默认构造函数,将原始内存初始化为合法的A对象;new A(10):分配内存后,调用A的带参构造函数,完成对象初始化;
    delete p2/delete p3:先调用A的析构函数(释放对象持有的资源),再调用operator delete(底层封装free)释放内存。
new [ ] / delete [ ] 的底层实现原理
  1. 当用new A[5]创建自定义类型数组时,new[]并非只分配5*sizeof(A)的内存,而是会在数组内存的 "头部" 额外开辟 4/8 字节(取决于系统位数)的空间,用于存储数组的元素个数(这里是 5)。new [ ] 会多开辟 "计数内存":[计数区(存储5)] + [A对象1] + [A对象2] + ... + [A对象5];返回的指针p4:指向第一个A对象的起始地址(而非计数区),对用户透明。
  2. delete [ ] 的执行逻辑(为何不能拆分成多个 delete):delete[ ] p4 执行时,会先根据p4回退到计数区,读取元素个数(5);从最后一个A对象开始,依次调用 5 次A的析构函数(保证每个对象的资源都被释放);最后释放包含计数区在内的全部内存(调用operator delete[ ])。

四、operator new与operator delete函数(重要点进行讲解)

operator new与operator delete函数(重点)

注意:new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new 在底层调用operator new全局函数来申请空间,delete 在底层通过operator delete全局函数来释放空间。

operator new 核心概念:

  1. 本质:C++ 提供的全局函数(也可类内重载),核心职责是仅分配堆内存,不涉及任何对象的构造逻辑。
  2. 函数原型(默认全局版):void* operator new(size_t size);
    参数 size:需要分配的内存字节数;
    返回值:成功返回指向分配内存的 void* 指针,失败默认抛出 std::bad_alloc 异常(而非像 malloc 那样返回 NULL)。
  3. 底层逻辑:默认实现是对 malloc 的封装,会循环尝试调用 malloc 申请内存;若 malloc 失败,会调用内存不足处理函数,仍失败则抛异常(异常章节学习)。

operator delete 核心概念:

  1. 本质:C++ 提供的全局函数(也可类内重载),核心职责是仅释放堆内存,不涉及任何对象的析构逻辑。
  2. 函数原型(默认全局版):void operator delete(void* ptr);
    参数 ptr:需要释放的内存指针(若为 NULL,函数无操作);
    无返回值。
  3. 底层逻辑:默认实现是对 free 的封装,释放前会做指针有效性校验,保证线程安全后调用 free 释放内存。

重载特性:

  1. 可在全局范围或类内重载 operator new/operator delete,实现自定义内存管理策略(如内存池、内存对齐、内存使用统计等);
  2. 类内重载时,该类的 new 操作会优先调用自定义版本,全局重载则影响所有未自定义的类。

代码示例:

cpp 复制代码
#include <iostream>
#include <malloc.h>   // malloc/free
#include <new>       // bad_alloc
using namespace std;

// ===================== 1. 全局重载 operator new/operator delete =====================
// 自定义全局 operator new:封装 malloc,添加分配日志
void* operator new(size_t size) 
{
    cout << "[全局 operator new] 申请 " << size << " 字节内存" << endl;
    void* ptr = malloc(size);  // 底层调用 malloc
    if (!ptr) 
    {  // 模拟内存不足场景
        throw bad_alloc();     // 失败抛异常
    }
    return ptr;
}

// 自定义全局 operator delete:封装 free,添加释放日志
void operator delete(void* ptr) noexcept
{
    cout << "[全局 operator delete] 释放内存:" << ptr << endl;
    if (ptr)
    {
        free(ptr);  // 底层调用 free
    }
}

// ===================== 2. 类内重载 operator new/operator delete =====================
class MyClass 
{
public:
    int num;

    // 类内重载 operator new:仅为该类分配内存,添加专属日志
    void* operator new(size_t size) 
    {
        cout << "[MyClass operator new] 为MyClass申请 " << size << " 字节内存" << endl;
        void* ptr = malloc(size);
        if (!ptr) 
        {
            throw bad_alloc();
        }
        return ptr;
    }

    // 类内重载 operator delete:仅释放该类的内存
    void operator delete(void* ptr) noexcept 
    {
        cout << "[MyClass operator delete] 释放MyClass内存:" << ptr << endl;
        if (ptr) 
        {
            free(ptr);
        }
    }

    // 构造函数(仅初始化,不涉及内存分配)
    MyClass(int n = 0) : num(n) 
    {
        cout << "MyClass 构造函数:num = " << num << endl;
    }

    // 析构函数(仅清理,不涉及内存释放)
    ~MyClass() 
    {
        cout << "MyClass 析构函数:num = " << num << endl;
    }
};

// ===================== 3. 测试逻辑 =====================
int main() 
{
    try 
    {
        // 测试1:基础类型(使用全局重载的 operator new/delete)
        int* p1 = new int(10);  // new运算符:调用全局operator new + 初始化
        cout << "p1 指向的值:" << *p1 << endl;
        delete p1;  // delete运算符:调用全局operator delete

        cout << "------------------------" << endl;

        // 测试2:自定义类(使用类内重载的 operator new/delete)
        MyClass* p2 = new MyClass(20);  // new运算符:调用MyClass::operator new + 构造函数
        cout << "p2->num:" << p2->num << endl;
        delete p2;  // delete运算符:调用析构函数 + MyClass::operator delete

    }
    catch (const bad_alloc& e) 
    {
        // 捕获内存分配失败的异常
        cerr << "内存分配失败:" << e.what() << endl;
        return 1;
    }

    return 0;
}

总结:通过上述两个全局函数的实现知道,operatornew实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operatordelete最终是通过free来释放空间的。

五、 定位new表达式(placement-new)

定位 new(placement new)是一种特殊的 new 表达式,它不在堆上申请新的内存,而是在一块已经分配好的、原始的内存地址上,直接调用构造函数来初始化一个对象。
核心作用 :在已分配的内存空间中 "就地" 构造对象,常用于内存池、自定义内存管理等场景。
使用格式

cpp 复制代码
new (place_address) type;
new (place_address) type(initializer-list);
//place_address:必须是一个合法的指针,指向一块大小足够、且未被使用的内存。
//type:要构造的对象类型。
//initializer-list:对象的初始化参数,用于调用对应的构造函数。

注意:通过定位 new 构造的对象,不能直接用 delete 释放,必须显式调用其析构函数,然后再释放原始内存。
代码示例

cpp 复制代码
class A {
public:
    A(int a = 0) : _a(a) {
        cout << "A() 构造函数: this = " << this << ", _a = " << _a << endl;
    }

    ~A() {
        cout << "~A() 析构函数: this = " << this << ", _a = " << _a << endl;
    }

    int GetA() const { return _a; }

private:
    int _a;
};

int main()
{
    // 场景1:在 malloc 分配的内存上使用定位 new
    cout << "=== 场景1:在 malloc 分配的内存上使用定位 new ===" << endl;
    // 1. 先分配原始内存
    A* p1 = (A*)malloc(sizeof(A));
    if (p1 == nullptr) 
    {
        cerr << "malloc 失败" << endl;
        return 1;
    }
    cout << "malloc 分配的内存地址: " << p1 << endl;

    // 2. 使用定位 new 在 p1 指向的内存上构造对象 A(10)
    new (p1) A(10);
    cout << "对象 p1 的值: " << p1->GetA() << endl;

    // 3. 显式调用析构函数,清理对象
    p1->~A();
    // 4. 释放原始内存
    free(p1);

    cout << "\n=== 场景2:在 operator new 分配的内存上使用定位 new ===" << endl;
    // 1. 先通过 operator new 分配原始内存
    A* p2 = (A*)operator new(sizeof(A));
    cout << "operator new 分配的内存地址: " << p2 << endl;

    // 2. 使用定位 new 在 p2 指向的内存上构造对象 A(20)
    new (p2) A(20);
    cout << "对象 p2 的值: " << p2->GetA() << endl;

    // 3. 显式调用析构函数
    p2->~A();
    // 4. 释放原始内存
    operator delete(p2);

    return 0;
}

六、malloc / free和new / delete的区别

malloc / free和new / delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

  1. malloc和free是函数,new和delete是操作符
  2. malloc申请的空间不会初始化,new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型。
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放。
相关推荐
小糯米60119 小时前
C++ 排序
c++·算法·排序算法
爱编码的小八嘎19 小时前
C语言完美演绎3—8
c语言
EverestVIP19 小时前
c++前置声明的方式与说明
开发语言·c++
天外来鹿20 小时前
Map/Set/WeakMap/WeakSet学习笔记
前端·javascript·笔记·学习
峥嵘life20 小时前
Android16 【GTS】 GtsDevicePolicyTestCases 测试存在Failed项
android·linux·学习
leixj02520 小时前
SVN学习笔记
笔记·学习·svn
毕设源码_廖学姐21 小时前
计算机毕业设计springboot古诗词学习App 基于SpringBoot的中华经典诗文数字化研习平台 SpringBoot框架下的传统诗词文化移动学习系统
spring boot·学习·课程设计
老约家的可汗21 小时前
C++篇之类和对象下
java·开发语言·c++
Mr_WangAndy21 小时前
C++数据结构与算法_排序算法
c++·排序算法·基础排序·高级排序
Irissgwe21 小时前
C&C++内存管理
c语言·开发语言·c++·c++内存管理