文字列の入力 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、文字列を取り上げます。ここまでのページで、“Hello” のような文字列リテラルが登場しましたが、リテラルなので、プログラムを実行してから入力される文字の並びを取り扱うことができません。文字の並びの入力を受け取ることができるように、リテラルでない文字列の扱い方を学びます。

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



文字、文字列、文字列リテラル

入力と変数」のページでは、std::cin を使って、実行時にキーボードからデータを入力させるプログラムを取り上げました。そこではいったん、話を整数の入力に限定しましたが、このページでは、「Hello」のような “文字の並び” の入力をあつかえるようになることを目指します。

まずは、用語を確認しておきます。

これまでのページでは、“Hello” のような文字列リテラル (string literal) が登場しています。リテラルは、以前「計算」のページで、「ビルドしても実行しても、なにかほかのものに変えられてしまうことがない、不変なもの」と説明しました。ソースコードを修正して、ビルドしなおさない限り、別の内容に変えることができません。

文字列 (string) も重要な用語です。文字列とは、0文字以上の文字 (character) が並んだデータのことです。具体的にいえば、次のようなものが文字列と呼ばれます。

文字が並んでいるという点では、ソースファイルに書いたソースコードも同様ですが、プログラムとデータとで同じ用語を使うとややこしくなってしまうので、あえて文字列とは呼ばないことが多いです。コードとか、プログラムテキストといったようにして呼び分けます。

0文字でも文字列といえることに注意しておきましょう。文字列リテラルで書くと、"" です。0文字の文字列のことを、空文字列 (empty string) と呼びます。

文字列の入力を受け取る

それでは、文字列の入力を受け取るプログラムを作っていきましょう。

入力に使うのは、これまでどおり std::cin です。その使い方も変わりありません。1点だけ問題なのは、入力を受け取る変数をどのように宣言すればいいのかです。

変数を宣言する構文は、以下のかたちでした。

型 識別子 {初期値};

問題なのは「型」の指定です。整数のときには int を使いましたが、文字列のときには、文字列をあらわす別の型を使わなければなりません。

std::string

文字列をあつかう変数を宣言するときには、std::string という型を使います。

std::string name {"Ken"};

std::string name = "Ken"; も可能です。

int型の変数の初期値に整数リテラルを使えたように、std::string型の変数の初期値には文字列リテラル(ここでは “Ken”)を使えます。型が同じほかの変数、constexpr変数を使えることも int型のときと変わりありません。

int型の場合の解説は「入力と変数」のページを参照してください。

初期値の指定が不要なら次のように書けます。

std::string name {};

この書き方のときは、型に応じたデフォルトの初期値で初期化されるのでした(「入力と変数」のページを参照)。std::string の場合は、空文字列で初期化されます。そのため、次のように書いても同じことです。

std::string name {""};

【C++98/03 経験者】もちろん、std::string name; でも構いません。デフォルトコンストラクタで、空文字列に初期化されます。

std::string を使うときには、#include <string> という記述が必要です。std::cout や std::cin を使うときに、#include <iostream> と書かなければならないのと同じです。2つとも必要になる場合、どちらを先に書いても構いません。


文字列を入力させ、応答を返すプログラムを次のように書けます。

#include <iostream>
#include <string>

int main()
{
    std::cout << "Please enter the your name.\n";
    std::string name {};
    std::cin >> name;

    std::cout << "Hello, " << name << ".\n";
}

実行結果:

Please enter the your name.
Ken   <-- 入力した文字列。最後に Enterキーを押している
Hello, Ken.

constexpr変数や auto を使う場合

理由の説明は難しいので避けますが、std::string型の constexpr変数は宣言できません。

【C++20】C++20 から可能になりました。

#include <iostream>
#include <string>

int main()
{
    constexpr std::string greeting {"Hello."};
}

そこで、constexpr変数で文字列を使いたいときには、constexpr auto を使います。この方法は、「定数式と識別子」のページでも取り上げました。

#include <iostream>

int main()
{
    constexpr auto greeting = "Hello.";
}

ただし、型の指定を auto にして、初期値に文字列リテラルを与えて宣言した場合、結果的にその型は std::string にはなりません。これは変数でも、constexpr変数でも同様です。文字列をあつかうという目的はきちんと果たせますから、この書き方が悪いということではありません。ただ、std::string型には、まだ紹介していない便利な機能が多数あるのですが、それらが使えなくなります。

【上級】文字列リテラルの型は const char[] だからです。配列からの暗黙の型変換もはたらき、結局、型は const char* であると判断されます。


文字列をあつかう変数や constexpr変数の宣言について、まとめると、次のようになります。

【上級】auto を使いつつも、std::string型にする方法は存在します。そのためには、auto name = "Ken"s; のように、文字列リテラルの直後に s を付加します。ただし、この方法を使うには、事前に using namespace std::literals::string_literals; という記述が必要です1。この方法は「文字列操作」のページで取り上げます。

複数の入力を受け取る

入力と変数」のページで、std::cin >> value1 >> value2; として、複数の整数の入力を受け取るプログラムを取り上げました(入力時にはスペースで区切ります)。文字列の場合も同じようにして、複数の入力を受け取れます。

次のプログラムでは、姓と名をスペースで区切って入力させています。

#include <iostream>
#include <string>

int main()
{
    std::cout << "Please enter the your fullname.\n";
    std::string family_name {};
    std::string first_name {};
    std::cin >> family_name >> first_name;

    std::cout << "Hello, " << family_name << " " << first_name << ".\n";
}

実行結果:

Please enter the your fullname.
Saitou Ken   <-- 入力した文字列
Hello, Saitou Ken.

また、整数と文字列の入力が混在することも許されます。次のプログラムは、名前に加えて、年齢も入力させています。

#include <iostream>
#include <string>

int main()
{
    std::cout << "Please enter your fullname and age.\n";
    std::string family_name {};
    std::string first_name {};
    int age {};
    std::cin >> family_name >> first_name >> age;

    std::cout << "Your name: " << family_name << " " << first_name << "\n";
    std::cout << "Your age: " << age << "\n";
}

実行結果:

Please enter your fullname and age.
Saitou Ken 25   <-- 入力した文字列と年齢
Your name: Saitou Ken
Your age: 25

空白が混ざった文字列の入力

さきほどの複数の文字列を入力する例では、2つの文字列のあいだをスペースで区切って入力しました。std::cin >> による入力処理では、スペースを入力の区切りと判断するので、これでうまくいくのですが、逆にこれが原因でうまくいかないケースがあります。

つまり、スペースも含めて1つの文字列にしたいときに困るのです。

たとえば「あなたの職業を入力してください」という質問に対する入力では、「Programmer」のような1語で済む場合もあれば、「Software Engineer」のように2語以上必要になる場合もあるでしょう。このケースでは、スペースの有無によらず、1つの文字列とみなして、1つの変数に受け取れるのが理想的です。

実際に試してみます。スペースが含まれた「Software Engineer」を入力してみるとうまくいかないことが分かります。

#include <iostream>
#include <string>

int main()
{
    std::cout << "Please enter the your profession.\n";
    std::string profession {};
    std::cin >> profession;

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

実行結果:

Please enter the your profession.
Software Enginner   <-- 入力した文字列
Software

入力したはずの「Enginner」の部分が出力されていません。このように、スペースを含んだ文字列の入力を受け取ると、うまく処理できなくなってしまいます。

std::getline関数

そこで、スペースを入力の区切りとみなさず、スペースも文字列の一部として受け取る方法が必要になります。そのためには、std::cin と >> による方法に代わって、std::getline関数 を使います。

std::getline関数の使い方は以下のようになります。

std::getline(std::cin, 入力された文字列を受け取る変数);

「std::cin」の部分はこのまま文字通りに書けばいいです。「入力された文字列を受け取る変数」には std::string型の変数の名前を書きます。

なお、std::getline関数を使うには、#include <string> の記述が必要です(std::string を使うので、必然的に書いていると思われますが)。

突然、これまでと雰囲気が違う文法になったのが不思議かもしれません。今のところ気にする必要はありませんが、std::getline という名前に続けて () のペアが現れるこのような記法になるのは、std::getline が関数であることに理由があります(main関数のところにも () が付いています)。

実際のプログラムは次のようになります。

#include <iostream>
#include <string>

int main()
{
    std::cout << "Please enter the your profession.\n";
    std::string profession {};
    std::getline(std::cin, profession);

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

実行結果:

Please enter the your profession.
Software Enginner   <-- 入力した文字列
Software Enginner

std::getline関数の考え方は、1行分の入力を受け取るということです。ですから、途中にスペースがあろうがなかろうが問題とせず、とにかく1行分のデータ入力を受け取ろうとします。

【上級】正確にいえば、これは std::getline関数のデフォルトの動作です。std::getline関数には2つの引数が隠されており、2つ目の引数で「どんな文字を入力の終わりとみなすか」を指定できます。デフォルトではこれが改行文字 (‘’) になっています。2

std::getline関数が実行されると、std::cin >> を使ったときと同じように、入力を待ち受ける状態で停止します。ここで「Software Enginner」のように、スペースが含まれた文字列を入力してみると、意図通り、1つの文字列として受け取れることが確認できます。

なお、std::getline関数は、空文字列の入力も受け取れます。さきほどのサンプルプログラムで入力をおこなうとき、Enterキーだけを押しても、きちんとプログラムが進行します。std::cin >> による方法ではこうはいきません。

前回入力した改行によるトラブル

std::getline関数を使う場合、その直前の std::cin >> での入力の影響を受けてトラブルが起こるケースがあります。

次のプログラムは、まず std::cin >> を使って整数の入力を受け取り、その後で、std::getline関数を使って、文字列の入力を受け取ろうとしています。しかし、実行してみるとうまくいきません。

#include <iostream>
#include <string>

int main()
{
    std::cout << "Please enter an integer.\n";
    int value {};
    std::cin >> value;
    std::cout << value << "\n";

    std::cout << "Please enter a string.\n";
    std::string s {};
    std::getline(std::cin, s);
    std::cout << s << "\n";
}

実行結果:

Please enter an integer.
100   <-- 入力した整数。最後に Enterキーを押している
100
Please enter a string.
(空行)

std::getline関数のところで入力待ち状態にならず、勝手に先に進んで、プログラムの実行が終了してしまいます。出力結果の最後に空行があるので、最後の std::cout が出力したものは空文字列のようです。

この現象は、std::cin >> value での入力のとき、最後に押した Enterキーによる「改行」の影響を受けたものです。

まず、改行の正体は改行文字です。これは、std::cout を使うときにいつも書いている \n のことです。

std::cin >> は、スペースを入力の区切りとみなすということでしたが、改行文字についても同様に区切りであると認識します。そのため、「100改行」という入力を行うと、まず、int型の変数value に、整数とみなせる部分「100」を入れて、そこで処理を終えることになります。改行文字は使われないまま取り残されています。

その後、std::getline関数を呼び出しますが、std::getline関数のほうは、改行文字があらわれるまでの部分を文字列として読み取るという仕様なので、先ほど取り残された改行文字をすぐに見つけて、処理を終わらせてしまいます。

この問題は、std::getline関数を使わず、std::cin >> による方法だけを使っていたら起こりません。1回目の std::cin >> の入力で改行文字が取り残されても、2回目の std::cin >> はその改行文字を、単に先頭に区切りがあるのだとみなして無視します。そうなると何も残りませんから、入力を求めて待機状態になります。

問題を解決するには、std::cin.ignore(); という文を追加します。

#include <iostream>
#include <string>

int main()
{
    std::cout << "Please enter an integer.\n";
    int value {};
    std::cin >> value;
    std::cin.ignore();
    std::cout << value << "\n";

    std::cout << "Please enter a string.\n";
    std::string s {};
    std::getline(std::cin, s);
    std::cout << s << "\n";
}

実行結果:

Please enter an integer.
100   <-- 入力した整数。最後に Enterキーを押している
100
Please enter a string.
abc   <-- 入力した文字列。最後に Enterキーを押している
abc

std::cin.ignore(); には、入力されたが使われていない文字を1文字だけ無視して、なかったことにするという効果があります。そのため、std::cin >> value; の直後に書いておけば、使われないままになっている改行文字を無視して、なかったことにできます。

文字列内の文字にアクセスする

文字列を文字列のままあつかうのではなく、文字列に含まれている文字に用がある場合もあります。たとえば、姓と名を入力させて、イニシャルを出力したいとすれば、それぞれの文字列の先頭の1文字にだけ用があります。

文字列に含まれる文字に用があるときは、次のようにします。

文字列[先頭から数えて何文字目かをあらわす整数]

「文字列」は、std::string型の変数や、文字列リテラルのような文字の並びのことです。

「先頭から数えて何文字目かをあらわす整数」は、文字列の先頭を 0文字目としたときの文字数です。これは整数リテラルで指定してもいいですし、int型の変数を使ったり、計算式で指定したりできます。なお、このようないくつか並んでいるものの何番目かを表すために用いる数のことを添字(そえじ) (index) とか、インデックス (index) などと呼ぶことがあります。

プログラミングでは1を基準にせず、0を基準にする場面が多数あります。0が基準になっている方が何かと便利な場合が多いからです。最初のうちは戸惑うと思いますし、何度も間違えると思いますが、慣れなければなりません。分かってくると自然に感じられます。

このような記述によって、文字列に含まれている特定の1文字だけを得られます(よく、文字を参照するとか、アクセスするという言い方をします)。

姓と名を入力させ、イニシャルを出力する実際のプログラムは次のようになります。

#include <iostream>
#include <string>

int main()
{
    std::cout << "Please enter the your fullname.\n";
    std::string family_name {};
    std::string first_name {};
    std::cin >> family_name >> first_name;

    std::cout << "Hello, " << family_name[0] << "." << first_name[0] << ".\n";
}

実行結果:

Please enter the your fullname.
Saitou Ken   <-- 入力した文字列
Hello, S.K.

イニシャルなので、先頭の文字をアクセスするために、family_name[0]first_name[0] のように記述しています。

範囲外アクセス

文字列に含まれる文字にアクセスするとき、その文字列の実際の文字数を越えたところを指定しないように注意する必要があります。

たとえば、次のプログラムには問題があります。

#include <iostream>
#include <string>

int main()
{
    std::string s {"abcde"};
    std::cout << s[10] << "\n";
}

変数s の値は “abcde” です。先頭の文字が 0番目なので、最後の「e」は 4番目ということになります。しかし、s[10] のように書いて、10番目の文字にアクセスしようとしています。10番目の文字などありません。

このように、存在しない文字へアクセスしようとする行為を範囲外アクセス (out-of-range access) と呼びます。範囲外アクセスは未定義動作であり、どのような結果になるか分かりませんから、このようなプログラムは避けなければなりません。

未定義動作については、ゼロ除算のときに説明しました。

【上級】範囲外アクセスによる未定義動作を防ぐ方法として、at関数があります。s[10] の代わりに s.at(10) のように書くことができ、この場合、範囲外アクセスを行うと、例外が送出され、捕捉しなければプログラムが終了することになります。

文字が存在していないとみなされるのは、見た目上の文字数(“abcde” なら 5)を越えたところからです。“abcde” に対して、s[5] なら問題ありませんが、s[6] になると未定義動作です。

s[5] は問題ないとはいえ、“abcde” の「e」のさらに後ろをアクセスしていることになるので、あまり意味がなさそうに思えますが、必要になる場面はあります。必要になったら解説します。

まとめ


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


参考リンク


練習問題

問題の難易度について。

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

問題1 (確認★)

次のプログラムで、変数s1、s2、s3、s4、s5 はそれぞれどのような初期値になりますか?

#include <iostream>
#include <string>

int main()
{
    constexpr auto s = "Test";

    std::string s1 {};
    std::string s2 {""};
    std::string s3 {"Hello"};
    std::string s4 {s3};
    std::string s5 {s};
}

解答・解説

問題2 (確認★)

次のプログラムは問題がありますか? あるとすれば、何が問題ですか?

#include <iostream>
#include <string>

int main()
{
    std::string s {""};
    std::cout << s[0] << "\n";
}

解答・解説

問題3 (基本★)

次のプログラムの実行結果はどうなりますか?

#include <iostream>
#include <string>

int main()
{
    constexpr auto message = "Hello, World.";
    constexpr auto position = 3;

    std::cout << message[1] << "\n";
    std::cout << message[4] << "\n";
    std::cout << message[position] << "\n";
    std::cout << message[position + 4] << "\n";
}

解答・解説

問題4 (基本★★)

フルネーム、年齢、職業を対話的に入力させるプログラムを作成してください。つまり、

  1. フルネームを入力させる
  2. 入力を受けたら返答して、次に年齢を入力させる
  3. 入力を受けたら返答して、次に職業を入力させる
  4. 入力を受けたら返答して、プログラムを終了する

といった流れをつくってください。

解答・解説

問題5 (応用★★★)

A~Z までのアルファベット26文字があります。1~26までのいずれかの整数を入力させ、その番号に応じたアルファベットを出力するプログラムを作成してください。たとえば、3が入力されたなら「C」を出力します。

1~26の範囲外の整数が入力されることは考えないものとして構いません。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る