Linux使用C语言进行目录遍历操作所需要的相关函数及知识,在下面的链接中有详细的介绍,这里不再赘述。
下面主要介绍非递归写法
非递归实现遍历的主要思想就是使用栈或队列保存待遍历的元素。
如果是深度优先遍历,应使用栈保存;如果是宽度优先遍历,应使用队列保存。
算法的主要步骤如下:
- 将当前目录下的所有元素加入栈或队列中
- 如果栈或队列不为空,则重复执行下述操作:
- 取出一个栈或队列中的元素
- 如果该元素为文件,则正常访问;如果该元素为目录,则将该目录下的所有元素加入栈或队列
确定数据结构
- 队列
考虑到现实中的目录结构,这里采用队列实现。即优先访问当前目录下的文件,当文件遍历完成后,再去遍历当前目录下的子目录。 - 链表
由于文件及目录的数量未知,可能很多。如果使用数组作为队列的实现,可能出现数组长度不够的情况。因此采用更为灵活的链表来实现队列。 - 双向链表
队列先进后出的特点要求在对链表进行操作时,要在头部弹出、尾部插入。由于需要使用尾插法,使用双向链表实现较为方便。 - 有头节点的双向循环链表
为了保证操作的统一性,最终决定使用有头结点的双向循环链表来实现。因为如果使用非循环的链表,在插入和弹出时,均需要对链表为空的情况做单独处理(可以回忆一下双向链表的插入和删除操作);同时还需要额外维护一个尾指针。这非常的不优雅。而使用循环链表则不会有上述问题,只需维护一个头节点即可,在链表空和非空时的插入和弹出操作都是完全相同的代码。
代码实现
- 首先是节点的结构体定义
typedef struct EntNode
{
struct dirent ent;
struct EntNode* next;
struct EntNode* pre;
char path[MAX_FILENAME_LENGTH];
}EntNode;
注意:这里额外定义了一个path数组用于记录当前结点所属目录的路径。这是因为在遍历时,我们需要使用opendir函数来获得DIR结构体的指针,因此需要将目录的路径信息保存在节点中。与递归版本的代码进行对照的不难发现,这一数组就相当于递归版本中在进行递归时传入路径参数的行为。
- 接下来是链表相关操作的函数。
static int isEmpty(EntNode* head)
{
return (head->next == head);
}
// 插入尾部
static int push(EntNode* head, struct dirent ent, char* dirPath)
{
EntNode* node = (EntNode*)malloc(sizeof(EntNode));
if (node == NULL) return 0;
node->ent = ent;
strcpy(node->path, dirPath);
node->pre = head->pre;
node->next = head;
head->pre->next = node;
head->pre = node;
return 1;
}
// 从头部弹出
static int pop(EntNode* head, struct EntNode* node)
{
if (isEmpty(head)) return 0;
EntNode* tmp = head->next;
tmp->next->pre = head;
head->next = tmp->next;
*node = *tmp;
free(tmp);
return 1;
}
static int pushAll(char* dirPath, EntNode* head)
{
// Bad address
if (!dirPath || !strlen(dirPath))
{
return EFAULT;
}
// 获得目录结构体指针
DIR* pDir = opendir(dirPath);
if (pDir == NULL)
{
return EFAULT;
}
// 遍历该目录下文件,将所有对象加入到队列中
struct dirent* ent = readdir(pDir);
int err = 0;
while (ent)
{
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
{
ent = readdir(pDir);
continue;
}
// 加入队列
if (!push(head, *ent, dirPath))
{
err = ENOBUFS;
break;
}
ent = readdir(pDir);
}
closedir(pDir);
return err;
}
- 遍历的函数
该函数成功时返回0,失败时返回错误码。
int dirWalk(char* dirpath, void* ctx)
{
// 创建队列
EntNode* head = (EntNode*)malloc(sizeof(EntNode));
if (head == NULL)
{
return ENOBUFS;
}
head->next = head;
head->pre = head;
// 将当前目录下所有对象加入队列
int err = pushAll(dirpath, head);
if (err) return err;
// 不断读取队列,直至队列为空
struct EntNode node;
while (!isEmpty(head))
{
pop(head, &node);
// 判断目标路径是否过长
if (strlen(node.path) + node.ent.d_reclen + 1 >= MAX_FILENAME_LENGTH)
{
return ENAMETOOLONG;
}
// 拼接出目标目录的路径
char targetPath[MAX_FILENAME_LENGTH];
sprintf(targetPath, "%s%c%s", node.path, FILE_SEPERATOR, node.ent.d_name);
// 目录,将该目录下的所有实例加入队列
if (node.ent.d_type == DT_DIR)
{
err = pushAll(targetPath, head);
if (err != 0) break;
}
// 文件,做你想做的事
else if (node.ent.d_type == DT_REG)
{
// TODO
}
}
return err;
}
以上是非递归的代码实现。
这里双向循环链表是自己定义的,如果要考虑编程的规范性和统一性,应该使用#include <sys/queue.h>
文件中提供的TAILQ(双向有尾链表)或CIRCLEQ(双向循环链表)进行实现。我会在下一篇文章中介绍。