【C语言学习笔记】四、指针

(七)指针

指针是C语言的精髓部分,程序员对指针的掌握程度直接决定了其编程能力。

1、内存是如何存储、读取数据的?
之前讲变量的时候提到:如果我们在程序中定义一个变量,那么程序在进行编译的时候,系统就会根据变量的类型,在系统中分配对应长度的空间来存储变量的内容。而我们访问变量的时候是通过变量名来访问的。通过变量名来访问内存是一种相对来说比较安全的方式,因为只有你定义了这个变量你才能访问它,也就是访问内存。你没定义变量,你就访问不到对应的内存空间。

内存的最小索引单元是一个字节。我们可以把一个字节抽象成一个房间,一个房间可以住8个人,把整个内存抽象成一个大楼。所以,大楼就是内存、每个房间就是一个字节、一个字节里面可以放8个01。
我们还规定每个房间都有一个房间号,类比就是每一个内存单元都有一个内存地址。

假如我们现在在大楼里面放一个整型变量,我们规定整型要用4个字节去存放,所以我们就的找4个房间放32个01去存我们这个整型变量。此时程序员只需要敲代码:int i=123;就表示程序员想存一个变量名叫i的变量,变量的值是123的整数。那编译器就把123这个十进制整数转化成二进制01,再转化为补码、再把第一位设置成signed,至此人类认识的这个123数字,就变成32个0101,然后编译器把这32个0101放到房间编号为10005号到10008号这4个房间即可。至此内存就帮我们存上123这个数字了。
当程序员调用变量i的时候,编译器一看i是整型,是从10005号内存地址开始存储的,那编译器从10005号开始取出它及它后面3个地址空间里面的共32个0101,然后补码变二进制码,判断符号位,再转十进制,等一步步还原返回给程序员,程序员就知道了变量i是123呀。

在这个过程中,变量名,在内存中是完全没有必要存放的,因为变量名是为了方便程序员的使用而定义的。而且变量名只需要程序员和编译器知道这个名字即可,而且编译器还知道变量名对应的内容存放的地址,所以编译器在编译变量的时候,根本不用存变量的名字,只要存住变量值的第一个内存地址以及变量的数据类型即可,用户调用变量的时候,编译器根据第一位内存地址+数据的类型,给用户读对应的数据即可。

比如用户int i=123;编译器编译的时候就把123这个整型数据存放到加入10005号内存地址里面。由于整型数据是要分配4个字节存储的,所以编译器就把i的值123这个整数存放到编号为10005-10008这4个存储单元里。编译器只要知道i的地址是10005,并且i是个整型类型即可。当用户访问变量i的时候,编译器就根据变量的第一个地址10005和变量的类型,返回内存编号为10005-10008这4个存储单元的01010即可。

2、什么是指针和指针变量

指针就是地址的意思,C语言中有专门的指针变量,用来存放指针。
和普通变量不同,指针变量存放的是一个地址,普通变量存放的是数据。
指针变量也有类型:指针变量的类型是指它存放的地址指向的数据的类型。

A:编译器就知道指针变量pa所在的地址是11000,而一个指针变量在编译系统中是占4个字节,所以,当用户要读取 指针变量pa的值时,编译系统就去 11000的内存单元,然后依次往下找4个字节,也就是编译系统就把内存单元是11000-11003里面的32个0101先取出来,然后解析出这32个0101就是10000这个内存地址,而且编译器也知道10000这个内存地址里面存放的是普通字符变量a的值:F。然后编译器就去内存地址是10000的内存单元中取8个0101(为啥取8个,是因为编译器知道这个地址存的是变量a的值,而变量a是一个字符类型的,所以只要取8位即可),然后再解析这8个0101后,就是字符大F,然后把大F返回给用户。

B:同理,pf也是一个指针变量。通过A的解释,我们知道,不管是普通变量还是指针变量,变量名都是不用存储的,变量名直接映射的是一个内存地址,普通变量映射的地址里面的内容就直接是这个普通变量的值。而指针变量映射的地址里面的内容不是指针变量值而是一个地址,这个地址里面存的数据才是指针变量存放的值。

所以:
上图中的10000地址和10005地址,我们称为指针。
而存放10000地址和10005地址的变量pa、pf,我们称之为指针变量。

3、如何定义指针

定义指针变量和定义普通变量差别不大,仅仅是中间多了一个 * 号

上图中的char表示:指针变量pa中存放的地址指向的内存单元中存放的数据的数据类型。同理int。
比如,上上图的指针变量pa中存放的地址就是变量a的地址,变量a又是一个字符F,所以上上图的指针变量pa定义的时候,就得定义成上图中的:char *pa;

注意:定义指针变量的时候,前面的数据类型一定要弄清楚,因为不同数据类型所占用的内存空间不同,如果你把指针变量的类型定义错误了,那么你在访问这个指针变量的时候,你取的数据长度就会出错,数据长度都搞错了,能解析出正确的值嘛?!

4、取址运算符 和 取值运算符

  • 如果要获取某个变量的地址,可以使用取址运算符:&
    比如:char *pa = &a; 就表示把普通字符变量 a 的地址 赋值给指针变量pa。
    再比如:int *pb = &f; 就表示把普通整型变量 f 的地址 赋值给指针变量pb。
  • 如果要访问指针变量指向的数据,可以使用取值运算符:*
    比如: printf("%c, %d\n", *pa, *pb);
    说明: 虽然这里的取值用的是 * 号,前面定义指针变量的时候也是用 * 号,二者虽符号一样,但意义不一样!

直接通过变量名访问变量值,我们称为直接访问。比如定义一个普通变量int a=3; 我们可以通过这个普通变量名a直接访问a对应的变量值3。
但是如果我定义一个指针变量 int *a = 3; 那此时我们访问变量a的值时,就是 间接访问,因为我们是先访问存放3的内存地址,取出3后,再访问内存地址是3的内存里存放的数据,然后返回这个数据。

5、小结
代码示例上面的知识点:如何定义指针变量、指针变量的值是什么、查看指针变量存的地址、查看指针变量的值的地址、什么是取值和取址等概念

6、切记:要避免访问未初始化的指针

上面的代码就是非常危险的代码!A处就没有给指针a初始化,就是没给指针a赋值,那这个指针就是一个野指针,就是我们也不知道这个指针指向内存中的哪个地址。它的指向是随机分配的,因为a是一个局部变量,所以a在栈里面的值是随机分配的!但是这种随机分配地址的行为又是合法的!就是不报错!那我们很容易的、想当然的在B处,对一个合法的位置进行赋值!但是这种做法是非常危险,因为它可能会覆盖到系统的一些关键代码。所以上面的代码虽然逻辑合理合法,但会让你得到意想不到的崩溃!

7、赋值示例1:通过指针变量改变普通变量的值

这种方法简单粗暴,直接改值!

8、赋值示例2:从scanf函数角度,理解指针变量如何改变普通变量的值
scanf函数的功能是:从标准输入流中读取一个数据到一个变量中。scanf函数的第一个参数表示你要读的标准输入流是什么类型的数据;第二个参数是你要把这个标准输入数据存放到哪个内存地址里面。
所以,之前我们用scanf函数的时候,第二个参数我们都是用的前面定义的变量名前面加个&取址操作符。

现在,我们再来深入理解一下第二个参数:我们知道之前我们定义的普通变量,其变量名其实是不存储的,都是编译系统直接把变量名对应到变量值所在的内存的内存地址的。所以我们这里&变量名其实就是给scanf函数传入的是一个地址信息!

所以,现在我们又学了指针变量,那我想用指针变量指向的地址,当作scanf的第二个参数传给scanf,那scanf接受的标准输入流就写入了指针变量指向的地址中。这样我们就借助scanf函数,将用户的输入存放到指针变量指向的普通变量了。下面是代码演示:

9、指针和数组
数组不是指针,指针也不是数组。但二者经常纠缠在一起,所以有必要再细说一下二者。下面几个案例都是从不同的角度再去深入理解指针和数组。

(1)用scanf函数给数组赋值,从地址的角度再次理解一下数组

可见,数组名同变量名一样,都是一个地址信息。并且数组名还是数组第一个元素的地址信息。并且数组里面的元素的位置都是依次紧挨排列的。

但是注意:数组名前面不需要加&就是表示是一个地址。而变量名前面要加&才是一个地址。而且,数组中每个变量的地址也是要加&才能取到地址的。因为数组里面的元素也是变量嘛。

小结一下变量和数组:变量名是不需要在内存中存储的,内存中存储的是变量值。同样数组名也是不用存放在内存中的,但是数组名有值!值不是数组里存的信息,而是第一元素的地址,所以数组第一个元素的地址信息是要存放在内存中的,并且只有编译器自己知道这个地址对应的就是这个数组名。所以数组名对应的值——第一个元素的地址,是以指针变量的形式存放在内存中的,因为存的是一个地址嘛,所以肯定是一个指针变量喽。

所以,调用数组名直接就返回的是其第一个元素的地址,而无需在数组名前面加&符号,如果你加了&符号,就错了!就是存放第一个元素地址的地址了!
所以,你想通过调用数组名来查看数组的初始化值,那你是天方夜谭,你只能调出一个地址信息。如果你想调用&数组名,那你调出的就是一个地址的地址。
所以,你想查看一个数组里某个元素的存储位置,你得&数组名[下标]获取,因为数组中的元素就&。

(2)指向数组的指针
前面学的基本都是指针变量指向的是一个普通变量,那指针变量如何指向一个数组呢?

  • 定义指向数组的指针
    将指针变量存储的地址改成数组名或者数组第一个元素的地址就可以了:
    char shuzu[]; 先定义一个存储字符串的数组shuzu
    char *zhizhen; 再定义个指针变量zhizhen
    zhizhen = shuzu; 或者 zhizhen = &shuzu[0] 把指针变量里面存放的地址改成数组名的值或者数组第一个元素的地址

我们创建指向数组的指针干嘛呢?是不是根据数组元素的长度,就可以操作数组了呀,也就是指针的运算。

  • 指针的运算代码示例:

之前我们访问数组都是用下标访问的,这里我们用的是指针进行访问的。这种用指针进行间接访问数组的方法叫指针法。

注意:这里的*(p+2)、*(p+3)、*(p+4)、*(p+5) 并不是在p表示的地址上,把地址号加2、加3、加4、加5,而是指向数组的下一个元素。这里的例子用的数组是char类型,里面的元素都是一个字节的空间分配,所以巧了。如果是一个整型的数组,每个元素都是4个字节,不是p+4的倍数那么复杂,还是p+1这样依次往下加,就是依次指向下一个元素。

从这个例子中也可以看到,对于整型数组,是不能用%d,a 把整型数组整体打印出来的。访问整型数组只能一个个元素的访问,不能整体访问!但是字符串数组,可以用%s,a打印出来。

从这个例子中还可以看到,既然数组名本身就是一个地址信息,那我干嘛要多此一举,还要定义一个指针变量,再用指针变量取数组的元素?!我都知道地址了,为啥不可直接读:

  • 用数组名直接读数组中的元素

10、指针和常量
我们还可以用指针指向一个常量。下面例子是用指针直接定义一个字符串,然后用下标法读取字符串的每个元素: 

A:之前我们给指针变量初始化的时候都是赋值一个地址给指针变量,并且基本都是定义加赋值一气呵成,没有出现过这里的写法。首先这种写法是没问题的。这种写法可以拆解开:首先等号右边的字符串是双引号引住的,这说明这个字符串是一个常量字符串,编译器在编译的时候就会找一块连续的内存空间去存放,这里的应该是存放了18个字节的空间(最后有个\0,但是strlen的时候字符是17个)。然后再把第一个字符n存放的地址赋值给指针变量zhizhen。所以,这里的指针变量里面还是存的是一个地址,而且这个地址指向的是常量"nihao,liyuanyuan"存放的地址。所以这种写法没问题。而且这种写法等价于:
char *zhizhen;
zhizhen = "nihao, liyuanyuan";

B:因为字符串是个常量,所以读取的时候直接%s zhizhen 这种直接常量名就可以读取了。同理计算字符长度也是strlen(指针名)就可以了。但是sizeof的时候就不是sizeof常量占内存空间的大小,而是这个指针占了多大的内存空间。我个人的电脑,对指针变量存放的内存空间设置的是8个字节,这个大小是因电脑而异的。所以我的sizeof返回的是8。那字符串常量如何知道size呢,就是C的写法,直接sizeof这个字符串,返回的就是17个字符加一个空字符,就是18个字节。

D:第一个是打印指针变量自己的存放地址,是一个16进制的地址。第二个是打印指针变量里面存放的地址,这个地址不像是内存地址啊?!是的,还是A中说的,在c语言中,字符串数组和常量字符串在内存中的存储区域是不一样的。字符数组存储在全局数据区或栈区,而字符串常量是存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。第三个打印的是指针变量里面存的地址里的数据,就是一个n。最后一个打印的是常量串存在哪里,打印出来就是指针变量里面存放的地址,一模一样,对的。

E:这个循环是展示,如何用下标法读取常量字符串中每个元素的。

至此,一个指针指向一个常量、一个变量、一个数组,我们都涉及到了,指针和它们之间的各种书写语法真是一不留神就会出错,所以背后的逻辑一定要清晰。

11、案例:手写一个计算字符串的长度的程序

我们用while循环计算字符串长度的思路是没问题的。
我们先把字符串放到一个字符串数组str里也是没问题的。
*str++的意思是:对变量str这个操作数进行++自增操作和*取值操作,而自增操作的优先级是1,取值操作的优先级是2,所以要对str先进行自增再取值。但是这报错了,报错的意思是:自增操作数必须是左值。显然这里的str是一个字符串数组名,是一个地址信息,名字不能变来变去,所以这个地址也不能变来变去,所以str是个右值,右值是不能变的,所以无法自增。

这个程序我们要加个指针变量指向字符串数组,这样写:

都是名字,为啥str是个右值,flag就是左值了呢?因为,str是个数组名,数组名就是一个右值,右值不能变,所以无法自增!但是,flag是个指针变量,指针变量名同样也不能自增(因为它也是一个地址,是一个右值),flag就表示指针变量值,敲flag读取的就是变量值,虽然这个变量值也是一个地址信息,所以flag是左值非右值!而且*flag就取出了这个地址信息对应的地址里面存放的数据,那就可以进行!=逻辑判断了,就是可以while循环了,程序就正常跑了。
如果有的人被绕晕了,那你就记住结论:数组名、变量名是一个地址,是右值。而指针名是一个左值。&指针名才是一个右值,不能变。

另外,要再说明一下flag++的意思!我们知道i++和++i的区别,i++是先赋值再自增,++i是自增再赋值。所以,同理,第一轮while循环的时候,是先把flag里面存的第一个地址(就是str的地址,或者说是str的第一个元素的地址)取值后再加1。所以第一轮循环判断的就是字符串数组里面的第一个字符n,第二轮循环时,flag变量里面存的地址信息已经+1了,同理,还是先取值再加1,那取得值就是第二个字符i。

12、指针数组 和 数组指针
(1)定义

  • 指针数组
    取值运算符*和数组下标运算符[]同时作用于p1。而[]的优先级是1,*的优先级是2。所以,p1先被定义为有5个元素的数组,然后是指向整型变量的指针。所以:指针数组是一个数组,每个数组元素存放一个指针变量。

  • 数组指针
    小括号和方括号的优先级都是1,那就从左到右排,所以p2先被定义成为一个指针变量,然后这个指针指向[5],[5]表示有5个元素的数组。而指针的类型就是它指向对象的类型,所以int也是数组[5]的5个元素的数据类型。所以:数组指针是一个指针,但它不同于我们前面定义的普通指向数组的指针, 它是指向一个数组的数组名(一个数组的数组名前面强调过就是一个存放第一个元素地址的指针变量)的指针。或者说是一个指向数组首元素地址的指针,就是一个指针的指针。

(2)初始化指针数组
案例1:每个元素初始化为指向一个变量

案例2:每个元素初始化指向一个字符串常量

(3)初始化数组指针
由于数组指针还是一个指针,所以初始化的时候,还是要给赋值成地址才对。而且赋的地址是:数组名的值的地址,也就是一个存放数组首元素地址的指针的地址。代码示例如下:

A:数组指针变量p里面的值是一个地址,并且这个地址是数组a首元素的地址。
B:是数组指针变量p自己在内存中的地址。
C:p里面是一个地址,*p就是解析出这个地址里的数据,这个数据就是数组名的值,也就是数组首元素的地址。也所以**p就是1
D:同理*p+1就是素组第二个元素的地址,所以*(*p+1)就是2了。

至此,大部分还是清晰的,只有一点点模糊,就是数组名,到底数组名是又开启一个新空间存储首元素地址呢,还是首元素地址本身就有两重意义:不仅要表示数组名还要表示首元素的值。显然不可能同时表示呀,那就可能是编译器又拿自己的小本本记笔记,只有编译器自己知道了。

13、小结

估计到这里大家都已经懵了,那就总结一下,先总结定义再总结它们之间错综复杂的关系!

首先还得再声明一下什么是内存:内存就是一个一个内存单元,类比大楼,里面都是一个个房间,房间就是内存单元。内存是用来存储数据的,就好比大楼是用来存人或者存东西的,一个房间存8个人或者8人的房间也可以存东西。那既然是存数据,就涉及到存在哪儿?存的是什么数据?就好比人或者东西是存在几号房间或者是几号到几号房间?存的是人还是东西。所以有地址和值的概念,地址就是房间号,值就是房间里是张三还是李四还是就一张纸条,纸条上写着你找的在几号几号房间。

(一)定义
(1)常量:
比如"lyy"就是一个常量,编译器直接将lyy编译成一个地址,也就是一个房间号。房间里存放的0101内容解码后就是lyy。而且常量放在常量区,只读不能写。所以一读某个地址直接返回的就是内容lyy。

(2)变量:
比如 int a=5, 或者char b ='A'
编译器就要把变量a的值5存起来。做法是:编译器在变量区随机选4个字节的空间,把5这个数用32个0101表示出来,然后写进去,同时把第一个字节(房间)的地址(房间号)自己做个小本本记录下来,a和这个房间号是对应的即可。 变量b同理,只不过存A的时候只需要1个字节的空间,也就是要一个房间即可。 所以你可以这样理解,变量名a、b本身就是一个地址。所以:

  • 当用户调用他定义的变量的时候:用户%d,a 或者 %c,b 编译器编译器就直接返回5 或者A。
  • 当用户想知道变量存在哪个房间的时候:用户%p,&a 或者 %p,&b 编译器就返回5存放的地址或者A存放的地址。

(3)数组:
数组是多个类型相同的变量。所以数组是多个变量一个挨一个放在一起。 比如int a[] 比如char str[] 比如还有二维数组int a[][] 就是数组。不管一维数组还是二维数组在内存中都是线性存放的,就是也是一块连续的空间存放的,也就是一个挨一个放的。
同理,编译器存数组里面的元素的时候也是一个挨一个存放的,然后把第一个元素的房间号赋值给数组名a、str,并且在内存中把这个房间号存储起来,然后用小本本记录下存储房间号的内存地址和数组名对应,并且这个对应关系只有编译器自己清楚。所以数组名本身也是不用存储的,但是数组名有值,值是它第一个元素的地址,并且这个地址是被存储在内存中的。所以我们调用数组名a、str就返回的是首元素的地址信息。同理数组中的每个元素比如a[1]或者str[2]也是一个地址。
所以:

  • 当用户a[1]=6的时候就是把6这个整型数字写入a[1]地址里面。同理,str[0]='a'就把a这个字符写入str[0]这个地址里面了。
  • 当用户调用数组的某个元素时:用户%d,a[1] 就读出里面的6了。同理,%c,str[0]就读出a了。
  • 当用户想知道数组某个元素存放在哪个房间的时候:用户%p &a[1] 编译器一看小本本就告诉用户这个数组的第二个元素的存放地址了。同理,%p &str[0]就是数组str第一个元素的存放地址。
  • 当用户想知道这个数组放在哪里的时候:%p,a 或者%p,str就看到数组a或者数组str的第一个元素的内存地址,也就是告诉用户你定义的数组是从这个地址开始存的。这里一定要注意!变量名取址的时候要加&符号,但数组名取址的时候不用加&。原因是数组名的值就是一个地址。
  • 数字数组不能整体查看,只能下标法或者循环一个个元素查看!而字符串数组特殊,不仅可以用%s str整体查看,还可以strlen(str)计算里面有几个字符。但前提是你要进入string.h头文件。

(4)指针:
指针就是在内存中选一个房间(有的机器是选2个房间)存放一个纸条,纸条上写另外一个房间的房间号。所以指针自己是有存放地址的,而且指针存的内容是一个地址。指针和变量类比,变量存的是值,指针存的是地址。

  • 定义一个指针:int *pa; 或者 char *pb; 那么变量pa和变量pb就是两个指针变量。同理,pa和pb本身是不用在内存中存储的,内存中存储的是pa和pb的值————一个地址,类似变量名。就类似房间里存的是一张纸条,纸条上写着另外一个房间的房间号。
  • 给指针变量赋值:就是放入一张纸条,纸条上写上另外一个房间的房间号。所以给指针变量赋值要赋一个地址。
  • 用户想看看指针变量pa里面存的纸条上写的啥?%p,pa就返回纸条上地址。
  • 用户想知道指针变量pa存放在哪里?%p,&pa就返回变量pa的存放地址。
  • 用户想知道指针变量pa里面的小纸条上的房间里存的是啥?%d,*pa或者%c,*pb就返回了纸条上地址里的房间里存的东西了。

至此,常量、变量、数组、指针的基本概念我们都搞清楚了,那现在看看它们之间的纠缠:

(二)常量、变量、数组、指针之间的关系
(1)指针指向一个常量
当指针的房间里的小纸条上写的地址是一个常量的地址,就是这个指针指向了一个常量。
char *p="balabala";就是指针变量p指向一个常量"balabala"。也可以写成:char *p; p="balabala"
此时:

  • %s,p就打印出balabala,还可以strlen(p)就知道balabala里面有几个字符了。但是sizeof(p)返回的是指针p占的空间不是balabala占的空间。要明白变量p里面存的还是地址不是balabala,仅仅存的是字符b的房间号。就是p的房间里也是一张小纸条,纸条上写着字符b的房间号。
  • %p,&p打印的是指针变量p在内存中的存储位置。
  • %p,p打印的是指针变量p房间里小纸条上写的地址。
  • %p,"balabala"打印的是常量balabala第一个字符b在内存常量区的地址
  • %c,*p就打印出b。

(2)指针指向一个变量
当指针的房间里的小纸条上写的地址是一个变量的地址,就是这个指针指向了一个变量。
int a=123; int *pa=&a; 就是指针pa指向变量a。
char str='z'; char *pstr=&str;就是指针pstr指向变量str。
此时:

  • %d, a ;%c,str 自然得到就是123 ;z
  • %p, &a; %p,&str 得到的就是变量a和变量str的值的存放地址。
  • %p, pa ;%p,pstr 得到就是指针变量房间里纸条上的房间号
  • %p, &pa ;%p,&pstr 得到就是指针变量pa和pstr自己房间的房间号
  • %d, *pa ;%c,*pstr 得到的就是pa和pstr房间里纸条上的房间里的内容。

(3)指针指向一个数组
当指针的房间里的小纸条上写的地址是一个数组的地址,就是这个指针指向了一个数组。
int a[]; int *pa=a; 就是指针pa指向变量a。
char str[]; char *pstr=str;就是指针pstr指向变量str。
此时:

  • %s,str 可以得到字符串数组str里面存的字符串。但是%d,a可是看不到整型数组a里面的所有元素,并且第一元素也看不到,返回的是一个乱值!

  • %p,str 和%p,a可以得到数组str和数组a第一个元素的存放地址。所以等价于:%p,&a[0]、%p,&str[0] ,并且还等价于:%p,pa、%p,pstr。
    %p,pa ;%p,pstr 得到的是指针变量房间里纸条上的房间号,也就是指向的数组的第一个元素的地址。
    所以:要打印一个数组的地址有3种方法:可以用数组名打印、可以用素组第一个元素打印、也可以用指向这个素组的指针打印。

  • %p, &pa ;%p,&pstr 得到就是指针变量pa和pstr自己房间的房间号

  • %d, a[0] ;%c,str[0] 得到的是数组a和数组str的第一个元素的值。
    %d, a[1] ;%c,str[1] 得到的是数组a和数组str的第二个元素的值。
    %d,*pa ;%c,*pstr 得到的就是pa和pstr房间里纸条上的房间里的数组的第一个元素的值。等同于:%d, a[0] ;%c,str[0]
    %d,*(pa+1) ;%c,*(pstr+1) 得到的就是pa和pstr房间里纸条上的房间里的数组的第一个元素的值。等同于:%d, a[1] ;%c,str[1]
    所以:要大于一个数组的某个元素的值有2种方法:可以用数组下标法打印,也可以用指针打印。

至此,它们之间的纠缠也讲清楚了。总之就是一定理解透定义,定义透了,纠缠就明白了,纠缠明白了一些变异(比如指针数组、数组指针)就懂了,那你写代码的时候就游刃有余了。

14、指针和二维数组
从这里开始,我们再继续悟悟数组名。 

从这里,我们终于揭开数组名的真面目!先说结论:
二维数组名b是b[0][0]的地址,也就是数组名也是数组首元素的地址。但是不是简单的变量值是地址、或者是普通指针指向一个地址,而是:二维数组名b是一个指向“一个包含了5个元素的数组”的数组指针。
代码示例一下这个说法:

可见:
(1)array的值就是数组首元素的地址,array+1的值是数组[1][0]元素的地址。

(2)array和array+1两个地址相差14,而地址是16进制,相差14就是十进制中的相差20。而相差20个字符正是数组一行5个元素占的内存空间。

(3)所以:一维数组名是一个存储首元素地址的普通指针。二维数组名是一个数组指针。

  • 所以你可以:*array=&array[0][0], *(array+1)=&array[1][0], **array=array[0][0], **(array+1)=array[1][0], *(*array+1)=array[0][1],*(array+1)+3=&array[1][3],*(*(array+1)+3)=array[1][3]等等依此类推即可。

  • 所以我们还可以这样做:当我们需要对一个数组指针初始化的时候,我们可以把一个二维数组直接赋值给这个数组指针即可。

(4)这里用地址取值就叫解引用 。这里的对数组下标法取值就叫语法糖。
语法糖(Syntactic sugar)是由 Peter J. Landin(和图灵一样的天才人物,是他最先发现了 Lambda 演算,由此而创立了函数式编程)创造的一个词语,它意指那些没有给计算机语言添加新功能,而只是对人类来说更“甜蜜”的语法。 语法糖往往给程序员提供了更实用的编码方式,有益于更好的编码风格,更易读。不过其并没有给语言添加什么新东西。 比如,在C语言里用a[n]表示*(a+n),用a[n][m] 表示*(*(a+n)+m),这就是语法糖的应用,因为在内部,编译器会自动将a[n] 转换为*(a+n) 的形式实现。

(5)至此,一个数组我们要取其某个元素的值,就有了3种方法:
一是数组名[]这种语法糖,最简单。
二是解引用数组名,就是在数组名前*,因为数组名本身就是一个地址嘛,通过地址取值嘛。
三是把数组赋值给一个数组指针,用指针取值。

15、指针和多维数组
二维自然的就可以推出多维,这里直接展示结论:

16、案例:数组指针 和 二维数组

可见:数组指针后面的数组4表示的是这个指针一步的跨度,一步跨4个数据。所以也是为什么前面说的,在定义数组时,数组的行可以省略而列不能省略,列的个数就是跨度的宽度。

17、void指针
void类型就是无类型的意思。 我们定义变量的时候,系统就是根据变量的类型来决定给变量分配多少内存的,所以定义变量的时候是必须要声明变量的类型的,因为类型背后的意思就是给你多大空间让你存储的意思。

那现在这个void无类型能干啥?它可以用来定义一个无类型的指针:void指针。
void指针又称通用指针,就是可以指向任意类型的数据。而且,任何类型的指针都可以赋值给void指针。

我们之前定义指针的时候,不管是普通指针、还是指针数组、还是数组指针,我们都得提前声明好这个指针指向的地方的值的类型,但现在void指针就不需要提前声明类型了,而且还可以通过其他有数据类型的指针的赋值,void指针就可以指向任何数据类型的数据。

A:我们可以把其他数据类型的指针赋值给一个void类型的指针,这样这个void指针就可以指向任何数据类型的数据了。
B:一个void指针是不能对其指向的数据进行指针解引用取值的,因为无数据类型的指针不知道数据类型嘛,就是不知道一次要读几个字节,就是不知道数据宽度,所以就无法解引用!但是我们可以把void指针强制转化成一个有数据类型的指针,此时这个指针就可以接引用了。
C:这里是常量,常量比较特殊,常量直接取的就是值。

18、NULL指针
空指针,就是不指向任何数据的指针。
#define NULL ((viod *)0)
这是一个宏定义,定义一个NULL,值是0,并且这个0地址是一个指针。在大部分的操作系统种,地址0通常是一个不被使用的地址,所以如果一个指针指向NULL,就是指向0地址,就认为这个指针不指向任何东西。
当你还不清楚要将指针初始化成哪个地址的时候,你就把它初始化成NULL。此时在对指针进行解引用的时候,先检查给指针是否是NULL。在编写大型程序时,这样做可以节省大量的调试时间。

注意:
NULL不是NUL。NULL用于指针和对象,表示控制,指向一个不被使用的地址。NUL也是'\0',表示字符串的结尾。

19、指向指针的指针

同理,我们可以定义一个指向指向指针的指针的指针:***p ,但是不建议这样用,因为别人看你的代码会骂你。

20、指针数组 和 指向指针的指针
下面的案例涉及到指针数组的定义、指向指针的指针、指向指针的指针数组 三个概念:

这样设计的好处是:一是避免重复分配内存;二是,修改书名的时候,只要在变量books中修改即可。

21、再品 常量和指针
第10点我们已经涉及到常量和指针了,这里再系统性的总结一下:

(1)常量的样子:520,'a',3.14 或者 宏定义 #define PI 3.14
变量的值可以被改变,而常量不可以。因为变量有一个标准其位置的名称,我们可以通过名称改变变量的值。而常量名就是值,值就是名,初始化存的是啥就是啥,不可改变。

(2)用const关键字可以将变量改成常量
const int a = 520;
const float pi = 3.14;
就是把变量改成只读,不可修改。

(3)指向常量的指针

小结:指针本身是一个变量,所以可以修改让它指向不同的地址。但是不能通过解引用指针修改常量的值!可以通过解引用指针修改变量的值!
那想让指针本身也不能改变,就是指针变量的值也不能改变,那就是:常量指针。

22、常量指针

小结:常量指针如果指向的是一个非常量,常量指针指向的值可以被改变,但常量指针自身的值(指向的地址)不可以被修改。

23、指向常量的常量指针
指针指向的值不可以被改变。指针自身的值(指向的地址)也不可以被修改。 

24、指向“指向常量的常量指针”的指针
套娃无止境!
const int num = 520;
const int * const p = #
const int * const *pp = &p;
上面变量pp就是一个指向“指向常量的常量指针”的指针!

  • 40
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值