这篇文章我们讲一下C++的动态内存管理,从一个比较陌生的知识说起,我们知道,一个工程可以创建很多.c文件,我们如果定义一个全局变量,只要用extern声明一下,在每个文件都可以用。而用static修饰的全局变量只能在当前文件可用,局部的static修饰的变量只有程序走到这时才会去创建,并且只会初始化一回,比如是在一个普通函数内,就意味着如果第二次调用该函数,就不会去在初始化了。
上面所说的全局变量和static修饰的变量是存储在静态区的,我们这里所说的动态内存分配都是在堆区上的,下面说一下new和delete的基本用法,基本的形式是这样的:
int main() {int* p1 = new int;int* p2 = new int[10];return 0;
}
基本用法就是这样,用一个指针去接收,跟malloc是一样,但是后面会简单很多,如果就开辟一个空间的话,就直接加类型,要开辟比如十个空间就是加上方括号。那么new函数会不会初始化呢?跟malloc一样,是不会初始化的。当然我们也可以让new去初始化,就像这样:
单个数据初始化就用括号,多个数据就用大括号,向上面的p2情况,如果是未完全初始化,那么后面会用0给初始化,就跟数组初始化一样
那么想要delete也简单,直接用就行,就像这样:
new时用方括号了delete时就用方括号,new时不用delete时就不用
那么下面大家想一想为什么我们C++不用之前的malloc了,而是新创建了new这个操作符,对!new是一个操作符,它跟函数调用是不一样的。不只是为了简单写,还有一些其他的用处
比如说,我们有一个自定义类型,我们用malloc去创建了一个指向该类型的对象的指针,我们是很难通过这个指针去解决初始化问题的,为什么呢?首先自定义类型的成员变量是私有的,我们不能去访问,另外构造函数是程序自动调用的,我们也无法去去调用对象的构造函数,所以我说它是很难去初始化的,但也是有办法
但这种办法确实很鸡肋,我们这只是为了证明它可以改变。
malloc会存在这种问题,所以我们才有了new这个函数,它不仅会去开辟空间还会去调用自定义类型的构造函数。
就可以这么去调用,上面是自定义类型有一个成员变量的样子,下面是有两个成员变量的样子,这就是我们上个博客写的内置类型转换。既然我们可以这么写了,那创建链表节点的时候就不用再去写相应的函数了,就可以这么去写:
struct ListNode
{ListNode(int val) {_val = val;_next = NULL;}int _val;ListNode* _next;
};int main() {ListNode* n1 = new ListNode(1);ListNode* n2 = new ListNode(2);ListNode* n3 = new ListNode(3);return 0;
}
它直接就去自动调用构造函数了,既然new自动调用构造,那么delete就会去自动调用析构函数并且去释放空间,我们平常去析构一个比如日期类对象是没有意义的,但是当我们去建一个栈的类时调用析构就有意义了
class stack {
public:stack(int capacity=4) {_a = new int[capacity];_top = 0;_capacity = capacity;}~stack() {delete(_a);_top = 0;_capacity = 0;}
private:int* _a;int _top;int _capacity;
};int main() {stack* p = new stack;delete p;return 0;
}
这个程序呢?先是p指针指向一个栈,这个栈的12个字节是在堆区开辟的,然后会去调用堆的构造函数,又在堆上开辟4*4个字节,之后delete p时,会先去调用栈的析构函数,释放4*4个字节的空间,再去释放p指向的空间,在这里delete的作用就很明显了。
除此之外,malloc如果失败的话会去返回一个空指针,而new报错的话会抛异常,更符合C++面向对象的特性
其实我们已经知道了new的作用就是开空间和调用构造函数,那么开空间其实就用我们的malloc就可以了,只不过我们要对malloc包装一下,为了处理抛异常问题,包装完之后就变成了operator new函数,这是一个全局函数,这里的operator跟我们的运算符重载是没有关系的,只是叫这个名字而已,通过汇编我们也可以看到,new会去调用两个函数
我们这两个call指令就是去调用函数的意思,就是跳到对应函数的地址去执行函数,以此类推,也有一个operator delete函数,它也是去封装了free,跟new是一样的。
我们知道构造函数是不能自动调用的,但是析构函数可以,但是我们也有办法显示调用构造函数,那就是使用定位new,基本使用是这样的
就像第二行这样,就是这么个形式,那么显示调用有什么用处呢?比如说,我们可能有时候会频繁的小规模的开辟空间,而开辟空间这个过程是比较效率低的,这是我们就可以先申请一大块空间,叫做内存池,这就留着用,这时去内存池要空间就不用开辟但是得初始化,这时我们的显式调用构造函数就派上用场了。