文章目录
前言
说到指针,大家也许会谈之色变;指针是C语言学习中一个非常重要的部分,有很多内容都会涉及到指针的知识,所以指针的学习非常的重要。
一、 地址、内存、指针
说到地址大家都不言而喻,地址对于我们要去某个地方或者要找某人等;都非常的重要,它能极大的帮助我们去完成这些事情。那么内存跟地址又能扯上什么关系呢?我们买的电脑内存通常有8GB/16GB/32GB或者更大,这些空间相对于单个数据来说是非常大的;当某个数据储存在内存的某个地方时,我们想要使用这个数据,怎样才能快速拿到这个数据呢?要是可以得到这个数据的地址就方便了,就能通过这个地址快速拿到这个数据。内存正是我们想的这样,内存存划分了⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。而这个内存单元的编号也称为地址。那指针又是什么呢?其实是C语言给地址取了个专门的名字,就是指针。所以我们可以理解为:内存单元的编号 = 地址 = 指针。
二、指针变量
指针变量是什么呢?把重点移到变量上,来看;我们知道变量是用来放某个类型的数据的,而指针其实就是地址,再来看,那指针变量不就是用来放地址的变量吗。
1)取地址操作符(&)和解引用操作符(*)
- 取地址操作符
在C语言里创建变量其实就是向内存申请空间,而内存的空间都会有自己的编号也就是地址;那也就是我们创建了变量,这个变量就有了自己的地址了吗?通过一段简单代码看看:
#include <stdio.h>
int main()
{
int a = 10;
return 0;
}
我们通过调试里的内存窗口来看看,是不是有某个地址存放了变量a
一个int类型的变量大小是4个字节,而一个内存单元的大小只有一个字节;所以要想存放下变量a就需要四个内存单元。可以看到从38~3B的四个内存单元被用来存储a变量了,并且0x004FFA38里存放了0a(这里是十六进制,a代表10)不就是a变量的值10吗?把10写成二进制,补满32位,每八个比特位写成一个十六进制得到就是:00 00 00 0a;这里是倒着存的,至于储存的方式跟电脑是大端还是小端机器有关。
那我们要如何得到变量的地址呢?这里就需要使用到一个取地址操作符(&),比如要得到变量a的地址;只需要这样写就行:&a
- 解引用操作符
我们在拿到某个变量的地址后,如果想把它存储起来,又该怎么办呢?我们知道指针变量可以用来存放地址,那么该怎样写呢?
int a = 10;
int* pa = &a;
int * pa = &a;//这种与上面的都是一样的,只是不同的写法
这里的pa就是一个指针变量,p是(pointer)指针单词的首字母,这是一种给变量命名的习惯,我们知道变量起名要尽可能有意义要好一些。
那这个指针变量的类型又是什么呢?int * pa;这里的* 表示pa是指针变量,前面的int表示指针变量pa指向的a变量是int类型的;把指针变量的变量名pa去掉剩下的int* ,就是这个指针变量的类型。指针变量的类型要和被取地址的变量的类型相对应。比如:char ch ,那么&ch就需要用char* 的指针变量来接收。
那么pa里面真的放的是a变量的地址吗?可以用占位符%p把pa打印出来看看:
可以看到&a的地址编号和指针变量pa里存的值是一样的,通过%p打印出来也是一样的。
拿到a变量的地址之后,就可以通过指针变量来得到或者修改a变量的值了:
int main()
{
int a = 10;
int* pa = &a;
printf("%d\n", *pa);
*pa = 20;
printf("%d", a);
return 0;
}
这里*pa的意思就是通过pa中存放的地址,找到指向的空间,也就是a变量;所以*pa其实就等同与a变量了。同理*pa = 20,也就是把a变量重新赋值成20了,究竟是不是这样呢?代码跑起来一试便知:
可以看到,结果确实如此!
2)指针变量的大小
我们知道不同类型的变量的大小都是不同的,那么不同类型的指针变量也是这样的吗?答案的:不是的,指针变量的大小跟电脑是32位还是64位的机器有关。
来看看是不是这样的,x86是32位的平台,x64是64位的平台;sizeof可以查看变量的大小,单位是字节:
这里可以得出结论:指针变量的⼤⼩和类型是⽆关的,只要是指针类型的变量,在相同的平台下,⼤⼩都是相同的。
3)指针变量类型的意义
那么既然在同平台下指针变量的大小都是相同的,指针变量为什么还要区分不同的类型呢?通过调试一段代码看看:
开始num在内存里存放了11 22 33 44;通过int*的指针p重新赋值成0;内存里存的值都被改成0了。下面再来看:
可以看到,开始num在内存中也是存了11 22 33 44;这次我们通过char*的指针来把它赋值位0,结果可以看到,只有一个字节被改成了0;
通过这个上面的例子可以得出:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(即⼀次能操作⼏个字节)。
- void*的指针
void*是一种特殊类型的指针可以理解为⽆具体类型的指针(也被称为泛型指针);这种指针可以接收任意类型地址,不过不能直接对其解引用来使用;需要先强制类型转换为需要的类型在对其解引用再来使用。该如何使用呢,来看一个库函数qsort:
qsort函数能对不同类型的数组进行排序,之所以能够这样,是因为使用了void*的指针;设计者在设计函数的时候当然不会知道我们使用这个函数时是用来排序什么类型的数据,而void*的指针可以接受任意类型的地址,就能满足这个要求。来看qsort的参数,第一个void* base,就是传需要排序的数组的起始地址;第二个size_t num,是需要排序数据的个数,size_t size,是排序的单个元素的大小;最后这个参数是一个函数指针,这个函数的返回类型是int,这个函数的两个参数都是void*的指针,你把这个函数传给它,它按你的要求来进行排序。
简单使用一下看看:
#include <stdio.h>
#include <stdlib.h>//qsort需要的头文件
int swap1(const void* n1, const void* n2)
{
return *(char*)n1 - *(char*)n2;
}
int swap2(const void* n1, const void* n2)
{
return *(int*)n1 - *(int*)n2;
}
int main()
{
char ch[] = "badzfhgioq";
int arr[] = { 2,1,4,9,5,7,3,6,8 };
size_t len = strlen(ch);//求ch长度
size_t sz = sizeof(arr) / sizeof(arr[0]);//求arr长度
printf("排序前:%s\n", ch);
qsort(ch, len, sizeof(ch[0]), swap1);
printf("排序后:%s\n", ch);
printf("排序前:");
for (int i = 0; i < sz; i++)
printf("%d ", arr[i]);
qsort(arr, sz, sizeof(arr[0]), swap2);
printf("\n排序后:");
for (int i = 0; i < sz; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
可以看到qsort对字符数组和数字数组都能排序,它还能排序更多类型的数据就不一一举例了,感兴趣的朋友可以去研究一下。
对于qsort函数的第四个参数的函数的返回值的规定:
如果需要满足条件(前一个元素 < 后一个元素),就让函数返回一个小于0的数
如果需要满足条件(前一个元素 == 后一个元素),就让函数返回0
如果需要满足条件(前一个元素 > 后一个元素),就让函数返回一个大于0的数
三、const修饰指针
1)const修饰变量
变量是可以被修改的,如果我们想要一个变量不能被修改就可以使用const来修饰这个变量。但是这样真的就能保证这个变量就一定不会被修改了吗?下面来看看:
能够看到被const修饰之后的变量a,直接修改编译器会直接报错;好像确实可以达到想要的效果了。继续来看:
居然就被修改了!const修饰的变量不是不能被修改了吗?举个例子来说下,比如一个房间我们把门给锁上,就像用const修饰一样的,门锁上了就不能从门进入这个房间了;但是进这个房间就只能从门进来吗?翻窗户也能进呀。这里其实是绕过a,使⽤a的地址,去修改了a,显然这样做是在打破语法规则。那该怎样才能使用地址也不能完成修改呢?下面继续来看
2)const修饰指针变量
const修饰指针变量时分两种,分别是将const加在*号的左右两侧;这两种达到的效果是不一样的。当然也可以在*号的两边都加上const,这样两种效果都能达到。
const如果放在*号的左边,修饰的是指针指向的内容,就是指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可以被修改。
const如果放在*号的右边,修饰的是指针变量本⾝,就是指针变量本身的内容不能修改,但是指针指向的内容可以通过指针修改。
四、指针运算
1)指针加减整数
在数组中,元素都是连续存放的,当然各个元素的地址也是连续的。我们知道每个元素都有对应的下标;比如有这么一个数组:
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
我们知道如果要访问5这个元素,可以这样:arr[5];其实就是*(arr + 5);数组名arr是数组首元素的地址,给arr加上5,就是第五个元素的地址,在解引用得到的就是第五个元素了。
2)指针减指针
来看一段代码:
int main()
{
char ch[] = "abcdef";
char* end = ch;
while (*end != '\0')
++end;
printf("字符串:%s的长度是:%d\n",ch, end - ch);
return 0;
}
上面代码里,先创建了一个指针变量end,从字符串的开头先走到字符串的结束标志’\0’(字符串的结尾默认会有结束标志’\0’),然后在利用end - ch做差;得到的就是字符串的长度了。来看看是不是这样的呢?
那能不能指针加指针呢?指针加指针其实是没有实际意义的,我们可以想象一下日期,用一个日期减掉另外一个日期得到的是他们之间相差的天数,但是一个日期加上另外一个日期得到的是什么呢?我们最多只会用一个日期加上多少天数,而不会是加上某个日期。这里是指针也是同样的道理。
3)指针的关系运算
指针和指针之间还能比较大小,比如还是数组:int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};从数组起始到结束地址是连续的,地址编号也是递增的。我们可以利用这点来打印这个数组里的元素:
可以看到这里利用了指针的大小比较作为条件,来成功访问的数组里的所有元素。
五、野指针
野指针的概念:指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1)形成原因
1.指针变量未初始化就对其解引用使用。
指针变量未初始化默认存的是随机值,如果直接对其解引用使用,就会把这个随机值当成一个地址,这时就会构成非法访问了,这个指针也就成为了野指针。
2.指针的越界访问。
在对数组进行访问时,比如数组有10元素,数组元素的下标只到9,但是我们使用指针访问到了超过9的位置,这时指针就成了野指针。
3.指针指向的空间被释放
在我们自定义的函数里创建的变量在函数结束之后就会被销毁了;如果把该变量的的地址返回给了主函数里的某个指针变量,再对这个这个指针进行解引用操作,就构成了非法访问,也会导致野指针。
2)避免方法
创建指针变量时,如果不知道给该指针初始化什么地址时,先给指针变量赋值NULL;对于使用完了,并且不需要再继续使用的指针变量,也将其置为NULL。
⼩⼼指针越界,只能对在内存里申请了的空间进行访问,超出范围访问就是越界了。
避免返回局部变量的地址,来避免变量已经被销毁了,却还对储存了已经被销毁的变量地址的指针进行解引用操作。
六、指针和数组
在上面指针的加减整数部分时,说数组名其实是数组首元素的地址;其实并不完全是这样,有两个例外;一个是:&数组名,这里并不是取出数组首元素的地址,而是取出整个数组的地址。另外一个是:sizeof(数组名),这里求的不是数组首元素的大小,而是整个数组的大小。
1)一维数组传参
在自定义函数时,如果需要将数组作为函数的参数;在函数定义时通常也会用一个数组来接收。但是数组在传参时传的只是数组名,这时的数组名也并不是上面说的两种情况之一。那这里的数组名就是数组首元素的地址了,既然是地址那接收时直接用指针变量来接收不就行了吗?确实是的,举例看看:
可以看到这两种方法都可以。
2)指针数组
首先问下大家,指针数组是指针还是数组呢?在说指针变量时,说指针变量其实是个变量,再比如:可爱的小狗、听话的小孩;我们一看便知说的是小狗和小孩。这里也一样,指针数组;指针就是一个修饰词,本质还是数组。只不过指针数组里存放的是指针。
我们知道数组名是首元素的地址,那么几个同类型的数组名是不是就能放到一个指针数组里管理起来呢?如果可以就能使用这个数组的下标来访问这些被管理起来的数组了。下面来看看:
能够看到是可行的,分析一下,开始i为0;进到第二层的for循环里,j会从0一直循环到4,第一次就是arr[0][0];这个其实就是:*(*(arr+0)+0);arr+0,解引用拿到的不就是arr1数组的首地址吗,在对arr1+0,解引用拿到的就是arr1数组的首元素了。这样j一直循环到4,就拿到了arr1数组的所有元素了。然后跳出循环,i加1又会再次进入第二层循环里,访问的就是arr2数组了。
仔细一看这个跟二维数组长的好像一样了,不过这里只是用指针数组模拟了一个二维数组的效果,并不等于二维数组,实际的二维数组里是元素在内存中都是连续存放的,这里模拟的并非是连续的。
3)数组指针
看过指针数组,我们就知道数组指针说的是指针,数组只是个修饰词。那这个数组指针又是什么呢?是指向数组的指针吗?对的,就是指向数组的指针。这个数组指针是什么样的呢?先拿个例子来看下吧:int (*parr)[10];就是一个数组指针;怎么理解呢?
这里*parr用括号括起来了,*先和parr结合表示parr是指针,后面的[10]表示有10个元素,前面的int表示每个元素都是int类型的。
看看是不是如所说的一样:
可以看到数组指针parr的类型和取&arr是一样的。
4)二维数组传参
二维数组在传参时,传的也是数组名;而二维数组本质又是一个数组里的元素也是数组,那这里传的不也就是首元素的地址吗?首元素又是一个数组,那接收这个数组是地址就能用到上面的数组指针了。看看是不是这样的:
这两种方式的结果也是一样的。
七、二级指针
二级指针也是指针变量,当我们把指针变量的地址取出来时,就需要用二级指针来接收。
int main()
{
int a = 10;
int* pa = &a;// 指针pa接收变量a的地址
int** ppa = &pa;//而指针变量pa的地址则需要用二级指针ppa来接收
}
如何来理解这个:int ** ppa呢?
这里的两个*,一个给前面的int,一个给ppa;*ppa表示ppa是指针变量;int*表示指针ppa指向的对象是int*的指针。
二级指针的运算操作:
- *ppa;对ppa解引用操作,得到的就是ppa指向的指针变量pa;即*ppa就相当于pa。
- **ppa;是先对ppa解引用找到pa,在对pa解引用,找到的就是变量a了;即**ppa就相当于a。
用个简单的例子来验证一下:
八、指针和函数
1)函数指针变量
函数指针;一看显然也是种指针,是一种指向函数的指针变量。那我们要怎样得到一个函数的指针呢?函数是否有地址呢?来看看:
能够看到,我们对函数名取地址,和直接使用函数名,利用%p打印其地址来观察;得到的地址是一模一样的。便可以得到结论,函数名其实就是函数的地址。
那既然函数是有自己的地址的,把一个函数的地址存起来就该用函数指针变量来存了。那么函数指针变量长什么样呢?再举个例子来看下:
可以看到正常调用函数和使用函数指针来调用函数都可以。
怎样来理解例子中使用的函数指针变量int (*pf) (int , int )呢?其实有点类似于数组指针。
- *pf使用括号括起来了;*先和pf结合,表示pf是指针变量。
- 前面的int;带表这个函数指针指向的函数的返回类型为int类型。
- 后面的(int, int);表示函数指针指向的函数有两个参数,类型都是int类型的。
2)函数指针数组
在有多个函数时,而且这些函数的返回类型,参数个数和类型都相同;这不就满足了数组的定义了吗?那我们是不是可以通过一个数组把这些函数都管理起来呢?理论上应该是可行的,因为我们这样并没有违反某个规定。实际这样也是可行的,这种数组就被称为函数指针数组。这里也举个例子看下:

我们可以来验证一下可不可行:

可以看到,我们利用函数指针数组的下标,成功的调用了函数指针数组里的函数。
讲到这里就结束了,指针部分是一块“硬骨头”;学习时需要花费较多的时间。指针也是C语言学习中不可或缺的部分,有太多的内容都会涉及到指针的知识。要把指针学好虽然比较具有挑战性,但是我们不要害怕指针,脚踏实地的把指针的知识都弄清楚,相信指针在我们面前终会变成“纸老虎”。爬坡的路是艰难的,但也是最让人进步的,一起努力吧!如果这篇博客能帮助到您,十分荣幸!如有讲的不对的地方还请能够指出,十分感谢!最后感谢大家的阅读!




















1万+

被折叠的 条评论
为什么被折叠?



