综合实战:构建一个跨平台的命令行工具
终于到了最后一章。说实话,每次讲到这里我都有点感慨——前面二十九章的知识点,就像一堆散落的零件,今天我们要把它们组装成一个真正能跑的东西。
我选了一个非常实用的项目:跨平台文件搜索工具。为什么选它?因为文件搜索是每个开发者都会用到的功能,而且它天然涉及路径处理、编码、多线程、内存管理这些跨平台开发的痛点。你想想看,一个简单的 find 命令,在 Windows 和 Linux 上的行为差异能让你抓狂。
项目需求与架构设计
我们先定个小目标:做一个命令行工具,支持递归搜索、正则匹配、文件类型过滤、结果排序。嗯,听起来不难?但跨平台版本就不一样了。
我直接给出核心架构图,这是我在实际项目中反复调整后的结构:
这个分层设计的好处很明显:核心逻辑只写一次,平台差异全部隔离在底层。我在做第一个跨平台项目时没注意这点,结果代码里到处都是 #ifdef _WIN32,维护起来简直噩梦。
核心数据结构设计
先定义搜索结果的载体。这里我用了动态数组,因为搜索前不知道会有多少结果:
typedef struct {
char *path; // 完整路径
char *filename; // 文件名
size_t size; // 文件大小
time_t mtime; // 修改时间
int is_dir; // 是否为目录
} FileEntry;
typedef struct {
FileEntry *entries;
size_t count;
size_t capacity;
} FileList;
// 初始化
FileList* file_list_create(size_t initial_cap) {
FileList *list = malloc(sizeof(FileList));
list->entries = malloc(initial_cap * sizeof(FileEntry));
list->count = 0;
list->capacity = initial_cap;
return list;
}
// 添加条目(自动扩容)
void file_list_add(FileList *list, const char *path,
const char *name, size_t size, time_t mtime, int is_dir) {
if (list->count >= list->capacity) {
list->capacity *= 2;
list->entries = realloc(list->entries,
list->capacity * sizeof(FileEntry));
}
FileEntry *e = &list->entries[list->count++];
e->path = strdup(path);
e->filename = strdup(name);
e->size = size;
e->mtime = mtime;
e->is_dir = is_dir;
}
strdup 不是标准 C 函数,但几乎所有平台都支持。如果你遇到不支持的情况,自己实现一个也不难。
跨平台目录遍历
这是整个工具里最麻烦的部分。Windows 用 FindFirstFile/FindNextFile,Linux/macOS 用 opendir/readdir。我封装了一个统一的接口:
#ifdef _WIN32
#include <windows.h>
#define PATH_SEP '\\'
#else
#include <dirent.h>
#include <sys/stat.h>
#define PATH_SEP '/'
#endif
typedef struct {
#ifdef _WIN32
HANDLE handle;
WIN32_FIND_DATA find_data;
int first;
#else
DIR *dir;
#endif
char path[1024];
} DirHandle;
// 打开目录
DirHandle* dir_open(const char *path) {
DirHandle *dh = malloc(sizeof(DirHandle));
strncpy(dh->path, path, sizeof(dh->path) - 1);
#ifdef _WIN32
char pattern[1024];
snprintf(pattern, sizeof(pattern), "%s\\*", path);
dh->handle = FindFirstFile(pattern, &dh->find_data);
dh->first = 1;
if (dh->handle == INVALID_HANDLE_VALUE) {
free(dh);
return NULL;
}
#else
dh->dir = opendir(path);
if (!dh->dir) {
free(dh);
return NULL;
}
#endif
return dh;
}
// 读取下一个条目
int dir_read(DirHandle *dh, char *name, size_t name_size,
int *is_dir, size_t *size, time_t *mtime) {
#ifdef _WIN32
if (dh->first) {
dh->first = 0;
} else {
if (!FindNextFile(dh->handle, &dh->find_data)) {
return 0;
}
}
// 宽字符转 UTF-8
WideCharToMultiByte(CP_UTF8, 0, dh->find_data.cFileName, -1,
name, name_size, NULL, NULL);
*is_dir = (dh->find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
*size = ((size_t)dh->find_data.nFileSizeHigh << 32) |
dh->find_data.nFileSizeLow;
*mtime = dh->find_data.ftLastWriteTime.dwLowDateTime;
return 1;
#else
struct dirent *entry = readdir(dh->dir);
if (!entry) return 0;
strncpy(name, entry->d_name, name_size);
char full_path[1024];
snprintf(full_path, sizeof(full_path), "%s/%s", dh->path, name);
struct stat st;
stat(full_path, &st);
*is_dir = S_ISDIR(st.st_mode);
*size = st.st_size;
*mtime = st.st_mtime;
return 1;
#endif
}
void dir_close(DirHandle *dh) {
#ifdef _WIN32
FindClose(dh->handle);
#else
closedir(dh->dir);
#endif
free(dh);
}
FindFirstFile 返回的是宽字符(UTF-16),直接当 char* 用会乱码。必须用 WideCharToMultiByte 转成 UTF-8。另外 Windows 上路径分隔符是反斜杠,但很多 API 也接受正斜杠,我建议统一用正斜杠,省得转义。
递归搜索与正则匹配
搜索逻辑其实不复杂,但要注意递归深度。我见过有人递归到几千层把栈搞崩了:
void search_directory(const char *base_path, const char *pattern,
FileList *results, int max_depth) {
if (max_depth <= 0) return;
DirHandle *dh = dir_open(base_path);
if (!dh) return;
char name[256];
int is_dir;
size_t size;
time_t mtime;
while (dir_read(dh, name, sizeof(name), &is_dir, &size, &mtime)) {
// 跳过 . 和 ..
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
char full_path[1024];
snprintf(full_path, sizeof(full_path), "%s%c%s",
base_path, PATH_SEP, name);
// 正则匹配文件名
if (regex_match(name, pattern)) {
file_list_add(results, full_path, name, size, mtime, is_dir);
}
// 递归子目录
if (is_dir) {
search_directory(full_path, pattern, results, max_depth - 1);
}
}
dir_close(dh);
}
正则匹配我用了 POSIX 的 regex.h,Windows 上需要额外处理。这里我封装了一个简单的正则匹配函数:
#ifdef _WIN32
// Windows 用通配符匹配代替正则(简化版)
int regex_match(const char *str, const char *pattern) {
return PathMatchSpec(str, pattern);
}
#else
#include <regex.h>
int regex_match(const char *str, const char *pattern) {
regex_t regex;
int ret = regcomp(®ex, pattern, REG_EXTENDED | REG_NOSUB);
if (ret) return 0;
ret = regexec(®ex, str, 0, NULL, 0);
regfree(®ex);
return ret == 0;
}
#endif
PathMatchSpec 只支持通配符(* 和 ?),不支持完整正则。如果你需要完整正则支持,可以考虑集成 pcre 库,或者用 std::regex(如果项目允许 C++)。我个人倾向于保持纯 C,所以用了这个折中方案。
多线程加速(可选)
搜索大目录时单线程太慢。我加了一个简单的线程池:
#ifdef _WIN32
typedef HANDLE ThreadHandle;
#define thread_create(h, func, arg) \
(*h = CreateThread(NULL, 0, func, arg, 0, NULL))
#define thread_join(h) WaitForSingleObject(h, INFINITE)
#else
#include <pthread.h>
typedef pthread_t ThreadHandle;
#define thread_create(h, func, arg) pthread_create(h, NULL, func, arg)
#define thread_join(h) pthread_join(h, NULL)
#endif
typedef struct {
char path[1024];
char pattern[256];
FileList *results;
int max_depth;
} SearchTask;
DWORD WINAPI search_thread(LPVOID arg) {
SearchTask *task = (SearchTask*)arg;
search_directory(task->path, task->pattern,
task->results, task->max_depth);
return 0;
}
FileList 的添加操作需要加锁。我一般用 CRITICAL_SECTION(Windows)或 pthread_mutex_t(POSIX)。另外线程数不要超过 CPU 核心数,否则上下文切换反而更慢。
结果排序与输出
搜索完成后,按修改时间排序输出:
int compare_by_mtime(const void *a, const void *b) {
FileEntry *ea = (FileEntry*)a;
FileEntry *eb = (FileEntry*)b;
if (ea->mtime < eb->mtime) return 1;
if (ea->mtime > eb->mtime) return -1;
return 0;
}
void print_results(FileList *list, int sort_by_time) {
if (sort_by_time) {
qsort(list->entries, list->count, sizeof(FileEntry),
compare_by_mtime);
}
for (size_t i = 0; i < list->count; i++) {
FileEntry *e = &list->entries[i];
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S",
localtime(&e->mtime));
printf("%c %10zu %s %s\n",
e->is_dir ? 'd' : '-',
e->size,
time_str,
e->path);
}
}
主函数与编译脚本
最后是主函数和跨平台编译配置。我习惯用 CMake,省心:
int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "用法: %s <路径> <模式> [选项]\n", argv[0]);
return 1;
}
FileList *results = file_list_create(128);
search_directory(argv[1], argv[2], results, 10);
print_results(results, 1);
// 清理
for (size_t i = 0; i < results->count; i++) {
free(results->entries[i].path);
free(results->entries[i].filename);
}
free(results->entries);
free(results);
return 0;
}
CMakeLists.txt 这样写:
cmake_minimum_required(VERSION 3.10)
project(fsearch C)
if(WIN32)
add_definitions(-D_WIN32)
set(PLATFORM_LIBS shlwapi)
else()
set(PLATFORM_LIBS pthread)
endif()
add_executable(fsearch main.c search.c regex.c)
target_link_libraries(fsearch ${PLATFORM_LIBS})
避坑总结
我把这些年做跨平台工具踩过的坑列出来,你写代码时对照着检查:
| 问题 | 表现 | 解决方案 |
|---|---|---|
| 路径编码 | 中文文件名乱码 | Windows 用 WideCharToMultiByte 转 UTF-8 |
| 路径分隔符 | 硬编码反斜杠导致 Linux 崩溃 | 用宏定义 PATH_SEP,或统一用正斜杠 |
| 递归深度 | 栈溢出崩溃 | 限制最大深度,或用迭代代替递归 |
| 线程安全 | 搜索结果丢失或重复 | 共享数据加锁,或每个线程独立结果最后合并 |
| 内存泄漏 | 长时间运行内存暴涨 | 每个 strdup 对应一个 free,用 valgrind 检查 |
说实话,跨平台开发没有银弹。你只能靠良好的分层设计、充分的测试、以及——嗯,踩坑经验。我至今记得第一次在 Windows 上跑 Linux 写的文件搜索工具,结果所有中文文件名都变成问号的那个下午。
但正是这些坑,让你真正理解平台差异的本质。当你把这份代码在 Linux、macOS、Windows 上都跑通时,那种成就感,值得。