这篇我们将从一个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;
}
这个例子的意义在于:我们如何对那些不支持数据抽象的语言中十分通用的约定,使用数据抽象来自动管理。
上面的例子中,我们通过opendir()
函数返回一个DIR*
类型的变量,这个变量在后续的所有操作中都有出现,例如:readdir(dp)
,closedir(dp)
。这个变量dp
充当了一种神奇的cookies,即:它使得库函数知道具体操作的是那个目录。
这个看似非常普通的程序,却包含了一些隐含的约定。首先看看我们可以针对这个例子提出哪些问题。
复杂的问题
- 如果传给
opendir
的字符串指定了一个不存在的目录会怎样?按照我们习惯,opendir
应该是返回了一个空指针。这样可以方便用户进行检测。但是这样就有了下面的问题。 - 如果
readdir()
接受了一个空指针的DIR*
会怎样?这里可能有两种情况:1. readdir直接使用了这个空指针,那么将导致coredump。2. readdir进行了某些检查,并进行了对应的处理。如果是第二种情况,又会导致下面的问题。 - 如果
readdir()
接受的参数既不是空指针,又不是opendir()
的返回值会发生什么?这种错误很难被发现。因为readdir()
几乎没有办法检查传入参数是否为合法值。针对readdir()
的返回值,也有下面的问题。 - 对
readdir()
返回的结果,什么时候释放掉申请的内存呢?如果发生下面的情况:
d1 = readdir(dp);
d2 = readdir(dp);
printf("%s\n", d1->d_name);
这个时候d1
还是有效的吗?为了解决这个问题,我们还需要弄清楚哪些操作会导致d1
失效,以及我们应该怎么使用这些库函数。
上面的分析可以得出,我们在使用C库函数的时候,已经遵守了一些隐含的约定。下面我们来看看,使用C++重新进行封装会不会减轻用户的负担。将这些约定通过数据抽象隐藏起来。
优化接口
首先我们针对这个DIR
神奇的cookies进行优化,我们将它修改为一个类。这样的话,opendir
和closedir
就可以分别对应于构造函数和析构函数。readdir
则作为成员函数。我们还可以根据C库提供的telldir()
和seekdir()
两个函数添加相应的方法到类Dir
中。
#include <dirent.h>
class Dir_offset;
class Dir {
public:
Dir(const std::string& s):dp(opendir(s.c_str())) {}
~Dir() {
if (dp != nullptr)
closedir(dp);
}
Dir(const Dir&) = delete;
Dir& operator=(const Dir&) = delete;
bool read(struct dirent&);
void seek(Dir_offset);
Dir_offset tell() const;
private:
DIR* dp;
};
class Dir_offset{
friend class Dir;
private:
long l;
Dir_offset(long n) : l(n) {}
operator long() { return l; } // 类型转换
};
下面我们实现一下Dir未实现的接口。
bool Dir::read(struct dirent& d) {
if (dp) {
struct dirent* r = readdir(dp) ;
if (r != nullptr) {
d = *r;
return true;
}
}
return false;
}
void Dir::seek(Dir_offset pos) {
if (dp) {
seekdir(dp, pos);
}
}
Dir_offset Dir::tell() const {
if (dp)
return telldir(dp);
return -1L;
}
总结
经过C++的封装,现在暴露在外面的名称从好多个库函数变成了三个类:Dir
,Dir_offset
,dirent
。并且经过封装,我们将需要遵守的约定隐藏在了类定义中。例如:
如果想操作目录,我们需要一个Dir的对象,这个对象创建的时候一定会使用opendir
函数的返回值赋值给自身的成员变量。该对象析构的时候一定会使用closedir
对资源进行回收。调用read
成员函数通过d = *r
进行拷贝,用户也不再需要关心哪些操作会导致之前的赋值失效了。
通过类的封装,我们可以将通用的编程约定隐藏在类中,这样用户就只需要使用对象进行操作就行了。
数据抽象:如果对某个类对象的所有单个操作都将对象置于一种合理的状态,那么对象的状态就会始终保持合理。通过运用这个观念,我们可以将对象的状态变化同样封装在类中,这样我们的对象将一直处在合理的状态中(状态机)。