说到指针,估计仍是有不少小伙伴都仍是云里雾里的,有点“知其然,而不知其因此然”。可是,不得不说,学了指针,C语言才能算是入门了。指针是C语言的「精华」,能够说,对对指针的掌握程度,「直接决定」了你C语言的编程能力。程序员
在讲指针以前,咱们先来了解下变量在「内存」中是如何存放的。编程
在程序中定义一个变量,那么在程序编译的过程当中,系统会根据你定义变量的类型来分配「相应尺寸」的内存空间。那么若是要使用这个变量,只须要用变量名去访问便可。数组
经过变量名来访问变量,是一种「相对安全」的方式。由于只有你定义了它,你才可以访问相应的变量。这就是对内存的基本认知。可是,若是光知道这一点的话,其实你仍是不知道内存是如何存放变量的,由于底层是如何工做的,你依旧不清楚。安全
那么若是要继续深究的话,你就须要把变量在内存中真正的样子是什么搞清楚。内存的最小索引单元是1字节,那么你其实能够把内存比做一个超级大的「字符型数组」。在上一节咱们讲过,数组是有下标的,咱们是经过数组名和下标来访问数组中的元素。那么内存也是同样,只不过咱们给它起了个新名字:地址。每一个地址能够存放「1字节」的数据,因此若是咱们须要定义一个整型变量,就须要占据4个内存单元。网络
那么,看到这里你可能就明白了:其实在程序运行的过程当中,彻底不须要变量名的参与。变量名只是方便咱们进行代码的编写和阅读,只有程序员和编译器知道这个东西的存在。而编译器还知道具体的变量名对应的「内存地址」,这个是咱们不知道的,所以编译器就像一个桥梁。当读取某一个变量的时候,编译器就会找到变量名所对应的地址,读取对应的值。ide
那么咱们如今就来切入正题,指针是个什么东西呢?函数
所谓指针,就是内存地址(下文简称地址)。C语言中设立了专门的「指针变量」来存储指针,和「普通变量」不同的是,指针变量存储的是「地址」。测试
指针变量也有类型,实际上取决于地址指向的值的类型。那么如何定义指针变量呢:url
很简单:类型名* 指针变量名操作系统
char* pa;//定义一个字符变量的指针,名称为pa注意,指针变量必定要和指向的变量的类型同样,否则类型不一样可能在内存中所占的位置不一样,若是定义错了就可能致使出错。
获取某个变量的地址,使用取地址运算符&,如:
char* pa = &a;若是反过来,你要访问指针变量指向的数据,那么你就要使用取值运算符*,如:
printf("%c, %d\n", *pa, *pb);这里你可能发现,定义指针的时候也使用了*,这里属于符号的「重用」,也就是说这种符号在不一样的地方就有不一样的用意:在定义的时候表示「定义一个指针变量」,在其余的时候则用来「获取指针变量指向的变量的值」。
直接经过变量名来访问变量的值称之为直接访问,经过指针这样的形式访问称之为间接访问,所以取值运算符有时候也成为「间接运算符」。
好比:
//Example 01程序实现以下:
//Consequence 01像这样的代码是十分危险的。由于指针a到底指向哪里,咱们不知道。就和访问未初始化的普通变量同样,会返回一个「随机值」。可是若是是在指针里面,那么就有可能覆盖到「其余的内存区域」,甚至多是系统正在使用的「关键区域」,十分危险。不过这种状况,系统通常会驳回程序的运行,此时程序会被「停止」并「报错」。要是万一中奖的话,覆盖到一个合法的地址,那么接下来的赋值就会致使一些有用的数据被「莫名其妙地修改」,这样的bug是十分很差排查的,所以使用指针的时候必定要注意初始化。
有些读者可能会有些奇怪,指针和数组又有什么关系?这俩货明明八竿子打不着井水不犯河水。别着急,接着往下看,你的观点有可能会改变。
咱们刚刚说了,指针实际上就是变量在「内存中的地址」,那么若是有细心的小伙伴就可能会想到,像数组这样的一大摞变量的集合,它的地址是啥呢?
咱们知道,从标准输入流中读取一个值到变量中,用的是scanf函数,通常貌似在后面都要加上&,这个其实就是咱们刚刚说的「取地址运算符」。若是你存储的位置是指针变量的话,那就不须要。
//Example 02程序运行以下:
//Consequence 02在普通变量读取的时候,程序须要知道这个变量在内存中的地址,所以须要&来取地址完成这个任务。而对于指针变量来讲,自己就是「另一个」普通变量的「地址信息」,所以直接给出指针的值就能够了。
试想一下,咱们在使用scanf函数的时候,是否是也有不须要使用&的时候?就是在读取「字符串」的时候:
//Example 03程序执行以下:
//Consequence 03所以很好推理:数组名其实就是一个「地址信息」,实际上就是数组「第一个元素的地址」。我们试试把第一个元素的地址和数组的地址作个对比就知道了:
//Example 03 V2程序运行结果为:
//Comsequense 03 V2这么看,应该是实锤了。那么数组后面的元素也就是依次日后放置,有兴趣的也能够本身写代码尝试把它们输出看看。
刚刚咱们验证了数组的地址就是数组第一个元素的地址。那么指向数组的指针天然也就有两种定义的方法:
...当指针指向数组元素的时候,能够对指针变量进行「加减」运算,+n表示指向p指针所指向的元素的「下n个元素」,-n表示指向p指针所指向的元素的「上n个元素」。并非将地址加1。
如:
//Example 04执行结果以下:
//Consequence 04有的小伙伴可能会想,编译器是怎么知道访问下一个元素而不是地址直接加1呢?
其实就在咱们定义指针变量的时候,就已经告诉编译器了。若是咱们定义的是整型数组的指针,那么指针加1,实际上就是加上一个sizeof(int)的距离。相对于标准的下标访问,使用指针来间接访问数组元素的方法叫作指针法。
其实使用指针法来访问数组的元素,不必定须要定义一个指向数组的单独的指针变量,由于数组名自身就是指向数组「第一个元素」的指针,所以指针法能够直接做用于数组名:
...执行结果以下:
p -> 00AFF838, p+1 -> 00AFF83C, p+2 -> 00AFF840如今你是否是感受,数组和指针有点像了呢?不过笔者先提醒,数组和指针虽然很是像,可是绝对「不是」一种东西。
甚至你还能够直接用指针来定义字符串,而后用下标法来读取每个元素:
//Example 05程序运行以下:
//Consequence 05在刚刚的代码里面,咱们定义了一个「字符指针」变量,而且初始化成指向一个字符串。后来的操做,不只在它身上可使用「字符串处理函数」,还能够用「下标法」访问字符串中的每个字符。
固然,循环部分这样写也是没毛病的:
...这就至关于利用了指针法来读取。
刚刚说了许多指针和数组相互替换的例子,可能有的小伙伴又开始说:“这俩货不就是一个东西吗?”
随着你对指针和数组愈来愈了解,你会发现,C语言的创始人不会这么无聊去建立两种同样的东西,还叫上不一样的名字。指针和数组终究是「不同」的。
好比笔者以前看过的一个例子:
//Example 06当编译器报错的时候,你可能会开始怀疑你学了假的C语言语法:
//Error in Example 06咱们知道,*str++ != ‘\0’是一个复合表达式,那么就要遵循「运算符优先级」来看。具体能够回顾《C语言运算符优先级及ASCII对照表》。
str++比*str的优先级「更高」,可是自增运算符要在「下一条语句」的时候才能生效。因此这个语句的理解就是,先取出str所指向的值,判断是否为\0,如果,则跳出循环,而后str指向下一个字符的位置。
看上去貌似没啥毛病,可是,看看编译器告诉咱们的东西:表达式必须是可修改的左值
++的操做对象是str,那么str究竟是不是「左值」呢?
若是是左值的话,那么就必须知足左值的条件。
❝❞
- 拥有用于识别和定位一个存储位置的标识符
- 存储值可修改
第一点,数组名str是能够知足的,由于数组名实际上就是定位数组第一个元素的位置。可是第二点就不知足了,数组名其实是一个地址,地址是「不能够」修改的,它是一个常量。若是非要利用上面的思路来实现的话,能够将代码改为这样:
//Example 06 V2这样就能够正常执行了:
//Consequence 06 V2这样咱们就能够得出:数组名只是一个「地址」,而指针是一个「左值」。
看下面的例子,你能分辨出哪一个是指针数组,哪一个是数组指针吗?
int* p1[5];单个的咱们均可以判断,可是组合起来就有些难度了。
答案:
int* p1[5];//指针数组咱们挨个来分析。
数组下标[]的优先级是最高的,所以p1是一个有5个元素的「数组」。那么这个数组的类型是什么呢?答案就是int*,是「指向整型变量的指针」。所以这是一个「指针数组」。
那么这样的数组应该怎么样去初始化呢?
你能够定义5个变量,而后挨个取地址来初始化。
不过这样太繁琐了,可是,并非说指针数组就没什么用。
好比:
//Example 07结果以下:
//Consequence 07这样是否是比二维数组来的更加直接更加通俗呢?
()和[]在优先级里面属于「同级」,那么就按照「前后顺序」进行。
int(*p2)将p2定义为「指针」, 后面跟随着一个5个元素的「数组」,p2就指向这个数组。所以,数组指针是一个「指针」,它指向的是一个数组。
可是,若是想对数组指针初始化的时候,千万要当心,好比:
//Example 08Visual Studio 2019报出如下的错误:
//Error and Warning in Example 08这实际上是一个很是典型的错误使用指针的案例,编译器提示说这里有一个「整数」赋值给「指针变量」的问题,由于p2归根结底仍是指针,因此应该给它传递一个「地址」才行,更改一下:
//Example 08 V2但是怎么仍是有问题呢?
咱们回顾一下,指针是如何指向数组的。
int temp[5] = {1, 2, 3, 4, 5};咱们本来觉得,指针p是指向数组的指针,可是实际上「并非」。仔细想一想就会发现,这个指针其实是指向的数组的「第一个元素」,而不是指向数组。由于数组里面的元素在内存中都是挨着个儿存放的,所以只须要知道第一个元素的地址,就能够访问到后面的全部元素。
可是,这么来看的话,指针p指向的就是一个「整型变量」的指针,并非指向「数组」的指针。而刚刚咱们用的数组指针,才是指向数组的指针。所以,应该将「数组的地址」传递给数组指针,而不是将第一个元素的地址传入,尽管它们值相同,可是「含义」确实不同:
//Example 08 V3程序运行以下:
//Consequence 08在上一节《C语言之数组》咱们讲过「二维数组」的概念,而且咱们也知道,C语言的二维数组其实在内存中也是「线性存放」的。
假设咱们定义了:int array[4][5]
array做为数组的名称,显然应该表示的是数组的「首地址」。因为二维数组实际上就是一维数组的「线性拓展」,所以array应该就是指的指向包含5个元素的数组的指针。
若是你用sizeof()去测试array和array+1的话,就能够测试出来这样的结论。
首先从刚刚的问题咱们能够得出,array+1一样也是指的指向包含5个元素的数组的指针,所以*(array+1)就是至关于array[1],而这恰好至关于array[1][0]的数组名。所以*(array+1)就是指第二行子数组的第一个元素的地址。
有了刚刚的结论,咱们就不难推理出,这个实际上就是array[1][2]。是否是感受很是简单呢?
总结一下,就是下面的这些结论,记住就好,理解那固然更好:
*(array + i) == array[i]咱们在上一节里面讲过,在初始化二维数组的时候是能够偷懒的:
int array[][3] = {刚刚咱们又说过,定义一个数组指针是这样的:
int(*p)[3];那么组合起来是什么意思呢?
int(*p)[3] = array;经过刚刚的说明,咱们能够知道,array是指向一个3个元素的数组的「指针」,因此这里彻底能够将array的值赋值给p。
其实C语言的指针很是灵活,一样的代码用不一样的角度去解读,就能够有不一样的应用。
那么如何使用指针来访问二维数组呢?没错,就是使用「数组指针」:
//Example 09运行结果:
//Consequence 09void其实是无类型的意思。若是你尝试用它来定义一个变量,编译器确定会「报错」,由于不一样类型所占用的内存有可能「不同」。可是若是定义的是一个指针,那就没问题。void类型中指针能够指向「任何一个类型」的数据,也就是说,任何类型的指针均可以赋值给void指针。
将任何类型的指针转换为void是没有问题的。可是若是你要反过来,那就须要「强制类型转换」。此外,不要对void指针「直接解引用」,由于编译器其实并不知道void指针会存放什么样的类型。
//Example 10这样会报错:
//Error in Example 10若是必定要这么作,那么能够用「强制类型转换」:
//Example 10 V2固然,使用void指针必定要当心,因为void指针几乎能够「通吃」全部类型,因此间接使得不一样类型的指针转换变得合法,若是代码中存在不合理的转换,编译器也不会报错。
所以,void指针能不用则不用,后面讲函数的时候,还能够解锁更多新的玩法。
在C语言中,若是一个指针不指向任何数据,那么就称之为「空指针」,用「NULL」来表示。NULL实际上是一个宏定义:
#define NULL ((void *)0)在大部分的操做系统中,地址0一般是一个「不被使用」的地址,因此若是一个指针指向NULL,就意味着不指向任何东西。为何一个指针要指向NULL呢?
其实这反而是一种比较指的推荐的「编程风格」——当你暂时还不知道该指向哪儿的时候,就让它指向NULL,之后不会有太多的麻烦,好比:
//Example 11第一个指针未被初始化。在有的编译器里面,这样未初始化的变量就会被赋予「随机值」。这样指针被称为「迷途指针」,「野指针」或者「悬空指针」。若是后面的代码对这类指针解引用,而这个地址又恰好是合法的话,那么就会产生莫名其妙的结果,甚至致使程序的崩溃。所以养成良好的习惯,在暂时不清楚的状况下使用NULL,能够节省大量的后期调试的时间。
开始套娃了。其实只要你理解了指针的概念,也就没什么大不了的。
//Example 12程序结果以下:
//Consequence 12固然你也能够无限地套娃,一直指下去。不过这样会让代码可读性变得「不好」,过段时间可能你本身都看不懂你写的代码了。
那么,指向指针的指针有什么用呢?
它可不是为了去创造混乱代码,在一个经典的实例里面,就能够体会到它的用处:
char* Books[] = {而后咱们须要将这些书进行分类。咱们发现,其中有一本是写Python的,其余都是C语言的。这时候指向指针的指针就派上用场了。首先,咱们刚刚定义了一个指针数组,也就是说,里面的全部元素的类型「都是指针」,而数组名却又能够用指针的形式来「访问」,所以就可使用「指向指针的指针」来指向指针数组:
...由于字符串的取地址值实际上就是其「首地址」,也就是一个「指向字符指针的指针」,因此能够这样赋值。
这样,咱们就利用指向指针的指针完成了对书籍的分类,这样既避免了浪费多余的内存,并且当其中的书名要修改,只须要改一次便可,代码的灵活性和安全性都获得了提高。
常量,在咱们目前的认知里面,应该是这样的:
520, 'a'或者是这样的:
#define MAX 1000常量和变量最大的区别,就是前者「不可以被修改」,后者能够。那么在C语言中,能够将变量变成像具备常量同样的特性,利用const便可。
const int max = 1000;在const关键字的做用下,变量就会「失去」原本具备的可修改的特性,变成“只读”的属性。
强大的指针固然也是能够指向被const修饰过的变量,但这就意味着「不能经过」指针来修改它所引用的值。总结一下,就是如下4点:
❝❞
- 指针能够修改成指向不一样的变量
- 指针能够修改成指向不一样的常量
- 能够经过解引用来读取指针指向的数据
- 不能够经过解引用来修改指针指向的数据
指针自己做为一种「变量」,也是能够修改的。所以,指针也是能够被const修饰的,只不过位置稍稍「发生了点变化」:
...这样的指针有以下的特性:
❝❞
- 指针自身不可以被修改
- 指针指向的值能够被修改
在定义普通变量的时候也用const修饰,就获得了这样的指针。不过因为限制太多,通常不多用到:
...
免责声明:本文系网络转载,版权归原做者全部。若有问题,请联系咱们,谢谢!
推荐阅读