分类: 2010 June

我看见的那份C++

园子里语言之争又热了起来,我想不知天高地厚的说一下我所理解的C++.

首先应该定下一个论调,C++之父Bjarne Stroustrup在谈及C++的设计和演化时说到,”计算机和程序设计语言可以被当做是一种艺术性的工作,但审美主义因素应该是去辅佐或提升其有用性,而不是取代或损伤它.”,这段真知灼见客观而不偏激,相信赞同的人还是多数的.对一门语言,无论是拥护还是批判,出发点首先应该是爱之深或是责之切,光是动动嘴皮,挑起一滩浑水,是很不负责的.而在两种语言之间,比较很少是公平的,更多时候纯粹是无意义的.要做到公允,评论者应当不偏不倚,但局限于个人的认识,我们总是无知且不自觉地偏向于某个特定的领域,某种风格的程序设计或是程序员社区中的某种文化.

对C++的攻击主要集中在C++的存储管理上,而这些攻击更多地集中在C++缺乏一种有效的垃圾收集机制..也许,垃圾收集是C++所剩无几的可能加入还没有加入的特性了.其实,垃圾收集机制的确是一种很不错的思想,对用户而言,它能够简化设计,让库的设计和构造更加简单,而且可以排除掉许多错误的根源;对多数应用而言,垃圾收集比人肉存储管理更为可靠.但是,垃圾收集机制也不是尽善尽美的,它可能会导致

  1. 严重的空间浪费和时间开销,例如极端情况下的多米诺效应般的缺页导致的内存抖动.
  2. 垃圾收集机制的实现和移植将带来不可避免的复杂问题,例如某些垃圾收集方案可能提出了对对象布局和对象创建的一些限制.
  3. 一些隐含着服务中断的垃圾收集技术将使得C++不合适做许多低层次的工作,比如实时应用,设备驱动程序,操作系统内核上,而这却是C++的一大应用领域.

够了,已经有充分多的论据可以反对这种观点:每个应用在有了垃圾收集机制后悔变得更好.但是,也有足够多的证据可以反对另一种观点:没有那个应用因为有了垃圾收集机制而做得更好.Bjarne Stroustrup得到的一个结论是,”从原则上和可行性上,垃圾收集都是必须的.但是,对于今天的用户,普遍的C++使用和硬件,我们还无法承受将C++的语义和它的基本库定义在垃圾收集系统之上的负担.”当然,这段话是Bjarne Stroustrup在上古时代1994年说的了,我们并不清楚Bjarne Stroustrup至今是否仍旧这么认为的.至少,对多数程序员来说,垃圾收集实在是太有用了,它可以解放我们的心智,让我们的天赋脱离细节的纠缠,真正的将精力用在思想的历练和设计的考虑之上.

前面仅仅是一个C++新特性缺位的小例子,还有的一个例子可以是内置的数据库支持.实际上,为了公正的评价C++,我们应该看到它的设计目标:抽象.Java和C#的大行其道足够说明,这一设计目标本身是没有什么问题的.C++可以被看做是为了描述模块化而做的一种C的扩充,它并不是一门全新的试验性语言,而是一个几乎与C兼容的C的超集.Stephen Jay Gould说,任何纽带的连接力都比不上遗传链.为了兼容,C++继承了太多C的污浊,为了效益它也无法抛弃C的危险的丑陋的某些特性.这就使得C++的设计尽管解决了C的某些问题(Dennis M.Ritchile语),却又保留了C的一些问题.C++的过度设计和过度优雅多多少少有点宗教上的形而上了,它带给程序员的负担太多,而带来的新的思维方式又太少.比如说,D&E这本书让我觉得,为了抽象,C++被模板搞得支离破碎,更显艰涩难懂;而且C++的抽象一味强调了OO,这样的一味偏重将阻挡我们的视野,实际上,抽象就是为了看清本质,OO不是唯一的,function programming的想法也是相当不错的,这就限制了我们在更高维度上的思考,总的来说,我们可以说,C++迈出了很重要的一步,但对当代的设计架构来说,这一步又显得太小了.这样说也许有点强人所难了,但是,C++毕竟只是一门工具,尊重点说,它是无数牛人的智力产物,但是,时代总是在进步的,语言也是在进化的,我们没有必要抱残守缺,增长见识比起无端争论来的更为实际.

<The Practice of Programming>的作者Rob Pike在<Systems Software Research is Irrelevant>一文中痛斥当下操作系统的固步自封,不思进取.我不敢说C++是不思进取的,但是,正如Bjarne Stroustrup说的那样,C++语言和它背后的设计理念,编程思想本身是不会演化的,真正演化的是C++用户对于实际问题的理解,以及我们对于为了帮助解决这个问题而需要的工具的理解..对于C++这门语言来说,它已经太过庞大太过复杂了,所有的修改都需要经过极为谨慎的考虑,而特性的添加更是令人心惊胆颤.也许,C++已经开始走向了封闭,它只是一种选择的桥梁,远远不是一个完美的答案.

最后,上面的废话无非就是一种废话,还是希望自己用好C,用好C++.

新域名,好心情

测试feedburner是否已更新….

终于搬家了.

首先需要说明,我对政治没有丝毫的兴趣,GFW肆虐,我没有什么好说的,技不如人,努力学习就是了.只怪自己年 少无知啊,注册了godorz.cn,幸运的是,域名8月中就到期了,因此今天把博客比较完美的搬到了godorz.info,用的仍旧是以前的虚拟主 机.Btw,之前本来打算使用@zzzcn友 情提供的快速主机,但是没有Cpanel控制面板我真的不习 惯,导入和修改数据库都要用php代码来写是一件很悲剧的事,我不愿在自己不喜欢的事上多动脑筋.不管怎么样,还是得感谢@zzzcn.

Cpanel上有phpMyAdmin这种工 具,搬家就变成了很轻松地一件事,先把旧博客的文件打包到新博客,然后将旧博客的数据库导入到新博客,之后再用 UPDATE 表名 SET 字段 = REPLACE(字段, ‘旧域名’ , ‘新域名’); 语句修改一下文章,留言,图片(表 wp_options,wp_posts,wp_comments)等链接,然后搜索主题,将以前自己hack的统统更新,接下来要做的就是将在 feedburned托管的域名更新一下,最后在google网站管理员工具里验证新域名,之后提交xml,然后将以前的网址更新,google anlysist如法炮制,最后修改旧博客下的.htaccess文件,做个301转向,这样,域名就可以平稳的过渡了,一是不掉pr,二是不掉rss订 阅数,三是旧链接不死..

而这篇文章,就是提醒各位订阅http://godorz.cn/feed的童鞋,赶紧赶紧更新rss源吧..

垃圾收集

下面是看CSAPP时的读书笔记..Chapter10.10

垃圾回收

在C malloc这样的显式分配器中,我们可以使用malloc和free来分配,释放堆块.
在程序返回时,忘记释放已分配的块是一种很让人郁闷的事情.比如说,

void do()
{
	int *p = (int *) Malloc(10); // Malloc为malloc的包裹函数
	... // do something
	return;
}

函数do()分配了一块临时存储,当它不再需要p时,函数do()并未将其释放,这就导致了内存的泄露和浪费.按照UNPv2的说法,该临时存储是process-persistent的,它在程序的生命周期中都保持为已分配状态,毫无必要的占用着堆空间.

问题总是需要解决的.垃圾收集器(garbage-collector)就是其中的一个解决方案.它是一种动态存储分配器,功能是自动释放程序不再需要的已分配块(即内存垃圾),自动回收堆存储的这个过程就叫做垃圾收集(garbage collection).

垃圾收集的历史是相当久远的,它早于C的出现,目前仍是一个重要的研究领域.垃圾收集的算法很多,下面讨论的是Mark&Sweep算法,它比较简单,可以建立在已成熟的malloc包的基础之上,为C和C++提供垃圾收集.

基本原理

首先,不加解释的给出一张linux进程的存储器映像图,

GC(即垃圾收集器)将存储器看做是一张有向可达图(directed-reachability-graph).在该图中,节点可以分为两组,一组是根节点(root node),一组是堆节点(heap node).其中,每个根节点对应于一个不在堆中的位置,每个堆节点对应于堆中的一个已分配块,有向边 p->q 说明块p中的每个位置指向块q中的某个位置..注意,这里所说的位置,既可以是寄存器,又可以是栈里的变量,甚至是虚拟存储器中读写数据局域内的全局变量(见前图).当存在一条从任意根节点到达p的有向路径时,p称为可达的(reachable),否则,p称为不可达的(unreachable),不可达节点所对应的位置,即为内存垃圾.概括起来,GC的角色就是维护这张有向可达图,并通过释放不可达的节点来回收内存垃圾.

在具体的实现中,我们可以将GC作为一个和应用并行的独立线程,在后台进行垃圾的收集.下面是一个将GC加入malloc的例子,

当应用程序调用malloc时,如果malloc找不到一个合适的空闲块,那么它就调用GC,GC识别垃圾,调用free函数将垃圾回收.

Mark&Sweep

由名字就可以知道,Mark&Sweep收集器可以分为两个阶段,标记(mark)和清除(sweep),其中,标记阶段标记处根节点的所有可达的和已分配的后继,而清除阶段释放每个未被标记的已分配块(即垃圾).

小例子:

每个块对应着存储器中的几个字,其中,块头部包含一个字,要么是已标记的,要么是未标记的.

(1)标记前:

在标记前,堆由6个已分配但未标记的块组成,根节点指向第4块,第4块包含指向第3块和第6块的指针,第3块包含指向第1块的指针.

(2)标记后:

在标记后,第1,3,4,6块被标记(可达),第2,5块仍然是未标记(不可达).

(3)清除后

在清除后,第2,5块被回收.

算法

mark函数如下:

typedef char * ptr;

void mark(ptr p)
{
	if((b = isPtr(p)) == NULL)
//如果p指向一个已分配块中的某个字,则返回指向这个块的起始位置的指针b,否则返回NULL.
		return;
	if(blockMarked(b)) //块b是否已标记
		return;
	markBlock(b); //标记块b
	len = length(b); //块b的字长,包括头部
	for(i=0; i<len; i++)
		mark(b[i]);
	return;
}

标记阶段为每个根节点调用一次mark函数,如果p不指向一个已分配但未标记的堆块,mark函数就立即返回,否则,它标记这个块,并对块中的每个字递归的调用自己.每次对mark函数的调用都标记某个根节点的所有未标记且可达的后继节点.在标记阶段结束后,未标记但已分配的块就是不可达的,为内存垃圾.

sweep函数如下:

typedef char * ptr;

void sweep(ptr b, ptr end)
{
	while(b<end)
	{
		if(blockMarked(b)) //块b是否已标记
			unmarkBlock(b); //将块b由已标记改为未标记
		else if(blockAllocated(b)) //块b是否已分配
			free(b);
		b = nextBlock(b); //返回堆中块b的后继
	}
	return;
}

sweep函数在堆中的每个块上循环,释放它所遇到的所有未标记的已分配块.

C程序的保守的Mark&Sweep

对C语言来说,Mark&Sweep收集器一定是保守的,即每个可达块都被正确的标记为可达,而一些不可达节点可能会被错误的标记为可达的.导致这一问题的原因,是C语言不会用类型信息来标记存储器位置.因此,像int这样的标量可以伪装成指针.一个很典型的例子是,假如某个可达的已分配块在它的有效载域中包含一个int(注意,mark函数会对块中的每个字递归的调用自己),其值碰巧对应于某个其他已分配块t的有效载域的一个地址.对收集器而言,它是无法断定这个数值到底是int还是一个指针的.因此,它只能保守地将t标记为可达的,尽管事实上t可能是不可达的.

ps: 截图真是悲剧啊..
ps2: 博客域名随时可能改为godorz.info,感谢无敌的@zzzcn