C++重复读取文件最后一行问题&&解决

写C++读文件程序时,会出现最后一行被读取两次的情况。这并非C++的BUG,是因为读完最后一行未到EOF,while判断仍为True,新string抽取失败但跳出中断,延用了上次内容,添加两行代码可解决该问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

#include <iostream>
#include <string>
#include <fstream>
using namespace std;
int main(int argc, char *argv[]) {
	ifstream in_file("test.txt", ios::in);
	if (!in_file) exit(-1);
	string x;
	while (!in_file.eof())
	{	in_file >> x;
		cout << x << endl;
	}
	in_file.close();

	return 0;
}

写一个C++读文件的程序,会发现最后一行被读取了两次

这不是C++的BUG,而是读完最后一行还没到EOF,while判断仍然为True,可也已经读完整个文件,新string抽取失败,但跳出了空中断,延用了上一次的内容。

解决方法:添加两行代码即可

		 in_file.get(); // 读取最后的回车符
		 if(in_file.peek() == '\n') break;

<think>我们有一个任务:在Linux环境下使用C++读取文本文件,移除重复行,并将结果输出到原输入文件。注意:由于我们要将结果写文件,因此需要谨慎处理,避免在读取过程中覆盖原文件。通常的做法是先读取文件内容,处理完毕后将结果写入一个临时文件,然后用临时文件替换原文件。步骤:1.打开文件读取所有行。2.去除重复行。注意:我们可能需要保留行的顺序,或者不保留?题目没有明确,但通常保留第一次出现的顺序。3.将处理后的内容写入一个临时文件。4.将临时文件重命名为原文件。但是,直接写入原文件的风险:如果文件很大,我们可能会在读取文件后,在写入时覆盖原文件,导致如果程序中断,原文件内容丢失。所以使用临时文件是更安全的做法。另外,去除重复行的方法:我们可以使用std::set或std::unordered_set来记录已经出现过的行,但是注意:如果文件很大,内存可能不够。如果文件非常大,可能需要使用外部排序等方法,但这里我们假设文件可以放入内存。如果要求保持行的顺序,那么我们可以遍历每一行,如果该行没有在集合中出现过,则保留并加入集合,否则跳过。具体步骤:1.读取文件:使用ifstream读取。2.逐行读取,存储到一个vector<string>中,同时使用一个set<string>来记录已经出现的行。但注意:我们也可以一边读一边去重,但是要保留顺序,所以需要将不重复的行按顺序存储。3.处理完成后,关闭输入文件。4.将处理后的内容(去重后的行)写入一个临时文件。5.重命名临时文件为原文件名。注意:重命名时,原文件会被替换。为了确保安全,我们可以在写入临时文件后再进行重命名操作。代码结构:注意:错误处理(如文件打开失败等)示例代码:```cpp#include<iostream>#include<fstream>#include<vector>#include<set>#include<string>#include<cstdio>//forstd::removeandstd::renameintmain(intargc,char*argv[]){if(argc!=2){std::cerr<<"Usage:"<<argv[0]<<"<filename>\n";return1;}std::stringfilename=argv[1];std::ifstreaminFile(filename.c_str());if(!inFile.is_open()){std::cerr<<"Erroropeningfile:"<<filename<<std::endl;return1;}std::vector<std::string>uniqueLines;std::set<std::string>seenLines;std::stringline;while(std::getline(inFile,line)){//注意:如果行尾有车,getline会去掉换行符,所以写入时要加换行符//如果该行没有出现过if(seenLines.find(line)==seenLines.end()){seenLines.insert(line);uniqueLines.push_back(line);}}inFile.close();//创建临时文件名,例如在原文件名后加一个后缀std::stringtempFilename=filename+".tmp";std::ofstreamoutFile(tempFilename.c_str());if(!outFile.is_open()){std::cerr<<"Erroropeningtemporaryfile:"<<tempFilename<<std::endl;return1;}//写入去重后的内容for(constauto&uniqueLine:uniqueLines){outFile<<uniqueLine<<'\n';}outFile.close();//替换原文件:删除原文件,然后将临时文件重命名为原文件名//注意:如果删除原文件失败,或者重命名失败,需要处理错误if(std::remove(filename.c_str())!=0){std::cerr<<"Errorremovingoriginalfile\n";return1;}if(std::rename(tempFilename.c_str(),filename.c_str())!=0){std::cerr<<"Errorrenamingtemporaryfile\n";return1;}return0;}```但是,上面的删除原文件操作可能在某些系统上会立即删除文件,如果此时程序中断,则文件可能丢失。更安全的做法是,在写入临时文件后,不立即删除原文件,而是使用重命名将原文件备份,然后将临时文件重命名为原文件。这样,如果重命名成功,再删除备份。不过,上面的代码直接删除了原文件,然后重命名临时文件。这要求原文件必须能够被删除(权限等)。另一种做法是使用`std::rename`来替换文件,但是C++标准库没有直接提供原子替换文件的函数,我们可以使用`rename`函数,它在同一个文件系统下是原子的。修改:我们先将原文件重命名为一个备份文件(比如filename.bak),然后将临时文件重命名为原文件最后删除备份文件。但是,这样如果程序在重命名后删除备份前中断,会留下备份文件。我们可以选择在最后删除备份文件。步骤:1.备份原文件:将原文件重命名为备份文件。2.将临时文件重命名为原文件。3.删除备份文件。代码修改:```cpp//...在写入临时文件后//备份原文件std::stringbackupFilename=filename+".bak";if(std::rename(filename.c_str(),backupFilename.c_str())!=0){std::cerr<<"Errorrenamingoriginalfiletobackup\n";std::remove(tempFilename.c_str());//删除临时文件return1;}//将临时文件重命名为原文件if(std::rename(tempFilename.c_str(),filename.c_str())!=0){std::cerr<<"Errorrenamingtemporaryfiletooriginal\n";//尝试恢复备份std::rename(backupFilename.c_str(),filename.c_str());return1;}//删除备份文件if(std::remove(backupFilename.c_str())!=0){std::cerr<<"Warning:couldnotremovebackupfile:"<<backupFilename<<std::endl;}```但是,这种方法在第二步失败时,我们尝试恢复备份,这样原文件内容不会丢失。不过,恢复操作也可能失败,所以更复杂的场景可能需要事务处理。然而,简单起见,我们也可以使用第一种方法(删除原文件然后重命名临时文件),因为如果删除原文件失败,那么重命名就不会进行,临时文件还存在,我们可以手动恢复。而且,在Unix系统上,重命名是原子的,所以第二种方法中的两个重命名操作不是原子的,可能中间状态会丢失文件。实际上,在Unix/Linux中,我们可以使用`rename`系统调用原子地将临时文件替换原文件。但是,这要求临时文件和原文件在同一个文件系统。所以,我们可以将临时文件创建在和原文件相同的目录下,然后使用`rename`替换。因此,我们最初的代码(删除原文件,然后重命名临时文件)不是原子的,因为删除和重命名是两个操作。而更好的做法是:直接使用`rename`将临时文件重命名为原文件,这会覆盖原文件吗?在Linux中,如果目标文件已经存在,`rename`会原子地替换它(前提是权限允许)。所以,我们可以直接重命名临时文件为原文件,这样会覆盖原文件,并且是原子的。修改:我们不需要先删除原文件,而是直接:std::rename(tempFilename.c_str(),filename.c_str());这样,如果重命名成功,则临时文件成为原文件,原文件的内容被替换。如果重命名失败,原文件还在,临时文件也存在。因此,改进后的代码:```cpp//...写入临时文件后//直接重命名临时文件为原文件(覆盖)if(std::rename(tempFilename.c_str(),filename.c_str())!=0){std::cerr<<"Errorreplacingoriginalfilewithtemporaryfile\n";//可以选择删除临时文件std::remove(tempFilename.c_str());return1;}```这样,整个操作是原子的(在文件系统支持的情况下)。因此,最终代码:```cpp#include<iostream>#include<fstream>#include<vector>#include<set>#include<string>#include<cstdio>//forstd::removeandstd::renameintmain(intargc,char*argv[]){if(argc!=2){std::cerr<<"Usage:"<<argv[0]<<"<filename>\n";return1;}std::stringfilename=argv[1];std::ifstreaminFile(filename.c_str());if(!inFile.is_open()){std::cerr<<"Erroropeningfile:"<<filename<<std::endl;return1;}std::vector<std::string>uniqueLines;std::set<std::string>seenLines;std::stringline;while(std::getline(inFile,line)){if(seenLines.find(line)==seenLines.end()){seenLines.insert(line);uniqueLines.push_back(line);}}inFile.close();std::stringtempFilename=filename+".tmp";std::ofstreamoutFile(tempFilename.c_str());if(!outFile.is_open()){std::cerr<<"Erroropeningtemporaryfile:"<<tempFilename<<std::endl;return1;}for(constauto&uniqueLine:uniqueLines){outFile<<uniqueLine<<'\n';}outFile.close();//用临时文件替换原文件if(std::rename(tempFilename.c_str(),filename.c_str())!=0){std::cerr<<"Errorreplacingoriginalfilewithtemporaryfile\n";//删除临时文件std::remove(tempFilename.c_str());return1;}return0;}```注意:这个程序在读取文件时,会将每行读入,并且使用set来检查重复。set的插入和查找是O(logn),所以总的时间复杂度是O(nlogn),其中n是行数。如果文件行数非常多,可能会比较慢,但通常对于文本文件,行数不会太多。另外,我们使用vector来保存去重后的行,以保持原来的顺序(第一次出现的顺序)。如果文件很大,内存可能不够。这时我们可以考虑使用外部排序和去重,但那样会更复杂。这里假设文件可以完全读入内存。编译和运行:g++-oremove_duplicatesremove_duplicates.cpp./remove_duplicatesmyfile.txt这样,myfile.txt中的重复行将被去除,并保留第一次出现的顺序。注意:空行也会被视为一行,如果空行重复,也会被去除。如果不需要去除空行,可以在处理前判断是否为空行,然后决定是否跳过。但题目没有要求,所以按原样处理。另外,行尾的换行符:getline会去掉换行符,我们写入时使用'\n',所以会使用系统默认的换行符(在Linux下是'\n')。如果原文件是Windows格式(\r\n),则读取后写入会变成Linux格式(\n)。如果希望保留原换行符,则需要在读取时保留换行符信息,但这比较复杂。通常,如果文件是Windows格式,我们可以在读取时以二进制模式打开,然后使用getline读取,但getline在二进制模式下也会去掉换行符(具体平台实现不同)。所以,如果对换行符有要求,需要额外处理。但题目没有要求保留换行符,所以我们可以按照文本模式处理,这样在Linux下是正常的。如果原文件是Windows格式,我们可能希望输出也是Windows格式,那么可以在写入时使用"\r\n"。但是,我们无法从读取的行中知道原换行符是什么。所以,一个简单的方法是:在读取文件时以二进制模式打开,然后逐行读取(包括换行符),然后去重时包括换行符。这样,如果两行内容相同但换行符不同,会被视为不同行。而且,这样我们写入时就可以直接写入原始行(包括换行符)。修改为二进制模式读取:步骤:1.以二进制模式打开输入文件:`std::ifstreaminFile(filename.c_str(),std::ios::binary);`2.使用`getline`读取时,可以指定分隔符,但默认是'\n',这样会读取到'\n'为止,并丢弃'\n'。所以不行。另一种方法:我们使用`std::getline`无法保留换行符,所以我们可以使用另一种方式:一次读取整个文件,然后按行分割(保留换行符)。但这样比较复杂。或者,我们可以自己写一个读取行的函数,保留换行符。但是,为了简单,我们假设文件是Linux格式(即换行符是'\n')。如果需要在Windows下运行,或者处理Windows文件,则需要额外处理。由于题目要求Linux环境,所以我们可以认为换行符是'\n',输出也是'\n'。因此,上面的代码在Linux下是合适的。总结:我们提供了一个C++程序,用于在Linux下去除文本文件重复行并写文件,同时保持行的原始顺序(第一次出现的顺序)。注意:该程序对于大文件可能占用较多内存,如果文件非常大,可能需要优化(例如使用哈希集和磁盘辅助)。现在,根据要求生成相关问题:</think>### 在Linux环境下使用C++实现文本去重并覆盖原文件 以下是完整的C++实现方案,包含详细注释和错误处理: ```cpp #include <iostream> #include <fstream> #include <unordered_set> #include <vector> #include <cstdio> // 用于文件重命名 int main() { const std::string filename = "input.txt"; // 输入文件名 // 1. 读取文件内容 std::ifstream inFile(filename); if (!inFile.is_open()) { std::cerr << "错误:无法打开文件 " << filename << std::endl; return 1; } // 2. 使用哈希集检测重复行并保留顺序 std::vector<std::string> uniqueLines; std::unordered_set<std::string> seenLines; std::string line; while (std::getline(inFile, line)) { // 仅当行未出现过时保存(保留首次出现的顺序) if (seenLines.insert(line).second) { uniqueLines.push_back(line); } } inFile.close(); // 3. 将去重结果写入临时文件 const std::string tempFile = filename + ".tmp"; std::ofstream outFile(tempFile); if (!outFile.is_open()) { std::cerr << "错误:无法创建临时文件" << std::endl; return 1; } for (const auto& uline : uniqueLines) { outFile << uline << '\n'; // 注意:Linux使用\n换行 } outFile.close(); // 4. 原子操作:用临时文件替换原文件 if (std::rename(tempFile.c_str(), filename.c_str()) != 0) { std::cerr << "错误:文件替换失败" << std::endl; std::remove(tempFile.c_str()); // 清理临时文件 return 1; } std::cout << "成功移除重复行并更新原文件" << std::endl; return 0; } ``` ### 关键实现细节 1. **文件处理流程**: - 先读取完整文件 → 去重处理 → 写入临时文件 → 原子替换原文件 - 避免直接覆盖原文件导致数据丢失风险[^1] 2. **去重算法**: - 使用 `std::unordered_set` (O(1) 哈希查找) - 配合 `std::vector` 保留行顺序 - `seenLines.insert(line).second` 返是否为新行 3. **原子文件替换**: - `std::rename()` 在Linux上是原子操作 - 确保操作中断时不会损坏原文件 4. **内存优化**: - 适合中等文件(<1GB) - 超大文件需分块处理或使用外部排序 ### 编译与运行 ```bash # 编译程序 g++ -std=c++11 -O2 -o dedup dedup.cpp # 执行去重 ./dedup ``` ### 性能特征 - 时间复杂度:$O(n)$ 平均(哈希插入) - 空间复杂度:$O(n)$(存储所有唯一行) - 文件限制:最大可处理内存允许的文件大小 > **注意**:此实现会保留空行(空字符串也被视为有效行),如需过滤空行,可在插入前添加 `if (!line.empty())` 判断
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值