- 追加された行はこの色です。
- 削除された行はこの色です。
C/C++の重要な題目であるメモリ管理を
本気を出して1つのトピックにまとめてみたいと思います
かなり長くなる予定なので読む人は覚悟してください
ちなみにVisualC++(以下VC)の環境下での話です。gccとかはわかりません
・概要(なぜメモリ管理が必要か?)
・変数の型
・変数のスコープ
・変数の寿命
・ポインタ
・構造体、クラス
・アライメント
・動的なメモリの確保
・placement new
・よくやる間違えと対策
・メモリリーク検出デバック技術
気づき次第+気力あり次第追記・・・
----
* 概要 [#yf007199]
プログラム上での「メモリ」というものが何を意味するのか?
ここでいうメモリというのは
コンピュータ上でデータ(数値、文字列等)をプログラム実行中に(一時的に)保存する箇所です。
メモリにはそれぞれがどの場所に配置されているのかという「アドレス」という情報がセットになっています。
ありきたりな書き方をするならばアドレスはメモリの住所みたいなものです。
アドレスがわかれば、そのアドレスが指すメモリの中身もわかるのです。
たとえば、
0x01番地にはAさんが住んでいる
0x02番地にはBさんが住んでいる
とすると
ここでいう0x01番地(0x02番地)がアドレスでAさん(Bさん)がメモリの中身です。
住所がわかれば、住んでいる人もわかるわけです
また、AさんとBさんが住む場所を入れ替わった時は
0x01番地にはBさんが住んでいる
0x02番地にはAさんが住んでいる
となり
住所(アドレス)をみると住んでいる人(メモリ)も当然入れ替わっていることになります
とりあえずここでは変数にはアドレスとメモリがセットであるという概念が大事です。
アドレスについてさらに詳しいことはポインタの項目で追記します。
プログラム上でメモリを使いたいとき、変数というものを宣言します。
また、このとき
&color(#ff0000){''<<重要>>:変数はメモリとアドレスがセットで作成され(割り当てられ)ます。''};
このことはメモリを管理する上でかなり重要です。
また、メモリ管理をうまくできれば、処理が早くなったり、
さまざまな使い道もできるようになります。
-----
* 変数の型 [#t503e160]
変数というものはアドレスとメモリがセットになっているものと説明しました。
また、&color(#ff0000){''どのような用途にメモリが使われるのかというのを決定するのが変数の「型」''};です。
C/C++言語では色々な型が使えます
基本的な型では
int型(整数格納用メモリ)
char型(文字格納用メモリ)
float型(浮動小数用メモリ)
double型(倍精度小数用メモリ)
などのほかに
ポインタ(アドレス格納用メモリ:ポインタの項目で後述)
構造体(自分で定義したメモリの格納方式:構造体の項目で後述)
クラス(自分で定義したメモリの格納方式+関数のセット:クラスの項目で後述)
の変数宣言が使えます
変数毎に確保されるメモリの領域(大きさ)が異なり、
さらに使っている処理系(コンパイラ)などによって異なりますが
sizeof(変数の型)でその変数が何バイトのメモリを使用しているか調べることができます
たとえば
Visual Studio2005,2008環境下での
C++変数のint型、float型は4バイトになっています。
当然ながら、確保している変数の要領を超えた量は保存できません
4バイトなら1バイト8ビットなので
2の32乗分の数値が判別できますが
これを超える数値は判別できず、そのような数値を代入した場合
おかしなことになります。
また&color(#ff0000){''constキーワードを変数の型の前につけることでその変数のメモリを初期化時以外に書き換えることを不能''};にします
const int a = 5; // 初期化時のみ代入可能
a = 10; // ←書き換え不能、コンパイルエラー
int b = a; // aのメモリのデータでほかのデータを書き換えることはできる
constキーワードを変数につけることでその変数は書き換えがない定数として扱うことができます
-----
* 変数のスコープ[#ea28f60c]
&color(#ff0000){''変数のスコープというのはある変数のメモリが処理計算に使える有効範囲''};です
というとわかりにくいと思うので例をあげます
たとえば、C/C++言語お決まりのmain関数(プログラムの開始点)も
ブロック{}(カギ括弧)で挟んで処理をおこなうわけですが・・・
ブロックの中で定義した変数はそのブロックの中でしか使えません
#include <stdio.h>
int main()
{
int a = 10;
int a = 10; //int型(整数用メモリ格納型)変数aの宣言
{
int b = 5; //int型(整数用メモリ格納型)変数
int b = 5; //int型(整数用メモリ格納型)変数bの宣言
}// bのスコープの終わり(このブロックをでるとbは使えない)
//a += b; // ←bのメモリは計算に使えないのでこの計算は危険なことになる
//a += b; // ←bのメモリは計算に使えない
return 0;
}// aのスコープの終わり
ここでは、{}の}を抜けたときに
ここでいいたいことは、ブロックの}を抜けたときに
処理計算に変数bのメモリが使えなくなるということです
ブロックで囲まれたところで宣言した変数をローカル変数といいます
また、すべてのブロックの外で定義された変数をグローバル変数といいます
グローバル変数はどの場所からでもその変数のメモリを使うことができますが
プログラムコードが長くなってきた場合、
どこで変数の書き変わりが起こったのかわからなくなるので好ましくはありません
極力、ローカル変数で変数は定義するようにしたほうがよいでしょう
また、ローカル変数で同名の変数が定義されていた場合、そちらの値が優先されます
ちょっと意地悪な例を出します、実行前にどの値が出力されるか考えてみてください
#include <stdio.h>
int a = 0; // グローバル変数
int main()
{
int b = a;
printf("%d\n",b);
int a = 5;// ローカル変数
int c = a;
printf("%d\n",c);
{
int a = 10; // ローカル変数
int d = a;
printf("%d\n",d);
}
int d = a;
printf("%d\n",d);
return 0;
}
局所的な計算で関数を作るまでもない処理の場合、
極力ブロックで囲むと変数の寿命が制御できて
コーディングがしやすいかもしれません
また関数などで
#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とfunc関数のaのアドレスが違う(関数の引数はメモリの値だけコピーされる)
のでmain関数のaの値は変わりません
これの解決策にmain関数のaのアドレスを渡して、
main関数でもfunc関数でも同じアドレスの指すメモリを書き換えるので
ほかのブロックでも書き換えが行われることになります
変数のアドレス自体を渡すにはポインタを使います(後述)
複数のファイルにまたがって変数の場合
externキーワードを変数の先頭につけます
externは複数のソースファイルにまたがり、共通して使えることを示します
-----
* 変数の寿命[#ea28f60c]
&color(#ff0000){''変数の寿命というのはある変数のメモリが確保されてからそのメモリが使えなくなるまでの期限''};です
次のような&color(#ff0000){''キーワードを変数の型の前につけてその変数の寿命を決定''};します。
auto 自動
static 静的
&color(#ff0000){''キーワードつけない場合は自動的にautoキーワードが付いている''};ものとして扱われます
ブロック内で宣言されたautoキーワードの変数の場合、ブロックをぬけるまでが変数の寿命となります
グローバル領域の場合はプログラムが終了するまでが寿命です
たとえば先ほどの例では
#include <stdio.h>
int main()
{
int a; //autoキーワードが省略されているint型(整数用メモリ格納型)変数
int a; //autoキーワードが省略されているint型(整数用メモリ格納型)変数a
return 0;
}// aのスコープの終わり(見えなくなる)かつ変数の寿命(メモリ自体が使えなくなる)
----
* ポインタ [#g285ff61]
----
* 構造体、クラス [#id45c9b3]
----
* アライメント [#yf007199]
処理系ごとに計算がしやすいという単位が決まっています
たとえば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;
}
----
* 動的なメモリの確保 [#yd0b6fdb]
----
* placement new [#kfa6d595]
通常、動的なメモリを確保するにはmallocやnewを使いますが
placement newはもとから割り当てられたメモリ領域から必要なデータサイズ分、
メモリを確保する方法です
メモリを確保する対象(配列など)をメモリプールとよび
通常の動的確保と違い、すでに割り当てているメモリプールから
空きメモリを取れる場所を探索するため、newと違い高速です。
利点としては、メモリ解放はもとから割り当てられている領域を使うので
newと違い、deleteしなくてもよい(メモリプールが解放される保証があればよい)
また、クラス内変数の別のクラスの配列の初期化を行うことができます
// placement new
#define _CRT_SECURE_NO_DEPRECATE 1 /* VisualC++2005 での警告抑制 */
#include <new>
#include <iostream>
#include <cstring>
char global_area[1000]; // グローバル領域
int main()
{
// グローバル領域に、char10個分の領域を確保して、そこを指すポインタを得る
char* p = new(global_area) char[10];
::strcpy( p, "aaaaaaaaa" );
// 同じ結果が出力されることを確認する
std::cout << p << std::endl;
std::cout << global_area << std::endl;
return 0;
} // プログラム終了時にglobal_areaは解放されるのでメモリリークにはならない
----
* よくやる間違えと対策 [#x4a71d91]
----
* メモリリーク検出デバック技術 [#r297ab33]
-----
* 投票 [#ea28f60c]
修正・追記の参考したいので
わかりやすかった節に投票をお願いします
#vote(概要[0],変数の型[0],変数のスコープ[0],変数の寿命[0],ポインタ[0],構造体、クラス[0],よくやる間違えと対策[0],動的なメモリの確保[0],メモリリーク検出デバック技術[0])
#vote(概要[0],変数の型[0],変数のスコープ[0],変数の寿命[0],ポインタ[0],構造体、クラス[0],アライメント[0],動的なメモリの確保[0],placement new[0],よくやる間違えと対策[0],メモリリーク検出デバック技術[0])