先頭へ戻る

stringstream | Programming Place Plus 新C++編

Programming Place Plus トップページ -- 新C++編

先頭へ戻る

このページの概要

このページでは、1つの文字列を複数の値に分解して変数に格納したり、反対に、複数の値を組み立てて1つの文字列にしたりする方法を取り上げます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



入力データのフォーマットの問題

久しぶりに電卓プログラムの話題に戻ります。「for文」のページで、計算式の入力を複数回繰り返せるようになり、次のソースコードを書きました。

#include <iostream>
#include <string>

int main()
{
    constexpr auto ADDITION = '+';
    constexpr auto SUBTRACTION = '-';
    constexpr auto MULTIPLICATION = '*';
    constexpr auto DIVISION = '/';

    for (int i = 0; i < 5; ++i) {
        std::cout << "Please enter the formula.\n";
        int value1 {};
        std::string op {};
        int value2 {};
        std::cin >> value1 >> op >> value2;

        switch (op[0]) {
        case ADDITION:
            std::cout << value1 + value2 << "\n";
            break;
        case SUBTRACTION:
            std::cout << value1 - value2 << "\n";
            break;
        case MULTIPLICATION:
            std::cout << value1 * value2 << "\n";
            break;
        case DIVISION:
            std::cout << value1 / value2 << "\n";
            break;
        default:
            std::cout << "The formula is incorrect.\n";
            break;
        }
    }
}

実行結果:

Please enter the formula.
1 + 2   <-- 入力した式
3
Please enter the formula.
10 - 1   <-- 入力した式
9
Please enter the formula.
3 * 5   <-- 入力した式
15
Please enter the formula.
5 / 2   <-- 入力した式
2
Please enter the formula.
-1 + 3   <-- 入力した式
2

たしかに繰り返せてはいるものの、その回数が5回に決め打ちされているところに不満がありました。

しかしその後、while文を学び、任意の条件でループを作れるようになりましたし、if文と break文を組み合わせれば、ループを強制的に終了させることもできるようになりました。固定された回数のループをやめることができそうです。

while文if文break文については、それぞれのページを参照してください。

そこで、このページでは、電卓プログラムを次のように改造します。

「"exit" が入力されるまでループする」というプログラムは、「while文」のページで何度も登場しました。しかし、電卓プログラムに適用しようと思うと問題があります。それは、計算式のフォーマット(形式)と合わないことです。

計算式のフォーマットは「整数 演算子 整数」であり、そのルールに沿った入力データがやってきます。ソースコード上では int型、std::string型、int型の変数で受け取るようにしてきました。一方で、"exit" は文字列ですから、入力データの形と合わず、条件判定を記述できません。

std::cout << "Please enter the formula.\n";
int value1 {};
std::string op {};
int value2 {};
std::cin >> value1 >> op >> value2;

if (/* ここが書けない */ == "exit") {
    break;
}

この問題を解決するには、「入力は1つの文字列で受け取る」ように変えることが必要です。そもそもこういった、入力を受け取って処理するプログラムでは、1つの文字列で受け取ってから、その内容を都合の良いように取り出したり、変換したりする方がうまくいくことが多いです。概要としてはこうなります。

// 「整数 演算子 整数」という内容の入力を、std::string型の変数に、1つの文字列として受け取る。
// たとえば、
// 3 + 5
// という入力を、"3 + 5" として受け取る。
std::cout << "Please enter the formula.\n";
std::string input_string {};
std::getline(std::cin, input_string);

// "exit" の入力を確認できるようになった
if (input_string == "exit") {
    break;
}

// "3 + 5" を分解して、
// int型の変数に 3、std::string型の変数に '+'、int型の変数に 5 が入るようにする。
int value1 {};
std::string op {};
int value2 {};
// ・・・その方法は、このページ内で取り上げる。

// あとは同じ。演算子に応じて分岐して計算。結果を出力する。

文字列を分解する (std::istringstream)

さきほどの概要コードの中で、これまでの知識でできないのは以下の部分です。

// "3 + 5" を分解して、
// int型の変数に 3、std::string型の変数に '+'、int型の変数に 5 が入るようにする。

つまり、文字列の内容を分解して、いくつかの変数に配分するという処理です。このとき、整数の部分は int型に変換します。

このような、文字列の分解には、std::istringstream を使います。istringstream は i と string と stream を合体させた名前です。i は input のことで、文字列そのものを入力とみなすのだということを表しています。

【C言語プログラマー】C言語では sscanf関数で実現できることであり、その方法は C++ でも可能です。std::istringstream の利点は、型の安全性です。sscanf関数では変換指定と受け取る変数の型を合わせ間違える可能性がありますが、std::istringstream ではその恐れがなくなります。また、新しい型を定義したときに、その型にも対応できるしくみもあります。

std::istringstream を使うには、#include <sstream> という記述が必要です。

std::istringstream は型の名前です。つまり、std::string と同じように、変数を宣言して使用します。このとき、初期値として、文字列を1つ指定します。

std::istringstream iss {"3 + 5"};

初期値に与えた文字列を分解するには、std::istringstream型の変数を起点にして >> でつないでいきます。

int value1 {};
std::string op {};
int value2 {};
iss >> value1 >> op >> value2;

これはちょうど、std::cin を使うのと同じかたちになっています。std::cin はキーボードからの入力を受け取り、その内容を適切な型の変数に分けて格納していくものでした。これに対し、std::istringstream は文字列という入力を受け取ったあと、その内容を適切な型の変数に分けて格納していくものです。

つまり、std::cin と std::istringstream は、元データの出所が違うだけであって、していることはまったく同じであり、まったく同じ使い方ができます。

std::istringstream を使って電卓プログラムを修正すると、次のようになります。

#include <iostream>
#include <sstream>
#include <string>

int main()
{
    constexpr auto ADDITION = '+';
    constexpr auto SUBTRACTION = '-';
    constexpr auto MULTIPLICATION = '*';
    constexpr auto DIVISION = '/';

    while (true) {

        // 計算式、または "exit" の入力を得る
        std::cout << "Please enter the formula.\n";
        std::string input_string {};
        std::getline(std::cin, input_string);

        // "exit" が入力されたら終わり
        if (input_string == "exit") {
            break;
        }

        // 計算式を分解する
        std::istringstream iss {input_string};
        int value1 {};
        std::string op {};
        int value2 {};
        iss >> value1 >> op >> value2;

        // 計算結果を出力
        switch (op[0]) {
        case ADDITION:
            std::cout << value1 + value2 << "\n";
            break;
        case SUBTRACTION:
            std::cout << value1 - value2 << "\n";
            break;
        case MULTIPLICATION:
            std::cout << value1 * value2 << "\n";
            break;
        case DIVISION:
            std::cout << value1 / value2 << "\n";
            break;
        default:
            std::cout << "The formula is incorrect.\n";
            break;
        }
    }
}

実行結果:

Please enter the formula.
3 + 5  <-- 入力した文字列
8
Please enter the formula.
10 / 2  <-- 入力した文字列
5
Please enter the formula.
exit  <-- 入力した文字列

計算式はこれまでと同じ形式で受け付けられていますし、"exit" が入力されたら終了できています。ループも5回繰り返す for文から、無限ループに変更してあります。"exit" が入力されるまでは、何度でも計算を行わせることができるようになりました。

分解を何度もおこなう場合

1つの std::istringstream型の変数を使って、何度も繰り返し、文字列の分解を行いたいとします。たとえば、最初に "12 + 16" のような文字列を分解させたあと、今度は "7 / 2" の分解もしたい、ということです。

分解したい文字列は、std::istringstream型の変数を宣言するときに初期値として与えていましたから、"12 + 16" の分解のあと、"7 / 2" の分解に使うには、対象の文字列をセットしなおす方法が必要です。そのためには、以下のように書きます。

iss.str("7 / 2");  // iss は std::istringstream型の変数

見慣れない構文ですが、これは「変数名.メンバ関数名()」という構文の式です。いずれきちんと解説しますが、型ごとにいくつかの機能が使えるようになっていて、この構文はその機能を呼び出すものです。今回の場合、std::istringstream という型が、str というメンバ関数 (member function)(機能)を持っており、それを呼び出しています。

この機能を使って、次のようなプログラムが書けますが、これではうまくいきません。

#include <iostream>
#include <sstream>
#include <string>

int main()
{
    int value1 {};
    std::string op {};
    int value2 {};

    // "12 + 16" を分解
    std::istringstream iss {"12 + 16"};
    iss >> value1 >> op >> value2;
    std::cout << value1 << " " << op << " " << value2 << "\n";

    // "7 / 2" を分解
    iss.str("7 / 2");  // 新しい文字列をセットする
    iss >> value1 >> op >> value2;
    std::cout << value1 << " " << op << " " << value2 << "\n";
}

実行結果:

12 + 16
12 + 16

期待した 7 / 2 という実行結果は得られず、12 + 16 が2回出力されてしまいました。

このような結果になる理由を説明しますが、ここはかなり難しいです。少なくとも入門段階で必須ということはありませんので、難しいようなら、「そこで解決方法として」ではじまるところまで飛ばして構いません。

理解に必要なのは、std::istringstream の内部状態についてです。つまり、std::istringstream型の変数が記憶している情報ということです。プログラムを書いた人の感覚では、"12 + 16" とか "7 / 2" といった文字列を指定しただけですが、std::istringstream型の変数には、ほかにも多くの情報が記憶されています。ここで注目しなければならないのは、以下の2つです。

  1. 現在、保持している文字列の内容
  2. 処理の状況をあらわす情報

1番は、"12 + 16" とか "7 / 2" という文字列のことです。変数宣言時に初期値として与えるか、iss.str("7 / 2"); のような記述によって与えられます。

2番は、処理が正常に進行できているか、エラーは起きていないか、文字列の最後まで処理が終わったか、といった情報です。iss >> value1 >> op >> value2; を実行し、最終的に value2 に 16 が入った時点で、その文字列は全部処理済みだということになり、それを表す情報が記憶されます。

【上級】いわゆる EOF です。

「文字列は全部処理済み」ということは「もうこれ以上の処理は行えない」ということです。

このあと、iss.str("7 / 2"); を実行しますが、これで変更されるのは、1つ目の情報(現在、保持している文字列の内容)だけです。「もうこれ以上の処理は行えない」という情報に変更はありません。そのため、2度目の iss >> value1 >> op >> value2; はすべて失敗に終わります。実行結果のように、"12 + 16" がもう1度出力されたのは、そもそも2度目の iss >> value1 >> op >> value2; は何もできなかったから、以前に value1。op、value2 に入っていたものがそのまま残された結果です。

そこで、正しく動作させるために、「処理の状況をあらわす情報」をリセットしてやらなければなりません。


そこで解決方法として、iss.clear(); という一文を挟みます。

#include <iostream>
#include <sstream>
#include <string>

int main()
{
    int value1 {};
    std::string op {};
    int value2 {};

    // "12 + 16" を分解
    std::istringstream iss {"12 + 16"};
    iss >> value1 >> op >> value2;
    std::cout << value1 << " " << op << " " << value2 << "\n";

    // "7 / 2" を分解
    iss.clear();       // 処理済みだという内部状態をクリアする
    iss.str("7 / 2");  // 新しい文字列をセットする
    iss >> value1 >> op >> value2;
    std::cout << value1 << " " << op << " " << value2 << "\n";
}

実行結果:

12 + 16
7 / 2

想定どおりの実行結果が得られました。

しかし、そもそも1つの std::istringstream型の変数でおこなう分解作業は1回だけにしておけば、このような難しい理解をする必要もありません。変数を1つで済ませることが魅力的に映る場合もあるかもしれませんが、ミスを犯しやすいコードを避けることが大切です。2回の分解作業を行うなら、2つの変数を使う方が間違いは少ないでしょう。

文字列を組み立てる (std::ostringstream)

文字列を複数の値に分解して、それぞれを別個の変数に入れられるようになりました。今度はその反対の操作をやってみます。つまり、複数の値を合体して、1つの文字列にするということです。

組み立て前の値は変数でなくても構わないので、その点では正確に反対の操作ではありません。

このような、文字列の組み立てには、std::ostringstream を使います。今度は名前の先頭が「o」になっています。もちろん、output の o です。

std::ostringstream を使うには、#include <sstream> という記述が必要です。

【C言語プログラマー】C言語では sprintf関数で実現できることであり、その方法は C++ でも可能です。std::ostringstream の利点は、バッファオーバーフローが防がれることです。std::ostringstream はその内部でバッファを管理しており、自動的に動的なメモリ割り当てを行います。また、変換指定がなく、型をコンパイラが自動的に判断します。

std::ostringstream は型なので、まず変数を宣言します。

std::ostringstream oss {};

そして、std::cout を使うときと同じように、<< で値を渡していきます。

oss << value << "abc";

この時点で、組み立てられた文字列を std::ostringstream が持っている状態になります。この文字列は以下のように、.str() という記述によって取得できます。

oss.str()

oss.str() によって取得した文字列は、以下のように、初期化や代入、std::cout に渡すといったことに使えます。

// 初期化に使う
std::string s{oss.str()};

// 代入に使う
s = oss.str();

// std::cout で出力する
std::cout << oss.str();

std::ostringstream を使ったプログラム例を挙げます。

#include <iostream>
#include <sstream>
#include <string>

int main()
{
    int n1 {123};
    int n2 {999};

    std::ostringstream oss {};
    oss << n1 << "abc" << n2;
    std::string s {oss.str()};

    std::cout << s << "\n";
}

実行結果:

123abc999

std::ostringstream の使い方は std::cout と同じですが、いつもの調子で末尾に "\n" を入れないように気を付けましょう(もちろん必要ならば付けていいですが)。文字列を組み立てるとき "\n" を付けて、std::cout で出力するときにも "\n" を付けると、2回改行されてしまいます。

このサンプルプログラムでは、組み立てた文字列を出力しているだけなので、次のように書いても同じ結果になります。

std::ostringstream oss {};
oss << n1 << "abc" << n2;
std::cout << oss.str() << "\n";

さらにいえば、次のように書いてしまっても同じ結果になります。

std::cout << n1 << "abc" << n2 << "\n";

文字列を組み立てる必要性があるのは、組み立てた後の文字列に用があるときに限られます。

組み立てを何度もおこなう場合

std::istringstream のときと同じく、1つの std::ostringstream型変数を使いまわして、組み立てを繰り返しおこなうことは単純にはできません。

#include <iostream>
#include <sstream>

int main()
{
    int n1 {123};
    int n2 {999};

    std::ostringstream oss {};
    oss << 100 << "a" << 30;
    std::cout << oss.str() << "\n";

    oss << 4 << "b" << 2;
    std::cout << oss.str() << "\n";
}

実行結果:

100a30
100a304b2

実行結果のように、最初に渡した "100a30" が残されたまま、その後ろに "4b2" が追加されるという挙動になります。

std::istringstream のときの話を理解していると大体理由はわかると思いますが、要は、最初に渡した文字列をクリアすればいいということです。そのためには、oss.str(""); を挟みます。

oss.str(); と間違えないようにしてください。

#include <iostream>
#include <sstream>

int main()
{
    int n1 {123};
    int n2 {999};

    std::ostringstream oss {};
    oss << 100 << "a" << 30;
    std::cout << oss.str() << "\n";

    oss.str("");  // 設定済みの文字列をクリアする
    oss << 4 << "b" << 2;
    std::cout << oss.str() << "\n";
}

実行結果:

100a30
4b2

文字列の分解と組み立て (std::stringstream)

std::istringstream は文字列の分解、std::ostringstream は文字列の組み立てを行いました。この両方の機能をあわせもつのが std::stringstream です。

std::stringstream を使うには、#include <sstream> という記述が必要です。

使い方についても、両方が合わさったような感じです。

#include <iostream>
#include <sstream>

int main()
{
    // まず、変数を宣言
    std::stringstream ss {"30 + 50"};

    // 分解
    int value1 {};
    std::string op {};
    int value2 {};
    ss >> value1 >> op >> value2;
    std::cout << value1 << " " << op << " " << value2 << "\n";

    // 次の処理に入る前にリセット
    ss.clear();
    ss.str("");

    // 組み立て
    ss << 5 << " * " << 2;
    std::cout << ss.str() << "\n";
}

実行結果:

30 + 50
5 * 2

このプログラムでは、最初に "30 + 50" という文字列を与え、それを 30 と '+' と 50 に分解しています。そのあと、同じ変数を使って、5 と " * " と 2 を組み立てて1つの文字列にしています。これらの処理はそれぞれ、std::ostringstream、std::istringstream と同じです。

1つの変数を使いまわすかたちになるので、ss.clear();ss.str(""); といったコードを挟まないとうまくいきません。それぞれの必要性は、「 分解を何度もおこなう場合」や「 組み立てを何度もおこなう場合」で触れたとおりです。

1つの変数を使いまわして、文字列の組み立てと分解をおこなう必要があるのなら仕方がないですが、そうでないなら、std::istringstream と std::ostringstream を使ったほうがいいでしょう。std::istringstream や std::ostringstream のほうがシンプルなので、ミスが起こりづらく、実行効率もわずかに良いです。


まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

3つの整数を、空白文字で区切りながら、1つの文字列に合体するプログラムを作成してください。

解答・解説

問題2 (基本★)

時・分・秒を "14:05:26" のような文字列で入力されるとき、時・分・秒をそれぞれ int型の変数に取り出すプログラムを作成してください。

解答・解説

問題3 (基本★★)

問題2のプログラムを改造して、入力された時刻の 30分後の時刻を出力するようにしてください。

解答・解説

問題4 (応用★★★)

以下のような、改行文字を含むことによって数行文の文字列を表すようにした、1つの文字列があります。

std::string message {"aa\nbbbbbbb\nccccc\nddd\neeeeee\n"};

この文字列をもとに、各行の末尾に "." を加えた、次のような出力を行うプログラムを作成してください。

実行結果:

aa.
bbbbbbb.
ccccc.
ddd.
eeeeee.

文字列に含まれている改行文字の個数は1個以上であれば、何個でもいいようにしてください。各行には1文字以上の文字があるものします。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー