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

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

1 关于不同版本C++环境的一些讨论
本课程使用的是赵宏老师在2019年编写的教材《程序设计基础》,但教材中使用的C++环境仍然是 Visual Studio 2010. 按照老师的要求,我们的作业批改是以程序在2010版本的运行结果为准的。
但是,现在许多同学都用上了VS2017, 2019,也有和我一样使用VisualStudioCode+MinGW配置的环境。这些不同版本的环境在运行C++程序时是存在微妙的不同的。这里总结如下:

1.1 库函数的调用
C++的cstring库,cmath库是两个很典型的库,有很多好用的函数,也常常在作业中被使用。
例如,第3章中牛顿迭代法一题中,需要判断的值,可以使用cmath库的 fabs() 函数;第4章中,判断字符串是否为回文一题,合理运用cstring库的 strcpy(), strcmp() 函数可以极大简化代码;第5章中则有一题求
的值,使用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章作业题:
验证组合数公式
这道题要求程序分别计算两边的值,判断是否相等。有的人虽然程序输出了yes,但仍然被我找到问题扣分。找出这些问题就是通过断点调试:在完成计算的地方添加断点后发现计算结果其实不对,属于歪打正着。
再比如第3章的那道牛顿法的题,我看到你的结果就能推测你的程序哪里错了,也是通过断点:通过循环体内加入断点就可以确定程序一共经历几次循环,从而推测可能的问题。
2.3 算法优化(这部分是谭助教上机课讲解的内容)
还是以上面那道验证组合数的题为例,我们发现大家计算组合数的方法普遍是
然而,这样做必须先计算3个阶乘再相除,太慢了,而且分子太大容易溢出!一个最简单的优化就是
更好的办法是考虑到
然后利用递归即可:

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

终极优化:
