C语言,尝试fopen()读写文件为什么会引发下面的“应用程序错误”?

[图片] [图片] [图片] [图片]
关注者
64
被浏览
121,123

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];