【问题标题】:Why does C's "fopen" take a "const char *" as its second argument?为什么 C 的“fopen”将“const char *”作为第二个参数?
【发布时间】:2011-01-31 22:24:09
【问题描述】:

C 函数“fopen”使用“const char *”作为第二个参数总是让我感到奇怪。我认为如果在 stdio.h 中定义了位掩码,例如“IO_READ”等,那么阅读您的代码和实现库的代码会更容易,因此您可以执行以下操作:

FILE* myFile = fopen("file.txt", IO_READ | IO_WRITE);

它实际上的方式是否有程序化的原因,还是只是历史性的? (即“就是这样。”)

【问题讨论】:

    标签: c file-io history


    【解决方案1】:

    我相信字符串而不是简单的位掩码的优点之一是它允许特定于平台的扩展而不是位设置。纯属假设:

    FILE *fp = fopen("/dev/something-weird", "r+,bs=4096");
    

    对于这个 gizmo,open() 调用需要被告知块大小,不同的调用可以使用完全不同的大小,等等。当然,现在 I/O 已经组织得很好(最初不是这样 -设备种类繁多,访问机制远非统一),因此似乎很少有必要。但是字符串值的开放模式参数允许更好的可扩展性。

    在 IBM 的大型机 MVS o/s 上,fopen() 函数确实采用了此处描述的一般线路的额外参数 - 如Andrew Henlenoted(谢谢!)。手册页包括示例调用(稍微重新格式化):

    FILE *fp = fopen("myfile2.dat", "rb+, lrecl=80, blksize=240, recfm=fb, type=record"); 
    

    底层的open() 必须通过ioctl()(I/O 控制)调用或fcntl()(文件控制)或隐藏它们的函数来实现类似的效果。

    【讨论】:

    【解决方案2】:

    一个词:遗产。不幸的是,我们不得不忍受它。

    只是猜测:也许当时“const char *”似乎更灵活的解决方案,因为它不受任何限制。一个位掩码只能有 32 个不同的值。我现在看起来像 YAGNI。

    更多猜测:伙计们很懒惰,写“rb”比 MASK_THIS 需要更少的输入 | MASK_THAT :)

    【讨论】:

    • 为什么?口罩似乎是这里的自然选择,尤其是在设计 fopen 的那一天。
    • "一个位掩码只能有 32 个不同的值" -- 发明 C 时,一个位掩码只能有 16 个不同的值。
    • @Windows 程序员:不,C 没有位掩码类型。任何整数类型都可以使用(最好是无符号的),unsigned long 至少为 32 位。
    • C 发明时,open() 的第二个参数是 int 类型,int 有 16 位,unsigned 不存在,long 不存在。 open() 的第二个参数是一个用作位掩码的 int。由于 int 被用作位掩码,因此 16 位可以表示 16 个不同的值。
    • 请注意,在定义和描述标准 I/O 库的时代(版本 7 Unix 和更早版本),open() 系统调用只接受两个参数。如果open() 因文件不存在而失败,您必须使用名称和模式调用creat() 来创建文件。
    【解决方案3】:

    Dennis Ritchie(1993 年)撰写了an article about the history of C,以及它是如何从 B 逐渐演变而来的。一些设计决策的动机是避免对用 B 或 C 的雏形版本编写的现有代码进行源代码更改。

    特别是,Lesk 写了一个“便携式 I/O 包' [Lesk 72] 是后来的 重做成为C`标准 I/O' 例程

    C 预处理器直到 1972/3 才引入,所以 Lesk 的 I/O 包是在没有它的情况下编写的!(在非常早期的非 C 中,指针适合平台上的整数正在使用,将隐式int返回值分配给指针是完全正常的。)

    在 1972-3 年左右发生了许多其他变化,但最重要的是引入了预处理器,部分原因是 Alan Snyder [Snyder 74] 的敦促

    如果没有#include#define,则无法选择IO_READ | IO_WRITE 这样的表达式。

    在 1972 年,fopen 在没有 CPP 的情况下调用的典型源代码中的选项是:

    FILE *fp = fopen("file.txt", 1);       // magic constant integer literals
    FILE *fp = fopen("file.txt", 'r');     // character literals
    FILE *fp = fopen("file.txt", "r");     // string literals
    

    魔法整数文字显然很可怕,所以不幸的是,显然最有效的选项(Unix 后来为open(2) 采用)因缺少预处理器而被排除在外。

    字符文字显然是不可扩展的;大概这对 API 设计者来说是显而易见的,即使在当时。但是对于fopen 的早期实现来说已经足够了(而且效率更高):它们只支持单字符串,检查*mode 是否为rwa。 (见@Keith Thompson's answer。)显然r+ for read+write(不截断)后来出现了。 (有关现代版本,请参阅 fopen(3)。)

    C 确实一个字符数据类型(添加到 B 1971 作为生产胚胎 C 的第一步,所以它在 1972 年仍然是新的。原始 B 没有 char ,是为将多个字符打包成一个单词的机器编写的,所以char() 是一个索引字符串的函数!请参阅 Ritchie 的历史文章。)

    使用单字节字符串实际上是通过 const 引用传递 char,由于库函数不能内联,因此会产生所有额外的内存访问开销。 (原始编译器可能没有内联任何东西,即使是在同一个编译单元中的琐碎函数(不像 fopen)也会缩小总代码大小以内联它们;现代风格的微型辅助函数依赖于现代编译器来内联它们。)


    PS:Steve Jessop 的同样引用的回答启发了我写这篇文章。

    可能相关:strcpy() return valuestrcpy 可能也写得很早。

    【讨论】:

      【解决方案4】:

      我必须说我很感激 - 我知道输入“r”而不是 IO_OPEN_FLAG_R 或者是 IOFLAG_R 或 SYSFLAGS_OPEN_RMODE 或其他什么

      【讨论】:

      • 这其实是一个非常好的点,API 更容易记住。
      • 我必须说我不同意你的观点。我应该能够通过查看函数调用的参数来判断函数调用的作用。你不能只用一个“r”来做到这一点。 “r”是什么意思?如果不阅读有关函数的文档,您将无法判断。
      • @Tuomas Pelkonen:写起来更容易记住,但以后阅读时会很痛苦。由于代码的读取频率比写入频率高得多,因此我会针对读取情况而不是写入情况进行优化。
      • 我同意代码的阅读频率比编写的频率高(我实际上写了一篇关于此的博客),但对我来说,当有一个“r”时,可读性非常好,但我已经编程了用C很久了……
      • 您知道要输入“r”,但如果您输入错误并且代码改为“f”,即使错误,您的代码仍然可以编译。如果您改用命名常量,并且将 IO_READ 错误输入为 IO_FEAD,则会收到编译错误以提醒您注意问题。
      【解决方案5】:

      我推测它是以下一种或多种(不幸的是,我无法快速找到任何类型的支持参考,所以这可能仍然是推测):

      1. Kernighan 或 Ritchie(或为 fopen() 提出接口的任何人)碰巧喜欢使用字符串而不是位图指定模式的想法
      2. 他们可能希望接口与 Unix open() 系统调用接口相似但明​​显不同,因此它会立即熟悉但不会错误地使用为 Unix 而不是 C 库定义的常量编译李>

      例如,假设采用位图模式参数的神话 C 标准 fopen() 使用标识符 OPENMODE_READONLY 来指定文件今天由模式字符串“r”指定。现在,如果有人对在 Unix 平台上编译的程序进行了以下调用(并且包含定义 O_RDONLY 的标头):

      fopen( "myfile", O_RDONLY);
      

      不会出现编译器错误,但除非 OPENMODE_READONLYO_RDONLY 被定义为相同的位,否则您会得到意外的行为。当然,将 C 标准名称定义为与 Unix 名称相同是有意义的,但也许他们希望排除需要这种耦合。

      话说回来,他们可能根本没想过这个……

      【讨论】:

        【解决方案6】:

        我发现最早对fopen 的引用是在 1978 年出版的 Kernighan & Ritchie 的“The C Programming Language”(K&R1) 的第一版中。

        它展示了fopen的示例实现,大概是当时C标准库实现中代码的简化版本。这是书中代码的缩写版本:

        FILE *fopen(name, mode)
        register char *name, *mode;
        {
            /* ... */
            if (*mode != 'r' && *mode != 'w' && *mode != 'a') {
                fprintf(stderr, "illegal mode %s opening %s\n",
                    mode, name);
                exit(1);
            }
            /* ... */
        }
        

        查看代码,mode 应该是一个 1 字符的字符串(不是 "rb",文本和二进制之间没有区别)。如果您传递了一个较长的字符串,则第一个之后的任何字符都会被忽略。如果您传递了无效的mode,该函数将打印一条错误消息并终止您的程序,而不是返回一个空指针(我猜实际的库版本没有这样做)。这本书强调简单的代码而不是错误检查。

        这很难确定,特别是考虑到这本书并没有花很多时间解释mode 参数,但看起来它被定义为一个字符串只是为了方便。单个字符也可以,但字符串至少可以让未来的扩展成为可能(这本书没有提到)。

        【讨论】:

          【解决方案7】:

          丹尼斯·里奇有话要说,来自http://cm.bell-labs.com/cm/cs/who/dmr/chist.html

          特别是,Lesk 写了一个“便携式 I/O 包' [Lesk 72] 是后来的 重做成为C`标准 I/O' 例程

          所以我说问Mike Lesk,在此处发布结果作为您自己问题的答案,并为此获得大量积分。虽然你可能想让这个问题听起来不像批评;-)

          【讨论】:

          • 当 Lesk 编写那个包时,没有像 const char * 这样的数据类型。不用等。根据实现如何处理字符串常量,数据类型可能存在也可能不存在,但 C 程序员无法在程序中指定该数据类型。
          • 是的,但是当strlen 被发明时,const 也不存在,但我认为我们不能由此得出结论,strlen 可能最初采用了其他任何参数比字符串指针 ;-) 只是说“字符串”的典型方式发生了变化。
          • 根据同一篇文档,Lesk写库的时候CPP是不存在的。这消除了 open(2) 样式的标志 ORed 到整数,因此字符串是最不坏的选项之一。见my answer
          【解决方案8】:

          原因很简单:允许 C 实现对模式进行扩展,因为它认为合适。 int 类型的参数不会这样做 C99 Rationale V5-10 7.19.5.3 The fopen function 说例如那个

          文件的其他规范,例如记录长度和块大小,由于其在不同操作环境中的差异很大,因此在标准中未指定。

          可以使用 setvbuf 函数更改文件访问模式和缓冲区大小 (参见第 7.19.5.6 节)。

          实现可以选择允许附加文件规范作为模式字符串参数的一部分。例如,

          file1 = fopen(file1name, "wb,reclen=80");
          

          在提供面向记录的二进制文件并允许 程序员指定记录长度。

          C89 Rationale 4.9.5.3中存在类似文字

          如果使用|ed 枚举标志,那么自然不可能进行这些类型的扩展。

          使用这些参数实现fopen 的一个例子是z/OS。一个例子有以下摘录:

             /* The following call opens:                                                 
                        the file myfile2.dat,                                             
                        a binary file for reading and writing,                            
                        whose record length is 80 bytes,                                  
                        and maximum length of a physical block is 240 bytes,              
                        fixed-length, blocked record format                               
                        for sequential record I/O.                                        
             */                                                                           
          
             if ( (stream = fopen("myfile2.dat", "rb+, lrecl=80,\                         
                blksize=240, recfm=fb, type=record")) == NULL )                           
                printf("Could not open data file for read update\n");      
          

          现在,想象一下,如果您必须将所有这些信息压缩到一个类型为 int 的参数中!!

          【讨论】:

            【解决方案9】:

            正如 Tuomas Pelkonen 所说,这是遗产。

            就我个人而言,我想知道是否有一些被误导的 sap 认为它是因为输入的字符更少而变得更好?在过去,程序员的时间比现在更受重视,因为它不太容易访问,编译器也没有那么好等等。

            这只是推测,但我可以理解为什么有些人会喜欢在这里和那里保存一些字符(注意在任何标准库函数名称中都缺乏冗长......我提出了 string.h 的“strstr”和“strchr”可能是不必要的简洁的最好例子)。

            【讨论】:

            • 库名称中缺乏冗长是因为他们希望支持仅支持外部名称中的 6 个有效字符的系统。记住 C 是很久很久以前定义的,并不是所有的系统都有很好的工具支持。
            • 好点,但这并不能解释 fprintf 和 sprintf。不过,我想这些可能会在以后定义,我承认我现在懒得查阅历史记录。
            • @Platinum: fprintfsprintf 因为它们必须在前 6 个字符中不同。符号允许超过 6 个字符,但如果删除第 6 个字符之后的字符,则必须区分。所以,我想他们本可以让它们更具人类可读性,只要它们以 6 个潜在的 gobbledygook 字符开头。
            • @Martin,我可以在电传打字机上打字比今天快(或更快)。电脑变快了,我变慢了。
            • @Michael Burr:一般规则是具有外部链接的名称在前六个字符中必须是唯一的,不包括大小写差异。这在 C89 中继续进行,尽管基本原理将该决定描述为“最痛苦的”。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2010-09-09
            • 2023-04-02
            • 2019-09-22
            相关资源
            最近更新 更多