目录
前言
C/C++的内存管理是一个老生常谈的问题,无论是才学不久的初学者,还是码了不少代码的老手对于这个方面的知识的探究都是必不可少的,这个这个知识是作为一根线,将代码的实现、编译器的运行、还是电脑对于内存的使用和保存等众多的计算机相关知识链接在一起,虽然这可能不会让你的代码能力提升一个台阶,但是这可以让你对于内存对于代码的运行有一个更好的认知,更加可以让你明白部分编译未错(语法错误),但运行崩溃的原因。
在这之前,我们已经对于编译器、电脑对于不同的内置类型的存储、读取和转化方式已经有了初步的了解(详情请看这两片文章:整形篇、浮点数篇),这次我们再来探究在代码运行时,编译器是如何使用内存的?
一、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.代码段(常量区)
globalVar在哪里?____ staticVar在哪里?____ num1 在哪里?____ | staticGlobalVar在哪里?____ localVar在哪里?____ |
char2在哪里?____ pChar3在哪里?____ ptr1在哪里?____ | *char2在哪里?___ *pChar3在哪里?____ *ptr1在哪里?____ |
答案:
二、回顾C语言(malloc/calloc/realloc/free)
void Test ()
{
int* p1 = (int*) malloc(sizeof(int));
free(p1);
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
free(p3 );
}
还记得他们的用法吗?
在C语言中,这四个内存管理的函数就已经包罗所有对于动态内存管理的用法,前三为申请和调整内存的大小,最后一个释放。
现在我们来到了C++,这时候他们还能够满足我们的需求吗?
答案是当然也是可以的。这是个即在情理中,也在意料之外的答案,毕竟在C语言中他就能满足我们的所有要求了,C++也当然可以的。但是,别忘了C++的初心,更优化的语言相对于C语言。那C++是如何相对于C语言优化内存管理的函数的呢?
三. C++ 是如何改造何优化的呢?
0*、C++的相关内存管理函数
new/delete操作内置类型
①new:
格式:
申请一个(内置/自定义)类型的空间 (初始化值可省略): new 类型(初始化值);
申请多个(内置/自定义)类型的空间 (初始化值可省略):new 类型[个数] { 初始化值 };
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
delete ptr4;
delete ptr5;
delete[] ptr6;
}
②delete:
强调:删除时一定要将delete和new的删除格式对应使用,即new时申请的单个,delete就删除单个的格式,切勿用混,如果用混了,会有各种不确定的后果,如:警告、报错、崩溃、内存泄漏 等等,这取决于你使用的编译器,每个编译器的底层实现可能不同,结果也就不确定
格式:
删除申请单个变量的空间: delete 地址
删除申请多个变量的空间:delete[ ] 地址
一、返回值优化(不同)
在上图中我们可以看到new时,我们直接是用的对应类型的指针来接收的,没有像C语言中,还需要强转void*类型来接收。
二、参数优化(不同)
我们还是来看上图,在new时,是直接将对象个数、初始化值传入的、而类型是在括号外,而并不像C语言中需要我们来计算大小,有同学说:new这个函数真奇怪,传入类型的时候居然写在括号外面,那个函数的传参的时候写在外面的?
欸!对了还真没有。所以new / delete不是函数,他们是运算符哦!
三、警告优化(不同)
1、 在C语言中,每一次向内存申请新的内存都需要判断是否还有足够的内存让我们申请,即判断返回值是否为空指针(如果你说:“哎,我就是不写,就是玩,我的编辑器还不报错,你气不气”,当然在有些编译器下,你不写它也不会报错,但是报错时难堪的就是你了,在现在的最新编译器中(那位Dev6.0的同学就把你的上古神器往后稍稍,肯定是不会报错的),存在了不写就报错强制规则的),如下图:
#include <stdio.h>
int main()
{
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
//判断ptr指针是否为空
if(NULL != ptr)
{
exit(-1);
}
free(ptr);
return 0;
}
2、在C++中则是对此进行了封装,又或者说优化。这里new将警告改为了,抛异常(类似于C语言中的error),直接将它自动化了。我们可以来看看,
new的源码:
看完之后我们还可以再一次证明,其实new就是malloc的优化和再封装(见上图红字)。
四、C++中new/delete的底层实现
①new的底层实现
在上图中,有眼睛尖的同学说:这不对啊,这个好像是函数啊,但是为啥又有一个operator运算符的标志呢?就是这个原因所以上文我才说他是运算符不是函数的原因吗?
说实话这确实被这个operator这个运算符单词误导了!!这可以说是C++的一个误导吧,上图这个operatore new其实是一个全局函数,不是运算符,这不是就和我上文说new是一个运算符矛盾了吗?那肯定不是这样的。
我们new谈了这么久,忘记了C++一个重要的东西——类和对象,这个作为C++的自定义类型,当然也是可以通过new来申请和创建的,此时我们再此回顾operatore new的源码图(最近的上图),发现它的功能好像不能满足我们自定义类型的创建,那它是如何实现的呢?
其实new中,分为2个部分,一就是我们提到的operatore new,这个部分用于空间申请,另一部分则是调用构造函数所以总的来说,new是一个运算符,依次调用operatore new(全局函数)和构造函数。
我们来实验一下当new一个类对象时会发生什么。
汇编,可以更加方便细致的观察编译器做了什么
②delete的底层实现
看完了new的实现,我们再来看delete就很简单了,delete无非也是包含两部分:operator delete 和相应的“析构函数”
四. 定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type 或者 new (place_address) type(初始值)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用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对象相同大小的一段空间,还不能算是一个对象,因为构造函数没
有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}