3.15.4 記憶クラス
記憶クラス(storage class)は、識別子がメモリ内に存在する寿命を決定します。
ここでの関心事は、変数(オブジェクト)が保持している値(データ)がいつまで失わ
れずに保たれるかということです。
記憶クラスには、大きく自動記憶クラス(一時的寿命)と静的記憶クラス(恒久的
寿命)の2つがあります。また、記憶クラスには次の4種類の記憶クラス指定子があ
ります。
auto(自動)、register(レジスタ)、extern(外部)、static(静的)
これらのキーワードを宣言の先頭に付けることで記憶クラスやリンケージが決まります。
前の2つは自動記憶クラスに関するもので、後ろの2つは静的記憶クラスに関係します。
ただし、重要なのは後ろの2つのキーワードです。
ローカル変数(オブジェクト)はデフォルトで自動記憶クラスです。この様なローカ
ル変数のことを単に自動変数と呼びます。これは、ブロックに入るときに変数(オブ
ジェクト)のメモリが割り当てられ、ブロックから出るとメモリは解放されます。関数
内の自動変数は、その関数が呼ばれる度に生成・初期化されて、関数が戻るときに消滅
します。自動記憶クラスになれるのは変数(オブジェクト)だけです。
【補足】auto(自動)と register(レジスタ)
自動記憶クラスの変数を指定するには、autoかregisterのどちらかを使
います。しかしこれらは、ほとんど使うことはありません。
autoは自動記憶クラスを指定するキーワードですが、ローカル変数(オブ
ジェクト)はデフォルトで自動記憶クラスだからです。
また、キーワードregisterは、ローカル変数の記憶場所をメモリ内ではな
く高速なプロセッサ内のレジスタに置くことをコンパイラに示唆するものです。
ただし、十分な個数のレジスタを利用できない場合(これはハードウェアに依
存する)、コンパイラはこれを無視します。現在のコンパイラの最適化機能は、
プログラマがregister宣言しなくても、それを自ら判断してくれます。
グローバル変数(オブジェクト)と関数は、静的記憶クラスです。これらのデフォル
トの記憶クラス指定子はextern(外部)です。グローバル変数(オブジェクト)は、
プログラムの実行開始時に変数のメモリが割り当てられて、その後プログロラムの実行
中ずっと値を保持しています。
静的記憶クラスのもう1つは、記憶クラス指定子staticを付けて宣言されたローカ
ル変数(オブジェクト)です。この場合、その宣言文が初めて実行されたとき変数のメ
モリが割り当てられて、その後プログロラムの実行中ずっと値を保持します。簡単なク
ラスのオブジェクトを使った例を示します。
// lst03_36.cpp
// 記憶クラスの例
#include <iostream.h>
// 簡単なクラス
class X{
public:
X(int i = 0) : data(i){
cout << "オブジェクトの生成:data = " << data << endl;
}
~X(){
cout << "オブジェクトの消滅:data = " << data << endl;
}
private:
int data;
};
X a = 3000; // グローバルオブジェクト
int main()
{
X a = 100;
static X b = 200; // 静的ローカルオブジェクト
cout << "\nブロックに入る前:" << endl;
{
X a = 10;
static X b = 20; // 静的ローカルオブジェクト
}
cout << "ブロックから出た後:" << endl << endl;
return 0;
} |
[実行結果] ソースプログラム ( lst03_36.cpp ) |
オブジェクトの生成:data = 3000
オブジェクトの生成:data = 100
オブジェクトの生成:data = 200
ブロックに入る前:
オブジェクトの生成:data = 10
オブジェクトの生成:data = 20
オブジェクトの消滅:data = 10
ブロックから出た後:
オブジェクトの消滅:data = 100
オブジェクトの消滅:data = 20
オブジェクトの消滅:data = 200
オブジェクトの消滅:data = 3000
|
復習を1つ。このクラスXのオブジェクトの宣言文は正しくは、
X a(3000); // グローバルオブジェクト
とすべきですが、「X a = 3000;」としてもコンストラクタを呼び出すことができます。
この様な振る舞いをするコンストラクタを、変換コンストラクタと呼びました。この様
な整数からクラス・オブジェクトへの暗黙の型変換を無効にするには、コンストラクタ
の先頭にexplicit を付加して宣言します。
class X{
public:
explicit X(int i = 0) : data(i){
cout << "オブジェクトの生成:data = " << data << endl;
}
・・・・・・・
};
これを明示的コンストラクタといいました。
ブロック内で宣言された静的ローカル・オブジェクトは、ブロックを抜けた後も存在
し続けます。注意すべきことは、記憶クラスとスコープ規則は全く別の概念です。ロー
カル変数をstatic宣言しても、スコープはローカルのままで変わりません。ブロック
の外からはアクセスできません。変わるのは、変数の値を保持し続けるということです。
グローバル変数(オブジェクト)はmain()関数が呼び出される前につくられます。上
の実行結果では最初のコンストラクタの呼び出し「オブジェクトの生成:data = 3000」
がそうです。これをもっとはっきりと見るには、lst03_36.cppのグローバル・オブジェ
クトの宣言文をファイルの最後(つまりmain()関数の後ろ)に移動してコンパイルして
みれば分かります。同じ実行結果が得られるはずです。
グローバル変数(オブジェクト)や静的ローカル変数(オブジェクト)は、生成され
た後はプログラムが終了するまで存在し続けます。上の結果でオブジェクトのデストラ
クタが呼び出される順番を見てください。ブロックから抜けた後は、次のようになって
います。
オブジェクトの消滅:data = 100 <=== main()関数内の自動オブジェクト
オブジェクトの消滅:data = 20 <=== ブロック内静的ローカルオブジェクト
オブジェクトの消滅:data = 200 <=== main()関数内静的ローカルオブジェクト
オブジェクトの消滅:data = 3000 <=== グローバルオブジェクト
これらの中で最初に消滅するのはmain()関数内の自動オブジェクトです。これはmain()
のブロックの終わり(スコープの終了)にあたります。残りの静的記憶クラスのオブジ
ェクトは、main()関数が制御を返した後(関数の終了後)に消滅します。また、3つの
静的記憶クラスのオブジェクトのデストラクタが呼ばれる順番は、コンストラクタが呼
ばれた時の逆順になります。
【注意】複数のソースファイルからなるプログラムでは、各ファイル
内のグローバル変数(オブジェクト)がどの順番で生成し消滅するか
は、処理系に依存します。
次のプログラムは、呼び出された回数を返すカウンタ関数を、静的ローカル変数を使
って表したものです。
// lst03_37.cpp
// 静的ローカル変数
#include <iostream.h>
// カウンタ関数
int counter()
{
static int c = 0; // 静的ローカル変数
return ++c;
}
int main()
{
for(int i = 0; i < 5; i++){
cout << "呼び出し回数: " << counter() << endl;
}
return 0;
} |
[実行結果] ソースプログラム ( lst03_37.cpp ) |
呼び出し回数: 1
呼び出し回数: 2
呼び出し回数: 3
呼び出し回数: 4
呼び出し回数: 5
|
関数counter() 内の静的ローカル変数cは、最初に呼び出されたときに1度だけ値が0
に初期化されて、その後存在し続けます。そして、この関数が呼び出される度に値をイ
ンクリメントして返します。
復習をもう1つ。このカウンタ関数は、参照型(リファレンス)を返すように定義す
ることで次のように表すこともできます。
// lst03_38.cpp
// 静的ローカル変数
#include <iostream.h>
// カウンタ関数
int& counter()
{
static int c = 0; // 静的ローカル変数
return c;
}
int main()
{
for(int i = 0; i < 5; i++)
cout << "呼び出し回数: " << ++counter() << endl;
return 0;
} |
ソースプログラム ( lst03_38.cpp )
関数counter() は、静的ローカル変数cの参照を返します。cは関数が戻った後も存在
し続けるので、呼び出し側で「++counter() 」とインクリメントすることでcの値を更
新できます。
記憶クラス指定子staticにはもう1つ別の側面があります。グローバル変数に対して
static宣言した場合、まったく別の意味を持ちます。これについては、後で述べます。
【補足】デフォルトの初期化
静的記憶クラスの変数(グローバル変数や静的ローカル変数)が、初期化設
定子を使って宣言されていない場合は、0(ゼロ)で初期化されます。一方、
自動変数は初期化してくれません。プログラマが初期化しないと「ゴミ値」で
初期化されます。
3.15.5 記憶クラスとメモリ
記憶クラスとはメモリ内の記憶領域の分類を意味します。ここで、色々な変数がメモ
リ内にどのように配置されているかを調べましょう。次のプログラムは、グローバル変
数、ローカル変数、静的ローカル変数および、動的に割り当てた変数(名無しの変数)
に対してアドレス演算子&を使ってそれらのアドレス値を表示します。
// lst03_39.cpp
// 記憶クラスとメモリ
#include <iostream.h>
int global = 1; // グローバル変数
void f()
{
int a1_local = 100; // ローカル変数
static int s1_local = 200; // 静的ローカル変数
int *p1 = new int(300); // 動的変数
cout << " ローカル変数: " << &a1_local << endl;
cout << " 静的ローカル変数: " << &s1_local << endl;
cout << " ポインタ変数: " << &p1 << endl;
cout << " 動的変数: " << p1 << endl;
delete p1;
}
int main()
{
cout << "int 型のサイズ: " << sizeof(int) << endl;
cout << "int*型のサイズ: " << sizeof(int*) << endl << endl;
cout << "[関数の外]" << endl;
cout << " グローバル変数: " << &global << endl;
cout << endl << "[関数 main]" << endl;
int a2_local = 10; // ローカル変数
static int s2_local = 20; // 静的ローカル変数
int *p2 = new int(30); // 動的変数
cout << " ローカル変数: " << &a2_local << endl;
cout << " 静的ローカル変数: " << &s2_local << endl;
cout << " ポインタ変数: " << &p2 << endl;
cout << " 動的変数: " << p2 << endl;
cout << endl << "[関数 f]" << endl;
cout << " 関数fのアドレス: " << f << endl;
f(); // 関数呼び出し
delete p2;
return 0;
} |
[実行結果] ソースプログラム ( lst03_39.cpp ) |
int 型のサイズ: 4
int*型のサイズ: 4
[関数の外]
グローバル変数: 0041914C
[関数 main]
ローカル変数: 0065FE00
静的ローカル変数: 00419154
ポインタ変数: 0065FDFC
動的変数: 00673098
[関数 f]
関数fのアドレス: 00401150
ローカル変数: 0065FDE4
静的ローカル変数: 00419150
ポインタ変数: 0065FDE0
動的変数: 006730A8
|
それぞれの変数のアドレスを見ると、アドレスの値はいくつかの階層に分かれて配置さ
れていることが分かります。この結果を、図で表したのが下図です。メモリアドレスの
値は下から上へと大きくなるように表現しています。少し長い図ですが、プログラム内
の変数名の記憶クラスの種類と、その実行結果とを合わせて良く見比べてください。
変数はすべて、int型かint型へのポインタ(int*型)で、いずれも4バイトの領域を
占めます。また、関数fのアドレス値は、次のようにして求めることができます。
cout << " 関数fのアドレス: " << f << endl;
丁度、配列名が配列の先頭アドレスを示す定数ポインタであるのと同じように、関数の
名前はメモリ内にある関数のアドレスを意味するポインタです。
一番下の方には関数が置かれています。その次に静的記憶クラスの変数が置かれ、自
動変数および動的変数という順に階層化されています。これらの記憶領域をそれぞれ順
に、プログラム領域、静的記憶領域、スタック(自動変数の記憶領域)およびヒープ領
域(または自由記憶領域)と呼びます。
注目すべきことは、ローカル変数で自動変数はスタックに置かれますが、静的ローカ
ル変数はグローバル変数と同じく静的記憶領域に置かれていることです。
これら4つの領域の配置順番や割り当てのサイズの取り方はマシンやコンパイラなど
の処理系によって違います。上の例では、関数や静的変数など、恒久的な寿命を持つ固
定されたメモリ領域は、下位メモリに置かれています。それより上部には一時的な自動
変数がスタックに置かれます。スタックは、変数の領域割り当てと解放とがリサイクル
される場所です。残りの上方アドレスには、動的に割り当てたメモリ領域が配置されて
います。このヒープメモリに置かれるのは、標準Cのライブラリ関数 malloc() やC++
のnew演算子を使って割り当てられたメモリ領域です。これらは、プログラマがその寿
命を管理します。関数 malloc()によってつくられたものは標準C言語のライブラリ関
数free()で解放され、new演算子を使って割り当てられたものは、delete演算子によ
って解放されます。
3.15.6 リンケージ
これまでの話は、1つのプログラムファイルを暗に想定してきました。複数のソース
ファイルからなるプログラムの場合では、ソースファイルの間で識別子がどのように扱
われるのでしょうか。
リンケージ( linkage)は、識別子がそのソースファイルの中だけでしか通用しな
いのか、他のソースファイルでも通用するのかを決定します。すでに見てきたように、
複数のソースファイルからなるプログラムは、個々のソースファイルがコンパイラによ
って別々にコンパイル(翻訳)されてマシン語コードに変換されます。それから、リン
カによって結合(リンク)されて1つの実行ファイルが作られます。リンケージは、フ
ァイル間で参照される識別子を対応するオブジェクトや関数と正しく結び付ける過程の
ことです。
リンケージの属性には、外部リンケージと内部リンケージの2つがあります。当然
ながらリンケージに関わるのはファイルスコープを持つもの、つまりグローバル変数(
オブジェクト)や関数です。これらのデフォルトの記憶指定子は、extern(外部)で
す。これは外部リンケージを意味し、外部のファイルからアクセスできます。
外部のソースファイルから、グローバル変数(オブジェクト)にアクセスするには、
利用する各ファイルの中でその変数(オブジェクト)を宣言しなければなりません。こ
のとき、記憶クラス指定子externを先頭に付けて宣言します。宣言文の一般形は、
extern 型 識別子;
となります。記憶クラス指定子externは、宣言した識別子が「同じファイルの別の場
所か、または別のファイルの中で定義されている」ことを意味します。コンパイラは、
この識別子に対する未解決の参照があることをリンカに知らせ、リンカはその識別子の
アドレスを見つけて未解決の参照を解決します。このとき、リンカが識別子本体の定義
を見つけられないときはリンクエラーとなり、実行ファイルの作成に失敗します。
関数プロトタイプも外部リンケージを持ちますので、他のソースファイルから利用す
ることができます。関数の場合はそれを利用するファイルでプロトタイプ宣言するだけ
で済みます。externは省略できます。つまり、関数プロトタイプは、宣言した関数名
をファイルスコープの中に入れると同時に、その関数の定義が同じファイルの別の所ま
たは別のファイルにあることを意味します。関数を複数のファイルから利用するには、
その関数を呼び出しているすべてのソースファイルで、その関数をプロトタイプ宣言す
るだけです。そしてそれらのファイルを一緒にコンパイルすれば済みます。通常、この
様な関数プロトタイプはヘッダファイルの中に配置することで、複数のソースファイル
から読み込んで利用できます。
反対に、識別子を他のファイルから見えなくするには、記憶クラス指定子staticを
指定します。グローバル変数(オブジェクト)や関数をstatic宣言すると、外のファ
イルからアクセスできなくなります。これを内部リンケージといいます。内部リンケー
ジは情報隠蔽の役割をします。そのファイル内でしか使用しないグローバル変数や関数
を、他のファイルから見えないように隠します。これによって、他のファイルからの不
要なアクセス権限を無くし、他で使われている識別子との名前の衝突を避けることがで
きます。
記憶クラス指定子staticの使い方に注意しましょう。static宣言はそれをローカ
ル変数に使う場合と、グローバル変数に使う場合とでは、まったく違う意味を持ちます。
ローカル変数では記憶クラスを静的にしますが、グローバル変数に対してはスコープを
そのファイル内に限定します。
以上をまとめた、プログラムを示します。
// lst03_40.cpp
// ソースファイルA
#include <iostream.h>
extern int x; // ファイルBの変数を使う
void g(); // ファイルBの関数を使う
void f(); // ファイルAの関数
int main()
{
cout << "ファイルBの変数x = " << x << endl;
f();
g();
return 0;
}
void f()
{
cout << "ファイルAの関数fが呼ばれました。" << endl;
}
|
ソースプログラム ( lst03_40.cpp )
|
// lst03_41.cpp
// ソースファイルB
#include <iostream.h>
int x = 100; // グローバル変数
static void f(); // ファイルAからは見えない
void g(); // ファイルAからは見える
void f()
{
cout << "ファイルBの関数fが呼ばれました。" << endl;
}
void g()
{
f();
} |
[実行結果] ソースプログラム ( lst03_41.cpp ) |
ファイルBの変数x = 100
ファイルAの関数fが呼ばれました。
ファイルBの関数fが呼ばれました。
|
ドライバプログラムのファイルAは、ファイルBで定義された変数xと関数gを使いま
す。また、2つのファイルには、同型で同名の関数fがあります。変数xと関数gを利
用するために、ファイルAで次のように宣言します。
extern int x; // ファイルBの変数を使う
void g(); // ファイルBの関数を使う
externは「他のファイルを探せ」という意味で、変数では必須ですが、関数プロトタ
イプでは省略できます。また関数名fの衝突を避けるために、ファイルBではstatic
宣言することで、ファイルAに対して名前を隠します。
[演習問題 3.13] 上のプログラムで、記憶クラス指定子 externを外すとどうなるか。
またstaticを外すとどうなるか。利用しているコンパイラを使って確かめよ。エラーを
起こす場合、どのようなエラーメッセージを表示するか。もしエラーを起こさないなら、
予期した実行結果が得られるか確認しなさい。
【ヒント】 GNU g++ や Visual C++ ではエラーとなります。Borland C++
ではエラーになりません(警告を表示します)が、上とは異なる
実行結果になります。xの値は0で初期化されます(つまりリン
クされません)。
C++ではグローバル変数をconst宣言すると内部リンケージとなります。グローバル
const変数を他のファイルから参照できるようにする(外部リンケージ化する)には、
宣言本体に明示的にextern宣言する必要があります。上の例で、プログラムBのxを
constにする場合は、
// ソースファイルB
extern const int x = 100; // externが必要
とする必要があります。一方ソースファイルAでは
// ソースファイルA
extern const int x;
となります。同じく、型名の宣言に使うキーワード typedefも、内部リンケージを持
ちます。グローバルのconstと typedefがデフォルトで内部リンケージを持つので、
これらをヘッダーファイルの中に入れることができます。複数のソースからヘッダーフ
ァイルを読み込んでも名前の衝突(変数の2重定義)は起きません。
[演習問題 3.14] C言語では、staticやexternを正しく指定しない場合どうなる
か。例えば次のプログラムで、externを外すとどうなるか。また、グローバル変数
xがconstの場合はどうなるか。利用しているコンパイラを使って確かめよ。
/* lst03_42.c
ソースファイルA
*/
#include <stdio.h>
extern int x; /* externが必要! */
int main(void)
{
printf("変数x = %d\n", x);
return 0; } |
/* lst03_43.c
ソースファイルB
*/
int x = 100; // グローバル変数
|
ソースプログラム ( lst03_42.c, lst03_43.c )
【ヒント】 Cではconst修飾したグローバル変数は、デフォルトで
外部リンケージを持ちます。つまり、constはリンケージ
属性を変えません。
Cや以前のC++では、static宣言で関数やグローバル変数(オブジェクト)に内部
リンケージ属性を持たせました。現在の標準C++ではこれと同じ効果を「無名の名前空
間」を使って実現できます。名前空間については、すぐ後のページで扱います。
| 目次 | 前のページ
| 次のページ | ページの先頭 |
Copyright(c) 2000 Yamada, K