メモリの操作

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

これは多くのプログラミング言語で共通ですが、C言語では任意のサイズのメモリをプログラマ自身が確保/解放する処理を書くことができます。

メモリの操作はやや難しく、扱いを間違えるとバグの原因にもなるので無理に使用することはありません。
しかし効果的に使えばより効率の良いプログラムを書くことができます。

メモリの動的確保

大きなデータを扱いたい場合、C言語では配列や構造体といった方法が提供されています。
しかし配列も構造体も、プログラミング時に定義した通りのサイズしか確保することができません。

プログラムが実際に実行されるまでサイズが分からないもの、例えばユーザーからのキー入力などに対応するには、原始的にはあらかじめ大きめの配列を用意する方法があります。
短いデータしか必要としない場合ならばこの方法でも大した問題にはなりません。

しかし、例えばテキストエディタを作る場合を考えてみます。
あらかじめ「char s[1000]」と決め打ちしてしまえば1000文字までしか扱えないテキストエディタとなってしまいます。
だからと言ってあまりに大きなサイズを確保すると、数十文字程度しか扱わないときに無駄が多くなりすぎます。

このような場合は、プログラムの実行時に必要なサイズのメモリを確保し、そこにデータを流し込む方法が必要になります。
これをメモリの動的確保(メモリの動的割り当て)といいます。

malloc関数

malloc関数は、指定したサイズのメモリを動的に確保する関数です。
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);

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を返すので、実行後に受け取ったポインタ変数を必ずチェックしましょう。

free関数

malloc関数で確保したメモリを解放するにはfree関数を使用します。
メモリの解放を忘れると、メモリがいつまでも解放されないままになり、メモリ不足(メモリリーク)の原因になります。

free関数の使い方は単純で、mallocで取得したポインタ変数を引数に指定するだけです。
malloc関数とfree関数はセットで使用する、と覚えておきましょう。

ただし、malloc関数が失敗した場合はメモリの確保は行われていませんから、free関数は必要はありません。

free関数が使えるのはmalloc関数(などのC言語が用意しているメモリ確保関数)でメモリを確保した場合のみです。
他の方法で確保されたメモリを解放しようとしてはいけません。
例えばOSが提供する機能(API)にもメモリを確保するものがありますが、これにはfree関数は使えません。
それ専用の解放関数が提供されているはずなので、それを使用しましょう。

また、一度解放したポインタを再度解放してはいけません。
解放した後のメモリ領域はどのような使われ方をしているかはプログラマは知ることができず、別のデータがすでに書き込まれているかもしれません。
二重解放はバグの原因になります。

freeで解放した後のポインタ変数は使用しないようにしましょう。
ただし、同じポインタ変数に再度別の(適切に管理されている)ポインタを割り当てて使うことはできます。

プログラムの終了時にはmalloc関数で確保したメモリ領域は自動的に解放されるので、サンプルコードのような単純な処理ではfree関数を忘れても大きな影響はありません。
(むしろfree関数を書かない主義の人もいます)
メモリの確保/解放を何度も繰り返すようなプログラムでは適切に解放する必要があります。

calloc関数

calloc関数は、確保したメモリ領域を自動的に0で埋めてしまいます。

#include <stdio.h>
#include <stdlib.h>
#include <memory.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」が適切な初期値であるかどうかはデータ型次第です。
例えば浮動小数点(doubuleなどの小数を扱うデータ型)は環境によっては「全ビット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関数と同じです。

realloc関数を使用する場合、戻り値はmalloc関数で受け取ったポインタ変数とは別のポインタ変数に受け取ります。
同じポインタ変数で受け取ってしまうと、エラー発生時にポインタ変数の値はNULLになってしまいます。
そうするとmallocで確保したメモリをfree関数で解放が出来なくなるためです。

メモリ確保が成功した後は、最初のポインタ変数に新たに確保したポインタを代入します。
これは必須ではありませんが、操作するポインタ変数が複数になるとミスにつながるかもしれないので、常に同じポインタ変数で管理するようにしておきます。
一時的に使用したポインタ変数(p_intTmp)にはNULLを代入しておき、このポインタ変数からは操作できないようにしておきます。

メモリ上のデータ書き換え

memset関数

malloc関数で確保したメモリ領域は、中にどのような値が入っているかは不定です。
すぐに何か値を代入してしまうならばそのままでも構いませんが、何らかの値で明示的に初期化したい場合はmemset関数を使用します。

#include <stdio.h>
#include <stdlib.h>

//memset関数を使用する場合、どちらか一方をinclude
#include <string.h>
#include <memory.h>

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>#include <memory.h>をコード先頭に記述します。

memsetは「指定範囲のメモリの全てのバイトを同じ値に揃える」関数です。
第二引数はint型ですが、実際にはunsigned char型(1バイト)に変換され、すべてのバイトを同じ値で埋めます。
そのため、対象がchar型などの1バイト型配列ならば配列の初期化にも使えなくはないですが、2バイト以上のデータ型では上手くいきません。

memcpy

確保したメモリ領域に特定のデータをコピーするにはmemcpy関数を使用します。

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

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。

第一引数がコピー先、第二引数がコピー元です。
第三引数は第一引数のメモリサイズ以上を指定しないように注意してください。

サイズさえ足りるならばどのようなデータでもコピーできます。

memcpyは「指定の場所に、指定のバイト分のデータをコピーする」という処理に過ぎません。
malloc関数とは関係なく使用することもできますが、コピー元とコピー先のデータ型が異なってもエラーチェックなどは行いません。

この理由から、構造体のコピーも可能ですが、これは推奨されません。
誤った構造体をコピーしてしまうとメモリ領域が破壊されるおそれがあるためです。
構造体のコピーは単純な代入が推奨されます。
誤った構造体を代入しようとするとコンパイル時エラーになるので、事前に間違いに気づくことができます。

確保したメモリを持ち回る

malloc関数で確保したメモリ領域は、free関数で解放します。
free関数を呼ぶまでは解放されることはありません。
(プログラムを終了すれば解放されます)

勝手に解放されないということは、アドレスさえわかればどこからでも利用可能なメモリ領域を作ることができます。

#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

int *MallocInt(int size)
{
	int *p;
	size_t s = sizeof(int) * 100;
	if (!(p = (int*)malloc(s)))
	{
		printf("メモリの確保に失敗しました");
		return NULL;
	}
	memset(p, 0, s);
	return p;
}

void MyFree(void *p)
{
	free(p);
}

void Print(int *p, int size)
{
	for (int i = 0; i < size; i++)
		printf("%d\n", p[i]);
}

int main()
{
	const int size = 100;
	int *p;

	p = MallocInt(size);
	if (!p)
	{
		getchar();
		return 0;
	}

	for (int i = 0; i < size; i++)
		p[i] = i;

	Print(p, size);

	MyFree(p);
	//これ以降ポインタpは使えない

	getchar();
}

int型のメモリ領域を確保する専用の関数MallocIntを定義しています。

通常であれば、関数を抜けるとローカル変数は破棄されてしまうので、関数内で配列を作ってそれを返す、というような処理は書けません。
(ポインタを返すようにしても、配列自体が破棄されるのでできない)
malloc関数で確保したメモリは自分で破棄するまではメモリ上に残るので、確保したメモリのポインタを返すようにすれば、大きなデータを返す関数を自作することができます。
しかもポインタを返すので処理が速いです。

malloc関数でメモリを確保した場合、メモリの管理はプログラマの責任となりますが、その分自由度の高いプログラムを作ることができます。

どこからでもメモリを操作できるという説明のためにMyFreeという関数も作っていますが、ただfree関数を呼んでいるだけなので、そのままmain関数でfreeしても構いません。

ファイルをすべてメモリに読み込むサンプル

最後に、ファイルをすべてメモリに読み込んでから表示するサンプルコードを示します。

ただ表示するだけなら適当なサイズの配列を用意して、ループ文で読み込み→表示→再度読み込み、を繰り返すだけで構いません。
メモリにすべて読み込むと、ファイルをクローズした後でも任意の場所のデータをいじくることができます。

※大きなサイズのファイルを読み込むと表示に時間がかかるので注意してください。

#include <stdio.h>
#include <stdlib.h>
#include <memory.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);
	}

	size = GetFileSize(file);
	if (size < 0)
	{
		Quit("ファイルサイズを取得できませんでした。\n");
	}

	p = MallocChar(size);
	if (!p)
	{
		Quit("メモリの確保に失敗しました。\n");
	}

	fread(p, size, 1, fp);

	fclose(fp);

	//ファイルを閉じた後でも操作可能
	Print(p, size);
	free(p);

	getchar();
}