C/C++の重要な題目であるメモリ管理を
本気を出して1つのトピックにまとめてみたいと思います
かなり長くなる予定なので読む人は覚悟してください
ちなみにVisualC++(以下VC)の環境下での話です。gccとかはわかりません
・概要(なぜメモリ管理が必要か?) ・変数の型 ・変数のスコープ ・変数の寿命 ・ポインタ ・構造体、クラス ・アライメント ・動的なメモリの確保 ・placement new ・よくやる間違えと対策 ・メモリリーク検出デバック技術
気づき次第+気力あり次第追記・・・
プログラム上での「メモリ」というものが何を意味するのか?
ここでいうメモリというのは
コンピュータ上でデータ(数値、文字列等)をプログラム実行中に(一時的に)保存する箇所です。
メモリにはそれぞれがどの場所に配置されているのかという「アドレス」という情報がセットになっています。
ありきたりな書き方をするならばアドレスはメモリの住所みたいなものです。
アドレスがわかれば、そのアドレスが指すメモリの中身もわかるのです。
たとえば、
0x01番地にはAさんが住んでいる
0x02番地にはBさんが住んでいる
とすると
ここでいう0x01番地(0x02番地)がアドレスでAさん(Bさん)がメモリの中身です。
住所がわかれば、住んでいる人もわかるわけです
また、AさんとBさんが住む場所を入れ替わった時は
0x01番地にはBさんが住んでいる
0x02番地にはAさんが住んでいる
となり
住所(アドレス)をみると住んでいる人(メモリ)も当然入れ替わっていることになります
とりあえずここでは変数にはアドレスとメモリがセットであるという概念が大事です。
アドレスについてさらに詳しいことはポインタの項目で追記します。
プログラム上でメモリを使いたいとき、変数というものを宣言します。
また、このとき
<<重要>>:変数はメモリとアドレスがセットで作成され(割り当てられ)ます。
このことはメモリを管理する上でかなり重要です。
また、メモリ管理をうまくできれば、処理が早くなったり、
さまざまな使い道もできるようになります。
変数というものはアドレスとメモリがセットになっているものと説明しました。
また、どのような用途にメモリが使われるのかというのを決定するのが変数の「型」です。
C/C++言語では色々な型が使えます
基本的な型では
int型(整数格納用メモリ) char型(文字格納用メモリ) float型(浮動小数用メモリ) double型(倍精度小数用メモリ)
などのほかに
ポインタ(アドレス格納用メモリ:ポインタの項目で後述) 構造体(自分で定義したメモリの格納方式:構造体の項目で後述) クラス(自分で定義したメモリの格納方式+関数のセット:クラスの項目で後述)
の変数宣言が使えます
変数毎に確保されるメモリの領域(大きさ)が異なり、
さらに使っている処理系(コンパイラ)などによって異なりますが
sizeof(変数の型)でその変数が何バイトのメモリを使用しているか調べることができます
またconstキーワードを変数の型の前につけることでその変数のメモリを初期化時以外に書き換えることを不能にします
const int a = 5; // 初期化時のみ代入可能 a = 10; // ←書き換え不能、コンパイルエラー int b = a; // aのメモリのデータでほかのデータを書き換えることはできる
変数のスコープというのはある変数のメモリが処理計算に使える有効範囲です
というとわかりにくいと思うので例をあげます
たとえば、C/C++言語お決まりのmain関数(プログラムの開始点)も
ブロック{}(カギ括弧)で挟んで処理をおこなうわけですが・・・
ブロックの中で定義した変数はそのブロックの中でしか使えません
#include <stdio.h> int main() { int a = 10; { int b = 5; //int型(整数用メモリ格納型)変数 }// bのスコープの終わり(このブロックをでるとbは使えない) //a += b; // ←bのメモリは計算に使えないのでこの計算は危険なことになる return 0; }// aのスコープの終わり
ここでは、{}の}を抜けたときに
処理計算に変数bのメモリが使えなくなるということです
また関数などで
#include <stdio.h>
void func(int a) { a += 5; // main関数のaとは別物 } int main() { int a = 10; func(a); printf("%d\n",a); return 0; }
とやっても、main関数のaとfunc関数のaのアドレスが違うので
main関数のaの値は変わりません
これの解決策にmain関数のaのアドレスを渡して、
main関数でもfunc関数でも同じアドレスの指すメモリを書き換えるので
ほかのブロックでも書き換えが行われることになります
複数のファイルにまたがって変数の場合
externキーワードを変数の先頭につけます
externは複数のソースファイルにまたがり、共通して使えることを示します
変数の寿命というのはある変数のメモリが確保されてからそのメモリが使えなくなるまでの期限です
次のようなキーワードを変数の型の前につけてその変数の寿命を決定します。
auto 自動 static 静的
キーワードつけない場合は自動的にautoキーワードが付いているものとして扱われます
ブロック内で宣言されたautoキーワードの変数の場合、ブロックをぬけるまでが変数の寿命となります
グローバル領域の場合はプログラムが終了するまでが寿命です
たとえば先ほどの例では
#include <stdio.h> int main() { int a; //autoキーワードが省略されているint型(整数用メモリ格納型)変数 return 0; }// aのスコープの終わり(見えなくなる)かつ変数の寿命(メモリ自体が使えなくなる)
処理系ごとに計算がしやすいという単位が決まっています
たとえば32bitマシンであれば、同時に32bit(4バイト)まで
1回の計算で処理することができます
これがメモリ管理時に困った事態を発生することがあります
(特にネットワーク系、またはメモリ使用量の制限があるとき)
というのは
たとえば32bitマシンにとっては4バイト単位のが計算しやすいので
勝手にVCのコンパイラは(構造体などの)データ型を4の倍数に統一してしまいます
このとき発生するデータの隙間の無駄メモリをアライメントといいます
対策としては32bitマシンの場合、
1つめは4バイト単位のデータ型から変数を定義していきパディングを入れます
パディングというのは詰め物という意味でメモリの隙間(アライメント)をunsigned char型などの
単位バイト(1バイト)の変数でつめてしまうことです
ただし、この場合処理系ごとに構造体の中身のパディングを変えないといけなくなります
もうひとつは#pragma packを使います
#pragma自体も処理系依存ですが
#pragma pack(push,アライメント)を使うことでアライメントを指定してやることができます
また、#pragma pack(pop)で適応範囲を指定してやることもできます
#pragma packはほとんどのCコンパイラで使えるらしい・・・(確認は必要)
// アライメント #include <stdio.h> typedef struct s1 { char ch1; int i1; } t1; // アライメントを指定してやる // 1,2,4,16,32(2の倍数単位) #pragma pack(push,1) typedef struct s2 { char ch1; int i1; } t2; #pragma pack(pop) int main(int argc,char *argv[]) { printf("size of t1=%d\n",sizeof(t1)); printf("size of t2=%d\n",sizeof(t2)); return 0; }
修正・追記の参考したいので
わかりやすかった節に投票をお願いします