如何读取和解析通过在 C++ 中接收 std::istream 对象以逗号分隔的用户输入?

How do I read and parse input from a user that is comma separated by receiving an std::istream object in c++?

我有一个用 c++ 编写的 class 叫做 Airplane。我需要使用 std::istream 创建一个读取函数,让用户在控制台提示后键入以逗号分隔的一行。然后,这行输入将使用逗号分开并分配给 class 的不同私有数据成员。例如,如果用户在控制台中键入“abc,12345,hello”,那么我需要解析该行并将 abc 分配给一个变量,将 12345 分配给另一个变量,将 hello 分配给最后一个变量。我相信在用户输入“123,abc,hello”后,该行存储在某处,我可以通过某种方式使用 istream 访问它?

我目前的情况如下:

std::istream& Airplane::read(std::istream& in) {
   if (comma_separated == true) {
   // parse the line inputted by the user and then assign it to 3 variables
   // after getting the input somehow assign to variables
   this->first_var = info_before_first_comma;
   this->second_var = second_comma_text;
   etc...
   }
}

我相信我还需要某种重载运算符函数来将 class 传递给,然后调用上面的读取函数来处理 class 数据。可能像下面这样的东西?

std::istream& operator>>(std::istream& output, Airplane& airplane) {}

这样我就可以创建一个 class,然后调用 cin >> class_name,它会接收输入、处理它,并将其分配给那个 classes 变量。 tldr:我需要从控制台读取用户输入并根据逗号分隔文本,然后分配给变量。我的困惑是我不知道从哪里开始或如何真正从用户那里得到“123,abc,你好”行来处理。感谢阅读。

更新信息 下面给出的代码运行(选择示例 3),但没有给出正确的结果。我调用 cin >> classname 并输入“1234,abcdaef,asdasd”,然后按回车键。然后我调用 cout << classname 并打印其存储的旧数据并忽略给定的输入。

当我尝试执行以下操作以检查令牌是否正在存储数据时:

            std::cout << token[0] << std::endl;
            std::cout << token[1] << std::endl;
            std::cout << token[2] << std::endl;

我收到调试“向量下标超出范围”错误。

这就是我将 3 个值存储到我的私有数据成员中的方式,我有一个 int 和 2 个 char 数组。

                this->store_int = std::stoi(token[0]);

                this->store_first_char = new char[token[1].length() + 1];
                strcpy(this->store_first_char, token[1].c_str());

                this->store_second_char = new char[token[2].length() + 1];
                strcpy(this->store_second_char, token[2].c_str());

但这也没有用。我忘记澄清的一件事是,如果重要的话,最后总是有一个逗号。谢谢。

让我们一步步来。

首先,也是最重要的,一个完整的输入行将使用函数 std::getline 读取。此函数将从 std::istream 中读取完整的一行并将其放入 std::string.

然后,我们需要将完整的字符串拆分成子字符串。子字符串以逗号分隔。最后,我们将有一个包含所有子字符串的 STL 容器。

然后我们进行完整性检查并查看拆分字符串后得到的子字符串的数量。如果计数正常,那么我们要么直接存储字符串,要么将它们转换为所需的数据类型,例如 intfloat.

由于std::getline一行的读法简单,我们先集中精力拆分字符串。这也称为标记字符串。

我将向您展示几种不同的方法来标记字符串:

将字符串拆分为标记是一项非常古老的任务。有许多可用的解决方案。都有不同的属性。有的难懂,有的难开发,有的比较复杂,慢的或快的或灵活的或不灵活的。

备选方案

  1. 手工制作,许多变体,使用指针或迭代器,可能难以开发且容易出错。
  2. 使用旧式 std::strtok 函数。也许不安全。也许不应该再使用了
  3. std::getline。最常用的实现。但实际上是一种“误用”,并没有那么灵活
  4. 使用专门为此目的开发的专用现代功能,最灵活,最适合 STL 环境和算法环境。但是比较慢。

请在一段代码中查看4个示例。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <regex>
#include <algorithm>
#include <iterator>
#include <cstring>
#include <forward_list>
#include <deque>

using Container = std::vector<std::string>;
std::regex delimiter{ "," };


int main() {

    // Some function to print the contents of an STL container
    auto print = [](const auto& container) -> void { std::copy(container.begin(), container.end(),
        std::ostream_iterator<std::decay<decltype(*container.begin())>::type>(std::cout, " ")); std::cout << '\n'; };

    // Example 1:   Handcrafted -------------------------------------------------------------------------
    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
        Container c{};

        // Search for comma, then take the part and add to the result
        for (size_t i{ 0U }, startpos{ 0U }; i <= stringToSplit.size(); ++i) {

            // So, if there is a comma or the end of the string
            if ((stringToSplit[i] == ',') || (i == (stringToSplit.size()))) {

                // Copy substring
                c.push_back(stringToSplit.substr(startpos, i - startpos));
                startpos = i + 1;
            }
        }
        print(c);
    }

    // Example 2:   Using very old strtok function ----------------------------------------------------------
    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
        Container c{};

        // Split string into parts in a simple for loop
#pragma warning(suppress : 4996)
        for (char* token = std::strtok(const_cast<char*>(stringToSplit.data()), ","); token != nullptr; token = std::strtok(nullptr, ",")) {
            c.push_back(token);
        }

        print(c);
    }

    // Example 3:   Very often used std::getline with additional istringstream ------------------------------------------------
    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
        Container c{};

        // Put string in an std::istringstream
        std::istringstream iss{ stringToSplit };

        // Extract string parts in simple for loop
        for (std::string part{}; std::getline(iss, part, ','); c.push_back(part))
            ;

        print(c);
    }

    // Example 4:   Most flexible iterator solution  ------------------------------------------------

    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };


        Container c(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {});
        //
        // Everything done already with range constructor. No additional code needed.
        //

        print(c);


        // Works also with other containers in the same way
        std::forward_list<std::string> c2(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {});

        print(c2);

        // And works with algorithms
        std::deque<std::string> c3{};
        std::copy(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {}, std::back_inserter(c3));

        print(c3);
    }
    return 0;
}

因此,在有了初始字符串(如“abc,12345,hello”)之后,我们现在将拥有一个 std::string 的容器,例如std::vector 包含子字符串:So, "abc","12345" 和 "hello".

“abc”和“hello”可以直接存储(赋值)到您的class的字符串变量中。 “12345”必须使用现有函数(例如 std::stoi)转换为 int 并分配给成员变量。

最后一步是在 class(或结构)中使用所有这些。

这看起来像这样的例子:

struct MyData {
    // Our data
    std::string item1{};
    int value{};
    std::string item2{};
    
    // Overwrite extractor operator
    friend std::istream& operator >> (std::istream& is, MyData& md) {
        if (std::string line{};std::getline(is, line)) {

            // Here we will store the sub strings
            std::vector<std::string> token{};

            // Put in an istringstream for further extraction
            std::istringstream iss{ line };
            
            // Split
            for (std::string part{}; std::getline(iss, part, ','); c.push_back(part))
                ;

            // Sanity check
            if (token.size() == 3) {
            
                // Assigns value to our data members
                md.item1 = token[0];
                md.value = std::stoi(token[1]);
                md.item2 = token[2];
            }

        }
        return is;
    }
};

很抱歉,但这是未编译的、未经测试的代码。它应该让您了解如何实施它。

现在您可以使用 std::iostream 将数据导入您的结构。

MyData md;
std::cin >> md;

希望我能回答你的问题。如果没有,请问。