欢迎光临散文网 会员登陆 & 注册

南开大学21级C++作业解析||特别篇

2021-12-07 23:10 作者:湮灭的末影狐  | 我要投稿

C++程序设计基础课程已经进入最后的章节,而随着问题越来越具体,大家的问题也越来越杂,这次就不再针对具体的题目进行专门解析了。针对这段时间大家出现问题的共性,这次想和大家讨论两个主题:1 不同版本的C++环境在何处不同;2 怎么优雅高效地解决作业问题。

1 关于不同版本C++环境的一些讨论

本课程使用的是赵宏老师在2019年编写的教材《程序设计基础》,但教材中使用的C++环境仍然是 Visual Studio 2010. 按照老师的要求,我们的作业批改是以程序在2010版本的运行结果为准的。

但是,现在许多同学都用上了VS2017, 2019,也有和我一样使用VisualStudioCode+MinGW配置的环境。这些不同版本的环境在运行C++程序时是存在微妙的不同的。这里总结如下:

1.1 库函数的调用

C++的cstring库,cmath库是两个很典型的库,有很多好用的函数,也常常在作业中被使用。

例如,第3章中牛顿迭代法一题中,需要判断%7Cx_n-x_%7Bn%2B1%7D%7C的值,可以使用cmath库的 fabs() 函数;第4章中,判断字符串是否为回文一题,合理运用cstring库的 strcpy(), strcmp() 函数可以极大简化代码;第5章中则有一题求 %5Csum_%7Bk%3D1%7D%5En%20k%5E%7B-m%7D 的值,使用cmath库的pow()函数也可以简化程序代码。

理论上,调用这些函数总是需要包含相应的文件:

但是,许多同学作业中没有上述语句,并表示自己电脑上这么写就是能运行。似乎是因为较新的版本的 IDE 已经有自动包含这些相关的文件。

后来我专门用机房电脑试了一下,经过研究,我发现:

在 VSCode+MinGW 环境(比如我的),上面两个include缺一不可;

在 VS2019 及更高版本里面,cmath 与 cstring 库都是无需专门声明的,确实可以直接用这两个库的函数;

在 VS2010 里面,cstring库无需专门声明,可以直接用;但cmath库仍然是需要声明的。

1.2 数组的声明

在第四章教结构化数据的时候,许多同学都找我问过用变量n指定数组长度的可行性。例如,希望先通过用户输入指定变量n的值,再以此作为创建的新数组的长度。他们尝试使用以下代码:

并且会发现这么做会报错。这是因为,C++程序编译的时候,N的值还未知,不知道为数组a分配多少内存空间,因此会直接报错。正确的做法应该要用到第六章中动态分配内存的方法:

通过这种方式,在程序中为数组a分配动态的内存空间,解决了以上问题。

但是,经过另一位助教学长的提醒,我在自己的电脑上(VSCode+MinGW)试了一下,发现前面那个简单直接的方法是可以运行的。当然这种操作终归是不规范的,大家学了第六章之后,还是应该用动态分配内存的方法。

1.3 数组的越界访问

第四章中讲了创建数组的方法,并明确了访问数组内容时下标不能越界。事实上,在 VS2010 中越界访问数组就会直接报错:

严格来说数组越界访问本身就是个违规操作,没必要单独拿出来讲。不过,这个错误操作有一个神奇的特性,想在这里分享给大家。

首先,上面那两行代码虽然在VS2010会直接报错,但是用 MinGW 编译器可以正常运行。虽然 a[3] 对应的内存地址没有专门分配给数组 a,程序仍然会在这个地址上写入4.

然后,如果执行以下代码:

理论上这个代码应该循环4次并输出4个1. 但是实际测试发现这段代码会进入死循环

循环体内添加断点并进行调试,发现 i 的值在012012... 不断循环。

我们两位助教讨论后,终于找到原因:由于一开始只为数组 a 分配了3个数的内存,for 循环中创建的局部变量 i 的地址就会紧接在 a[2] 的后面。接下来,当循环体进入第4次循环时,i=3,所以 a[3] 表达式强行越界访问,其实访问的是 i 的地址!也就是说,这里 a[3]=0 其实等价于 i=0,这就是为什么上面这段程序陷入死循环了!以上就是这个好玩的特性。

虽然这里给大家介绍了不同版本的微妙区别,但是记住本课程目前以VS2010老古董版本为准,所以前面说的VS2019无需调用库的情况,即使程序在你的环境能运行,如果在10版跑不起来还是会被扣分。只要严格遵守规范的语法,绝大多数时候不同版本还是能兼容的。

2 如何优雅高效地解决问题

关于这个问题,我想从两个方面讨论:如何更好地编辑和调试代码;如何优化解决问题的算法。

2.1 更好的编程环境

虽然本课程学习内容以 Visual Studio 2010 为准,但如果大家将来还需要常常用到 C++ 解决问题,我们还是建议大家给自己配置一个更好的代码编辑器和或 IDE。无论是新版本的 VS 还是配置了 MinGW 环境的 VSCode 都能够给用户更好的编程体验,其优势主要体现在更完善的错误提示、自动补全等,极大提高了编程效率。笔者之前曾经花了一周时间研究 VSCode 的 C++ 环境配置,并在这篇专栏中总结了相关经验:

当然如果想尝试这个,请优先保证课上的内容已经听懂了,作业做完了而且学有余力,并保证至少半天空闲时间。VSCode环境配置对新人来说还是挺麻烦的,我自己摸索整整花了一周。但是一旦环境配好,你的工作效率将得到飞跃。

2.2 断点调试

随着编程作业越来越复杂,有时算法出现问题,输出错误,而我们往往面对一堆代码无从下手。这时断点调试绝对是个好办法。具体操作大家可以看教材配套的上机实习第243页。(其实上机实习这本书附录很好用的...但凡大家都认真看过我遇到的问题得少一半)

添加断点后,程序会在断点处暂停,我们可以实时监视断点处各个变量、常量、指针的状态和值,对我们找到程序中可能的问题很有帮助。

比如说,第5章作业题:

验证组合数公式

C_%7B2n%7D%5En%20%3D%20%5Csum_%7Bk%3D0%7D%5En(C_n%5Ek)%5E2

这道题要求程序分别计算两边的值,判断是否相等。有的人虽然程序输出了yes,但仍然被我找到问题扣分。找出这些问题就是通过断点调试:在完成计算的地方添加断点后发现计算结果其实不对,属于歪打正着。

再比如第3章的那道牛顿法的题,我看到你的结果就能推测你的程序哪里错了,也是通过断点:通过循环体内加入断点就可以确定程序一共经历几次循环,从而推测可能的问题。

2.3 算法优化(这部分是谭助教上机课讲解的内容)

还是以上面那道验证组合数的题为例,我们发现大家计算组合数的方法普遍是

C_n%5Ek%3D%5Cfrac%7Bn!%7D%7Bk!(n-k)!%7D

然而,这样做必须先计算3个阶乘再相除,太慢了,而且分子太大容易溢出!一个最简单的优化就是

C_n%5Ek%3D%5Cprod_%7Bi%3D1%7D%5E%7Bk%7D%20%5Cfrac%7Bn-i%2B1%7D%7Bi%7D

更好的办法是考虑到

C_%7Bn%7D%5E%7Bm%7D%3DC_%7Bn-1%7D%5E%7Bm%7D%2BC_%7Bn-1%7D%5E%7Bm-1%7D

然后利用递归即可:

递归计算组合数

考虑进一步的优化,递归需要多次重复调用函数,不妨考虑建一个二维数组,空间换时间:

数组代替递归(a是全局变量)

终极优化:

我看不懂,但我大受震撼


南开大学21级C++作业解析||特别篇的评论 (共 条)

分享到微博请遵守国家法律