ARM学习之路–指针
变量到指针
静态内存由于在整个程序运行期间不再发生变化,因此我们可以通过变量名直接访问,地址在编译期间已经确定了。对于栈,每一次运行的地址是不固定的,所以只能通过栈帧指针结合相对寻址来访问。
函数在每次运行的过程中,分配的栈帧空间的地址会发生改变,但是每个局部变量在函数栈帧内相对栈帧指针FP的相对位置不会发生变化,因此每一次函数运行都能够正常访问。 但是malloc会申请堆内存,不仅是动态变化的,而且还是匿名内存,我们无法借助变量名或者栈指针来访问,因此只能借助指针来间接访问!
指针的本质
其实指针最原始的用途就是用来访问一片匿名的静态内存!
指针变量和普通变量的区别在于:普通变量存放的是一个数,指针变量存放的是一个地址,区别就在于这个地址上可能存放的时不同类型的数据。
指针变量的大小只和系统有关,32位的系统一个指针变量的大小都是4个字节,64位系统中则是8个字节。
既然指针变量所占的内存大小不会改变,那为什么还要指定一个类型呢?
其一:为了应对编译器的类型检查,编译器会根据指针指向的数据类型对程序进行语义检查
其二:不同指针的运算规则不同,更加适合我们通过指针访问不同类型的数据。这一点我们前面应该有提到过,对于一个char * a 类型和int* b 的指针变量来说, a+1 和 b+1 的地址偏移量是不一样的!
可以看到 a+1 地址偏移量是 1,而 b+1 地址偏移量是4;
指针类型
指针一般可以分为三个大类:
函数指针: void(* fp)(int,int)我们一般在写所谓的回调函数就是使用的这种类型的指针。
对象指针:char * ,int * ,long * ,struct xx* ,即一般常用的指针类型
void * 指针:一般作为通用指针,作为函数的形参。
通常情况下,指针会与一些运算符号结合使用,这写运算符有优先级高低之分,在某些表达式的计算上可能会容易混淆,他们的优先级从高到低依次:
()[] . -> ++ – * &
我们一般会有如下一些表达式:
对于这种形式的分析,有一套固定的逻辑,《C指针》提供了一套左右法则:首先从最里面的圆括号看起,先往右看,再往左看,每当遇到圆括号的时候,就应该调转阅读方向,一旦解析完圆括号里所有的东西就跳出圆括号。重复这个过程,直到整个声明解析完毕。
如果是一个指针,它指向的是什么?如果是一个数组,装的是什么元素?如果是函数,它的形参是是什么,返回值是什么?
利用以上原则,我们看一下下面这几个:
我挑一个最长的第四个试着解析一下:
首先我们可以把它看成一个 int (* f)(int * ,int, int),那么它就是一个函数指针,返回值类型为int f(…),同时呢,它的第一个形参是整形的指针变量,第二个形参为整形变量,第三个形参为函数指针,指向函数的类型为 int f(int * ,int)
指针与数组的关系
-
数组名作为函数参数时相当于一个指针地址
-
数组和指针一样,都可以通过* 访问
-
数组和指针一样,都可以通过下标运算符[]访问
从C语言语法的角度上看,数组和指针的访问方式,主要用途都不相同,指针和数组的主要区别:
之所以下标运算符[]可以混用,是因为C语言对[]下标运算符的访问是转化成指针来实现的,即编译器会将 a[n] 转化为 * (a+n)的形式,数组名a代表的是数组首元素的地址。
那么你会发现:无论是通过下标运算符还是指针访问,最终都会转化为 * (a+n)的形式,所以这才是下标运算符和指针间接访问可以混用的原因。
那么数组名到底代表着什么呢?
数组名其实也是有类型的,我们定义一个字符数组: char a[5];数组名a的类型就是char[5],&a的类型就是char (* )[5].如果我们定义一个常量指针char * const p;那么p的类型就是char * const,这里就能发现常量指针和数组名不是一回事,其次他们在内存中所占的空间大小也不同!
指针数组和数组指针
指针数组的本质是一个数组,存放的不是普通的数据,而是指向数据的地址。数组指针本质上是一个指针,指针指向的数据类型是一个数组。
数组指针又称为行指针:例如 int (* p)[5] p就是一个数组指针,指向一个一维数组,一维数组的长度是5,当我们执行 p+1 的时候,p要跨过 5个int 数据的长度。
如果要将二维数组赋给一个指针:
int a[3][4]
int (* p)[4]
p = a
p++
它只占用内存中的一个指针存储的空间,里面就是存了一个地址。
指针数组即有n个指针类型的数组元素,p+1,时,p指向下一个数组元素
如果要将一个二维数组赋值给指针数组:
int * p[3]
int a[3][4]
p++ //表示 P 要指向下一个数组元素
for(i = 0; i < 3;i++)
P[i] = a[i]
它占用许多个指针存储的空间,因为他里面存储了很多数组。
这里不能使用 p = a 的原因:p代表着一个指针数组的地址,是一个更高维度的地址,而a代表着二维数组的第一个数组或者对一个元素的地址,两个东西不再一个维度,所以不能这么使用;但是可以 * p = a
,解引之后的p则表示指针数组第一个元素的值!
指针数组和数组指针作为函数参数的时候,都可以用来传递一个二维数组的地址,有什么区别和注意的地方吗?当一维数组的数组名作参数时,数组名就转化为了数组首元素的地址,二维数组作为函数参数的时候,数组名依然转化为了首元素的地址,只不过这个首元素依然是一个数组,其实搞明白二维数组就是一个存放数组的数组罢了!
二级指针
一般的指针变量保存的是一个普通变量的地址,我们可以通过这个地址访问和修改这个变量。而当一个指针变量保存的是另一个指针变量的地址时,我们称该指针是指向指针的指针,或者二级指针。
二级指针的作用:
-
修改指针变量的值
-
指针数组传参
-
操作二维数组
我们在前面有了解过:函数的参数传递实际上是值传递,形参的变化并不会改变实参的值。因为形参和实参分别存储在内存的不同区域,形参保存的是实参的副本。所以我们一般都会将指针作为参数,将实参的地址作为参数传递给形参。
同样的,我们用一阶指针修改普通变量的值,那我们就需要用二级指针修改一个指针变量的值了!
指针数组传参我们只需要了解,当指针数组作为形参传递时一般有两种等价的写法:即 * a[]
和 ** a
,而将二维数组作为形参传递给函数的时候,也有下面两种等效的写法:a[][5]
和(* a)[5]
当数组作为函数参数时,可以匹配的形参参考如下:
函数指针
函数指针用来指向一个函数,一般我们会定义一个函数指针变量来保存函数的入口地址。
函数名的本质就是指向函数的指针常亮,即函数的入口地址。在fp = func 中,函数名会通过隐式转换,转换成fp=&func;的形式,当我们通过这个指针调用函数的时候,就直接等效与func()了
我们经常用到的回调函数就是通过这种方法进行注册完成异步处理的!
void * 指针
void作为形参传递表示函数无参数传递,void * 指针可以指向任意数据类型。任意数据类型也都可以直接赋值给void * 指针,任意数据类型与void * 相互转换的时候都不会发生数据丢失和值变化!
那么空指针,void * 和 NULL有什么区别呢?有什么关系呢?
-
空指针是一个赋为(0)的指针,在没有被初始化之前,其值为0
-
NULL是一个标准规定的宏定义,用来表示空指针常量,这两个都可以定义为一个空指针。
-
void只用于对函数返回的限定和对函数参数的限定,表示无类型和无类型指针。一般不能对void指针进行算法操作!
野指针
这里区分一下野指针和NULL指针的区别,野指针是指指向垃圾内存的指针
成因一般有三种:
-
指针变量没有被初始化就使用,这个时候我们是不知道它指向哪儿的
-
指针被free之后没有置位NULL,这个是至关重要的,平时经常有人犯错
-
指针操作超越了变量的作用范围
更多内容请关注微信公众号:学习学个der