C++基础——内存管理
ʕ • ᴥ • ʔ
づ♡ど
🎉 欢迎点赞支持🎉
个人主页:励志不掉头发的内向程序员;
专栏主页:C++语言;
文章目录
前言
一、C/C++内存分布
二、C语言中动态内存管理方式:malloc/realloc/calloc/free
三、C++内存管理方式
3.1、new/delete操作内置类型
3.2、new/delete操作自定义类型
四、operator new与operator delete函数
五、new和delete实现原理
5.1、内置类型
5.2、自定义类型
六、定位new表达式
七、malloc/free和new/delete的区别
总结
前言
我们在学习完类和对象后,应该已经深刻的理解了我们的C++和C语言的区别,这一章节我们继续看看C++和C语言的不同之处,那就是申请空间的方式不同,我们来继续领略我们的C++的新特性的方便。
一、C/C++内存分布
我们C++的内存分布和我们的C语言的内存分布是保持一致的,主要分为代码段,数据段堆区栈区和内核空间。
我们可以看到我们不同的数据的分配空间位置是不相同的,就比如我们的一些内置类型就是存储在我们的栈区的,而开辟的空间就是存储在我们的堆区等,这里的指针大家可能有点疑惑为什么解引用是这样的,我这里再附一张图以供大家理解
栈也叫做堆栈,就像它的特性一样是一层一层压栈去分配空间的,所以它是向下增长的,它主要给非静态局部变量/函数参数/返回值等赋予空间。
堆主要就是给我们程序运行时动态内存分配时赋予空间比如malloc/realloc/calloc以及本章所讲的new所申请的空间就会在堆,然后返回一个地址(地址就是存在栈中),堆是向上增长的。堆也是我们唯一需要在乎的内存空间,其他的区域都是编译器自动管理的。
数据段主要就是存储全局数据和静态数据使用的。
代码段则主要是存储我们的可执行代码和只读常量。
我们分区其实就是分配我们的生命周期,向我们的静态区变量的生命周期就是从开始到结束,但是我们的内置类型的生命周期却很短,这是因为它们在栈中。
二、C语言中动态内存管理方式:malloc/realloc/calloc/free
复习一下,我们的malloc就是开辟空间不初始化,我们的calloc就是开辟空间且初始化,而我们的realloc则是我们的扩容。
void Test()
{int* p1 = malloc(sizeof(int));// 参数为元素的数量和每个元素的大小int* p2 = calloc(4, sizeof(int));int* p3 = realloc(p1, sizeof(int)*10);free(p2);free(p3);// 不用释放p1是因为扩容后我们的realloc自动释放了p1了
}
三、C++内存管理方式
其实我们的C语言的分配动态内存的方法我们的C++也是可以使用的,但是有些地方会无能为力,而且大部分时候总是太麻烦了,所以我们的C++便提出了自己的规划动态内存的方法,那就是new/delete操作符。
3.1、new/delete操作内置类型
C++的new和delete的用法如图
我们可以看到我们C++申请空间的方式简单了很多,想要申请空间只需new后面加我们所需的变量类型即可。如果想多开辟几个我们就再在后面个[]里面填想要的数字即可。我们如果不想使用了就直接delete即可,但是要注意我们的delete要和new匹配,如果是new,那就是delete,如果是new[],那就得delete[]。
我们之前C语言的想要初始化还得调用第二个函数,但是我们C++想要初始化直接在后面加()即可
int main()
{// 申请对象+初始化int* ptr1 = new int(1);int* ptr2 = new int[10] {0};int* ptr3 = new int[10] {1, 2, 3, 4, 5};delete ptr1;delete[] ptr2;delete ptr3;return 0;
}
我们可以看出它全部初始化了,非常的方便。
3.2、new/delete操作自定义类型
我们从刚才的内置类型可以看出我们C++比C语言的内存管理操作简单了不少,但是我们仅仅是因为这样就创造出了我们的new/delete吗,实际上不只是这样,这还和我们的C++特有的类和对象有关
class A
{
public:A(int a = 0): _a(a){cout << "A(): " << endl;}~A(){cout << "~A(): " << endl;}private:int _a;
};int main()
{// 自定义类型申请方式A* p1 = (A*)malloc(sizeof(A));A* p2 = new A;A* p3 = new A();free(p1);delete p2;delete p3;return 0;
}
我们运行看看
我们发现我们的malloc没有调用我们的构造函数和析构函数,这就是我们C语言申请内存的弊端,它不会自动去调用,但是我们的new和delete却又自动调用构造和析构的功能。这很重要,我们都知道我们的构造和析构是用来初始化的,如果类内没有自己指向的资源危害还小一点,可能就是出出bug,但是如果有那就会出现很严重的问题,没有构造就说明无法申请空间,没有析构就说明会内存泄漏。
四、operator new与operator delete函数
我们的new和delete是用来动态申请内存和释放内存的操作符,它们的底层包含了operator new和operator delete函数,这两个函数是系统为我们提供的全局函数我们的new通过我们的operator new来申请空间,而我们的delete则通过我们的operator delete来释放,虽然这个和运算符重载的那种写法很像,但是它们是一个独立的函数,而且运算符重载中间不会加空格,这两个也不是运算符而是操作符,大家不要混淆了(我之前就以为是/(ㄒoㄒ)/~~)。
我们的operator new底层是malloc,如果malloc成功了就直接返回,而如果失败了,则会调用我们用户给operator new提供的措施,如果用户没有提供就会抛异常。
operator delete的底层是free,通过free来释放空间。
五、new和delete实现原理
5.1、内置类型
5.2、自定义类型
new:
我们的new的实现原理先调用我们的operator new函数去申请空间,申请成功后去执行我们的构造函数,完成对象的构造。C++创建了operator new的原因在于C++希望我们如果申请空间失败了不要只是简单的返回nullptr,而是能够有一种新的机制,所以它在这里定义了一种异常的机制,给我们的malloc套了一层马甲,就成了operator new。
delete:
我们的delete的实现原理先调用析构函数去析构我们的对象后再调用operator delete去释放空间。
new T[N]:
和new一样,先调用operator new[]函数,operator new[]实际就是调用N次operator new进行内存申请,完成之后就会对这些申请出来的空间执行N次构造函数。
delete[]:
delete[]则是先在要释放的空间上执行N次析构函数,然后再调用operator delete[]释放空间。
我们的这些操作符应该严格匹配,如果不匹配会有一些问题发生。
首先我的new和delete的底层是malloc和free,所以如果我们创建的是内置类型的空间,不匹配是没有什么问题的。
int main()
{int* a = new int[10];delete a;return 0;
}
但是我们的自定义类型就有所不同了
class A
{
public:A(int a = 0): _a(a){cout << "A(): " << endl;}~A(){cout << "~A(): " << endl;}private:int _a;
};int main()
{A* a = new A[10];delete a;return 0;
}
我们运行试试看
我们的程序崩溃了。
这个原因是因为我们的new再存储多个对象时,会在我们的的开辟的空间前面多开辟出4字节来记录我们的对象个数,但是我们返回的指针却指向我们的对象第一个的地址。如果此时调用的时delete[],那删除的时候就会自动往空间前面偏移4字节把记录的空间一起释放掉,但是如果是用delete,那就会直接从对象第一个地址那里开始释放,但是我们不允许空间不从头开始释放,所以程序就崩溃了。
class B
{
private:int _a;int _b;
};int main()
{B* b = new B[10];delete b;return 0;
}
我们来看这个类
我们去运行发现程序居然没有崩溃,这是因为我们刚才多开辟的4字节空间的主要作用是用来记录我们的对象要析构多少次的,我们这里没有写析构函数,所以我们的编译器认为这里不用析构,所以优化掉了,此时我们的编译器就不会多开辟4字节的空间了,所以就不会有问题了,这也是为什么内置类型为什么可以乱用的原因了。我们如果在B中写入析构,编译器就优化不了了,所以就会崩溃了。
class B
{
public:~B(){ }
private:int _a;int _b;
};
所以我们一定要对应使用。
六、定位new表达式
我们知道了我们的new是由我们的operator new + 构造函数实现的,所以说其实我们可以自己手动的去尝试实现一个我们的new操作符
class A
{
public:A(int a = 0): _a(a){cout << "A(): " << endl;}~A(){cout << "~A(): " << endl;}private:int _a;
};int main()
{A* a1 = new A(1);A* a2 = (A*)operator new(sizeof(A));return 0;
}
我们可以看到,我们的new是初始化的,但是我们的operator new却没有
那我们该如果调用我们的构造函数呢?这里就要用到我们的定位new表达式了,它的作用就是用来对已经分配的原始内存空间中调用构造函数初始化一个对象,它的用法是
new(place_address)type或者new(place_address)type(initializer-list)
place_address必须是一个指针,initializer是类型的初始化列表
int main()
{A* a1 = new A(1);A* a2 = (A*)operator new(sizeof(A));//定位new表达式new(a2)A(1);return 0;
}
此时我们的a2也完成了初始化。
我们的析构函数是可以显示调用的所以就没有这种东西。
int main()
{A* a1 = new A(1);A* a2 = (A*)operator new(sizeof(A));new(a2)A(1);a2->~A();operator delete(a2);return 0;
}
这就是定位new表达式的用法了。这个语法的用处主要在于内存池,一般来说没啥用,大家了解即可。
七、malloc/free和new/delete的区别
总结
我们已经明白了我们的C++申请内存的方式,接下来我们来看看我们C++的一个伟大的思想,泛型思想,这一思想是我们的很多代码很简单就能兼容不同类型的对象,我们下期再见。
🎇坚持到这里已经很厉害啦,辛苦啦🎇
ʕ • ᴥ • ʔ
づ♡ど