目录

C学习笔记内存管理

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

**1、C/C++**内存分布

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

图片示例:

https://i-blog.csdnimg.cn/direct/e9bccdc0af2c4e5193319c48a7cefb6b.png

https://i-blog.csdnimg.cn/direct/9bf9b32ef57f48d6a9772d01dcedc6e5.png

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

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的区别?

特性malloccallocrealloc
功能分配指定大小的内存块分配并初始化指定数量、大小的内存块调整已分配内存块的大小
函数原型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的实现原理?

3、C++内存管理方式

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

3.1 new/delete操作内置类型

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****操作自定义类型

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 函数库代码

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 函数库代码

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个对象中资源的清理。 
  2. 调用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)来手动释放。

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