我们知道计算上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,内存其实可以划分为一个个内存单元,每个内存单元的大小取一个字节
这里我们先来补充一个知识点:计算机中常见的单位(如图)
1个比特位可以存放一个二进制位(0/1)
每个内存单元都有一个内存编号,有了这些编号,CPU就可以快速找到这些内存空间,在计算机中我们把这些内存编号也称为内存地址,在C语言中给地址起了新的名字,叫指针。
所以,我们可以理解为:
内存单元的编号==地址==指针。
首先,CPU访问内存中的某个字节空间,必须知道这个字节空间在内存中的什么位置,而因为内存中字节很多,所以要给内存进行编址。
计算机中的编址是通过硬件设施来完成的。
首先,必须理解计算机中是有很多的硬件单元,而硬件单元是要互相协同工作的,也就是说,至少相互之间要能够进行数据传递,那么,是如何进行通信的呢,答案就是用’线‘连起来,不过我们今天关心一种’线‘--地址总线。
我们可以简单理解32位机器有32根地址总线,每根线只有两态0/1(电脉冲有无),那么32根地址线就能表示2^32种含义,每一种含义都代表一个地址。
理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量其实就是向内存申请空间(如图)
比如上诉的代码就是储存了整型变量a,内存中申请4个字节用来存放整数10,其中每个字节都有地址,那么我们如何取得a的地址呢?这里我们就要用到一个操作符--取地址操作符(&)
取地址a取出的是4个字节中较小的地址,只要知道第一个字节的地址,我们就能顺藤摸瓜地找到后面的地址
接下来我们来了解指针变量和解引用操作符(*)
我们拿到是地址也是一个数值,这个数值有时我们也是要存储起来方便后期再使用的,那么我们用什么来存储呢?--指针变量
指针变量也是一种变量,这种变量我们都是用来存放地址的
接下来教会大家如何拆解指针类型
我们看到pa的类型是int*,我们该如何理解它呢?
*是说明pa是指针变量,int是说明pa这个指针变量指向的对象是int整型
同理,char类型的指针变量ch我们就要放到char*类型的指针变量中
接下来我们来了解解引用操作符*
我们通过指针变量将一个地址存储起来了,那我们之后要怎么取使用它呢?
这里我们就要用到解引用操作符*。
这里我们就通过解引用操作符将a的值修改为了0.
接下来我们再来了解指针变量的大小
前期我们了解到指针变量是用来存放地址的,那么指针变量的大小就要4个字节才可以(32位下),同理64位下就要8个字节才可以。
那么,指针类型有什么特殊的意义呢?接下来我们来学习。
3.1指针的解引用
经过调试,我们发现代码1的n全部变为了0,而代码2的n只有第一个字节变为了0.
由此我们可以发现指针的类型决定了我们访问这个指针的时候访问的权限有多大。
int*型一次能访问4个字节,char*类型一次能访问1个字节。知道这个之后我们就能进行指针的+-整数运算了。
3.2指针+-整数
代码的运行结果如下
由此我们可以看到char*类型的指针变量+1跳过1个字节,int*类型的指针变量+1跳过4个字节,这就是指针变量类型的差异带来的变化。
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量,但我们可以使用const限制这个变量,使其不能被修改。
如上所示,此时m可以被修改,而n就不能被修改了。
但我们如果绕过n,使用n的地址就可以做到修改n了。
使用如上代码我们就可以通过指针变量p来修改n的值了,这样就打破了const的限制,那么我们该如何限制它呢?
const修饰指针变量的时候,如果放在*左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可变。
const修饰如果放在*的右边,那么指针变量本身的内容不可变,但是指针指向的内容可以通过指针变量来改变。
所以我们可以在*的两边都加上const修饰。
指针的基本运算有3种,分别是:
指针+-整数运算;
指针-指针运算;
指针的关系运算。
因为数组在内存中是连续存放的,所以只要知道第一个元素的地址,后面的元素就都能找到。
所以只要printf("%d ",*(p+i));
i++;
就能访问到数组的每一个元素。
指针-指针得到的是元素的个数。
做到依次访问数组的每个元素。
概念:野指针就是指向的位置是不可知的。
那么什么情况下会出现野指针呢?
1.指针未初始化;
2.指针越界访问;
3.指针指向的空间释放。
在知道了这些成因之后,我们该如何来规避野指针呢?
如果明确知道地址就给指针赋值地址,如果不知道指针指向哪里就给指针赋值NULL。
NULL是C语言中定义的一个标识符常量,值是0,0也是地址,只不过这个地址无法使用,读取时会报错。当我们指针变量不再使用时,即使给指针变量赋值NULL,指针使用之前检查其可效性。
assert.h这个文件定义了宏assert(),用于运行时确保符合执行条件,如果不符合,就报错终止运行程序,这个宏常常被称作”断言“。
上面代码在程序运行到此句时,验证变量p是否=NULL,如果等于NULL,程序就会报错。
assert()宏接受一个表达式作为参数,如果表达式为真,程序继续运行,如果表达式为假,程序就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,并标上文件名和行号。
通过以上代码我们可以看到传址调用对实参进行了修改。
我们首先要知道数组名本身是一个地址,我们使用&arr[0]的形式可以读取数组首元素的地址,而数组名也能达到同样的效果。(如下图)
注意,有两个意外:
1.sizeof(数组名)sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
除此之外,任何地方使用数组名,数组名都表示首元素地址。
数组元素的访问在编译器处理的时候,也是转换成首元素的地址加偏移量求出元素的地址,然后解引用来访问的。
数组传参本质上传递的是数组首元素的地址。
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?这就是二级指针。
对于二级指针的运算有:
*ppa通过ppa中的地址进行解引用,这样找到的是pa;
**paa先通过*ppa找到pa,再*pa找到a。
我们来类比一下,整型数组存放的是整型的数组,字符数组存放的是字符数组,那么指针数组,存放的就是指针数组。
指针数组的每个元素是地址,又可以指向一片区域。
在指针的类型中我们知道有一种字符类型的指针char*。
这段代码非常容易让同学们以为是把hellow bit放到pstr里了,但是本质是把字符串首字符的地址放到pstr里了。
数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
p先与*相结合,说明p是一个指针变量,然后指向的是一个大小为十个整型的数组。
数组指针类型的解释如图所示
首先我们再次理解一下二维数组,二维数组可以看成每个元素是一维数组的数组。那么,数组的首元素就可以看成一行一维数组。
那么二维数组传参就可以写成如下形式。
由上面的知识可以推断出,函数指针变量就是存放函数地址的变量。
函数是有地址的,函数名就是函数的地址,当然也可以通过&函数名来得到函数的地址。
函数指针变量的写法其实与数组指针非常类似(如下图)
函数指针类型分析如图所示
通过函数指针调用指针指向的函数。
输出结果为5和8.
typedef是用来类型重命名的,可以将复杂的类型简单化。
比如,如果你觉得unsigned int 写起来不方便,你可以写成uint。
如果是指针类型,能否重命名呢?答案是可以的。
比如int*重命名为ptr—t可以写成
typedef int* ptr—t。
但是对于数组指针和函数指针稍微有点区别。
比如我们有数组指针类型int(*)【5】,需要重命名为parr_t可以这样写
函数指针的重命名也是一样
要把一个函数的地址传到一个数组中我们就要用到函数指针数组。
parr1先和【】结合,说明parr1是数组,是int(*)()类型的函数指针。
回调函数是什么?
回调函数就是一个通过函数指针调用的函数。
如果你把一个函数指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。