进一步理解指针:灵活性和动态内存分配
指针内涵
指针的作用
指针通常有两种常见的用途,第一种是给变量起个别名,用来指代某个变量;第二种是操纵动态的内存空间,根据需要进行扩大或者缩小。
1 |
|
修改了*p
的同时,i
的值也被修改了,可以操作指针修改指针指向的变量,要注意的是赋予指针的值一定是个地址。在C的函数中,如果你想交换入参的值,就不得不传入指针。
1 |
|
指针的作用就是指向内存当中某个地址的值,并且可以对该值进行修改。与一般变量存在本质差别的是,指针存储的是一个地址值,而别的一般变量存储的是特定类型的值。
第一种用途的指针只要注意解引用符号基本不会出现太多的问题,因为不涉及到堆内存的空间分配。动态分配则不同,需要主动释放空间,下文会细说,这里暂且不表。
指针的类型
1 |
|
这里我为了方便而没有给指针赋初值,具有一定的风险
指针的类型并不决定指针的大小,一般来说它的大小随着操作系统固定的。指针的类型是为了标识指针所指向的区域存储的值类型,当你对指针进行加法操作的时候,类型的差别就开始显露了。
1 |
|
看来编译器会根据指针的类型去计算对应地址的起始位置,当出现加减操作时,根据指针的类型大小进行计算
万能指针
既然指针的类型并不会对指针产生本质的影响。你也可以定义指针的类型为void
,这样的指针叫做万能指针,它可以指向任意类型的值,不过在用之前记住指向区域的值类型,不然真的很容易出错。
1 |
|
这里给p
分配了int
类型大小的空间,然后初始化指向值。每次在解引用指针时需要进行强制转化,不然无法编译通过。
数组和指针
数组是内存中连续的相同类型值的集合,数组的标志是[],这个在别的语言里面似乎也是这样,好像是约定俗成的。
之所以会把数组和指针搞混,可能是因为数组可以和指针进行相似的操作,下面两种访问数组的结果是一致的。
1 |
|
可以总结为,a[n] = *(a + n) 这对指针也适用。而且指针不止可以指向单个值,还可以指向连续的多个值,并且可以动态地进行扩大或者缩小,这就突显了指针与数组的最大区别——灵活性。
数组很不灵活,你只能初始化的时候固定数组的大小,然后就无法随心所欲地改变数组大小了。arr
就只能表示数组的首地址,如果是指针的话还可以修改指向的地址。所以指针的功能更为强大,它甚至可以直接替代数组做到相同的效果。
动态内存分配
在编程的时候我们可以将内存空间视为三类,栈内存、堆内存和静态内存,这边主要介绍前两者。栈内存主要存储局部变量,比如函数域内定义的变量,特点是会在程序执行离开域之后变量会自动被释放,想栈一样后进先出;堆内存则不会收到函数域或者是块区域的影响,只会在程序退出之后清理。通过调用内存分配函数,我们可以将堆内存分配好的地址赋予指针,并且可以同时分配超过两个以上。
1 |
|
以上为一个指针分配了三个连续的空间,存储值为int
类型,使用起来就像数组一样。最后用完不要忘了使用free()
释放对应的内存空间。由于内存空间被释放,指针p
的就指向了未定义的地址,所以最好将指针赋值空地址,避免直接使用导致的异常。
1 |
|
这里演示了二级指针的内存分配情况,三级四级也是同理,只不过二级以上的指针用起来会很繁琐,也很容易出错,所以实际当中很少会用到(我从来没在项目代码中见过)。需要注意的是,free()
需要由内而外释放。
错误例子
以下的代码是错误的案例,演示了为啥一不小心会出现内存泄露。
1 |
|
这里给*p
分配了空间,这个函数的本意是想输入一个指针,然后给这个指针分配空间并初始化。但是没有注意*p
是个形参,导致指针切换了指向,最后函数结束时形参自动释放,p
指向的空间未及时释放,导致内存泄露。
正确的例子如下
1 |
|