1. 指针到底是什么?指针其实也就是地址,是内存中最小单元的内存编号
2. 学习指针必须得了解清楚内存,而内存到底是什么东西呢?内存就是电脑上的存储设备(除了内存之外,还有硬盘,寄存器等等),那内存到底是来干啥的呢?程序运行的时候会载入到内存当中,也会占用一些内存空间,那既然程序会使用内存,那么这个内存到底该怎么去管理它呢?
3. 就像现实生活当中有许多房子,那该怎么去管理房子呢?就是说把它划分成一个一个房间,这个房间是你的,那个房间是他的,这样子大家居住起来了。而实际上在内存里面也一样,我们把内存里面划分成一个一个小的格子(好比内存就是房子,然后在房子里面划分一个一个房间,道理一样的)
4. 我们把一个小格子叫做一个内存单元,在空间划分实践中,把一个内存单元的大小规定为一个字节。然后把每一个这样的内存单元依次编号(好比房间的门牌号),这样子的话,未来如果我要在内存中找一个内存单元,我只需要通过这个编号找就可以了。
5. 这个编号就叫做内存单元编号,也就是地址,地址在c语言当中拥有一个叫法,叫指针。因此指针,内存单元编号,地址这三个概念完全等价。
6. 内存确实被你划分成了一个一个单元,那每一个单元的大小是多大呢?是一个字节。那你之前说对内存单元依次进行编号,那么问题就来了,这个内存单元的编号是怎么去编的呢?
7. 对于32的机器,这台物理机器里面有32根地址线,每根地址线通电后,在寻址的时候会产生高电平和低电平,把电信号转化为数字信号后,也就是0和1。(这也就解释了为什么计算机这么一个硬件只能识别二进制,因为通电后,只会产生高电平和低电平,转化为数字信号只有0和1)。
8. 当这32根物理电线给它通电之后,每根上面要么就是高电平,要么就是低电平,转化为数字信号出来的无非就是0和1,既然有32根地址线,每根都是如此,那么这样就会产生2的32次方的32位二进制序列,这个二进制序列就可以作为编号
9. 因此这样子的话,每个内存单元都有一个编号与它对应起来,刚才我们已经得知,我能够产生2的32次方这么多的地址,一个地址管理一个内存单元(每个内存单元占一个字节),那么2的32次方个地址也就是说能管理2的32次方个内存单元,然后一个内存单元大小是一个字节,那也就是说能够管理2的32次方字节的内存空间,那这到底是多大的一个内存空间呢?这边必须得明白一下各个单位之间的关系,最小的是比特bit,在往上面是字节byte,然后是KB,然后是MB,然后是GB,然后是TB等等,一个比特位相当于一个二进制位,一个字节对应八个比特位,然后接下来都是1024换算。因此在32位机器上,最多也只能访问4GB的内存空间。
1. 随便打个比方:C语言当中,一个整型变量的大小是四个字节,一旦创建一个整型变量a,就需要在内存当中腾出四个内存空间分配给它。&a,表示对a取地址,可是a占了四个内存空间,每一个内存空间都有它自的地址/编号,然后对a取地址到底是取出哪个字节的地址呢?其实取出的是其所占的第一个内存空间的地址(较小的地址),只要知道起始地址,后面就是顺藤摸瓜的事儿。
2,.取地址的时候取的是低地址.
1. 当我把地址取出来的时候,发现地址就是一个值,那我在想到底能不能把它存起来呢?在c语言当中可以创建一个指针变量来把地址存起来,因此指针变量存放的是地址/编号/指针的,那么它的类型到底该怎么写呢?用int*,char*这样子表示,我想这个你应该是懂的
2. 指针变量是一种变量,是专门用来存放地址的。平时口中说的指针,通常指的是指针变量,是用来存放内存地址的一种变量
3. 所以总结一下:指针就是地址,口语中说的指针通常指的是指针变量,再次得强调一下:指针变量里面存放的是变量的地址。%p就是打印指针/地址的
4. 那么如果我要把这个地址存起来,因为这个地址它有32个比特位,所以如果我要存起来的话,需要32个比特位的空间,其实就需要四个字节的空间,因此地址本身的话需要四个字节的地址来存储,所以任何一个指针变量的大小都应该是四个字节。那如果在64位机器上,一个指针变量的大小为八个字节,这样才能存放的下一个地址。
5. 因此总结一下:指针变量是用来存放地址的,地址是唯一标示一个内存单元的。指针大小在32位平台是四个字节,在64位平台是八个字节。
6. 指针变量因为是需要存放地址的,因此它也有自己的一个内存空间,它也是需要占用内存的。
7. 这个地址可以是&取出来的,但有些东西它本身也是一个地址,如数组名,字符串常量
1. 那么指针变量既然创建出来之后,该如何去使用它呢?其实就是对它进行解引用操作,在指针变量前面加上一颗*,就是对它进行解引用操作,这个*指的就是解引用操作,*pa就是通过pa找到a。
3. 当你对指针进行解引用操作的时候,事实上你要记牢: 从该地址开始往后(该地址也包括在内),它是有一个“势力范围”的,相当于就是说在这个势力范围之内,在这个范围之内的一个内存空间,我都是通过对指针解引用能一下子访问到的,那么这个“势力范围”是由谁来决定的呢?是由指针变量类型来决定的。
1. 32位机器,地址是4个字节,也就是说指针变量的大小是4个字节,那对于指针变量而言,为什么还要区分char* pa, int* p2, float* p3等等,反正大家都是四个字节,为什么还要这么去区分类型呢?为什么不能用一个通用类型的指针呢?而事实上,你会发现并没有出现过一种通用类型的指针,每一种指针类型还是有差异的,有属于它自己的类型。那么就来探讨一下,指针类型存在的意义是什么?(再次强调一下: int* pa与char* pa虽然是两个不同类型的指针变量,但由于指针变量这玩意儿里面存的就是地址,存的就是个32位的二进制序列,因此,这两个指针变量的大小都是四个字节)
2. 但是我要说的是:指针变量类型其实是有意义的,这边得再次强调一下: 指针变量里面存放的是地址,说白了存放的就是32位的二进制序列,也就是八个十六进制数。那个变量里面存放的就是这玩意儿,然后可以通过解引用操作找到地址对应的变量里面的值。
因为刚才上面我们讲过,比说创建一个整型a,就这个a本身而言,它是占有四个字节的,然后对这个整型变量a取地址,取出的是这个a所占有的四个内存单元的第一个内存单元的地址。然后接下来我就要说,为什么指针变量的类型有意义,因为指针变量里面存放的是一个地址,注意是一个地址,就这么一个。指针变量的类型的大小就决定了指针进行解引用操作的时候,一次性访问多少个字节,也就是说在解引用的时候决定了你的“势力范围是多少”如果是字符指针(char* 的指针),在对指针进行解引用操作的时候,只访问下去一个字节。如果是int* 的指针,在进行解引用操作的时候,一次性访问下去四个字节。那么道理是一模一样的,如果是float* 类型的指针,那么由于float类型是占四个字节,那么float* 类型的指针进行解引用操作的时候也是一次性访问四个字节,因此我们如果在未来想要通过指针访问多大的空间,只要用合适的指针变量的类型就可以了,所以指针类型其实是有意义的,指针变量类型大小就决定了进行解引用操作的时候,一次性可以访问多少个字节。
3.假设我需要从这个字节向后访问一个字节(该字节也包括,事实上就是访问这个字节),用char* 的指针类型最为合适,假设我需要从这个字节向后访问两个字节,用short* 最为合适
4.假设我需要用这个字节向后访问四个字节,用int* 类型指针可以,然后这时候又发现问题了,发现float* 类型的指针好像也可以啊,因为按道理来说,这个类型也是向后访问四个字节,事实上,这两个也是有区别的,如果用int* 类型的指针,编译器会把你这四个字节当中的数据当成int类型数据处理;如果用float* 类型的指针,编译器会把你这四个字节当中的数据当成float类型数据处理。
5.刚才上面一直在说的一次性访问几个字节其实也就是访问权限大小
6. 那么指针类型除了上面的这个意义,还有没有其他的意义呢?比如说我创建一个整型a,然后对a取地址,这时候我无论用什么样类型的指针来存放这个&a,打印出来的结果都是一模一样的,因为无论是什么样类型的指针,里面存放的都是变量所占内存空间的第一个内存单元的地址,但是指针类型决定了指针的步长(也就是说指针+1到底跳过几个字节,如果是字符指针+1,跳过的是一个字节;如果是整型指针+1,跳过四个字节)
7.然后有人就会问,指针类型有这2种意义,那它到底有啥用?其实用处十分明显,我就又来举一个例子,假设创建一个整型变量a,那么在内存里面就要为a开辟一个空间,大小为四个字节,如果想要对a的这四个字节一个字节一个字节访问,那就用char* pa = &a, 对于pa这个指针,如果我对它进行解引用,我现在访问的就这么一个字节(因为是char*类型指针),如果我访问了这个字节后不想再访问这个字节了,需要去访问下一个字节,那我+1是不是就可以跳过一个字节了。其实也就是说,如果你未来想要怎么样访问字节,你就得选择合适的指针类型
8.当然了,这边还有一个小细节需要注意: 因为a创建的时候是一个整型变量,因此&a取的是一个int* 类型的指针,而我如果想要用char* pa这个指针变量去存放,不能直接就等于,不然编译器会报个警告,要这样: char* pa = (char*)&a,也就是说进行强制类型转化。
9. 到现在为止必须脑子里面有这么一个观念: 指针变量是用来存放地址的,这个是关乎到内存地址,我接下来就是对这个地址进行解引用操作,这个是关乎到内存里面的值,这个东西可以通过调试监视看的一清二楚
10. 通过今天的学习就会发现,通过指针去访问内存真的是可以做到游刃有余
11. 指针什么时候用的比较多?在访问内存的时候用的很多。总结一下: 指针的不同类型,其实提供了不同的视角去观看和访问内存,比如说char*类型的指针,解引用一次访问一个字节,+1跳过一个字节;那如果是int*指针,它的视角是什么呢?也就是解引用一次访问4个字节,+1跳过4个字节;其实就是提供了不同的视角,让我们去看待内存当中的数据,我希望要什么样的方式去访问内存当中的数据。
12. 当然,上面我们一直在讲对指针进行+1,那我能不能对指针-1,-5,+3等等呢?答案是可以的,这其实就是指针的运算(指针+-整数),那这些到底怎么判断的?其实也很简单,比如说你把+3看成+1+1+1,比如说你把-5看成-1-1-1-1-1。加号的话是向高地址奔走,减号的话是向低地址奔走。当然如果说给你换个类型,你也要会,反正就是一句话:指针变量的类型要给我搞清楚了。
1.接下来我们再学一个模块: 叫做野指针,那么这个新鲜的概念到底是指什么意思呢?野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)。野指针其实是跟野狗是一样的,同样在程序当中也是非常危险的。
2. 那么这个危险的东西到底是怎么样产生的呢?有如下的原因:
1. 指针未初始化(什么叫做初始化你懂的),比如说在函数里面有这么一串代码: int* p;这个指针变量p是一个局部变量,由于没有初始化,因此里面就是随机值,这个操作就非常危险。因此未来我们在使用指针的时候,指针必须要有一个明确的指向对象。
2. 指针越界访问,比如说通过指针去访问数组的元素,结果搞着搞着就超出数组本身的长度
3. 指针指向的空间释放,这个也是非常经典的一种指针问题。(这边得补充一下,比如说如果在函数里面返回的是地址,比如:返回&a,那么函数在进行定义的时候,最开始那个前缀应该写成int*,来表明这个返回的东西的类型是指针,也就是地址,因为指针/地址/内存编号同一回事儿)。这个指的就是去用指针访问已经还给操作系统的这么一个内存空间
那么如何规避野指针呢?
1. 指针一定要进行初始化。(创建指针变量的时候就明确给它一个地址,当你如果实在不知道该给一个什么地址/指针的时候,也给它去塞一个空指针NULL,这个NULL就是用来专门初始化指针防止变成野指针的,表示我现在没有指向任何有效地址,暂时先放一个空指针NULL,在你不知道给指针变量初始化什么样的地址的时候,这个好比就是把一条野狗拴在树上,不能让它太危险去乱窜,先给你来个空指针,先赋一个值。但是同时也要清楚,把狗拴起来之后,难道就意味着不危险了吗?不尽然。同时如果一个指针是空指针,这时候进行解引用操作,程序会崩掉。因此进行任何解引用操作的时候,用if语句排除空指针才是最正确的操作)空指针NULL也是一个地址,只是不能去用了。
2.小心指针越界。
3.指针指向的空间释放,及时置NULL。(当主人不见的时候,给狗拴在一棵树上)
4.函数避免返回局部变量的地址(一旦跳出函数范围,函数里面的一切东西都会还给操作系统)
5.指针使用之前检查有效性(就是用if语句判断一下是否为空指针,一旦是空指针,就不能去使用它)
说是这么说的,但实际上写代码的时候碰到的情况远比这个复杂,以后我们再写代码的时候,要经常锻炼自己的能力
指针其实是可以运算的,但我现在还是要把指针的运算给他它总结一下,指针到底可以进行哪些运算呢?
(尤其要注意:地址,指针,内存编号,这三个概念都是完全等价的)
1. 指针+-整数。
2.指针-指针。
3.指针的关系运算(也就是指针比较大小)
1.指针+-整数:
(之前有个知识别忘了,随着数组下标的递增,地址是由低变高)
这个已经变得很简单了。
2. 指针-指针:
这个的前提条件是两个指针要指向同一块空间(比如说同一个数组空间),并且这两个指针的类型肯定要一样,不是随便两个指针都能够进行相减操作,指针与指针相减得到的绝对值是两个指针之间的元素个数,至于正负,高减低肯定是正,这个想想就好了。
那么这个玩意到底有什么用呢?还记得之前的写一个函数求一个字符串的长度吗?
我们之前有两个方法,一个是计数器的方法(字符串其实最末尾默认带有一个/0,求字符串的长度其实是在统计/0之前出现的字符的个数),另一个是用递归的方法。同时我在这边还得要强调一下:目前为止,就把字符串等价与字符数组。
首先,我先来演示计数器方法
不使用计数器,用指针相减
用递归
那什么是指针的关系运算呢?说白了就是指针之间比较大小,相等还是不相等。
接下来我们要讲指针与数组,那么我们先回忆一下指针与数组到底有什么关系?
1. 首先我们要知道指针和数组是两个不同的对象,指针是一种变量,是专门用来存放地址的,大小四个或八个字节,大小是固定的。而数组是一组相同类型元素的集合,是可以放多个元素的,大小是取决于元素个数和元素的类型的。
2. 他们之间有什么联系呢?数组的数组名是数组首元素的地址 ,而地址是可以放在指针变量里面的,一个数组的起始地址放到指针变量里面之后,可以通过指针去访问数组。这是他们之间的联系,利用指针可以去遍历数组。
1. 打印/遍历数组内容
2. 将数组内容全部初始化为0
在代码里面,arr[ i ]与*(arr+i)这两者是完全等价的
注意: 地址/指针是不可以修改的,但是指针变量可以