メモリの操作
変数を使用すると、自動的にメモリ上に必要なサイズの領域が確保されます。
変数の寿命が尽きると、メモリは自動的に解放されます。
これは多くのプログラミング言語で共通ですが、C言語では任意のサイズのメモリをプログラマ自身が確保/解放する処理を書くことができます。
メモリの操作はやや難しく、扱いを間違えるとバグの原因にもなるので無理に使用することはありません。
しかし効果的に使えばより効率の良いプログラムを書くことができます。
メモリの動的確保
大きなデータを扱いたい場合、C言語では配列や構造体といった方法が提供されています。
しかし配列も構造体も、プログラミング時に定義した通りのサイズしか確保することができません。
プログラムが実際に実行されるまでサイズが分からないもの、例えばユーザーからのキー入力などに対応するには、原始的にはあらかじめ大きめの配列を用意する方法があります。
短いデータしか必要としない場合ならばこの方法でも大した問題にはなりません。
しかし、例えばテキストエディタを作る場合を考えてみます。
あらかじめchar s[1000]
と決め打ちしてしまえば1000文字までしか扱えないテキストエディタとなってしまいます。
だからと言ってあまりに大きなサイズを確保すると、数十文字程度しか扱わないときに無駄が多くなりすぎます。
このような場合は、プログラムの実行時に必要なサイズのメモリを確保し、そこにデータを流し込む方法が必要になります。
これをメモリの動的確保(メモリの動的割り当て)といいます。
malloc関数
malloc
関数は、指定したサイズのメモリを動的に確保する関数です。
(memory allocation=メモリの割り当て、の略)
malloc
関数を使用するには<stdlib.h>
をインクルードします。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char *str;
int length = 100;
//参考:こういうのはできない
//(比較的新しいコンパイラではオプションにより可能)
//char str2[length];
//char型100個分のサイズのメモリを確保
str = (char*)malloc(sizeof(char) * length);
if (!str)
{
//メモリの確保に失敗した場合
printf("メモリの確保に失敗しました");
getchar();
return 0;
}
//確保したメモリにデータを入れる
strcpy_s(str, length, "ABCDEFG");
printf("%s", str);
//確保したメモリを解放する
free(str);
getchar();
}
-
void *malloc(
size_t size
); -
sizeバイトのメモリを確保し、その先頭のポインタを返す。
メモリの取得に失敗した場合はNULLを返す。
malloc
関数の戻り値はvoid型ではなく「void*」型です。
これはデータ型に依存しないポインタという意味になります。
(メモリ上の場所を指すだけで、データ型は不明)
実際に使用する場合は目的のデータ型にキャストします。
char *str;
str = (char*)malloc(sizeof(char) * 100);
引数は確保したいサイズのバイト数を指定します。
今回はsizeof
演算子を使用して、「char型を100個分」のメモリを確保しています。
char型は1バイトなので結局100バイトなわけですが、こういう書き方に慣れておくとデータ型が変わった時でも迷うことなく書くことができます。
また、環境によってデータ型のサイズが異なる場合でも必要な分のサイズのメモリが確保できます。
malloc
関数で得られたポインタは、そのアドレスから指定したサイズ分までは自由に使用して構いません。
サンプルコードではchar型ポインタ変数にstrcpy_s関数で文字列をコピーしていますが、配列のように使用することもできます。
int *p_int;
p_int = (int*)malloc(sizeof(int) * 6);
//int型6個分を自由に使える
p_int[0] = 25;
p_int[1] = 3;
p_int[2] = 19;
p_int[3] = -9;
p_int[4] = 14;
p_int[5] = 35;
//p_int[5] = 35;
//↓全く同じ
//*(p_int + 5) = 35;
malloc
関数を使用するとサイズが可変の配列を作ることができます。
配列に使用する添字演算子は、ポインタによる記述の簡略表現(糖衣構文、シンタックスシュガー)です。
(→ポインタの配列的な記述)
メモリの確保に失敗した場合
malloc
関数は常に成功するわけではなく、メモリが足りない場合などに指定容量が確保できずに失敗することがあります。
malloc
関数は失敗した時にNULL
を返すので、実行後に受け取ったポインタ変数は必ずチェックしましょう。
char *m = (char*)malloc(sizeof(char) * 10);
if (m == NULL) {
//メモリ確保失敗
}
//NULLを反転すると真になるので
//これでもチェック可能
if (!m) {
//メモリ確保失敗
}
free関数
malloc
関数で確保したメモリを解放するにはfree
関数を使用します。
メモリの解放を忘れると、メモリがいつまでも解放されないままになり、メモリ不足(メモリリーク)の原因になります。
free
関数の使い方は単純で、malloc
関数で取得したポインタ変数を引数に指定するだけです。
malloc
関数とfree
関数はセットで使用する、と覚えておきましょう。
ただし、malloc
関数が失敗した場合はメモリの確保は行われていませんから、free
関数は必要はありません。
free
関数が使えるのはmalloc
関数(などのC言語が用意しているメモリ確保関数)でメモリを確保した場合のみです。
他の方法で確保されたメモリを解放しようとしてはいけません。
例えばOSが提供する機能(API)にもメモリを確保するものがありますが、これにはfree
関数は使えません。
それ専用の解放関数が提供されているはずなので、そちらを使用しましょう。
二重解放の禁止
一度解放したポインタ(メモリ領域)は再度解放してはいけません。
解放した後のメモリ領域はどのような使われ方をしているかはプログラマは知ることができず、別のデータがすでに書き込まれているかもしれません。
二重解放はバグの原因になります。
free
関数で解放した後のポインタ変数は使用しないようにしましょう。
ただし、同じポインタ変数に再度別の(適切に管理されている)ポインタを割り当てて使うことはできます。
二重解放を防ぐために、free
関数を実行した直後にポインタ変数にNULLを代入しておくと良いです。
free
関数にNULLを指定しても何も処理は発生しないので安全です。
char *m = (char*)malloc(1);
//何か処理
free(m);
m = NULL;
//NULLポインタの解放処理は安全
free(m);
プログラムの終了時にはmalloc
関数で確保したメモリ領域は自動的に解放されるので、サンプルコードのような単純な処理ではfree
関数を忘れても大きな影響はありません。
(むしろfree
関数を書かない主義の人もいます)
メモリの確保/解放を何度も繰り返すようなプログラムでは適切に解放する必要があります。
calloc関数
calloc
関数は、メモリを確保し、確保したメモリ領域をすべて0で埋めます。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p_int;
//100個のint型のメモリを確保
p_int = (int*)calloc(100, sizeof(int));
if (!p_int)
{
printf("メモリの確保に失敗しました");
getchar();
return 0;
}
//すべて0が表示される
printf("%d\n", p_int[0]);
printf("%d\n", p_int[1]);
printf("%d\n", p_int[2]);
free(p_int);
getchar();
}
-
void *calloc(
size_t num,
size_t size
); -
大きさnumのデータをsize個分のメモリを確保し、その先頭のポインタを返す。
確保した領域はすべて0で埋められる。
メモリの取得に失敗した場合はNULLを返す。
calloc
関数の引数はmalloc
関数の引数を二つに分割したような書き方をします。
malloc
関数はメモリ領域を確保するだけで、確保した領域に何が入っているかは不定です。
(初期化していない変数のようなものです)
calloc
関数は確保した領域すべてに0をセットします。
その分malloc
関数よりも若干パフォーマンスが落ちます。
また、あくまでも「すべてのビットを0で埋める」という処理に過ぎません。
「全ビット0」が適切な初期値であるかどうかはデータ型次第です。
例えば浮動小数点(doubleなどの小数を扱うデータ型)は環境によっては「全ビット0」が数値としての0と等価ではないかもしれません。
realloc関数
一度確保した領域を拡大/縮小したい場合はrealloc
関数を使用します。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p_int;
int *p_intTmp;
p_int = (int*)malloc(sizeof(int) * 100);
if (!p_int)
{
printf("mallocに失敗しました");
getchar();
return 0;
}
p_int[0] = 1;
p_int[1] = 2;
p_int[2] = 3;
//100→200に拡大
//mallocとは別のポインタで戻り値を受ける
p_intTmp = (int*)realloc(p_int, sizeof(int) * 200);
if (!p_intTmp)
{
//malloc時のメモリを解放
free(p_int);
printf("reallocに失敗しました");
getchar();
return 0;
}
//元のポインタに戻す
p_int = p_intTmp;
p_intTmp = NULL;
//malloc時に書きこんだ値を表示
printf("%d\n", p_int[0]);
printf("%d\n", p_int[1]);
printf("%d\n", p_int[2]);
//200個分使用できるかチェック
p_int[199] = 200;
printf("%d\n", p_int[199]);
free(p_int);
getchar();
}
1 2 3 200
-
void *realloc(
void *memblock,
size_t size
); -
ポインタmemblockの領域の大きさをsizeに変更する。
データは維持されるが、元のサイズより小さく変更した場合は切捨てが発生する。
戻り値は確保した領域の先頭へのポインタ。
メモリの取得に失敗した場合はNULLを返す。
第一引数にサイズを変更したいポインタを指定する以外はmalloc
関数と同じです。
ちなみに第一引数にNULLを指定した場合はmalloc
関数と同じ動作をします。
realloc
関数が失敗した場合、元の(サイズ変更前の)メモリ領域のデータはそのまま残ります。
realloc
関数の実行結果を元のポインタ変数に代入すると、関数失敗時にそのポインタ変数にNULLが代入されることになってしまい、元のデータにアクセスする(メモリを解放する)手段がなくなってしまいます。
これを防ぐために、realloc
関数の実行結果は元のポインタ変数とは別のポインタ変数に受け取るようにします。
メモリ確保が成功した後は、元のポインタ変数に新たに確保したポインタを代入します。
これは必須ではありませんが、操作するポインタ変数が複数になるとミスにつながるかもしれないので、常に同じポインタ変数で管理するようにしておきます。
一時的に使用したポインタ変数(p_intTmp
)にはNULLを代入しておき、このポインタ変数からは操作できないようにしておきます。
サイズ変更の結果として元と同じアドレスが返される可能性もあります。
別のメモリ領域が確保された場合、元のメモリ領域は自動で解放されます。
(これをfree
関数で解放すると二重解放になってしまいます)
メモリ上のデータ書き換え
memset関数
malloc
関数で確保したメモリ領域は、中にどのような値が入っているかは不定です。
すぐに何か値を代入してしまうならばそのままでも構いませんが、何らかの値で明示的に初期化したい場合はmemset
関数を使用します。
#include <stdio.h>
#include <stdlib.h>
#include <string.h> //memsetに必要
int main()
{
int *p_int;
//こうすると一行で書ける
if (!(p_int = (int*)malloc(sizeof(int) * 100)))
{
printf("メモリの確保に失敗しました");
getchar();
return 0;
}
//初期化していないので
//不定値が表示される
printf("%d\n", p_int[0]);
printf("%d\n", p_int[1]);
printf("%d\n", p_int[2]);
//確保したメモリをすべて'\0'で埋める
memset(p_int, 0, sizeof(int) * 100);
printf("\n");
//すべて0が表示される
printf("%d\n", p_int[0]);
printf("%d\n", p_int[1]);
printf("%d\n", p_int[2]);
free(p_int);
getchar();
}
-
void *memset(
void *dest,
int c,
size_t count
); -
ポインタdestが指すアドレスにデータcをcountバイト分セットする。
戻り値はdest。
第一引数はmalloc
関数で取得したポインタを指定します。
第二引数は埋めたいデータを指定します。
第三引数は書き込み先のデータのサイズを指定します。
確保した領域全てを埋める場合はmalloc
関数の第一引数と同じものをそのまま指定します。
memset
関数を使用するには#include <string.h>
をコード先頭に記述します。
memset
関数は「指定範囲のメモリの全てのバイトを同じ値に揃える」関数です。
第二引数はint型ですが、実際にはunsigned char型(1バイト)に変換され、すべてのバイトを同じ値で埋めます。
そのため、対象がchar型などの1バイト型配列ならば配列の初期化にも使えなくはないですが、2バイト以上のデータ型では上手くいきません。
(ただし0での初期化なら大体は上手くいきます)
memcpy関数
確保したメモリ領域に特定のデータをコピーするにはmemcpy
関数を使用します。
#include <stdio.h>
#include <stdlib.h>
#include <string.h> //memcpyに必要
int main()
{
int nums[100];
for (int i = 0; i < 100; i++)
{
nums[i] = i;
}
int *p_int;
p_int = (int*)malloc(sizeof(int) * 100);
if (!p_int)
{
printf("メモリの確保に失敗しました");
getchar();
return 0;
}
//確保したメモリにデータをコピー
memcpy(p_int, nums, sizeof(nums));
for (int i = 0; i < 100; i++)
{
printf("%d\n", p_int[i]);
}
free(p_int);
getchar();
}
-
void *memcpy(
void *dest,
const void *src,
size_t count
); -
ポインタdestが指すアドレスにポインタsrcが指すデータをcountバイト分コピーする。
戻り値はdest。
第一引数がコピー先、第二引数がコピー元です。
第三引数は第一引数のメモリサイズ以上を指定しないように注意してください。
これはstrcpy
関数(文字列のコピー)とほぼ同じ動作をします。
strcpy
関数はNULL文字があるとそこで処理を終了させますが、memcpy
関数は処理を終わらせずに指定のバイト数分のコピーを行います。
サイズさえ足りるならばどのようなデータでもコピーできます。
memcpy
は「指定の場所に、指定のバイト分のデータをコピーする」という処理に過ぎません。
malloc
関数とは関係なく使用することもできますが、コピー元とコピー先のデータ型が異なってもエラーチェックなどは行いません。
この理由から、構造体のコピーも可能ですが、これは推奨されません。
誤った構造体をコピーしてしまうとデータが破壊されるおそれがあるためです。
構造体のコピーは単純な代入が推奨されます。
誤った構造体を代入しようとするとコンパイル時エラーになるので、事前に間違いに気づくことができます。
memmove関数
memcpy
関数に似たものにmemmove
関数があります。
使い方も全く同じです。
-
void *memmove(
void *dest,
const void *src,
size_t count
); -
ポインタdestが指すアドレスにポインタsrcが指すデータをcountバイト分コピーする。
戻り値はdest。
memcpy
関数は、コピー先とコピー元の領域が重なり合う場合の動作は未定義で、どのような動作をするかの保証はありません。
memmove
関数はそのような場合でも問題ないことが保証されています。
#include <stdio.h>
#include <stdlib.h>
#include <string.h> //memmoveに必要
#define BUFFER 10
int main()
{
char str[] = "ABCDEFGHIJ";
printf("%s\n", str);
//未定義動作
//memcpy(str, str + 1, strlen(str) - 1);
//安全
memmove(str, str + 1, strlen(str) - 1);
printf("%s\n", str);
getchar();
}
ABCDEFGHIJ BCDEFGHIJJ
文字列配列の中身を一つ手前にずらす処理をしています。
このような場合はコピー先とコピー元のメモリ領域が重複しているため、memcpy
関数は危険なのでmemmove
関数を使用します。
別々にmalloc
関数で取得した領域や、ある配列から別の配列へのコピーなどはメモリ領域が重なっていることはありませんから、memcpy
関数で問題ありません。
memcpy
関数のほうが速度的に優れている可能性があります。
メモリの検索
あるメモリ領域から文字(1バイトデータ)を検索するにはmemchr
関数を使用します。
詳しくは文字列の検索の項で説明しています。
メモリ上のデータの比較
メモリ上にあるデータの比較を行うにはmemcmp
関数を使用します。
これはstrncmp関数とほぼ同じ働きをします。
違いは、memcmp
関数はNULL文字が現れても処理を終了させない点です。
-
int memcmp(
const void* s1,
const void* s2,
size_t size
); -
s1とs2をsizeバイト分比較し、同じならば0を返す。
s1のほうが小さければ-1以下を返す。
s1のほうが大きければ1以上を返す。
#include <stdio.h>
#include <stdlib.h>
//バイト列を表示する
void PrintByte(const void* buf, size_t size)
{
unsigned char* pBuf = (unsigned char*)buf;
for (size_t i = 0; i < size; i++) {
printf("%02X ", pBuf[i]);
}
}
int main()
{
const char s1[] = "ABC\0DEF";
const char s2[] = "ABC\0ABC";
size_t size = sizeof(s1);
PrintByte(s1, size);
printf("\n");
PrintByte(s2, size);
printf("\n");
printf("strncmp: %d\n", strncmp(s1, s2, size));
printf("memcmp : %d\n", memcmp(s1, s2, size));
getchar();
}
41 42 43 00 44 45 46 00 41 42 43 00 41 42 43 00 strncmp: 0 memcmp : 1
途中にNULL文字(0)を含むデータであっても、memcmp
関数は正しく比較ができています。
確保したメモリを持ち回る
malloc
関数で確保したメモリ領域は、free
関数で解放します。
free
関数を呼ぶまでは解放されることはありません。
(プログラムを終了すれば解放されます)
勝手に解放されないということは、アドレスさえわかればどこからでも利用可能なメモリ領域を作ることができます。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//任意のサイズのint型配列を作る
int* CreateArrayInt(size_t size)
{
int* p;
size_t s = sizeof(int) * size;
if (!(p = (int*)malloc(s)))
{
printf("メモリの確保に失敗しました");
return NULL;
}
memset(p, 0, s);
return p;
}
//int型配列の中身を全て表示
void ShowArrayInt(int* arr, size_t size)
{
for (size_t i = 0; i < size; ++i)
printf("%d\n", arr[i]);
}
//メモリを解放
void DeleteArray(void* p)
{
free(p);
}
int main()
{
size_t size = 100;
int* arr;
arr = CreateArrayInt(size);
if (!arr)
{
getchar();
return 0;
}
for (size_t i = 0; i < size; i++)
arr[i] = i;
ShowArrayInt(arr, size);
DeleteArray(arr);
//これ以降ポインタarrは使えない
arr = NULL;
getchar();
}
int型のメモリ領域を確保する専用の関数CreateArrayInt
を定義しています。
通常であれば、関数を抜けるとローカル変数は破棄されてしまうので、関数内で配列を作ってそれを返す、というような処理は書けません。
(ポインタを返すようにしても、配列自体が破棄されるのでできない)
malloc
関数で確保したメモリは自分で破棄するまではメモリ上に残るので、確保したメモリのポインタを返すようにすれば、任意のサイズのデータを返す関数を自作することができます。
しかもポインタを返すので処理が速いです。
malloc
関数でメモリを確保した場合、メモリの管理はプログラマの責任となりますが、その分自由度の高いプログラムを作ることができます。
どこからでもメモリを操作できるという説明のためにDeleteArray
という関数も作っていますが、ただfree
関数を呼んでいるだけなので、そのままmain関数でfree
しても構いません。
ファイルをすべてメモリに読み込むサンプル
最後に、ファイルをすべてメモリに読み込んでから表示するサンプルコードを示します。
ただ表示するだけなら適当なサイズの配列を用意して、ループ文で読み込み→表示→再度読み込み、を繰り返すだけで可能です。
しかしメモリ上には残らないので、後からデータに手を付けることはできません。
メモリにすべて読み込むと、ファイルをクローズした後でも任意の場所のデータをいじくることができます。
※大きなサイズのファイルを読み込むと表示に時間がかかるので注意してください。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
//char型でメモリ確保
char* MallocChar(long size)
{
char* p;
if (!(p = (char*)malloc(size)))
return NULL;
memset(p, 0, size);
return p;
}
//メモリ内の文字を表示
void Print(const char* p, long size)
{
for (int i = 0; i < size; i++)
printf("%c", *p++);
}
//ファイルサイズ取得
long GetFileSize(const char* file)
{
struct stat statBuf;
if (stat(file, &statBuf) == 0)
return statBuf.st_size;
return -1L;
}
//メッセージを表示してプログラム終了
void Quit(char* message)
{
printf(message);
getchar();
exit(0);
}
int main()
{
const char* file = "test.txt";
FILE* fp;
long size;
char* p;
//fp = fopen(file, "rb");
fopen_s(&fp, file, "rb");
if (fp == NULL)
{
char message[300];
snprintf(message, 300, "%sのオープンに失敗しました。\n", file);
Quit(message);
return;
}
size = GetFileSize(file);
if (size < 0)
{
Quit("ファイルサイズを取得できませんでした。\n");
return;
}
p = MallocChar(size);
if (!p)
{
Quit("メモリの確保に失敗しました。\n");
return;
}
fread(p, size, 1, fp);
fclose(fp);
//ファイルを閉じた後でも操作可能
Print(p, size);
free(p);
getchar();
}