こんにちは。田原です。
今回は、C++を使うにあたって欠かせない、main()関数、標準入出力、ローカル変数、基本の型についての基礎を解説します。
図1.main()関数が呼ばれるまでの流れ
main()関数はC/C++のプログラムで最初に呼ばれる関数です。このmain()関数から「呼び出し元」へリターンするとプログラムも終了します。
上記の「図1.main()関数が呼ばれるまで」を御覧ください。
「あなたのプロセス」は、あなたが開発したプログラムを起動すると生成されます。その流れは次の通りです。
- 「あなたのプログラム」を起動すると、
- OSは「プログラム」が必要とするメモリを獲得して、
- ハードディスクから獲得したメモリへ「プログラム」を読み込みます。
- そして、「プログラム」に含まれているスタートアップ・ルーチンを呼びます。
- スタートアップ・ルーチンは静的変数用メモリとスタック用メモリを初期化し、
- main()関数を呼びます。
- ヒープ用メモリは「プログラム」からの要求により獲得/解放されます。
- 最後に、main()関数から戻ってくると必要な後始末を行った後、main()関数からの戻り値をOSへ渡してプログラムを終了します。
このようにmain()関数とOSとの橋渡しをする「スタートアップ・ルーチン」が存在しています。この縁の下の力持ちの存在は意外に重要です。普段のプログラミングで意識することはあまりないですが、その存在を知っていると理解が捗ると思います。
空のmain()関数は下記のようなコードになります。
int main(int argc, char* argv[]) { return 0; }
argcとargvは、コマンドライン・パラメータと呼ばれるものです。
例えば、Windowsでtest.exe foo bar baz
と実行すると、先に述べたスタートアップ・ルーチンにて次のように設定されてmain()関数が呼ばれます。
argc=4; argv[0]="test.exe"; argv[1]="foo"; argv[2]="bar"; argv[3]="baz";
では、それを確認するためのサンプルです。
command-test.cpp
#include <iostream> // std::coutのため #include <cstdlib> // EXIT_SUCCESSのため int main(int argc, char* argv[]) { for (int i=0; i < argc; ++i) { std::cout << "argv[" << i << "]=" << argv[i] << "\n"; } return EXIT_SUCCESS; }
CMakeLists.txt
project(command-test) add_executable(command-test command-test.cpp)
これは以下のように実行されます。
1. 変数iに0を設定する。
2. iがargc以上なら6.へジャンプ(ループ終了)
3. std::coutを使ってargv[i]の内容を分かりやすく表示する。
4. iを+1する。
5. 2.へ戻る。
6. EXIT_SUCCESSを呼び出し元へ返却して終了する。
EXIT_SUCCESSはOSに対して「正常終了」したことを通知するためのコードです。(多くの処理系で0です。)異常終了したことを通知する場合はEXIT_FAILUREを使います。(このコードは多くの処理系で1です。)
この戻り値はOSによっては意味が異なることがあるため、このようにマクロが定義されています。戻り値が0や1ではないOS向けの処理系(コンパイラ)では別の値が設定されています。
以下、ビルドして実行する操作の例です。(第2回(Windows向け)と第3回(linux向け)では統合開発環境での使い方も解説していますので、そちらも参考にされて下さい。)
Windows:
>mkdir build >cd build >cmake .. (CMake出力省略) >cmake --build . (ビルド出力省略) >Debug\command-test.exe foo bar baz argv[0]=Release\command-test.exe argv[1]=foo argv[2]=bar argv[3]=baz
ubuntu:
$ mkdir build $ cd build $ cmake .. (CMake出力省略) $ cmake --build . (ビルド出力省略) $ ./command-test.exe foo bar baz argv[0]=Release\command-test.exe argv[1]=foo argv[2]=bar argv[3]=baz
このようにコマンドラインでスペースで区切ったコマンドラインの文字列の数がargcに、分解された文字列がargv[]で渡されます。これらの文字列はコマンドライン・パラメータと呼ばれてます。
そして、戻り値をcommant-textを起動した側で確認するには下記のようにします。
Windows:
> Release\command-test.exe ← ここでプログラム起動 argv[0]=release\command-test.exe > echo %ERRORLEVEL% ← ここでプログラムからの戻り値を表示 0
ubuntu:
$ ./command-test.exe ← ここでプログラム起動 argv[0]=./command-test.exe $ echo $? ← ここでプログラムからの戻り値を表示 0
command-test.cppのEXIT_SUCCESSを他の値へ変更して、上記表示がどのようになるか確認してみると良いと思います。
最後に、この戻り値はWindowsでは「終了コード」、linuxでは「終了ステータス」と呼ばれています。検索するとこれらの使い方を解説しているサイトが多数ありますので、興味の有る方は是非見てみて下さい。
コマンドライン(DOSプロンプトや端末)でプログラムを実行する場合、その主な入出力先は「標準入力」、「標準出力」、「標準エラー出力」の3種類になります。
そして、C++にて標準入出力との間で入出力する際には、std::cin、std::cout、std::cerrを用いることがお薦めです。(他にC言語のstdin, stdout, stderrも使えますが、使い勝手が良くないのであまりお勧めしません。)
まず、これらを使えるようにするために、#include としてインクルードします。
- 標準入力
変数に入力する時は、std::cin >> 変数;で入力できます。 -
標準出力
変数や定数を出力する時はstd::cout << “foo : ” << 変数 << “\n”;のように記述します。
std::coutはC言語のprintfのように変数の型を指定しなくてよいのでミスが発生しづらいです。その分書式指定は面倒ですが、必要であればマニピュレータを使って書式指定できます。 -
標準エラー出力
std::cerrはstd::coutと使い方は同じです。
サンプル・ソースです。ビルドと実行方法は先程と同じです。
iostream-test.cpp
#include <iostream> // std::coutのため #include <cstdlib> // EXIT_SUCCESSのため int main(int argc, char* argv[]) { std::cout << "Please input a number.\n"; int number=0; std::cin >> number; std::cout << "Your number is " << number << ".\n"; return EXIT_SUCCESS; }
CMakeLists.txt
project(iostream-test) add_executable(iostream-test iostream-test.cpp)
C++では実行文(各種計算、条件分岐、関数呼び出しなどなど)は関数の中に書くことが出来ます。逆に言うと関数の外には実行文を原則として書けません。
その関数の中の複数の実行文を{}ブロックで更に細かく囲むことが出来ます。そして、その{}ブロック内だけで有効な変数のことをローカル変数と呼びます。(ブロックの外からアクセスすることができない変数なので「ローカル」です。)
下記の例の、変数x, y, z, iは全てローカル変数です。ブロック内で定義されてから、そのブロックの終わりまで有効です。
int main(int argc, char* argv[]) { // ブロックAの始まり int x; for (int i=0; i < argc; ++i) { // ブロックBの始まり : int y; // この行以降、変数yは有効 : } // ブロックBの終わり // この直前の}まで変数i, yは有効。ここでは無効。 { // ブロックCの始まり : int z; // この行以降、変数zは有効 : } // ブロックCの終わり // この直前の}まで変数zは有効。ここでは無効。 return EXIT_SUCCESS; } // ブロックAの終わり // この直前の}まで変数xは有効。 // ここには実行文を書けないので全ての変数を操作できない。
ローカル変数の隠蔽について
同じ名前のローカル変数を2つ以上、同じブロック内に定義することはできませんが、ブロックが異なれば可能です。そして、もし、外側のブロックと内側のブロックに同じ名前の変数を定義すると、内側のブロックでは外側のブロックで定義した変数を変更できません。
うっかりやってしまうと、変数に結果を設定しているのに設定されていないという辛いことになります。私も昔やっちゃいました。なかなかハマります。同じ関数内で同じ名前のローカル変数は使わないようにしましょう。
その様子が分かるサンプルを示します。
local-var.cpp
#include <iostream> // std::coutのため #include <cstdlib> // EXIT_SUCCESSのため int main(int argc, char* argv[]) { int i; int j; i=10; j=100; std::cout << "i=" << i << " j=" << j << "\n"; for (int i=0; i < argc; ++i) { if (j == 100) { i=20; j=200; std::cout << "i=" << i << " j=" << j << "\n"; } } std::cout << "i=" << i << " j=" << j << "\n"; return EXIT_SUCCESS; }
CMakeLists.txt
project(local-var) add_executable(local-var local-var.cpp)
Windows:
>mkdir build >cd build >cmake .. (CMake出力省略) >cmake --build . (ビルド出力省略) >Debug\local-var.exe x=10 y=100 x=20 y=200 x=10 y=200
ubuntu:
$ mkdir build $ cd build $ cmake .. (CMake出力省略) $ cmake --build . (ビルド出力省略) $ ./local-var.exe x=10 y=100 x=20 y=200 x=10 y=200
ローカル変数iは内側のブロックで覆い隠されて変更されませんでした。
こんな小さなプログラムでも見落としそうですね。くれぐれもご用心下さい。
C++は変数の型について、比較的厳密なプログラミング言語です。使い勝手を良くするために大らかな部分と不具合を出しにくくするため厳密な部分が混在しているので、意外に難しいです。
ですが、変数の型をきちんと理解しておくとC++を使ったプログラミングがよりスムーズになります。
ここでは、C++のコア言語が提供する基本的な型について説明します。enum型、struct/class、配列型、ポインタ型、および、参照については後日、解説します。(unionについては基礎講座では扱わないことにしました。)
全ての整数型を列挙します。
正規型名 | 省略形 | 備考 |
---|---|---|
bool | trueかfalseのみ | |
char | 1バイト文字。符号の有無は実装依存。 | |
signed char | 1バイト文字。符号有り。 | |
unsigned char | 符号無し。サイズは実装依存 | |
wchar_t | ワイド文字。符号の有無・サイズは実装依存 | |
char16_t | 2バイト文字。符号無し。C++11以降 | |
char32_t | 4バイト文字。符号無し。C++11以降 | |
short int | short | 符号あり。サイズは実装依存 |
unsigned short int | unsigned short | 符号無し。サイズは実装依存 |
int | 符号あり。サイズは実装依存 | |
unsigned int | unsigned | 符号無し。サイズは実装依存 |
long int | long | 符号あり。サイズは実装依存 |
unsigned long int | unsigned long | 符号無し。サイズは実装依存 |
long long int | long long | 符号あり。サイズは実装依存 |
unsigned long long int | unsigned long long | 符号無し。サイズは実装依存 |
全ての浮動小数点型を列挙します。
正規型名 | 備考 |
---|---|
float | サイズもフォーマットも実装依存。 |
double | サイズもフォーマットも実装依存。サイズはfloat以上である。 |
long double | サイズもフォーマットも実装依存。サイズはdouble以上である。 |
msvcとgccはどちらともIEEE 754に準拠しているようです。
msvcのlong doubleはdoubleと同じサイズです。
gccのlong doubleは16バイトですが、実際に値を表現するのに使われているのは10バイトです。つまりIEEE754のbinary128ではなく拡張精度形式です。
特殊な型が2つあります。
- void型
「何もない」ことを示す型です。関数の戻り値がないことを示すために指定したり、指している先の型を決めないポインタをvoid*で表現したりするのに用います。 -
std::nullptr_t型
これはC++11で定義されたnullptrの型です。nullptrはどこも指していないポインタの値です。nullptrは良く使いますが、std::nullptr_tを使うことはかなり稀です。このような型があることだけ知っておいて下さい。
数値型のサイズは実装依存が多く表現可能な値の範囲の相違でハマることが有ります。ですので、初めて使うコンパイラでは表現可能な範囲を一度調べておくことをお勧めします。マニュアルに記載があれば簡単ですが、ない場合はプログラムで確認しても良いと思います。そのためには、標準ライブラリのstd::numeric_limitsクラス・テンプレートを用いることができます。後ほどサンプル・ソースを示します。
また、異なる処理系の間でデータ交換する場合、表現できる範囲が異なる変数を使ってデータ交換すると問題が起きやすいです。
C++11ではサイズが保証された整数型が定義されました。サイズが保証された型を用いると互換性の問題を軽減できますので、異なる処理系間で整数型のデータ交換する変数についてはこれらの型を用いることをお勧めします。
次に、サイズが同じでもそのメモリ上に記録する時のフォーマットが同じとは限りません。エンディアンの相違が問題になることが多いです。サイズが2バイト以上の数値型について、上位バイト→下位バイトの順序でメモリに記録する(ビッグ・エンディアン)、もしくは、下位バイト→上位バイトの順でメモリに記録する(リトル・エンディアン)の2種類の方式がメジャーです。私の知る限りインテルのCPUは下位桁→上位桁の順で記録します。インテル以外のCPUは上位桁→下位桁の順で記録するものが多いようです。
サイズが同じでもエンディアンが異なる場合は、エンディアン変換処理が必要なります。意外に手間がかかりますので要注意です。
ちょっと宣伝です:
私が開発しているTheolizer®と言うシリアライザは自動的にエンディアン変換する機能も内蔵しています。同じ処理系間でのデータ保存やデータ交換だけでなく、異なる処理系間でのデータ交換の開発負荷も低減できます。
基本型のサイズや範囲を調査するサンプル・ソースです。
fundamental-types.cpp
#include <iostream> // std::coutのため #include <iomanip> // std::boolalphaのため #include <limits> // std::numeric_limitsのため #include <cstdlib> // EXIT_SUCCESSのため #define PRINT(dType) \ std::cout << std::setw(25) << #dType \ << " : sizeof=" << sizeof(dType) \ << " min=" << std::setw(20) << std::numeric_limits<dType>::min()\ << " max=" << std::setw(20) << std::numeric_limits<dType>::max()\ << " signed=" << std::boolalpha \ << std::numeric_limits<dType>::is_signed << "\n" #define PRINTCH(dType) \ std::cout << std::setw(25) << #dType \ << " : sizeof=" << sizeof(dType) \ << " min=" << std::setw(20) \ << static_cast<int>(std::numeric_limits<dType>::min()) \ << " max=" << std::setw(20) \ << static_cast<int>(std::numeric_limits<dType>::max()) \ << " signed=" << std::boolalpha \ << std::numeric_limits<dType>::is_signed << "\n" int main(int argc, char* argv[]) { // 整数型 PRINT(bool); PRINTCH(char); PRINTCH(signed char); PRINTCH(unsigned char); PRINT(wchar_t); PRINT(char16_t); PRINT(char32_t); PRINT(short int); PRINT(unsigned short int); PRINT(int); PRINT(unsigned int); PRINT(long int); PRINT(unsigned long int); PRINT(long long int); PRINT(unsigned long long int); // 浮動小数点型 PRINT(float); PRINT(double); PRINT(long double); // 特殊型(値が数値ではないため、std::numeric_limits<>を使えません。) std::cout << std::setw(25) << "nullptr_t" << " : sizeof=" << sizeof(std::nullptr_t) << "\n"; // sizeof(void)はコンバイル・エラーになります。 // std::cout << std::setw(25) << "void" << " : sizeof=" << sizeof(void) << "\n"; return EXIT_SUCCESS; }
基本型全てのバイト数と値の範囲、および、符号の有無を表示しました。
値の範囲はstd::numeric_limits<>::min(), max()を使ってますが、この戻り値は調べたい型と同じです。そして、std::coutはchar, signed char, unsigned charについては数値ではなく文字として出力しますので、文字コードが割り当てられていない値を表示しようとするとおかしな表示になります。そこで、PRINTCH()マクロではint型へ型変換(static_cast())して表示してます。
表示を整えるためにマニュピレータを2つ使いました。
- std::setw(n)
これは次に表示されるものをn文字幅で右詰めで表示します。 -
std::boolalpha
bool型を入出力する時にture/falseを使えるようにします。
CMakeLists.txt
project(fundamental-types) if(NOT MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") endif() add_executable(fundamental-types fundamental-types.cpp)
今回、CMakeLists.txtが少し従来と異なります。
std::numeric_limits等C++11以降で使えるものを使っています。これらを有効にするため、g++はコンパイル・オプションとして-std=c++11を記述する必要が有ります。msvcは対応している最新版を自動的に仮定するので追加のパラメータは不要です。そこで、msvc以外の時、-std=c++11というパラメータを追加しています。(MSVCはCMakeのキーワードです。)
以下、実行結果です。ところどころ相違があることが分かると思います。
Windows(32ビット・ビルド)実行結果
bool : sizeof=1 min= 0 max= 1 signed=false char : sizeof=1 min= -128 max= 127 signed=true signed char : sizeof=1 min= -128 max= 127 signed=true unsigned char : sizeof=1 min= 0 max= 255 signed=false wchar_t : sizeof=2 min= 0 max= 65535 signed=false char16_t : sizeof=2 min= 0 max= 65535 signed=false char32_t : sizeof=4 min= 0 max= -1 signed=false short int : sizeof=2 min= -32768 max= 32767 signed=true unsigned short int : sizeof=2 min= 0 max= 65535 signed=false int : sizeof=4 min= -2147483648 max= 2147483647 signed=true unsigned int : sizeof=4 min= 0 max= 4294967295 signed=false long int : sizeof=4 min= -2147483648 max= 2147483647 signed=true unsigned long int : sizeof=4 min= 0 max= 4294967295 signed=false long long int : sizeof=8 min=-9223372036854775808 max= 9223372036854775807 signed=true unsigned long long int : sizeof=8 min= 0 max=18446744073709551615 signed=false float : sizeof=4 min= 1.17549e-38 max= 3.40282e+38 signed=true double : sizeof=8 min= 2.22507e-308 max= 1.79769e+308 signed=true long double : sizeof=8 min= 2.22507e-308 max= 1.79769e+308 signed=true nullptr_t : sizeof=4
ubuntu(64ビット・ビルド)実行結果
bool : sizeof=1 min= 0 max= 1 signed=false char : sizeof=1 min= -128 max= 127 signed=true signed char : sizeof=1 min= -128 max= 127 signed=true unsigned char : sizeof=1 min= 0 max= 255 signed=false wchar_t : sizeof=4 min= -2147483648 max= 2147483647 signed=true char16_t : sizeof=2 min= 0 max= 65535 signed=false char32_t : sizeof=4 min= 0 max= 4294967295 signed=false short int : sizeof=2 min= -32768 max= 32767 signed=true unsigned short int : sizeof=2 min= 0 max= 65535 signed=false int : sizeof=4 min= -2147483648 max= 2147483647 signed=true unsigned int : sizeof=4 min= 0 max= 4294967295 signed=false long int : sizeof=8 min=-9223372036854775808 max= 9223372036854775807 signed=true unsigned long int : sizeof=8 min= 0 max=18446744073709551615 signed=false long long int : sizeof=8 min=-9223372036854775808 max= 9223372036854775807 signed=true unsigned long long int : sizeof=8 min= 0 max=18446744073709551615 signed=false float : sizeof=4 min= 1.17549e-38 max= 3.40282e+38 signed=true double : sizeof=8 min= 2.22507e-308 max= 1.79769e+308 signed=true long double : sizeof=16 min= 3.3621e-4932 max= 1.18973e+4932 signed=true nullptr_t : sizeof=8
今回は、下記についてサンプル・ソースを使って解説しました。
- main()関数が呼び出されるところから初めて、main()関数のバラメータの説明
- 標準入出力の基本的な使い方
- ローカル変数の性質と注意点
- C++の基本型の種類と性質
ソフトウェアは本当に様々な振る舞いをしますので、その細かい部分を全て解説することは現実的ではありません。重要な部分について解説していますので細かい部分は実際に皆さんで色々動かしてみて振る舞いを理解するのが早道です。頑張って下さい。
次回は、C++の様々な演算子について解説します。また、変数としての左辺値(lvalue)、演算した結果としての右辺値(rvalue)の概念についても少し解説する予定です。お楽しみに。