为什么fopen函数的第二个参数要传入一个字符串?

19

我一直觉得 C 函数 fopen() 接受一个 const char * 作为第二个参数是很奇怪的。我认为,如果在 stdio.h 中定义了位掩码,例如 IO_READ 等,那么阅读代码和实现库都会更容易,这样你就可以执行以下操作:

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

这样做的编程原因是什么,还是只是历史原因?(即“那就是它现在的样子”)

我一直被C库中这个问题困扰着。 - user16217248
9个回答

12

我相信,与简单的位掩码相比,使用字符串的优点之一是它允许平台特定的扩展,这些扩展不是位设置。纯粹假设:

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

对于这个小玩意儿,open() 调用需要告诉块大小,而不同的调用可以使用完全不同的大小等。尽管 I/O 现在已经被很好地组织起来(原来并非如此——设备多种多样,访问机制远非统一),因此似乎很少需要。但是字符串值的打开模式参数更好地允许了这种可扩展性。
在 IBM 的主机 MVS 操作系统上,fopen() 函数确实需要沿着这里描述的一般线路带有额外的参数——正如 Andrew Henle 所指出的(谢谢!)。手册中包括了这个示例调用(稍作重新格式化):
FILE *fp = fopen("myfile2.dat", "rb+, lrecl=80, blksize=240, recfm=fb, type=record"); 

底层的open()函数必须通过ioctl()(I/O控制)调用或fcntl()(文件控制)函数进行增强,或者使用隐藏它们的函数以达到类似的效果。


谢谢Jonathan。这是关于字符串的另一个我不知道的有趣优点。 - Chris Cooper
请参阅IBM的MVS fopen()文档以获取实际示例。 - Andrew Henle
在Windows上,它可以接收编码:fopen("newfile.txt", "rt+, ccs=encoding") - phuclv
相反,fopen 可以是一个可变参数函数,接受二进制标志和可选的实现定义的额外参数。 - user16217248
@user16217248 相反,fopen 可以是一个可变参数函数... 但目前的 API 不支持。与 open() 不同,O_CREAT 被设置为指示 mode 参数存在的标志,而无法使用传递给 fopen() 的字符串来指示任何类型的扩展参数的存在或不存在,除非将这些字符串的内容扩展到现有标准值之外,并且 这将更改每个人的函数原型,对已编译的代码可能产生未知的后果。如果你要这样做... - Andrew Henle
显示剩余2条评论

9

丹尼斯·里奇(1993年)撰写了一篇C语言历史的文章,介绍了C语言是如何逐渐从B语言演变而来的。一些设计决策是为了避免对已经用B语言或C语言早期版本编写的现有代码进行源代码更改。

特别地,莱斯克编写了一个“可移植I/O包”[Lesk 72],后来被重新制作成C标准I/O例程。

C预处理器直到1972/3才被引入,因此莱斯克的I/O包是在没有它的情况下编写的!(在非常早期的还不是C语言的版本中,在使用的平台上指针适合于整数,并且将一个隐式int返回值分配给指针是完全正常的。)

许多其他变化发生在1972-3年左右,但最重要的是预处理器的引入,部分原因是由于艾伦·斯奈德的建议[Snyder 74]。

没有#include#define,像IO_READ | IO_WRITE这样的表达式就不是一个选项。

如果没有CPP,1972年使用fopen时的选项可能在典型源代码中如下:

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+是后来出现的。(请参见fopen(3)以获取现代版本。)

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

使用单字节字符串相当于通过const-reference传递char,因为库函数无法内联,会带来额外的内存访问开销。(即使是简单的函数(例如fopen)在同一编译单元中也不会被内联,原始编译器可能根本不会内联任何东西;现代风格的小助手函数依赖于现代编译器将它们内联。)


PS:史蒂夫·杰索普(Steve Jessop)使用同样的引用激励了我写这篇文章。

可能相关:strcpy() return valuestrcpy 可能也是早期编写的。


为什么fseek可以使用整数常量(SEEK_SET, SEEK_CUR, SEEK_END),但fopen不能呢? - user16217248
@user16217248:好问题;我很好奇它是否存在于Lesk的原始代码中,后来成为C stdio。如果是后来添加的,那就是显而易见的原因。否则,早期的代码可能在CPP出现之前不得不使用魔术常量整数。或者可能使用较少,并且更改起来不那么痛苦。 - Peter Cordes
无论如何,我认为他们应该在命名常量可用之后尽快更改fopen接口,以免为时已晚(现在有点晚了)。 - user16217248
1
@user16217248:显然,他们非常重视与现有代码库的向后兼容性,即使是在C语言标准化之前编写的代码。就像x86一样,向后兼容性可能是早期成功的原因之一,但现在已经成为一个负担,其设计实际上无法改变。 - Peter Cordes

8

一个词:遗留。不幸的是,我们必须与之共存。

只是猜测:也许在当时,const char * 看起来更灵活,因为它没有任何限制。位掩码只能有32个不同的值。现在看来对我来说似乎是 YAGNI

更多猜测:家伙们很懒,写 "rb" 比写 MASK_THIS | MASK_THAT 更省打字 :)


1
为什么呢?在这里,掩码似乎是自然的选择,特别是在 fopen 设计的时代。 - GManNickG
4
“一个位掩码只有32种不同的值”--在C语言被发明时,一个位掩码只能有16种不同的值。 - Windows programmer
2
@Windows程序员:不,C语言没有位掩码类型。任何整数类型都可以使用(最好是无符号的),而“unsigned long”至少有32位。 - David Thornley
4
在C语言被发明的时候,open()函数的第二个参数类型为int,int类型只有16位,unsigned不存在,long也不存在。open()函数的第二个参数是一个作为位掩码使用的int类型变量。因为这个int类型变量被用作位掩码,所以16位可以表示16个不同的值。 - Windows programmer
2
请注意,在定义和描述标准I/O库的时代(Unix第7版及更早版本),open()系统调用只有两个参数。如果open()因文件不存在而失败,则必须使用名称和模式调用creat()来创建文件。 - Jonathan Leffler
显示剩余7条评论

4

我必须说,我很感激这一点 - 我知道要输入“r”,而不是IO_OPEN_FLAG_R、IOFLAG_R、SYSFLAGS_OPEN_RMODE或其他什么。


4
这实际上是一个非常好的观点,记住API要容易得多。 - Tuomas Pelkonen
我必须说我不同意你的观点。通过查看函数参数,我应该能够知道函数调用的作用。但是仅凭一个“r”是无法做到这一点的。“r”代表什么?如果没有阅读相关函数文档,你是无法知道的。 - Billy ONeal
2
@Tuomas Pelkonen:写代码时记住这个规则很容易,但是当你阅读代码时会感到非常痛苦。由于代码被阅读的频率远高于编写的频率,因此我会优化代码的可读性而不是可写性。 - Billy ONeal
我同意代码被阅读的频率要高于写作的频率(实际上我写过一篇关于此的博客),但对我而言,当代码中有“r”时可读性相当不错,尽管我已经使用C语言编程很长时间了... - Tuomas Pelkonen
2
你知道如何打出“r”,但如果你输错了,你的代码却写成了“f”,即使是错误的,你的代码仍然可以编译通过。如果您改用命名常量,而您将IO_READ误写为IO_FEAD,编译器会发出警报以提醒您存在问题。 - Wyzard
显示剩余3条评论

4
我猜测可能是以下原因之一(不幸的是,我无法快速找到任何支持参考资料,所以这只会保持猜测状态):
1. Kernighan或Ritchie(或者设计fopen()接口的人)只是喜欢使用字符串来指定模式,而不是位图。 2. 他们可能希望接口与Unix系统调用接口类似,但又不会误将常量定义为Unix而不是C库。
例如,假设神话般的C标准fopen()采用了位图模式参数,并使用标识符OPENMODE_READONLY来指定今天由模式字符串"r"指定的文件。现在,如果有人在Unix平台上编译程序并进行以下调用(并且已经包含了定义O_RDONLY的头文件):
fopen( "myfile", O_RDONLY);

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

再说,也许他们根本没有考虑过这个问题...


4

我找到的关于fopen的最早参考资料是在Kernighan和Ritchie的第一版《C程序设计语言》(K&R1)中,出版于1978年。

书中展示了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参数,但看起来它被定义为字符串只是为了方便。单个字符也可以工作,但字符串至少使未来的扩展成为可能(这是该书没有提到的)。

3
丹尼斯·里奇在这里说,引用自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程序员在程序中指定该数据类型。 - Windows programmer
是的,但当 strlen 被发明时,const 也不存在,但我认为我们不能从中得出结论,即 strlen 最初可能接受除字符串指针之外的任何参数;-) 只是说“字符串”的典型方式发生了变化。 - Steve Jessop
根据同一份文件,当Lesk编写该库时,CPP并不存在。这消除了将open(2)风格的标志ORed到整数中的选项,因此字符串是最不错的选项之一。请参见我的答案 - Peter Cordes

3
原因很简单:允许C实现根据需要扩展模式。类型为int的参数无法做到这一点。 C99 Rationale V5-10 7.19.5.3 The fopen function 指出,例如:

Other specifications for files, such as record length and block size, are not specified in the Standard due to their widely varying characteristics in different operating environments.

Changes to file access modes and buffer sizes may be specified using the setvbuf function (see §7.19.5.6).

An implementation may choose to allow additional file specifications as part of the mode string argument. For instance,

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

might be a reasonable extension on a system that provides record-oriented binary files and allows a programmer to specify record length.

类似的文本在C89 Rationale 4.9.5.3中存在。

如果使用了|枚举标志,则这些扩展将不可能。

使用这些参数实现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的参数中!!

0

正如Tuomas Pelkonen所说,这是遗留问题。

就我个人而言,我想知道是否有些误入歧途的人认为由于输入字符较少而更好?在早期,程序员的时间比今天更有价值,因为它不太可访问,编译器也不是很好等等。

这只是猜测,但我可以理解为什么有些人会喜欢在这里和那里节省一些字符(请注意标准库函数名称中的简洁性...我认为string.h的“strstr”和“strchr”是不必要的简洁性的最佳例子)。


4
图书馆名称的简洁是因为他们希望支持仅支持外部名称中的 6 个有效字符的系统。请记住,C 语言是在很久以前定义的,并且并非所有系统都具有良好的工具支持。 - Michael Burr
不错的观点,但这并不能解释fprintf和sprintf。我猜这些可能是后来定义的,不过我承认我现在有点懒得查看历史记录。 - Platinum Azure
@Platinum:fprintfsprintf之所以要区分,是因为它们在前6个字符中必须不同。如果第6个字符后的字符被省略,那么允许符号比6个字符更长,但必须是不同的。因此,我想只要以6个可能的无意义字符开头,它们就可以更易读。 - Michael Burr
@Martin,我在电传打字机上的速度和现在一样快(甚至更快)。计算机变得更快了,而我变慢了。 - Mark Ransom
@Michael Burr:一般规则是具有外部链接的名称在前六个字符内(不计大小写差异)必须唯一。这一规定在C89中得以延续,尽管《Rationale》将其描述为“最痛苦的”决定。 - David Thornley

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