標準入出力ストリームの基礎 | Programming Place Plus C++編【言語解説】 第5章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


関連する話題が、以下のページにあります。

標準出力ストリーム

これまでの章でも登場していますが、標準出力への出力には std::cout を使います。

#include <iostream>

int main()
{
    std::cout << "Hello, World" << std::endl;
    std::cout << 100 << std::endl;
    std::cout << 1.23 << std::endl;
}

実行結果:

Hello, World
100
1.23

printf関数と違って、型の指定を間違える可能性がないので、安全性が高まっています。

【上級】また、プログラマーが任意で作った型に対しても適用できる拡張性にも特徴があります。これについては、第35章で取り上げます。

1度に出力させることも可能です。

#include <iostream>

int main()
{
    std::cout << "Hello, World" << "\n"
              << 100 << "\n"
              << 1.23 << std::endl;
}

実行結果:

Hello, World
100
1.23

printf系の関数のときには、整数を3文字幅で出力するために “%3d” と書くように、いくらかの特別な記法が用意されていました。std::cout の場合は、マニピュレータという仕組みを使って、通常とは異なる方法での出力を実現できます。マニピュレータについては後述します

標準入力ストリーム

標準入力から入力を受け取るには、std::cin を使います。

#include <iostream>

int main()
{
    char str[80];
    int n;
    double d;

    std::cin >> str;
    std::cin >> n;
    std::cin >> d;

    std::cout << str << "\n"
              << n << "\n"
              << d << std::endl;
}

実行結果:

abc   <-- 入力
100   <-- 入力
1.23  <-- 入力
abc
100
1.23

std::cin の場合は、std::cout とは使う演算子が異なります。入力された値を変数へ格納するという流れになるので、格納先の変数の方へ向いているように見せるため、>>演算子を使います。

scanf関数のように、格納先の変数のメモリアドレスを指定する必要はなく、変数名をそのまま指定できます。

文字配列へ格納する場合は、結果的にメモリアドレスを指定していることになりますから、これだけは例外的です。

【上級】変数名を指定するだけで済むのは、参照(第16章)という仕組みで指定しているからです。文字配列の方はポインタを使っています。そして、このような仕組みの切り分けのために、関数のオーバーロード第8章)を行っています。

std::cin による入力では、空白文字を読み飛ばします。ここでいう「空白文字」とは、isspace関数が 0以外を返す文字を指します。

先ほどのサンプルプログラムを修正して、3つの入力を一気に行うようにしてみます。“abc 100 1.23” のように、空白で区切って入力することで、正しい結果を得られます。

#include <iostream>

int main()
{
    char str[80];
    int n;
    double d;

    std::cin >> str >> n >> d;

    std::cout << str << "\n"
              << n << "\n"
              << d << std::endl;
}

実行結果:

abc 100 1.23   <-- 入力
abc
100
1.23

std::cout と同様に、“%d” や “%s” といった scanf関数のようなフォーマット指定が不要であり、型の違いを考慮する必要がありません。しかし、入力の場合には、バッファオーバーフローの問題が考えられることに注意が必要です。先ほどのサンプルプログラムでいえば、変数str の大きさを超える入力を与えると、バッファオーバーフローになってしまいます。

文字列がバッファオーバーフローを起こすことを防ぐためには、そもそも、文字型の配列を使うことを避けて、std::basic_string を使うことです。std::cin も std::cout も、std::basic_string をきちんと扱ってくれますから、非常に簡単に、安全性を高められます。

#include <iostream>
#include <string>

int main()
{
    std::string str;

    std::cin >> str;
    std::cout << str << std::endl;
}

実行結果:

abc  <-- 入力
abc

std::basic_string を使って受け取ると、どんなに長い文字列が入力されたとしても、それに応じて自動的にメモリを割り当ててくれますから、バッファオーバーフローは起こり得ません。

標準エラーストリーム

標準エラーへの出力を行うには、std::cerr または std::clog を使います。どちらを使っても出力先は同一です。

2つの違いは、std::cerr はバッファリングされておらず、std::clog はバッファリングされていることです。

使い方に関しては、std::cout と同じです。

#include <iostream>

int main()
{
    std::cerr << "cerr" << std::endl;
    std::clog << "clog" << std::endl;
}

実行結果:

cerr
clog


マニピュレータ

C++ の入出力ストリームでは、マニピュレータを使って、入出力の方法を制御できます。

標準入力ストリーム」のところで、入力を文字配列に受け取ろうとすると、バッファオーバーフローの危険性があると指摘しました。std::basic_string を使うことが最良の対策ですが、マニピュレータを使って、受け取る文字数に制限を掛けることで対応してみましょう。

#include <iostream>
#include <iomanip>

int main()
{
    char str[5];

    std::cin >> std::setw(sizeof(str)) >> str;
    std::cout << str << std::endl;
}

実行結果:

abcde  <-- 入力
abcd

ここでは、std::setw というマニピュレータを使いました。直後の ( ) に最大文字数を指定すると、入力がその文字数までに制限されます。この ( ) は特別な構文ではなく、単なる実引数の指定です。引数がないマニピュレータもありますが、その場合には ( ) を付けるとコンパイルエラーになります。

std::setw のように引数を伴うマニピュレータを使うには <iomanip> をインクルードします。

マニピュレータは、<< や >> の前後に挟み込むようにして使います。これらの演算子の形状が、データの流れの方向を表しているのだとみれば、マニピュレータを通過することで、データに何らかの制御が加えられるとイメージできます。今回の場合、std::cin から入力データが std::setw に送られ、ここで 5文字幅という制限が加えられます。そして、制限された結果が変数str へ送られていきます。

実は、これまで使ってきた std::endl もマニピュレータの一種です。以前説明したとおり、std::endl は、改行文字+フラッシュを意味しています

改行だけが必要ならば、「std::cout << “\n”;」のように書けば良いですし、フラッシュだけが欲しければ、「std::cout << std::flush;」と書きます。std::flush もマニピュレータです。

マニピュレータは種類が多いので、言語解説編ではこれ以上扱いません。【標準ライブラリ】第30章で取り上げます。

ワイドストリーム

std::cout、std::cin、std::cerr、std::clog はそれぞれ、char型で表現される文字を扱います。

wchar_t型で表現されるワイド文字を扱いたい場合には、それぞれの名前の頭に w を付けたワイドストリーム版を使います。それぞれ、std::wcoutstd::wcinstd::wcerrstd::wclog となります。機能的には変化はありません。

ワイドストリームによる文字の入力は、ストリームのエンコーディング形式に従って読み取りを行い、wchar_t型に変換した結果を得ます。出力は反対に、wchar_t型から、ストリームのエンコーディング形式へ変換して行います。

wchar_t型が使うエンコーディング形式は実装依存です。Visual Studio 2017 では、UTF-16 が使われています。

ストリームのエンコーディング形式の部分で、ロケールが絡んできます。ロケールについては、C言語編でも少し触れていますが、C++ ではプログラミングの仕方が大きく異なるので、あらためて取り上げます。

ロケール

ロケールは、国や地域における文化慣習の違いを吸収するための仕組みです。たとえば、小数点の表現方法であったり、通貨記号の違いであったりというものに別個に対応するコードを書かなくて済みます(実際にはかなり難しい場合もあり、理想論に過ぎないともいわれますが)。

ロケールの設定を変更することで、結果としてエンコーディング形式を変更できます。ロケールという仕組みの本来の趣旨を考えると、エンコーディング形式を変更するために使うのはやや不自然な気はしますが(文字を表現するために Shift_JIS を使うのか、UTF-16 を使うのか、というのは文化慣習の違いなのでしょうか?)、このような方法が取られています。

C++ のロケール設定には以下のように種類があり、影響を与える範囲が異なります。

  1. グローバルロケール
  2. ストリームのロケール
  3. C言語のロケール

C言語では、setlocale関数を使ってロケールの設定を行いました(C言語編第46章)。C++ でもこの関数は使用できますが、この関数で変更されるのは「C言語のロケール」だけです。つまり、C言語由来の関数だけが影響を受け、std::wcout のような C++ の機能には影響しません。

C++ ではさらに、プログラム全体に影響するグローバルロケールと、各ストリームに別個に設定されるロケールがあります。

グローバルロケールのデフォルトは、“C”ロケール(Classicロケール)です。“C”ロケールは、アメリカ英語の環境をターゲットにしたものであり、ASCII で表現できない文字は扱えません。

グローバルロケールは、std::locale::global関数を使って変更できます。

【上級】std::locale はクラス(第11章)であり、global はその staticメンバ関数第18章)です。

std::locale::global関数はその内部で setlocale関数を呼び出して、C言語のロケールも同時に変更します。グローバルロケールとC言語のロケールの設定が食い違っている状況はトラブルの元になるので、C++ では自力で setlocale関数(std::setlocale関数)を呼ばない方が良いと思われます。

ストリームのロケールのデフォルトは、そのストリームが作成された時点でのグローバルロケールの内容に従います。

std::wcout などの標準のストリームは、プログラム開始時点(main関数の最初の文の実行よりも前)で作成済みになっています。

ストリームのロケール設定は、グローバルロケールが変更されても追従しませんが、imbue関数を使って変更できます。

【上級】imbue関数は、std::ios_baseクラスのメンバ関数です。std::wcout は std::basic_ostreamクラステンプレートのオブジェクトであり、std::basic_ostream の基底クラスに std::ios_base があります。この辺りの関係性については、【標準ライブラリ】第27章を参照してください。

std::locale::global関数も、imbue関数も、実引数にロケール設定を表す値を指定して呼び出します。たとえば、ネイティブロケールと呼ばれる、実行環境が定義する、その環境での標準的なロケール設定に変更するには、次のように呼び出します。<locale> のインクルードが必要です。

#include <iostream>
#include <locale>

int main()
{
    // グローバルロケールを、ネイティブロケールに変更する
    std::locale::global(std::locale(""));

    // std::wcout のロケールを、ネイティブロケールに変更する
    std::wcout.imbue(std::locale(""));
}

いずれも実引数に、「std::locale("")」を指定しています。この指定が、ネイティブロケールに変更することを意味しています。日本向けの環境でネイティブロケールに変更するということは、日本語が扱えるような設定になるということです。

実引数の指定の仕方はほかにもやり方がありますが、たとえば「std::locale()」とすると、現在のグローバルロケールの設定を使うという意味になります。

#include <iostream>
#include <locale>

int main()
{
    // グローバルロケールを、ネイティブロケールに変更する
    std::locale::global(std::locale(""));

    // std::wcout のロケールを、現在のグローバルロケールと同じにする
    std::wcout.imbue(std::locale());
}

ここまでを踏まえて、ワイドストリームを使った入出力は以下のようになります。

#include <iostream>
#include <locale>
#include <string>

int main()
{
    // グローバルロケールを、ネイティブロケールに変更する
    std::locale::global(std::locale(""));

    // ストリームのロケールを、現在のグローバルロケールと同じにする
    std::wcin.imbue(std::locale());
    std::wcout.imbue(std::locale());

    std::wstring s;
    std::wcin >> s;

    std::wcout << s << std::endl;
}

実行結果:

あいうえお  <-- 入力
あいうえお

最低限変更しなければならないのは、std::wcin と std::wcout のロケール設定です。前述のとおり、ストリームのロケールのデフォルトはグローバルロケールに従い、グローバルロケールのデフォルトは “C”ロケールです。“C”ロケールのままでは日本語の文字を出力できないので、std::wcin と std::wcout のロケール設定を変更する必要があります。そのためには、環境のデフォルトのロケール、つまりネイティブロケールにしておけば良いはずです。

imbue関数だけを使うのでも目的は果たせますが、グローバルロケールとストリームのロケールの状態を一致させておくために、まず std::locale::global関数を使って、グローバルロケールをネイティブロケールに変更しました。そのあとで、現在のグローバルロケールの設定を、std::wcin と std::wcout に適用しています。

std::wcin や std::wcout は、プログラムの実行直後の時点で作成済みになっていますし、ストリームのロケールは、グローバルロケールの変更を追従しませんから、std::locale::global関数だけではうまくいかないはずです。ところが、Visual Studio 2017 では、std::locale::global関数を呼び出して、ネイティブロケールに変更するだけでも、正しい結果が得られるようです。std::setlocale関数を使っても同様に正しい結果が得られるので、std::wcin や std::wcout が、C言語のロケール設定の影響を受けているようです。


練習問題

問題① std::hex、std::oct、std::dec というマニピュレータを使うと、16進数、8進数、10進数での整数出力が可能になります。適当な整数値を出力して試してみてください。

問題② 問題①のマニピュレータを使うだけでは、16進数における 0x や、8進数における 0 といったプリフィックスが付きません。これを付けるためには、std::showbase が使えます。また、付けないようにする(戻す)ためには、std::noshowbase が使えます。それぞれ、試してみてください。


解答ページはこちら

参考リンク


更新履歴

’2019/2/12 VisualStudio 2015 の対応終了。

’2018/8/4 全体的に見直し修正。
ワイドストリーム」の項の中に、「ロケール」の項を追加。サンプルプログラムも追加。

’2013/12/15 新規作成。



前の章へ (第4章 文字列)

次の章へ (第6章 ファイルストリームの基礎)

C++編のトップページへ

Programming Place Plus のトップページへ



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