设为首页 加入收藏

TOP

1.4 进入C++
2013-10-07 00:08:51 来源: 作者: 【 】 浏览:85
Tags:1.4 进入

1.4  进入C++(www.cppentry.com)

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

当时,我想C++(www.cppentry.com)有这么几个特点对我有帮助。

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

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

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

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++(www.cppentry.com)使我得以从根本上在程序里更精确地表达我的意图。

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

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++(www.cppentry.com)修改ASD与用C修改相比较,前者得到的程序更简短,而所依赖的常规更少。作为例子,让我们回顾C++(www.cppentry.com) ASD程序。该程序的第一句是为m.s赋值:

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

当然,m.s是结构体m的一个元素,m.s也可以是更大的结构体的组成部分,等等。如果我必须自己记住要释放m.s的位置,就必然对两件事情有充分的心理准备。第一,我不会一次正确得到所有的位置;要清除所有bug肯定要经过多次尝试。第二,每次明显地改变某个东西的时候肯定会产生新的bug。
我发现使用C++(www.cppentry.com)就不必再担心所有这些细节。实际上,我在写C++(www.cppentry.com) ASD时,没有找到任何一个与内存分配有关的错误。


回书目   上一节   下一节

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇1.5 重复利用的软件 下一篇1.2 C语言的早期体验

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: