在 C 语言中检查文件是否存在的最佳方法是什么?

563

除了简单地尝试打开文件,有更好的方法吗?

int exists(const char *fname)
{
    FILE *file;
    if ((file = fopen(fname, "r")))
    {
        fclose(file);
        return 1;
    }
    return 0;
}

4
你是真的只想检查文件是否存在吗?还是想要检查并在文件不存在时写入文件?如果是这样,请参考我的答案,使用不会发生竞态条件的版本。 - Dan Lenski
8
我不明白 - 那种fopen/fclose的方式有什么问题? - Johannes Schaub - litb
22
fopen()/fclose() 方法的一个问题在于,尽管文件存在,您可能无法打开它以进行读取。例如,/dev/kmem 存在,但大多数进程即使要进行读取也无法打开它。 /etc/shadow 是另一个这样的文件。当然,stat()access() 都依赖于能够访问包含文件的目录;如果您无法执行此操作(在包含文件的目录上没有执行权限),那么所有都是不确定的。 - Jonathan Leffler
1
if (file = fopen(fname, "r")) will give a warning. Use parenthesis around statement inside the if-statement if ((file = fopen(fname, "r"))) - Joakim
2
@Joakim (()) 只是在解决症状,而不是问题。 将其分成两行; 多一行也不会有太大的影响。 file = fopen(fname, "r"); 如果(file) - alx - recommends codidact
显示剩余5条评论
8个回答

782

查找位于unistd.h中的access()函数。您可以使用该函数替换您的函数。

if (access(fname, F_OK) == 0) {
    // file exists
} else {
    // file doesn't exist
}

在Windows(VC)下,unistd.h不存在。为了让它工作,需要定义:
#ifdef WIN32
#include <io.h>
#define F_OK 0
#define access _access
#endif

您还可以使用R_OKW_OKX_OK来代替F_OK,检查读取权限、写入权限和执行权限(分别),而不是仅存在性,并且您可以将它们中的任何一个进行OR运算(即使用R_OK|W_OK检查读取和写入权限)。
更新:请注意,在Windows上,您不能使用W_OK可靠地测试写入权限,因为访问函数不考虑DACL。access(fname, W_OK)可能会返回0(成功),因为文件没有设置只读属性,但您仍然可能没有权限写入该文件。

7
让我挑剔一下 :) access() 不是一个标准函数。如果“跨平台”要被理解为更广泛的意义,这可能会失败 :) - Remo.D
90
POSIX是ISO标准,它定义了access()。C是另一个ISO标准,但它没有定义access()。 - Jonathan Leffler
22
access() 存在陷阱。在使用 access() 和之后的任何其他操作之间存在“检查时间”和“使用时间”(TOCTOU)漏洞窗口,可能会导致安全问题。[...待续...] - Jonathan Leffler
27
更加深奥的是,在POSIX系统上,access() 函数检查的是真实的用户ID和组ID,而不是有效的用户ID和组ID。这只对于设置了SetUID或SetGID权限的程序有影响,但它非常重要,因为它可能会给出“错误”的答案。 - Jonathan Leffler
14
大多数情况下,使用access()来检查文件是否存在是可以的,但在SUID或SGID程序中,即使这样做也可能是不正确的。如果测试的文件位于真实UID或真实GID无法访问的目录中,则access()可能会报告该文件不存在,尽管它实际上存在。这听起来很抽象、不太可能发生?是的。 - Jonathan Leffler
显示剩余19条评论

145

像这样使用stat:

#include <sys/stat.h>   // stat
#include <stdbool.h>    // bool type

bool file_exists (char *filename) {
  struct stat   buffer;   
  return (stat (filename, &buffer) == 0);
}

并这样进行调用:

#include <stdio.h>      // printf

int main(int ac, char **av) {
    if (ac != 2)
        return 1;

    if (file_exists(av[1]))
        printf("%s exists\n", av[1]);
    else
        printf("%s does not exist\n", av[1]);

    return 0;
}

3
所有操作系统上,该方法无法处理大于2GB的文件。 - Ludvig A. Norin
5
@LudvigANorin:在这样的系统上,access() 也可能存在问题,但有一些选项可以使用,使 access()stat() 能够处理大于2GB的文件。 - Jonathan Leffler
19
你们能否提供关于2GB后失败的文档?在这种情况下有什么替代方法吗? - chamakits
@JonathanLeffler stat是否不像access一样容易受到TOCTOU漏洞的影响?(我不确定这是否更好。) - Telemachus
11
stat()access()均存在TOCTOU漏洞(lstat()也有,但fstat()是安全的)。根据文件的存在与否来确定接下来要做什么。通常使用正确的open()选项处理问题是最好的方法,但制定正确选项可能有些棘手。同时可以参考EAFP(“容易请求宽恕,而不是事先获准”)和LBYL(“先看后跳”)的讨论,例如查看Java中LBYL vs EAFP - Jonathan Leffler
4
如果您需要避免TOCTOU问题,在Linux系统上您可以只需使用open()函数以TOC方式打开文件(此时open()的返回值就是检查文件是否存在的依据),然后将此描述符用于TOU操作。这样即使在TOU操作期间文件不存在,您仍然可以通过文件描述符访问它。只要有进程打开文件,其内容就会被保留。 - Hi-Angel

101

通常当您想要检查文件是否存在时,这是因为您想要在文件不存在时创建该文件。如果您不想创建该文件,则Graeme Perrow的回答 很好,但是如果您想创建该文件,则会存在竞争条件:另一个进程可能会在您检查文件是否存在之间创建该文件,并实际上打开它进行写操作。(别笑...如果创建的文件是符号链接,则可能具有严重的安全影响!)

如果您想要检查文件是否存在并在文件不存在时创建该文件,原子地,以避免竞争条件,则使用此代码:

#include <fcntl.h>
#include <errno.h>

fd = open(pathname, O_CREAT | O_WRONLY | O_EXCL, S_IRUSR | S_IWUSR);
if (fd < 0) {
  /* failure */
  if (errno == EEXIST) {
    /* the file already existed */
    ...
  }
} else {
  /* now you can use the file */
}

8
如果您要使用O_CREAT,需要将mode(权限)作为第三个参数提供给open()函数。此外,还需考虑是否应该使用O_TRUNC或O_EXCL或O_APPEND。 - Jonathan Leffler
6
Jonathan Leffler是正确的,这个例子需要使用O_EXCL才能按照原来的方式工作。 - Randy Proctor
6
同时,您需要将模式指定为第三个参数:open(lock, O_CREAT | O_WRONLY | O_EXCL, S_IRUSR | S_IWUSR)。 - andrew cooke
4
需要翻译的内容:需要注意的是,这只有在文件系统符合POSIX标准的情况下才能保证安全;特别是旧版本的NFS存在严重的竞态条件,而这正是O_EXCL应该避免的!有一种解决方法在open(2)文档中有记录(适用于Linux系统;其他操作系统的手册可能会有所不同),但它相当丑陋,并且可能无法抵御恶意攻击者。 - Kevin
1
@GemTaylor 根据C11标准fopen()函数的x文件访问模式提供了类似于O_EXCL的独占打开/创建语义。 - Andrew Henle
显示剩余2条评论

35

是的,使用 stat()。参见stat(2)的man页面。

stat()会在文件不存在时失败,否则大多数情况下都会成功。如果文件存在,但您无法读取它所在的目录,则也会失败,但在这种情况下,任何方法都将失败(根据访问权限,您如何检查可能看不到的目录的内容?简单地说,您不能)。

哦,正如其他人提到的,您还可以使用access()。然而,我更喜欢使用stat(),因为如果文件存在,它将立即为我提供大量有用的信息(最后一次更新时间、大小、拥有者和/或组,访问权限等等)。


10
如果您只需要知道文件是否存在,访问是首选。如果您不需要所有额外的信息,stat() 可能会有很大的开销。 - Martin Beckett
4
实际上,当我使用ls命令列出一个目录时,它会为每个文件调用stat,运行ls的开销很大这一点对我来说是新的。实际上,你可以在拥有成千上万个文件的目录上运行ls,并在几秒钟内返回结果。 - Mecki
3
@Mecki:与支持硬链接的系统上的访问相比,stat 命令具有额外的开销。这是因为访问只需要查看目录项,而 stat 命令还需要查找 inode。在寻道时间较长的存储设备(例如磁带)上,差异可能会很大,因为目录项和 inode 不太可能相邻。 - Kevin
4
除非你只传递 F_OK 参数给 access() 函数,否则该函数会检查文件的访问权限。这些权限存储在该文件对应的 inode 中,并不在其目录项中(至少对于所有具有类似 inode 结构的文件系统是如此)。因此,access() 函数必须像 stat() 一样精确地访问 inode。所以,如果你没有检查任何权限,你所说的话就不成立了!实际上,某些系统上的 access() 函数甚至是基于 stat() 实现的(例如,在 GNU Hurd 上的 glibc 就是这样实现的),因此首先就不存在保证。 - Mecki
1
@Mecki:谁说要检查权限了?我特别是在谈论F_OK。是的,有些系统实现得很差。在任何情况下,访问速度至少与stat相同,并且有时可能更快。 - Kevin

20
FILE *file;
    if((file = fopen("sample.txt","r"))!=NULL)
        {
            // file exists
            fclose(file);
        }
    else
        {
            //File not found, no memory leak since 'file' == NULL
            //fclose(file) would cause an error
        }

27
fopen() 是标准的 C 函数,它不会消失。只有 Microsoft 将其标记为“已弃用”。如果你不想编写平台特定、不可移植的代码,请勿使用 fopen_s() - Andrew Henle
这是最兼容所有旧和新系统的方案。 - Eddy
布尔值设在哪里? - Jean-François Fabre
似乎这样做是低效的。事实上,这是唯一“兼容”的方式表明C文件系统接口有缺陷。 - user16217248
@Jean-FrançoisFabre 布尔表达式是(file = fopen("sample.txt","r"))!=NULL - user16217248

7
我认为unistd.h中的access()函数是Linux上的不错选择(你也可以使用stat)。您可以按照以下方式使用它:
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>

void fileCheck(const char *fileName);

int main (void) {
    char *fileName = "/etc/sudoers";

    fileCheck(fileName);
    return 0;
}

void fileCheck(const char *fileName){

    if(!access(fileName, F_OK )){
        printf("The File %s\t was Found\n",fileName);
    }else{
        printf("The File %s\t not Found\n",fileName);
    }

    if(!access(fileName, R_OK )){
        printf("The File %s\t can be read\n",fileName);
    }else{
        printf("The File %s\t cannot be read\n",fileName);
    }

    if(!access( fileName, W_OK )){
        printf("The File %s\t it can be Edited\n",fileName);
    }else{
        printf("The File %s\t it cannot be Edited\n",fileName);
    }

    if(!access( fileName, X_OK )){
        printf("The File %s\t is an Executable\n",fileName);
    }else{
        printf("The File %s\t is not an Executable\n",fileName);
    }
}

您将获得以下输出:

The File /etc/sudoers    was Found
The File /etc/sudoers    cannot be read
The File /etc/sudoers    it cannot be Edited
The File /etc/sudoers    is not an Executable

7
您可以使用 realpath() 函数。
resolved_file = realpath(file_path, NULL);
if (!resolved_keyfile) {
   /*File dosn't exists*/
   perror(keyfile);
   return -1;
}

如果第二个参数为NULL,那么realpath()函数将使用malloc分配缓冲区,调用者应该使用free释放该缓冲区。 - Peter Holzer
realpath() 还会将 errno 设置为 ENOENT,以指示所命名的文件实际上不存在。 - Peter Holzer

6

从Visual C++帮助文档来看,我倾向于选择

/* ACCESS.C: This example uses _access to check the
 * file named "ACCESS.C" to see if it exists and if
 * writing is allowed.
 */

#include  <io.h>
#include  <stdio.h>
#include  <stdlib.h>

void main( void )
{
   /* Check for existence */
   if( (_access( "ACCESS.C", 0 )) != -1 )
   {
      printf( "File ACCESS.C exists\n" );
      /* Check for write permission */
      if( (_access( "ACCESS.C", 2 )) != -1 )
         printf( "File ACCESS.C has write permission\n" );
   }
}

值得注意的是,_access(const char *path, int mode) 的模式值:
  • 00:仅存在

  • 02:写权限

  • 04:读权限

  • 06:读和写权限

由于您的fopen在文件存在但无法按请求打开的情况下可能会失败。
编辑:刚刚看到Mecki的帖子。 stat() 看起来更加整洁。哎呀。

如果你只需要知道文件是否存在,建议使用访问(access)。stat() 可能会有很大的开销。 - Martin Beckett
1
不要使用void main,而是使用int main。 - Anic17
@Anic17 为什么我们不应该使用 void main - user16217248
@user16217248,C标准中只有两种有效的main()签名,它们都返回int。如果编译器/环境支持void main(),那么它只是作为非官方扩展支持。 - HunterZ
@HunterZ,使用void main()是否会涉及到标准中的未定义行为? - user16217248
就标准而言,是的。但就大多数编译器而言,不是这样的。大多数编译器只会强制一个返回类型为void的主函数返回0作为退出代码。 - user1280483

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接