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

一、_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:\\", §orsPerCluster, &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, §orsPerCluster, &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。

