【问题标题】:C fopen to a specific directoryC fopen 到特定目录
【发布时间】:2017-09-01 22:01:20
【问题描述】:

所以我正在尝试创建一个程序,该程序将根据系统是 Windows 还是 linux 来更改 fopen() 函数的文件目录。

在我的主要功能中,我通过

确定操作系统
#ifdef __unix
    printf("linux machine \n");
    windows = false;
    linux = true;
#endif

#ifdef _WIN32   
    printf("windows machine \n");
    windows = true;
    linux = false;
#endif

然后在我打开文件的函数中

FILE *fp, *fpo;
    if ((windows == true) && (linux == false)) 
    {
        fp = fopen("/temp/coursein/p1-in.txt", "r");
        fpo = fopen("/temp/courseout/p1-out.txt", "w");
    }
    else
    {
        fp = fopen("~/temp/coursein/p1-in.txt", "r");
        fpo = fopen("~/temp/coursein/p1-in.txt", "w");
    }

我已经尝试了所有我能想到的“/”“//”“\”“\”变体,在 linux 上我得到一个 seg 错误,在 Visual Studio 中我得到一个 Debug Assertion Failed。 表达式:stream != nullptr

【问题讨论】:

  • 这是完全错误的。 ~ 在 Linux 中的 C 程序中的文件名中不起作用。在 Windows 中还有另一个原因。崩溃是因为文件未打开,因此fp 或fpo 为NULL。使用 perror 打印错误信息。
  • 您可能需要在 Windows 上使用 "C:/temp/coursein/p1-in.txt"(用于适当的驱动器号)。我不确定这是否有必要,但值得一试。 OTOH,我确信在继续之前测试每个fopen() 的结果绝对是必要的。程序无法打开文件的原因有很多;你必须假设fopen()会失败并做出适当的反应。
  • @JonathanLeffler, CreateFile 使用运行时库解析路径,它将正斜杠替换为反斜杠,并且对于驱动器相对路径,从进程工作目录中提取驱动器或 UNC 共享。如果驱动器或共享分别没有“\temp\coursein”和“\temp\courseout”目录,则打开“p1-in.txt”和创建“p1-out.txt”将失败。此外,“p1-in.txt”必须存在。此外,Linux 示例以“r”和“w”模式两次打开同一个文件,这看起来像是一个错误。
  • 请注意,在 Windows 上使用正斜杠而不是反斜杠是不好的做法;我相信它会在这种特殊情况下起作用,所以我不认为这是你的问题,但这是一个坏习惯,因为有 种情况不会 工作。
  • 为了澄清对~ 的评论,这是一个在程序执行之前 扩展的shell 元字符(如*)。

标签: c linux windows


【解决方案1】:

您在 linux 版本中遇到崩溃很可能是因为在将流指针传递给流函数之前未检查 NULL。

假设fopen() 总是成功是一种糟糕的编程习惯。始终检查fopen() 和许多其他库函数的返回值。

linux版本好像有错字:fpo = fopen("~/temp/coursein/p1-in.txt", "w");应该是fpo = fopen("~/temp/courseout/p1-out.txt", "w");

此外,fopen 不会将初始的~/ 扩展到用户的主目录,shell 会在命令行上执行此操作,而不是标准 C 库。在 unix 中,您可以使用getenv("HOME") 检索用户的主目录,并使用snprintf 编写要打开的文件名。您可以为 fopen 编写一个包装函数,该函数将处理系统特定的行为并返回流句柄。

有一个更简单的解决方法:您可以使路径相对于当前目录:

    fp = fopen("temp/coursein/p1-in.txt", "r");
    fpo = fopen("temp/courseout/p1-out.txtt", "w");

或

    fp = fopen("coursein/p1-in.txt", "r");
    fpo = fopen("courseout/p1-out.txt", "w");

只要您从适当的目录运行程序或使用chdir 更改启动函数中的当前目录,您就可以在 linux 和 windows 上运行,您可以在其中测试您运行的操作系统。

【讨论】:

    【解决方案2】:

    有很多方法可以解决这个问题,但关键归结为使用getenv 获取所需的基本目录。它适用于windows和unix。您只需检查并找到每个操作系统下所需的环境变量的正确名称。

    例如,要从用户主目录开始构建路径,您可以在 Linux 上使用环境变量 HOME 或在 Windows 上使用 USERPROFILE。

    要将您的代码设置为自动使用适当的目录,您通常会使用预处理器检查来检查操作系统,然后设置一个定义来识别操作系统以在您的代码的其余部分中使用。 (您当然可以反复使用相同的测试,但标准做法只是为操作系统定义一个标识符,例如 Windows 上的 HAVEWIN 或各种 Unix 风格上的 HAVEUNIX。

    我通常将它分成两部分,但您可以一次完成所有操作,例如

    #if defined (_WIN32) || defined (_WIN64)
     #define HAVEWIN 1
    #elif defined (__unix__)
     #define HAVEUNIX 1
    #endif
    
    #ifdef HAVEWIN
     #define HOMEENV "USERPROFILE"
    #elif HAVEUNIX
     #define HOMEENV "HOME"
    #endif
    

    对于您的路径名分隔符,Windows 将允许您使用 POSIX 路径名分隔符 '/' 来分隔您的路径组件,但是当路径包含 spaces 时它会变得很挑剔(技术术语) .在这种情况下,使用正常的 DOS 反斜杠(在构建路径时必须将其转义),Windows 路径名解析会表现得更好。

    例如,假设我们对驻留在用户主目录中的 Windows 和 Linux 上名为 testfile.txt 的文件感兴趣(例如,Windows 上的 C:\Users\username 和 Linux 上的 /home/username),例如

    在微风中:

    C:\users\david>type testfile.txt
    hello windows
    

    在 Linux 上:

    $ cat ~/testfile.txt
    hello unix
    

    您可以使用sprintf 和足以容纳组件的缓冲区来构建到达每个组件的路径。 #include <limits.h> 提供了一个 PATH_MAX 常量以确保您有足够的空间用于文件名(通常是 4096 字符)。要构建路径,您可以执行以下操作:

    #include <limits.h>
    ...
    int main (void) {
    
        char *home = getenv (HOMEENV);
        char filename[PATH_MAX] = "";
        char buf[BUFSIZ] = "";
        FILE *fp;
    
        if (!home) {
            fprintf (stderr, "user home environment not found.\n");
            return 1;
        }
    
    #ifdef HAVEWIN
        sprintf (filename, "%s\\%s", home, FILENAME);
    #elif HAVEUNIX
        sprintf (filename, "%s/%s", home, FILENAME);
    #endif
    ...
    

    这几乎提供了(简而言之)您尝试对输入和输出文件执行的操作的基础知识。将所有部分放在一起,您可以执行以下操作:

    #include <stdio.h>
    #include <stdlib.h>
    #include <limits.h>
    
    #define FILENAME "testfile.txt"
    
    #if defined (_WIN32) || defined (_WIN64)
     #define HAVEWIN 1
    #elif defined (__unix__)
     #define HAVEUNIX 1
    #endif
    
    #ifdef HAVEWIN
     #define HOMEENV "USERPROFILE"
    #elif HAVEUNIX
     #define HOMEENV "HOME"
    #endif
    
    int main (void) {
    
        char *home = getenv (HOMEENV);
        char filename[PATH_MAX] = "";
        char buf[BUFSIZ] = "";
        FILE *fp;
    
        if (!home) {
            fprintf (stderr, "user home environment not found.\n");
            return 1;
        }
    
    #ifdef HAVEWIN
        sprintf (filename, "%s\\%s", home, FILENAME);
    #elif HAVEUNIX
        sprintf (filename, "%s/%s", home, FILENAME);
    #endif
    
        if (!(fp = fopen (filename, "r"))) {
            fprintf (stderr, "error: file not found '%s'\n", filename);
            return 1;
        }
    
        if (fgets (buf, sizeof buf, fp))
            puts (buf);
    
        return 0;
    }
    

    使用/输出示例

    在微风中:

    C:\users\david>Documents\src-c\bin\getenv_testfile.exe
    hello windows
    

    在 Linux 上:

    $ ./src-c/bin/getenv_testfile
    hello unix
    

    如开头所述,有多种方法可以将所有部分组合在一起。您可能会发现将所有不同的 windows/unix 定义放在顶部会更干净,这样您在main() 中就没有任何预处理器条件,例如

    ...
    #ifdef HAVEWIN
     #define HOMEENV "USERPROFILE"
     #define PATHFMT "%s\\%s"
    #elif HAVEUNIX
     #define HOMEENV "HOME"
     #define PATHFMT "%s/%s"
    #endif
    
    int main (void) {
    
        char *home = getenv (HOMEENV);
        char filename[PATH_MAX] = "";
        char buf[BUFSIZ] = "";
        FILE *fp;
    
        if (!home) {
            fprintf (stderr, "user home environment not found.\n");
            return 1;
        }
    
        sprintf (filename, PATHFMT, home, FILENAME);
    
        if (!(fp = fopen (filename, "r"))) {
            fprintf (stderr, "error: file not found '%s'\n", filename);
            return 1;
        }
    
        if (fgets (buf, sizeof buf, fp))
            puts (buf);
    
        return 0;
    }
    

    (注意:虽然这看起来有点干净,但在较大的代码中这在很大程度上是不切实际的)

    查看所有答案和 cmets,如果您有任何其他问题,请告诉我。

    【讨论】:

    • Windows 文件系统 API 在带空格的路径中使用斜杠没有问题。但是,为了与命令行解析和其他不将斜杠作为路径分隔符处理的 Windows API 保持一致,我通常建议使用反斜杠(例如,注册表路径和使用“本地”和“全局”命名的内核对象,其中都直接在 NT 对象命名空间上实现,只支持反斜杠路径)。
    • 请注意,在 Windows 中使用 char * 字符串和函数是不可靠的。环境变量和文件系统路径是 UTF-16 wchar_t * 字符串。 ANSI API 对遗留代码页使用有损编码,它将用最佳匹配匹配或问号替换未映射的字符。选择无法在系统 ANSI 代码页中编码的用户名并不少见,因此,如果此代码用于一般用途,那么获得第一个错误报告只是时间问题。
    • 用户配置文件不是主目录,也几乎不是基目录的合适选择。有用于查找合适的基本目录的 API 函数,例如,文档或应用程序数据。 不是,例如,Documents 文件夹将始终为%USERPROFILE%\Documents。
    • 同意@HarryJohnston。此处的目的不是建议 “嘿!把东西转储到 %USERPROFILE% ——听起来是个存放东西的好地方……” 不。目的是展示使用预处理器检查和环境使用,只需从每个可以工作的操作系统中获取一个基本环境——无需将任何更多的路径组件捆绑在一起。 (有趣的是,Vim 确实使用该目录作为 _viminfo 和 _vimrc 而 Gtk 使用它作为 .gtk-bookmarks,可以说两者都应该在 %LOCALAPPDATA%....)
    【解决方案3】:

    假设您在主目录中运行,也就是 ~

    fopen 的路径来自您当前的工作目录。因此波浪号是错误的(如上所述,因为它是一个奇怪的外壳)。

    fp = fopen("temp/coursein/p1-in.txt", "r");

    如果您需要移动到特定目录,请考虑使用 chdir(Linux 上的 man 2 chdir)。在打开输出文件之前不要忘记有关测试 fp 的其他建议。

    【讨论】:

    • 写一个只能在特定目录下运行的程序是个坏主意。
    • 从来没有说过,你到底是怎么想的?事实上,只要有一个可写的“临时”目录,无论它从哪里开始,这种修改都可以工作。我认为读者应该弄清楚的限制。
    【解决方案4】:

    在这两种情况下,崩溃都是因为fopen() 调用失败并返回NULL,并且后续代码在没有检查的情况下使用它们(例如读取或写入)。

    在 unix 下的问题是路径中的 ~ 是由 shell 扩展的(例如,在命令行中键入的路径中)而不是 C 库。您需要做的就是自己扩展路径。

    幸运的是,有一些方法可以做你想做的事,但在 windows 和 linux 下它们通常略有不同。我不会写代码,但我会指出你可以使用的东西。

    • &lt;stdlib.h&gt; 中的getenv() 可用于获取扩展环境变量。例如,getenv("XXX") 将返回一个字符串的指针(指向第一个字符),该字符串是名为 XXX 的环境变量的扩展,在 windows 和 unix 下。如果环境变量XXX 不存在,它将返回一个NULL 指针。
    • 在 Unix 下,名为 HOME 的环境变量通常会扩展到用户的主目录。
    • 在windows下,你可能会很幸运并定义了HOME环境变量。但这并不总是正确的。如果不是这样,通常会有一个名为 HOMEDRIVE 的环境变量扩展到主驱动器(例如,"C:" 如果主目录在 C 驱动器上)和另一个名为 HOMEPATH 的变量扩展到用户主目录没有附加命名的驱动器。
    • 在 unix 下,可以通过添加 '/' 字符和子目录名称来构建路径。例如"/user/home"。
    • 在 windows 下,路径可以以相同的方式构造,除了 windows API 函数(由 fopen() 调用)接受 '/' 和 '\\' 作为路径分隔符。

    还要记住fopen() 不会构建目录。要使fopen("/temp/x.dat", "w") 成功,目录/temp 必须已经存在,fopen() 只会尝试在其中创建x.dat。

    但是请注意,使用环境变量并不是获取用户主目录的特别好方法——尤其是在 Windows 下——因为用户可以更改环境变量。获取主目录的其他方法将更加健壮,但通常也特定于 unix 或 windows(例如操作系统特定的 API 函数,而不是使用 getenv())。我将把找到这些函数作为练习。

    无论您如何构建路径,在将句柄传递给其他读写函数之前检查fopen() 是否成功仍然是一个好主意。

    【讨论】:

    • 请注意,“用户的主目录”在 Windows 上并不是一个特别有意义的概念。一般来说,您需要用户的 Documents 目录或 Application Data 目录之一。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-22
    • 1970-01-01
    相关资源
    最近更新 更多