那接下来呢,我们进入今天一个主要的内容, 指向二维数组的指针。 那么在开始讲这部分之前呢,我首先说明一下。那么,指向二维数组的指针啊, 对同学们来说可能是一个难点。比方说在我们这个课上, 有些同学啊,可能践足于一些参考教材,往前学了一些。 他的进度比我们课程稍微快了一点。 当他看到这个部分的时候呢,他就产生了很多的问题。于是呢,就写邮件给我,来询问这些问题。 我很理解这个状况。首先呢,这部分啊稍微有一点点的麻烦。 而且呢,我也确实看到,在很多的教材上解释得不是很清楚。 啊,所以说呢,给同学们的理解造成了麻烦。 其实啊在这一部分很关键是一个理解的问题。 Ok ,那接下来啊,在介绍指向 二维数组的指针之前,我们首先啊,先再来谈一下一维数组的 地址的问题。Ok ,大家还记得这一页 PPT 吗? 哪,在这一页 PPT 上啊,我解释了一个问题。 如果你定义了一个指针变量 p2。 它呢,指向了一个普通的整型的变量 a。 啊,因为 a 是一个整型的变量,int 型的变量,所以说, a 在内存空间中占了1,2,3,4 ,4 个字节。 这个时候呢,如果我们对指向 a 的指针变量 p2 进行 ++ 操作的话, 那么,得到的结果并不是对 p2 的这个地址进行了 +1 的操作, 而是进行了地址 + 4 的一个操作。所以经过 ++ 以后呢,p2 移到了这里。 啊,也就是说,一下跨越了 4 个字节。这是我们曾经讲过的。 总结一下的话就是说,当我们对 一个指向整型的指针变量进行 ++ 操作的话, 那么指针变量的值一下子增加了 4。 啊,这是上次课我们曾经讲过的。我们还说,这个东西呢,它体现了 基类型的作用。啊,就因为定义了这样一个基类型,所以才会出现 这样的一个结果。Ok ,那么回顾完这次讲的知识点以后啊, 我们就来看一段程序。我们来看这个程序,读一下这个程序。 想一下,这个程序的输出啊,会是什么样子的? 在这个程序里头啊,首先呢,我们定义了一个数组,int a[4] = 1,3,5,7 数值。 那么当我定义完这样的一个数组的时候啊,那么肯定在内存里头 就开辟出一片存储空间,并且呢,把这个数组放进去。 比方说,啊内存的状况是这样的,4个数组元素摆在这里。 a[0], a[1], a[2], a[3]。 这一边呢,都代表内存地址,啊这都是内存的地址。 当然,内存的地址是一个字节一个字节的来标定的。 啊,比方说这是一个字节,那么这就是由一个字节相对应的内存地址。 Ok ,那我们就来分析一下底下程序的运行过程。首先啊,我在这里打印数组名 a 的值。 毫无疑问,因为数组名是数组首元素的指针,也就是数组首元素的地址 也是数组首元素的起始地址。所以说在这儿毫无疑问会打印这个值 0028F7C4。 会打印这样一个值。这是毫无疑问的。Ok, 再往下,我们去打印 a+1 的值。 这个时候啊,可能有的同学就有不同的建议了。那 a+1 的值应该是多少呢? 到底是在 0028F7C4 的这个基础上 +1 得到 0028F7C5 呢,还是要打印一个其它的值呢 ? 要分析清楚啊,也不难。我们只要搞清楚这个 a 是什么含义就可以了。 对不对?a 的含义我们再清楚不过了。 a 是数组的名字,也就是 指向数组首元素的那个指针。数组的首元素,第一个元素是什么类型的啊? 是 int 类型的元素。我们刚刚讲过,当一个指针变量指向一个 int 型的元素,而我们对这个指针变量进行 +1 的操作的时候,它会跨过 几个字节啊?会跨过 4 个字节。 也就是说,当对 a 进行 +1 的时候,它会直接跨过 4 个字节到哪儿啊? 到这里。 到这里。也就是说, a+1 的值会打印哪一个值啊?会打印这个值。 0028F7C8 ,一下跨过了 4 个字节。 啊,我相信呢,这个,大家也不难理解。Ok, 这是这两个。 继续往下看。接下来呢,我要打印的东西叫做 &a。 &a 。有的同学在这儿可能就产生疑问了。 a 呢,它是一个常量,它不是一个变量。 那么在 a 的前面再加一个 & 的取地址符号,这个怎么解释呢? 这个程序能正确运行吗 ? 啊,可能很多同学都会有这样的疑问。 Ok, 那其实关于这个问题呢,在上一节课被更新以后的 视频中,我已经给出来解释了。那么在这儿呢,我再解释一下。 这是上次课,我们去解释数组的名字相当于指向数组第一个 元素的指针的时候,我们用的一张 PPT 。那么在这个 PPT 上呢, 我列出来了 3 个从 C 语言的规范中截起来的文字的片段。 那么,在中间的这一段,关于数组类型的表达式,比方说数组名, 它的类型的解释中,明确的指出来了这样的一个问题。 就是说,如果这个数组名啊,except when it is operand of 它,它和 & operator 。如果它不是这个 & 的操作符的一个操作数的时候, 也就是说,如果一个数组的名字 不出现在这个取地址符号的后面的时候, 那么,它的类型将被转换成 an expression with type pointer to type。 也就是说,它就会被转换成为指针类型。 而且这个指针类型是指向哪的呢?指向数组的第一个元素的。 那么这一段呢,就解释得非常的清楚了。就是说,一个数组名, 如果不出现在取地址符号后面的话,那么它呢,就相当于 指向数组首元素的指针。那么如果出现在这个取地址符号后面 会怎样呢?那么根据 C 语言的规范, 当一个数组名出现在 & 的符号以后的话, 它会返回一个指向这个数组的一个指针。 什么意思呢?根据 C 语言的规范,当你把一个有 E 的表达式写在 取地址符号后面的时候,比方说,&E, 那么,根据 C 语言的规范呢,整个这个式子 返回的将是指向这个 E 的一个指针。 那么在这个程序里头,出现在取地址符号后面的是数组的名字 a。 那么根据取地址符号的这个运算规则,在这个时候,整个这个 &a 返回的结果将是指向数组 a 的一个指针。也就是说,这个指针的类型 不再是指向数组的第一个元素的了,而是指向整个数组的 一个指针。Ok ,那么下面我们回到这个例子,再跟大家说明一下。 也就是说,原来呢我们只打印 a 的时候,那么这个程序呢,将会打印这个地址。 对吧?那么现在呢,当我们打印 &a 的时候, 它依然会打印这个地址。 但是,&a 跟 a 有什么不同呢? a 呢,是相当于指向数组首元素的那个指针。 而 &a 也就是取地址符号加上 a ,相当于什么呢? 相当于指向整个数组的一个指针。 啊,也就是说啊,这两个 a 啊, 虽然打印出的地址是同一个地址, 但是它们的管辖范围不同。 那么这个 a 呢,它的管辖范围只是数组的首元素。但是 &a 这个表达式所返回的那个指针, 它的管辖范围是哪呢?是整个数组。是整个数组。 虽然他们打印出的地址是一样的,但是它们的 管辖范围完全不同。啊也就是说, 如果在数组名字前面加上一个 & 的符号的话, 就相当于把这个数组名字的管辖范围 上升了一级,给它升官了。明白这个意思了吗? 啊,这就是 &a 的含义。好! 我们呢清理一下屏幕,接着往下看, 通过刚才我们这个介绍啊,大家都知道, &a实际上是一个指向整个数组的一个指针。 那么在打印完&a以后,我再去打印 &a+1,请问它会打印哪个地址呢? 那根据我们前面讲过的,如果对一个指针进行+1的话, 它会跨过它的基类型所占用的全部内存单元。 对不对,如果对a进行+1,我们得到了这个地址, 得到了这个结果;如果对&a+1的时候,我们应该得到哪个结果啊?是不是从这 将跨越整个数组啊, 对不对,将会跨越这个数组a+1。所以说 &a+1会指向哪里?会打印哪个地址啊,会打印这个地址。 这是&a+1的这个涵义。 它会跨越整个数组。好,说到这呢,我们不妨先来看一下 这个程序的执行结果。这是这个程序的执行结果,我们来核实一下。 当我去打印数组名a的时候, 我得到的是0028F7C4,也就是这个地址, 这毫无疑问是正确的。当我去打印a+1的时候,我所得到的是, 0028F7C8,是这个地址,这个也是正确的。 当我再去打印&a的时候,我们说这个时候, 虽然它跟a的结果应该是一样的,但它的权限范围不同。 我们先来看打出来的结果是不是一样的,0028F7C4, 跟这个结果是一模一样的。 那我接下来再去打印&a+1,那么根据我们的分析它将打印这个地址。我们来看一眼, 是不是0028F7D4?没有错这是这个地址。 通过这几次打印,我们就能搞清楚a和&a的涵义。 好再完成这个输出之后啊,我又打印了一个这样的表达式, *(&a),而且呢 先算&a 再去算*,那么对于这样的一个表达式我们应该怎么样去理解呢? &a,a是数组名,&a是指向数组的 一个指针,它是指向整个数组的这么一个指针, 对它进行指针预算,求*(&a),那这个会返回一个什么样的结果呢? 那么根据C语言的规范,出现在* 运算后面的这个表达式, 比方说这个E,如果这个表达式是一个指针的话, 那么*E这个表达式返回的结果将是, 这个表达式E,它不是个指针吗?它所指向的那个内容。 回到这个例子,比方说在这,我们在&a前面使用了*, 那根据刚刚我们所讲过的, 整个这个表达式,也就是*(&a) 这个表达式,返回的结果将是, &a这个式子所指向的那个东西,那&a指向的东西应该是什么啊, 我们刚刚讲过,&a指向的是数组, 所以说当我们去打印这个表达式*(&a)的时候,其实整个的打印结果就相当于我们去打印a 就是这样的一个结果。 所以说在这打印的时候,你看我们打印出来是0028F7C4, 是这个值。那有的同学说了,哎呀,你打印出来这个地址,我可不知道这个地址表示什么涵义, 因为在这,在这都会打印这个地址,那么 为了证明这个地址的涵义,很好办,我去打印一下*(&a)+1的值。 那么通过这个打印我们发现,它的值打印出来0028F7C8是哪个,是这个值, 说明我们在这打印*(&a)跟打印a的结果是一样的。 ok,这就是整个这个程序运行的结果。 那么鉴于这个程序运行结果的重要性,我们再来梳理一下它的输出结果。 我定义一个数组a,4个元素, 那么接下来当我去打印数组名a的时候,毫无疑问, 我会打印数组的起始地址,也就是这个地址。那么当我去打印数组名+1的时候, 因为数组名相当于指向数组第一个元素的指针, 因此对数组名进行+1的时候,将会跨过第一个元素,所以说这个值会打印这个地址。 接下来当我们去打印&a的时候,那么根据刚才我们所讲的, &a呢将会返回指向整个数组的一个指针。 当然当我们去打印&a的时候,仍然会打印这个地址,也就是说数组的起始地址。 但是呢我们知道这个时候,&a它的涵义已经不同了。 在a的前面加了一个&符号,那么就相当于把a的管辖范围给它上调了一级, 上升了一级。 原来a的管辖范围呢是第一个数组元素, 那么上调了一级之后呢,&a的管辖范围就变成了 整个数组了, 这是&a的涵义。所以说当我们再去打印&a+1的时候, 那么它会跨过整个数组的存储范围而去打印 这个地址。这是刚才我们所看到的。 ok,接下来再去打印*(&a),刚才我们也讲过, 这一句相当于打印a ,所以说, 我仍然会打印这一个地址,仍然会打印这个地址。 那么*(&a)的涵义呢完全就相当于a,也就是说啊当我们把一个*号 放在一个表达式的前面的是时候,就相当于把这个表达式的管辖范围, 又给它降了一级,对不对,原来&a的管辖范围是整个数组, 那么现在呢,我给它降了一级之后,*(&a)的管辖范围又变成了 数组的第一个元素了。怎么去证明这一点呢,当我们打印*(&a)的时候,它又会跨过数组的 第一个元素而去打印这个地址啦, 对不对,这就是这个程序执行的所有的结果。 ok,通过这个程序啊, 我们啊就可以得到这样一些结论,关于一维数组的。 首先呢,我们知道,数组名呢相当于指向数组第一个元素的指针。 那比方说这个数组a呢是指向这个首元素的指针, 也就是说a相当于&a【0】,这是我们知道的, 那么在这个前提下,通过刚才的例子,我们能得出来这样两个结论。第一个, 当我面写&a的时候,这个&符号用在a上面,就相当于把a的管辖范围, 做一个比方的话,上调了一级或者叫上升了一级, 也就是说原来a的管辖范围呢,只是a[0],数组的第一个元素。 如果我们在前面增加一个&的符号的话,那么 &a的管辖范围就变大了,变成了整个数组了, 上升了一级。那么跟这个结论相对的,我们还可以得到* 运算, 就是当我们在一个表达式的前面加一个* 运算指针运算符的时候, 也就是相当于我们把这个表达式的管辖范围啊, 下降了一级,比方说对于这样一个例子,那么原来a是指向a【0】的一个 指针,那如果我们对它做*a的运算,我们得到的 只是a 【0】的值,也就是说如果我们打印a的时候,好歹我们得到的是一个地址, 那么如果我们去打印*a的话,我们得到的只能是a[0]的值, 对不对。那么对比刚才的那个例子, 对于数组a,我如果&a的话,相当于上升了一级, 那如果我对这个上升的结果再做一个* 运算的话, 就把上升的那一级又给它降回来了,所以这个就相对于a。 ok,其实呢这一点都不复杂,总结起来就两句话, 也就是&号,就相当于把&后面的表达式,上调了一级,管辖 范围扩大了一级,* a呢就相当于管辖范围下降了一级。 如果这么理解的话,就非常非常的简单,也非常容易记住。 ok,有了这个概念,我们再去看二维数组,就容易的多了。