第5回はC++の動的なメモリの確保についてです

なぜ、動的なメモリ確保が必要になるのか?
そもそも動的な確保ってなにさ?って話をします。

通常、変数を宣言時
何も考えなくてもその変数の暗黙のメモリ領域が確保されます(自動変数という)
Visual Studio2005,2008環境下での
C++変数のint型、float型は4バイトになっています。
ある変数(関数)がどれだけのメモリ容量を確保しているかは
sizeof()演算子で調べることができます。
当然ながら、確保している変数の要領を超えた量は保存できません
4バイトなら1バイト8ビットなので
2の32乗分の数値が判別できますが
これを超える数値は判別できず、そのような数値を代入した場合
おかしなことになります。

一番わかりやすい例は
配列の範囲外にアクセスすることです

int a[10];

とした場合, a[0]〜a[9]までの変数がつかえますが
a[10],a[11],a[-1]など配列の宣言した範囲外を使おうとした場合
メモリが確保されておらず、アクセスできずエラーになります
(仮にアクセスできたとしても関係ない値が入っている)

配列というデータ構造は
連続したアドレスにメモリを確保しています
これによりa[0]やa[1]など
高速に変数にアクセスすることが可能になっています
(連続でないデータ構造にはリストや木などがあります)

配列の長所としてはアドレスに直接アクセスできるため高速です
配列の欠点としては連続配置されているため挿入、削除をされると
並びなおし(ソート)に時間がかかるという点です

さて、動的なメモリ確保の話にもどりますが、
プログラムの途中で使えるメモリ領域を増やしたいな
と思う場合があります
たとえば、使える配列の量を増やしたいなという場合です。
動的なメモリを確保するためには
確保したい変数型のポインタを用意します

newキーワードを使います
int型ならば

int *a = new int;

とするとint型変数のサイズ分メモリ領域が確保されます
動的に確保したメモリは必ず解放しなければなりません
解放するためには
deleteキーワードを使います
これは変数の型によらず

delete a;//←aは任意の変数型のポインタ

で解放できます

配列でも動的なメモリは確保できます
int型のデータを100個もつ配列を作りたい場合は

int *a = new int[100];

配列の場合の解放は

delete[] a;//←aは任意の変数型のポインタ

で行います

使い終わったら必ずnewしたメモリはdelete、delete[]で解放をしてください
これを行わないとプログラム終了時にメモリが解放されず
プログラムを起動ごとに使えるメモリ領域が減っていきます
(メモリリークという)
特に携帯電話などメモリ使用が限られている環境でこれをやると致命的です
プログラムを起動し続けるとメモリ容量がなくなり、プログラムが止まることになります
メモリは資源だという認識を常に頭にいれておいてください

ここではとりあえず、配列の動的確保について説明します
単体でメモリ確保していくのはリストや木などのデータ構造に用いられるのですが、
それについては俺の余力があったらいつか説明します

#include <iostream>
using namespace std;

int main()
{
       // int型ポインタ
	int *memory;

       // int型を配列で100個分確保
	memory = new int[100];

	for(int i = 0;i < 100;++i)
	{
		memory[i] = i;
		cout << memory[i] << endl;
	}
       
       // 使わなくなったり、再確保したかったりする場合は必ずメモリ解放
	delete[] memory;

       // int型を配列で200個分確保
	memory = new int[200];

	for(int i = 0;i < 200;++i)
	{
		memory[i] = i;
		cout << memory[i] << endl;
	}

       // 使わなくなったり、再確保したかったりする場合は必ずメモリ解放
	delete[] memory;

	return 0;
}

newキーワードの利点はC言語でのmalloc関数と違い
メモリ領域確保のためのデータ型のサイズをいちいち調べなくていい点です
また、malloc関数で確保するときはクラスのコンストラクタは呼ばれません(←重要!!)
よってC++では(クラスを使うので)newでメモリの動的確保する癖をつけましょう
(お兄さんとの約束だぞ☆)

C++にはこれとは別にvectorという可変長配列のテンプレートクラスを使う方法
がありますが、配列の動的確保はファイル読み込みとかで
必要なデータ数がファイルによって異なる場合とか重宝します

さて、ここで
メモリ領域を途中で増設したいなと思うときがあります。
しかもよく考えるのが配列みたいに連続で確保できないかなぁ・・・
みたいなことです。
連続で確保したい理由としては、
配列の利点である添え字で1発でアクセスできるという利点を生かしたいということです。
(for文などで繰り返し処理させたいなぁと思うときもあるでしょうし・・・)
C言語ではrealloc関数という便利な関数で
現状の動的メモリ領域のデータを維持したまま、連続でメモリを増設させることができました。
しかし、C++言語にはrenewなどというキーワードや関数は存在しません。
ではどうするかというと、
現状の動的メモリ配列と別に拡張後の動的メモリ配列を用意し
memcpy関数で移植します。

int *data = new int[100];
 
//なんかdataに適当なデータをいれる処理後

// dataと別に動的配列を確保
int *temp = new int[200];
memcpy(temp,data,sizeof(int) * 100);
// コピーし終わったら元の動的配列はいらない
delete[] data;

// 終了処理
delete[] temp;

はい、非常にめんどくさいですね(・ω・`)
それにどの程度の量のメモリを再確保しなければならないのか
わからない場合というのが多々あります
ここで可変長配列クラスというものがあります
vectorクラスと呼ばれるもので
(追記:やってることは上記のようなメモリ再確保を内部で自動でやってくれてます)
どんな型でもテンプレートと呼ばれる機能により格納できる可変長配列クラスで
スタックやキューみたいに逐次データを連続で詰めていくことができます
使い方は次のソースコードに示します
vectorヘッダーをインクルードします

	#include <iostream>
	#include <vector>
	using namespace std;

	class Myclass
	{
	public:
		int number;
	};

	int main()
	{
		// int型の可変長配列の宣言
		vector<int> ArrayInt;
		
		// 可変長配列にデータを代入
		for(int i = 0;i < 100;++i)
		{
			// 配列の後ろの要素を生成し、int型データを詰めていく
			ArrayInt.push_back(i);
		}

		// 生成後の可変長配列の使い方は普通の配列と同じ
		// ArrayInt.size()で可変長配列の要素数がわかる
		for(unsigned int i = 0;i < ArrayInt.size();++i)
		{
			cout << ArrayInt[i] << endl;
		}

		cout << endl;

		// Myclass型の可変長配列の宣言
		vector<Myclass> ArrayMyclass;

		// 可変長配列にデータを代入
		for(unsigned int i = 0;i < 100;++i)
		{
			Myclass temp;
			temp.number = i * 2;
			// 配列の後ろの要素を生成し、Myclass型データを詰めていく
			ArrayMyclass.push_back(temp);		
		}

		for(unsigned int i = 0;i < 100;++i)
		{
			cout << ArrayMyclass[i].number << endl;		
		}


		return 0;
	}

なお、最初から配列の個数が分かっている場合は

       // int型の可変長配列の宣言時に100個用意する    
       vector<int> ArrayInt(100);
       ArrayInt[99] = 10;

もしくはresizeメソッドを使い

       // int型の可変長配列の宣言
       vector<int> ArrayInt;
       // resizeメソッドで100個確保
       ArrayInt.resize(100);
       ArrayInt[99] = 10;

のようにやればnew[]の配列みたいに動的確保することができます
さらにvectorは自動的に破棄してくれるので
delete[]をする必要性もありません

若干高等なお話:
pushbackで配列に詰める前に
要素数がある程度わかっている場合は
reserveメソッドで使う個数を予約するといいです(この時点ではまだ確保してない)
というのはvectorクラスはpushbackするたびにあらかじめ用意していたメモリ領域が足りなかった場合
内部的にnew,deleteを行っているからです
頻繁にnewするとメモリに隙間が空いてしまう(断片化してしまう)のでよろしくありません
(newでメモリを確保するという処理自体も遅い)
使う個数があらかじめ何個以上とわかっている場合は
内部的なメモリ確保の回数を減らすことが大事です

使い方は100個以上使うことがあらかじめわかっている場合
pushbackの前に

ArrayInt.reserve(100);

とします。

おまけ:
C++、C言語でforループなどで
配列の要素数を知りたいと思うことが多々あると思います
そんな場合は次のようなマクロ関数(簡易関数)を
プログラムの先頭に書くのをおススメします

#define LENGTH(p) (sizeof(p))/(sizeof(p[0]))    // 配列の要素数
#define LENGTHMEM(p) (_msize(p)) / (sizeof(p[0])) //◆動的配列の要素数

使い方はfor文などで

// 通常の配列
int a[10];
for(unsigned int i = 0;i < LENGTH(a);++i)
{
  // 処理
}
// 動的確保した配列
int *b = new int[20];
for(unsigned int i = 0;i < LENGTHMEM(b);++i)
{
  // 処理
}
delete[] b;

とすれば、マクロ関数で要素数がわかるようになっています
(注意:_msizeは動的なクラス配列には使えません)

さらに、おまけ
メモリリークの検出方法
知らない人は必須の情報
メモリリーク(動的確保して解放してないメモリ)を検出するには
まず、

#include <crtdbg.h>	// メモリリーク検出

をインクルードします、このヘッダー<crtdbg.h>は
デバックモードでしか使えません
デバックモードは実行ファイル出力のビルド形式をDebugの状態でF5キーを押します

#ifdef _DEBUG
	_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
#endif

ここで

#ifdef _DEBUG

#endif
で囲まれた部分は実行ファイル出力のビルド形式がDebug状態でのみビルドされる部分です
つまりReleaseモードなどでは素通りします

DebugモードでF5キーで実行し終了すると
解放されてないメモリのアドレスとバイト数とバイト情報が出力されます
追記:vectorクラスに代表されるSTLはDebugモードだとエラーチェックが入っているので非常に重いです、
   完成版を実行ファイルとして出すときは必ずReleaseモードにしてビルドしてください

選択肢 投票
(^ω^)わかったお 64  
普通 4  
。(`ω´#)。わかりにくぃぜぇえええええ 1  

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-10-02 (月) 16:45:07