如何在C++中逐行迭代cin输入?

38

我想逐行迭代std::cin,将每一行表示为std::string。哪种方法更好?

string line;
while (getline(cin, line))
{
    // process line
}
或者
for (string line; getline(cin, line); )
{
    // process line
}

正常的做法是什么?

5个回答

81

既然UncleBen提出了他的LineInputIterator,我想再添加几种替代方法。首先是一个非常简单的类,它作为字符串代理:

class line {
    std::string data;
public:
    friend std::istream &operator>>(std::istream &is, line &l) {
        std::getline(is, l.data);
        return is;
    }
    operator std::string() const { return data; }    
};

使用这种方法,你仍然可以使用普通的istream_iterator进行读取。例如,要将文件中的所有行读入一个字符串向量中,你可以使用以下代码:

std::vector<std::string> lines;

std::copy(std::istream_iterator<line>(std::cin), 
          std::istream_iterator<line>(),
          std::back_inserter(lines));
关键点是当您读取时,指定一行。否则,你只有字符串。
另一个可能性使用标准库中大多数人甚至不知道存在的部分,更别说实际用途了。使用operator>>读取字符串时,流会返回一个字符字符串,直到该流的区域设置指定的空格字符为止。特别是如果您在进行所有基于行的工作,那么创建一个仅将新行分类为空格的ctype facet的区域设置可以很方便。
struct line_reader: std::ctype<char> {
    line_reader(): std::ctype<char>(get_table()) {}
    static std::ctype_base::mask const* get_table() {
        static std::vector<std::ctype_base::mask> 
            rc(table_size, std::ctype_base::mask());

        rc['\n'] = std::ctype_base::space;
        return &rc[0];
    }
};  

使用这个功能,您需要用该facet为将要从中读取的流注入locale,然后正常地读取字符串,对于字符串的operator>>读取整行。例如,如果我们想要读取行,并按排序顺序写出唯一的行,我们可以使用以下代码:

int main() {
    std::set<std::string> lines;

    // Tell the stream to use our facet, so only '\n' is treated as a space.
    std::cin.imbue(std::locale(std::locale(), new line_reader()));

    std::copy(std::istream_iterator<std::string>(std::cin), 
        std::istream_iterator<std::string>(), 
        std::inserter(lines, lines.end()));

    std::copy(lines.begin(), lines.end(), 
        std::ostream_iterator<std::string>(std::cout, "\n"));
    return 0;
}

请注意,这会影响流中的所有输入。使用这个选项基本上排除了将行定向的输入和其他输入混合在一起(例如,使用stream>>my_integer从流中读取一个数字通常会失败)。


谢谢 - 我也有点喜欢它。 :-) - Jerry Coffin
3
我知道有一种方法可以指定在哪个字符停止……嗯,我还是有点希望有更简单的方法……我想知道这种语言是如何变得如此粗糙的,有时候这让我感到难过。代理加1分。易于编写、易于使用,而且更加灵活。 - Matthieu M.
4
伙计们,他的名字是cppLearner。确实如此。 - bobobobo
这个被引用很多的例子,在我这里不会编译,除非我把 operator std::string() 改成 const:https://ideone.com/xcJ4Z vs https://ideone.com/YY8cQ - Cubbi
@Cubbi:嗯...我不确定这个问题怎么会一直被忽略,但我已经修复了它。 - Jerry Coffin
这个"facet"技巧确实很巧妙。我为了不同的目的借鉴它,只是想看看它是否可行,结果它确实可行!https://stackoverflow.com/questions/46439291/parsing-a-csv-file-consisting-numbers-with-characters-in-between-c/46439752#46439752 - Retired Ninja

8

我所拥有的(作为一项练习,但或许有一天会变得有用)是LineInputIterator:

#ifndef UB_LINEINPUT_ITERATOR_H
#define UB_LINEINPUT_ITERATOR_H

#include <iterator>
#include <istream>
#include <string>
#include <cassert>

namespace ub {

template <class StringT = std::string>
class LineInputIterator :
    public std::iterator<std::input_iterator_tag, StringT, std::ptrdiff_t, const StringT*, const StringT&>
{
public:
    typedef typename StringT::value_type char_type;
    typedef typename StringT::traits_type traits_type;
    typedef std::basic_istream<char_type, traits_type> istream_type;

    LineInputIterator(): is(0) {}
    LineInputIterator(istream_type& is): is(&is) {}
    const StringT& operator*() const { return value; }
    const StringT* operator->() const { return &value; }
    LineInputIterator<StringT>& operator++()
    {
        assert(is != NULL);
        if (is && !getline(*is, value)) {
            is = NULL;
        }
        return *this;
    }
    LineInputIterator<StringT> operator++(int)
    {
        LineInputIterator<StringT> prev(*this);
        ++*this;
        return prev;
    }
    bool operator!=(const LineInputIterator<StringT>& other) const
    {
        return is != other.is;
    }
    bool operator==(const LineInputIterator<StringT>& other) const
    {
        return !(*this != other);
    }
private:
    istream_type* is;
    StringT value;
};

} // end ub
#endif

所以你的循环可以用算法来替换(这是C++中的另一个推荐实践):
for_each(LineInputIterator<>(cin), LineInputIterator<>(), do_stuff);

也许一个常见的任务是将每一行存储在一个容器中:
vector<string> lines((LineInputIterator<>(stream)), LineInputIterator<>());

1

第一个。

两者都可以实现相同功能,但第一个更易读,并且在循环结束后您可以继续使用字符串变量(在第二个选项中,它被限制在for循环范围内)。


7
在for循环范围内保留这条线路不是件好事吗?在范围外它没什么用,因为最后会保存最后一行的值或其他东西。 - cppLearner
@cppLearner:很好的建议,但也许你应该将这个代码放入一个独立的函数中,这样临时使用的字符串就会自动销毁。 - UncleBens
你总是可以使用空括号,以限制字符串的范围,但同时也有其他代码位于循环之前或之后应该在同一个函数中的充分理由。我认为限制作用域不应该决定使用“for”还是“while”,决定因素应该是你是否在等待某些东西变为假(while),或者遍历概念上是一个范围的东西(for)。显然,两者之间的区别是模糊的边界。它们在逻辑上是等价的,只是关于如何构思循环的问题。 - Steve Jessop
“裸括号”是指{ string line; while (getline(cin,line)) { // process line } } // more code goes here - Steve Jessop

0
这是基于Jerry Coffin的回答。我想展示c++20的std::ranges::istream_view。我还为类添加了行号。我在godbolt上完成了这个过程,以便查看发生了什么。这个版本的line类仍然可以与std::input_iterator一起使用。

https://en.cppreference.com/w/cpp/ranges/basic_istream_view

https://www.godbolt.org/z/94Khjz

class line {
    std::string data{};
    std::intmax_t line_number{-1};
public:
    friend std::istream &operator>>(std::istream &is, line &l) {
        std::getline(is, l.data);
        ++l.line_number;
        return is;
    }
    explicit operator std::string() const { return data; }
    explicit operator std::string_view() const noexcept { return data; }
    constexpr explicit operator std::intmax_t() const noexcept { return line_number; }    
};
int main()
{
    std::string l("a\nb\nc\nd\ne\nf\ng");
    std::stringstream ss(l);
    for(const auto & x : std::ranges::istream_view<line>(ss))
    {
        std::cout << std::intmax_t(x) << " " << std::string_view(x) << std::endl;
    }
}

输出:

0 a
1 b
2 c
3 d
4 e
5 f
6 g

0

使用while语句。

请参阅Steve McConell的《代码大全2》第16.2章(特别是374和375页)。

引用如下:

当while循环更合适时,不要使用for循环。在C++、C#和Java中,对灵活的for循环结构的常见滥用是将while循环的内容随意塞入for循环头中。

.

C++中while循环被滥用地塞进了for循环头的示例
for (inputFile.MoveToStart(), recordCount = 0; !inputFile.EndOfFile(); recordCount++) {
    inputFile.GetRecord();
}
C++使用while循环的适当示例
inputFile.MoveToStart();
recordCount = 0;
while (!InputFile.EndOfFile()) {
    inputFile.getRecord();
    recordCount++;
}

我省略了一些中间部分,但希望这能给你一个好的想法。


6
像往常一样,Steve有一个好主意,但是执行得不太好。首先,这种使用for循环并不特别滥用。其次,更重要的是,两个版本(使用for或while)都显示出反模式,将EndOfFile()作为循环退出条件,这几乎保证会给出错误的结果。 - Jerry Coffin
赞同 for(;;) 滥用原则。但我不同意这个例子是滥用。就像 Jerry 把 EndOfFile() 测试放在那里一样,这是不可以的。这是反模式。虽然我可能会把 recordCount 移到循环体中,并把 GetRecord() 移到 for(;;) 中。 - Martin York
@Martin,他提供了另一个示例的 for 循环,他认为这个稍微比你描述的更好。 - Jonathan Fingland

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接