C++では,データには定数と変数がある.変数とは書き換え可能な名前(識別子)付きメモリ領域のことで,定数とは書き換え不可能な名前付きメモリ領域(記号定数)と,名前を付けず直接値を記述したもの(リテラル)の両方を指す.このデータが格納されたメモリ領域は,オブジェクトと呼ばれる.オブジェクトはビット列で構成され,1つの型を持つ.内部的には,オブジェクトのビット列は,型によって大きさとそこに含むビット列の解釈方法が決定する.型によって解釈されたビット列の意味を値と呼ぶ.いくつかの型の値は,特定の記述によってソースコード内で直接表すことができる.
オブジェクトの初期値は,そのオブジェクトの定義やストレージクラス,コンストラクタの定義によって決まる。組み込みデータ型のオブジェクトは,明示的に初期設定しない限り初期設定されない.この仕様はCに対する互換性を確保するためである.しかし組み込みデータ型もまたデフォルトコンストラクタを持つ.コンストラクタとはオブジェクトの初期化を行う特別な関数のことである。コンストラクタの中でも引数を持たないもののことをデフォルトコンストラクタと呼ぶ。
オブジェクトの性質
ここではオブジェクトが持つ3つの性質について説明する.性質とは,スコープとライフタイム,ストレージクラスである.それらの性質は,宣言位置や指定子によって決定する.それらの性質について簡単に説明する.
- スコープ
- 名前(識別子)の有効範囲.
- ライフタイム
- オブジェクトがメモリに用意されてから排除されるまでの時間.
- ストレージクラス
- オブジェクトが格納されたメモリ空間の種類.
スコープ
スコープとは名前の有効範囲のことで,宣言された位置とブロック({と}で囲まれた空間)の関係で決まる.ユーザは基本的に名前を通してオブジェクトにアクセスするため,オブジェクトの生存の有無や格納場所に限らずスコープの範囲内でしか扱えない.名前についての詳細は「名前」で説明するが,以下に各スコープ名とブロックと宣言位置の関係を示す.
- グローバルスコープ
- 全てのブロックの外で宣言された場合.
- ファイルスコープ
- グローバルスコープに似ているが,アクセス範囲が定義したファイル内に限定される。このようなオブジェクトを定義するには,グローバルなオブジェクトを定義する際に頭にstaticを付ける。
- ローカルスコープ
- 関数内で宣言された場合(引数を含む).
- 名前空間スコープ
- 名前空間内でで宣言された場合.
namespace nm { int a; }; int main() { nm::a = 0; return 0; }
- クラススコープ
- クラス内で宣言された場合
class cls { private: int a; // クラス内でのみアクセス可能 public: static int d; int b; void func(); }; void cls::func() { a = 0; b = 1; } int cls::d = 2; int main() { cls c; c.b = 0; return 0; }
- 文スコープ
- forやwhile,if,switch文の中で宣言された場合(丸括弧内を含む).
for (int i=0;i<0;i++){ val[i] = i; }
ライフタイム
ライフタイムとは,オブジェクトがメモリに物理的に用意されてから論理的に削除されるまでの時間のことである.ライフタイムの過ぎたオブジェクトは,例え物理的にメモリ空間に存在していてアクセス可能であっても,その空間がいつ他のオブジェクトのために利用されるか分からない.従ってアクセスするべきでない.ライフタイムは,スコープや記憶指定子によって5種類に分類される.記憶指定子についての詳細は後述する.
- ローカル(自動)オブジェクト
実行スレッドがそれらに到達した時に作成され,スコープの終わりで削除される.以下に例を示す.
int func(int arg) // 変数argはローカルオブジェクト { int i = arg; // 変数iはローカルオブジェクト return i; }
- 一時オブジェクト
特定の部分式によって作成され,完全な式の最後に削除される.完全な式とは,他の式の部分式ではない式のこと.以下に例を示す.
int v = func(1) + 1; // func(1)の結果が一時オブジェクトに記録され,式全体の最後に削除される
- 名前空間オブジェクトと静的クラスメンバ
プログラムの開始(main関数)の前に作成され,後に削除される.以下に例を示す.
int g_i = 100; /// 名前空間(グローバル)オブジェクト namespace nm { int i = 10; // 変数iは名前空間オブジェクト }; class Cls { public: static int j; // 変数jは静的クラスメンバ Cls::Cls(){} // コンストラクタ Cls::~Cls(){} // デストラクタ }; int func() { nm::i++; return 0; } int main() { std::cout << nm::i << std::endl; return 0; }
- ローカル静的オブジェクト
実行スレッドがそれらに初めて到達した時に一度だけ作成され,プログラムの終了時に削除される.以下に例を示す.
int func() { static int i = 10; return i++; }
- フリーストアオブジェクト
newによって作成され,deleteによって削除される.以下に例を示す.
int func() { int *p = new int; // new演算子で生成される.pが指す先にある. *p = 100; delete p; // delete演算子によって削除される. return 0; }
ストレージクラス
オブジェクトは,ライフタイムやデータのアクセス保護情報の違いによって異なる種類のメモリ空間に属する.また,同じメモリ空間に属する場合であっても初期値によって,その初期化方法が異なる場合もある.それらのメモリ空間は,それぞれ異なるアクセス保護が行われる.ストレージクラスとは,オブジェクトがそれらのどのメモリ空間に属するかを指す.
これらのメモリ空間の種類は,正確には言語処理系に依存するが,命令列が格納される空間(テキスト領域)も含めて代表的なメモリ空間が4つある.
- スタック領域
- 基本的に言語処理系が必要に応じて,メモリを確保したり解放したりする領域.
- ヒープ領域
- プログラマが自由にメモリを確保したり解放したりして良い領域.
- データ領域
- 言語処理系が使用する固定サイズの領域.プログラムの開始時にメモリが確保され,終了時に解放される.
ストレージクラスは,一般的にオブジェクトがこの3つのどの領域に属するかで分類される.ここではC++のソースコードで現れる各オブジェクトがそれぞれどの領域(ストレージクラス)に対応するかを説明する.
- 自動ストレージ
関数内で定義された(ローカルスコープ名の)変数はstaticと明示的に宣言される場合を除いて,自動ストレージ(スタック領域)に配置される.自動ストレージに生成されたデータは,関数が呼び出された際に割り当てられ,呼び出し元に制御が戻る際に開放される.関数が再帰的に呼び出された場合,ある関数のあるデータはこの領域に複数存在する可能性がある.
- 静的ストレージ
グローバルスコープや名前空間スコープで宣言される変数は,関数やクラス内でstaticとして明示的に宣言される変数と同様に静的ストレージ(データ領域)に格納される.リンカはプログラムが実行を開始する前に静的ストレージを割り当てる.
- フリーストア(ヒープ)
newによって作成されたオブジェクトはフリーストア(ヒープ領域)に割り当てられる.deleteによって削除される.
記憶クラス指定子
オブジェクトを初期化する際,記憶クラス指定子を付けることで振る舞いを変更できる。
- storage-class-specifier
- auto
- 自動変数であることを示す記憶クラス指定子.この記憶クラス指定子を付けたオブジェクトは自動ストレージに保持される。autoは省略可能である。
- register
- 使用するオブジェクトができるだけレジスタを使用するように指定する。 汎用レジスタの数はメモリに格納できるオブジェクト数に比べて圧倒的に小さい。しかし多くのアーキテクチャでは,メモリからレジスタにロードし,レジスタを指定することで処理を行う。そのため,頻繁にアクセスされるようなオブジェクトに優先的にレジスタを使用させることで,プログラムの処理速度を向上できる。
- static
-
この記憶クラス指定子を付けたオブジェクトは静的ストレージに保持される。このキーワードは3種類の使い方があり,それによってスコープやライフタイムが異なる。
- ファイルスコープ
static int a; void funcA() { a = 0; } void funcB() { a = 1; }
- 静的なローカルオブジェクト
int funcA() { static int a; a += 1; return a; }
- クラス変数
-
あるクラスに関連付けられたグローバルスコープのオブジェクト。
class cls { static int a; // 宣言で付ける }; int cls::a = 10; // 定義では付けない
- extern
-
外部で定義されていることを示す記憶指定子.
定義は一度しかできない。
- fileA.cpp
int a = 10;
- fileB.cpp
extern int a; // 宣言。別ファイルで定義されたオブジェクトであることを意味。 void func() { a += 1; }
- mutable
-
定数メンバ関数であっても書き換え可能であることを示す記憶指定子.詳細はオブジェクト指向プログラミングの「mutable」を参照.
class Hoge { private: mutable int x; public: void func() const { x = 10; return; } }
その他
配列とvector
C++では,配列よりもSTLのvectorが推奨されている.配列やvectorは[]演算子で各要素にアクセスできるが,コンパイラは[]演算子の範囲チェックをできない。しかしvectorのat関数は,範囲外にアクセスした場合にout_of_range例外を送出する。これにより,[]演算子よりも安全なコードを記述しやすい。また,vectorは自動で長さが変わるため,mallocやreallocを使うことによるコードのバグを防ぐことができる。