【C++初阶】:(5)内存管理

前言

C++赋予了开发者对内存的精细掌控权。从栈的自动分配到堆的动态管理,理解其内存模型是编写高效、安全程序的核心。掌握这些机制,方能避免内存泄漏与悬空指针,真正释放这门语言的力量。

1. 内存分布

1.1 代码演示及存储位置分析

首先看一段包含了全局变量,静态变量,局部变量以及动态内存分配的演示代码:

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";//指针变量(const:具有常性),用于存储字符串常量的地址
	//动态内存分配(堆上)
	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.2 内存的区域划分

内存主要划分为栈,堆,数据段(静态区),代码段(常量区),内存映射段(非本节重点,暂不讨论),各个区域的功能与变量的存储规则如下:

1.栈 :用于存储函数的局部变量,函数参数,返回地址 等。由编译器自动分配和释放,生命周期和函数的调用周期保持一致效率极高。内存地址从高向低分配(向下增长)。

2.堆 :用于程序运行时进行动态内存分配的区域 (如new/malloc)。由程序员手动申请和释放(或由垃圾回收机制管理),其生命周期由程序员控制,但管理不当容易导致内存泄漏。内存地址从低向高分配(向上增长)。

3.数据段

  • data段(初始化数据区) :存储已初始化的全局变量和静态变量(static)。
  • bss段(未初始化数据区) :存储未初始化或初始化为0的全局变量和静态变量。程序加载时由操作系统初始化为零。

4.代码段

  • text段 :存储程序执行的机器指令(即代码本身),通常是只读的。
  • 常量区 :通常位于代码段内,存储字符串常量const修饰的全局/静态常量。只读。

5.内存映射段 :内核将文件(如动态库)直接映射到此区域,也用于创建匿名映射以作为共享内存 或大块内存分配(如某些malloc实现)。

基于上面讲述的规则,完成下面的选择题:

(选项:A. 栈 B. 堆 C. 数据段 D. 代码段)

  1. globalVar(全局变量)→ C

  2. staticGlobalVar(静态全局变量)→ C

  3. staticVar(静态局部变量)→ C(静态变量无论是否在函数内,均存于数据段)

  4. localVar(局部变量)→ A

  5. num1(局部数组)→ A(数组是局部变量的一种,存储在栈上)

  6. char2(局部字符数组)→ A(数组本身是局部变量,存于栈上)

  7. *char2(数组内容)→ A(\"abcd\" 被拷贝到栈上的数组中,内容存于栈)

  8. pChar3(指针变量)→ A(指针是局部变量,存于栈上)

  9. *pChar3(指针指向的内容)→ D(指向字符串常量 \"abcd\",存于代码段)

  10. ptr1(指针变量)→ A(指针是局部变量,存于栈上)

  11. *ptr1(指针指向的内容)→ B(指向 malloc 分配的动态内存,存于堆上)

图解如下所示:

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

在C语言中,进行动态内存管理主要通过这四个函数:malloc,calloc,realloc和free,接下来为大家介绍这四个函数的功能实现。

2.1 动态内存管理函数功能实现

(1) malloc:最基础的动态内存分配

  • 函数原型void* malloc(size_t size);

  • 功能 :从 上申请 size字节的连续内存,不初始化内存内容(内存中保留随机值)。

  • 返回值 :成功返回指向内存的 void*指针,失败返回 NULL

  • 用法示例

cpp 复制代码
// 申请4个int大小的内存(int占4字节,共16字节)
int* p = (int*)malloc(4 * sizeof(int));  
if (p == NULL) { // 必须判空,防止内存分配失败
    perror("malloc failed");  
    return 1;  
}
// 使用内存(如赋值、访问)
p[0] = 10; p[1] = 20;  
// 后续需手动释放(配合free)

(2) calloc:带初始化的动态内存分配

  • 函数原型void* calloc(size_t num, size_t size);

  • 功能 :从 上申请 numsize字节的连续内存(总大小 num*size),并自动将所有字节初始化为0

  • 返回值 :成功返回指向内存的 void*指针,失败返回 NULL

  • 用法示例

cpp 复制代码
// 申请4个int大小的内存(共16字节),并初始化为0
int* p = (int*)calloc(4, sizeof(int));  
if (p == NULL) { 
    perror("calloc failed");  
    return 1;  
}
// 此时p[0]~p[3]均为0,可直接使用
p[0] = 100; // 覆盖初始化的0
// 后续需手动释放(配合free)

(3) realloc:动态调整已分配内存的大小

  • 函数原型void* realloc(void* ptr, size_t new_size);

  • 功能 :调整之前通过 malloc/calloc/realloc分配 的内存块的大小(可扩大/缩小)。若原内存块后有足够空间,直接扩展;否则会新申请一块内存 ,将原内存内容拷贝过去,再释放原内存。

  • 返回值 :成功返回指向新内存的 void*指针(可能与原 ptr相同,也可能不同),失败返回 NULL(原内存块仍有效,需注意判空)。

  • 用法示例

cpp 复制代码
int* p = (int*)malloc(4 * sizeof(int)); // 初始申请4个int
if (p == NULL) { /* 错误处理 */ }

// 现在需要扩容为6个int
int* new_p = (int*)realloc(p, 6 * sizeof(int));  
if (new_p == NULL) { 
    // realloc失败,原p仍有效,需处理(如释放p或继续使用)
    free(p); 
    perror("realloc failed");  
    return 1;  
}
p = new_p; // 更新指针为新内存地址
p[4] = 500; p[5] = 600; // 使用扩容后的内存
// 后续需手动释放(配合free)

(4) free:释放动态分配的内存

  • 函数原型void free(void ptr);*

  • 功能 :释放之前通过 malloc/calloc/realloc分配 的堆内存,将其归还给系统。若 ptrNULLfree无操作(安全)。

  • 注意 :释放后,ptr成为悬空指针 (指向已释放的内存),建议立即置为 NULL,避免误用。

  • 用法示例

cpp 复制代码
int* p = (int*)malloc(4 * sizeof(int));  
if (p == NULL) { /* 错误处理 */ }

// 使用内存...
free(p);   // 释放内存
p = NULL;  // 置空,避免悬空指针

2.2 核心区别总结

|---------|-------|------|-----------------|
| 函数 | 初始化 | 内存调整 | 典型场景 |
| malloc | 不初始化 | 不支持 | 快速申请未初始化内存 |
| calloc | 初始化为0 | 不支持 | 申请需初始化的内存(数组清零) |
| realloc | 依赖原内存 | 支持 | 动态调整内存大小(扩容/缩容) |
| free | 无 | 无 | 释放动态分配的内存 |

2.3 常见面试题

  1. malloc/calloc/ralloc的区别?

2.malloc的实现原理?

3. C++内存管理方式

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

3.1 new/delete操作内置类型

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	//申请单个整型变量的内存空间
	int* p1 = new int;
	//申请10个连续的整型变量的内存空间
	int* p2 = new int[10];
	delete p1;//销毁单个整型变量的内存空间
	delete[]p2;//销毁10个连续整型变量的内存空间


	//申请空间+初始化
	int* p3 = new int(0);
	int* p4 = new int[10] {0};
	int* p5 = new int[10] {1, 2, 3, 4, 5};

	//销毁
	delete p3;
	delete[]p4;
	delete[]p5;

	return 0;
}

调试结果如下:

3.2 new/delete操作自定义类型

cpp 复制代码
class A
{
public:
	A(int a1 = 0)
		:_a1(a1)
	{
		cout << "A(int a1 = 0)" << endl;
	}

	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}

	~A()
	{
		//delete _ptr;
		cout << "~A()" << endl;
	}

	void Print()
	{
		cout << "A::Print->" << _a1 << endl;
	}

	A& operator++()
	{
		_a1 += 100;

		return *this;
	}
private:
	int _a1 = 1;
};


int main()
{
	A* p1 = new A;
	A* p2 = new A(1);

	delete p1;
	delete p2;

	return 0;
}

运行结果如下:

可以观察到,当用new和delete操作自定义类型时,会自动调用相应的构造函数和析构函数,内置类型是不存在这个操作的。知道这个特点之后,可以利用这一点直接手动构造一个链表,就会方便很多

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(1);
	ListNode* n3 = new ListNode(1);
	ListNode* n4 = new ListNode(1);
	n1->next = n2;
	n2->next = n3;
	n3->next = n4;

	return 0;
}

所以在很多C++的OJ题目中往往会包含相应的构造函数:

当然,在上面的类中,所展示的构造函数属于一个参数的默认构造函数,当构造函数是两个甚至两个以上参数的默认构造函数时,new/delete操作符的使用方法如下:

cpp 复制代码
//两个参数的默认构造函数
class A
{
public:
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{
		cout << "A(int a1 = 0, int a2 = 0)" << endl;
	}

	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}

	~A()
	{
		//delete _ptr;
		cout << "~A()" << endl;
	}

	void Print()
	{
		cout << "A::Print->" << _a1 << endl;
	}

	A& operator++()
	{
		_a1 += 100;

		return *this;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};

int main()
{
	A* p1 = new A(1);
	A* p2 = new A(2,2);
	A* p3 = new A[3];
	return 0;
}

运行结果如下:

可以看到new几次就可以调用几次构造函数,当然这是建立在默认构造的函数的基础上的,如果构造函数不是默认构造函数(无参构造函数)时,比如半缺省参数的构造函数时,那么对连续自定义类型空间初始化时就必须要进行传参:

第一种写法:有名对象的拷贝构造

cpp 复制代码
//不是默认构造函数
class A
{
public:
	A(int a1, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{
		cout << "A(int a1 = 0, int a2 = 0)" << endl;
	}

	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}

	~A()
	{
		//delete _ptr;
		cout << "~A()" << endl;
	}

	void Print()
	{
		cout << "A::Print->" << _a1 << endl;
	}

	A& operator++()
	{
		_a1 += 100;

		return *this;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};

int main()
{
	A aa1(1, 1);
	A aa2(2, 2);
	A aa3(3, 3);

	A* p3 = new A[3]{aa1,aa2,aa3};
	return 0;
}

从运行结果可以看出,这次调用的是拷贝构造。但这种方法并不好。

第二种:利用匿名对象,编译器会进行优化

cpp 复制代码
int main()
{

	A* p4 = new A[3]{ A(1,1),A(2,2),A(3,3) };
	return 0;
}

第三种写法:利用多参数的隐式类型转换

cpp 复制代码
int main()
{

	A* p4 = new A[3]{ {1,1},{2,2},{3,3} };
	return 0;
}

由此可以感受到默认构造函数的重要性!!!

3.3 注意事项

**1.**申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间时,使用new[]和delete[],需要搭配使用,如果不匹配会导致内存泄漏或程序崩溃(尤其是对于自定义类型)。

  1. 在申请自定义类型的空间时,new会自动调用构造函数,delete会自动调用析构函数,而malloc和free不会。

  2. 当使用new申请动态空间出现异常时,可以对程序进行捕获异常,写法如下,要学会这种写法:

cpp 复制代码
int main()
{
	try
	{
		// throw try/catch 
		void* p1 = new char[1024 * 1024 * 1024];
		cout << p1 << endl;

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

		void* p3 = new char[1024 * 1024 * 1024];
		cout << p3 << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

运行结果:

只要一行代码发生错误,就会直接跳转到catch行进行错误提醒的输出。

相关推荐
XiYang-DING2 小时前
【Java SE】包装类(Wrapper Class)
java·开发语言
麦兜顶当当2 小时前
subprocess与子进程交互
java·开发语言·jvm
Zarek枫煜2 小时前
zig与C3的算法 -- 桶排序
c语言·嵌入式硬件·算法
等风来Boy2 小时前
JAVA集成CAS客户端总结
java·cas
青槿吖2 小时前
第二篇:Spring Boot进阶:整合异常处理、测试、多环境与日志,开发稳得一批!
java·spring boot·后端·spring·面试·sqlserver·状态模式
星如雨グッ!(๑•̀ㅂ•́)و✧2 小时前
Spring WebFlux 中的并发
java·spring·oracle
Rooting++2 小时前
C语言中的共用体应用场景
算法
東雪木2 小时前
java学习—— 8 种基本数据类型 vs 包装类、自动装箱 / 拆箱底层原理
java·开发语言·java面试
Lyyaoo.2 小时前
【JAVA基础面经】JVM、JRE、JDK
java·开发语言·jvm