C/C++ 内存管理

🎈 个人主页👉: tbRNA-CSDN博客

💯 个人简介:在校大学生一枚💋.
😍 希望我的文章对大家有着不一样的帮助,欢迎大家关注我,感谢大家的多多支持!

🎉 欢迎 👍点赞 ✍评论 ⭐收藏
往期文章👇

C++基础知识点(六)

C++基础知识点(五)

C++基础知识点(四)

C++基础知识点(三)

C++基础知识点(二)

C++基础知识点(一)

目录

[1. C / C++ 内存分布](#1. C / C++ 内存分布)

[2. C语言中动态内存管理方式](#2. C语言中动态内存管理方式)

[一、malloc vs calloc](#一、malloc vs calloc)

二、free:内存回收与释放

三、realloc:不仅是扩容

四、总结

[3. C++ 内存管理方式](#3. C++ 内存管理方式)

[一、new / delete操作内置类型](#一、new / delete操作内置类型)

二、new和delete操作自定义类型

三、C++异常的捕获和处理

[4. operator new与operator delete函数](#4. operator new与operator delete函数)

[5. new和delete的实现原理](#5. new和delete的实现原理)

一、内置类型

二、自定义类型

[😋 new的原理](#😋 new的原理)

[😜 delete的原理](#😜 delete的原理)

[🤪 new [N] 的原理](#🤪 new [N] 的原理)

[🤗 delete[]的原理](#🤗 delete[]的原理)

[6. malloc / free 和 new / delete的区别](#6. malloc / free 和 new / delete的区别)


1. 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);
}

请填写各变量 / 表达式对应的内存区域:

选项:A. 栈 B. 堆 C. 数据段 (静态区) D. 代码段 (常量区)

【说明】

  1. 栈又叫堆栈:非静态局部变量 / 函数参数 / 返回值等等,栈是向下增长的。

栈区用于存储函数调用时的局部变量、函数参数以及返回地址。当函数调用完成后,分配给这个函数的栈空间会被释放。

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

void function(int a, int b) 
{
    int localVar = a + b;
    cout << localVar << endl;
}

int main() {
    function(3, 4);
    return 0;
}

在这个例子中,a、b和localVar都是局部变量,它们存放在栈区。

当 function 函数调用结束后,对应的函数栈所占用的空间(参数 a、b,局部变量 localVar)都会被回收。

  1. 内存映射段是高效的I / O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口

创建共享共享内存来做进程间通信。

  1. 堆用于程序运行时动态内存分配,堆是可以上增长 的,堆区是用于动态内存分配的区域,当使用new(C++) 或者**malloc(C)**分配内存时,分配的内存块就位于堆区。

  2. 数据段:存储全局数据和静态数据

  3. 代码段:可执行的代码 / 只读常量。

全局 / 静态存储区和常量存储区的对比

2. C语言中动态内存管理方式

C语言的动态内存函数都在**<stdlib.h>** 头文件中,返回类型都是void*需要类型转换

它们操作在堆上,堆内存不像栈那样自动释放,必须手动管理。

否则,就可能出现野指针:指向已释放内存的指针

一、malloc vs calloc

malloc 是最基础的分配函数,它向内存申请一块连续可用的空间,并返回指向这块空间的指针。calloc 函数也用来动态内存分配。

它们的原型和功能如下:

void* malloc (size_t size);

void* calloc (size_t num, size_t size);

|-----|---------------------------------------|---------------------------------|
| 特性 | malloc | calloc |
| 功能 | 申请指定字节大小的内存 | 为 num 个大小为 size 的元素开辟空间 |
| 初始值 | 随机垃圾值(未初始化) | 全为 0(每个字节初始化为 0) |
| 参数 | 需手动计算总大小(如 10 * sizeof ( int ) ) | 自动计算(如 10, sizeof ( int ) ) |
| 性能 | 极快(只分配,不清理) | 稍慢(分配 + 清零) |
| 安全性 | 低 若不立刻赋值,读取即乱码 | 高 指针成员默认为 NULL,整数为 0 |

😍如果你在为结构体数组分配内存建议使用 calloc

原因:结构体中往往包含指针。如果用 malloc,这些指针指向的是野指针,一旦误用直接导致崩溃。而 calloc 会把它们初始化为 NULL。

这是更安全的防御性编程,你可以安全检查if ( ptr -> member == NULL)

二、free:内存回收与释放

free 函数 专门用来做动态内存的释放和回收

它的原型是:

void free (void* ptr);

😘注意:

  1. 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的,比如释放栈上的局部变量。

  2. 如果参数 ptr 是 NULL 指针,则 free函数什么事都不做。

  3. free 函数用来释放动态开辟的内存

🥰建议:

  1. ptr 必须是malloc / calloc / realloc返回的指针,否则行为未定义。

  2. free(NULL) 没事,但 free 非动态内存(如栈变量)会崩溃。

  3. 记住:free 后,ptr 仍指向旧地址,但内存已无效 ------ 这就是野指针的来源。

  4. 建议 free 后总是跟着设 ptr = NULL。

三、realloc:不仅是扩容

realloc 函数让动态内存管理更加灵活,可以对动态开辟内存大小进行调整。

它的原型是:

void* realloc (void* ptr, size_t size);

其中,ptr 是要调整的内存地址,size 是调整之后的新大小。

😉它的底层行为有两种:

行为 A:原地扩容(原有空间之后有足够大的空间)

直接在原有内存之后追加空间,原来空间的数据不发生变化。指针地址不变

行为 B:异地搬家(原有空间之后没有足够大的空间)

在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址 。同时,它会将原来内存中的数据移动到新的空间。

🙃陷阱:旧指针失效

这是个隐形陷阱。在异地搬家发生后,原指针 ptr 指向的内存块被释放,如果你继续使用原指针,就是严重的 Use-After-Free 错误。

四、总结

写C代码时严格遵守以下模板,养成习惯,能避开 90% 坑:

  1. malloc 成功返回一个指向开辟好空间的指针,如果开辟失败,则返回一个 NULL 指针 ,因此 malloc 的返回值一定要做检查

  2. free(p); 后,立刻执行p = NULL; ,这既防止了双重释放,也防止了悬空指针导致的 Use-After-Free。

  3. 使用 realloc 时,永远不要直接赋值给原指针

  4. 避免越界,用循环时,严格 < size,或用安全函数如strncpy。

(strncpy用法可以参考这篇文章C语言基础 字符串函数

3. C++ 内存管理方式

😚C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦

因此 C++又提出了自己的内存管理方式:通过 newdelete 操作符进行动态内存管理.

一、new / delete操作内置类型

cpp 复制代码
void Test()
{
    // 动态申请一个int类型的空间
    int* ptr1 = new int;
    // 动态申请一个int类型的空间并初始化为10
    int* ptr2 = new int(10);
    // 动态申请3个int类型的空间
    int* ptr3 = new int[3];
    // 动态申请10个int类型的空间并初始化为0
    int* ptr4 = new int[10] {0};
    // 动态申请10个int类型的空间并将前5个初始化为1,2,3,4,5剩余自动初始化为0
    int* ptr5 = new int[10] {1, 2, 3, 4, 5};

    delete ptr1;
    delete ptr2;
    delete[] ptr3;
    delete[] ptr4;
    delete[] ptr5;
}

注意:

😊 申请和释放单个元素的空间,使用newdelete操作符.

😍 申请和释放连续的空间,使用new[]delete[],注意:匹配起来使用.

同时我们通过new创建链表也会更方便:

cpp 复制代码
struct ListNode
{
	int val;
	ListNode* next;

	ListNode(int x)
		:val(x)
		,next(nullptr)
	{}
};

int main()
{
    ListNode* n1 = new ListNode(1);
    ListNode* n2 = new ListNode(2);
    ListNode* n3 = new ListNode(3);
    ListNode* n4 = new ListNode(4);

    n1->next = n2;
    n2->next = n3;
    n3->next = n4;
    n4->next = nullptr;

    ListNode* cur = n1;
    while (cur != nullptr)
    {
	    cout << cur->val;
	    if (cur->next != nullptr)
		    cout << "->";
	    cur = cur->next;
    }
    cout << endl;

    ListNode* tmp = nullptr;
    cur = n1;
    while (cur != nullptr)
    {
	    tmp = cur;
	    cur = cur->next;
	    delete tmp;
    }
    
    return 0;
}

二、new和delete操作自定义类型

cpp 复制代码
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

int main()
{
	// new/delete 和 malloc/free最大区别是:
    // new/delete对于"自定义类型"除了开空间还会调用构造函数和析构函数
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A(1);
	free(p1);
	delete p2;
	// 内置类型是几乎是一样的
	int* p3 = (int*)malloc(sizeof(int)); // C
	int* p4 = new int;
	free(p3);
	delete p4;

	A* p5 = (A*)malloc(sizeof(A) * 10);
	A* p6 = new A[10];
	free(p5);
	delete[] p6;

	return 0;
}

😉总结一下:

C++中的new和delete 不仅在用法上跟C中的malloc和free 有所差别,其实最大的差别是new会调用构造函数,delete会调用析构函数。

三、C++异常的捕获和处理

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。

**throw:**当问题出现时,程序会抛出一个异常(可以抛任意类型的异常)。这是通过使用throw 关键字来完成的。

**catch:**在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常,可以有多个catch进行捕获。

**try:**try块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。

cpp 复制代码
try
{
    // 保护的标识代码
}
catch( ExceptionName e1 )
{
    // catch 块
}
catch( ExceptionName e2 )
{
    // catch 块
}
catch( ExceptionName eN )
{
    // catch 块
}

代码示例:

cpp 复制代码
double Division(int len, int time)
{
	if (time == 0)
	{
		throw "除0错误";
	}
	else
	{
		return (double)len / (double)time;
	}
}
//多个try catch 会优先跳近的,但是前提是近的类型是匹配的  如果不匹配还是会优先调匹配的  所以优先级1、类型。2、就近
void Func()
{
	try {
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (const char s) //如果在该位置捕获,那么后面的语句还是会正常去调用
	{
		cout << "Func()" << s << endl;
	}
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* str)
	{
		cout << "main()" << str << endl;
	}

	return 0;
}

😇C++ 异常的缺点:

  1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时比较困难(主要问题)。

  2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。

  3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。

  4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。

  5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。

所以异常规范有两点:

一、工程实践中,建议自定义异常统一继承自 std::exception(或其派生类),便于统一捕获和处理;

二、C++11 及以上,函数 "是否抛出异常" 通过 noexcept 规范,"抛什么异常" 已无合法的语法级规范(仅可通过文档说明)。

4. operator new与operator delete函数

😇newdelete 是用户进行动态内存申请和释放的操作符,而operator newoperator delete是系统提供的全局函数。

😘new 在底层调用operator new 全局函数来申请空间,delete 在底层通过operator delete全局函数来释放空间。

operator new:

  1. 该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回

  2. 申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。

cpp 复制代码
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// try to allocate size bytes
	void* p;
	while ((p = malloc(size)) == 0)
		if (_callnewh(size) == 0)
		{
			// report no memory
			// 如果申请内存失败了,这里会抛出 bad_alloc 类型异常
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
	return (p);
}

operator delete: 该函数最终是通过free来释放空间的。

cpp 复制代码
void operator delete(void *pUserData)
{
    _CrtMemBlockHeader * pHead; // Debug模式内存块的头部结构(存储内存块信息)
    RTCCALLBACK(_RTC_Free_hook, (pUserData, 0)); // RTC(运行时检查)钩子,调试用

    if (pUserData == NULL) // 空指针保护:释放NULL是安全的,直接返回
        return;

    _mlock(_HEAP_LOCK); /* 线程锁:阻塞其他线程,保证堆操作线程安全 */
    __TRY // MSVC异常保护块(类似 try)
    {
        pHead = pHdr(pUserData); // 从用户指针转换到内存块头部(Debug模式内存块有额外头部)
        _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)); 
        // 断言校验:确保内存块类型合法(Debug下触发)
        _free_dbg( pUserData, pHead->nBlockUse ); 
        // Debug版释放函数:记录内存释放、检测内存泄漏
    }
    __FINALLY // 类似 finally,保证锁一定会释放
    {
        _munlock(_HEAP_LOCK); /* 释放线程锁:无论是否抛异常,都解锁 */
    }
    __END_TRY_FINALLY
    return;
}

通过上述两个全局函数的实现知道:

😊operator new实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。

😚operator delete 最终是通过free来释放空间的。

5. new和delete的实现原理

一、内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似。

不同的地方是: new / delete 申请和释放的是单个元素 的空间,new[] / delete[] 申请的是连续空间 ,而且new在申请空间失败时会抛异常,malloc会返回NULL。

二、自定义类型

😋 new的原理

  1. 调用operator new函数申请空间。

  2. 在申请的空间上执行构造函数,完成对象的构造。

😜 delete的原理

  1. 在空间上执行析构函数,完成对象中资源的清理工作。

  2. 调用operator delete函数释放对象的空间。

🤪 new [N] 的原理

  1. 调用operator new[] 函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请。

  2. 在申请的空间上执行N次构造函数

🤗 delete[]的原理

  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

  2. 调用operator delete[] 释放空间,实际在operator delete[]中调用operator delete来释放空间

6. malloc / free 和 new / delete的区别

🤫 共同点是:都是从堆上申请空间 ,并且需要用户手动释放

🤔 不同点是:

  1. mallocfree是函数newdelete是操作符

  2. malloc申请的空间不会初始化,new可以初始化。

  3. malloc 申请空间时,需要手动计算空间大小并传递new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。

  4. malloc的返回值为void* , 在使用时必须强转,new不需要,因为new后跟的是空间的类型

  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。

  6. 申请自定义类型对象 时,malloc / free只会开辟空间,不会调用构造函数与析构函数 ,而new 在申请空间后会调用构造函数 完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中资源的清理释放。

💯如果这篇文章对你有用的话,请继续关注!

相关推荐
WBluuue21 小时前
AtCoder Beginner Contest 438(ABCDEF)
c++·算法
k***921621 小时前
【c++】多态
java·开发语言·c++
Cappi卡比21 小时前
C++性能优化
c++
深盾科技21 小时前
C++ 中 std::error_code 的应用与实践
java·前端·c++
wzfj1234521 小时前
Opaque Pointer / Incomplete Type
c++·算法·c
fpcc21 小时前
C++23中的模块应用说明之三深入分析和混合编程
c++·c++23
能量鸣新21 小时前
资源分享第三天
c语言·开发语言·c++·python·计算机视觉·c#
Morwit21 小时前
Qt CMake 项目中 QML 和资源文件的引入方式
开发语言·c++·qt
C++chaofan21 小时前
JUC 中 synchronized 的底层实现原理解析——Monitor
java·开发语言·c++·rust·ruby·juc·字节码