
1. 项目概述为什么获取文件大小是基本功在C/C开发中处理文件是家常便饭。无论是读取配置文件、加载资源还是做日志分析、数据备份第一步往往就是搞清楚你要处理的这个文件到底有多大。这个看似简单的操作——获取文件大小——背后却有好几种不同的实现方式每种方式都有其特定的适用场景和隐藏的“坑”。新手可能会直接抄一段网上搜来的fseek代码老手则会根据文件类型、操作系统甚至性能要求来选择最合适的方法。今天我们就来彻底拆解在C和C中获取文件大小的三种主流方法使用标准C库的fseek/ftell组合、使用POSIX标准的stat函数族以及使用特定操作系统提供的API如Windows的GetFileSizeEx。我会结合十多年的踩坑经验告诉你什么时候该用什么方法以及那些手册里不会写的细节。2. 核心思路与方案选型三种方法的本质区别获取文件大小本质上就是向操作系统询问一个文件的元数据信息。不同的方法其实是走了不同的“问询”路径其效率、精度和可移植性天差地别。2.1 方法一标准C库的fseek与ftell这是教科书里最常见的方法。思路很直接打开文件将文件指针移动到末尾然后获取当前指针的位置这个位置值就是文件的大小以字节为单位。优点纯C标准库实现理论上跨平台只要是支持ANSI C的环境。缺点必须打开文件这意味着你需要文件路径和适当的权限。对于某些你只有读取元数据权限而无读取内容权限的文件此方法会失败。性能开销涉及文件打开、寻址、关闭等IO操作如果仅仅为了获取大小开销较大。大小限制ftell返回的是long类型在32位系统上它无法正确处理大于2GB的文件long可能为32位有符号整数最大值约2.1GB。2.2 方法二POSIX标准的stat函数族这是Linux/Unix/macOS等类Unix系统上的首选方法。stat、fstat、lstat这些函数直接查询文件系统的索引节点inode信息无需打开文件本身就能获取包括大小在内的所有元数据。优点高效不涉及文件内容IO直接从内存中的文件系统结构读取信息速度极快。无需打开文件只需要文件路径避免了权限和资源占用问题。支持大文件stat结构体中的st_size成员通常是off_t类型在定义了_FILE_OFFSET_BITS64的现代系统上它是64位整数能处理EB级别的大文件。缺点属于POSIX标准不是ANSI C的一部分。在Windows上原生支持有限虽然有_stat这样的兼容版本但行为和路径处理可能有差异。2.3 方法三操作系统原生API为了追求极致的性能或需要获取更详细的信息如压缩文件大小、稀疏文件的实际占用空间可以直接调用操作系统提供的底层API。Linux除了stat还可以使用statx更新、功能更强或直接通过fstat传入文件描述符。Windows使用GetFileSizeEx或GetFileInformationByHandleEx函数。这些函数通过文件句柄工作能提供准确的大小信息并且直接使用64位整数LARGE_INTEGER。优点功能强大、精准能处理一些特殊情况如符号链接、挂载点、压缩文件。缺点完全丧失可移植性代码绑定特定操作系统。选型决策逻辑追求可移植性且文件不大2GB可考虑fseek/ftell但需知晓其限制。开发环境主要为Linux/Unix/macOS或需要高性能无条件选择stat。这是行业内的标准做法。开发Windows原生应用优先使用GetFileSizeEx。需要处理超大文件或特殊文件属性深入研究操作系统原生API。3. 核心细节解析与实操要点3.1fseek/ftell的陷阱与正确用法很多人会这样写FILE *fp fopen(file.bin, rb); if (fp) { fseek(fp, 0, SEEK_END); long size ftell(fp); fclose(fp); printf(Size: %ld bytes\n, size); }这段代码有三个潜在的坑模式必须为二进制在Windows系统上如果以文本模式r打开fseek和ftell对换行符的处理可能导致返回的大小不准确。务必使用rb模式。检查fseek返回值fseek可能失败例如文件流错误。良好的习惯是检查其返回值是否为0。long的类型限制如前所述这是硬伤。一个改进版本是使用ftello如果支持它返回off_t。但ftello也不是ANSI C标准。实操心得在实际生产代码中我几乎不会用这种方法来获取文件大小。它更像是一个教学示例用于理解文件指针的概念。其唯一的价值在于当你已经为了其他目的打开了文件流FILE*并且顺带想知道大小时可以顺便用一下。3.2 深入理解stat结构体stat函数的核心是填充一个struct stat结构体。我们关心的文件大小在st_size成员中。但这里面有更多细节#include sys/stat.h #include stdio.h int main() { struct stat file_stat; if (stat(path/to/file, file_stat) 0) { printf(File Size: %lld bytes\n, (long long)file_stat.st_size); printf(Blocks: %lld\n, (long long)file_stat.st_blocks); // 磁盘占用块数 printf(Block Size: %lld bytes\n, (long long)file_stat.st_blksize); // 文件系统块大小 } else { perror(stat failed); } return 0; }st_size文件的逻辑大小即用户看到的数据字节数。st_blocks文件实际占用的磁盘块数通常每块512字节。这对于判断稀疏文件文件中有很多“空洞”逻辑大但实际占用小或文件系统压缩情况很有用。st_blksize文件系统I/O操作的首选块大小对于后续的读写操作有优化意义。fstat和lstat的区别fstat(int fd, struct stat *buf)通过已打开的文件描述符获取信息。当你已经用open打开了文件想获取信息又不想再走路径查询时用它效率最高。lstat(const char *path, struct stat *buf)对于符号链接软链接stat会追踪链接指向的目标文件而lstat获取的是链接文件本身的信息。如果你需要区分链接和真实文件必须用lstat。3.3 WindowsGetFileSizeEx实战Windows API的风格与C库不同它基于句柄Handle。基本步骤如下使用CreateFile打开文件获取一个文件句柄。注意需要指定FILE_READ_ATTRIBUTES权限这比泛泛的GENERIC_READ更安全、更精准。将句柄传递给GetFileSizeEx。使用CloseHandle关闭句柄。#include windows.h #include stdio.h int main() { HANDLE hFile CreateFileA(C:\\path\\to\\file.dat, FILE_READ_ATTRIBUTES, // 关键只需读属性权限 FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile INVALID_HANDLE_VALUE) { printf(Failed to open file. Error: %lu\n, GetLastError()); return 1; } LARGE_INTEGER fileSize; if (GetFileSizeEx(hFile, fileSize)) { // fileSize.QuadPart 是一个64位有符号整数表示字节数 printf(File Size: %lld bytes\n, fileSize.QuadPart); } else { printf(GetFileSizeEx failed. Error: %lu\n, GetLastError()); } CloseHandle(hFile); return 0; }注意CreateFile的参数非常复杂对于只是获取大小务必使用最小权限FILE_READ_ATTRIBUTES这可以避免因文件被独占锁定而打开失败也更符合安全原则。4. 三种方法的完整实现与对比测试下面我们用一个具体的例子在Linux环境下实现并对比这三种方法。我们创建一个测试文件并模拟一个简单的性能测试。4.1 测试环境搭建与代码实现首先创建一个1MB的测试文件dd if/dev/urandom oftestfile.bin bs1024 count1024接下来是完整的C程序filesize_comparison.c#include stdio.h #include sys/stat.h #include sys/time.h #include unistd.h #include fcntl.h // 方法1: fseek/ftell long get_file_size_method1(const char* filename) { FILE* fp fopen(filename, rb); if (!fp) return -1; if (fseek(fp, 0, SEEK_END) ! 0) { fclose(fp); return -1; } long size ftell(fp); fclose(fp); return size; } // 方法2: stat long long get_file_size_method2(const char* filename) { struct stat st; if (stat(filename, st) 0) { return st.st_size; } return -1; } // 方法3: fstat (通过文件描述符) long long get_file_size_method3(const char* filename) { int fd open(filename, O_RDONLY); if (fd -1) return -1; struct stat st; long long size -1; if (fstat(fd, st) 0) { size st.st_size; } close(fd); return size; } // 简单的微秒级计时函数 long long get_current_time_us() { struct timeval tv; gettimeofday(tv, NULL); return (long long)tv.tv_sec * 1000000LL tv.tv_usec; } int main() { const char* filename testfile.bin; const int iterations 10000; // 重复执行次数用于性能对比 printf(Testing file: %s\n, filename); printf(\n); // 测试方法1 long long start get_current_time_us(); long size1 0; for (int i 0; i iterations; i) { size1 get_file_size_method1(filename); } long long end get_current_time_us(); printf(Method1 (fseek/ftell):\n); printf( Size: %ld bytes\n, size1); printf( Time for %d iterations: %lld us, Avg: %.2f us\n, iterations, end - start, (double)(end - start)/iterations); // 测试方法2 start get_current_time_us(); long long size2 0; for (int i 0; i iterations; i) { size2 get_file_size_method2(filename); } end get_current_time_us(); printf(\nMethod2 (stat):\n); printf( Size: %lld bytes\n, size2); printf( Time for %d iterations: %lld us, Avg: %.2f us\n, iterations, end - start, (double)(end - start)/iterations); // 测试方法3 start get_current_time_us(); long long size3 0; for (int i 0; i iterations; i) { size3 get_file_size_method3(filename); } end get_current_time_us(); printf(\nMethod3 (fstat via open):\n); printf( Size: %lld bytes\n, size3); printf( Time for %d iterations: %lld us, Avg: %.2f us\n, iterations, end - start, (double)(end - start)/iterations); return 0; }4.2 编译、运行与结果分析使用gcc编译并运行gcc -o filesize_test filesize_comparison.c ./filesize_test在我的测试环境Linux虚拟机上一次典型的输出结果如下Testing file: testfile.bin Method1 (fseek/ftell): Size: 1048576 bytes Time for 10000 iterations: 1250340 us, Avg: 125.03 us Method2 (stat): Size: 1048576 bytes Time for 10000 iterations: 152890 us, Avg: 15.29 us Method3 (fstat via open): Size: 1048576 bytes Time for 10000 iterations: 1834560 us, Avg: 183.46 us结果解读准确性三种方法都正确获取了1MB1048576字节的文件大小。性能stat方法2遥遥领先平均每次调用仅需约15微秒。因为它只读取内存中的inode信息几乎没有磁盘I/O。fseek/ftell方法1平均125微秒慢了近一个数量级因为它需要执行打开文件、寻址、关闭文件这一整套IO操作。fstat方法3最慢平均183微秒。虽然fstat本身很快但我们的封装函数里包含了open和close每次循环都重复打开关闭文件开销巨大。这说明了如果你已经持有一个文件描述符那么fstat是极快的但如果你只是为了获取大小而去打开文件那还不如直接用stat。结论对于“给定路径获取大小”这个场景stat是性能最佳选择。fseek/ftell性能较差且有类型限制应避免在严肃项目中使用。fstat适用于已持有文件描述符的场合。5. 进阶议题与边界情况处理掌握了基本方法后在实际项目中还会遇到一些棘手的情况。5.1 处理超大文件2GB这是fseek/ftell方法的死穴。在Linux上确保你的程序支持大文件LFS。通常有两种方式编译时定义宏在源文件开头或编译命令中定义-D_FILE_OFFSET_BITS64。这样off_t和相关函数如stat的st_size会自动变为64位。gcc -D_FILE_OFFSET_BITS64 -o myprogram myprogram.c使用过渡函数使用fseeko和ftello它们明确使用off_t类型。同样需要上述宏定义支持。#define _FILE_OFFSET_BITS 64 #include stdio.h #include sys/types.h off_t get_file_size_large(const char* filename) { FILE* fp fopen(filename, rb); if (!fp) return -1; if (fseeko(fp, 0, SEEK_END) ! 0) { fclose(fp); return -1; } off_t size ftello(fp); fclose(fp); return size; }在Windows上使用GetFileSizeEx或_stat64微软扩展天然支持64位。5.2 处理特殊文件符号链接、管道、设备文件stat和lstat在这里派上大用场。符号链接使用lstat获取链接本身的大小即存储目标路径字符串的长度使用stat获取目标文件的大小。管道、套接字、设备文件如/dev/null对于这些不是普通磁盘文件的“文件”st_size字段可能没有意义通常是0。在获取大小前应该用S_ISREG(st.st_mode)宏判断它是否是一个普通文件regular file。struct stat st; if (stat(path, st) 0) { if (S_ISREG(st.st_mode)) { printf(Regular file, size: %lld\n, (long long)st.st_size); } else if (S_ISLNK(st.st_mode)) { printf(Its a symbolic link.\n); } else { printf(Not a regular file (e.g., pipe, device). Size may be meaningless.\n); } }5.3 跨平台代码封装实践在一个需要同时支持Linux和Windows的项目中我们通常会封装一个统一的接口// filesize_util.h #ifdef __cplusplus extern C { #endif // 返回文件大小字节数失败返回-1 long long get_file_size(const char* filepath); #ifdef __cplusplus } #endif// filesize_util.c (或根据平台拆分为 .c 文件) #include filesize_util.h #ifdef _WIN32 #include windows.h #else #include sys/stat.h #include unistd.h #endif long long get_file_size(const char* filepath) { if (!filepath) return -1; #ifdef _WIN32 // Windows实现 HANDLE hFile CreateFileA(filepath, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile INVALID_HANDLE_VALUE) { return -1; } LARGE_INTEGER size; if (!GetFileSizeEx(hFile, size)) { CloseHandle(hFile); return -1; } CloseHandle(hFile); return size.QuadPart; #else // Linux/Unix/macOS实现 struct stat st; if (stat(filepath, st) ! 0) { return -1; } return (long long)st.st_size; #endif }这样业务代码只需调用get_file_size(“path”)无需关心底层实现。6. 常见问题排查与调试技巧即使代码写对了在实际运行中也可能遇到各种问题。这里记录几个我踩过的坑和解决方法。6.1 文件大小显示为0或负数可能原因1文件不存在或路径错误。stat和fopen都会失败。务必检查函数返回值。使用perror(“stat”)或strerror(errno)打印具体错误信息。可能原因2文件是符号链接且指向不存在的目标。stat会失败而lstat会成功并返回链接本身的大小。可能原因3权限不足。即使文件存在如果没有父目录的执行权限x或文件的读权限stat也可能失败。使用ls -la命令仔细检查权限。可能原因4fseek/ftell用于文本模式文件。在Windows上文本模式会导致大小计算错误。永远用二进制模式”rb”。6.2stat返回的大小与实际du命令显示不同这是新手常问的问题。du命令显示的是文件在磁盘上实际占用的空间块数 * 块大小而stat的st_size是文件的逻辑大小。稀疏文件用dd命令可以创建。ls -l显示的逻辑大小很大但du显示很小。stat结构体中的st_blocks字段乘以512约等于du的值。文件系统块大小磁盘分配空间是按块block进行的比如4KB一块。一个1字节的文件也会占用一个块4KB。st_size1但du显示4KB。压缩/去重某些高级文件系统如Btrfs, ZFS支持透明压缩或块级去重这也会导致逻辑大小和物理占用不一致。6.3 性能瓶颈分析与优化如果你的程序需要频繁获取大量文件的大小例如遍历目录树性能就至关重要。批量处理避免对同一个文件重复调用stat。可以缓存结果如果文件不会被修改。使用更高效的遍历方式在Linux上使用fts系列函数fts_open,fts_read或getdents系统调用遍历目录比用opendir/readdir循环中每个文件都stat一次要高效因为前者可以在一次系统调用中获取更多信息取决于标志位。异步I/O在极端高性能场景下可以考虑使用异步I/O如Linux的io_uring来提交一批statx请求但这对编程复杂度要求很高。Profile性能剖析使用strace -c可以统计程序调用了多少次stat系统调用使用perf工具可以分析热点函数。很多时候瓶颈不在stat本身而在低效的路径拼接或字符串处理上。6.4 错误处理的最佳实践永远不要假设函数调用会成功。健壮的代码应该这样写long long safe_get_file_size(const char* path) { if (!path || path[0] \0) { errno EINVAL; // 设置错误码与系统调用风格一致 return -1; } #ifdef _WIN32 // Windows 错误处理 HANDLE hFile CreateFileA(/* ... */); if (hFile INVALID_HANDLE_VALUE) { // 可以记录日志GetLastError() return -1; } // ... #else // Unix-like 错误处理 struct stat st; if (stat(path, st) ! 0) { // stat失败errno已被设置如ENOENT, EACCES // 可以记录日志strerror(errno) return -1; } if (!S_ISREG(st.st_mode)) { // 不是普通文件根据业务逻辑决定是返回错误还是返回0 errno EINVAL; // 或 ESPIPE 等 return -1; } return (long long)st.st_size; #endif }把错误信息errno或GetLastError()记录下来对于后期调试有巨大帮助。