【C++笔记】内存管理

前言:

在C / C++ 中,内存主要分为五个区域:栈(Stack)、堆(Heap)、全局/静态存储区、常量存储区和代码区,我们通常讨论的"管理",主要集中在栈和堆。

一、内存分布

1.1 内存区域分布

在C 和 C++ 程序中,内存被划分为几个关键区域,每个区域都有其特定的用途和管理方式

详情说明:

1. 栈:用于存储非静态局部变量、函数参数和返回值等数据,其内存空间采用向下增长的方式分配。

2. 内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库,用户可使用系统接口创建共享共享内存,做进程间通信。

3. 堆:用于程序运行时动态内存分配,其内存空间采用向上增长的方式分配。

4. 数据段: 用于存放全局变量和静态变量。

5. 代码段:用于存储可执行的代码/只读常量。

1.2 实战演示

cpp 复制代码
#include <iostream>
#include <cstdlib>
using namespace std;
 
int globalVar = 1;              // 全局变量
static int staticGlobalVar = 1; // 静态全局变量
 
void Test()
{
    static int staticVar = 1;    // 静态局部变量
    int localVar = 1;           // 局部变量
    
    int num1[10] = {1, 2, 3, 4}; // 局部数组
}

题组一:

选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

① globalVar在哪里?____

② staticGlobalVar在哪里?____

③ staticVar在哪里?____

④ localVar在哪里?____

⑤ num1 在哪里?____

解答:

① globalVar 为全局变量,存储在数据段,故而选择C。

② staticGlobalVar 为静态全局变量,存储在数据段,故而选择C。

③ staticVar 为静态局部变量,存储在数据段,故而选择C。

④ localVar 为局部变量,存储在栈,故而选择A

⑤ num1 为数组名,num1 是一个局部数组,它的生命周期随函数的调用而开始,随函数的结束而销毁,其内存空间完全位于函数的栈帧中,故而选择A

cpp 复制代码
#include <iostream>
#include <cstdlib>
using namespace std;
 
void Test()
{
    
    char char2[] = "abcd";       // 局部字符数组
    const char* pChar3 = "abcd"; // 指向常量字符串的指针
    
    int* ptr1 = (int*)malloc(sizeof(int) * 4);  // 动态分配
    int* ptr2 = (int*)calloc(4, sizeof(int));   // 动态分配并初始化为0
    int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); // 重新分配
    
    free(ptr1);
    free(ptr3);
}

题组二:

选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

① char2在哪里?____

② *char2在哪里?___

③ pChar3在哪里?____

④ *pChar3在哪里?____

⑤ ptr1在哪里?____

⑥ *ptr1在哪里?____

解答:

① char2 : 为局部数组,数组本身在栈上,字符串内容被拷贝到了栈中,故而选择A。

② *char2 : char2为数组名,在非sizeof(char2) 和 &char2时,指向数组的首元素,所以解引用指向的是数组的首元素,因为数组在栈上,所以元素也在栈上, 故而选择A。

③ pChar3:指向常量字符串的指针,局部指针变量本身,存放在栈上,故而选择A

注意:const放在指针名左边,指的是不能修改指针所指向的内容。

④ *pChar3: pChar3存储的是 "abcd" 位于常量区首个字符的地址,当我们对它解引用时,访问的就是它指向的那个数据,也就是位于代码段 (常量区) 的字符 'a',故而选择D。

⑤ ptr1: 局部指针变量本身,存放在栈上,故而选择A。

⑥ *ptr1: ptr指向的是malloc在堆上分配的内存空间的地址,所以*ptr1解引用得到是**malloc 分配的内存上存储的元素,其存储在堆上,故而选择B。**

二、C++ 内存管理方式

2.1 简单回顾C语言内存管理

C语言动态内存分配,它的核心价值在于:打破了"数组大小必须在写代码时确定"的限制,允许程序在运行时按需申请和释放内存。

三种函数的区别:

函数 初始化 参数形式 使用场景
malloc 不初始化 malloc(size) 普通内存分配
calloc 初始化为0 calloc(count, size) 需要零初始化的数组
realloc 保持原数据 realloc(ptr, new_size) 调整已分配内存大小

代码示例:

cpp 复制代码
#include <iostream>
#include <cstdlib>
using namespace std;
 
void TestCMemory() 
{
    // malloc - 分配指定字节数的内存,不进行初始化
    int* p1 = (int*)malloc(sizeof(int) * 4);
    
    // calloc - 分配并初始化为0
    int* p2 = (int*)calloc(4, sizeof(int));  // 分配4个int,初始化为0
    
    // realloc - 重新分配内存大小
    int* p3 = (int*)realloc(p2, sizeof(int) * 10);
    
    // 注意:p2已被realloc处理,不需要再free
    free(p1);
    free(p3);  // 释放重新分配后的内存
}

2.2 C++的新内存管理方式

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

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

2.2.1 new 和 delete 的基本语法

在 C++ 中,newdelete 是运算符,而不是函数,它们不仅负责分配内存,更重要的是负责对象的生命周期管理(构造与析构)。

基本语法如下代码所示:

1. 单个对象的分配与释放

cpp 复制代码
// 1.在堆上申请一个int 大小的空间
int *p = new int;       // 分配空间,值未初始化(可能是垃圾值)
int *q = new int(10);   // 分配空间,并初始化为 10

// 2. 使用
*p = 5;

// 3. 释放
delete p;
delete q;

2. 数组的分配与释放 (注意方括号 [])

cpp 复制代码
//1. 在堆上申请10个整形的空间
int* ptr1 = new int[10];

//2.申请多个对象+初始化
int* ptr2 = new int[10] {0};

//3.申请多个对象+部分初始化,剩余未指定的部分默认初始化未0。
int* ptr3 = new int[10] {1, 2, 3, 4, 5};

//释放多个整形的空间
delete[] ptr1;

delete[] ptr2;

delete[] ptr3;

2.2.2 核心区别:new vs malloc

这是面试和实际开发中最常问的问题,为什么 C++ 要发明 new,而不使用 malloc

最根本的原因是:类 (Class) 和对象 (Object)。

1. 在C++中,对于自定义类型使用 关键字new 和 delete ,会自动调用默认构造函数和析构函数。

2. 在C语言中,对于malloc 、calloc 和 free 函数,不会自动调用构造函数和析构函数。

代码示例:使用new时构造函数的自动调用,使用delete时析构函数的自动调用。

cpp 复制代码
class Student
{
public:
	Student() { cout << "构造函数:学生出生了!" << endl; }
	~Student() { cout << "析构函数:学生毕业了(销毁)!" << endl; }
};

int main() 
{
	cout << "--- 使用 malloc ---" << endl;

	// malloc 只分配了内存,是一块"死"的内存,没有任何初始化
	Student* s1 = (Student*)malloc(sizeof(Student));

	// 仅仅归还内存,没有做任何清理工作
	free(s1); 

	cout << "\n--- 使用 new ---" << endl;

	// new 分配内存 + 自动调用构造函数
	Student* s2 = new Student();

	// delete 自动调用析构函数 + 释放内存
	delete s2;

	return 0;
}

打印结果如下:

2.3 深入解析new和delete的重要性

为什么使用new和delete申请动态内存或释放资源时,调用构造函数和析构函数重要的原因?

简单来说:mallocfree 只管内存字节,而构造和析构函数管的是对象的状态和资源。

2.3.1. 核心层面:维护"不变量"

在 C 语言的 struct 中,数据是公开的,你可以随便改。但在 C++ 的 class 中,我们强调"封装",一个对象必须处于"合法状态"才能使用。

场景: 假设你有一个 BankAccount(银行账户)类。

如果不调构造函数 (使用malloc): 内存里的值是随机的垃圾值。余额可能是 -842150451,账号可能是乱码,如果不小心用了这个对象,系统直接逻辑错误。

如果调构造函数 (使用new): 构造函数会把余额初始化为 0.0,这保证了你拿到对象的那一刻,它就是可用的、安全的。

2.3.2. 资源层面:资源获取即初始化

这是 C++ 最核心的设计哲学,对象通常不仅仅包含 intfloat,它们往往持有外部资源,如果不调用构造函数和析构函数,这些外部资源将永远无法初始化和释放!

假设你有一个类 Student,它的名字是动态分配的:

cpp 复制代码
class Student
{
    char* name; // 指针,指向另一块堆内存
public:
    Student(const char* n)
     {
        // 构造函数:向系统申请 100 字节存名字
        name = new char[100]; 
        strcpy(name, n);
    }

    ~Student() 
    {
        // 析构函数:归还那 100 字节
        delete[] name; 
    }
};

现在我们看看 new/deletemalloc/free 的区别:

A. 正确做法 (new + delete):

Student* s = new Student("Tom");

delete s;

①分配 Student 对象本身的内存(比如 8 字节存指针)。

②自动调用构造函数 -> 分配 name 的 100 字节。

③自动调用析构函数 -> 释放 name 的 100 字节。

④释放 Student 对象本身的内存。

结果:完美,无泄漏。

B. 灾难做法 (malloc + free):

Student* s = (Student*)malloc(sizeof(Student));

free(s);

①分配 Student 对象本身的内存。

②构造函数没执行! s->name 是一个随机的野指针。如果你尝试打印名字,程序崩溃。

③析构函数没执行! 假设你手动修复了 name 指针让程序跑下去了,现在你调用 free。

④它只回收了 Student 对象那 8 字节。

结果 :name 指向的那 100 字节(如果分配了的话)没人管了,变成了孤儿内存(内存泄漏)。

2.4 注意事项

2.4.1 易错点1

如果一个类没有默认构造函数(即必须带参数才能创建对象),你直接写 new ClassName[10],编译器不仅仅是不调用构造函数,而是直接报错(编译失败)。

面对这种情况,正如你所说,我们需要"手动分配空间,再手动创建对象"。

方案一:手动创建多个对象

cpp 复制代码
class B
{
public:
	B(int b1, int b2)
		:_b1(b1)
		, _b2(b2)
	{
		cout << "B(int _b1,int _b2)" << endl;
	}

	// 新增:显式拷贝构造函数
	B(const B& other)
		: _b1(other._b1)
		, _b2(other._b2)
	{
		cout << "Copy B()" << endl;
	}

	~B()
	{
		cout << "~B()" << endl;
	}
private:
	int _b1;
	int _b2;
};

int main()
{
	//1.方法一:手动创建三个对象
	B b1(1, 1);
	B b2(2, 2);
	B b3(3, 3);

	//通过调用拷贝构造函数,将创建的对象拷贝到堆上
	B* pb1 = new B[3]{ b1,b2,b3 };
	delete[] pb1;

	return 0;
}

方案二:使用匿名对象

cpp 复制代码
class B
{
public:
	B(int b1, int b2)
		:_b1(b1)
		, _b2(b2)
	{
		cout << "B(int _b1,int _b2)" << endl;
	}

	// 新增:显式拷贝构造函数
	B(const B& other)
		: _b1(other._b1)
		, _b2(other._b2)
	{
		cout << "Copy B()" << endl;
	}

	~B()
	{
		cout << "~B()" << endl;
	}
private:
	int _b1;
	int _b2;
};

int main()
{
	//2.方法二:使用匿名对象
	
	// 通过调用拷贝构造函数,将匿名对象拷贝到堆上,但编译器会进行优化到调用直接构造
	B* pb2 = new B[3]{ B(1,1),B(2,2),B(3,3) };

	delete[] pb2;

	return 0;
}

2.4.2 易错点2

如果当动态内存申请失败时,通常需要进行异常处理,但是一般对于日常使用来说,动态内存开辟不会失败。

代码示例:动态内存申请失败,进行捕获异常操作。

cpp 复制代码
int main()
{
	try
	{
		void* p = new char[0xFFFFFFFFFFFFFFFFULL];
	}
	catch(const exception &e)
	{
		cout << e.what()<<endl;
	}
    
    return 0;
}

三、new 和 delete 的底层

要深入理解 newdelete,你必须把它们拆解开。在编译器眼里,new 和 delete 并不是一个单一的操作,而是一套组合拳。

3.1 了解两个关键函数

operator new 与 operator delete函数

operator new

函数原型:void* operator new(size_t size);

职责:只负责分配字节,不负责初始化(不调构造函数)。

地位:它是 malloc 的 C++ 封装版。

operator delete:

函数原型:void operator delete(void* p);

职责:只负责归还字节,不负责清理(不调析构函数)。

地位:它是 free 的 C++ 封装版。

3.2 new和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和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数。

②new在底层调用operator new全局函数来申请空间,通过构造函数来初始化资源

③delete在底层通过 operator delete全局函数来释放空间,通过析构函数来释放资源。

四、new/delete 和 malloc/free 的区别

malloc/free和new/delete的共同点在于:它们都是从堆内存中动态申请空间,并且都需要用户手动释放内存,二者的主要区别如下:

①语法差异:malloc/free是标准库函数,而new/delete是C++操作符

②初始化处理:malloc不会初始化申请的内存空间,而new可以自动完成初始化

③空间计算:使用malloc时需要手动计算并传入所需空间大小,而new只需指定类型即可;对于数组对象,new只需在[]中指明元素数量

④返回值类型:malloc返回void*指针,使用时需要强制类型转换;new直接返回对应类型的指针

⑤错误处理:malloc失败时返回NULL,需要检查返回值;new失败时会抛出异常,需要进行异常捕获

⑥对象处理:对于自定义类型对象,malloc/free仅分配/释放内存,不调用构造/析构函数;new会调用构造函数初始化对象,delete会先调用析构函数再释放内存

既然看到这里了,不妨关注+点赞+收藏,感谢大家,若有问题请指正。

相关推荐
寻寻觅觅☆8 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
fpcc8 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
ceclar1239 小时前
C++使用format
开发语言·c++·算法
lanhuazui1010 小时前
C++ 中什么时候用::(作用域解析运算符)
c++
charlee4410 小时前
从零实现一个生产级 RAG 语义搜索系统:C++ + ONNX + FAISS 实战
c++·faiss·onnx·rag·语义搜索
老约家的可汗10 小时前
初识C++
开发语言·c++
crescent_悦10 小时前
C++:Product of Polynomials
开发语言·c++
小坏坏的大世界11 小时前
CMakeList.txt模板与 Visual Studio IDE 操作对比表
c++·visual studio
乐观勇敢坚强的老彭11 小时前
c++寒假营day03
java·开发语言·c++
愚者游世12 小时前
brace-or-equal initializers(花括号或等号初始化器)各版本异同
开发语言·c++·程序人生·面试·visual studio