唐 建,王 雷
(中国科学技术大学 信息科学技术学院,安徽 合肥230027)
“程序设计”是计算机、软件等信息类专业的核心基础课,是理工科其他各专业的公共基础课程[1]。正是由于其重要性,程序设计的话题一直就没有断过,总是不断出现相应系列课程的教材建设和教学研究[2~5],并不断引入新的元素和思想[6],包括计算思维[7][8]。由于C语言独特的魅力和优点,大多数理工科学校目前仍然是以C语言为主要语言来讲授程序设计基础[9]。
指针教学是一个教学难点[10],尤其是函数指针及其应用,有的甚至把函数指针作为自学内容[6]。学习C语言的困难主要是由于“抽象”和“自动化”[11]。有时候为了查找和解决一个很小的错误都要花费很大精力,从而导致莫名奇妙的怨气,甚至产生放弃进一步学习的念头,极大地挫伤了学生的学习热情。所以,要学好C语言程序设计,需要教师的引导,包括知识引导和心理引导。
近几年逐步推动的一个新的重要教学思想,是在教学计划、教学设计以及课堂教学各环节中引入课程思政元素[12~14]。这表明当前大学教育不仅要考虑知识目标和能力目标的培养,还需要重视思政目标的培养。在各门课程里引入思政元素不尽相同,在C语言程序设计里,首先要遵守基本规则,再者要体现出精益求精的工匠精神,更需要积极探索的拼搏精神。思政目标的达成将促进知识目标和能力目标的培养,三者之间是互动的。
思政元素的引入不仅有助于学习,还能教育学生如何面对人生未来、职业发展中可能遇到的一些问题和困难,从而培养出遵纪守法追求进步的接班人,这是大目标。
从2014年到2017年,上海中医药大学共设立了100门校级重点建设课程开展了结合课程思政的教育,试点效果良好,给全国高校思想政治工作会议提供了重要的参考。其中基础医学院张黎声教授的“人体解剖学”课介绍了遗体捐献背后的故事,并带领学生参与红十字会活动,从而更加充分地了解了红十字精神,通过采访遗体捐献者让学生了解他们为什么捐献遗体,并通过这些普通人内心强大的社会责任感引导学生敬畏生命,从而这门“冷冰冰”的课程深深地打动了学生的心并改变了学习态度。
在“人体解剖学”这类课堂里开展思政教育比较容易,如何在更为一般的基础课、专业课上引入并运用好课程思政呢?又如何理解每门课都有育人功能、都有思政元素呢?这是需要认真思考才可以做好的,而且要做到恰到好处,既不引起尴尬而又达到课程思政的真正的有效的目的。文献[15]给出了一些参考意见,还需要教师结合自己的课程去深入思考,挖掘课程背后的“故事”,以达到启迪学生的思政作用,让学生深刻理解课程的地位和作用、学好课程后将来能做出的贡献和人生价值。
文献[16]提到培育和践行核心价值观重在落细落小落实,要在落小上下功夫,落小要求之“小”只是小在切入口,而小切口同样需要大手笔。那么,在大学非政治类的基础课、专业课上开展课程思政教育,就可以采用“教育切入口要小,从细小处入手,以小见大。”的策略。力避脱离实际的“空话”、“大话”,注重分析不同学生的身心特点和实际情况。要采用能够让学生感觉贴心、容易接受、可以采纳、易于效仿并能够产生积极效果的做法,积小善为大善,并不断地改进和完善,从而实现以小见大的作用。
就C语言程序设计而言,思政元素可以是围绕做好程序设计来引出做人做事的道理,以及未来的职业道德和职业精神。从落小来说,在C语言程序设计里可以从小问题入手:“为什么编程总是出错?”。这个问题看似小,但可能反应大问题。小问题可以是没有用好数据类型、没有注意语句规范,大问题却是没有掌握编程要领、没有良好计算思维。那问题到底怎样,需根据具体情况去分析,并提供恰当的方法以解决问题,这就是落实。这需要教师对问题有较好的把控能力和解决能力以及拓展能力。
从课程思政角度该如何具体引导学生呢?没有正确使用数据类型和编程语句,就是没有遵守C语言的基本编程规则。好比法制是基础,若不遵守法制,错误行为将会导致违法犯罪,所以做任何事情须遵守相应的基本规则。更大一些的问题,就是行为习惯不好,总是马虎,在编程时小问题很多,这需要学习工匠精神,精益求精才能出成果。再大一些的问题,就是懒,不愿意吃苦,没有拼搏精神,没有训练出良好的计算思维,最终在编程这个行业就不能够提升到一个更高的境界。因此,只有遵守规则掌握语法、认真专研克服粗心、努力奋斗提升计算思维能力,才可以学好C语言程序设计,这跟做人做事的道理是相通的。
本文侧重C语言函数指针教学研究,它是一个难点,但在实际应用中它有重要作用。例如,可以把函数当作形参传递给具有一定通用功能的函数模块并封装成接口来提高代码的灵活性和后期维护的便捷性。另外,有些地方必须使用函数指针才能完成给定的任务,在嵌入式系统应用中用户程序调用一些系统函数时只能使用函数指针的方式,通过将系统函数的入口地址传给函数指针来达到调用ROM中程序的目的,又如在Linux系统里异步信号中断处理中发生某一触发信号时需要调用相应的处理函数,此时需要使用函数指针来实现。
函数指针的定义形式为:
类型(*变量名)(参数列表);
函数指针与函数原型类似,但是函数名用(*变量名)代替;参数类型列表可以省略,但一般不要省略。例如,下面定义的function是一个函数指针:
int(*function)(int x,int y);//定义指向函数的指针
函数指针主要用于函数的参数,即把指向函数的指针变量作为函数参数,从而把一个函数传递给另一个函数。C编译会自动把函数名转换为指向该函数在内存中入口地址的指针变量,利用该变量可以更灵活地进行函数调用;一个函数对应一块指令序列,其首地址就是函数的入口地址。因此,函数指针,又称为指向函数的指针,它与函数的入口地址有关。
通过简洁的代码、生动有趣的内容、暗设bug、再解决bug、再深入讨论和分析的教学过程来理解、辨析、掌握尽可能多的知识点,设置开心学习、乐意思考的教学情景,实现低成本、高收益、付出少、回报大的教学效果。在教学过程中逐步引出一些课程思政元素以教导学生如何做事,也包含做人的道理以及敬业精神,对计算思维训练也有促进作用。
本案例以函数指针为中心来设计,而函数指针又可以扩展到数组。本案例设计了若干个子函数来辅助教学,子函数由函数指针数组来关联。又设计了函数指针的返回结果为指针变量,从而将函数指针、函数指针数组、指针函数结合在一起。同时,在细节方面,案例教学中也考虑了变量的存储类型、字符串的控制。为此,需要相关代码的配合来完成案例教学,从而丰富了案例教学的知识点。
案例教学的核心语句为:
char*(*pf[3])(char*p);
如果熟悉了函数指针和指针函数的两个定义,再结合数组的概念,就比较容易地知道,pf就是一个函数指针数组,这里的数组元素有3个,是pf[0]、pf[1]、pf[2]。每一个数组元素都是一个函数指针,每个函数指针都返回一个字符指针。每个函数指针的入口参数都是一个字符指针变量p。为本案例为所设计的基础的主函数和子函数分别如下代码所示。
在程序执行过程中,将3个子函数的入口地址分别赋给了函数指针数组变量,然后通过调用函数指针数组变量就可以完成对3个子函数的调用。按照这样做法,如果有很多的子函数,就可以不用去记忆它们各自具体的名字,而只需使用不同的函数指针数组变量就可以完成对不同子函数进行关联和调用,非常方便实用。这就是设置函数指针数组的好处,其中函数指针是关键,是案例教学的核心。
在主函数里定义了字符指针数组char*result[3]={‘ ’},并由它来接收子函数的返回值,即一个字符指针变量的地址。主函数里通过result[0]=pf[0](“西瓜”);语句实现function1的调用并完成输入参数传递(“西瓜”的首地址)、回收子函数返回的字符指针;子函数输出I got西瓜,然后返回I Love西瓜这个字符串的首地址给result[0]并在主函数里输出在屏幕上。
上述的基础代码输出了3组结果:第1组是3个子函数只输出自己的结果,第2组是3个子函数输出自己结果的同时返回字符信息给主函数再输出来,第3组输出是主函数在3个子函数调用完成之后再统一输出从子函数获取的字符信息。3组输出结果如下所示:
从输出结果可以看出,第1组和第2组输出都是对的。第2组输出是3个子函数分别接收、输出并返回了西瓜、桃子、芝麻,然后主函数通过result数组分别输出了西瓜、桃子、芝麻,这是希望的结果。但第3组结果是通过result数组变量都输出了芝麻,就是西瓜没有了,桃子没有了,最后只剩下了芝麻!这不是想要的结果,为什么会是这种结果呢?
原因是因为3个子函数里的变量ILoveP1、ILoveP2、ILoveP3均是默认的auto类型。auto类型变量在子函数被调用时分配空间、在子函数返回时释放空间;又由于3个子函数是特意设置的,长得基本一样,所以后一个子函数的变量在分配空间时使用了前一个子函数变量使用过的地址。最终,result数组变量result[0]、result[1]、result[2]均指向了同一个地址空间。这就是问题所在——没有遵守基本规则!解决办法是设置ILoveP1、ILoveP2、ILoveP3变量为static类型。
通过上述讨论、定位问题、解决问题,深刻理解了auto和static的作用,同时也深刻体会到要遵守规则才能避免一些比较明显的错误。好比校纪班规、法律法规是基本要求,需要遵照执行,才能维持正常秩序,从而避免一些不必要的错误行为。这些道理都懂,但是不一定能遵守到位,如果不遵守,像上述结果,最后只得到“芝麻”!西瓜没有了、桃子也没有了,结果是错的。
在实际生活中,程序中小错误可能导致大灾难,这样的案例教训有很多。例如,交通路口的红绿灯控制,看似很简单,但是作用非常大;如果红绿黄灯出现了错误指示,将可能导致严重交通事故。
但是,按上述解决办法之后,第2组和第3组的结果还并不是这样的:
要得到这个结果该怎么办?问题更为复杂些,需要更进一步思考才能给出解决方法。根据这样的教学过程,一步一步地设置问题、分析问题、解决问题,就可以引导学生将多个知识点通过这样的小代码串起来,既增强学习的乐趣,又提升自主学习能力。
这个问题教导学生做事马虎不得。在程序设计上需要静下心来认真做事,细节决定成败。程序设计上有细节很多,需要敬业精神,在未来的工作岗位上也是如此。
有了上述案例锻炼之后,再来看一些涉及函数指针的更为复杂更为抽象的定义和运用就不难了。例如,下面这个定义也是关于函数指针的,只不过形式更为复杂了,初看起来是更为难懂的:
char(*(*x[3])())[5];
若不结合上面案例教学中得到的收获,而是从头分析这个定义,具有一定的难度。甚至有人问:这个定义有没有什么实际用处?一个并不很困难的定义使得一些人产生了对其使用价值的怀疑!原因就是感觉有些难而不想去理解和使用它。现在,在掌握了前面案例再来理解上述定义,是不难理解的。采用对比方法分析如下。相关两条语句为:
char*(*pf[3])(char*p);
char(*(*x[3])())[5];
对比上述两条语句,下一条相比于上一条(即案例教学中的)定义,主要的差别就是中间多了一对圆括号,末尾多了个[5],而函数的输入参数为空。中间多出的那一对()是来改变结合顺序的,是用于声明函数的返回值是指针,然后再跟外围的char[5]结合,就是声明函数返回的指针指向一个char[5]字符数组。这样的理解就省了很多麻烦,从而对函数指针有了更全面、更深入的理解;而且更深入地结合了指针函数,因而增强了对相关知识点的认识和理解,进一步地掌握了使用方法。
这不仅仅训练了发现问题、解决问题的能力,对激发学习兴趣和提升学习能力也产生了推动作用,因为它推动了进一步的尝试和验证,还找到了可以回答他人问题“这个定义有用吗?”的答案。C语言函数一般情况下只返回一个数据变量,想要返回更多数据需要其它方法;上述这种定义就是一种很好的办法,其作用和价值就显而易见了。
能有这样的收获,定会感到高兴,这就是奋斗出来的辛福感!在未来的科研工作中也需要锐意进取的拼搏精神,国家才能更加繁荣富强,人民更加幸福安康。这是引入思政元素的大目标。
本文结合课程思政教育,设计了函数指针教学案例并在教学过程中融入了思政元素。教导学生要遵守规则、要有精益求精的工匠精神、要有奋斗精神才能学好C语言程序设计。这与做人做事的道理非常相似,也是案例教学要达到的遵纪守法、认真做人踏实做事、努力奋斗的思政目标。
就本案例教学的知识目标和能力目标训练而言,也实现了良好效果。它以函数指针为中心,扩展到函数指针数组,并结合了指针函数、指针函数数组、变量的存储类型、字符串的控制等相关知识点的综合运用,代码量小而知识点丰富,易于组织教学,从而容易达到良好的教学目标。