大家好,我是梁唐。
想要追求更好阅读体验的同学,可以点击文末的「阅读原文」,访问github仓库。
危险的case
指针由于能够操作内存,所以如果使用的时候不够仔细,很容易引发一些意想不到的错误。
C++ Primer当中给了这样一个例子:
- int *ptr;
- *ptr = 2333;
在这段代码当中我们声明了一个int型的指针,并且将它指向了2333。然而,这里有一个问题,我们在声明指针的时候并没有进行初始化。没有初始化的指针并不为空,而是指向一个未知的地方。如果说它指向的是一个常量1200的地址,我们让它等于2333,那么之后当我们使用1200这个常量的时候,得到的结果都是2333。
更可怕的是,整个过程非常地隐蔽,很难察觉。debug的时候会令人抓狂。
所以千万不要修改一个没有初始化的指针指向的值。
指针和数字
C++ Primer当中还给了另外一个例子,当我们输出指针的时候,得到的是一串十六进制的数。那我们能不能反过来将一个十六进制的数赋值给指针呢?
- int *p;
- p = 0xB8000000;
答案是不行,因为类型不一致。虽然我们打印指针的时候看起来得到是十六进制数,但它的类型其实是指针类型,而不是整数类型。所以我们将一个整数赋值给一个指针是不行的,如果非要赋值,必须要进行类型转换。
- int *p;
- p = (int*) 0xB8000000;
但是这一转换之后显然又出现了一个问题,我们知道0xB8000000这个地址指向哪里么?显然不知道,自然也就说不清改了这里的值之后会引发什么结果。
所以虽然这么做可行,但也强烈不建议这样干。
new操作
前文说过使用指针有一个非常大的好处就是可以在程序运行的时候,动态分配内存。其实在C语言当中也有类似的功能,可以使用malloc来分配内存。不过在C++当中有了更好用的运算符——new。
比如我们要动态创建一个int类型的变量,可以这样写:
- int *ptr = new int;
new运算符根据之后的类型确定需要的内存大小,找到这样的内存之后,返回地址。刚好指针接收的值就是内存地址,因此刚好可以完成这样的赋值操作。
上面的代码也可以写成这样:
- int a;
- int *ptr = &a;
这两者有什么区别呢?表面上看没有区别,都是创建了一个int类型的变量。只不过第二种写法除了可以使用指针ptr之外,还可以使用变量名a来访问这个int。
但实际上这两者的内部实现完全不同,我们直接通过变量名创建的变量它的值会被存储在栈内存当中,而通过new创建的对象则被存储在堆内存当中。栈内存是由系统自动分配,而堆内存则是由程序员进行申请使用。这两者的内存模型是完全不同的,我们会在之后的文章详细地讨论这点。目前简单来理解的话,就是堆内存更加灵活,它的空间也更大,可以存储下更大的数据。
delete操作
有了动态创建,自然也就有动态删除,所以C++当中有一个delete操作和new相对应。
delete运算符可以在变量使用结束之后,将内存归还给内存池。因为很多时候程序当中的变量都是一次性使用或者是有生命周期的,当生命周期结束,使命完成就没有必要继续占用着资源了。毕竟系统内的内存资源是有限的,尤其是在一些大型项目或者嵌入式系统当中,内存资源非常紧张。
delete运算符之后跟一个指针,它会释放改指针指向的内存。
- int *ptr = new int;
- delete ptr;
这里面有很多坑,千万要当心。首先是使用了new创建了内存之后,一定要记得delete,否则这块内存将会永远被占用无法得到释放,这种情况被称为内存泄漏(memory leak)。另外,我们不能delete一个已经delete过的指针,这也会引发严重错误。C++ Primer对此的描述是:什么情况都可能发生。当然也不能再使用一个已经被delete的指针,这会引发空指针错误。
指针对于C++来说是一把双刃剑,像是Java、Python、Go等其他语言,内存回收的工作都是由系统自动执行的。例如Java的JVM虚拟机设计了严密的GC(垃圾回收)机制,程序员无须关心内存的回收问题,全部交给程序自动完成。
而在C++当中,这一过程是由程序员手动执行的,某种程度上来说,这当然非常好,程序员拥有了很高的权限以及灵活度。但同样也是一个坑,尤其是在复杂系统当中,很难准确判断delete执行的时间。这会引发严重的问题,例如内存泄漏严重,野指针到处飞等……