> Linux教程 > Linux学习 >

在Linux下和Windows下遍历目录的方法及如何达成一致性操作

    最近因为测试目的需要遍历一个目录下面的所有文件进行操作,主要是读每个文件的内容,只要知道文件名就OK了。在Java中直接用File类就可以搞定,因为Java中使用了组合模式,使得客户端对单个文件和文件夹的使用具有一致性,非常方便。但在C中就不一样了,而且在不同的平台下使用方法也不同。在Linux下实现该功能就非常方便,因为自带有API库,几个函数用起来得心应手(虽然有些小问题,后面说),在Windows下实现就不是那么方便,虽然也有自己的API,但用法有些晦涩难懂,因为没有封装起来,需要自己一步一步进行操作,因为用的是Windows API库函数所以如果对Windows编程不熟悉的话,照搬网上的代码错了也不易调试。为此,我把这些操作都封装成类似Linux下的库函数,一方面简化透明了操作,另一方面(也许更重要)就是移植性,这样将包含该功能的程序从Windows上移植到Linux下就无需改动代码了(删掉实现封装的文件,因为Linux下自带了),当然从Linux下移植到Windows下同样方便(增加实现封装的文件即可),这就是所谓的OCP原则吧(开放封闭原则,具体见:程序员该有的艺术气质—SOLID原则)。好了,首先看下Linux下是如何实现这个功能的。

一、Linux下遍历目录的方法

 Linux下实现目录操作的API函数都在头文件dirent.h中,截取部分该文件内容如下:

 
/** structure describing an open directory. */
typedef struct _dirdesc {
    int    dd_fd;          /** file descriptor associated with directory */
    long    dd_loc;        /** offset in current buffer */
    long    dd_size;       /** amount of data returned by getdirentries */
    char    *dd_buf;       /** data buffer */
    int    dd_len;         /** size of data buffer */
    long    dd_seek;       /** magic cookie returned by getdirentries */
    long    dd_rewind;     /** magic cookie for rewinding */
    int    dd_flags;       /** flags for readdir */
    struct pthread_mutex    *dd_lock;    /** lock */
    struct _telldir *dd_td;    /** telldir position recording */
} DIR;

typedef    void *    DIR;

DIR    *opendir(const char *);
DIR    *fdopendir(int);
struct dirent *readdir(DIR *);
void     seekdir(DIR *, long);
long     telldir(DIR *);
void     rewinddir(DIR *);
int     closedir(DIR *);

struct dirent
{
     long d_ino;              /* inode number*/
     off_t d_off;             /* offset to this dirent*/
     unsigned short d_reclen; /* length of this d_name*/
     unsigned char d_type;    /* the type of d_name*/
     char d_name[1];          /* file name (null-terminated)*/
};
 

关键部分就是DIR这个结构体的定义,包括文件描述符、缓冲区偏移、大小、缓冲区内容等,下面定义的就是具体的目录操作函数了,有打开目录、读目录、重置读取位置、关闭目录等,这里我所需要的就是打开、读和关闭这三个最基本的目录操作,下面是使用例子:

 
#include <stdio.h> 
#include <stdlib.h>    
#include <string.h>  
#include <dirent.h> 

#define MAX_LEN 65535

int main(void) 
{ 
    DIR *dir; 
    struct dirent *ptr; 
    char *flow[MAX_LEN];
    int num = 0, i = 0;
   
    if ((dir=opendir("./data")) == NULL) 
    { 
        perror("Open dir error..."); 
        exit(1);        
    } 
    // readdir() return next enter point of directory dir
    while ((ptr=readdir(dir)) != NULL) 
    { 
        flow[num++] = ptr->d_name;
//      printf("%s\n", flow[num - 1]);
    } 

    for(i = 0; i < num; i++)
    {
        printf("%s\n", flow[i]);
    }
   
    closedir(dir); 
}
 

运行结果如下:

 

一看这结果就不对,输出的都是同一个文件名(最后一个文件的文件名), 哪里出了问题呢?将代码中// printf("%s\n", flow[num - 1]);这行注释去掉再运行,发现注释处输出的是正确的,两者都是输出的flow数组元素怎么结果不一样呢?经过调试发现是flow[num++] = ptr->d_name;这句代码的问题,因为这是引用拷贝(地址拷贝),所有的flow元素全部指向同一个对象ptr->d_name,虽然ptr->d_name对象每次的内容不同(也就是前面正确输出的原因),但所有内容都共享一个地址,用一个简单的图说明就是:

当然这个问题也比较好解决,也是比较常见的问题,用字符串拷贝或内存拷贝就行了,给flow每个元素重新申请一块内存。

 
#include <stdio.h> 
#include <stdlib.h>    
#include <string.h>  
#include <dirent.h> 

#define MAX_LEN 65535

int main(void) 
{ 
    DIR *dir; 
    struct dirent *ptr; 
    char *flow[MAX_LEN];
    int num = 0, i = 0;
   
    if ((dir=opendir("./data")) == NULL) 
    { 
        perror("Open dir error..."); 
        exit(1);        
    } 
    // readdir() return next enter point of directory dir
    while ((ptr=readdir(dir)) != NULL) 
    { 
        flow[num] = (char*)malloc(sizeof(char));
        strcpy(flow[num], ptr->d_name);
        num++;
    } 

    for(i = 0; i < num; i++)
    {
        printf("%s\n", flow[i]);
    }
   
    closedir(dir); 
}
 

 最终结果就正确了。

二、Windows下遍历目录的方法

 在Windows下就比较麻烦了,所要用到的函数都在windows.h中,Windows编程本来就比较繁琐,下面就不一一介绍所用到的函数了,直接给出封装的过程。

1. 首先模拟Linux下自带的头文件dirent.h

不同的是DIR中去掉了一些不需要的属性,及只定义了三个我所需要的操作(按需定义)。

 
// dirent.h
#ifndef _SYS_DIRENT_H
#define _SYS_DIRENT_H

typedef struct _dirdesc {
    int     dd_fd;      /** file descriptor associated with directory */
    long    dd_loc;     /** offset in current buffer */
    long    dd_size;    /** amount of data returned by getdirentries */
    char    *dd_buf;    /** data buffer */
    int     dd_len;     /** size of data buffer */
    long    dd_seek;    /** magic cookie returned by getdirentries */
} DIR;

# define __dirfd(dp)    ((dp)->dd_fd)

DIR *opendir (const char *);
struct dirent *readdir (DIR *);
void rewinddir (DIR *);
int closedir (DIR *);

#include <sys/types.h>

struct dirent
{
     long d_ino;              /* inode number*/
     off_t d_off;             /* offset to this dirent*/
     unsigned short d_reclen; /* length of this d_name*/
     unsigned char d_type;    /* the type of d_name*/
     char d_name[1];          /* file name (null-terminated)*/
};

#endif
 

 

2. 三个目录操作函数的实现

当然这是最关键的部分,我不知道Linux下是怎么实现的(找了下没找到),Windows下实现如下,主要是FindFirstFile()和FindNextFile()这两个Windows函数,对Windows编程不精,也不好解释什么,需要搞明白为啥这样实现请上网搜或MSDN。

 
// dirent.c
#include <stdio.h>     
#include <windows.h>  
#include "dirent.h"
  
static HANDLE hFind;  
  
DIR *opendir(const char *name)  
{  
    DIR *dir;  
    WIN32_FIND_DATA FindData;  
    char namebuf[512];  
  
    sprintf(namebuf, "%s\\*.*",name);  
  
    hFind = FindFirstFile(namebuf, &FindData );   
    if(hFind == INVALID_HANDLE_VALUE)   
    {  
        printf("FindFirstFile failed (%d)\n", GetLastError());  
        return 0;  
    }   
  
    dir = (DIR *)malloc(sizeof(DIR));  
    if(!dir)  
    {  
        printf("DIR memory allocate fail\n");  
        return 0;  
    } 

    memset(dir, 0, sizeof(DIR));  
    dir->dd_fd = 0;   // simulate return  
  
    return dir;  
}  

struct dirent *readdir(DIR *d)  
{  
    int i; 
    static struct dirent dirent;  
    BOOL bf;  
    WIN32_FIND_DATA FileData;  
    if(!d)  
    {  
        return 0;  
    }  
  
    bf = FindNextFile(hFind,&FileData);  
    //fail or end  
    if(!bf)  
    {  
        return 0;  
    }

    for(i = 0; i < 256; i++)  
    {  
        dirent.d_name[i] = FileData.cFileName[i];  
        if(FileData.cFileName[i] == '\0') break;  
    }  
    dirent.d_reclen = i;  
    dirent.d_reclen = FileData.nFileSizeLow;  
  
    //check there is file or directory  
    if(FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)  
    {  
        dirent.d_type = 2;  
    }  
    else  
    {  
        dirent.d_type = 1;  
    }  
      
  
    return (&dirent);  
}  
  
int closedir(DIR *d)  
{  
    if(!d) return -1;  
    hFind=0;  
    free(d);  
    return 0;  
} 
 

 

3. 使用方法

与Linux下使用一模一样,不需要改动一句代码就可应用,但却发现了与Linux下自带实现同样的问题,即也是引用拷贝,如下。

 因为这是我们自己实现的代码,所以字符串拷贝不是最佳解决方案,修改原实现代码才是最好的方法,当然如果是为了可移植性,就不需要改动了,就用字符串拷贝这样代码到Linux下就不需要改动了。下面看如何修改原实现解决:

a. 首先定位问题,可以很明显的知道是readdir这个函数的问题;

b. 然后找出问题根源,通过前面的分析可知问题的根源在于每次ptr->d_name使用的是同一内存地址,即ptr地址不变,而ptr是readdir返回的struct dirent指针,所以问题的根源在于readdir返回的dirent结构体地址问题,从上面代码中可以看到static struct dirent dirent; 这句代码,其中dirent的地址就是返回的地址,注意到dirent被定义为static,大家都知道C中static声明的变量调用一次后地址就不变了,存在静态存储区,也就是每次readdir返回的地址都是不变的,但指向的内容每次都被覆写,这就是问题所在;

c. 最后解决问题,知道问题根源后,问题就比较容易解决了,就是每次给dirent重新申请内存,看如下我的做法,注意我这里不能简单的struct dirent *dirent = (struct dirent *)malloc(sizeof(struct dirent))就结束了,看前面dirent结构体定义中char d_name[1];这里我只给d_name一个内存空间,显然不够,所以也要给它申请内存,我这里是按需申请内存,如果定义为char d_name[256];这样的就不需要了(一般文件名不是太长吧)。

 
struct dirent *readdir(DIR *d)  
{  
    int i;  
    
    BOOL bf;  
    WIN32_FIND_DATA FileData;  
    if(!d)  
    {  
        return 0;  
    }  
  
    bf=FindNextFile(hFind,&FileData);  
    //fail or end  
    if(!bf)  
    {  
        return 0;  
    }

    struct dirent *dirent = (struct dirent *)malloc(sizeof(struct dirent)+sizeof(FileData.cFileName));

    for(i = 0; i < 256; i++)  
    {  
        dirent->d_name[i] = FileData.cFileName[i];  
        if(FileData.cFileName[i] == '\0') break;  
    }  
    dirent->d_reclen = i;  
    dirent->d_reclen = FileData.nFileSizeLow;  
  
    //check there is file or directory  
    if(FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)  
    {  
        dirent->d_type = 2;  
    }  
    else  
    {  
        dirent->d_type = 1;  
    }  
      
    return dirent;
} 
 

 最终Windows运行结果如下:

PS:不知道这里大家有没有注意一个很小的细节,就是输出的不同(用的是一个相同的目录结构),Linux下输出了当前目录.和上层目录..而Windows下只输出了上层目录..,当然这没关系,因为我要的只是下面的文件名即可。OK,终于完成了,中间找bug花了不少时间,嘿嘿~~~

 

参考资料:

http://blog.csdn.net/lindabell/article/details/8181866


 

(责任编辑:IT)