web-dev-qa-db-ja.com

std :: getline()がフォーマットされた抽出後に入力をスキップするのはなぜですか?

ユーザーに名前と状態の入力を求める次のコードがあります。

#include <iostream>
#include <string>

int main()
{
    std::string name;
    std::string state;

    if (std::cin >> name && std::getline(std::cin, state))
    {
        std::cout << "Your name is " << name << " and you live in " << state;
    }
}

私が見つけたのは、名前は正常に抽出されたが、州は抽出されなかったことです。入力と結果の出力は次のとおりです。

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

状態の名前が出力から省略されたのはなぜですか?適切な入力を行いましたが、コードは何らかの形でそれを無視します。なぜこれが起こるのですか?

89
0x499602D2

なぜこれが起こるのですか?

これは、ユーザーが自分で提供した入力とは関係ありませんが、デフォルトの動作std::getline()が示すものとは関係ありません。名前(_std::cin >> name_)の入力を提供したとき、次の文字を送信するだけでなく、暗黙的な改行もストリームに追加されました。

_"John\n"
_

選択すると、入力に常に改行が追加されます Enter または Return 端末から送信するとき。また、ファイルで次の行に移動するために使用されます。改行は、nameへの抽出後、次のI/O操作が破棄または消費されるまで、バッファに残ります。制御フローがstd::getline()に達すると、改行は破棄されますが、入力はすぐに停止します。これが発生する理由は、この関数のデフォルトの機能が、そうすべきだと指示しているためです(行を読み取ろうとし、改行が見つかると停止します)。

この主要な改行はプログラムの期待される機能を抑制するため、無視する必要があります。 1つのオプションは、最初の抽出後にstd::cin.ignore()を呼び出すことです。次に使用可能な文字が破棄されるため、改行は邪魔になりません。


詳細な説明:

これは、呼び出したstd::getline()のオーバーロードです:

_template<class charT>
std::basic_istream<charT>& getline( std::basic_istream<charT>& input,
                                    std::basic_string<charT>& str )
_

この関数の別のオーバーロードは、charT型の区切り文字を取ります。区切り文字は、入力シーケンス間の境界を表す文字です。この特定のオーバーロードは、デリミタが提供されなかったため、デフォルトで改行文字input.widen('\n')に区切り文字を設定します。

現在、これらはstd::getline()が入力を終了する条件の一部です:

  • ストリームが最大量の文字を抽出した場合、_std::basic_string<charT>_が保持できる
  • ファイルの終わり(EOF)文字が見つかった場合
  • 区切り文字が見つかった場合

3番目の条件は、対処している条件です。 stateへの入力は次のように表されます。

_"John\nNew Hampshire"
     ^
     |
 next_pointer
_

ここで_next_pointer_は解析される次の文字です。入力シーケンスの次の位置に格納されている文字が区切り文字であるため、std::getline()はその文字を静かに破棄し、_next_pointer_を次の利用可能な文字にインクリメントし、入力を停止します。つまり、指定した残りの文字は、次のI/O操作のためにバッファに残ります。行からstateへの別の読み取りを実行すると、std::getline()の最後の呼び出しで区切り文字が破棄されたため、抽出により正しい結果が得られることがわかります。


フォーマットされた入力演算子(operator>>())で抽出する場合、通常この問題に遭遇しないことに気づいたかもしれません。これは、入力ストリームが入力の区切り文字として空白を使用し、_std::skipws_を持つためです。1 マニピュレータはデフォルトでオンに設定されています。ストリームは、フォーマットされた入力の実行を開始するときに、ストリームの先頭の空白を破棄します。2

書式付き入力演算子とは異なり、std::getline()unformatted入力関数です。そして、すべてのフォーマットされていない入力関数には、次のコードが多少共通しています。

_typename std::basic_istream<charT>::sentry ok(istream_object, true);
_

上記は、標準C++実装のすべての書式付き/書式なしI/O関数でインスタンス化される監視オブジェクトです。 Sentryオブジェクトは、I/Oのストリームを準備し、ストリームがフェイル状態かどうかを判断するために使用されます。 unformatted入力関数でのみ、セントリーコンストラクターの2番目の引数がtrueであることがわかります。この引数は、先頭の空白が入力シーケンスの先頭からnot破棄されることを意味します。以下は、標準[§27.7.2.1.3/ 2]からの関連する引用です。

_ explicit sentry(basic_istream<charT, traits>& is, bool noskipws = false);
_

[...] noskipwsがゼロでis.flags() & ios_base::skipwsがゼロ以外の場合、次の利用可能な入力文字cが空白文字である限り、関数は各文字を抽出して破棄します。 [...]

上記の条件は偽なので、監視オブジェクトは空白を破棄しません。この関数によってnoskipwstrueに設定される理由は、std::getline()のポイントが未フォーマットの未フォーマット文字を_std::basic_string<charT>_オブジェクトに読み込むためです。


ソリューション:

std::getline()のこの動作を停止する方法はありません。 std::getline()を実行する前に新しい行を自分で破棄する必要があります(ただし、フォーマットされた抽出の後に実行します)。これは、ignore()を使用して、新しい行に到達するまで入力の残りを破棄することで実行できます。

_if (std::cin >> name &&
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n') &&
    std::getline(std::cin, state))
{ ... }
_

_<limits>_を使用するには、_std::numeric_limits_を含める必要があります。 std::basic_istream<...>::ignore()は、区切り文字が見つかるか、ストリームの最後に到達するまで、指定された量の文字を破棄する関数です(ignore()は、見つかった場合に区切り文字も破棄します)。 max()関数は、ストリームが受け入れることができる最大文字数を返します。

空白を破棄する別の方法は、入力ストリームの先頭から先頭の空白を抽出および破棄するように設計されたマニピュレーターである_std::ws_関数を使用することです。

_if (std::cin >> name && std::getline(std::cin >> std::ws, state))
{ ... }
_

違いは何ですか?

違いは、ignore(std::streamsize count = 1, int_type delim = Traits::eof())3 count文字を破棄するか、区切り文字(2番目の引数delimで指定)を見つけるか、ストリームの最後に達するまで、文字を無差別に破棄します。 _std::ws_は、ストリームの先頭から空白文字を破棄するためにのみ使用されます。

書式付き入力と書式なし入力を混在させており、余白を破棄する必要がある場合は、_std::ws_を使用します。そうでない場合、無効な入力をそれが何であるかに関係なく消去する必要がある場合は、ignore()を使用します。この例では、ストリームがname変数の_"John"_の入力を消費したため、空白のみをクリアする必要があります。残ったのは改行文字だけでした。


1:_std::skipws_は、フォーマットされた入力の実行時に先頭の空白を破棄するように入力ストリームに指示するマニピュレーターです。これは、_std::noskipws_マニピュレーターでオフにできます。

2:入力ストリームは、デフォルトでスペース文字、改行文字、フォームフィード、キャリッジリターンなどの特定の文字を空白とみなします。

3:これはstd::basic_istream<...>::ignore()の署名です。ストリームから単一の文字を破棄するための引数なし、一定量の文字を破棄するための引数1つ、count文字を破棄するための引数2つ、またはdelimに達するまで呼び出すことができます。最初に来ます。通常、区切り文字の前にいくつの文字があるかわからない場合でも、countの値としてstd::numeric_limits<std::streamsize>::max()を使用しますが、とにかくそれらを破棄します。

105
0x499602D2

次の方法で初期コードを変更すれば、すべて問題ありません。

if ((cin >> name).get() && std::getline(cin, state))
10
Boris

これは、改行文字としても知られる暗黙的なラインフィード\nが、ストリームに新しい行を開始するように指示するときに、端末からのすべてのユーザー入力に追加されるために発生します。ユーザー入力の複数の行をチェックするときに std::getline を使用することで、これを安全に考慮することができます。 std::getlineのデフォルトの動作は、入力ストリームオブジェクト(この場合は\n)から改行文字std::cinまでのすべてを読み取ります。

#include <iostream>
#include <string>

int main()
{
    std::string name;
    std::string state;

    if (std::getline(std::cin, name) && std::getline(std::cin, state))
    {
        std::cout << "Your name is " << name << " and you live in " << state;
    }
    return 0;
}
Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in New Hampshire"
0
Justin Randall