什么时候用C而不用C++?

为什么用C

此时,你可能会问自己:“他为什么要用C来做呢?”毕竟我所描述的簿记工作用其他的语言来写会容易得多,比如Smalltalk、Lisp或者Snobol,它们都有垃圾收集机制和可扩展的数据结构。

排除掉Smalltalk是很容易的:因为它不能在我们的机器上运行!Lisp和Snobol也有这个问题,只不过没那么严重:尽管我写ASD那会儿的机器能支持它们,但无法确保在以后的机器上也能用。实际上,在我们的环境中,C是唯一确定可移植的语言。

退一步讲,即使有其他的语言可用,我也需要一个高效的操作系统接口。ASD在文件系统上做了很多工作,这些工作必须既快又稳定。人们会同时发送成百上千个文件,这些文件可能有数百万个字节,他们希望系统尽可能快,而且一次成功。

进入C++

在那种情况下,我决定来看看能否用C++来解决我的问题。尽管我已经非常熟悉C++了,但还没有用它做过任何严肃的工作。好在Bjarne Stroustrup的办公室离我不远,在C++演化的过程中,我们曾经在一起讨论。

当时,我想C++有这么几个特点对我有帮助。

第一个就是抽象数据类型的观念。比如,我知道需要将向每台计算机发送软件的申请状态存储起来。我得想法把这些状态用一种可读的文件保存起来,然后在必要的时候取出来,且在与机器会话时应请求更新状态,并能最终改变标识状态的信息。所有这一切都要求能够灵活进行内存的分配:我要存储的机器状态信息中,有一部分是在机器上所执行的任何命令的输出,而这输出的长度是没有限定的。

另一个优势是Jonathan Shopiro最近写的一个用于处理字符串和链表的组件包。这个组件包使得我能够拥有真正的动态字符串,而不必在簿记操作的细节上战战兢兢。该组件包同时还支持可容纳用户对象的可变长链表。有了它,我一旦定义了一个抽象数据类型,比如说叫machine_status,就可以马上利用Shopiro的组件包定义另一个类型——由machine_status对象组成的链表。

为了把设计说得更具体一些,下面列出一些从C++版的ASD spooler中选出来的代码片断。这里变量m的类型是machine_status[1]:

struct machine_status {
    String p;                    // 机器名
    List<String> q;              // 存放可能的输出
    String s;                    // 错误信息,如果成功则为空
}
//…

m.s = domach(m.p, dfile, m.q);   // 发送文件
if (m.s.length() == 0) {         // 工作是否正常
    sendfile = 1;                // 成功——别忘了,我们是在发送一个文件
    if (m.q.length() == 0)       // 是否有输出
        mli.remove();            // 没有,这台机器的事情已经搞定
    else
        mli.replace(m);          // 有,保存输出
} else {
    keepfile = 1;                // 失败,提起注意,稍后再试
    deadmach += m.p;             // 加到失败机器链表中
    mli.replace(m);              // 将其状态放回链表
}

这个代码片断对于我们传送文件的每台目标机器都执行一遍。结构体m将发送文件尝试的执行结果保存在自己的3个域中:p是一个String,保存机器的名字;q是一个String链表,保存执行时可能的输出;s是一个String,尝试成功时为空,失败时标明原因。

函数domach试图将数据发送到另一台机器上。它返回两个值:一个是显式的;另一个是隐式的,通过修改第三个参数返回。我们调用domach之后,m.s反映了发送尝试是否成功的信息,而m.q则包含了可能的输出。

然后,我们通过将m.s.length()与0比较来检查m.s是否为空。如果m.s确实为空,那么将sendfile置1,表示我们至少成功地把文件发送到了一台机器上,然后来看看是否有什么输出。如果没有,那么可以把这台机器从需要处理的机器链表中删除。如果有输出,则将状态存储在List中。变量mli就是一个指向该List内部元素的指针(mli代表machine list iterator[机器链表迭代器])。

如果尝试失败,未能有效地与远程机器对话,那么将keepfile置为1,提醒我们必须保留该数据文件,以便下次再试,然后将当前状态存到List中。

这个程序片断中没什么高深的东西。这里的每一行代码都直接针对其试图解决的问题。跟相应的C代码不同,这里没有什么隐藏的簿记工作。这就是问题所在。所有的簿记工作都可以在库里被单独考虑,调试一次,然后彻底忘记。程序的其余部分可以集中精力解决实际问题。

这个解决方案是成功的,ASD每年要在50台机器上进行4000次软件更新。典型的例子包括更新编译器的版本,甚至是操作系统内核本身。较之C,C++使我得以在程序中从根本上更精确地表达我的意图。

我们已经看到了一个C代码片断的例子,它展示了一些隐秘的细枝末节。现在,我们来研究一下为什么C必须考虑这些细枝末节,然后再来看一看C++程序员怎样才可能避免它们。

C中隐藏的约定

尽管C有字符串文本量,但它实际上没有真正的字符串概念。字符串常量实际上是未命名的字符数组的简写(由编译器在尾部插入空字符来标识串尾),程序员负责决定如何处理这些字符。因此,尽管下面的语句是合法的;

char hello[] = "hello";

但是这样就不对了:

char hello[5];
hello = "hello";

因为C没有复制数组的内建方法。第一个例子中用6个元素声明了一个字符数组,元素的初值分别是‘h’、‘e’、‘l’、‘l’、‘o’和‘\0’(一个空字符)。第二个例子是不合法的,因为C没有数组的赋值。最接近的方法是:

char *hello;
hello = "hello";

这里的变量hello是一个指针,而不是数组:它指向包含了字符串常量“hello”的内存。

假设我们定义并初始化了两个字符“串”:

char hello[] = "hello";
char world[] = " world";

并且希望把它们连接起来。我们希望库可以提供一个concatenate函数,这样就可以写成这样:

char helloworld[];                          //错误
concatenate(helloworld, hello, world);

可惜的是,这样并不奏效,因为我们不知道helloworld数组应该占用多大内存。通过写成

char helloworld[12];                        //危险
concatenate(helloworld, hello, world);

可以将它们连接起来,但是我们在连接字符串时并不想去数字符的个数。当然,通过下面的语句,我们可以分配绝对够用的内存:

char helloworld[1000];                      //浪费而且仍然危险
concatenate(helloworld, hello, world);

但是到底多少才够用?只要我们必须预先指定字符数组的大小为常量,就要接受猜错许多次的事实。

避免猜错的唯一办法就是动态决定字符串的大小。因此,我们希望可以这样写:

char *helloworld;
helloworld = concatenate(hello, world);     //有陷阱

让concatenate函数负责判断包含变量hello和world的连接所需内存的大小、分配这样大小的内存、形成连接以及返回一个指向该内存的指针等所有这些工作。实际上,这正是我在ASD最初的C版本中所做的事情:我采用了一个约定,即所有字符串和类似字符串的值的大小都是动态决定的,相应的内存也是动态分配的。然而什么时候释放内存呢?

对于C的串库来说无法得知程序员何时不再使用字符串了。因此,库必须要让程序员负责决定何时释放内存。一旦这样做了,我们就会有很多方法来用C实现动态串。

对于ASD,我采用了3个约定。前两个在C程序中是很普遍的,第三个则不是。

1.字符串由一个指向它的首字符的指针来表示。

2.字符串的结尾用一个空字符标识。

3.生成字符串的函数不遵循用于这些串的生命期的约定。例如,有些函数返回指向静态缓冲区的指针,这些静态缓冲区要保持到这些函数的下一次调用;而其他函数则返回指向调用者要释放的内存的指针。这些字符串的使用者需要考虑这些各不相同的生命周期,要在必要的时候使用free来释放不再需要的字符串,还要注意不要释放那些将在其他地方自动释放的字符串。

类似“hello”的字符串常量的生命周期是没有限制的,因此,写成

char *hello;
hello = "hello";

后不必释放变量hello。前面的concatenate函数也返回一个无限存在的值,但是由于这个值保存在自动分配的内存区,所以使用完后应该将它释放。

最后,有些类似getfield的函数返回一个生存期经过精心定义但是有限的值。甚至不应该释放getfield的值,但是,如果想要将它返回的值保存一段很长的时间,我就必须记得将它复制到时间稍长的存储区中。

为什么要处理3种不同的存储期?我无法选择字符串常量:它们的语义是C的一部分,我不能改变。但是我可以使所有其他的字符串函数都返回一个指向刚分配的内存的指针。那么就不必决定是否要释放这样的内存了:使用完后就释放内存通常都是对的。

不让所有这些字符串函数都在每次调用时分配新内存的主要原因是,这样做会使程序十分巨大。例如,我将不得不像下面这样重写C程序代码段(见1.3.1节):

/* 读取八进制文件 */
param = getfield(tf);
mode = cvlong(param, strlen (param), 8);
free(param);

/* 读入用户号 */
s = getfield(tf);
uid = numuid(s);
free(s);

/* 读入小组号 */
s = getfield(tf);
gid = numgid(s);
free(s);

/* 读入文件名(路径) */
s = getfield(tf);
path = transname(s);
free(s);

/* 直到行尾*/
geteol(tf);

看来我还应该有一些其他的可选工具来减小我所写的程序。

使用C++修改ASD与用C修改相比较,前者得到的程序更简短,而所依赖的常规更少。作为例子,让我们回顾C++ ASD程序。该程序的第一句是为m.s赋值:

m.s = domach(m.p, dfile, m.q);

当然,m.s是结构体m的一个元素,m.s也可以是更大的结构体的组成部分;等等。如果我必须自己记住要释放m.s的位置,就必然对两件事情有充分的心理准备。第一,我不会一次正确得到所有的位置;要清除所有bug肯定要经过多次尝试。第二,每次明显地改变某个东西时肯定会产生新的bug。

我发现使用C++就不必再担心所有这些细节。实际上,我在写C++ ASD时,没有找到任何一个与内存分配有关的错误。

在使用你的程序时,如果因为不遵守规则而导致工作失败,大部分人不会反躬自省,反而会怪罪到你头上。C可以做好很多事情,但不能处理灵活多变的字符串。

C++版本的ASD spooler也使用字符/字符串函数,已经有人写过这些函数,所以我不用写了。和我当初发布C字符串规则比起来,编写这些函数的人更愿意让其他人来使用这些C++字符串例程,因为他不需要用户记住那些隐匿的规定。同样,我使用串库作为例程的基础来实现分析文件名所需的指定的模式匹配,而这些例程又可抽取出来用于别的工作。

本段截选自《C++沉思录

什么时候用C而不用C++?

 

我对C或C++ 都不了解,是不是应该先学习C?不,首先学习C++。C++ 的C子集对于C/C++ 的新手是比较容易学的,又比C本身容易使用。原因是C++(通过强类型检查)提供了比C更好的保证。进一步说,C++ 还提供许多小特征,例如运算符new,与C语言对应的东西相比,它们的写法更方便,也更不容易出错。这样,如果你计划学习C和C++(而不只是C++),你不应该经由C那条迂回的路径。为能很好地使用C,你需要知道许多窍门和技术,这些东西在C++ 里的任何地方都不像它们在C里那么重要、那么常用。好的C教科书倾向于(也很合理)强调那些你将来在用C做完整的大项目时所需要的各种技术。好的C++ 教科书则不太一样,强调能引导你去做数据抽象、面向对象的程序设计的技术和特征。理解了C++ 的各种结构,而后学习它们在(更低级的)C里替代物将会很简单(如果需要的话)。

要说我的喜好:要学习C,就用 [Kernighan,1988];要学习C++,就用[2nd]。两本书的优点是都组合了两方面内容:一方面是关于语言特征和技术的指导性的描述,另一方面是一部完整的参考手册。两者描述的都是各自的语言而不是特定的实现,也不企图去描述与特定实现一起发布的特殊程序库。

现在有许多很好的教科书和许多各种各样风格的材料,上面只是我对理解有关概念和风格的喜好。请仔细选择至少两个信息来源,以弥补可能的片面性甚至缺陷,这样做永远是一种明智之举。

没有无故的与C的不兼容性:C语言是有史以来最成功的系统程序设计语言。数以十万计的程序员熟悉C,现存的C代码数以十亿行计,存在着集中关注C语言的工具和服务产业。而且C++ 又是基于C的。这就带来一个问题:“C++ 的定义在与C相匹配方面到底应该靠得多么近?”C++ 不可能把与C的100% 兼容作为目标,因为这将危及它在类型安全性和对设计的支持方面的目标。当然,在这些目标不会受到干扰的地方,应该尽量避免不兼容性——即使这样做出的结果不太优雅。在大部分情况下,已经接受的与C语言的不兼容性,都出现在C规则给类型系统留下重大漏洞的地方。

在过去这些年里,C++ 最强的和最弱的地方都在于它与C的兼容性。这种情况不奇怪。与C兼容性的强弱将来也一直会是一个重要议题。在今后的年代里,与C的兼容性将越来越少地看作是优点,而更多变成一种义务。必须找到一条发展的道路(第9章)。

在C++ 之下不为更低级的语言留下空间(除汇编语言之外):如果一个语言的目标就是真正成为高级的——也就是说,它想完全保护自己的程序,使之避开基础计算机中丑陋且使人厌倦的细节——那么它就必须把做系统程序设计的工作让给其他语言。典型情况下,这个语言就是C。但另一种情况也很典型,C在许多领域中将取代这种高级语言,只要在这里控制或速度被认为是最关键的问题。常见情况是,这最终将导致整个系统完全用C语言来编写;或者是导致这样的一个系统,只有对两种语言都非常熟悉的人才能够把握它。在后一情况下,程序员常常会遇到一个艰难的选择:给定的任务究竟适合在哪个层次上做程序设计呢,他不得不同时记住两种语言的原语和准则。C++ 试图给出另一条路,它同时提供了低级特征和抽象机制,支持用这两种东西构造混合的系统。

为了继续成为一种可行的系统程序设计语言,C++ 必须保持C语言的那种直接访问硬件、控制数据结构布局的能力,保有那些能以一对一的风格直接映射到硬件的基本操作和数据结构。这样,它的替代品就只能是C或者汇编语言。语言设计的工作就是去隔离这些低级特征,使不直接操作系统细节的代码不需要用这些低级特征。这里的目标是保护程序员,防止出现无意中越界的偶然的错误使用。

本段截选自《C++语言的设计和演化》

什么时候用C而不用C++?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值