C++学习笔记——内存管理

**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.globalVar在哪里?____
2.staticGlobalVar在哪里?____
3.staticVar在哪里?____
4.localVar在哪里?____
5.num1 在哪里?____
6.char2在哪里?____
7.*char2在哪里?___
8.pChar3在哪里?____
9.*pChar3在哪里?____
10.ptr1在哪里?____
11.*ptr1在哪里?____

问题分析:

选项说明:​​
A. 栈:存储非静态局部变量、函数参数、返回值等
B. 堆:动态内存分配(malloc/calloc/realloc)
C. 数据段(静态区):存储全局数据和静态数据(包括全局变量和static修饰的变量)
D. 代码段(常量区):存储可执行代码和只读常量(如字符串常量)
具体分析:​​
1.globalVar:全局变量 → 数据段(静态区)→ C
2.staticGlobalVar:静态全局变量 → 数据段(静态区)→ C
3.staticVar:静态局部变量(虽在函数内,但由static修饰)→ 数据段(静态区)→ C
4.localVar:非静态局部变量 → 栈 → A
5.num1:非静态局部数组(在函数内)→ 栈 → A
6.char2:非静态局部字符数组(在函数内)→ 栈 → A
注意 :char2[] = "abcd" 在栈上分配数组并初始化,字符串"abcd"本身在常量区,但数组副本在栈上)
7.*char2:解引用char2(即char2数组的第一个元素)→ 存储在栈上(因为char2本身在栈)→ A
8.pChar3:非静态局部指针变量(在函数内)→ 栈 → A
9.*pChar3:解引用pChar3(指向字符串常量"abcd"的首元素,里面存首元素的地址)→ 字符串常量存储在代码段(常量区)→ D
10.ptr1:非静态局部指针变量(在函数内)→ 栈 → A
11.*ptr1:解引用ptr1(指向malloc动态分配的内存)→ 堆 → B
图片示例:

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

cpp 复制代码
void Test()
{ 
	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);
	// 这里需要free(p2)吗? 
	free(p3);
}

这里需要free(p2)吗?

答:不需要,而且绝对不能这样做。

原因分析:​​

realloc 函数的功能是重新分配之前由 malloc, calloc, 或 realloc 所分配的内存块(p2所指向的内存块)的大小。

当调用** int* p3 = (int*)realloc(p2, sizeof(int)*10); **时,会发生以下两种情况之一:

情况一(原地扩容): 如果 p2 指向的内存块后面有足够的空闲空间,realloc 会尝试在原地扩大这块内存。函数返回的地址值 p3 等于传入的地址值 p2。 此时,p2 和 p3 指向同一个内存块。

​情况二(异地迁移): 如果原地没有足够空间,realloc 会在堆的另一块区域分配一个足够大的新内存块(sizeof(int)*10),将旧内存块(p2指向的)中的数据原样拷贝过来,然后自动释放旧内存块。 函数返回的地址值p3 是一个新地址,与 p2 不同。

关键在于,​无论哪种情况,realloc 调用后,之前由 p2 指向的旧内存块都已经被系统接管,程序员不再拥有它的所有权,也不应该再尝试去释放它。​​

在情况一中,这块内存被扩大了,需要通过 p3 来管理。

在情况二中,这块内存已经被 realloc 函数内部自动 free 掉了。

因此,代码中只需要 free(p3) 即可,它释放的就是当前有效的那一块内存。如果在 realloc 后再 free(p2),会导致双重释放的错误,这是一种未定义行为,通常会导致程序崩溃。

牢记:永远只 free 由 malloc, calloc, realloc 返回的最新指针。在一次成功的 realloc 调用后,旧的指针就应该被立即视为无效,不应再使用或释放。

面试题总结

1. malloc/calloc/realloc的区别?

|----------------|-----------------------------------|-----------------------------------------|----------------------------------------------|
| 特性 | malloc | calloc | realloc |
| ​功能​ | 分配指定大小的内存块 | 分配并初始化指定数量、大小的内存块 | 调整已分配内存块的大小 |
| ​函数原型​ | void* malloc(size_t size); | void* calloc(size_t num, size_t size); | void* realloc(void* ptr, size_t new_size); |
| ​初始化​ | ​不初始化 内存内容,内容是随机值 | ​初始化 内存内容为全零​ | 根据情况,新增加的内存区域不会被初始化​ |
| ​参数​ | size:要分配的字节数 | num:元素个数 size:每个元素的字节数 | ptr:要调整的旧内存指针 new_size:新的总字节数 |
| ​使用场景​ | 需要分配内存且不关心初始值,或准备自行初始化时 | 需要分配数组或结构体并希望初始值全为零时 | 需要扩大或缩小之前动态分配的内存时 |

2. malloc的实现原理?

glibc中malloc实现原理

3、C++内存管理方式

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

此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

3.1 new/delete操作内置类型

cpp 复制代码
void Test()
{
    // 动态申请一个int类型的空间 
    int* ptr4 = new int;        // 分配空间,值未初始化(是随机值)
    delete ptr4;                // 释放空间

    // 动态申请一个int类型的空间并初始化为10 
    int* ptr5 = new int(10);    // 分配空间并初始化为10
    delete ptr5;                // 释放空间

    // 动态申请3个int类型的空间 
    int* ptr6 = new int[3];     // 这里3表示对象个数,值都未初始化
    delete[] ptr6;              // 释放连续的数组空间

    // 动态申请3个int类型的空间并初始化 
    int* ptr7 = new int[3] {1,2,3}; // 值初始化为1,2,3
    delete[] ptr7;                  // 释放连续的数组空间
}

1.初始化​: new 可以通过 new int(10) 的方式对单个元素进行初始化,而 malloc 无法方便地做到这一点。
​2.数组操作​: 使用 new[] 来分配数组,使用 delete[] 来释放数组。​必须匹配使用,否则行为未定义(尤其是对自定义类型,会导致析构函数调用次数错误)。

​3.对于内置类型​(如 int),new/delete 和 malloc/free 在功能上几乎没有区别,都是分配和释放内存。但 new 的语法更简单,且支持初始化。

3.2 newdelete****操作自定义类型

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);              // 1. 分配内存 2. 调用构造函数 A(1)
    free(p1);                      // 只释放内存,不调用析构函数
    delete p2;                     // 1. 调用析构函数 2. 释放内存

    // 对比二:内置类型(行为几乎相同)
    int* p3 = (int*)malloc(sizeof(int)); 
    int* p4 = new int; 
    free(p3); 
    delete p4; 

    // 对比三:对象数组
    A* p5 = (A*)malloc(sizeof(A)*10); // 分配10个A对象的内存,全部未构造
    A* p6 = new A[10];                // 分配内存,并调用10次默认构造函数
    free(p5);                         // 直接释放内存,10个对象都未析构
    delete[] p6;                      // 调用10次析构函数,然后释放内存

    return 0; 
}

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

free不会。

4. operator newoperator delete函数(重点)

new/delete 操作符​与operator new/operator delete全局函数之间的关系和区别。

1.new 和 delete 是 C++ 语言中的操作符(关键字) ​,供程序员直接使用来进行动态内存分配和释放。
2.operator new 和 operator delete 是系统提供的全局函数,是 new 和 delete 操作符在底层实现时所调用的工具。
​3.调用关系​:
new 操作符的底层行为可以分解为两步:

a. 调用 operator new 函数来分配内存。

b. 在分配好的内存上调用对象的构造函数。
delete 操作符的底层行为也可以分解为两步:

a. 调用对象的析构函数。

b. 调用 operator delete 函数来释放内存。
​4.功能定位​:

operator new/operator delete 的核心职责只是管理内存的分配和释放​(类似于更高级的 malloc 和 free),它们不负责调用构造函数和析构函数。调用构造和析构是 new/delete 操作符自己的逻辑。

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

operator new 函数库代码

cpp 复制代码
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
    // try to allocate size bytes
    void *p;
    while ((p = malloc(size)) == 0) // 底层调用malloc尝试分配size字节的内存
    {
        if (_callnewh(size) == 0) // 如果malloc失败,检查用户是否设置了"new-handler"函数
        {
            // report no memory
            // 如果用户没有设置new-handler,或者new-handler也无法解决问题,则抛出异常
            static const std::bad_alloc nomem;
            _RAISE(nomem); // 抛出std::bad_alloc类型异常
        }
        // 如果用户设置了new-handler(_callnewh返回非0),循环会继续,再次尝试malloc
    }
    return (p); // 分配成功,返回指向内存的指针
}

operator new底层调用 malloc

operator new 的本质是 malloc 的一个封装。它的主要工作就是调用 malloc(size) 来分配指定大小的内存。


失败处理机制(与malloc的关键区别)​​:
1.malloc 失败时直接返回 NULL 空指针。
2.operator new 失败时,会进入一个循环。 它首先检查用户是否通过 std::set_new_handler 设置了一个new-handler​ 函数。如果没有设置new-handler或它也无法解决问题,抛出 std::bad_alloc 异常。

operator delete 函数库代码

cpp 复制代码
void operator delete(void *pUserData)
{
    ... // 一些调试和线程安全代码
    if (pUserData == NULL) // 遵循C++标准:delete空指针是安全的,直接返回
        return;
    ...
    _free_dbg( pUserData, _NORMAL_BLOCK ); // 底层调用free的调试版本
    ...
    return;
}
// free的本质
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

operator delete底层调用 free​

1.operator delete 的本质是 free 的一个封装。它的主要工作就是调用 free(pUserData) 来释放内存。

​2.operator delete 检查传入的指针是否为空 (NULL),如果是则直接返回。这保证了 delete nullptr; 是安全的操作。

上述两个全局函数的总结:

通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果

malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施

就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。

5、newdelete****的实现原理

5.1 内置类型

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

5.2 自定义类型

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个对象中资源的清理。
  1. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。

6、定位new表达式(placement-new)****(了解)

概念​:

定位 new 是一种特殊形式的 new,它不在堆上分配新的内存,而是在已经分配好的原始内存空间上调用构造函数来初始化(创建)一个对象。

​使用格式​:

1.new (place_address) type

2.new (place_address) type(initializer-list) (带初始化列表,用于有参构造函数)

3.place_address 必须是一个指针,指向预先分配好的内存块。

​使用场景​:

最主要的用途是配合内存池。内存池为了提高效率,会预先分配大块内存,然后从中切分给小对象使用。这些内存块是"原始"的,没有经过构造函数的初始化。定位 new 就被用来在这些原始内存上构造对象。


注意​: 由于对象是手动构造的,其析构函数也必须手动调用。内存的释放也同样需要根据其分配方式(如 malloc 或 operator new)来手动释放。

cpp 复制代码
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};
// 定位new/replacement new 
int main()
{
	// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,
    // 因为构造函数没有执行
    //使用 malloc 分配原始内存
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A; // 使用定位new在p1指向的内存上构造A对象。
              // 注意:如果A类的构造函数有参数时,此处需要传参 
	p1->~A(); // 手动调用析构函数,销毁对象。对象所占的内存还在,但对象的生命周期结束
	free(p1); // 释放p1指向的内存块
    
    //使用 operator new 分配原始内存
	A* p2 = (A*)operator new(sizeof(A)); // 使用operator new分配内存
	new(p2)A(10); // 使用定位new在p2指向的内存上构造A对象
	p2->~A();     // 手动调用析构函数
	operator delete(p2); // 使用operator delete释放内存
	return 0;
}

**注意:**​使用 malloc/free 或 operator new/operator delete。必须确保分配和释放的方式匹配(malloc 配 free, operator new 配 operator delete)。在对象周期结束时,需要手动调用析构函数,销毁对象。此时,对象所占的内存还在,但对象的生命周期结束。使用匹配释放的方式才会释放之前对象所占的内存空间。

7、malloc/freenew/delete****的区别(重点)

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 在释放空间前会调用析构函数完成。(最主要不同)

相关推荐
一只乔哇噻4 小时前
java后端工程师进修ing(研一版‖day48)
java·开发语言·学习
涧榆vkQAQ4 小时前
52Hz——FreeRTOS学习笔记——延时函数
笔记·学习
regret~5 小时前
【笔记】Docker使用
笔记
Brookty5 小时前
【算法】滑动窗口(一)-长度最小的子数组
java·学习·算法·力扣·滑动窗口
大筒木老辈子5 小时前
MySQL笔记---数据库基础
数据库·笔记·mysql
Cre_Des6 小时前
[学习笔记][机器学习-周志华] 第1章 绪论
人工智能·笔记·学习·机器学习
岑梓铭6 小时前
考研408《操作系统》复习笔记,第四章(1)《文件管理概念》
笔记·考研·操作系统·408·os
开开心心loky6 小时前
[iOS] YYModel 初步学习
学习·ios·objective-c·cocoa
Lynnxiaowen6 小时前
今天继续学习nginx服务部署与配置
linux·运维·服务器·学习·nginx