内存管理:掌控对象的诞生与销毁

本文代码已同步Github

一、C/C++内存分布

C/C++ 程序运行时的虚拟内存空间(从低地址到高地址)通常划分为以下几个核心区域:

1. 代码段(.text)

  • 存放内容:CPU 执行的机器指令(函数体二进制代码)。
  • 特点:只读,防止程序意外修改自身指令。

2. 常量区(.rodata)

  • 存放内容 :字符串常量、const 修饰的全局/静态变量(通常)。
  • 特点:只读,尝试写入会引发段错误(Segment Fault)。

3. 数据段(.data)

  • 存放内容 :已初始化且初值不为0 的全局变量和静态局部变量(包括 static 修饰的变量)。
  • 特点:程序启动时从可执行文件加载,生命周期贯穿整个程序运行。

4. BSS段(.bss)

  • 存放内容 :未初始化或初值为0的全局变量和静态局部变量。
  • 特点:不占用可执行文件磁盘空间,程序启动时由操作系统统一清零初始化。

5. 堆(Heap)

  • 存放内容 :通过 malloc(C)/ new(C++)动态分配的内存。
  • 特点 :由低地址向高地址增长,需要手动释放(free/delete,或 C++ 智能指针自动管理)。

6. 内存映射段(Memory Mapping Segment)

  • 存放内容 :动态链接库(.so/.dll)、mmap 映射的大文件或匿名内存。
  • 特点:位于堆和栈之间,用于文件映射和线程间共享内存。

7. 栈(Stack)

  • 存放内容:局部变量、函数参数、返回地址、栈帧(Frame)信息。
  • 特点:由高地址向低地址增长,由编译器自动分配和释放,效率高但容量有限(通常几 MB)。

二、C/C++内存管理方式

I、C语言管理方式

在C语言中,通常使用malloc,calloc,reallocfree来管理内存;

首先我们要知道malloc,calloc,realloc三者之间的区别

malloc申请的空间是未初始化的,速度快;

calloc可以开空间并将申请的区域初始化为0;

realloc通常用于扩容,分为原地扩容异地扩容,异地扩容成功后会释放掉之前申请的空间,但是如果扩容失败就会导致内存泄露,因此要用临时指针进行接收

free就是释放掉我们申请的空间,对于申请的空间,一定要free


下面我们来看一道题目:

cpp 复制代码
void Test()
{
	int* p1 = (int*)calloc(4, sizeof(int));
	int* p2 = (int*)realloc(p1, sizeof(int) * 10);

	//需要free(p1)吗?
	free(p2);
}

答案是不需要

如果p2是原地扩容,那么就会直接返回p1的地址,最终释放的是同一块地址,无需释放p1

如果p2是异地扩容,那么会将原空间内容拷贝到新空间,接着释自动释放掉原空间,并将p1置为空,无需手动free

||、C++管理方式

1、new和delete

由于C++是兼容C语言的,因此C语言的malloc,calloc,realloc, free在C++编译环境中依然能正常使用;

C++中通过newdelete操作符进行动态内存管理

下面通过代码来了解newdelete的使用方法

2、内置类型

cpp 复制代码
int main()
{
	//申请一个int类型空间
	int* p1 = new int;
	delete p1;

	//申请一个int类型空间并初始化为1
	int* p2 = new int(1);
	delete p2;

	//申请5个int类型的空间
	int* p3 = new int[5];
	delete[] p3;

	//申请5个int类型的空间并初始化
	int* p4 = new int[5] {1, 2, 3, 4, 5};
	delete[] p4;

	//申请10个int类型的空间并初始化
	//后续没有被初始化的值均为0
	int* p5 = new int[10] {1, 2, 3, 4, 5};
	delete[] p5;

	return 0;
}

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

​ 申请和释放连续的空间,使用newdelete

必须要匹配使用申请空间和释放空间

3、自定义类型

对于自定义类型,newdelete最大的区别就是new会在申请完空间之后自动调用构造函数;

delete会先调用析构函数然后再释放空间;

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

void Test02()
{
	//对于自定义类型
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A(1);//自动调用构造函数和析构函数
	free(p1);
	delete p2;

	A* p3 = new A[10];
	delete[] p3;
}

我们通过程序运行结果来看是否互自动调用构造函数和析构函数

和预想一样,确实自动调用了构造函数和析构函数;

这就是C++中自己的内存管理方式,我们继续向下看;


在C语言中,对于malloc申请的空间通常用临时指针来接收返回值判断是否申请失败;

而在C++中new申请之后没有返回值,要采用捕获异常的方式来判断,如下所示

cpp 复制代码
//x86环境
int main()
{
	try
	{
		// throw try/catch 
		//相当于申请1G	
		void* p1 = new char[1024 * 1024 * 1024];
		cout << p1 << endl;

		void* p2 = new char[1024 * 1024 * 1024];
		cout << p2 << endl;

	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

在上面程序中,我们想要在堆上申请2G空间,在x86环境下,看能否申请成功

程序打印出来了bad allocation,说明程序申请失败了,只申请了1G空间;

当前阶段,只需会捕获异常即可!

三、实现原理

newdelete是用户进行动态内存申请和释放的操作符,operator newoperator delete

系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过

operator delete全局函数来释放空间

  • new/delete 操作符 :这是C++的关键字,是你在代码中直接使用的入口;它负责编排整个流程,但不直接进行内存分配
  • operator new/operator delete 函数 :这是C++标准库提供的全局函数new 操作符会调用它们来执行真正的内存分配和释放。可以把它看作是 malloc/free 的"C++风格包装器"。
  • malloc/free 函数 :这是C标准库的函数,是底层内存管理的最终执行者。operator newoperator delete 在底层最终调用的就是它们。

a、对于普通对象

也就是说,当我们写下一个new T时:

1、编译器首先会分配内存,调用operator new(sizeof(T))函数,这个函数又会调用malloc,让其从堆上分配一块空间,如果分配失败,operator new会抛出一个std::bad_alloc异常,并不会像malloc那样返回NULL

2、接着会调用T的构造函数,完成对对象的初始化

当我们写下一个delete T时:

1、编译器首先会调用析构函数,释放对象内部申请的资源;

2、接着调用operator delete(void* p)这个函数,这个函数内部会调用free,将申请的空间进行释放

b、对于对象数组

new T[N]

1、编译器首先会调用operator new[],实际上会调用operator new来申请内存

注意:编译器通常会多申请一块小空间,放在这个内存块前面,存储数组元素个数

2、在申请的内存,依次调用n次析构函数,初始化数组中的每个对象;

delete[] p

1、首先会先读取数组元素个数,从p指向位置向前偏移,读取存储元素个数

2、根据n的大小来确定调用析构函数的次数,清理对象

3、最终调用operator delete[](内部会调用operator deletefree)释放整块内存(包括存储元素个数的部分)

特别注意delete[] 依赖这个存储的"元素个数"来知道要调用多少次析构函数;因此,用 new\[\] 分配的内存,必须用 delete\[\] 释放 ,否则可能导致只析构了第一个对象,造成内存泄漏或其他未定义行为。对于没有自定义析构函数的简单类型(如 int),编译器可能不会存储这个计数。

四、定位new表达式

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

使用格式:new(place_address)type或者new(place_address)type(initializer-list)

place_address必须是一个指针,initializer-list是类型的初始化列表

使用场景:定位new表达式在实际使用中一般是配合内存池使用;因为内存池分配出来的内存没有初始化,那就需要 使用new的定义表达式显示调用构造函数

我们通过下面代码来体会显示调用构造函数

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

int main()
{
	//此时p1指向的是一块分配好的空间,但并没有被初始化,因为没有执行构造函数
	A* p1 = (A*)malloc(sizeof(A));

	new(p1)A;//如果A类的构造函数有参数时,需要传参
	p1->~A();//析构函数

	//显示调用析构函数之后,可以直接使用free,不过建议配套使用
	free(p1);


	return 0;
}

先来看上面这段程序运行结果:

上述这个例子的构造函数是无参的,下面来观察带参构造函数

cpp 复制代码
int main()
{
	A* p2 = (A*)operator new(sizeof(A));

	new(p2)A(10);//显示调用构造函数
	p2->Print();
	p2->~A();//析构函数

	operator delete (p2);//底层调用free
	return 0;
}

完全没有问题!和预期结果一致

五、malloc/free与new/delete的区别

在C语言中,malloc,calloc,realloc,free管理内存的都是函数

而C++中,newdelete关键字

下面就来分析C/C++中malloc/freenew/delete的区别

首先来看共同特点:都是从上申请空间,并且需要手动释放!

不同点:

  • mallocfree是函数,而newdelete是操作符
  • malloc申请的空间不会初始化,new可以初始化
  • malloc申请的空时,需要手动计算空间大小并传递,new只需在后面跟上类型即可,如果是多个对象, 中指定对象大小即可
  • malloc的返回值为void*,在使用时必须强转,new不需要,new后面就是空间类型
  • malloc申请空间失败时,返回的是NULL,因此必须要判空处理;new不需要判空,需要捕获异常
  • 申请自定义类型时,malloc/free只会申请/释放空间,不会调用构造函数和析构函数,而new在申请完空间后会调用构造函数完成对象的初始化,delete会在释放前调用析构函数清理对象资源,最终释放整块内存

如果觉得有帮助,可以关注Github项目持续更新