最近因為測試目的需要周遊一個目錄下面的所有檔案進行操作,主要是讀每個檔案的内容,隻要知道檔案名就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);
}
運作結果如下:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiM2AjN0ADN0EDMyETMzEDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
一看這結果就不對,輸出的都是同一個檔案名(最後一個檔案的檔案名), 哪裡出了問題呢?将代碼中// 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
微信學習公衆平台-媛媛推薦
微信号:programer-idea
名稱:程式媛想事兒
功能介紹:媛媛的主題包括技術蛋糕(包括IT最新資訊、C/C++/Java等程式設計語言知識及有關算法探讨等IT資料)、生活指南、輕松一刻三個欄目,每天會推送這三個方面的資訊給大家,讓猿媛們在學習IT知識的同時能關注生活關注健康,同時還能輕松開懷一笑。同時,大家可以回複關鍵詞定制自己想要的資訊,如隻看C/C++相關資料、隻看Java相關資料、隻看生活指南或隻想開懷一笑都是可以的,後期會根據需要開設疑難解惑等其它平台,歡迎大家加入學習!!!
作者:Alexia(minmin)
CSDN:http://blog.csdn.net/lanxuezaipiao
部落格園:http://www.cnblogs.com/lanxuezaipiao/
本文版權歸作者和CSDN共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接
否則保留追究法律責任的權利