c语言指针详解
一,引入
我们知道当我们创建一个变量的时候就会在栈区开辟一块内存空间,而在内存中每一块内存都具有独自的信息,也就是地址,地址是一串数字,数字的长度由开发环境决定。一般地x86环境地址是32位二进制数字,x64环境地址是64位二进制数字。我们也知道获取一个变量的地址可以使用取地址操作符&,只需要在变量名前添加取地址操作符就是取地址运算。
那么在取地址运算过后得到的变量的地址又应该如何处理呢?这里我们引入指针变量,将获得的地址放在相对应类型的指针变量中。

如图所示代码便是将整型变量i的地址存储到整型指针变量p当中,通过打印我们可以证明储存在p中的数据便是整型变量i的地址。
我们知道一个变量,必定在内存中有一定的大小,那么指针变量的大小是多少,我们可以通过sizeof()操作符进行验证。

在64位环境下,地址是64位二进制数字,占有64个比特位,就目前情况整型指针变量的大小为8个字节,同样,在32位环境下也就是x86环境中,整型指针变量的大小为4个字节。
二,指针类型及其意义
通过前面的c语言学习我们了解到,要想知道一个变量的类型只需要在创建变量的语句中去除变量的名称,那么我们可以知道整型指针变量类型就是int*。
推而广之,int*用于存放int 类型变量的地址,那么char*就用于存放char类型变量的地址,double*就用于存放double类型变量的地址...x*就用来存放x的地址(x为变量类型)。我们将x*统称为指针变量类型。
那么我们知道在同一开发环境下,地址的长度不变,那么存放地址的变量的大小必然不会改变,也就是说在同一开发环境下,指针变量的大小相同。我们用x86环境测试一下:
既然不同类型的指针大小都是相同的,那么又何必大费周章,创造如此之多的变量,直接统一出一个通用变量不就行了吗?答案是不行的,我们可以带着疑虑进入第三板块。
三,指针变量的运算
1.指针的解引用操作
虽然不同的指针都指向所指向的数据在内存中存储的第一个字节,但指针变量的类型决定了指针指向区域的管辖范围,管辖范围的大小和指针所指向变量大小相同。举例说明,如果p是一个指向整型变量t的一个指针变量,p指向的是t(存储的数据为为0x11223344)在内存中存储的第一个字节,p所管辖的区域就是一个整型变量的大小也就是4个字节。
这样我们就可以通过解引用操作符(*)来获取指针变量所指向区域的数据了:

我们归纳总结一下:&操作符是获取变量的地址,而*操作符是通过指针访问所指向的数据,二者可以说是一对逆运算。
2.指针加减整数
先说结果,指针加减整数得到的结果还是指针,而这个值与原指针的差值是由指针变量的类型决定的。如果是一个x*类型的指针p进行运算:p+n所表示的指针变量存储的地址是p+n*sizeof(x).
我们通过代码来检验一下:

3.指针-指针
指针减指针的绝对值是两指针所指向同一区域中,两指针中间的元素个数,所以两个指针向减,需要两个指针必须是相同类型的并且必须指向同一块区域。通过这样一个运算我们就可以来模拟实现一种方式的strlen函数:

代码当中提到了const 关键字,我们先不着急,下一个板块且听我娓娓道来。
4.两指针之间的逻辑运算
两指针之间的逻辑运算说到底就是指针比大小,这里就不一一赘述了。
四,const修饰指针变量
先说结论:在创建一个指针变量p:int*p=&a;
如果const在*之前,a的值就不可通过解引用修改了,如int const*p=&a;const int*p=&a;
如果const在*之后,那么p的值就不可修改了,如int*const p=&a;
如果*前后都有const修饰,则p和a的值(解引用途径)都不可以修改。
案例如下图:

记忆方法:我们可以把const看作是一把锁,寻找锁之后的对象*a/a,锁之后的第一个对象是不能通过此途径来改变的。
五,数组与指针
1.[ ]的本质
在数组部分我们学到了下标访问操作符[ ],下面是对[ ]的本质解释
如果创建一个数组int arr[20]={0};
int *p=&arr[0];
那么访问下标为k的元素:arr[k],*(p+k)二者是等价的.
由于加法的交换律,所以上式与*(k+p),即k[arr],p[k]都是等价的。
那么我们就可以通过以下代码来访问一个一维数组。(注释部分是殊途同归的写法)

2.数组传参
在数组传参中,函数接收的也是数组首元素的地址,我们以以下代码为例:
3.数组名和指针的关系
一般来说,数组名及首元素的地址,当数组是一维数组时arr=&arr[0];

但是存在特例,当遇见&数组名和sizeof(数组名)时要当心,这时候数组名不再是数组首元素的地址而是整个数组的地址,通俗地讲这时候的数组名虽然还是一个指针变量,其中存放的地址与数组首元素地址相同,但是管辖范围发生了变化,指向了整个数组。我们通过以下代码来验证;

那么既然&arr中arr是一个指针,但是它指向一个数组,那么它是一个什么类型的指针呢?这里我们引入一种新的指针变量类型——数组指针。
4.数组指针
数组指针,顾名思义,就是和&arr中的arr有异曲同工之妙的指向一个数组的指针。在创建数组指针的代码:int *p[20]=&arr;将变量名称去除就是变量的类型:int*[20](数组指针),推而广之,一个指向存放n个x类型的数组的指针的类型是x*[n].
5.指针数组
指针数组,就是存放指针的数组,如果存放整型指针,那么数组的元素都是int*类型,下面是指针数组的用法之一——用指针数组模拟二维数组:

6.二维数组传参
我们来看一下以下代码:

通过对上述代码的观察我们发现二维数组传参和上述用指针数组模拟二维数组大同小异(可能逻主上有问题,但不妨碍将二维数组传参的本质解释清楚)。二维数组传参和一维数组一样都是传的数组名,数组名即数组首元素的地址,那么二维数组的首元素究竟是什么?
我们将这组代码与用指针数组模拟二维数组的代码相对比就不难得出结论:二维数组的首元素(注意不是arr[0][0])与指针数组首元素相同,都是指针变量,但二维数组首元素是一个数组指针,指向一维数组,指针数组的首元素未必是数组指针。言归正传:二维数组传参传的是首元素,是指向数组第一列(将第一列看成是一个数组)的一个数组指针,即arr[0].
7.指针数组和数组指针的比较
指针数组:是数组,存放的元素是指针
数组指针:是指针,指向数组
辨别,由于[ ]的运算优先级较*高,所以int* [10]表示指针数组,int(*)[10]表示数组指针。
六,字符串与指针
我们可以通过字符指针来存放一个常量字符串,也可以通过一个字符数组来存放一个字符串,二者的区别请看如下代码:

用字符指针只能存放一个常量字符串,所以这个指针是const char*类型,而且常量字符串在内存中的存储会在内存中单独开辟一块空间,所以p1,p2的地址相同。而p3,p4是数组名,将字符串赋值到这两个数组中,所以p3,p4指向的位置不同。
七,函数与指针
1.断言
首先介绍一个工具assert宏:assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。
2.传值调用和传址调用
如果我们要写一个函数将两个值交换:

上述代码就是函数的传值调用,然而程序不符合我们想要达到的效果。
理由如下:

通过调试我们发现a和x,b和y它们的地址不相同,在函数传参中,形式参数(x,y)仅仅是实际参数(a,b)的一份临时的拷贝,所以改变形式参数的值对实际参数的值毫无影响。
要想改变实际参数就必须在函数传参过程中使用传址调用:

3.函数指针
先看一段代码

函数指针(假设一个函数返回值是x,参数个数为n,参数类型分别是x1,x2,......xn,那么这个函数指针类型为x(*)(x1,x2,......,xn))是指向函数的指针,就是函数名,所以在获取函数的地址时&函数名和函数名等价,在使用函数指针进行函数调用时*函数指针和函数指针等价。
4.转移表
格式相似的函数存在类型相同的函数指针,我们将这些函数指针存放到一个函数指针数组当中,通过访问数组中的元素进行函数调用,代替多分支switch语句,这样的一个函数指针数组就叫做转移表。
空说无凭,我们通过代码来加强一下理解:

5.回调函数
回调函数:将函数a的函数指针作为函数b的参数,在函数b的运算过程中通过函数a的指针调用a,进行计算得到的c,c参与运算得到函数b的返回值,那么函数a就称之为回调函数。
举例

c语言中有一常用函数就使用了回调函数——qsort 函数——根据一定的顺序给任何类型的数组排序的库函数。使用时需引用stdlib.h库。
首先引入一种指针类型——泛型指针void*类型。可以接受任何类型的指针,但是在通过*访问接收地址所指向的变量x时需要将它强制类型转换成x所对应的类型。
什么意思?
举例:
int a=0;
int *p=&a;
void*pp=p;
*((int*)pp)才可以访问a;
下面介绍qsort的用法

函数有四个参数,第一个参数是数组的地址,第二个参数是待排序的元素个数,第三参数是元素的大小,第四个参数是两两元素的比较函数指针,(这里就用到了回调函数)。
实战操作如图:

八,野指针和指针的规范使用
什么是野指针
野指针就是指向位置未知的指针
野指针的成因
指针的越界访问(如使用指针p=&arr[0]访问数组arr[10],*(p+10)中的p+10,就是野指针)
使用的指针未初始化
(如int*p;printf("%p",p);)
指针指向的空间已经释放(如p是指向函数fun的形式参数x的指针,在main函数中fun后通过p访问x,p就是野指针)
如何避免野指针
指针初始化,使用时判断指针是否越界,不用的指针置为NULL,使用指针判断指针p是否为空(如使用assert(p!=NULL)).
完!!!









