前回はHello Worldプログラムを通して、C++におけるプログラムの作成 & 実行方法を学びました。しかし、"Hello World"が出力される理由に関しては全く触れずじまいでした。そこで今回は、出力の仕組みを中心に、演算子の多重定義、プリプロセッサ、名前空間といった話題を紹介します。
目次
1. おさらい
C++ の出力の仕組みに触れる前に、 まずは Java の出力の仕組みをおさらいしておきましょう。 既に理解しているさ、という方は読み飛ばしてください : )。 サンプルコードは前回同様、下記の Test.java です。
/// Test.java /// class Test { public static void main(String[] args) { System.out.println( "Hello, Java !!" ); } }
出力に関する命令は、 もちろん次の 1 行です。
System.out.println( 文字列 );
このように、Java で標準出力したいときには、 出力のために必要な機能を備えている java.io.PrintStream クラスの オブジェクト java.lang.System.out を利用します。
続いて C++ のサンプルコードを確認しましょう。
/// Test.cpp /// #include <iostream> using namespace std; int main() { cout << "Hello, C++ !!" << endl; return 0; }
容易に想像がつくでしょうが、C++ の出力命令は次の 1 行です。
cout << "Hello, C++ !!" << endl;
文字列はダブルクオテーション(")で囲む、 というルールは Java と同じです。 しかし困ったことに、文字列以外には Java との共通点は見当たりません。 今回はまず、この謎の命令文の意味を先頭から順に、すなわち cout、<<、endl の順に探っていくことにします。
2. cout の正体
まずは行の先頭の cout ですが、これは C++ に標準で用意されている、 標準出力用のクラス ostream のオブジェクトで、 乱暴に言ってしまえば Java における java.io.PrintStream クラスのオブジェクト java.lang.System.out に相当します。 ostream クラスは java.io.PrintStream クラスと同様、 出力のために必要な機能を備えています。 そして、cout は System.out オブジェクトと同様に、 標準出力のために用意されているオブジェクトなのです。
3. << 演算子
続いて、Java プログラマにとっては馴染みの薄い << 演算子 です。 この << 演算子は、Java でも数値を左シフトする演算子として利用します。 通常の業務系アプリケーションの開発では利用する機会は少ないかもしれませんが、 基本情報技術者試験などでコンピュータの基礎を Java で学習された方には 覚えがあるのではないでしょうか? 念のため、Java における << 演算子の使い方を確認しておきましょう。
/// LeftShiftTest.java /// public class LeftShiftTest { public static void main( String[] args ) { int x = 1; int y = x << 1; // 整数を 1 bit 左シフト System.out.println( x + " を 1 bit 左シフトすると " + y + " になります" ); } }
実行結果は、以下のようになります。
c:\work>java LeftShiftTest 1 を 1 bit 左シフトすると 2 になります。
基本的に C++ の場合も、Java と同様に << 演算子は数値を左シフトする役割を持っています。 ところが、奇妙なことに
cout << "Hello"
は、(左辺) も (右辺) も数値ではありません。 その上、左シフト演算ではなく、(右辺)の文字列を出力してくれます。 これはどうしたことでしょうか?
実は、(左辺)に ostream クラスのオブジェクトを、(右辺)に文字列を持つ場合には、 左シフト演算ではなく、右辺の文字列を出力するよう、 << 演算子の定義が変更されているのです。 C++ では << 演算子に限らず、ほとんど全ての演算子(+、 -、<、 =)の定義を、 プログラマが自由に変更することができます。 この仕組みは 演算子の多重定義(オーバーロード) と呼ばれています。
なお、 ostream クラス は 文字列 の出力のみだけでなく、 int 型などの基本データ型も出力できるよう、 << 演算子は多重定義されています。 また、以下のように、連続して標準出力に書き込めるようにも定義されています。
/// Test2.cpp /// #include <iostream> using namespace std; int main() { int x = 10; cout << "x の値は" << x << "です。" << endl; return 0; }
なお、最初のうちは 演算子の多重定義を無理に学ぶ必要はありません。 今回の 出力用の << 演算子のように、 標準ライブラリ等で使われているケースがある、という程度の認識を持っていれば十分です。
4. endl マニピュレータ
さて、残るは endl ですが、 これは簡単に言うと、改行およびバッファのフラッシュをおこなう関数 です。 このような、入出力のストリームを操作する特殊な関数を マニピュレータ と呼びます。 << 演算子は、(左辺)に ostream クラスのオブジェクトを、 (右辺)に マニピュレータ を持つ場合、 (右辺)のマニピュレータが実行されるように、多重定義されています。
5. #include
ところで Java の場合、 出力用のオブジェクトは java.lang.System クラスが static に所持していました。 それでは C++ の場合には、cout オブジェクトはどうして利用可能なのでしょうか。 実はその仕組みが、ファイル冒頭の #include に隠されています。
#include は、"含む" という意味が示すとおり、記述箇所に指定したファイルを 含ませる 機能を持っています。より厳密に言うと、#include <XXX> という 1 文は、ファイル XXX の内容に置換されます。
ですから、
#include <iostream>
の 1 文は、iostream というファイルの内容に置換されます。 そして、何を隠そう、この iostream というファイルに、 ostream クラスやendlマニピュレータの定義、cout オブジェクトの生成などの、 入出力のために必要なコードが記述されているのです。 したがって 入出力機能を利用したいときには、 ソースコード 2 のように、iostream ファイルを include してやればよいのです。
なお、この iostream というファイルは、MinGW をインストールした際に ローカルディスクに配備されています。 例えば、MinGW を C:\MinGW にインストールした場合、 C:\MinGW\include\c++\3.2.3 フォルダ に iostream ファイルが配置されています(図 1)。 iostream のような標準ライブラリのファイルを include するときは < > でファイルを指定します。 一方、自分で作成したファイルを include する際には、 ダブルクオテーション(")で囲む決まりになっています。
図 1.iostream ファイルの在り処
ところで、この #include による置換処理は、コンパイル時に行われるわけではなく、コンパイル前 に行われます。 この コンパイル前に行われる処理 のことを プリプロセス と呼びます。 また、#include のようなプリプロセスで処理される命令を プリプロセッサティレクティブ と呼びます。 プリプロセッサディレクティブは # で始め、文末の ; は記述しないので、 通常の C++ コードと区別できるようになっています。
なお、 前回コンパイル用のコマンドとしてご紹介した g++ コマンドは、 厳密にはコンパイルだけでなく、プリプロセス もしてくれます(図 2)。
図 2.プリプロセス〜コンパイルの流れ
※ より厳密には「アセンブル」「リンク」と呼ばれる処理もされているのですが、今回は省略させて頂きます。
理屈だけでは面白くありませんので、 実際にプリプロセスされた直後の、 すなわち、コンパイルされる直前のコード内容を確認してみましょう。 MinGW では、下記のように、-E オプションをつけることで、 コンパイルを行う直前の、プリプロセスされた結果を表示させられます。
D:\workspace> g++ -E Test.cpp
ただし、今回のプリプロセスされた結果は、とても 1 画面では表示できないボリュームです。 このような場合には、 -o オプションを利用して出力ファイル名を指定することで、 プリプロセスした結果をファイルに保存できます。
D:\workspace> g++ -o Test.ii -E Test.cpp
※拡張子 “.ii"は、プリプロセス済みの C++ のソースコードであることを意味します。
ちなみに、先の iostream のファイルの中身を確認された方は、 ここまでファイルサイズが大きくなってしまうのを不思議に思われるかもしれません。 これは、iostream ファイルの中でも #include が利用されており、 またその include 対象のファイルの中でも #include が利用される、 といったように、#include の連鎖が作られているためです。
ところで、#include は、Java の import と同様、 他のプログラムの機能を利用するために使われます。 しかし、両者には 処理内容 および 処理されるタイミング という 2 点において、以下のような違いがあります。
#include(C++) | import(Java) | |
---|---|---|
処理内容 | 指定したソースファイルの内容に置換される。 | 指定したクラスファイルが JVM に読み込まれる。 |
タイミング | コンパイル前 | プログラム実行時 |
6. 名前空間
さて、ソースコード 2 に残された最後の謎は、ファイル冒頭の
using namespace std;
ですが、試しにこの 1 文をコメントアウトして、コンパイルしてみてください。
// Hello_CompileError.cpp // #include <iostream> //using namespace std; // コメントアウト int main() { cout << "Hello, C++ !!" << endl; return 0; }
残念ながら、このコードは下記のようにコンパイルエラーになってしまいます。
C:\work>g++ Hello_CompileError.cpp compile_error.cpp: In function 'int main()': compile_error.cpp:8: 'cout' undeclared (first use this function) compile_error.cpp:8: (Each undeclared identifier is reported only once for each function it appears in.) compile_error.cpp:8: 'endl' undeclared (first use this function)
エラーメッセージの内容を確認すると、 どうやら「cout やら endl なんて宣言されていないよ !!」 と叱られているようです。 実は、先ほど 「iostream ファイルを include すれば、cout や endl が利用可能になる。」 という説明をしたばかりですが、 この説明が間違いだったのです。 正しくは、 「iostream ファイルを include すれば、std::cout や std::endl が利用可能になる。」です。
論より証拠、下記のソースコードをコンパイルしてみましょう。
// Hello_Namespace.cpp // #include <iostream> //using namespace std; // コメントアウト int main() { std::cout << "Hello, C++ !!" << std::endl; return 0; }
今度は無事 コンパイル & 実行 できるはずです。
std::cout は 名前空間 std に属する cout オブジェクト を意味します。 名前空間 とは、Java の パッケージ の概念に相当し、 名前の衝突を避けるために利用されます。 ある名前空間 A に属する変数は、::(スコープ解決演算子)を用いて、
(名前空間名)::(変数名)
と表記します。 ですから、本来は cout も endl も、 ソースコード 6 のように、std::cout、std::endl と表記すればよいのですが、 毎回毎回 律儀に std:: を表記するのは面倒です。 そういった場合には、ファイル冒頭で下記のように 「これから私は名前空間 std を利用しますよ !!」と声高らかに宣言しておけば、 std:: を逐一記述する必要が無くなるのです。
using namespace std;
ちなみに、cout や endl だけでなく、 標準ライブラリ は 名前空間 std に属します。 また、プログラマが独自に名前空間を作ることもできます。 例えば、int 型の変数 x を名前空間 test の中に入れたい場合には、 下記のように表記します。
namespace test { int x; }
最後に、以下のソースコードで、名前の重複を避ける、という名前空間の目的を確認しておきましょう。 このソースコードには、同一の名前 x を与えられた変数が存在しますが、 それぞれ異なる名前空間に属するため、コンパイルエラーになることはありません。
// NamespaceTest.cpp // #include <iostream> using namespace std; namespace foo { int x = 10; // このグローバル変数 x は、名前空間 foo に属する。 } namespace boo { int x = 20; // このグローバル変数 x は、名前空間 boo に属する。 } int main() { cout << "foo::x = " << foo::x << endl; cout << "boo::x = " << boo::x << endl; return 0; }
なお、Java ではフォルダ構成とパッケージを同一に揃えなければなりませんが、 C++ では、名前空間とフォルダ構成は一致させる必要はありません。
7. まとめ
今回は、前回積み残していた HelloWorld プログラムの内容の解説を中心に、 演算子の多重定義、プリプロセッサ、名前空間などをご紹介しました。 多くの入門書では、今回取り上げた内容を「魔法のおまじない」として 紹介されていることが多いので、 今回は敢えてその内容に踏み込んでみました。(あくまで入門レベルに留めたつもりですが)。 次回はクラスについて取り上げたいと思います。 See you later.