C语言,尝试fopen()读写文件为什么会引发下面的“应用程序错误”?
18 个回答
这么烂的程序我竟然看了,我现在是有多淡的难受啊。。。唉。。。
言归正传,说问题吧:
首先你这个程序错误太多了,很容易看出来,很多人也说给你了, 但是大部分人没有说出导致上图“应用程序错误”那个弹窗的根源,就是这行:
p = fopen(argv[0], "w+");
你写个程序,格式乱也就算了。
赋值q判断p,赋值p判断q也忍了。
没有初始化临时变量a就做判断也忍了。
给fgetc()和fputc()用char型变量a也忍了。
但是一个函数你不能好好看看文档再用吗?fopen()的"w+"是什么意思你查了吗就瞎用?!因为我手头只有Linux系统,让我们看一下Linux下fopen的文档是怎么描述他的mode参数的:
# man fopen
┌─────────────┬───────────────────────────────┐
│fopen() mode │ open() flags │
├─────────────┼───────────────────────────────┤
│ r │ O_RDONLY │
├─────────────┼───────────────────────────────┤
│ w │ O_WRONLY | O_CREAT | O_TRUNC │
├─────────────┼───────────────────────────────┤
│ a │ O_WRONLY | O_CREAT | O_APPEND │
├─────────────┼───────────────────────────────┤
│ r+ │ O_RDWR │
├─────────────┼───────────────────────────────┤
│ w+ │ O_RDWR | O_CREAT | O_TRUNC │
├─────────────┼───────────────────────────────┤
│ a+ │ O_RDWR | O_CREAT | O_APPEND │
└─────────────┴───────────────────────────────┘
...
所以如果你想读程序本身(argv[0])的内容,那你使用"w+"是什么鬼? “w+”相当于“O_RDWR | O_CREAT | O_ TRUNC”,O_CREAT虽然只会尝试在文件不存在时创建文件,但是那个O _TRUNC可是一定会去truncate要打开的文件的,你对一个正在执行的文件使用这样的操作是想干什么?'r'还不够满足你吗?
(感谢评论区chk同学指出问题)我之前因为疏忽,以为是在程序尝试Truncate一个正在执行的文件时崩溃了,但实际上是因为我少写了return,而没注意程序继续向下执行了。实际上第二个fopen没有选择crash掉,而是选择了返回错误。也就是说第二个fopen没有成功,返回了NULL,下面对这个空指针的访问触发了程序的crash。不过不管怎么说,第二个fopen是罪魁祸首,后面程序对错误检查没有做到位导致NULL被传递了下去。
从字面上看题主似乎是想读取程序本身(argv[0])的内容到"C:\\q.exe"。但是a=fgetc()后判断返回值是不是a!='\0'是什么鬼?你好好看fgetc()的文档了吗?
(我最近的一个回答稍微说到了fgetc()的问题,感兴趣的朋友可以看看:
)
如果你真的想学习,在你理直气壮地问问题之前,请先对你自己程序的内容有足够的认知。
看到这么不认真的学生就生气。在你正式走上程序员道路前还是好好学习吧,避免以后落个“你是我司专门雇来写Bug的吧?”的名号。
最后附上一个简易修改后的,尽量还原你原意的程序:
// foo.c
// Usage: foo <target file>
#include <stdio.h>
int main(int argc, char *argv[])
FILE *src;
FILE *dest;
int c = 0;
printf("transform %s to %s\n", argv[0], argv[1]);
dest = fopen(argv[1], "w+");
if (!dest)
perror("fopen foo");
src = fopen(argv[0], "r");
if(!src)
perror("fopen myself");
while(1) {
c = fgetc(src);
if (!feof(src))
fputc(c, dest);
break;
printf("- DONE -\n");
return 0;
}
测试,尝试程序不断复制自己并执行:
# gcc -o foo0 foo.c -Wall
# for ((i=0; i<10; i++));do ./foo${i} foo$((i+1)) && chmod +x foo$((i+1));done
transform ./foo0 to foo1
- DONE -
transform ./foo1 to foo2
- DONE -
transform ./foo2 to foo3
- DONE -
transform ./foo3 to foo4
- DONE -
transform ./foo4 to foo5
- DONE -
transform ./foo5 to foo6
- DONE -
transform ./foo6 to foo7
- DONE -
transform ./foo7 to foo8
- DONE -
transform ./foo8 to foo9
- DONE -
transform ./foo9 to foo10
- DONE -
# ls -l foo*
-rwxrwxr-x. 1 test test 8520 Jun 5 17:47 foo0
-rwxrwxr-x. 1 test test 8520 Jun 5 17:48 foo1
-rwxrwxr-x. 1 test test 8520 Jun 5 17:48 foo10
-rwxrwxr-x. 1 test test 8520 Jun 5 17:48 foo2
-rwxrwxr-x. 1 test test 8520 Jun 5 17:48 foo3
-rwxrwxr-x. 1 test test 8520 Jun 5 17:48 foo4
-rwxrwxr-x. 1 test test 8520 Jun 5 17:48 foo5
-rwxrwxr-x. 1 test test 8520 Jun 5 17:48 foo6
-rwxrwxr-x. 1 test test 8520 Jun 5 17:48 foo7
-rwxrwxr-x. 1 test test 8520 Jun 5 17:48 foo8
-rwxrwxr-x. 1 test test 8520 Jun 5 17:48 foo9
-rw-rw-r--. 1 test test 398 Jun 5 17:46 foo.c
我只是尽量还原你的程序的可能的原意。单论自我拷贝,这并不是一个好方法。如果上面的你看不懂,请持续学习基础知识再来问。
补充一:
谢谢有朋友在评论里指出Windows系统读取二进制文件时最好使用rb来代替r。因为我用的是Linux系统,Linux的fopen文档并没有特别提到b这个参数,我查了IEEE标准,对fopen的说明:
其中对参数b也没有过多说明,好像只有一句:
The character 'b' shall have no effect, but is allowed for ISO C standard conformance.
所以我猜测Windows系统可能用b参数来解决它换行回车双字符的问题。我现在没有windows系统,测试不了,有兴趣的朋友可以试一下。不过如果windows有这样的需求,那在这里使用rb来替换r可以在保持程序符合通用标准的前提下增加程序的兼容性,是不错的选择。
补充二:
没想到竟然有朋友在纠结我上面随便写的while(1)循环,和for(;;)比较哪样写好?其实这样的比较和过多的争论没有必要,这两个基本上是一个东西。
我现在手头没有电脑,只好用手机链接我的服务器给大家测试一下看看了,因为是手机,我不方便贴代码,我就直接截图了,测试结果截图如下(我连成长图了):
为了方便辨认,我在两个不同函数中一个使用while(1)循环,一个使用for(;;)循环。编译为汇编语言后完全一样(红圈部分),没有谁比谁多执行几条语句。后来我又试了下O2优化编译,结果还是两种方式一样。
所以不用再纠结哪种语法表述更好更帅了,我觉得如果你所参与的项目倾向于哪个你就用哪个,如果是新项目,那就根据自己喜好吧,没必要为这么个没意义的事儿浪费时间。
补充三:
服了……又开始纠结while (1)好不好看的问题了…… 我觉得有时间的话还是不要纠结这些无关痛痒的事情比较好吧,即使是被你们“崇拜”的Linux内核项目,你们知道里面有多少while (1),有多少while (true),又有多少for (;;)吗?见下图。
所以你们眼中的“大神”们都是不在乎这些纯扯皮的东西的。自己喜欢,maintainer也不介意就行了。
谢邀。
首先我要怼人。
我不想批评你菜的问题——我也菜。但是p和q写反了自己都没发现,就先提问…怎么说也不太合适吧。
而且,提问代码问题的基本操作不是贴代码吗…为什么只有截图…
好了,我们进入正题。
能看出来你是要写一个C语言下的自我复制…
但是这段代码中…坑太多了我们一点点说…
首先是fopen的第二个参数……
我知道你可能不太明白——我也弄不明白…但是至少上网查一下…
File access mode flag "b" can optionally be specified to open a file in binary mode.
我们要写的并不是一个纯文本的文件,所以这里最好还是把“w+”改成"wb+"或者"wb"会比较合适吧…
而且写C盘需要管理员权限…这点在运行时请注意一下…
然后是读文件那一侧——正在运行的文件是受到写保护的,所以我们没有办法以包含“读”权限的模式打开它。
那么这一侧的"w+"应该改成"rb"才对…
而如果程序没有被写保护(实际上并不会这样),那么问题会变得更加严重——
"w+"的打开模式,在文件已存在的情况下,语义是“清空文件并打开”…
不过,只有这些是不会引起程序崩溃的…
在您的程序中还有一个很重要的问题:
您根本没有靠谱的错误处理。
是的,您检查了p和q是不是null pointer,如果不是就输出一个OK。
之后呢?
如果是null pointer不应该直接return 0中止程序吗?
带着无效的地址继续跑下去,那么为什么要设计这个错误处理呢。
然后,是那个坑人的fgetc和fputc。
我不知道您们老师是怎么讲的——但是任何一个能在大学任教职的老师都不可能教您用\0来判断文件结尾——因为\0根本就不是文件结尾的标识;它只是C语言约定的可读字符串结尾…
以及,虽然很多人都忘了,但是也不会有老师教您用char来接收fgetc的返回值的。
说到底…fgetc的返回值类型是int啊…
Return value
The obtained character on success or EOF on failure.
所以说…把您代码中的a定义成一个int,而且把循环条件替换成a!=EOF才是正常的做法啊…(需要注意的是在读入后先判断啊)
尤其是您贴的后一张图…for(;1;;)这种东西都出现了…循环体内还没有break…
您是准备写一个常驻程序么…
最后再说一个小小的意见…argv[0]不一定等于完整的程序名,这是非常容易坑人的一点…
If argv[0] is not a null pointer (or, equivalently, ifargc> 0), it points to a string that represents the name used to invoke the program, or to an empty string.
不过先无视这一点,我们整理出了如下的代码
#include <stdio.h>
int main(int argc,char *argv[]){
FILE *p;
FILE *q;
q=fopen("c:\\q.exe","wb+");
if(q){
puts("OK");
}else{
perror("Open destination file");
return 0;
printf("%s\n",argv[0]);
p=fopen(argv[0],"rb");
if(p){
puts("OK");
}else{
perror("Open source file");
return 0;
int a;
for(;1;){
a=fgetc(p);
if(EOF==a)break;
fputc(a,q);
}
然后说两句题外话
如果不是一定要坚持纯C的话,我们实际上可以有一些更加简洁的做法…
比如说,利用c++17的特性,我们可以如下操作
#include <filesystem>
int main(int argc,char *argv[]){
try{
std::filesystem::copy(argv[0],"e:\\w.exe");
}catch(std::exception &e){
puts(e.what());
return 0;
而如果不准备做跨平台的话(废话,c:\\q.exe这种路径怎么跨平台),我们也可以用Win32 API来实现。
#include <Windows.h>
#include <stdio.h>
int main(int argc,char *argv[]){
wchar_t Src[MAX_PATH+1];