周二强
(河南城建学院计算机与数据科学学院,平顶山467036)
数组是C语言教学中的重点和难点,本文提出虚拟变量的概念,直观而深刻地揭示数组变量的本质。把数组统一为变量,为学生自主理解辨析数组和指针变量铺平了道路,也为反转教学提供必要的条件,极大地提高学生的学习积极性。
虚拟变量;数组;指针;C语言
图1 相关变量的存储状态简图
C语言是经典的结构化程序设计语言,其语法被现代编程语言广泛借鉴,国内许多高校都开设了C语言课程。国内现行C语言教材大多强调实训,注重编程能力的培养,忽视了C语言和计算机的关系,导致C语言教材中的一些概念不清,既不利于学生系统分析能力的培养,也不便于反转课堂等教学改革的进行。近年来新出现的“反转”课堂(Flipped Class,也译为“翻转”课堂)教学模式,引起了教育界的广泛关注。“反转”即师生互换角色,教师是主导,学生是主体,这充分发挥了以人为本的教学思想和教师的主导作用[1]。在课堂教学活动中应强调学生的参与意识,充分体现学生在课堂中的主体地位[2]。
数组是C语言教学中的重点。使用数组可以方便地重复处理大量的数据,作为数据结构,数组的基本用法不难掌握。但由于现行教材对数组概念的模糊处理,使得多数学生不能自主学习有关知识点,更不要说积极主动地参与反转课堂了,导致学习效果不理想,不会利用指针变量操作复杂的数组。
在C语言中,借助变量使用计算机中的存储单元,一个变量标识了计算机中的一块存储单元。存储单元是内存块在C语言中的抽象,具有固定的大小,分类型,某类存储单元只能存放该类数据。
语句int i;定义了一个整型(int)变量i,它对应于一个整型(int)存储单元,只能存储一个整数(int)。语句int*pi;定义一个整型指针变量pi,它对应于一个整型指针(int*)存储单元,只能存储一个整型存储单元的地址(int*)。与普通变量存储常见的数据不同,指针变量用于存储地址。地址是存储单元在计算机中的编号,用于定位存储单元。
语句i=5;可以把整数5存入与变量i相关的存储单元中。语句pi=&i;可以把与变量i相关的整型存储单元的地址存入与变量pi相关的存储单元中,此时常形象地称指针变量pi指向了变量i。它们在内存中的状态可用图1简单地表示。
可简单地认为,变量i的值是5,指针变量pi的值是&i。由于地址属于计算机的内部数据,而非用户数据,故对初学者而言,指针变量难以理解。指针变量的用法分两步,首先,以直接引用的方式使用指针变量本身,如pi=&i;,使得指针变量指向变量i;然后,以间接引用的方式借助指针变量使用其指向的变量,如*pi=6;。尽管直接引用的方式简便,但受变量作用域的限制。只要获得了存储单元的地址就能以间接引用的方式使用存储单元,间接引用的方式可以扩展存储单元的使用范围[3]。
语句int a[3]={1,2,3};定义一个数组变量a,即申请了一块存储单元用于存储整数1,2,3。数组变量a由3个整型变量组成,其内存状态可能如图2所示。
图2 数组a的内存状态
由图2可知,数组变量a没有专属于自己的存储单元,并不是一个真正的变量,数组变量a的值是C语言规定的。从这个角度分析,数组变量a是一个符号常量。但sizeof(a)的值是12,即数组变量a的存储单元有12个字节,数组元素的存储单元都属于数组变量a,所以,不能简单地把数组变量a理解为普通的符号常量。数组a应理解成一个虚拟的变量[3]。数组变量a指向了其首元素,数组变量a与其首元素a[0]的关系可用图3表示。
图3 虚拟的数组变量a
数组变量a是一个int[3]型存储单元。由图3可知,其地址和值均为0x0012ff00,但这两个地址的意义不同,其地址(&a)是一个int[3]型地址,其值(a)是一个int地址(&a[0]),即&a+1指向下一个int[3]型存储单元,其值为0x0012ff12,但a+1指向a[1],其值为0x0012ff04。
以二维数组int a[3][2]={{1,2},{21,22},{31,32}}为例,它的内存状态可能如图4(1)所示。
图4 二维数组a的内存状态
二维数组a有3个数组元素a[0]、a[1]和a[2],数组元素的类型是长度为2的一维整型数组,即它们标识一个int[2]型存储单元。
二维数组变量a是一个虚拟的变量,它标识的存储单元的长度为24个字节(sizeof(a)的值为24),类型为int[3][2]。二维数组变量a也指向了首元素a[0],其值为int[2]型地址0x0012 ff00,*a与a[0]可以互换使用,sizeof(*a)与sizeof(a[0])的值为8。
一维数组a[0]所标识的存储单元长度为8个字节,类型为int[2]。一维数组变量a[0]指向了首元素a[0][0],其值为int型地址0x0012 ff00,*a[0]与a[0][0]可互换使用,sizeof(*a[0])或sizeof(a[0][0])的值为4。
二维数组变量a,一维数组变量a[0],整型变量a[0][0]三者的关系如图4(2)所示。
如有int*p,则指针变量p不能用二维数组a赋值,因为二维数组变量a的首元素a[0]为int[2]型,即长度为2的一维整型数组,而指针变量p只能存储int型地址。可以存储int[2]型地址的指针变量理想的定义方式为int[2]*p,而C语言中实际的定义方式为int(*p)[2]。定义了可以指向int[2]型存储单元的指针变量p后,就可以用p=a(或p=&a[0])让指针变量p指向数组a的首元素a[0]了。此时指针变量p、二维数组变量a、一维数组变量a[0]及a[0][0]的关系如图5所示。
图5 二维数组与指向其首元素的指针变量
下面程序用三种方式输出了二维数组a的数组元素。
分析:
p=a;执行后,指针变量p也指向了数组a的首元素a[0],故p+i指向了a[i],*(p+i)与a[i]标识了同一个长度为2的一维整型数组,表达式(*(p+i))[j]标识了该数组型存储单元的第j个数组元素,表达式(*(p+i))[j]与a[i][j]可互换。
pi=a[i];执行后,pi也指向了a[i]的首元素a[i][0],故pi+j指向了a[i][j],因此表达式*(pi+j)与a[i][j]标识同一个整型存储单元。
pi=a[0];执行后,pi也指向了a[0]的首元素a[0][0]。表达式*pi++求值时,两个操作符优先级相同,右结合,先计算子表达式pi++,值为pi,原表达式的值为*pi,但求值的同时,变量pi的值自增1,pi指向了与原指向的存储单元相邻的下一个同类型存储单元。第一次求值时,表达式*pi++与a[0][0]标识了同一个存储单元,且pi指向了a[0][1];第二次求值时,表达式*pi++与a[0][1]标识了同一个存储单元,且pi指向了a[0][2];……。由于数组的数组元素相邻,故通过重复地输出表达式*pi++的值也可以输出数组a的所有数组元素。
指针的使用,极大地提高了C语言的执行效率,但地址的存在,增加了C语言的学习难度。给初学者提供一个直观且准确的概念,不仅可以极大地降低学习难度,不会造成C语言知识点的残缺,而且也为启发式教学提供了可能,教学效果显著。