思路分析
目的
希望能将给定txt文件的最后一行(无论是空行还是第一行)删掉,并直接保存,而不是另存为某个新的txt文件。
例1:
原文件:
1234567
abcdefg
处理后:
1234567
例2:
原文件:
Hello World!
处理后:
Hello World!
例3:
原文件:
This is an example.
处理后
算法流程
大体思路如下:
- 以只读方式打开原文件。
- 以只写方式打开一个临时文件。
- 通过从原文件末尾向前查找换行符的方式确定最后一行的位置。
- 从原文件开头逐个读取字符,写入临时文件,直到第3步中确定的位置。
- 关闭文件,保存。
- 将原文件删除,将临时文件重命名为原文件的名称。
注:本文只用C语言实现,没有C++的内容。
几个需要注意的点
所用的函数
基本上就这几个:
errno_t __cdecl fopen_s(FILE **_Stream, const char *_FileName, const char *_Mode)
int __cdecl fseek(FILE *_Stream, long _Offset, int _Origin)
fgetc
&fputc
&fclose
&ftell
remove
&rename
具体用法看后面的源码吧,都不难,这里提供一个C++的帮助文档:cplusplus,大部分时候都挺好用的,所有函数都有例程,还可以在线跑例程。
二进制模式与文本模式
在使用fopen_s
打开文件时,可以在第三个参数_Mode
中添加字符b
来开启二进制模式,否则为文本模式。
例如,fopen_s(&oldFile, txtFilePath, "rb")
就是开启了二进制模式。
这两者的区别几乎只在于换行符,在本人的编程环境 Win10 Visual Studio 2019 (v142) 下:
- 二进制模式读取的换行符为2个字符
\r\n
;而文本模式为2个字符\n\n
- 二进制模式写入
\n
再读取为\n
;而文本模式写入\n
再读取为\r\n
,即文本模式将\n
填补为了完整的换行符
可以这么理解:
- 二进制模式下,写入、读取和实际存储的数据,一定相同
- 文本模式下,写入、读取和实际存储的数据,不一定相同
fseek的坑
fseek
的第三个参数_Origin
有以下三个选择:
常量 | 代表的参考位置 |
---|---|
SEEK_SET | 文件开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件末尾 |
(第二个参数_Offset
代表相对于参考位置的偏移量,在此不展开赘述)
有趣的事情来了,使用SEEK_SET
或者SEEK_END
结合适当的偏移量都可以让文件指针换行,但SEEK_CUR
无论偏移多少都无法让文件指针换行,十分地不明所以。
源码
Talk is cheap. Show me the code.
#include <stdio.h>
#include <string.h>
int main()
{
//初始化变量
FILE* oldFile;
FILE* newFile;
char txtFilePath[] = "data.txt";
char tmpFilePath[] = "data_tmp.txt";
//用"r"方式打开文件进行读取,"w"方式打开文件进行写入
//使用二进制模式而不是文本模式打开文件(后面加了"b"),是为了处理起来更准确
//二进制模式可以保证写入什么字符,就是什么字符,主要是在换行符上有作用
//二进制模式下如果写入"\n",就是1个字符"\n",而文本模式下会自动填充成2个字符"\r\n"
if (fopen_s(&oldFile, txtFilePath, "rb") || fopen_s(&newFile, tmpFilePath, "wb")) {
printf("Error while opening file. \n");
return 1;
}
char buffer = 0; //存储读取的字符
//从文件末尾开始向前寻找"\n"换行符,并记录位置
int curPos = 1; //当前光标位置
while (true) {
fseek(oldFile, -curPos, SEEK_END); //移动光标到文件倒数第curPos个字符前
if (ftell(oldFile) == 0) {
//若没有查找到,且光标已经位于文件开头,说明文件只有一行,则退出循环,删除该行
break;
}
buffer = fgetc(oldFile); //读取字符到buffer
//printf("%c", buffer); //输出查看
if (buffer == '\n') {//注:C++中,双引号""表示字符串,单引号''表示字符,字符与字符串不能进行比较运算
//换行符在二进制模式下为2个字符"\r\n",在文本模式下为2个字符"\n\n"
//若查找到了,说明光标处于"\r"和"\n"之间,将光标再前移1次
fseek(oldFile, -curPos - 1, SEEK_END); //将光标定位在倒数第二行的换行符前
//退出循环,删除该换行符及之后的内容
break;
}
else {
//若没有查找到,则继续向前查找
curPos++;
}
}
int endPos = ftell(oldFile); //记录该光标位置
//将从文件开头到endPos的内容复制到临时文件中
fseek(oldFile, 0, SEEK_SET); //当前光标回到文件开头
while (true) {
curPos = ftell(oldFile);
if (curPos == endPos) {
break;
}
buffer = fgetc(oldFile); //读取字符到buffer
fputc(buffer, newFile);
}
//关闭文件,保存
fclose(oldFile);
fclose(newFile);
//删除旧文件,将临时文件名称改为旧文件名称,得到新文件
remove(txtFilePath);
if (rename(tmpFilePath, txtFilePath)) {
printf("Error while renaming file. \n");
return 2;
}
//结束
printf("Successful. \n");
return 0;
}
结语
很久没写C了,其实C++会的也不多……一直都是纯C写得多一点。
C给人的感觉就是一切都尽在掌握,自己会对代码的一举一动了如指掌,以及无比地僵硬(其实我还挺喜欢这种感觉的)。
网上的代码千奇百怪,还是要自己动手,才能写出简洁高效的代码!