C语言教学的若干实践和体会
2001年11月
摘 要:
C语言是一种应用广泛又比较难学的计算机高级语言,它接近硬件和系统低层软件资源,在教学上与其它语言有区别,本文结合教学实践和等级考试,强调了C语言教学要有一定的操作深度,并需特别关注C语言的弱类型性。
关键字:
C语言教学
C语言是一种应用极为广泛的计算机高级语言,自70年代初诞生以来,以其丰富的数据手段、强大的运算能力、简洁的表达形式和接近硬件的高效处理方式获得了众多程序员的青睐,成为面向过程应用中最热门的计算机语言。如果仔细检点我们所使用的各种软件环境和软件工具,很快就会发现:它们中的多数都是用C或C的衍生工具开发的,很难找出一两个例外。
C的成功使它进一步向周边领域延伸,例如向面向对象和可视化编程领域延伸,向工业控制领域延伸,向网络应用领域延伸,于是,我们有了C++和可视化的C++工具,有了支持51单片机的C51,有了J/J++语言。
C的成功也使人们学习C的热情空前高涨。这里仅举一个例子,由谭浩强教授编写的教材“C语言程序设计”,首次出版以来已重印(包括再版)了30多次,累计印数达300万册,有的印次一次印量就达30万册!仅此一点就已生动说明了C语言学习的广泛性。
但是,C又是一门比较难学的高级语言,与其它语言(如BASIC)相比,它更接近硬件、接近系统的低层软件资源,这就大大增加了学习的深度和难度。笔者曾连续数年参加面向大学生的省计算机等级考试阅卷工作,在题目(包括单选题和上机编程题)完全相同的情况下,二级C语言的原始通过率不足10%,与二级BASIC的通过率形成巨大反差,初学C的难度和效果由此可见一斑。换句话说,如果出一道编程题,规定可以用C、或者用BASIC来实现,那么对初学者来说,前者意味着更多的陷阱和出错机会,而对一个编程老手来说,非前者不能表现程序的简洁、优雅和高效,这就是C的特点和魅力。
考虑到C的应用前景和学习难度,一些学校和专业在课程设置上采取了“两步走”的策略,即先学一门较简单的算法语言如BASIC,让学生先掌握基本的语言手段和典型的算法,然后再用较少的学时来完成C的学习和提高。应该说这一做法比较符合人们的认识规律,但缺点是时间跨度大,学习内容重叠,后续课程相应延迟。
随着教学改革步伐的加快和后续课程的迫切要求,已有一批学校和工科专业将C安排为第一语言,并且在一年级就开始授课,这无疑给C的教学带来了新的压力和挑战。
笔者是硬件专业(应用电子)“出身”,教授C/C++已有近十年的历史,除C/C++以外,还教授过BASIC、FORTRAN、数据库等高级语言,也教授“微机原理”和“单片机原理”等硬件课程。在教学中曾先后使用过4本C语言教材和2本C++教材,参编过一本C语言教材,并且用C做过项目。但最早入门靠的是自学:92年自学C,99年自学C++,经历过初学的艰辛和困惑。相信这一背景对我的C语言教学有一定的补益,也许还使我有一些新的视角。下面拟结合教学实践和省等级考试的一些情况,谈谈我在C语言教学中的若干看法、做法与体会,期盼与同行们做进一步的交流。
一、C与其它语言的教学比较
笔者认为,所有计算机高级语言课的教学均包含两个主要目标,一是语言环境和语言手段,二是与一定数据结构相结合的典型算法,C语言也不例外。从算法设计的角度看,各种语言在解决同一问题时,其思路不会有太大的差别,因此这一块内容在教学上差别不大,学过一种语言以后再去学别的语言,其难度大大降低;但是,从算法实现的角度来看,不同的语言有着不同的设计背景和设计目的,其应用环境和应用手段有差别,有时差别还很大,例如,FORTRAN最早是为了满足科学和工程方面的公式计算而设计,它的复数数据类型、输出输入方式、数组的按列存储和公用数据区等都是很独特的,与其它语言大不不同。
最极端的例子是BASIC和C,前者为初学者而设计,后者为实现UNIX操作系统而设计;前者的应用简单而粗略,不要求用户有多少计算机方面的专业知识,后者的应用深入而精细,向用户提供了很多技术手段和技术细节。因此,除了内涵大小不同以外,这两门课也不可能在同一层次上进行操作。
有一个例子可以说明这一情况。在等级考试中有这样一道级数求和题:

由于算法简单,BASIC考生多数都做对了,因为系统已将所有的变量默认为表示能力最强的浮点类型,在运算中它们的类型是统一的。而C语言考生很多都做错了,其错误主要发生在3处:①没有将存放“部分和”的变量初始化、或初始化的地方不对,这种情况BASIC考生中也有;②将存放“部分和”的变量设计为整型,不能有效存放最终的运算结果;③虽然将“部分和”变量设计为浮点或长整型,但变量n设计为整型、没有考虑到n*(n+1)*(n+2)子项在运算过程中将超出整型范围,导致最终结果仍然出错。后两种情况均属数据类型错误,在BASIC中是不会发生的。
C考生的这种错误表面上看似乎并不严重,程序只需稍稍改一下就能对,是一种“疏忽性”错误,但笔者并不完全认同这一看法。我的看法是,这在一定程度上暴露了考生对C的理解不深,也在一定程度上反映了教材以及教学上对数据类型这一块强调不够,或操作不太到位。
笔者一直认为,在学习C时更要强调它的背景,即设计者的初衷是用它来编写UNIX操作系统、并部分取代汇编语言。由于操作系统是管理、调度计算机软硬件资源的一个大型软件,所以,作为实现它的语言工具,C没法不接近硬件。换句话说,要学好C也没法完全躲开包括硬件在内的一些深层知识,这就要求C的教学与其它语言相比要有一定的深度。
我想,尽管我们的学生也许最后不会去编操作系统那样的软件,但总要把C当作一个能用的语言工具来学,如果学得很虚,最后干不了实事,那还不如另换一个好学好用的语言工具。最近,Visual BASIC(VB)风头很盛,它是一个采用面向对象技术和可视化技术的先进语言工具,既相对简单,又能胜任WINDOWS下的编程,有可能成为今后非计算机专业语言学习的主流。
二、C语言教学要有一定的操作深度
C语言教学要有一定的深度,但到底应该操作到什么程度?很难说出一个公认的标准,我的主张是要尽量满足学生对基本语法现象的理解,“磨刀不误砍材工”,有时费点劲把深层道理讲了,学生在理解上省了力,反而免去了不少死记硬背的工夫。
下面以Turbo C中的3个函数getchar()、gets()和scanf()为例,说一说背景情况和我的操作。
1、这3个函数都属于标准输入函数,它们在头文件stdio.h中进行说明,其功能分别是:从标准输入文件得到一个字符、从标准输入文件得到一个字符串、按所给字符格式的要求从标准输入文件得到若干项不同类型的数据。显然,这3个函数在操作上存有共性。
2、除gets()外,另外两个函数在教学上出现很早,因为一开始编程就要用到它们,躲也躲不开,尽管它们所涉及的知识在教材上展开很晚,或者到了也没有展开。此外,scanf()函数有诸多的操作细节,教材上虽然都讲了,但道理上讲得不多(可能是想回避深层知识),学生很难形成统一的认识,在不太理解的情况下只好去死记,把握起来难度很大,并导致一些后续的错误,比如当scanf()函数连续使用时情况就不太正常(已有多人向笔者请教这方面的问题)。
3、我的处理是将3个函数捆在一起、硬着头皮往深里讲,所抱的指导思想是“讲十分可能会七分,如果讲七分就只能会五分”。
4、这3个函数的操作对象都是标准输入文件,该文件在系统启动时自动打开,文件指针由系统定义为stdin。所谓标准输入文件,实际上是在DOS的高层将键盘设备当作一个输入文件,将键盘操作视为文件操作;由于键盘是计算机必备的输入设备,所以又称它为标准输入设备或标准输入文件。显然,DOS的这一高层操作必然要依赖于DOS的低层功能,其具体机制是,系统为标准输入文件配有一个文件缓冲区、并支持行编辑操作,即:用户连续键入字符,只要用户不键入回车('\r'),则前面键入的字符可退回任意修改;一旦回车,键入字符将进入文件缓冲区,包括最后键入的回车符也将转变为换行符('\n')送入文件缓冲区,此后它们将不能再被修改。
5、这3个函数面对同一个设备对象,共享同一个文件缓冲区,在操作上均分成两个环节。第一个环节,如果文件缓冲区空,它们等待用户键入字符直到用回车结束本行操作;第二个环节,它们从文件缓冲区中提取字符,并进行各自的相应处理:
getchar()函数将从文件缓冲区中截取一个任意字符(包括空格和换行符)返回。
gets()函数将从文件缓冲区中截取若干字符(其中包括空格符),直到遇到换行符并将它转变为控制字符NULL('\0'),并以它作为串的结束标志。该函数返回串的首地址。
scanf()函数将按格式的要求逐项截取字符,将它转变为要求的数据类型后存入相应的地址。截取原则有3个:①按格式要求的数据类型和域宽来截取,②遇到空格符、制表符和换行符时截取告一段落,③遇到与所要求的格式不匹配的字符时截取告一段落。注意被scanf()函数截取后,最后的换行符仍被遗留在文件缓冲区内,成为影响后续接收的“垃圾”。该函数返回成功匹配的数据项的个数。
6、以上操作后,如果文件缓冲区中还有字符,将留待下次由任何一个标准输入函数来继续截取;如果文件缓冲区已空,但scanf()函数中全部数据项还未获得满足,它将等待用户继续输入字符和回车,并将进行再一次的字符截取。
7、在连续使用scanf()函数时,可用getchar()、fflush(stdin)或其它函数清除前次遗留在文件缓冲区中的“垃圾”字符。
三、要特别关注C是一种弱类型语言
C虽然提供了丰富的数据类型,但它却是一个弱类型语言,所谓弱类型是指它对数据类型的检查很弱、很宽松。在C中,数据类型的应用对用户来说主要表现在数据的定义和存储,此后在进行混合运算、格式输出、函数参数传递,以及数组和指针操作时,编译程序对数据类型的限制很少,即表面上它对数据类型的不一致表现得非常宽容,实则是在内部按一定的规则自行进行了处理。这一点,与其它语言相比差别极大。C给人的感觉好象是程序怎么编都出结果,怎么“玩”都不“死”,但实际上其内部的处理和编程者的设想可能有出入,导致最后得不到预期的结果。C的这一特点,给有经验的程序员带来了很大的灵活和便利,也给初学者造成了众多的疑惑和麻烦。阅读下面的程序/程序段,判断其输出结果,多少可以说明这方面的问题。
① char ch=32*‘A’;
int i=65535*2;
printf(“%c,%d,%u,%d\n”,ch,ch,ch,i);
② main()
{ printf(“%d\n”,max(3.0,5.0));
}
int max(int a,int b)
{ return a>b?a:b;
}
③ char a[]={0x12,0x34};
int *p=a;
printf(“%d\n”,*p);
④ union
{ float f;
long l;
}x;
x.f=12.5;
printf(“%lx,%f\n”,x.l,x.f);
根据笔者的经验,因数据类型方面的错误导致程序失败的例子很多很多,正因为如此,笔者对于数据描述、数据转换和数据运算这些内容的教学历来比较用力,并注意在后续课程中不断提醒学生关注数据类型,比如:用函数原型说明函数、在函数定义时不要缺省返回说明、在表达式中尽量用显式转换等等。
下面是我在数据描述和数据转换中的操作轮廓,借此说明我的观点和风格:
1、数据描述(在Turbo C范围内讨论)
① C中的数据按系统定义和用户构造可分为基本类型和构造类型两类;按使用特点可分为常量和变量两类(用例子展开说明直接常量、符号常量和变量);按存储属性还可继续区分为其它的类型(不展开)。
② 基本类型按数据的表示方法分定点数和浮点数两类。其中,定点数中小数点的位置默认在最低有效位的右端。定点数的特点是数据绝对准确,但表示范围受数据宽度的制约。浮点数在机器中分两段(域)来表示,即阶码域和尾数域,它们分别表示数据的量级和精度,在浮点数的两个域中小数点的位置也是默认的。浮点数的特点是表示范围较大,并且具有相当的精度,但表示范围和精度受到各自域宽的制约。
③ 定点数按数据宽度的不同可分为字符型、整型、长整型。此处展开说明相应的关键字和各自的数据宽度,说明在有符号情况下各自的表示范围,以及典型的直接常量。
④ 按符号处理的不同定点数可进一步分为有符号数和无符号数,展开说明相应的关键字、机器数和真值的概念,说明符号位、原码、反码、补码,演练求补的过程,说明同样长度的有符号数和无符号数在机器数一级是统一的。
⑤ 浮点数按数据宽度的不同可分为单精度、双精度、长双精度(很少用到,不展开),展开说明相应的关键字和典型的直接常量,说明各自的数据宽度、域宽分配和数据表示的大致范围和精度,举例说明国际标准的浮点表示。
⑥ 简要介绍构造类型数据,包括:数组(由多个同类数据连续存储而构成)、结构体(由多个不同类型的数据捆绑而成)、共用体(同一数据存储区段由多个不同类型的数据互斥地共享)、枚举类型(整型的变种,其变量的取值有限,每种取值用不同的名字来表示),让学生对数据类型建立一个整体的轮廓,但这一部分不做更多的展开。
2、数据转换(在Turbo C范围内讨论)
① 数据的类型转换包括用户强制转换和系统隐式转换,前者由用户通过强制类型转换运算来显式实现,操作上一般没什么问题;而后者是在用户不知晓的情况下由系统自动进行,如果用户不能了解和把握,就可能导致错误。
② 不同数据类型在数据的表示能力和表示特点上有差异,如果撇开数据的表示特点,单看数据的综合表示能力,那么从弱到强依次为:字符型→整型→长整型→单精度浮点→双精度浮点。当数据从表示能力弱的类型转换到表示能力强的类型时,一般不会出太大的问题,但少数情况下也可能会丢失数据的精度(长整型→单精度);当数据从表示能力强的类型转换到表示能力弱的类型时,则很有可能造成数据的失真或精度的损失。
③ 具有同样宽度的有符号整型和无符号整型,相互间也可进行转换,但这两种数据在机器内部形式统一,其实并不存在转换问题,它们的区别主要表现在输出环节,即同一个机器数在输出时将被解释为不同的数据,如字符型定点机器数0x80可以按无符号形式输出128,也可以按有符号形式输出-128;同样的情况,也会发生在共用体数据的不同成员之间。
④ 隐式数据转换将发生在以下场合:
为了统一内部的数据处理(归一化),系统会自动将字符型数据转换为整型、将单精度浮点数据转换为双精度浮点,这一操作对结果没有任何不利影响。
在变量初始化和变量赋值时,系统会将右值表达式的结果自动转换为左值变量的类型,如int x=3L;、x=‘A’;。
在用printf()函数进行输出时,系统会将输出数据自动转换为格式所要求的类型,如printf(“%u\n”,-128) ;。
在某些运算场合,操作数的类型将会转换为结果所要求的类型,如&a、3.5>4.2。
不同类型的数据在进行双目运算时,表示能力弱的操作数将转换为表示能力强的操作数的类型,如3*3L/1.0+‘A’。
如果函数在调用前进行了原型说明,那么当实参类型与形参类型不一致时,系统会自动将实参类型转换为形参所要求的类型。
当函数的返回值与函数所定义的返回类型不一致时,系统会自动将该值转换为所要求的返回类型。
四、语言课教学的其它体会
1、计算机语言课,不管是哪个语种其实践性都很强,对C语言来说因学习难度大,更要强调学生从上机实践中学习,从程序调试和各种挫折中学习。此外,语言课的考核方式如果仅靠笔试尚不能完全反映学生的语言应用能力,要努力引进机试手段。
2、算法是语言课的主要教学目标和永恒主题,有些第一语言课因学时少、或者其它一些原因,教师在操作时压缩算法,将重心偏离到语法规则和语言手段上,这样做不利于实现语言课的两个主要目标,不利于学生掌握实际的语言应用能力;当然,在学生方面也有不重视算法、缺少算法锻炼的情况。在历次等级考试阅卷中,笔者常会碰到有些考生编程没有思路,所写的东西完全不“沾边”或者连续几道题都不着一字的情况,这种情况多数都是因为考生的算法能力太差,也从一定程度上反映了语言课中算法训练环节有待进一步加强。
笔者认为,语言课应保证用于算法的基本学时,教师操作上应保证和突出最起码的算法介绍,比如:累加器与累乘器的操作,级数求和,求定积分,求素数,数据的排序与检索,一维数组的遍历、求和与求均,二维数组的按行遍历与按列遍历,字符串处理,以及穷举、迭代、递归等面向解题思路的算法。在讲解一个新算法时,教师不应上来就讲程序,而应先讲清思路,然后分配变量角色,最后才是具体的程序设计。
3、语言课总免不了要举例,在举例方面我有以下的做法与体会:
对说明语法规则和语法现象的例子追求尽量简单,对说明算法的例子追求尽量典型,除了综合演练外所有例子都不要太过复杂。
不但要举正面的例子,而且要特别注意举反例。
一个程序例材用后不要轻易抛弃,可尽量变化使用,例如每次改一点,看看情况会如何变化,加深学生对程序中各项参数和要素的理解。
一个题目原型,可采用多种办法来实现,讲解时不一定要一次操作完,例材可以重复使用,这样可节省背景介绍,加深学生的印象。
对一些典型算法(如排序)要注意提取其轮廓框架,进一步浓缩其算法要点。
对使用频度高、容易掌握的算法或程序结构(比如多种求素数的算法中就有较容易的)可多讲;反之,可少讲。
对非结构化的语言来说,结构化的手段和例子要多讲,非结构化的可少讲或不讲。
4、文件和文件操作几乎总是排在了语言课的最后,因此无论是教还是学,往往都显得十分匆忙:一是课程收尾,二是多半已没了上机时间,三是学生也来不及消化,所以效果总不太好,而老师在考试时也会有所回避,使得这块内容多少有点被忽略。但是,文件始终是应用中的重要一环,在等级考试中它也占了相当的比重。在笔者的阅卷印象中,文件方面的编程题鲜有完全做对的考生,反映了这方面的欠缺。所以,也想提醒各位老师和同学,应加强文件方面的训练,特别在等级考试前,考生无论如何要练练手,做到有备而战。
下一页 教学论文——面向对象与C++语言


