广告位 · 页头(后台-模块管理-新建模块后替换此区域)

C++怎么获取文件在磁盘上的真实占用空间?_stat结构体详解

在 Windows 系统上开发 C++ 程序时,如果只是调用 _stat 或 stat 函数拿到 st_size,得到的其实是文件的逻辑大小,并不等于文件在磁盘上真实占用的空间。比如一个 1024 字节的小文件,在 NTFS 默认 4KB 簇大小的分区上,实际占用可能是 4096 字节。这种差异在磁盘空间统计、文件备份或容量分析工具中会造成明显误差。因此有必要弄清楚 _stat 结构体的真实含义,以及 Windows 下如何准确计算文件占用空间。

C++怎么获取文件在磁盘上的真实占用空间?_stat结构体详解

一、_stat 结构体解析与 st_size 的局限

Windows 的 C 运行时库提供了 _stat 函数,它的参数是 struct _stat 结构体。这个结构体定义在 sys/stat.h 中,常见的字段包括 st_dev、st_ino、st_mode、st_nlink、st_uid、st_gid、st_rdev、st_size、st_atime、st_mtime 和 st_ctime。其中 st_size 的类型是 _off_t,表示文件的字节长度,也就是我们常说的逻辑大小。下面的代码演示了如何通过 _stat 获取一个文本文件的逻辑大小。

#include <sys/stat.h>
#include <iostream>
int main() {
    struct _stat fileInfo;
    if (_stat("C:\\test\\example.txt", &fileInfo) == 0) {
        std::cout << "文件逻辑大小: " << fileInfo.st_size << " 字节" << std::endl;
    } else {
        std::cout << "获取文件信息失败" << std::endl;
    }
    return 0;
}

需要注意的是,在 32 位编译环境下,_stat 的 st_size 通常只有 32 位,最大只能表示约 2GB。对于超过 2GB 的文件,_stat 调用可能返回错误或截断。因此建议使用 _stat64 结构体和 _stat64 函数,它把 st_size 扩展为 64 位,能够处理大文件。下面是用 _stat64 获取大文件逻辑大小的示例。

#include <sys/stat.h>
#include <iostream>
int main() {
    struct _stat64 fileInfo;
    if (_stat64("C:\\test\\largefile.bin", &fileInfo) == 0) {
        std::cout << "文件逻辑大小: " << fileInfo.st_size << " 字节" << std::endl;
    } else {
        std::cout << "获取文件信息失败" << std::endl;
    }
    return 0;
}

从输出结果可以看到,st_size 只是告诉我们文件内容的长度,并没有考虑文件系统分配磁盘块的方式。要想知道实际占用空间,必须继续了解磁盘簇的机制。

二、磁盘簇与真实占用空间的计算方法

Windows 使用的 NTFS、FAT32、exFAT 等文件系统,都以簇为单位分配存储空间。一个簇通常由多个扇区组成,默认 NTFS 的簇大小为 4096 字节,FAT32 则可能是 512 字节到 32KB 不等。文件即使只有 1 个字节,也会占用至少一个完整的簇。因此实际占用空间的计算公式为:先将文件逻辑大小除以簇大小,如果不是整除就向上取整,再乘以簇大小。公式可以写成:

diskUsage = ((logicalSize + clusterSize - 1) / clusterSize) * clusterSize;

要获得某个卷的簇大小,可以调用 GetDiskFreeSpace 函数。它需要传入卷根路径,例如 C:\,并返回每个扇区字节数和每簇扇区数,两者相乘即为簇大小。下面的代码展示了获取 C 盘簇大小的过程。

#include <windows.h>
#include <iostream>
int main() {
    DWORD sectorsPerCluster = 0;
    DWORD bytesPerSector = 0;
    DWORD freeClusters = 0;
    DWORD totalClusters = 0;
    if (GetDiskFreeSpace(L"C:\\", &sectorsPerCluster, &bytesPerSector, &freeClusters, &totalClusters)) {
        DWORD clusterSize = sectorsPerCluster * bytesPerSector;
        std::cout << "每簇字节数: " << clusterSize << std::endl;
    } else {
        std::cout << "获取磁盘信息失败" << std::endl;
    }
    return 0;
}

得到 clusterSize 后,再结合文件的逻辑大小即可算出占用空间。不过 GetDiskFreeSpace 接收的是卷根路径,如果文件在 D 盘或移动硬盘上,应当传入相应的根路径。实际项目中可以通过文件路径提取盘符,拼成 D:\ 这样的根路径字符串。

三、使用 Windows API 获取真实占用空间的完整实现

获取文件的 64 位逻辑大小,推荐使用 CreateFile 打开文件并调用 GetFileSizeEx,因为 _stat64 虽然也能得到大小,但在某些符号链接或特殊文件上可能不够稳定。GetFileSizeEx 返回 LARGE_INTEGER,可以直接转换成 ULONGLONG 使用。下面给出一个完整的函数,它接收文件完整路径,返回该文件在磁盘上占用的字节数。

#include <windows.h>
#include <iostream>
#include <string>
ULONGLONG GetFileDiskUsage(const std::wstring& filePath) {
    HANDLE hFile = CreateFileW(filePath.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ,
                               NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        return 0;
    }
    LARGE_INTEGER fileSize;
    if (!GetFileSizeEx(hFile, &fileSize)) {
        CloseHandle(hFile);
        return 0;
    }
    CloseHandle(hFile);
    // 提取盘符并构造根路径,例如 C:\
    wchar_t rootPath[4] = {0};
    if (filePath.length() >= 3 && filePath[1] == L':') {
        rootPath[0] = filePath[0];
        rootPath[1] = L':';
        rootPath[2] = L'\\';
    } else {
        wcscpy_s(rootPath, L"C:\\");
    }
    DWORD sectorsPerCluster = 0;
    DWORD bytesPerSector = 0;
    DWORD freeClusters = 0;
    DWORD totalClusters = 0;
    if (!GetDiskFreeSpace(rootPath, &sectorsPerCluster, &bytesPerSector, &freeClusters, &totalClusters)) {
        return 0;
    }
    DWORD clusterSize = sectorsPerCluster * bytesPerSector;
    ULONGLONG logicalSize = static_cast<ULONGLONG>(fileSize.QuadPart);
    ULONGLONG diskUsage = ((logicalSize + clusterSize - 1) / clusterSize) * clusterSize;
    return diskUsage;
}
int main() {
    ULONGLONG usage = GetFileDiskUsage(L"C:\\test\\example.txt");
    std::cout << "文件实际占用磁盘空间: " << usage << " 字节" << std::endl;
    return 0;
}

这个函数先通过 CreateFileW 以只读属性方式打开文件,获得句柄后调用 GetFileSizeEx 拿到逻辑大小。然后调用 GetDiskFreeSpace 取得文件所在分区的簇大小。注意示例代码中简化使用了根路径动态提取逻辑,实际开发时需要根据文件路径提取盘符,拼成 D:\ 这样的根路径字符串。最后按照向上取整公式计算并返回结果。

有一种常见误解是认为 GetCompressedFileSize 可以返回文件的磁盘占用空间。实际上,对于未压缩且非稀疏的文件,GetCompressedFileSize 的返回值就是逻辑文件大小,并不包含簇对齐带来的额外空间。只有对压缩文件或稀疏文件,它才返回压缩后或者实际分配的字节数。因此普通文件必须用簇大小公式计算。

四、稀疏文件、压缩文件与大文件场景的处理

稀疏文件在 NTFS 上只存储非零数据块,逻辑大小可能非常大,但实际占用远小于逻辑大小。例如一个 10GB 的稀疏文件可能只占用几十 MB 磁盘空间。对于这类文件,GetFileSizeEx 返回逻辑大小,而 GetCompressedFileSize 可以返回实际占用的磁盘字节数。但必须注意,稀疏文件的簇对齐仍然存在,GetCompressedFileSize 返回的已经是分配后的占用空间,不需要再加簇计算。

压缩文件的情况类似。NTFS 支持透明压缩,文件在磁盘上以压缩形式存放,逻辑大小与占用空间可能差异很大。开发者可以通过 DeviceIoControl 配合 FSCTL_GET_COMPRESSION 判断文件是否被压缩,或者使用 GetFileAttributes 检测 FILE_ATTRIBUTE_COMPRESSED 标志。如果文件具有压缩或稀疏属性,应优先使用 GetCompressedFileSize 获取占用空间;否则才使用第 3 节的簇对齐公式。

另外,在处理大于 4GB 的文件时,一定要避免使用 32 位 API,例如 GetFileSize 需要与 GetFileSizeEx 搭配,或者使用 GetFileInformationByHandle 的 nFileSizeHigh 与 nFileSizeLow 合并成 64 位值。_stat 结构体在 64 位编译下默认 st_size 是 64 位,但为了兼容性,建议统一使用 _stat64 和 GetFileSizeEx。

广告位 · 文章底部(后台-模块管理替换此区域)