• 我的女朋友漏电了–论C++中的失败(failure) 缺陷(bug)和异常
  • 发布于 2个月前
  • 94 热度
    0 评论
  • 卡农
  • 14 粉丝 40 篇博客
  •   
我承认有点标题党了,不过这真的是一篇写软件的文章,所以如果你已经抽出了一张面巾纸,那么趁早再把它完美的放回去。这篇软件文章很软,源代码不多,而且大部分都是伪代码。所以很适合所有人看。我特别推荐年轻的初学者,把纸巾放回去后,继续看下去。如果把这几个概念理清楚,对未来的工作非常有帮助。

先说失败(failure)。常见的软件的失败主要分为三种,编译失败,运行失败,结果失败。下面通过一个程序的例子来说明:
—————————————
int l1 = 0;
ll= 23; //compile fail
int a[10];
a[10] = l1; //run time fail
if(l1=1){
… // result fail.
}
——————————————

你可以把编译失败理解成世界上最聪明的一堆程序员在帮你审核你的代码。如果审核失败,他们会提醒你在造成严重错误之前修改你的问题。单从这一点看,C++也比一些解释性的语言更加安全。例如源码中你想把变量l1修改一下。但是由于笔误写成了ll。C++的编译器会对你大吼的。但是Python一声也不吭,看你运行时候的笑话。

运行失败是指你已经有了可执行程序,当你运行它的时候,它crash了,挂了,死了,kick the bucket了。一个你最经常看见的输出就是访问越界内存带来的Segmentation Fault。所以请记住:男人头,女人腰,还有越界内存。这三样东西永远不要摸,否则会挂掉。

比起结果失败,你会发现运行失败是多么美好的一件事。上面的if语句是C++中非常著名的一个结果失败的例子。你的程序编译正常,运行正常,就是结果不对。而且你还不知道原因所在。另外一个常见的原因就是C++的数值计算溢出。这在C++中是非常著名的“未定义”。 C++标准委员会乐观的认为如果我“未定义”,那么天才的编译器程序员会发明一种最聪明的方法来解决这个问题。但是实际的情况是:天才的编译器程序员是最懒惰的程序员!如果你“未定义”,那么他们就什么也不干。结果就是到底发生什么,鬼知道!!!

如果你还没看懂,没关系!我再给你举了生活的例子。一个程序员根本就没有女朋友。这个就类似于编译时失败。一个程序员有个女朋友,但是他第一天就去摸人家女孩子的腰。有些地方还真是和尚摸得,你却摸不得,结果女朋友愤然离去,这可以对应运行时错误。最后一种情况当然就是结果错误了。程序员有了一个漂亮的女朋友,第一天他去摸人家的腰,女孩子不仅没有生气,还娇羞的把头靠在程序员的肩旁上,对他说:“讨厌了,你刚才把人家摸怀孕了啦!”

现在的问题就是:如何能尽早的发现女朋友是否环孕?不对,我说错了,我是想说如何能够更早的发现失败?

首先,常见的失败的原因主要有两种,一种是缺陷(bug),另外一种是异常(exception, error)。好多人搞不清楚,所以我上段代码:
———————————
String key = name;
If(key = “Yan”){
handle = openFile(key);
salary = handle.read();
}
———————————

请注意,我说的缺陷就是bug的含义。也就是常说的虫子。缺陷主要有两种,设计缺陷和实现缺陷。上面的代码中,用名字来做为查找键值就是一种明显的设计缺陷。如果你在办公室喊一声“狗蛋”,会有好多同事答应的。另外一种实现缺陷我们上面介绍过,key==“Yan”写成了“key=Yan”。这些都会造成程序的失败。
那么我们如何能尽早的发现bug呢?一个很牛逼的工具就是unit test。不过C++也提供另外一个有力的工具,那就是assert。Assert用法是最简单的。下面我们看看它是如何发现我们的缺陷的。
—————————————
PrintSalary(string name, int salary){
Assert(name == “Yan” && salary<=200,000);
}
————————————-

Assert的基本理念可以应对成一句话:“反常必有妖”。上面这段代码的含义是如果发现Yan赚的钱超过了20万,那程序一定是哪里出现问题了。就像你发现你的名字出现在福布斯富豪榜上了,那富豪榜一定是印错了。你最好复查一下计算薪水的代码。如果你再阅读一下上面计算salary的代码,你就会发现是重名的设计缺陷和if的实现缺陷才让Yan的salary那么多。

关于assert,有三点需要说明,第一就是在哪里放assert和用assert验证什么? 一个基本的原则就是函数的各种输入和输出值,但是具体上则完全是根据具体的问题和程序员的经验。你可以把assert想象成传感器。要检验一辆车的质量,一个有经验的工程师知道哪里需要放传感器,用传感器检测什么。而工程师的女朋友会发现:这个颜色我不喜欢!

关于assert 的第二点就是在程序发布版中,assert是失效的。这个很好理解。一个车出厂前才需要用各种传感器来发现各种缺陷,一但发现缺陷,就应该找到缺陷的原因并修正它,直到零缺陷,你才应该把车出厂去买才对。如果用户去买车,发现车上都是传感器。这会影响用户的速度和体验;同时就算是用户发现车漏油这个问题,你让普通用户做什么呢?所以说assert只是工程师用于产品出厂前检测缺陷的,而不是用于最终产品的。当然,零缺陷只是传说,所以车厂有召回,软件公司有补丁。

Assert的第三点是现在c++支持static _assert了。它可以在编译的时候就发现问题,这印证了我前面说过的,编译失败要好于运行失败。
———————————–Static_assert(sizeof(int)>2)
————————————–

现在我们回来看看计算薪水的代码,看看OpenFile这一段,这里并不是缺陷,而是会发生异常。文件打不开有太多原因,权限不够,名字不对,别人给删掉了。所有这些原因都不是我们的程序能完全控制的。虽然这造成了程序的失败,但是这既不是设计缺陷,也不是实现缺陷,而是一种异常。

对于异常,我们应该用try catch捕捉到这种异常,然后根据上下文做出正确的反应。常见的异常多发生在IO,网络链接和用户交互上。它和assert一样,如果要用好很大程度上取决于具体的问题和程序员的经验。与assert不同的是,它也必须存在于产品的发布版本中。在车的例子中, 车漏油是缺陷,但是车胎扎了就是一种异常。在平时使用的时候,你也应该时刻准备着捕捉到这种异常,并做出正确的反应。这是它和缺陷最大的不一样的地方。
—————————————–
Assert(car doesn’t leak);

Try{
Drive on a nail;
}
Catch(“flat tire”){
Replace spare tire;
}
——————————————–

没看懂吗?没关系。让我们继续程序员和女朋友的故事吧。我们上文提到的程序员自从把女朋友摸环孕后有点怕怕了。他决定从日本买一个型号为“苍井-玛丽亚”的女朋友。拿到货后很快发现女朋友不仅漏气,而且漏电!漏气这个事,明显属于异常范围的。因为即使正常的使用,漏气也会正常地发生。所以一般厂家会提供一些胶水来应对这种情况。

但是漏电这明显是一个bug,厂家是应该用传感器在出厂前就发现并修正这一问题的。也就是说厂家是不应该把这一产品出厂来买的。你需要向厂家报告这一缺陷(bug)。正常的情况下,你会收到厂家的教科书式的回复:“It’s not a bug, It’s a feature!”。我还真的承认。女朋友漏电这个事,人家说是feature(特性),Make sense(没毛病)!

从程序员和女朋友的故事中,我希望你能记住以下几点:
1)编译失败好于运行失败,运行失败好于结果失败。
2)失败通常由缺陷和异常造成。
3)对于缺陷(bug),用assert和单元测试在开发期间尽早发现并修正。
4)对于异常(exception),用try catch 在产品使用的全程捕捉并处理。
5)程序员就不应该找女朋友!

用户评论