1. 理解内存和地址
1.1 内存
内存,顾名思义就是电脑用来存储数据的,当CPU(中央处理器)在工作时,不仅需要从内存中拿取数据也需要将数据放入内存当中,当把内存引入到现实当中,就像学校里面的宿舍楼一样,每个宿舍楼有许多间宿舍,每一间宿舍住着8个人,内存里面有许多的内存单元,每一个内存单元就像一间宿舍一样,而这个内存单元叫做字节,每个字节又相当于8个比特位,每一个比特位对应8人间的每一个人。
1.2 地址
宿舍有宿舍的地址,每一个内存单元也有自己的地址,宿管阿姨可以通过寝室号找的你的宿舍,而程序员也就可以通过地址找到内存里面存储的数据,在C语言中地址也叫作指针。
内存单元的编号==地址==指针
2. 指针变量
2.1 取地址操作符(&)
&:取地址操作符是将一个变量的地址取出来
当我们创建变量时,就会向内存申请一块内存空间,就像我们在外住宿时就会去酒店开一间房间,但我们要去房间住宿时,前台会给我们房间号找到这个房间,同样的,当我们需要将一个数据放到一块内存空间里面时,就要通过&(取地址操作符)找到这个变量的地址
我们可以通过监视窗口看见&a的值,再通过内存窗口发现a的地址和&a的值是一样的
所以&(取地址操作符)的作用就是获得一个变量的地址,然后我们通过这个地址将数据存储到变量里面
2.2 如何创建一个指针变量
简单介绍一下指针变量是什么:
指针变量就是一个存放指针(也就是地址)的变量
创建指针变量的基本格式:
解释:
左边分别是整形变量,字符变量,浮点型变量。右边是他们对应的指针变量。
这⾥p1,p2,p3左边写的是 int*,char*,double* , * 是在说明p1,p2,p3是指针变量,⽽前⾯的 int ,char,double是在说明p1,p2,p3指向的是整型(int),字符型(char),浮点型(double)类型的对象
2.3 解引用操作符(*)
我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?
在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。
C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符(*)
1.(*)解引用操作符对比生活,他就像一把钥匙,当我们知道一个房间号(地址),想要拿到里面的东西,就需要一把钥匙把房间打开才能拿到里面的东西,当然出来从里面拿东西也可以将里面的东西腾出来,放入新的东西。
2.printf("%d",*p);p里面存放着a的地址,我们用钥匙(*)打开这个地址,就可以看见里面存放的东西也就是100
3.*p=10;就是将10放入到p所指向的地址里面去,从而改变了a的值
2.4 指针变量的大小
我们知道,32位机器下有32根地址总线,每一根脉冲信号为0或1,而这些脉冲信号组成的二进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储,如果用指针变量来存储一个地址,至少需要4个字节才能存储,同理在64位机器下需要8个字节才能存储这些地址
小结:
• 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
• 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
• 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是 相同的。
3. 指针变量的意义
既然各种各样的指针在相同平台下的大小都是相同的,为什么还要弄这么多的指针类型,用一个指针不就行了吗?那么接着往下看,你就会知道设计师为什么这么设计。
3.1 指针的解引用
用例1:
当我们创建一个整形变量,并且给他赋0x11223344(因为8位的16进制数刚好可以占满4个字节),当用整形指针来接收a的地址,再将p解引用再赋值为0时,通过内存窗口可以看见,4个字节全部被修改为0.
用例2:
但我们用字符指针来接收a的地址时,再将p解引用再赋值为0时,通过内存窗口可以看见,发现只有第一个字节被修改为0
结论:
指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节
3.2 指针加减整数
通过代码我们可以发现,char*类型的指针+1跳过一个字节,而int*类型的指针+1跳过4个字节,指针的类型决定了指针向前或者向后走一步能跳过几个字节
3.3 void*类型的指针
3.3.1 void类型
1.void类型和char,int,double等类型的区别是:void类型不能用来声明变量
下面这种声明变量是错的
void a=10;
2.void类型也常常出现在函数返回值,表示该函数无返回值
void compar(参数1,参数2);
3.void类型也可以出现在函数参数里
int fun(void); int fun();
这两种写法是等价的,都表示不需要给函数传递参数
3.3.2 void*指针
void*指针也叫做泛型指针(无具体类型的指针),他可以用来接收各种类型的指针,但是他不能直接进行指针加减整数和解引用的操作,需要进行强制类型转换才能进行指针加减整数和解引用的操作
两段代码对比,用void*类型的指针来接收int*类型的指针是可以的,但是直接对p解引用是错误的,我们需要将void*类型强制转换为int*类型才能使用
⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据,在下面讲到qsort函数时,我们会对void*的使用有更深的了解
4. 指针的运算
4.1 指针加减整数
指针加减整数表示指针可以跳过几个字节
数组在内存中是连续存放的,我们将3的地址给到p,当我们对p进行加一操作时,可以发现在3地址的基础上加上了4个字节,得到了4的地址,从而访问到了4,同理,1,2,5也是同样的原理
4.2 指针减指针
指针加减整数我们知道得到的还是指针,那么指针减指针得到的就是整数,这个整数也就是两个指针之间相差多少个元素
arr[0]和arr[5]之间相差20个直接,而int*类型加1跳过4个直接,所以p1-p为5
有指针减指针,有指针加指针吗?
看看下面这两段代码
两个指针相加的到的是指针还是整数?答案是都不是,这种写法是错误的。
4.3 指针的关系运算
指针的关系运算简而言之就是指针比大小,指针不都是地址吗?地址也有大小吗?答案是地址也有大小
我们可以看出p1的值比p的大,而这两个值分别是a[3]和a[0]的地址,所以指针是可以比较大小的
5. 野指针
5.1 常见的三种野指针
1.未进行初始化的指针
创建了int*类型的指针,但是并没有给p赋值,所以不知道p指向何处,故p为野指针
2.越界访问的指针
第11个数打印的是随机值,指针指向的地址超出arr,p就是野指针
3.指针指向的空间被释放掉
在test()里面创建了一个n变量,返回n变量的值,用p来接收,但是当&n返回时,也就是出test()函数是,n变量就已经被销毁了,虽然还能打印出n的值,但是p是一个野指针。
5.2 如何规避野指针
1.创建指针变量时,对指针进行初始化,如果指针没有明确的指向时,可以将指针初始化为NULL,NULL是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错
2.防止指针越界访问,我们向内存申请了多少空间,那就让指针在申请的空间内进行使用,不要超出范围访问
3.当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL
4.避免返回局部变量的地址
6.传值调用和传址调用
我们在写自定义函数时常常会进行传参,参数可分为传值和传址
6.1 传值调用
这是一个交换函数,我们将实参传给形参,然后形参进行交换,最后发现x和y的值没有发生改变,这是因为当实参传给形参时,形参是实参的一份临时拷贝,当函数进行结束后,形参会被销毁,而在整个函数进行过程中一直是形参在参与运算,实参一点也没有参与,所以最后实参的值不会被改变,形参和实参分别占有不同的内存块,对形参的修改是不会改变实参的。
6.2 传址调用
我们将swap的参数部分改为指针,然后通过指针去交换x和y的值,发现可以将两者进行交换,这是因为,我们通过地址直接对x,y进行修改,就算局部变量销毁,也不会影响x和y的值,让函数和函数外面的变量建立起联系,也就是函数的内部可以直接操作函数外部的变量。