在 C++ 中读取文件并在行之间进行比较

Reading a file in c++ and comparing between lines

假设 file.txt 包含如下随机文件名:

a.cpp
b.txt
c.java
d.cpp
...

我的想法是从文件名中分离出文件扩展名作为子字符串,然后比较扩展名以查找重复项。
这是我的代码:

#include<iostream>
#include<fstream>
#include<string>
using namespace std;

    int main()
    {

    ifstream infile;   
    infile.open("file.txt"); 

    string str,sub;
    int count,pos=0;

    while(infile>>str)  
    {
    pos=str.find(".");
    sub=str.substr(pos+1);
    
    
    if(sub==?)
        // I stopped here
        count++;  
    
    }
    cout<<count;    
    return 0;
    }

我是 C++ 的新手,所以我不知道使用哪个函数来跳转到下一行,我搜索了很多,但没有找到。

您可以使用以下程序打印输入文件中每个扩展名对应的计数。该程序使用 std::map 来跟踪计数。


#include <iostream>
#include <map>
#include <fstream>

int main()
{
   
    std::ifstream inputFile("input.txt");
    
    std::map<std::string, int> countExtOccurence; //this will count how many time each extension occurred
    
    std::string name, extension;
    if(inputFile)
    {
        while(std::getline(inputFile, name, '.')) //this will read upto a . occurrs 
        {
            std::getline(inputFile, extension, '\n');
            {
                countExtOccurence[extension]++; //increase the count corresponding to a given extension
            }
        }
    }
    else 
    {
        std::cout<<"input file cannot be opened"<<std::endl;
    }
    inputFile.close();
    
    //lets print out how many times each extensino occurred in the file 
    for(const std::pair<std::string, int> &pairElem: countExtOccurence)
    {
        std::cout<<pairElem.first<<" occurred: "<<pairElem.second<<" time"<<std::endl;
    }
    return 0;
}

上面程序的输出可见here.

好的,您要读取存储在文件中的文件名,然后获取扩展名的计数。

这看起来很简单,其实不然。原因是,现在的文件名可以包含各种特殊字符。里面可能有space,还有多个点('.')。根据文件系统的不同,可能会有斜杠“/”(如 Unix/Linux 中)或反斜杠“\”(如 Windows 系统中)作为分隔符。还有没有扩展名的文件名和以句点开头的特殊文件。 (如“.profile”)。所以基本上没那么容易。

即使只有文件名,您至少应该从字符串的右端搜索点“.”(可能)表示文件扩展名。永远不要从左侧。因此,在您的情况下,您应该使用 rfind 而不是 find.

现在,对于你的问题,如何阅读下一行。您使用格式化输入函数的方法将适用于示例源文件中显示的文件名,但如果文件名中有 spaces,则将不起作用。例如,您的语句 infile>>str 将在第一个白色 space 字符后停止转换。

示例:文件名为“Hello World.txt”,那么“str”将仅包含“Hello”,下一次读取将包含“World.txt”。因此,您应该阅读带有专用函数 std::getline 的完整行。请阅读说明 here

这样你就可以逐行阅读:while(std::getline(inputFile,str).

然后,以后你可以拆分扩展名并计算它。

对于扩展的拆分,我已经给了你一些提示和一些注意事项。但是,非常好,C++ 有一个现成的解决方案供您使用。 filesystem-库描述了 here。这里有您需要的一切,随时可用。

特别有用的是path type, which has a function extension。这将为您完成所有细节。

因为它确实如此,我强烈推荐使用它。

现在,生活变得简单了。请参阅以下示例:

#include <iostream>
#include <string>
#include <filesystem>

// Namespace alias to save a lot of typing work . . .
namespace fs = std::filesystem;

int main() {
    // Read any kind of filename from the user
    std::string line{};   std:getline(std::cin, line);

    // Print the extension
    std::cout << fs::path{ line }.extension().string();
}

因此,不用担心操作系统和任何类型的文件名。它会简单地为您做所有的基础工作。


接下来,数数。

有一种或多或少的标准方法可以计算容器中的某些东西或输入给定的东西,然后可以另外获取并显示其排名。所以,按出现频率排序。

对于计数部分,我们可以使用关联容器,如 std::mapstd::unordered_map。在这里,我们将一个“键”(在本例中为“扩展名”)与一个值(在本例中为特定“扩展名”的计数)相关联。

幸运的是,基本上选择这种方法的原因是两张地图都有一个非常好的索引 operator[]。这将查找给定的键,如果找到,return 对计数的引用。如果未找到,则它将使用键(“扩展名”)和 return 对新条目的引用创建一个新条目。因此,在这两种情况下,我们都将获得对用于计数的值的引用。然后我们可以简单地写:

std::unordered_map<std::string, int> counter{};
counter[extension]++;

这看起来非常直观。

经过这个操作,你已经有了频率table。使用 std::map 按键(单词)排序或未排序,但使用 std::unordered_map.

可更快访问

在您的情况下,如果您只对计数感兴趣,建议使用 std::unordered_map,因为不需要按键对 std::map 中的数据进行排序,以后就不用了这种排序。


那么,也许您想根据 frequency/count 进行排序。如果您不想这样做,请跳过以下内容:

不幸的是,无法按值对地图进行排序。因为 map 的主要 属性 - 容器系列是它们对 key 的引用,而不是值或计数。

因此我们需要使用第二个容器,例如 std::vector 等,然后我们可以使用 std::sort 对任何给定谓词进行排序,或者,我们可以将值复制到容器,就像隐式排序其元素的 std::multiset 一样。因为这只是一个衬里,所以这是推荐的解决方案。

此外,因为为 std 容器编写所有这些长名称,我们使用 using 关键字创建别名。

在我们得到单词的排名后,单词列表按其计数排序,我们可以使用迭代器和循环来访问数据并输出它们。


因为您想从文件中读取,所以我还想提供有关打开和关闭文件(流)的附加信息。

如果您了解 ifstream,那么您会发现它有一个构造函数,它将文件名作为输入,还有一个析构函数,它将自动为您关闭文件。通过构造函数打开的文件将 return 一个具有状态的文件流变量。顺便说一下,这对任何流都是如此。

背景是,它的bool-operator is overwritten and will return the state of the stream. Also the not-运算符!被覆盖了,可以使用了。由于这些被覆盖的运算符,您可以编写类似 if (filestream) 的内容来查看是否可以打开文件。

此外,从 C++ 17 开始,我们有一个扩展的 if 语句,您可以在其中的条件前面使用初始化列表。这很重要,因为它允许我们定义一个变量,稍后将对其进行检查,但范围仅限于 if 复合语句。在大多数情况下,强烈推荐使用它。示例:

// Open a file and check, if it could be opened
if (std::ifstream infile("file.txt"); infile) {

   // ....   Do things fith file stream

} // Here the file will be closed automatically by the destructor

比不必要的 openclose 语句好多了。


现在,在我们考虑了设计之后,现在我们可以开始编写代码了。之前没有。

所以,我们现在将得到:

#include <iostream>
#include <fstream>
#include <string>
#include <filesystem>
#include <unordered_map>
#include <set>
#include <type_traits>
#include <utility>

// ------------------------------------------------------------
// Create aliases. Save typing work and make code more readable
using Pair = std::pair<std::string, unsigned int>;

// Standard approach for counter
using Counter = std::unordered_map<Pair::first_type, Pair::second_type>;

// Sorted values will be stored in a multiset
struct Comp { bool operator ()(const Pair& p1, const Pair& p2) const { return (p1.second == p2.second) ? p1.first<p2.first : p1.second>p2.second; } };
using Sorter = std::multiset<Pair, Comp>;

// Namespace alias
namespace fs = std::filesystem;
// ------------------------------------------------------------


int main() {

    // Open the source file and check, if it could be opened
    if (std::ifstream inFileStream{ "r:\file.txt" }; inFileStream) {

        // Here we will count the extensions of the file names
        Counter counter{};

        // Read source file strings and count the extensions
        std::string line{};
        // Read all lines from file
        while (std::getline(inFileStream, line))

            // Get extensions and count them
            counter[ fs::path{ line }.extension().string() ]++;

        // Show result to the user. 
        for (const Pair& p : counter) std::cout << p.first << " --> " << p.second << '\n';

    } // File will be closed here
    else {
        // file could not be opened
        std::cerr << "\n\n*** Error: Input file could not be opened\n\n";
    }
}

函数 main 中只有 8 个语句,我们可以完成所有需要的任务,包括所有类型的路径格式和错误处理。

还有更多优化的可能。

正如我提到的,缩小变量范围总是一个好主意。在上面的代码中,我们可以看到变量“line”定义在while 循环的外部作用域中。那是没有必要的。因为我们的 forwhile 循环基本相同,所以我们可以更好地使用 for 循环,因为它有一个初始化部分。

而不是

std::string line{};
        // Read all lines from file
        while (std::getline(inFileStream, line))

我们可以写

        for  (std::string line{};std::getline(inFileStream, line);  )

我们甚至可以夸张一点,在 for 循环的迭代表达式部分进行计数。并在一个 for 语句

中完成整个读取和计数
        for (std::string line{}; std::getline(inFileStream, line); counter[fs::path{ line }.extension().string()]++)
            ;

所以,一行代码一条语句完成文件的完整读取和各种扩展名的完整统计。哇!

但是可读性有点低,我们不会使用它。


在输出语句中,我们可以做一些更具可读性的东西。基本上 Pair.first.second 不是很好理解。 C++ 也有一个解决方案。它被称为structured binding

综上所述,我们现在进入最终实现,包括按频率排序的输出:

#include <iostream>
#include <fstream>
#include <string>
#include <filesystem>
#include <unordered_map>
#include <set>
#include <type_traits>
#include <utility>

// ------------------------------------------------------------
// Create aliases. Save typing work and make code more readable
using Pair = std::pair<std::string, unsigned int>;

// Standard approach for counter
using Counter = std::unordered_map<Pair::first_type, Pair::second_type>;

// Sorted values will be stored in a multiset
struct Comp { bool operator ()(const Pair& p1, const Pair& p2) const { return (p1.second == p2.second) ? p1.first<p2.first : p1.second>p2.second; } };
using Sorter = std::multiset<Pair, Comp>;

// Namespace alias
namespace fs = std::filesystem;
// ------------------------------------------------------------


int main() {

    // Open the source file and check, if it could be opened
    if (std::ifstream inFileStream{ "r:\file.txt" }; inFileStream) {

        // Here we will count the extensions of the file names
        Counter counter{};

        // Read all lines from file
        for (std::string line{}; std::getline(inFileStream, line); )

            // Get extensions and count them
            counter[ fs::path{ line }.extension().string() ]++;

        Sorter sorter(counter.begin(), counter.end());

        // Show result to the user. 
        for (const auto& [extension, count] : sorter) std::cout << extension << " --> " << count << '\n';

    } // File will be closed here
    else {
        // file could not be opened
        std::cerr << "\n\n*** Error: Input file could not be opened\n\n";
    }
}