メモリの操作

変数を使用すると、自動的にメモリ上に必要なサイズの領域が確保されます。
変数の寿命が尽きると、メモリは自動的に解放されます。

これは多くのプログラミング言語で共通ですが、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();
}