ここでは、次の項目について解説します。
C++ は、C 言語と同様、メモリや計算速度に関する効率がとても良い言語です。私の経験からいえば、計算速度的には、究極の高速化を必要とする部分でも C++ で書いて問題になったことはありません。
C++ にはガベージコレクション機能がないので、C 言語から進化するときに言語的に進む方向を間違った、などという意見もありますが、もともと C++ はアセンブリ言語なのだという認識を持てば、これは計算効率を保つために必須の選択であったといえます。私は、極論すれば「C++ はアセンブリ言語である」と言っても間違いではないと考えています。アセンブリ言語レベルの効率を保ちつつ、このサイトのテーマである、演算子のオーバーロードや、オペレータ、テンプレートなど、高級言語の機能まで使えるなんて、すばらしいではないですか。
このページでは、変数の生成、消滅をテーマとして、C++ のアセンブリ言語的側面について見ていくことにします。
ガベージコレクションとは、端的に言えば new でシステムから取ったメモリを delete で開放しなくても、システムが勝手に後始末をしてくれるという機能です。このようなことを自動で行おうとすれば、そのオーバーヘッドが大きくなることは目に見えていて、アセンブラである C++ がこれを導入しなかったのは当然の選択であるということが明白ですね。
C++ ではネイティブな変数として次のようなものが使えます。C 言語を知っている人にとっては常識的なことなので、この表は読み飛ばしてもかまいません。変数型自体の説明や、変数の代入による型のプロモーションなどについてはマニアックな内容ではないので特に解説しません。
Type | 意味あい | VC++ でのサイズ | イメージ |
---|---|---|---|
char | 整数型 | 1 バイト | |
unsigned char | 符号なし整数型 | 1 バイト | |
signed char | 符号付き整数型 | 1 バイト | |
short | 符号付き整数型 | 2 バイト | |
unsigned short | 符号なし整数型 | 2 バイト | |
int | 符号付き整数型 | 4 バイト | |
unsigned int | 符号なし整数型 | 4 バイト | |
long | 符号付き整数型 | 4 バイト | |
unsigned long | 符号なし整数型 | 4 バイト | |
float | 浮動小数型 | 4 バイト | |
double | 浮動小数型 | 8 バイト | |
long double | 浮動小数型 | 8 バイト |
変数が、システムのメモリ上にどのように配置されるのかを理解すると、自分の書いたコードによってコンピュータの中で何が起こるのかが想像できるようになります。この動きを理解することが C++ プログラミングでは必須です。
コンピュータの主記憶は、通常大きな、たとえば連続した 2GB の塊ですが、システムは、その中を左下の図のような三つの領域に分けて使用します。
CODE は実行のための機械語をおく領域で、現在実行中のコード位置は IP(インストラクション・ポインタ) で指し示されています。
HEAP はプログラムの中で、「必要に応じてシステムから取り」、「使用後にシステムに戻す」という使い方をするメモリのプール領域です。一時的にこの中をさす DP(データ・ポインタ)が使われることもあります。C++ ではヒープのデータを取るには new 演算子を使用し、開放(システムに戻す)には delete 演算子を使用します
STACK は CPU の SP(スタック・ポインタ)で管理されるローカル記憶などをおく領域です。スタックは、アクセスの方法として、机の上に本を積み上げるときのように、下から積んだデータは、上からしか取り崩さないという方式を使います。スタックの主な使用方法は三つで、一つ目は、関数呼び出しの戻りアドレスを積んでおいて、関数から戻るときにスタックからアドレスを読んで元のアドレスに戻ることです。二つ目は、関数呼び出しの引数となる変数を積んでから関数を呼び出すことにより関数に引数を引き渡し、関数から戻るときに戻り値をスタックに積んで結果を返すことです。三つ目は、関数呼び出し後に、その関数で使用するローカル変数をスタックに積んで使用し、関数から戻るときに取り崩して跡形もなく始末することです。
一気に書いてしまいましたが、上記の事情のうち変数の生成、消滅にかかわる部分を C++ のコードとの関連で見てみましょう。
まず、グローバル領域でデータを取った場合を考えて見ましょう。ポイントは、変数のメモリが「ヒープ上に取られる」のか、「スタック上に取られる」のかということです。
#include <cstdio> using namespace std; int iValue1 = 0; // グローバル領域でメモリを取る。 char acName[] = "Japan"; // グローバル領域でメモリを取る。 static int iValue2 = 1; // グローバル領域で static なメモリを取る。 int main() { return 0; }
グローバル変数の生成消滅の確認サンプル Variable1.cpp
上記で取った iValue1、acName、iValue2 のメモリは main が実行される前にヒープに取られて居座ります。このメモリは、プログラムが終了するときに開放されてシステムに戻されます。iValue2 には static をつけましたが、グローバル領域での変数はもともと static なので、この場合つけてもつけなくても同じことになります。結論的には、グローバル変数はヒープに取られるということです。
システムによっては、ヒープに入らずに、特別なデータセグメントとして存在するかもしれませんが、ヒープに入ると考えてもプログラミング上は問題ありません。
次に、ローカル変数についてみてみましょう。ここでも、ポイントは、変数のメモリが「ヒープ上に取られる」のか、「スタック上に取られる」のかということです。
#include <cstdio> using namespace std; void func() { static int iValue1(1); // static なローカル変数 iValue1 は func() が 1 回目に呼ばれたときに生成され、 // 1 で初期化されて居座る。2 回目以降は、初期化されない。 int iValue2 = 2; // ローカル変数 iValue2 は func() が呼ばれるたびに 2 で初期化される。 } // ここで、iValue2 は消滅する。iValue1 は消滅しないで居座ったままである。 int main() { func(); func(); { int iValue3 = 3; // ローカル変数 iValue3 が生成され 3 で初期化される。 } // ここで、iValue3 は消滅する。 return 0; } // ここで、居座っていた iValue1 は消滅する。
スタック/ヒープ上に生成される変数の生成消滅の確認サンプル Variable2.cpp
main() から func() が 2 回呼び出されていますが、func() の中で定義されているローカル変数 iValue1 は static であるため、func() の呼び出しの 1 回目でヒープ上に生成され 1 で初期化された後、ヒープ上に居座ります。これは func() の呼び出しの 2 回目以降では、初期化されません。消滅するのは main() が終了して、プログラム全体が終了するときになります。
これに対して、func() の中で定義されているもう一つのローカル変数 iValue2 は、static では無いため、func() の呼出しごとにスタック上に生成され 2 で初期化され、func() の終了と共にスタックから取り下ろされて消滅します。
main() の中の {} で囲まれた iValue3 は、{} の中でスタック上に生成され、3 で初期化された後、} の後でスタックから取り下ろされて消滅します。
このように、スタック上のローカル変数は、ブレース {} で生成消滅が自動的に管理されます。static な変数がヒープに取られるということとあわせて上記メカニズムを理解していれば、プログラム中で何がおきているかよくわかります。スタック上の変数も、static な変数も特に変数の消滅のための後始末処理を記述する必要はありません。
変数の生成、消滅を理解すると、たとえば、上記の例で、iValue3 が消滅した後で iValue3 を使用するコードを書けば、もう実体がないので当然エラーになるというようなことが分かります(よね)。
本当は、マニアックでもなんでもないのですが、このページで言いたかったことをここで言っておきましょう。前項までには出てこなかったコードを入れた、次のプログラムをコメントを含めて読んでみてください。あなたが C++ 超初心者または超エキスパートであるのなら、読み飛ばしてもかまいません。
#include <cstdio> using namespace std; int main() { 3; // 定数 3 が、スタック上に積まれるが、使われないので、 // スタックから取り下ろされる。 int(); // 無名の int 型変数がデフォルトコンストラクタで 0 に初期化され、 // スタックに積まれるが、使われないので、スタックから取り下ろされる。 int(3); // 無名の int 型変数がコンストラクタで 3 に初期化され、 // スタックに積まれるが、使われないので、スタックから取り下ろされる。 int iValue1 = 0; // int 型変数 iValue1 がコンストラクタで 0 に初期化され、 // スタックに積まれる。0 がコピーされるわけではない。 int iValue2 = int();// int 型変数 iValue2 がデフォルトコンストラクタで 0 に初期化され、 // スタックに積まれる。0 がコピーされるわけではない。 int iValue3(5); // int 型変数 iValue3 がコンストラクタで 5 で初期化され、 // スタックに積まれる。 return 0; } // iValue3, iValue2, iValue1 の順で、スタックから取り下ろされる。
テンポラリオブジェクトと通常の変数の生成消滅の確認サンプル Variable3.cpp
最初の 3 ですが、これは、int 型整数 3 がスタック上にいったん置かれ、使われないのでスタックから下ろされて、何もなかったかのように実行が次に移ると考えられます。実際のところは、コンパイラによっては、スタック上でなく、CPU のレジスタに 3 を置くのかもしれないし、さらに進んで、結果的に何もおきないことが読み取れるので、本当に何も行われないかもしれません。しかし、概念的には、最初の解釈のように動くと思ってよいでしょう。こんなコードにどんな意味があるのかというと、3 が、いったん即使用可能な状態になるということと、次に移行してしまうと、3 には変数名などがないために、次ではもうアクセスのしようがない旬のあるオブジェクトだということです。このようなオブジェクトを私はテンポラリオブジェクトと呼んでいます。
次の int() という書き方は、C 言語にはなかったのですが、C++ では、ネイティブな組み込み型である int 型などのオブジェクトもクラスのコンストラクタと同じ記法で生成できるようになっています。これも、スタックに 0 が積まれてすぐに下ろされます。int(3) では、スタックに 3 が積まれてすぐに下ろされます。これらもテンポラリオブジェクトです。
オブジェクトとは端的には変数のことです。クラスオブジェクトも端的には変数です。
次の int iValue1 = 0; では、iValue1 がコンストラクタで 0 で初期化されスタックに積まれます。これは、次のように書いたのと同じように扱われます。0 がコピーされるのではなく、コンストラクタに 0 が渡されます。
int iValue1(0); // コンストラクタが呼ばれる。
次の int iValue2 = int(); では、右辺でテンポラリオブジェクトが生成されそうですが、実際には iValue2 のデフォルトコンストラクタが呼ばれて、0 に初期化されます。この例の場合には型が int なので問題になりませんが、たとえば生成するのが数百 MB の大きさのオブジェクトであるような場合に、これと同じ書き方をして、もし、右辺でテンポラリオブジェクトが生成されるとすると、メモリが倍必要になるので、これは大きな違いになってきます。このような問題を避けるために、この表現は次の表現と同じように扱われます。
int iValue2; // デフォルトコンストラクタが呼ばれる。
ちなみに、次のように () の中を空にして書いてしまうと、関数宣言とみなされてしまいます。
int iValue2(); // int 型を返す関数の宣言
次の int iValue3(5); では、iValue3 がコンストラクタで 5 に初期化されます。クラスを継承したときのベースクラスオブジェクトの初期化にもこのような形式が使われますので、このような記述が可能であるということをここで確認しておきましょう。これも、次のような表現をしてもよく、この場合もコピーではなく、コンストラクタが呼ばれます。
int iValue3 = int(5); // コンストラクタが呼ばれる。