C++痛恨者手册(有意思:)

C++
90年代的COBOL

---------------------------------------------------------------------------
---
问:"C"和"C++"的名字是怎么来的?
答:这是他们的成绩

——Jerry Leichter

再没有比C++更能体现Unix“绝不给用户好脸”的哲学思想的了。

面向对象编程可以追溯到60年代的Simula语言,在70年代的Smalltalk语言上得
到极大发展。许多书会告诉你面向对象语言如何能提高编程效率,使代码更健壮,
和减少维护费用。不过你甭想在C++里得到这些。

这是因为C++根本就没理解面向对象的实质。非但没有简化什么,反而增加了更
多的复杂性。和Unix一样,C++从没被好好设计过,它从一个错误走向另一个错
误,是件打满补丁的破衣服。连自己的语法都没有严格定义(没一个语言敢这样),
所以你甚至无法知道一行代码是不是合法。

把C++比做COBOL,其实是对COBOL的污辱。COBOL在那个时代的技术条件下,是做
出了很不同凡响的贡献的。如果有谁用C++做成过什么事,那就算是很不同凡响
了。幸运的是,很多不错的程序员知道必须尽量避免C++的伤害,他们只用C,对
那些荒唐费解的功能敬而远之。通常,这意味着他们必须自己写个非面向对象的
工具,以获得自己想要的功能。当然,他们的代码会显得极为怪异,失去兼容性,
难于理解和重用。不过只要有一点儿C++的味道,就足够说服头头批准他们的项
目。

许多公司已经被多年遗留下来的混乱难懂的COBOL代码搞得焦头烂额了。那些转
而使用C++的公司刚刚意识到自己上了当。当然,这已经太晚了。软件灾难的种
子已经播下了,浇水施肥,得到悉心照料,就等着十几年后长成参天大树了。等
着瞧吧!



面向对象的汇编语言

C++没有一丝一毫高层次语言的特性。为什么这么说?让我们看看高层次语言应
该具备那些特性:

 


优雅:在表示方式及其所表达的概念之间有着简单易懂的关系

抽象:高层次语言的每个表达式只表示一个概念。概念能够被独立表达?br>⒛茏杂勺
楹稀?

强大:高层次语言的能够对任何精确完整的程序行为进行提供直接了当?br>拿枋龇绞健?


高层次语言使程序员能够采用问题空间的方式描述解决方案。高层次的程序很容
易维护,因为它们的目的性(intent)十分明确。根据一行高层次程序代码,现代
编译器能够为各种平台生成高效的代码,所以高层次程序的可移植性和可重用性
自然会很强。

使用低层次语言则需要对考虑无数细节,其中大部分是和机器内部操作有关的东
西,而不是要解决的问题本身。这不但造成代码难于理解,而且很容易过时。现
在几乎每隔今年就要更新系统,过时的必须花费很高代价修改低层代码或者彻底
重写。


对不起,你的内存泄漏了

高层次语言对于常见问题有内置解决方案。例如,众所周知内存管理是产生错误
最多的地方。在使用一个对象之前,你必须为它分配内存,适当进行初始化,小
心跟踪使用,并正确释放。当然,每件事儿都异常乏味而且很容易出错,极小的
一个错误可能会导致灾难性后果。定位和修改这类错误是臭名昭著的困难,因为
它们对于配置或使用方式的变化极其敏感。

使用未分配内存的结构指针会造成程序崩溃。使用未正确初始化的结构也会使你
的程序崩溃,不过不一定立刻完蛋。如果未能好好跟踪结构的使用情况,则很可
能释放一块还在使用中的内存。还是崩溃。最好再分配一些结构用来跟踪那些结
构对象。不过如果你太保守,不去释放那些不很肯定未在使用的内存,那么你可
要小心了。内存中很快就会充斥着无用的对象,直到内存耗尽,程序崩溃。这就
是恐怖的“内存泄漏”。

如果你的内存空间碎片太多,那该怎么办呢?解决办法是通过移动对象对内存重
新归整,不过在C++里没戏——如果你忘了更新对象的所有引用(reference),那
么你就会搞乱程序,结果还是崩溃。

很多真正的高层次语言提供了解决办法——那就是垃圾回收(garbage
collector)。它记录跟踪所有的对象,如果对象用完了会加以回收,永远不会出
错。如果你使用有垃圾回收功能的语言,会得到不少好处:

 

大量的bug立马无影无踪。是不是很爽呀?

代码会变得更短小更易读,因为它不必为内存管理的细节操心。

代码更有可能在不同平台和不同配置下高效运行。

 

唉,C++用户必须自己动手去拣垃圾。他们中的许多人被洗了脑子,认为这样会
比那些专家提供的垃圾回收工具更为高效,如果要建立磁盘文件,他们估计不会
使用文件名,而更愿意和磁道扇区打交道。手动回收垃圾可能会对一两中配置显
得更高效些,不过你当然不会这么去使用字处理软件。

你不必相信我们这里说的。可以去读一下B. Zorn的《保守垃圾回收的代价测量》
(科罗拉多大学Boulder分校,技术报告CU-CS-573-92),文中对程序员用C手动优
化的垃圾回收技术和标准垃圾回收器进行了性能比较,结果表明C程序员自己写
的垃圾回收器性能要差一些。

OK,假设你是个幡然醒悟的C++程序员,想使用垃圾回收。你并不孤单,很多人
认为这是个好主意,决定写一个。老天爷,猜猜怎么着?你会发现根本没法在
C++中提供其他语言内置的那样好的垃圾回收。其中一个原因是,(惊讶!)C++里
的对象在编译后和运行时就不再是对象了。它们只是一块十六进制的烂泥巴。没
有动态类型信息——垃圾回收器(还有调试器)没法知道任何一块内存里的对象究
竟是什么,类型是什么,以及是否有人此时正在使用它。

另一个原因是,即使你能写个垃圾回收器,如果你用了其他未使用垃圾
回收功能的代码,你还是会被干掉。因为C++没有标准的垃圾回收器,而且很有
可能永远也不会有。假设我写了一个使用了我的垃圾回收功能的数据库程序,你
写了一个使用你自己的垃圾回收功能的窗口系统。但你关闭一个装有我的数据记
录的窗口,你的窗口不会去通知我的数据记录,告诉它已经没有人引用它了。这
个对象将不会被释放,直到内存耗尽——内存泄漏,老朋友又见面了。


学起来困难?这就对了

C++和汇编语言很相象——难学难用,要想学好用好就更难了。


日期: Mon, 8 Apr 91 11:29:56 PDT
发信人: Daniel Weise
收信人: UNIX-HATERS
主题: From their cradle to our grave (从他们的摇篮到我们的坟墓)

造成Unix程序如此脆弱的一个原因是C程序员从启蒙时期就是这么被教
育的。例如,Stroustrup(C++之父)的《C++编程语言》第一个完整
程序(就是那个300K大小的"hello world"程序之后的那个)是一个英制/
公制转换程序。用户用结尾"i"表示英制输入,用结尾"c"表示公制输入。
下面是这个程序的概要,是用真正的Unix/C风格写的:


#include

main() {
[变量声明]
cin >> x >> ch;
;; A design abortion.
;; 读入x,然后读入ch。

if (ch == 'i') [handle "i" case]
else if (ch == 'c') [handle "c" case]
else in = cm = 0;
;; 好样的,决不报告错误。
;; 随便做点儿什么就成了。

[进行转换]

往后翻13页(第31页),给了一个索引范围从n到m的数组(而不是从0到
m)的实现例子。如果程序员使用了超出范围的索引,这个程序只是笑
嬉嬉地返回数组的第一个元素。Unix的终极脑死亡。

语法的吐根糖浆(Syrup of Ipecac,一种毒药)

语法糖蜜(Syntactic sugar)是分号癌症的罪魁祸首。
——Alan Perlis

在使用C编程语言中所能遇到的所有语法错误几乎都能被C++接受,成功编译。不
幸的是,这些语法错误并不总能生成正确的代码,这是因为人不是完美
的,他们总是敲错键盘。C一般总能在编译是发现这些错误。C++则不然,它让你
顺利通过编译,不过如果真的运行起来,就等着头痛吧。

C++的语法形成也和它自身的发展密不可分。C++从来没有被好好设计过:它只是
逐步进化。在进化过程中,一些结构的加入造成了语言的二义性。特别
的规则被用于解决这些二义性,这些难懂的规则使得C++复杂难学。于是不少程
序员把它们抄在卡片上以供不时之需,或者根本就不去使用这些功能。

例如,C++有个规则说如果一个字符串既可以被解析为声明也可以被解析为语句,
那么它将被当做声明。解析器专家看到这个规则往往会浑身发冷,他们知道很难
能正确实现它。AT&T自己甚至都搞不对。比如,当Jim Roskind想理解一个结构
的意思时(他觉得正常人会对它有不同的理解),他写了段测试代码,把它交给
AT&T的"cfront"编译器。Cfront崩溃了。

事实上,如果你从ics.uci.edu上下载Jim Roskind的开放C++语法,你会发现
ftp/pub目录里的c++grammar2.0.tar.Z有这样的说明:“注意我
的语法和cfront不一定保持一致,因为 a) 我的语法内部是一致的(这源于它的
规范性以及yacc的确证。b) yacc生成的解析器不会吐核(core
dump)。(这条可能会招来不少臭鸡蛋,不过...每次当我想知道某种结构的语法
含义是,如果ARM(Annotated C++ Reference Manual, 带注释的C++参考手册)对
它的表述不清楚,我就会拿cfront来编译它,cfront这时总是吐核(core dump))”


日期: Sun, 21 May 89 18:02:14 PDT
发信人: tiemann (Michael Tiemann)
收信人: sdm@cs.brown.edu
抄送: UNIX-HATERS
主题: C++ Comments (C++注释)

日期: 21 May 89 23:59:37 GMT
发信人: sdm@cs.brown.edu (Scott Meyers)
新闻组: comp.lang.c++
组织: 布朗大学计算机系

看看下面这行C++代码:

//********************** color=green>

C++编译器该如何处理它呢?GNU g++编译器认为这是一行由一堆星星
(*)组成的注释,然而AT&T编译器认为这是一个斜杠加上一个注释开始
符(/*)。我想知道哪个是正确解析方式,可是Stroustrup的书(《C++编
程语言》)里面却找不到答案。

实际上如果使用-E选项进行编译,就会发现是预处理器(preprocessor)
搞的鬼,我的问题是:

 


这是否AT&T预处理器的bug?如果不是,为什么?如果是bug,2.0版是否会得到修
正?还是只能这么下去了?

这是否GNU预处理器的bug?如果是,为什么?


Scott Meyers
sdm@cs.brown.edu

UNIX解析中有个古老的规则,尽量接受最长的语法单元(token)。这样'
foo'就不会被看成三个变量名('f', 'o'和'o'),而只被当成一个变量'
foo'。看看这个规则在下面这个程序中是多么的有用(还有选择'/*'作
为注释开始符是多么的明智):

 

double qdiv (p, q)
double *p, *q;
{
return *p/*q;
}


为什么这个规则没有被应用到C++中呢?很简单,这是个bug。

Michael


糟糕的还在后头,C++最大的问题是它的代码难读难理解,即使对于每天都用它
的人也是如此。把另一个程序员的C++的代码拿来看看,不晕才怪。C++没有一丝
品位,是个乱七八糟的丑八怪。C++自称为面向对象语言,却不愿意承担任何面
向对象的责任。C++认为如果有谁的程序复杂到需要垃圾回收,动态加载或其他
功能,那么说明他有足够的能力自己写一个,并且有足够的时间进行调试。

C++操作符重载(operator overloading)的强大功能在于,你可以把一段明显直
白的代码变成能和最糟糕的APL, ADA或FORTH代码相媲美的东西。每个C++程序员
都能创建自己的方言(dialect),把别的C++程序员彻底搞晕。

不过——嘿——在C++里甚至标准的方言也是私有的(private)。


抽象些什么?

你可能会觉得C++语法是它最糟糕的部分,不过当你开始学习C++时,就会知道你
错了。一旦你开始用C++编写一个正式的大型软件,你会发现C++的抽象机制从根
儿上就烂了。每本计算机科学教材都会这样告诉你,抽象是良好设计之源。

系统各个部分的关联会产生复杂性。如果你有一个100,000行的程序,其中每一
行都和其他行代码的细节相关,那你就必须照应着10,000,000,000种不同的关联。
抽象能够通过建立清晰的接口来减少这种关联。一段实现某种功能的代码被隐藏
在模块化墙壁之后发挥作用。

类(class)是C++的核心,然而类的实现却反而阻碍着程序的模块化。类暴露了如
此多的内部实现,以至于类的用户强烈倚赖着类的具体实现。许多情况下,对类
做一点儿改变,就不得不重新编译所有使用它的代码,这常常造成开发的停滞。
你的软件将不再“柔软”和“可塑”了,而成了一大块混凝土。

你将不得不把一半代码放到头文件里面,以对类进行声明。当然,类声明所提供
的public/private的区分是没有什么用的,因为“私有”(private)信息就放在
了头文件里,所以成了公开(public)信息。一旦放到头文件里,你就不大愿意去
修改它,因为这会导致烦人的重编译。程序员于是通过修补实现机制,以避免修
改头文件。当然还有其他一些保护机制,不过它们就象是减速障碍一样,可以被
心急的家伙任意绕过。只要把所有对象都转换(cast)成void*,再也没有
了讨厌的类型检查,这下世界清净了。

其他许多语言都各自提供了设计良好的抽象机制。C++丢掉了其中一些最为重要
的部分,对于那些提供的部分也叫人迷惑不解。你是否遇到过真正喜欢模板
(template)的人?模板使得类的实现根据上下文不同而不同。许多重要的概念无
法通过这种简单的方式加以表达;即使表达出来了,也没法给它一个直接的名字
供以后调用。

例如,名空间(namespace)能够避免你一部分代码的名字和其他部分发生冲突。
一个服装制造软件可能有个对象叫做"Button"(钮扣),它可能会
和一个用户界面库进行链接,那里面也有个类叫做"Button"(按钮
)。如果使用了名空间,就不会有问题了,因为用法和每个概念的意思都很
明确,没有歧义。

C++里则并非如此。你无法保证不会去使用那些已经在其他地方被定义了的名字,
这往往会导致灾难性后果。你唯一的希望是给名称都加上前缀,比如
ZjxButton,并但愿其他人不会用同一个名字。


日期: Fri, 18 Mar 94 10:52:58 PST
发信人: Scott L. Burson
主题: preprocessor (预处理器)

C语言迷们会告诉你C的一个最好的功能是预处理器。可事实上,它可能
一个最蹩脚的功能。许多C程序由一堆蜘蛛网似的#ifdef组成 (如果各
个Unix之间能够互相兼容,就几乎不会弄成这样)。不过这仅仅是开始。

C预处理器的最大问题是它把Unix锁在了文本文件的监牢里,然后扔掉
了牢门钥匙。这样除了文本文件以外,C源代码不可能以任何其他方式
存储。为什么?因为未被预处理的C代码不可能被解析。例如:


#ifdef BSD
int foo() {
#else
void foo() {
#endif
/* ... */
}


这里函数foo有两种不同的开头,根据宏'BSD'是否被定义而不同。直接
对它进行解析几乎是不可能的 (就我们所知,从来没实现过)。

这为什么如此可恶?因为这阻碍了我们为编程环境加入更多智能。许多
Unix程序员从没见过这样的环境,不知道自己被剥夺了什么。可是如果
能够对代码进行自动分析,那么就能提供很多非常有用的功能。

让我们再看一个例子。在C语言当道的时代,预处理器被认为是唯一能
提供开码(open-coded,是指直接把代码嵌入到指令流中,而不是通过
函数调用)的方式。对于每个简单常用的表达式,开码是一个很高效的
技术。比如,取小函数min可以使用宏实现:

#define main(x,y) ((x) < (y) ? (x) : (y))color=green>


假设你想写个工具打印一个程序中所有调用了min的函数。听上去不是
很难,是不是?但是你如果不解析这个程序就无法知道函数的边界,你
如果不做经过预处理器就无法进行解析,可是,一旦经过了预处理,所
有的min就不复存在了!所以,你的只能去用grep了。

使用预处理器实现开码还有其他问题。例如,在上面的min宏里你一定
注意到了那些多余的括号。事实上,这些括号是必不可少的,否则当
min在另一个表达式中被展开时,结果可能不是你想要的。(老实说,这
些括号不都是必需的——至于那些括号是可以省略的,这留做给读者的
练习吧)。

min宏最险恶的问题是,虽然它用起来象是个函数调用,它并不真是函
数。看这个例子:

a = min(b++, c);


预处理器做了替换之后,变成了:

a = ((b++) < (c) ? (b++) : (c)color=green>


如果'b'小于'c','b'会被增加两次而不是一次,返回的将
是'b'的原始值加一。

如果min真是函数,那么'b'将只会被增加一次,返回值将是'b'的原
始值。

 

C++对于C来说,就如同是肺癌对于肺

“如果说C语言给了你足够的绳子吊死自己,那么C++给的绳子除了够你上
吊之外,还够绑上你所有的邻居,并提供一艘帆船所需的绳索。”
——匿名

悲哀的是,学习C++成了每个计算机科学家和严肃程序最为有利可图的投资。它
迅速成为简历中必不可少的一行。在过去的今年中,我们见过不少C++程序员,
他们能够用C++写出不错的代码,不过...

....他们憎恶它。

 

程序员进化史

初中/高中


10 PRINT "HELLO WORLD"
20 END


大学一年级


program Hello(input, output);
begin
writeln('Hello world');
end.


大学四年级


(defun hello ()
(print (list 'HELLO 'WORLD)))


刚参加工作


#include
main (argc, argv)
int argc;
char **argv; {
printf ("Hello World!\n");
}


老手


#include

const int MAXLEN = 80;

class outstring;
class outstring {
private:

int size;
char str[MAXLEN];

public:
outstring() { size=0; }
~outstring() { size=0; }
void print();
void assign(char *chrs);
};
void outstring::print() {
int in;
for (i=0; i cout << str[i];
cout << "\n";
}
void outstring::assign(char* chrs) {
int i;
for (i=0; chars[i]!='\0'; i++)
str[i] = chrs[i];
size=i;
}

main (int argc, char **argv) {
outstring string;
string.assign("Hello World!");
string.print();
}


老板


“乔治,我需要一个能打印'Hello World!'的程序”

posted @ 2007-09-10 00:51  中土  阅读(1527)  评论(0编辑  收藏  举报
©2005-2008 Suprasoft Inc., All right reserved.