苏杭丽,李 燕,童 端
(南京财经大学信息工程学院,江苏 南京 210023)
指针是C/C++中的重点和难点[1-4],指针的“抽象”及使用的“灵活”让初学者感到无从下手和无所适从。
作者参考了大量的例题、习题以及学生上机时的各种出错代码后发现:如果根据每句代码的功能、分类成组、以组为单位重新排序,那么每个指针类题目的代码都符合一个通用的规律,即本文归纳出的“三步法”。“三步法”是一个好用的、通用的、易学的方法,能帮助学生快速掌握指针编程并提高代码的一次性正确率。
指针在“定义时”并不开辟存储空间,指针只能“依附”于基本变量或数组所开辟的存储空间。所以,在定义一个指针变量时需要定义一个“同种数据类型”的非指针量,通过给指针赋该变量所开辟空间的地址,让指针指向该变量的空间,从而实现指针对此存储空间的数据操作。也可以这样理解:非指针量在“定义”时开辟存储空间,该空间可以采用指针和非指针两种方式来定位(动态开辟存储空间除外)。将这一思路整理后再配合画图得出本文的指针编程三步法。
这一步需要注意的是:在定义一个指针量时,还需要定义一个同种数据类型的非指针量。非指针量定义时开辟的存储空间是为指针量作空间准备。所以,指针变量在定义时,是与非指针量“成对”出现的。
给指针赋值就是让指针指向某一存储空间。这里的赋初值,就是在指针和非指针量定义后就可以让指针指向非指针量所开辟的存储空间。
这一步需要注意的是:这里的给指针赋初值(如指针定义为int *p),是指“p 等于什么”,而不是“*p 等于什么”。p是“地址”;*p在“大多数”情况下是指“值”而非“地址”。
以int 类型为例,说明基本变量x、一维数组a[10]、二维数组b[3][4]相对应的指针定义、指针赋初值,以及x、a[i]、b[i][j]的地址和值的指针表示格式,见表1。
说明:
⑴一维数组指针赋初值有两种格式pp=a 或pp=&a[0],其中pp=a 是常用格式。不论一维还是二维数组,数组名代表数组的首地址。数组a 的首地址是首元素a[0]的地址&a[0],所以pp=&a[0]等价于pp=a。
⑵二维数组的指针定义有两种格式,分别是二维指针格式和一维指针格式。
●二维指针格式,如表1 的(*p1)[4],是二维数组指针的常用定义格式。
指针定义时带有一个与对应二维数组“第二维”相同的下标,任何情况下(如函数的实参、形参)的定义不可省略下标值。这种定义格式,指针与数组完全匹配,指针将按照行、列的二维模式访问数组元素。
●一维指针格式,如表1 的*p2,这种定义二维数组的指针格式不常用。
*p2 是一维模式的指针定义格式,只能按照一维的模式进行数据访问。因此,采用一维指针格式指向二维数组,要将二维数组变形为一维(或理解为降维),然后按照一维的模式访问数组。“二维降为一维”通过给指针赋初值实现,如p2=b[0]或p2=*b,达到降维的目的。然后可以按照一维的模式访问数组b,如数组任意元素b[i][j]在变形为一维数组后,相对应的元素离首地址p2 的位置为i*4+j,因此,b[i][j]用一维指针表示为*(p2+i*4+j)。
p2=&b[0][0]这种赋值方式,与基本变量和一维数组一样,直接赋某一个具体元素的地址。
⑶当函数的实参和形参是指针类型,它们之间的参数传递与指针赋初值同理。
当实参和形参指针的定义格式相同时,“函数调用时实参的格式和种类”可以参考“指针实参赋初值的格式和种类”。举例如下:
基本变量:int x,*p;p=&x;函数形如fun(int*q),相对应的调用格式为fun(p)或fun(&x)。
一维数组:
二维数组的二维指针格式:
二维数组的一维指针格式:
如果实参只定义了数组没定义指针,则函数调用除指针格式外其他的格式依然成立;或者先增加一个与形参格式相同的指针实参,给实参指针赋值、依据实参指针的赋值写出函数调用所有可能的参数格式,最后再去掉与添加的指针有关的代码。例如参考文献[1]第251 页例8.14,在main 主函数中只定义了二维数组score,没定义指针格式的实参;函数average 和search 的形参分别是一维指针和二维指针,它们都指向主函数的数组score。
为了理解本题,我们可以在主函数中增加一个与average 函数形参float *p 格式相同的实参float *p2;增加一个与search 函数形参float (*p)[4]格式相同的实参float(*p1)[4]。main函数主要代码相应改为:
如果函数average的形参定义为二维指针格式,如float (*p)[4],则函数调用为average(score),函数average定义的代码需要相对应地改为二维模式*(*(p+i)+j)访问,例如可以如下改变:
同样,函数search 的形参也可以改为一维指针模式,如float *p,则函数定义的代码和函数调用也需要进行相对应的改变。
涉及指针的问题采用画图的方式解决,更形象也更容易。“三步法”中的画图有如下约定:
⑴非指针量的画图
用方框“□”表示基本变量或数组元素定义时开辟的存储空间,如图1所示。
图1 基本变量x定义时的图
当给基本变量赋值时,例如x=5,也就是将5 放在x的方框中。
⑵指针的画图
“三步法”中,指针的值用箭头“指向”来表示。指针在定义时值为null,因此指针在定义时没有箭头,如图2所示。
图2 指针p和基本变量x定义时的图
当给指针赋一个变量的地址时,指针箭头将“指向”变量的空间,如图3。当给指针赋另一个指针时,如p1=p2,则p1 也将“指向”p2 所指的空间,即两个指针相等,则它们的“指向”相同。
图3 指针p赋初值的图
这里只观察符号*、[]和&之间的关系,得出:*和[]相当于“一个维度”,二者在符号效果上等价;&相当于消掉“一个维度”。
因为*p 和x 是定义在同一种类型下,所以*p 与x地位等价。当*p 去掉一个维度*变为p,x 也消掉一个维度变为&x,所以p=&x在符号关系上是等价的。
⑵一维数组定义和赋初值的符号关系
*pp 与a[]在定义时等价,*pp 与a[]分别消掉一个维度,得到pp=a。
在&a[]中,&的作用是消掉一个维度,[]表示一个维度,所以&a[]和a 在符号上是等价的,所以pp=&a[]与pp=a等价。
⑶二维数组定义和赋初值的符号关系
(*p1)[]与b[][]在定义时等价,二者分别消掉两个维度,(*p1)[]变为p1,b[][]变为b,所以p1=b在符号关系上是等价的。
b[][]和*p2在定义时等价,b[][]消掉一个维度变为b[],*p2消掉一个维度变为p2,所以p2=b[]在符号关系上是等价的。
*与[]都相当于一个维度,所以*b 与b[]在符号上是等价的,所以p2=*b与p2=b[]等价。
&相当于抵消一个维度,所以&b[][]相当于b[],所以p2=&b[][]与p2=b[]等价符号关系仅为了帮助记忆指针的不同赋值格式,从这个角度考虑问题没有理论依据,仅供参考和讨论。
现以参考文献[1]第224 页例8.3 为例,输入a 和b两个整数,按先大后小的顺序输出a和b。要求用函数处理,而且用指针类型的数据作函数参数。
⑴代码对比
参考文献[1]例8.3的代码标行号后如下:
将上面的代码,按照“三步法”的步骤拎出各步的代码行:第4、5行属于第一步,指针与非指针的“成对”定义;第8、9行属于第二步,指针赋初值。
按照“三步法”的顺序,将上述代码重新排序标行号后,得到的“三步法”代码为:
“三步法”的前两步“成对定义”和“赋初值”看起来非常简单,但它是指针编程通用的入口核心代码,让初学者拿到题目就知道该从哪里入手、该定义哪些指针、什么时候给指针赋初值、初值应该赋什么,可以有效解决无从下手并避免没有给指针赋初值就开始各种指针操作等各种各样的问题。
⑵画图对比
按“三步法”的画图约定,“三步法”代码行画图如图4所示。
图4 三步法代码画图
swap函数执行结束后回到main主函数继续执行,第9 行输出a、b 的值,由图4 可以看出,此时a、b 的值分别是9和5,*pointer_1、*pointer_2也是9和5。
参考文献[1]在第225 页给出本题的图示8.5。多种方法对比学习有助于开阔思维、更好地掌握知识。
“三步法”中,指针的“定义”和“赋初值”保证了指针操作“开端”的正确,第三步的“画图”能形象地演示指针“后续”的各种操作,因此,“三步法”对指针编程整个流程的有序、完整和正确有一定的保证,大大提高了代码的一次性正确率。
“三步法”有步骤、每一步有具体的要求和细节上的说明,明确了从哪里入手、每一步该做什么、该怎么做,是值得一试的指针编程方法。