一个库接口实例-摘自《C++沉思录》Andrew Koenig

       为什么向不熟悉的人解释抽象数据类型(ADT)会是一件很有挑战性的事情呢?因为很难找到一个与实际情况一样复杂,但又小巧易懂的例子。毕竟,数据抽象的目的就是控制复杂度,所以要找一个简单的例子简直是自相矛盾。

       尽管如此,还是有一个很好的例子,就是一个得到广泛应用的C库例程集,它能够检查文件系统目录的内容。C程序可以在种类繁多的操作系统中运行,而且大多数操作系统关于文件和目录的概念都相似。因此,我们可以泛泛的讨论这些概念,而不必具体针对某种操作系统。

       这个例子的意义在于着重说明我们如何对那些不直接支持数据抽象的语言中十分通用的约定,使用数据抽象来自动进行管理通过在类中隐藏这些约定,我们可以使用户免于处理它们。这样做不仅使类更简单,也增强了类的健壮性

       要知道C库例程是如何工作的,最简单的方法就是观察一个使用这些例程的程序:

/* 这是一个 C 程序 */
#include <stdio.h>
#include <dirent.h>

int main() {
	DIR *dp = opendir(".");
	struct dirent *d;

	while (d = readdir(dp))
		printf("%s\n", d->d_name);

	closedir(dp);
	return 0;
}

       C 程序通过两个分别叫做 DIR 和 struct dirent 的类型与库进行通信。指向 DIR 对象的指针被当作神奇的 cookies(magic cookies)--一种具有超自然力的、能使程序作某些事情的小对象。

       我们并不打算弄清楚 DIR 对象里面有什么东西。调用 opendir 就会获得一个 DIR 指针,我们把这个指针传递给 readdir 去读取一个目录条目,还把它传给 closedir 去释放由 opendir 分配的资源。

       对 readdir 的调用将返回一个指向 struct dirent 的指针,该 struct dirent 表示刚才读取的目录条目。另外,我们也不想知道 struct dirent 的完整内容,而只要知道它的一个成员是一个以 null 结尾的、包含了这个目录条目名字的字符数组,该数组叫做 d_name。

       这个范例程序工作的方式就是调用 opendir 来获得表示当前目录的神奇 cookies;反复调用 readdir 来从这个目录中取出并打印目录条目;最后调用 closedir 来清理内存。

       尽管这个小程序没有用到它们,但是为了完整性,我还是应该说说另外两个库函数。telldir 函数获得表示目录的 DIR 指针,并返回一个表示目录当前位置的 long 。函数 seekdir 获得 DIR 指针和由 telldir 返回的值,并将指针移动至指定位置。

 

1.   复杂问题

       所有这些看上去都非常简单,但是果真如此吗?先前的非正式描述忽略了几个真正的问题,它们会给实际程序员编写代码带来麻烦。下面我们来看看一些更重要的问题

       ×   如果目录不存在会怎样? 如果给 opendir 的是一个不存在的目录名,它不能直接死机了之--它必须做点什么。这种情况下通常返回一个空指针。这样做有助于程序检查到底有没有打开。想法还不错是吧,可是就带来了下一个问题。

       ×   如果传给 readdir 的参数是一个空指针会怎样? 如果在前面的程序中目录“.”不存在,就会出现这种情况。对于这个问题至少有两种答案。readdir 可能找不到空参数,此时我们很自然就会预料到内核转储或者其他的灾难性后果;或者,readdir 可以实施某种检查,并进行相应处理。后一种情况再次引起一个新问题。

       ×   如果传给 readdir 的参数既不是一个空指针也不是一个由 opendir 函数返回的值又会怎样? 这种错误是很难察觉的;要检测到这种错误需要构建存放有效 DIR 对象的表,每次调用 readdir 时都对该表进行搜索。这太复杂,并且相应的消耗也太大,所以C库例程通常不这样做。于是衍生出 readdir 返回结果的一个问题。

       × 对 readdir 的调用返回指向由库分配的内存块指针。什么时候释放这块内存?在这个例子中,对 readdir 的每次调用都返回一个保存在 d 中的指针值。如果程序照下面这样写会发生什么情况呢?

       d1 = readdir(dp1);

       d2 = readdir(dp2);

       print("%s\n", d1->d_name);

       我们怎样才能知道调用 readdir(dp2) 后指针 d1 是否还指向一个有效的位置?是否只有当 dp1 != dp2 时 d1 才有效?还是另有某个其他规则?弄清楚这段代码是否有效的唯一方法就是弄清楚哪些操作会使 d1 所指向的值无效,以及我们的实际做法如何。

 

2.   接口优化

       现在先不回答这些问题,我们先重新设计C++中的接口,以便在可能的地方不必考虑这些问题。我们将用对象取代那个神奇 cookie,并取消对指针的使用,从而实现对接口的重新设计。

       在 C 版本中我们看到的第一个神奇 cookie 是 DIR 指针;让我们把这个指针放到一个取名为 Dir 的类中。Dir 对象表示对目录的一次查看;C 版本中除了两个控制 DIR 指针的函数外,其他所有函数都应该变成 Dir 类的成员函数。那两个控制指针的函数分别是 opendir 和 closedir,必须对应于构造函数和析构函数。那么,类定义就与下面的类似:

class Dir {
public:
	Dir(const char*);
	~Dir();
	// 关于 read、seek 和 tell 的声明
};

       read、seek 和 tell 成员函数的参数及结果类型是什么?我们先解决 seek 和 tell,因为它们最简单;由于 C 版本采用了神奇 cookie,所以 C++ 版本应该用一个小型类来表示偏移量。这个类的对象表示目录内的偏移量,所以我们称之为 Dir_offset:

class Dir_offset {
	friend class Dir;
private:
	long l;
	Dir_offset(long n) { l = n; }
	operator long() { return l; }
};

       注意这个类没有公共数据。尤其是其构造函数也是私有的。因此,创建 Dir_offset 对象的唯一方法就是调用知道如何创建它的函数--推荐设为 Dir 类的成员函数。一旦我们有了这样的对象,当然就可以复制它,但是由于这个类的定义方式,用户不能直接探查该类的对象。

       Dir_offset 对象的唯一数据成员是一个对应于 telldir 返回的值的 long 对象。现在该讨论 read 函数来。由于一个非常重要的原因,C 版本返回一个指向 struct dirent 的指针;这样就可以通过返回一个空指针来标识到达目录尾部了。我们在这儿没有费力封装 dirent 结构体,而是改变 Dir 读取它的方法。通过使用 C++ 中...,我们可以以不同的方式检测是否到达目录尾部;为 read 提供一个表示可以放入其结果中的对象的参数,并让它返回一个表示读取是否成功的“布尔值”(实际上是一个整数)。

#include <dirent.h>

class Dir {
public:
	Dir(const char*);
	~Dir();
	int read(dirent &);
	void seek(Dir_offset);
	Dir_offset tell() const;
};

       有了这个接口,我们现在就可以如下所示重写范例程序:

#include <iostream>
#include <dirlib.h>

int main() {
	Dir dp(".");
	dirent d;

	while (dp.read(d))
		cout << d.d_name << endl;
}

       这里,头文件 dirlib.h 包括关于 Dir 和 Dir_offset 的声明。

 

3.   温故知新

       因为还没有用成员函数定义来充实 DIr 类,所以还不能运行这个程序。但是,我们已经知道一些如何改进程序的方法。

       首先,注意这个库的 C 版本在全局名称空间中加入了 7 个名字:DIR、dirent、opendir、closedir、readdir、seekdir、和 telldir。相反,C++版本只用了 Dir、dirent 和 Dir_offset。

       其次,我们发现程序的 C++ 版本根本不包括指针变量。特别的,d 是一个表示目录条目的对象,而不像 C 版本中那样是一个指向这样的对象的指针。因此,我们就去掉了一个可能有问题的类;没有使用指针的程序不会导致由于为定义指针而引起的崩溃。

       再次,因为 C++ 不需要在声明对象时在前面加上 struct 或者 class 关键字,所以关于 d 的声明就变得更简洁了。

       最后,C++ 版本回答了 C 版本没有回答的问题:

       1.   如果目录不存在会怎样?我们还是必须处理这个问题。实际上,使用 C++ 令我们更明确了要解决这个问题,因为类似下面的程序

       Dir d(some directory);

       d.read(somewhere);

必须做些有意义的事情:即使打开目录失败,d 也是一个对象。要注意确保 Dir 构造函数将它的对象置于一种恒定的状态,即使下一次对 opendir 底层调用失败也是如此。如果在库中一次性把这件事解决,使用这个库的人就不必担惊受怕的顾虑这个问题。

       另一种做法就是,如果我们被要求创建一个 Dir 对象,该对象指向一个不存在的目录,可以抛出一个异常。再一种可行的办法是允许创建 Dir 对象,但是对于读取它的请求要抛出异常。

       2.   如果传给 readdir 的参数是一个空指针会怎样?这在 C++ 版本中不再是问题;我们必须对某个对象调用 read,而那个对象必须是已经被创建的。

       3.   如果传给 readdir 的参数既不是一个空指针也不是一个由 opendir 函数返回的值又会怎样?这也不是问题,理由同上。

       4.   对 readdir 的调用返回一个指向由库分配的内存的指针。什么时候释放这些内存?我们让 read 读取用户提供的对象,而不是返回一个指针。这样就把内存分配的职责交给用户了,但是我们通过使用 read 来读取局部变量减轻了这个负担。

       显然,我们仅仅通过把这些例程改写成 C++ 的,就使它们更加健壮了,这主要是因为我们尝试将 C 接口中的底层概念转化成了 C++ 接口中的显式对象。

 

4.   编写代码

       应该注意的是我们已经明确了接口,设计它的实现应该不难。Dir 类封装了一个 DIR 指针,所以我们将在 Dir 类的私有数据中包括这个指针。我们还将通过赋值和初始化私有化使对 Dir 对象的复制无效:

class Dir {
public:
	Dir(const char*);
	~Dir();
	int read(dirent &);
	void seek(Dir_offset);
	Dir_offset tell() const;

private:
	DIR* dp;

	// 禁止复制
	Dir(const Dir &);
	Dir& operator=(const Dir &);
};

       我们不希望允许复制 Dir 对象,因为对一个对象进行读操作会影响另一个对象的状态。另外,复制一个 Dir 对象后,原来的 Dir 对象和副本都不得不被销毁。我们可以设计一个更复杂的 Dir 对象,使它适用于这种可能的情况;如何实现就留作读者自己联系。

       现在我们可以写成员函数了。构造函数调用 opendir:

       Dir::Dir(const char* file): dp(opendir(file)) {}

       因此,我们必须弄清楚如果 opendir 失败会发生什么情况。答案当然是 dp 将为空;我们必须记得在其他成员函数中检测这种情况,并做出相应处理。

       析构函数很简单--我们调用 closedir,除非打开失败:

Dir::~Dir() {
	if (dp)
		closedir(dp);
}

       如果打开失败,dp 将为 0.检测 dp 就是为了弄清楚打开是否成功,这样我们就不依赖底层 C 库是否正确的允许我们对一个没有指向打开的 Dir 的 Dir 指针调用 closedir。

       seek 和 tell 函数也简单;我们调用 seekdir 或者 telldir。唯一的问题就是如果打开失败,从 tell 返回什么。幸运的是,返回什么无关紧要,因为任何相应的 seek 都不会针对错误的发现做任何反应:

void Dir::seek(Dir_offset pos) {
	if (dp)
		seekdir(dp, pos);
}

Dir_offset Dir::tell() const {
	if (dp)
		return telldir(dp);
	return -1;
}

       最后,我们就有了 read 成员。这是所有成员函数里面最复杂的一个,但还是相当简单:

int Dir::read(dirent & d) {
	if (dp) {
		dirent* r = readdir(dp);
		if (r) {
			d = *r;
			return 1;
		}
	}
	return 0;
}

       我们遵循了对于错误返回 0 以及对于成功返回 1 的约定。这段代码首先检查打开是否失败,如果失败立即返回  0。然后调用 readdir 来读取一个目录条目;如果得到一个,则马上复制这个条目到调用者提供的 dirent 对象中。于是我们就回答了前面的问题;读取一个不存在的目录的行为类似于该目录根本没有条目。

       将 *r 的值复制到用户的空间就使用户不必在担心 *r 的生存期,这不仅因为当读取关于 struct dirent (*r 类型)的描述时,我们知道它不依赖于任何位于该结构体外的任何成员。如果不是这种情况,就必须在 C++ 中用一个动态字符串类定义一个独立的 dirent 类,而不是用 C 版本的 dirent 结构体。

       如果有个函数能够显式的检查 Dir 对象是否成功的打开了它的底层目录就更好了。这个函数--并不比我们已经在这里见过的函数难--就留作练习。

       顺便提醒一点,就是可以通过内联 Dir 成员函数减小这个接口原本已经很小的开销

 

5.   结论

       这个库的 C++ 接口是对 C 接口以一种很有效的方法稍微加以改进得到的。所有这些改进都得益于数据抽象的观念;如果对某个类对象的所有单个操作都将对象置于一种合理的状态,那么对象的状态就会始终保持合理

       C 接口具有几种我们前面的问题暴露出来的隐藏约定。不遵守这些约定的程序运行起来可能会出现奇怪的情况从而导致失败。使这些约定显式的作为接口的一部分,我们可以更早检测到错误,程序员工作起来也会更有信心。

 

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值