本文是C++内存管理部分的学习分享 希望能够对你有所帮助~
那咱们废话不多说,直接开始吧!
1. 内存分布
1.1 引入
在开始之前,我们先来看一道题目:
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.代码段(常量区)
globalVar在哪里?____
staticGlobalVar在哪里?____
staticVar在哪里?____
localVar在哪里?____
num1 在哪里?____
char2在哪里?____
*char2在哪里?___
pChar3在哪里?____
*pChar3在哪里?____
ptr1在哪里?____
*ptr1在哪里?____
思考的差不多了吗?答案揭晓:
变量/表达式 | 存储区域 |
---|---|
globalVar | C(数据段) |
staticGlobalVar | C(数据段) |
staticVar | C(数据段) |
localVar | A(栈) |
num1 | A(栈) |
char2 | A(栈) |
*char2 | A(栈) |
pchar3 | A(栈) |
*pchar3 | D(代码段/常量区) |
ptr1 | A(栈) |
*ptr1 | B(堆) |
其实理解起来并不困难:
全局变量以及静态局部变量都在数据段上;
在函数中除了静态成员变量外,其他的变量都存在这个函数的栈帧中;(因此我们经常说出了函数成员变量也随之消失,本质上还是因为函数的栈帧内存归还给系统了)
只能读、多个进程共享的(这部分会在后续的学习中分享)且大小不变的数据都放在代码段中;
动态开辟的空间就是开在堆上的
1.2 内存区域划分
C/C++ 程序的内存区域划分如下:
内存区域 | 描述 |
---|---|
栈 | 存储非静态局部变量、函数参数、返回值等,向下增长。 |
堆 | 动态内存分配区域,向上增长。 |
数据段(静态区) | 存储全局变量、静态变量(如 staticGlobalVar 和 staticVar )。 |
代码段(常量区) | 存储可执行代码和只读常量(如字符串常量 "abcd" )。 |
内存映射段 | 用于文件映射、动态库加载等,用户可通过系统接口创建共享内存。 |
2. C语言动态内存管理
2.1 函数区别
-
malloc:分配指定字节的未初始化内存。
-
calloc:分配并清零初始化内存(参数为元素个数和大小)。
-
realloc:调整已分配内存的大小(可能迁移数据)。
2.2 注意事项
-
realloc
后,原指针p2
可能失效,需直接释放p3
(见示例代码)。
void Test ()
{
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
free(p3 );
}
3. C++内存管理(new/delete)
3.1 基本用法
int* ptr4 = new int; // 分配一个 int
int* ptr5 = new int(10); // 分配并初始化为 10
int* ptr6 = new int[3]; // 分配 3 个 int 的数组delete ptr4; // 释放单个对象
delete[ ] ptr6; // 释放数组
3.2 自定义类型
-
new:调用构造函数。
-
delete:调用析构函数。
class A { /* ... */ };
A* p2 = new A(1); // 调用构造函数
delete p2; // 调用析构函数
3.3 注意事项
看完上述对于内置类型以及自定类型的基本用法后,也许思维活跃的你就会想到:那如果我创建了多个自定义类型变量,但我就只用delete还能够达成我释放内存的目的吗?
class A
{
public:
A(int a=1,int b=1)
{
_a = a;
_b = b;
}
~A()
{
cout<<"~A()"<<endl;
}
private:
int _a;
int _b;
};
int main()
{
A* p7 = new A[10];
delete p7;
return 0;
}
事实上,在我们运行代码后会发现系统报错了,为什么呢?
这里涉及到一个内存泄漏的问题:
首先当然就是只能释放一个对象的问题
当然,不只是这个
按理来说一个A类的对象大小根据内存对齐规则来定应该为8,那么我们这里开了10个对象就应该是开了80字节的空间,但我们检查一下会发现一个很神奇的现象
内存大小成了84个字节?!发生甚么事了?
其实另外的四个字节是用来存储对象个数的,拿这段代码来说是A[10]的这个10,并且这个数字存储的位置非常特殊,在指向的空间的前面
但是在调用delete的时候是直接从p7只想的位置释放的,那前面的这四个字接就泄露了......
但是为什么要保存这个成员个数呢?别急,答案将在下面第五点的实现原理部分揭晓~
4. operator new 与 operator delete函数
-
operator new:底层通过
malloc
实现,失败时抛出异常(malloc
返回NULL
)。 -
operator delete:底层通过
free
实现。
代码示例
void* operator new(size_t size) {
void* p = malloc(size);
if (p == nullptr) throw bad_alloc();
return p;
}
5. new/delete 的实现原理
5.1 内置类型
-
与
malloc/free
类似,但支持异常机制。
5.2 自定义类型
-
new:
-
调用
operator new
分配内存。 -
调用构造函数。
-
-
delete:
-
调用析构函数。
-
调用
operator delete
释放内存。
-
因此,刚刚说到的10实际上是给delete[ ]用的,是要告诉它有多少个成员,这样它就知道需要调用多少次析构函数了,真是太~妙~了~
我们这时将写的显式析构函数注释掉:
class A
{
public:
A(int a=1,int b=1)
{
_a = a;
_b = b;
}
//~A()
//{
//cout<<"~A()"<<endl;
//}
private:
int _a;
int _b;
};
int main()
{
A* p7 = new A[10];
delete p7;
return 0;
}
再计算一次,发现新生成的内存大小会发现又变回80了
我也不卖关子了,这是因为类中没有了显式析构函数,构造函数中也没有额外申请资源(申请内存),那么系统就会认为此时的默认析构函数可调可不调,从而进行了优化,直接在p7开始指向的位置直接进行内存释放。
但不管怎么说,咱老老实实地将new与delete正确匹配着来用准没错!
6. malloc/free 与 new/delete 的区别
区别点 | malloc/free | new/delete |
---|---|---|
类型 | 函数 | 操作符 |
初始化 | 不初始化 | new 可初始化 |
大小计算 | 需手动计算 | 自动计算(类型 + 数量) |
返回值 | void* (需强转) | 类型指针(无需强转) |
失败处理 | 返回 NULL | 抛出异常 |
构造/析构 | 不调用 | 调用构造/析构函数 |
那么以上便是本次内存管理部分学习分享的全部内容了
十分感谢你能够看到这里~
如果感觉对你有帮助的话也请给我一键三连 这将会给我莫大的鼓舞!
后续我依旧会持续更新其他的内容 敬请期待
那么 就让我们
下次再见~