C++内存管理深度剖析

ok,类和对象终于结束了,实在不容易,事实证明:世上无难事,只怕有心人。只要好好学,死学,硬学,酷酷学,就没有学不会的。

本博客章节是对过去一些知识点的总结,会用到之前学的函数栈帧,堆栈空间调用。

欧克,那废话不多说,直接开始今天的学习。


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在哪里?____

    staticGlobalVar在哪里?____

    staticVar在哪里?____

    localVar在哪里?____

    num1 在哪里?____

    char2在哪里?____

    *char2在哪里?___

    pChar3在哪里?____

    *pChar3在哪里?____

    ptr1在哪里?____

    *ptr1在哪里?___


答案(附带分析):

复制代码
staticVar在哪里?__C_      localVar在哪里?__A_
globalVar在哪里?__C_       staticGlobalVar在哪里?__C_
num1 在哪里?__A_
globalVar全局变量在数据段  staticGlobalVar静态全局变量在静态区
staticVar静态局部变量在静态区  localVar局部变量在栈区
num1局部变量在栈区

char2在哪里?_A___	            *char2在哪里?_A__
pChar3在哪里?_A___           *pChar3在哪里?__D_
ptr1在哪里?__A_               *ptr1在哪里?__B_
char2局部变量在栈区  
char2是一个数组,把后面常量串拷贝过来到数组中,数组在栈上,所以*char2在栈上
pChar3局部变量在栈区   *pChar3得到的是字符串常量字符在代码段
ptr1局部变量在栈区     *ptr1得到的是动态申请空间的数据在堆区

sizeof(num1) = _40__;数组大小,10个整形数据一共40字节
sizeof(char2) = __5_;  包括\0的空间       strlen(char2) = __4_;不包括\0的长度
sizeof(pChar3) = _4/8__; 分32位和64位 pChar3为指针    strlen(pChar3) = __4_;字符串"abcd"的长度,不包括\0的长度
sizeof(ptr1) = _4/8__;分32位和64位  ptr1是指针

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

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

创建共享共享内存,做进程间通信。(目前现在只需要了解一下,后续会讲)

  1. 堆用于程序运行时动态内存分配,堆是可以上增长的。

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

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


C语言中动态内存管理方式:(malloc/calloc/realloc/free)

我们在C语言中已用过内存函数向堆中申请空间,那这三个内存函数的区别是什么,还记得吗?
malloc(memory allocation)

c 复制代码
void* malloc(size_t size);

从堆中分配一块指定字节大小的连续内存空间。

成功:返回指向分配内存起始地址的 void* 指针(需强制类型转换后使用)。

失败:返回 NULL(如内存不足时)。

num:要分配的元素个数。

size:每个元素的字节大小。

cpp 复制代码
// 分配 10 个 int 类型的内存(共 40 字节,假设 int 占 4 字节)
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) { // 必须检查是否分配成功
    perror("malloc failed");
    exit(1);
}
// 此时 arr 中的值是随机的,需手动初始化
arr[0] = 10;
free(arr); // 释放内存
arr = NULL; // 避免野指针

calloc(contiguous allocation)

c 复制代码
void* calloc(size_t num, size_t size);

从堆中分配一块连续内存,内存大小为 num * size(即元素个数 × 每个元素的字节大小)。将分配的内存全部初始化为 0(这是与 malloc 的核心区别)。返回值同malloc

num:要分配的元素个数。

size:每个元素的字节大小。

cpp 复制代码
// 分配 10 个 int 类型的内存(共 40 字节),并初始化为 0
int* arr = (int*)calloc(10, sizeof(int));
if (arr == NULL) {
    perror("calloc failed");
    exit(1);
}
// 此时 arr[0] ~ arr[9] 全部为 0
free(arr);
arr = NULL;

realloc(re-allocation)

c 复制代码
void* realloc(void* ptr, size_t new_size);

调整已分配内存的大小(扩大或缩小),是动态内存管理的 "灵活工具"。

ptr:指向已通过 malloc/calloc/realloc 分配的内存地址(若为 NULL,则 realloc 等价于 malloc(new_size))。

new_size:调整后的内存字节大小(可大于 / 小于原大小)。

原地扩容:如果原内存块后面有足够的连续空间,直接扩展内存,返回原指针。

异地扩容:如果原内存块后面没有足够空间,会在堆中重新分配一块 new_size 大小的内存,将原内存中的数据拷贝到新内存,自动释放原内存,返回新指针。

缩容:直接截断原内存,返回原指针(截断的部分会被释放,但数据丢失)。

成功:返回调整后内存的起始地址(可能是原指针,也可能是新指针)。

失败:返回 NULL(此时原内存块不会被释放,需注意)。

cpp 复制代码
// 先分配 10 个 int 的内存
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
    perror("malloc failed");
    exit(1);
}
// 扩容为 20 个 int 的内存
int* new_arr = (int*)realloc(arr, 20 * sizeof(int));
if (new_arr == NULL) { // 扩容失败,原 arr 仍有效
    perror("realloc failed");
    free(arr); // 释放原内存
    exit(1);
}
arr = new_arr; // 指向新内存(或原内存)

// 缩容为 5 个 int 的内存
new_arr = (int*)realloc(arr, 5 * sizeof(int));
if (new_arr == NULL) {
    perror("realloc failed");
    free(arr);
    exit(1);
}
arr = new_arr;

free(arr);
arr = NULL;

C++内存管理方式

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

new/delete操作内置类型

代码案例:

cpp 复制代码
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
delete ptr4;
delete ptr5;
delete[] ptr6;
}

注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用

new[]和delete[],注意:匹配起来使用。


new和delete操作自定义类型

new/delete 和 malloc/free最大区别是 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()
{

	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;
}

注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。

为了验证结果我们在vs编译器下观看到底有没有调用构造

先进入new的底层

进来之后发现是运算符重载,然后在反汇编下看第二行确实用运算符重载调用了,其实底层就是malloc。在下图中蓝色行中可以看到调用了构造,delete也是这样的会先调析构后free。这里就不一 一看了。

operator new与operator delete函数

内置类型

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

new的原理

  1. 调用operator new函数申请空间
  2. 在申请的空间上执行构造函数,完成对象的构造

delete的原理

  1. 在空间上执行析构函数,完成对象中资源的清理工作
  2. 调用operator delete函数释放对象的空间

new T[N]的原理

  1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
  2. 在申请的空间上执行N次构造函数

delete[]的原理

  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

如果new申请空间失败了如何处理

在C语言中我们申请空间失败就会打印一下错误信息,并退出重新来。

c 复制代码
#include <stdio.h>
#include <stdlib.h> // malloc、free、exit
#include <errno.h>  // errno
#include <string.h> // strerror

int main() {
    //注意这是一个案例,内置类型 如:int也是可以
    // 尝试分配一个极大的内存(必然失败,比如 SIZE_MAX 是无符号最大值)
    size_t huge_size = (size_t)-1; // 等价于 SIZE_MAX(定义在 stdlib.h)
    int* ptr = (int*)malloc(huge_size);

    // 核心:检查返回值是否为 NULL
    if (ptr == NULL) {
        // 方式1:用 perror 打印错误(推荐,简单)
        perror("malloc failed");

        // 方式2:用 strerror 打印错误(更灵活,可自定义输出格式)
        fprintf(stderr, "malloc failed: %s (errno: %d)\n", strerror(errno), errno);
        // 失败后:终止程序(exit(1) 表示异常退出,0 表示正常退出)
        exit(EXIT_FAILURE); // EXIT_FAILURE 等价于 1,定义在 stdlib.h
    }

    // 分配成功后的逻辑(这里不会执行)
    printf("内存分配成功,地址:%p\n", (void*)ptr);
    free(ptr); // 释放内存
    ptr = NULL; // 避免野指针

    return 0;
}

在C++中我们申请空间一般情况下不会失败,只要别太过分,程序不会崩溃,如果申请空间失败了那就要抛异常了。这里大概描述一下,以后会详细讲解:抛异常。

C++ 标准规定,默认的 new 表达式(如 int* p = new int;)在内存分配失败时,会抛出 std::bad_alloc 异常(该异常继承自 std::exception,定义在 头文件),而不会返回 NULL。

因此,我们需要用 try-catch 块捕获这个异常,进行错误处理。

cpp 复制代码
#include <iostream>
#include <new>       // 必须包含:std::bad_alloc 定义
#include <exception> // std::exception(可捕获所有标准异常)

int main() {
    try {
        // 尝试分配一个极大的内存(必然失败,触发异常)
        // 注意:size_t 是无符号整数,这里用一个超大值(如 SIZE_MAX)
        const size_t huge_size = static_cast<size_t>(-1); // 等价于 SIZE_MAX(最大无符号值)
        int* p = new int[huge_size]; // 分配失败,抛出 std::bad_alloc

        // 如果分配成功,后续逻辑(这里不会执行)
        std::cout << "内存分配成功!" << std::endl;
        delete[] p; // 释放数组内存(注意用 delete[],对应 new[])
    }
    // 捕获具体的 bad_alloc 异常(推荐:精准处理内存分配失败)
    catch (const std::bad_alloc& e) {
        // e.what() 返回异常的描述信息(实现相关,如 "std::bad_alloc")
        std::cerr << "内存分配失败:" << e.what() << std::endl;
    }
    // 捕获所有标准异常(兜底:防止其他异常未被捕获)
    catch (const std::exception& e) {
        std::cerr << "发生异常:" << e.what() << std::endl;
    }
    // 捕获所有未知异常(最后兜底)
    catch (...) {
        std::cerr << "发生未知异常!" << std::endl;
    }

    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在释放空间前会调用析构函数完成空间中资源的清理释放
相关推荐
BBB努力学习程序设计2 小时前
Java Scanner完全指南:让程序与用户对话
java
BBB努力学习程序设计2 小时前
Java面向对象编程:封装、继承与多态深度解析
java
Lucky_Turtle2 小时前
【Springboot】解决PageHelper在实体转Vo下出现total数据问题
java·spring boot·后端
Mr.朱鹏2 小时前
大模型入门学习路径(Java开发者版)下
java·python·学习·微服务·langchain·大模型·llm
万法若空2 小时前
【wxWidgets教程】控件基础知识
c++·gui·wxwidgets·事件处理
期待のcode2 小时前
验证码实现
java·vue.js
老华带你飞2 小时前
志愿者服务管理|基于springboot 志愿者服务管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
图形学爱好者_Wu2 小时前
每日一个C++知识点|模板
c++
汤姆yu2 小时前
基于springboot的宠物服务管理系统
java·spring boot·后端