C指针相关详细知识点

指针的本质:实现间接访问

指针变量就是用来存储其他变量的地址的一个变量。指针的概念就不在这里赘述了

1.易错的定义方式

如图,上面的是正确的连续定义方式,下面的是错误的方式。

    编译器警告中可以看出,下面的定义方式只有a才是指针变量,而b和c都是普通的整形变量。由此可以得知,定义指针变量的时候,*和‘变量名'是一体的,不能分开。

2.指针的传递使用场景

    这里就要先说一下函数的参数传递了,函数的参数传递都是值传递(无论传递的是普通变量还是指针变量),只不过前后两者一个是普通变量的值传递,一个是指针变量的值传递,下面用个小例子。

从例子中可以看出, 在函数change()中通过修改j的值,并不能修改i的值。

为什么呢?接下来进行说明:

    其实每个函数在调用的时候,系统都会为该函数在栈空间中分配一块内存空间,供其使用。 而delivery_pointer()和change()两个函数自然被分配了不同的栈空间。 在delivery_pointer()中定义的i变量,占用的是delivery_pointer()的栈空间, change()中的形参j,占用的是change()的栈空间。

    在delivery_pointer()函数中通过change()传递i的时候,其实是相当于将i的值赋值给change()中的j,(int j=i;)。虽然两个变量数值相同,但是却存于不同的地址空间,所以更改j并不能同时改掉i,更改j仅仅相当于更改了i的一个副本。通过分别打印变量i和变量j的地址来进行确认。

从图中可看出,i和j的地址并不相同,故无法通过更改j而改掉i的值。

那么怎么在其他函数中更改i的值呢,这时候就需要使用到指针变量了。如图,我们重新写一个change1()方法。

可以看到,在change1中成功更改了i的值。这是因为我们利用指针变量进行了间接访问操作,所以成功改了i的值。

指针变量就是用来存储其他变量的地址的一个变量。我们在调用change1();方法的时候,传入的是&i(变量i的地址),而在change1();中的形参我们定义成了int *j,即j便是指针变量了,我们用j指针变量存储了i的地址。说白了就是指针变量j的值,就是变量i的地址。我们打印验证一下。

可以看到这时候我们就可以通过j来更改i的值了,因为j的值便是变量i的地址了。

这时候在j的前面加上*(星号就是取值符,通过地址取值,*j就相当于i),就可以通过j里存储的i的地址,找到i变量的存储位置,找到了i的地址,就可以成功的对其值进行更改。

三、指针的偏移和自增自减

指针的偏移可以理解为指针的加减。指针的偏移步长跟他的基类型有关。

如指针p指向一个整型一维数组,那么如果进行(p+1)操作,并不是简单意义上的p存储的地址加1,而是p存储的地址值加了4(int类型变量的大小)

 因为整型数组变量的每个元素都是int类型,占四字节内存,故(p+1)即是将指针从原来的指向首元素变为指向第二个元素的了,地址值+4

 同理如果是结构体指针,加一的结果就是偏移一个结构体的大小。

了解了这个原理,就可以用指针来对数组进行打印了。如图

接下来看几个指针自增的小例子

四、指针与动态内存申请(堆内存使用)

平时我们定义的数组长度是固定的,是因为都定义在栈空间中,而栈空间的大小在编译时是确定的。如果使用的空间大小不确定,就要使用堆空间。而使用堆空间就需要用到malloc()函数进行堆空间的申请

void *malloc(size_t __size) __result_use_check __alloc_size(1);

注意:malloc的参数类型是size_t,size_t就说明申请内存的大小是以字节为单位来计算的

首先上一个小例子,

(1)申请:malloc申请好地址之后,返回的是(void *)类型,我们需要根据需要对该返回值进行类型转换。

这里说一下(void *),即无类型的指针变量,为什么不能使用(void *)呢

当我们使用(void *)类型的指针的时候,如果我们对该指针进行偏移操作时,如+1操作,这时编译不会通过,因为(void *)使得编译器并不知道该指针的具体类型,偏移时无法确定偏移步长,因为在前面说过,指针的偏移步长是与及类型相关的。

(2)释放:malloc申请的空间,使用完需要手动释放,用free()函数,释放的原理就是根据申请时的起始地址,和我们申请地址的长度,找到对应的地址空间,然后进行回收,而起始地址和长度,是在我们malloc申请的时候,就被操作系统存到一个表里了,释放的时候就会去查表,然后释放。

注意:给free()函数传入的指针必须是malloc()原本返回的

 因为如果当我们申请到空间,将起始地址保存在指针变量p中后,对指针p进行了偏移操作,那么将无法回收地址,因为起始地址发生了偏移,操作系统无法找到相应的空间进行回收操作,程序会报错。

 如果非要进行偏移操作,那么在偏移操作之前,要将起始地址先存下来,可以存放在另一个指针中,等到回收时,将保存起始地址的指针传给free()函数即可

五、堆空间和栈空间的区别

记录完堆空间的申请之后,用一个小例子来说明堆、栈空间的区别,或者说使用注意事项。用字符串为例,如图

如上图小示例,stack_string()中创建的字符串,没有被成功打印,而heap_string()中创建的字符串,就成功被打印了。这是因为

(1)栈空间在函数结束时被释放,释放后该栈空间中原本所定义的变量,也都会随之释放。

(2)堆空间不会随着函数的执行结束而被释放。(注意是p所指向的申请到的那片堆空间不会被释放),heap_string()函数的栈空间依旧会被释放

所以stack_string()函数分配的栈空间,在退出该函数时就释放了。虽然p仍然指向原本hello那一片地址,但是stack_string()栈空间中字符串hello已经随着栈空间释放而释放了

六、字符数组与字符指针

首先,C中对字符串进行了优化,即相同的字符串在代码段中只有一份,之后遇到相同的字符串时,就用同一份地址(字符串被存放在代码段区,故地址为代码段区地址)。

p[0]不可更改的原因:因为指向字符串常量区,没有写权限,代码段区域的数据在编译时就已经定死了,不能更改

为什么a[0]能更改呢?因为当字符串在赋给数组时,会调用strcpy(c,"hello"),将代码段中的"hello"字符串copy了一份到栈空间,栈空间堆空间可读可写,故可以修改。

再说下面的p="world",这个应该不用解释了,就是更换了p的指向,并没有试图更改代码段的操作。

a="world"是直接就会报错的语句,编译都不能通过,因为a的地址值已经确定下来了,数组名里存的地址编译时确定,数组是一块固定的分配好的空间,地址不能更改。你只能修改数组元素的值。

七、野指针

什么是野指针?就是当一个指针变量所指向的地址空间被释放了(回收了),却没有将指针置空,这时指针仍然指向那块被释放掉的内存空间,就被称为野指针。野指针是有很大的风险的,看例子。

为什么*p3的值由3变为了100呢?

原因就是前面对p1指针指向的内存空间进行了free释放,但是却没有将指针p1置为空,就导致虽然释放了空间,但是p1仍然指向那块被释放的空间,这时候p1就是野指针。

为了降低系统调用的调用次数,free释放的空间并不是直接还给操作系统,而是偷偷留着了,等到其他malloc内存申请的时候,就可以直接将其给出去,所以上面中p1经过free之后,释放的空间又被后面的p3申请了过去,而此时p1还没有被赋为NULL,所以p1和p3同时指向这一片空间,故后面继续使用p1修改数值的时候,就导致了*p3的数值也跟着变化。

因此,在使用指针的时候,一定要注意,释放指针指向的空间后要把指针置空,即p=NULL;

八、理解const,以及const和define的区别

以整形变量为例,首先const int 类型一旦定义,就不能修改,而int类型可以随时修改。

因为const int 是用来保存一些全局变量的,因此这些常量在编译期间可以修改,但在运行期间不能修改,听起来很像宏,其实他确实是用来取代宏的。

例如

 比较#define PI 3 和 const int pi = 3;

 如果代码中用到了100次PI(宏),代码中就会保存100个常数3。而换用了const int pi=3;时,程序编译后的机器码中就不需要出现100次常量3,而只在需要时引用存储有3的常量。

 const定义的常量在程序运行过程中只有一份副本,而#define定义的常量在内存中有若干副本。

接下来看一下const使用易错点,上例子,展示两个使用const定义指针变量的易错点。

原因已经在图片里写得很明白了,就不做赘述。

九、数组指针和二维数组

指针和二维数组之前在C数组的笔记中已经介绍了一些,在这里再详细的说一下。

首先要注意区分数组指针指针数组的区别以及定义方法的区别。如图

这里就要进行一番文字说明了:

当我们直接打印数组名的时候,其类型是相应的数组类型。但是特殊的是,数组名中是存有地址的,是可以像指针一样进行偏移操作的,而在对数组名进行偏移操作时,他的类型就会变为指针类型。后面有例子论证

一维数组名里保存的是第一个元素的地址。

二维数组名里保存的是第一行的一维数组的地址,注意是整个一维数组的地址(如数组是4列的,则指向的是长度为16字节的空间),故当对数组名a进行偏移时,(a+1)与a的地址是相差16字节的,打印印证一下:

从图片中也可看到a在偏移时的类型,就是数组指针类型,指向一维数组。

故可以让数组指针指向a,因为数组指针就是用来指向一维数组的。以上图中变量为例,开始逐步推理二维数组的取值原理

(1)p和a中都是存放的该二维数组的第一行的一维数组的地址,如图

(2)(p+1)和(a+1)是相同的,都是指针的偏移操作,步长为一个一维数组的长度。都指向该二维数组的第二行一维数组的地址,如图

(3)*(p+1)和*(a+1)对存放的一维数组地址取值,结果就是得到对应的一维数组,等价于a[1] (在二维数组中a[1]是一维数组名,并不代表某一元素的值),*(p+1)和*(a+1)现在就相当于一维数组名,而一维数组名又相当于整形指针,指向该一维数组的首元素地址,如图

从这可以看出,*(p+1)和*(a+1)就是代表数组名,打印显示他们的类型就是一维数组,而我们如果对其进行偏移时,他的类型就会转化为指针类型,如图

这就论证了前面开头所说的当我们直接打印数组名的时候,其类型是相应的数组类型。而在对数组名进行偏移操作时,他的类型就会变为指针类型。

(4)故对*(p+1)和*(a+1)继续进行偏移操作,*(p+1)+1和*(a+1)+1,便是指向了该一维数组的第二个元素,因为这里*(p+1)和*(a+1)就相当于是一维数组名,故偏移一位,步长为四字节,也就是一个整型元素的长度。如图,

(5)最后再进行取值操作*(*(p+1)+1)和*(*(a+1)+1),便得到了a这个二维数组,第二行第二列的元素值了。

(6)a[][]的形式也是同理,与上方的*(*(a+1)+1)同理,原理就是两次指针偏移,第一次偏移是数组的行偏移,第二次是列偏移,而后便可找到对应下标的元素值([]符号是变址运算符,与*()是同理的)

(7)拓展一下&a+1代表什么呢?首先&a的类型为二维数组指针(int (*)[][]),它指向了该二维数组的整个空间,加一偏移操作就相当于向后偏移了一个该二维数组大小的空间,即a数组空间地址的末尾

可以从打印中看出,a数组的首地址与&a+1的打印结果相差了48字节,而a数组中有12个整型变量,每个整型变量占四字节,加起来刚刚好是48字节,这就印证了我们的结论。     

总结:任何变量取地址+1,都指向该变量所使用空间的末尾。

这回彻底了解了二维数组和数组指针的关系,便利用数组指针对二维数组做一次打印,练习一下收尾。

完成。

十、函数指针

通过一个小例子简单展示一下函数指针的用法

本例中定义了函数指针p,他将函数b赋给p。这里为什么可以进行赋值呢?其实是因为函数名本身存储的即为函数入口地址,接着将p传递给函数a,相当于把一个行为传递给函数a,之前我们传递给函数的都是数据,通过函数指针可以将行为传递给一个函数,这样我们调用函数a就可以执行b的行为,当然也可以执行其他函数的行为。

图片中对于函数指针的定义有警告,是因为编译器认为定义不规范,想去掉警告的话,在()里定义一个参数就好了,这里展示就没有定义参数。

十一、二级指针

二级指针的使用并没有一级指针那么多,且虽然看起来比一级指针要难,但是其实二级指针更加单纯,它不像一级指针那样,可以指向各种类型的变量,数组,包括结构体等。二级指针只能指向一级指针,用于存放一级指针的地址,可以理解为套娃操作,通过二级指针可以找到一级指针,而通过一级指针可以找到相应数据。

二级指针的偏移和传递

一级指针的偏移服务于数组,例如整型一级指针就服务于整型数组,所以二级指针的偏移也服务与数组,服务对象为指针数组。举个例子,我们网购时,搜索的商品信息就存储在内存中,如果以某个查询条件搜索商品,那么网站需要按照我们的要求对商品排序,这时交换内存中商品的信息会极大的降低效率,因为不同的用户会有不同的查询需求,而每种商品本身的信息存储量又较大。这时如果让每个指针只想商品信息,在排序比较时,我们比较实际的商品信息,但在交换时实际上交换指针,那么交换成本将会极大地降低。这种思想称为索引排序。如图,

排序时只交换指针数组中指针的位置,不去移动信息量较多的商品信息,会很大程度提高效率。

接下来先看一个简单的例子,适应一下。

本例中,可以看到pi,pj都为一级指针,在作为参数传递的时候,&pj用一个二级指针来接收,因为二级指针就是用来存放一级指针变量的地址的。并且我们通过二级指针成功改掉了一级指针pj的指向,让其从指向j变为指向i。比较简单,不做赘述。

下面看一个上面所说的,利用二级指针进行索引排序的例子。

这次在代码中做了详细的注释,这个例子就是创建了一个指针数组p和一个二维数组b,目的是先让p中的每个指针都分别指向b中的每个字符串,而后进行上面所论述的索引排序,即按照b中字符串的大小进行排序,排序只改变指针数组中指针变量的顺序,而不去改变b数组中的原数据的顺序。

一级指针传递,使用二级指针进行接收是很好理解的。但是指针数组的传递也用二级指针接收可能不太好想通,在这里说一下指针数组传递为什么要用二级指针接收?

为什么用char **接收呢,我来说一下我的理解,先回顾一下一维数组,在这里以int类型一维数组为例,一维数组名其中是保存着首元素的地址的,所以在一维数组传递时,为了接收数组名中存放的首地址(即第一个int元素的地址),我们就需要用到一级指针(int *)来接收,存放该地址

接下来再看本例中的p(指针数组),p便是数组名,那么依照整型数组的推理来看,p数组名中也是保存着数组首元素的地址的,而在p指针数组中,元素都是指针变量,首元素自然也是指针变量,那么为了接收指针变量的地址,自然而然就需要用到二级指针了,是不是很好理解了呢。

你可能感兴趣的:(C指针相关详细知识点)