| | |
|
组别 | 士兵 |
级别 | 奋威校尉 |
好贴 | 1 |
功绩 | 6 |
帖子 | 143 |
编号 | 17767 |
注册 | 2004-9-16 |
| |
| | |
|
|
|
发信人: Purusa (木偶机器人), 信区: CPlusPlus
标 题: [C++珠玑]怎样写烂程序(1)
发信站: BBS 水木清华站 (Sun Mar 21 11:08:58 2004)
怎样写烂程序
作者: Andrew Koenig
对本文题目的惊鸿一瞥只会引起更多的问题而不是答案。可能有人会问,难道就算是写一
篇关于马拉客运软件的文章还有任何意义吗?不错,即使如此,本文讨论书写不干活的程
序的技术。我不想在这里解释为什么我要写这样的东西;那是我在JOOP上发表的相关文章
“什么时候写烂程序”的主题。为了讨论起见,让我们假设有时候真的有理由写一个烂程
序。
为了我们当前的目的,我们可以区分三类烂程序。一种烂到家的;另一种是时对时错的;
还有一种是看起来工作可靠实则有微妙缺陷的。我们的第一个任务是学习怎么写彻底烂到
家的程序。随后我们将审阅更微妙的出错技术。
--
乔峰吃了一惊:“好身手,这人是谁?”回掌护身,回过头来,不由得哑然失笑,
只见对面也是一条大汉单掌斜立,护住面门,含胸拔背,气凝如●,原来后殿的佛像
之前安着一座屏风,屏风上装着一面极大的铜镜,擦得晶光净亮,镜中将自己的人影
照了出来,铜镜上镌着四句经偈,佛像前点着几盏油灯,昏黄的灯光之下,依稀看到
是:“一切有为法,如梦幻泡影,如露亦如电,当作如是观。”
※ 来源:·BBS 水木清华站 http://smth.org·[FROM: 131.252.207.*]
发信人: Purusa (木偶机器人), 信区: CPlusPlus
标 题: [C++珠玑]怎样写烂程序(2)
发信站: BBS 水木清华站 (Sun Mar 21 11:11:12 2004)
连编译都不行的程序
写一个不能编译的程序毫无挑战性。大概最容易的方法,就是交给C++编译器某种甚至根本
不是C++程序的东西,比如任意的一堆文章。
然而,虽然根本不能编译的程序不是很有用,有时候它们还是很有趣的。例如,如果我把
我自己计算机上的C++编译器拿来,让它编译一个名为a.c的文件,里面只包含foo,那么输
出是::
CC a.c:
“a.c”, line 1: error: bad declaration of foo-did you
forget a ‘;’?
1 error
假设我们现在把这个有用的错误信息拿来当成一个程序如何?那看起来像一个好办法,可
以让编译器对我们说一些有趣的话。而结果,最终是::
CC a.c:
“a.c”, line 1: error: CC a : CC is not a type name
“a.c”, line 1: error: ‘.’ used for qualification;
please use ‘::’
“a.c”, line 1: error: body of non function c
“a.c”, line 2: error: syntax error
“a.c”, line 2: error: syntax error
“a.c”, line 2: error: bad declaration of line-did you
forget a ‘;’
“a.c”, line 2: error: syntax error
“a.c”, line 2: error: syntax error
“a.c”, line 2: error: body of non function error
“a.c”, line 2: error: bad declaration: bad is not a type name
“a.c”, line 2: error: declaration of :
declaration is not a type name
“a.c”, line 2: error: of foo : of is not a type name
“a.c”, line 2: error: syntax error
“a.c”, line 2: error: did you : did is not a type name
Sorry, too many errors
14 errors
倒数第二行很有趣:这个特定编译器在某个程序中遇上多于某个数目的错误以后就扔毛巾
(认输)了。
这种行为在很多场合证明是有用的。通常当一个程序产生如此多的编译错误,其中至少有
一部分是由编译器试图猜测如何纠正前面的错误引起的。
通常最好先修正编译器发现前两个错误,然后再试一遍,然后再试图一次修正它们。假设
我们把上面的诊断输出信息拿来再编译一次如何?那样我们就得到::
CC a.c:
“a.c”, line 1: error: CC a : CC is not a type name
“a.c”, line 1: error: ‘.’ used for qualification;
please use ‘::’
“a.c”, line 1: error: body of non function c
“a.c”, line 2: error: syntax error
“a.c”, line 2: error: syntax error
“a.c”, line 2: error: bad declaration of line-did you
forget a ‘;’
“a.c”, line 2: error: syntax error
“a.c”, line 2: error: syntax error
“a.c”, line 2: error: body of non function error
“a.c”, line 2: error: CC a : CC is not a type name
“a.c”, line 2: error: syntax error
“a.c”, line 2: error: CC is : CC is not a type name
“a.c”, line 2: error: is not : is is not a type name
“a.c”, line 2: error: not a : not is not a type name
Sorry, too many errors
14 errors
这已经非常类似前次尝试的输出了,虽然不可否认它还不完全一样。然而,如果我们重复
这个过程,编译器的输出真的完全与其输入一样。
我们刚刚得到了一个不寻常的自生成程序: 一个导致编译器生成的诊断信息与其本身一样
的程序。我们实际上把一个没用的程序转换成了一个有用的,只需改变我们对这个程序要
干什么的期望。换言之,我们写了一个甚至无需运行的自生成C++程序。
--
乔峰吃了一惊:“好身手,这人是谁?”回掌护身,回过头来,不由得哑然失笑,
只见对面也是一条大汉单掌斜立,护住面门,含胸拔背,气凝如●,原来后殿的佛像
之前安着一座屏风,屏风上装着一面极大的铜镜,擦得晶光净亮,镜中将自己的人影
照了出来,铜镜上镌着四句经偈,佛像前点着几盏油灯,昏黄的灯光之下,依稀看到
是:“一切有为法,如梦幻泡影,如露亦如电,当作如是观。”
※ 来源:·BBS 水木清华站 http://smth.org·[FROM: 131.252.207.*]
发信人: Purusa (木偶机器人), 信区: CPlusPlus
标 题: [C++珠玑]怎样写烂程序(3)
发信站: BBS 水木清华站 (Sun Mar 21 11:13:29 2004)
能编译但是不能链接的程序
产生无法编译的程序太容易了,一点没有挑战性。一个多少有趣的问题是写一个能通过编
译器但是导致链接器抱怨的程序。
最简单的这样一个程序是一个空文件。技术上来讲,这不是一个合法的C程序,因为每一个
C源文件至少包含一个声明。然而,这是一个词法正确的C++程序,而且很多C编译器也会接
受。
它不链接的原因,当然,在于main没在任何地方定义。显然找到这点不是什么挑战,所以
让我们看是否可能写一个以更微妙的方式链接失败的程序。
链接器的目的是匹配声明和定义。如果一个源文件声明了某物而链接器没找到定义,它就
抱怨。当然,仅声明某物是不够的:通常它还必须被用到。因此,譬如,程序::
extern void foo();
main()
{
}
将愉快地链接即使foo没在任何地方定义;该定义是不必要的除非foo实际上被调用了。即
使一个调用的确出现了,某些编译器会足够聪明地发现在某些情况下该调用永不被执行。
例如,我用的编译器接受
extern void foo();
main()
{
if (0)
foo();
}
不带一声抗议,而我的链接器也能正常地链接。
如果未定义的东西是一个虚拟函数,情形就变得更有趣了。编译器一般无法决定一个虚拟
函数是否会被调用,因为这样的调用可以是间接的。
因此如果我们写类似这样的东西::
struct Base {
virtual void f();
};
struct Derived: Base {
void f();
}
一个不包含直接引用Derived::f的程序仍可以通过一个类Base的对象来调用。
因此如果一个类的任何对象被生成的话,不管它们(函数或者对象)是否被使用,其虚拟函
数必须总是被定义。
我们可以利用这一点来写一个不链接的程序:
struct A {
virtual void f();
};
main()
{
A a;
}
这里我们遇上了有利可图的发现,因为用该编译器编译这个程序在这里导出了一条看起来
与问题完全无关的信息::
ld: Undefined symbol
____vtbl__1A
我们忽视了定义A::f,这条诊断信息跟那有什么关联呢?
最终弄清楚了这个特定的编译器优化虚拟函数表的生成。从实现的观点来看,基本的想法
是每个,比如说,A类的对象包含一个指向一个虚表作为该对象属于类A的标识。该优化在
于,对包含声明类A的每一个源文件避免了编译器不得不为其产生该虚表的内容。它是通过
选取一个特定的源文件并只在该源文件里产生虚表来做到优化的。
要做到这个,它得知道有一个特殊的源文件总会存在。它通过对每一个类定义寻找一个“
奇幻”函数来获得该保证,然后在定义该特定函数的源文件中定义该虚表。这个特定的编
译器选取第一个不内联的虚函数所在的文件。
因此当编译器看见
struct A {
virtual void f();
}
它意识到一定在某个源文件里存在A::f的单一定义,所以当编译该源文件的时候它会同时
生成A的虚表的定义。
当我们未书写该源文件时,我们就愚弄了编译器,使得虚表没有得到定义。当此发生时,
链接器在还没遇上对f的调用的时候就先遇上了该问题,也可能是实际上根本不存在对f的
调用。
因此我们看到甚至某些明显和链接器错误一样简单的东西有时候会表现出其实不是那么简
单。
--
乔峰吃了一惊:“好身手,这人是谁?”回掌护身,回过头来,不由得哑然失笑,
只见对面也是一条大汉单掌斜立,护住面门,含胸拔背,气凝如●,原来后殿的佛像
之前安着一座屏风,屏风上装着一面极大的铜镜,擦得晶光净亮,镜中将自己的人影
照了出来,铜镜上镌着四句经偈,佛像前点着几盏油灯,昏黄的灯光之下,依稀看到
是:“一切有为法,如梦幻泡影,如露亦如电,当作如是观。”
※ 来源:·BBS 水木清华站 http://smth.org·[FROM: 131.252.207.*]
发信人: Purusa (木偶机器人), 信区: CPlusPlus
标 题: [C++珠玑]怎样写烂程序(4)
发信站: BBS 水木清华站 (Sun Mar 21 11:16:08 2004)
不运行的程序
因为在C++中的编译和链接检查相当严格,写不能通过这些检查的程序,远没有写那些可以
一路过关运行直到失败的程序来得有趣。
即使在这里,引起眩目的失败也是轻而易举的事。随着失败变得更微妙,产生它们就变得
更有趣。最有趣的程序就是那些看起来工作但是实际上只是因为偶然才工作的程序。
让我们从简单事情着手:一个消耗所有内存的程序。
void loop() { loop(); }
这个小程序不干别的事,就只管调用自己。在很多C++实现中,那将消耗掉无限的内存,因
为实现将保持每一个当前调用的记录。然而,在某些实现中,这个函数将实际上永远地运
行下去,因为该编译足够聪明以至于它能认出尾递归。一个尾递归就是当一个函数在返回
前干的最后一件事情就是调用自己。这样的递归一般可以写成一条goto语句,使得我们的
loop函数等价于
void loop()
{
spin: goto spin;
}
从写烂程序的角度来说,尾递归是有用的,因为它们导致在某些编译器上行而在另一些编
译器不行的程序,特别是在缺乏大量内存的机器上。
正如所发生的,一些尾递归难以优化。优化依赖于在调用新函数之前废弃旧函数所有内存
的能力。但是,假设我们打算保留一个指向该内存的指针,比如说将其作为递归调用的参
数传递又如何?譬如,考虑下面这个有些故意设计的程序,接受两个整数,m和n,并返回
m乘以n的阶乘::
int timesfact(int m, int n)
{
if (n==0)
return m;
return timesfact(m*n, n-1);
}
该函数是一个完全合法的尾递归,可以重写成这样::
int timesfact(int m, int n)
{
while (n != 0)
m *= n--;
return m;
}
然而,如果我们把原来的程序拿过来,把它改成*指针*作为第一个参数::
int timesfact(int *m, int n)
{
int mn;
if (n==0)
return *m;
mn = *m * n;
return timesfact(&mn, n-1);
}
这样即使该函数仍然具有尾递归的形式,却不再可能进行尾递归优化了。这样的函数有很
大的机会羁绊优化,特别是因为很少有人在实际中这么写程序。
当然,这个例子不是很公平,因为它依赖于编译器来制造麻烦而不是直接制造麻烦。一个
直接得多的使程序出错的办法就是利用边界错误。
--
乔峰吃了一惊:“好身手,这人是谁?”回掌护身,回过头来,不由得哑然失笑,
只见对面也是一条大汉单掌斜立,护住面门,含胸拔背,气凝如●,原来后殿的佛像
之前安着一座屏风,屏风上装着一面极大的铜镜,擦得晶光净亮,镜中将自己的人影
照了出来,铜镜上镌着四句经偈,佛像前点着几盏油灯,昏黄的灯光之下,依稀看到
是:“一切有为法,如梦幻泡影,如露亦如电,当作如是观。”
※ 来源:·BBS 水木清华站 http://smth.org·[FROM: 131.252.207.*]
发信人: Purusa (木偶机器人), 信区: CPlusPlus
标 题: [C++珠玑]怎样写烂程序(5)
发信站: BBS 水木清华站 (Sun Mar 21 11:17:19 2004)
由于偶然性而看起来工作的程序
考虑,比如,下面这个程序::
main()
{
int a[10], i;
for(i=0; i<=10; i++)
a = 0;
}
在某些机器上,这个程序看起来可以正常运行和结束。在其它机器上,它会进入一个无限
循环。原因,当然是,数组a的下标从0到9,但是循环里改变了a[10]的值。
在某些机器上,a[10]“元素”占用了一块正好没被任何其它地方使用的内存,所以这个程
序看起来能工作。在其它机器上,a[10]恰好和i所在的内存所在一样,所以当i=10时语句
a[I]=0;把i又设回了0。这样又重新开始了整个循环。
当然,在C和C++中指针和下标之间的整个关系是如此根深蒂固,很难想象改变它又不带来
一整个新的语言。
在C++里,可以把数组操作封装在类里,使得进行检查容易多了,同时能做出更高级的数据
结构。然而,如果你的目标就是要写有时候能工作而其它时间不工作的程序,很难找到比
粗心使用数组和指针更好的办法。
作为另外一个例子,假设你要实现一个数据结构,存储类Thing的对象的一个变长数组。当
然,我们可以使用一个类来把该数组对其用户隐藏起来,但是我们怎么去实现呢?
做这个的干净的办法就是定义一个类,除了其它东西以外,还包括了该数组的长度以及其
起始元素的地址::
class Thingarray {
// …
Thing* data;
int length;
// …
};
然后象这样分配一个数组::
Thingarray:ingarray(int n) :
data(new Thing[n]), length(n) {}
然而,要记住,我们这里是要写不可靠的程序。这么做的一个常见技术是坚持把length在
内存中的位置相邻放在Thing的对象们的旁边。例如,想象这样的一个结构::
struct Buggyarray {
int length;
Thing t[1];
};
现在我们使类Thingarray指向一个Buggyarray::
class Thingarray {
// …
Buggyarray* data;
// …
};
然后我们可以象下面那样分配一个Thingarray::
Thingarray:ingarray(int n)
{
data = (Buddyarray*) malloc
(sizeof(Buggyarray) + n * sizeof(Thing));
data->length = n;
new (data->t) Thing[n];
}
严格来说,这个技术是非法的,因为它访问了在Buggyarray对象范围之外的内存。然而,
因为该内存被显式算进了对malloc的调用里,它大约在很多系统下都能工作。
这一技术提供了一些真正漂亮的将在稍后失败的机会。例如,如果你试图用一个带良好排
错支持的C++编译器来运行它,该编译器将正直地抱怨超越Buggyarray::t的边界进行访问
的企图。这完全是有好处的,因为屏蔽使用这样的编译器会使得其它漏洞更加难以寻找。
不仅如此,如果有人甚至在Buggyarray中引入虚函数,结果很可能会变成一塌糊涂。原因
在于C++实现一般会在每个其类带虚拟函数的对象中存储有关虚拟函数的信息。对于一个像
Buggyarray这样的类,信息看起来不像是会放在对象的尾部。所以编译器翻译下面的声明
::
struct Buggyarray {
int length;
Thing t[1];
};
为大概是这样效果的某物::
struct Buggyarray {
int length;
Thing t[1];
void *__virtual_table_pointer;
};
现在任何修改Buggyarray::t成员之后的数据的企图都会痛殴虚表指针。为该指针寻找适宜
的有创造性的值作为练习留给读者。
结论
写烂程序的技术要比描述它们所需要的空间多得多。本文仅仅是抛砖引玉。记住,写一个
轰然崩溃的程序几乎没有什么挑战性。
真正的乐趣在于写些看起来工作,实则隐藏了一个严重问题并仅在后来才暴露出来的程序
。
当然,C++提供了一套工具使得这样晦涩的问题不太容易出现。如果不是直接使用下标和指
针,而是使用一个设计良好的类库,很多这样的隐晦问题就变得明显。处理这些的办法之
一就是避免使用库。
为了同样的目的,避免能排错的编译器,以及任何能使这些问题容易找到的工具。否则你
可能会无意地得到一个实际上能工作的程序。那样还有什么挑战性呢?
--
乔峰吃了一惊:“好身手,这人是谁?”回掌护身,回过头来,不由得哑然失笑,
只见对面也是一条大汉单掌斜立,护住面门,含胸拔背,气凝如●,原来后殿的佛像
之前安着一座屏风,屏风上装着一面极大的铜镜,擦得晶光净亮,镜中将自己的人影
照了出来,铜镜上镌着四句经偈,佛像前点着几盏油灯,昏黄的灯光之下,依稀看到
是:“一切有为法,如梦幻泡影,如露亦如电,当作如是观。”
※ 修改:·Purusa 於 Mar 21 11:20:27 2004 修改本文·[FROM: 131.252.207.214]
※ 来源:·BBS 水木清华站 http://smth.org·[FROM: 131.252.207.*]
|
|
|
|