C言語を基礎から順に学びたい場合は、C言語 学習ロードマップ を参照してください。
前提として、構造体・typedef・enum・構造体配列を読んでいると、動的に構造体の配列を確保する例が理解しやすくなります。
配列を作るとき、これまでは次のように件数をコードへ直接書いていました。
int scores[5];
この書き方は、あらかじめ件数が分かっている場合に向いています。
一方で、実行してから入力された件数だけデータを扱いたい場合があります。
商品を何件登録するか、実行してから決める
↓
必要な件数だけメモリを用意する
↓
使い終わったら解放する
このようなときに使うのが、動的メモリ確保です。
この記事では、次の内容を扱います。
-
mallocでメモリを確保する -
callocでゼロのビット列で初期化されたメモリを確保する -
reallocで確保済みメモリのサイズを変更する -
freeで確保したメモリを解放する -
memsetが何をする関数かを理解する - メモリリーク、二重解放、解放後アクセスを避ける
動的メモリは便利ですが、使い方を誤ると不具合や異常終了につながります。
そのため、使い方だけでなく「してはいけないこと」も一緒に確認します。
前提
この記事は、次の内容を学んだ人を対象にしています。
- ポインタの基本
-
NULLの意味 - 配列、構造体、構造体の配列
sizeof- 関数と戻り値
先に、次の記事を読んでください。
構造体、typedef、enum、構造体の配列を学んでから進むと理解しやすくなります。
C言語でデータをまとめて扱う|構造体・typedef・enum・構造体配列
1. 動的メモリ確保とは
通常の配列は、プログラムを書く時点で件数を決めます。
int scores[5];
一方、動的メモリ確保では、プログラムの実行中に必要な件数を決められます。
プログラムを実行する
↓
「商品を何件登録しますか」と入力を受け取る
↓
入力された件数分のメモリを確保する
↓
商品データを使う
↓
使い終わったらメモリを解放する
動的に確保したメモリを扱う変数は、ポインタです。
int *scores = NULL;
この時点では、まだメモリを確保していません。
NULL は「どこも指していない」状態です。
2. malloc でメモリを確保する
malloc は、指定したサイズのメモリを確保する関数です。
使うには stdlib.h を読み込みます。
#include <stdlib.h>
たとえば、整数を5個入れられる領域を確保する場合は、次のように書きます。
int *scores = malloc(sizeof(int) * 5);
ただし、malloc は確保した領域に初期値を入れません。
確保直後の値を、そのまま使ってはいけません。
malloc で確保した直後
↓
中身は未初期化
↓
使う前に、必ず値を設定する
また、メモリの確保に失敗すると、malloc は NULL を返します。
そのため、確保直後に必ず確認します。
malloc の基本例
malloc_scores.c というファイルを作成してください。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
// 整数を3個扱うことにします。
int score_count = 3;
// 整数3個分のメモリを確保します。
// sizeof *scores は、scores が指す先の int 1個分のサイズです。
int *scores = malloc(sizeof *scores * score_count);
// 確保に失敗した場合は、以降の処理を行いません。
if (scores == NULL)
{
printf("メモリを確保できませんでした。\n");
return 1;
}
// malloc で確保した直後の値は使わず、先に値を設定します。
scores[0] = 72;
scores[1] = 88;
scores[2] = 65;
// 設定した値を表示します。
for (int index = 0; index < score_count; index++)
{
printf("%d人目: %d点\n", index + 1, scores[index]);
}
// malloc で確保したメモリを解放します。
free(scores);
// 解放済みであることを明確にします。
scores = NULL;
return 0;
}
コンパイルして実行します。
gcc -std=c17 -Wall -Wextra -Wpedantic malloc_scores.c -o malloc_scores
./malloc_scores
sizeof *scores と書く理由
次の2つは、この例では同じ意味です。
malloc(sizeof(int) * score_count);
malloc(sizeof *scores * score_count);
sizeof *scores は、「scores が指している先の型1個分のサイズ」です。
ポインタの型を int * から別の型へ変えても、sizeof *scores ならサイズ指定を合わせやすくなります。
そのため、動的メモリ確保では次の形をよく使います。
Type *items = malloc(sizeof *items * item_count);
C言語では、malloc の戻り値をキャストしません。
// C言語では、このようなキャストは不要です。
int *scores = (int *)malloc(sizeof *scores * score_count);
キャストを付けると、#include <stdlib.h> の書き忘れによる警告を見えにくくすることがあります。
3. calloc でメモリを確保する
calloc は、指定した個数と1個分のサイズを受け取り、メモリを確保する関数です。
int *scores = calloc(score_count, sizeof *scores);
calloc は、確保した領域をすべてゼロのビット列で初期化します。
ただし、プログラムで使う値は、必要に応じて明示的に設定してください。
特にポインタを含む複雑な構造体では、「ゼロ初期化されているから、すべての意味で使える状態」と決めつけない方が安全です。
構造体の配列を calloc で確保する
calloc_products.c というファイルを作成してください。
#include <stdio.h>
#include <stdlib.h>
// 商品の販売状態を表す型です。
typedef enum
{
PRODUCT_STATUS_NOT_FOR_SALE = 0,
PRODUCT_STATUS_ON_SALE = 1
} ProductStatus;
// 商品1件を表す型です。
typedef struct
{
int id;
char name[32];
int unit_price;
int stock_count;
ProductStatus status;
} Product;
// 商品1件を表示する関数です。
void print_product(const Product *product)
{
if (product == NULL)
{
return;
}
printf("商品ID: %d\n", product->id);
printf("商品名: %s\n", product->name);
printf("単価: %d円\n", product->unit_price);
printf("在庫数: %d個\n", product->stock_count);
}
int main(void)
{
// 今回は商品を3件扱います。
size_t product_count = 3;
// Product を3件分、動的に確保します。
Product *products = calloc(product_count, sizeof *products);
// 確保に失敗した場合は、以降の処理を行いません。
if (products == NULL)
{
printf("メモリを確保できませんでした。\n");
return 1;
}
// 1件目の商品情報を設定します。
products[0] = (Product){
101,
"りんご",
120,
10,
PRODUCT_STATUS_ON_SALE
};
// 2件目の商品情報を設定します。
products[1] = (Product){
102,
"みかん",
100,
8,
PRODUCT_STATUS_ON_SALE
};
// 3件目の商品情報を設定します。
products[2] = (Product){
103,
"牛乳",
210,
5,
PRODUCT_STATUS_NOT_FOR_SALE
};
// すべての商品を表示します。
for (size_t index = 0; index < product_count; index++)
{
print_product(&products[index]);
printf("\n");
}
// 確保したメモリを解放します。
free(products);
products = NULL;
return 0;
}
4. realloc でメモリのサイズを変更する
realloc は、すでに確保しているメモリのサイズを変更する関数です。
たとえば、商品を3件分確保した後に、もう1件追加したい場合に使えます。
Product を3件分確保する
↓
4件目を追加したくなる
↓
realloc で4件分へ広げる
↓
4件目のデータを設定する
realloc で最も注意する点は、戻り値を元のポインタへ直接代入しないことです。
次の書き方は避けてください。
products = realloc(products, sizeof *products * new_product_count);
確保に失敗すると、realloc は NULL を返します。
このとき、元の products を上書きすると、以前に確保していた領域を解放できなくなる可能性があります。
一時的なポインタへ受け取ってから、成功時だけ元のポインタへ代入します。
realloc の安全な基本形
Product *resized_products = realloc(
products,
sizeof *products * new_product_count);
if (resized_products == NULL)
{
// products は、まだ元の確保済み領域を指しています。
free(products);
return 1;
}
products = resized_products;
商品を1件追加する例
realloc_product.c というファイルを作成してください。
#include <stdio.h>
#include <stdlib.h>
typedef struct
{
int id;
char name[32];
int unit_price;
} Product;
void print_product(const Product *product)
{
if (product == NULL)
{
return;
}
printf("商品ID: %d, 商品名: %s, 単価: %d円\n",
product->id,
product->name,
product->unit_price);
}
int main(void)
{
// 最初は2件分のメモリを確保します。
size_t product_count = 2;
Product *products = calloc(product_count, sizeof *products);
if (products == NULL)
{
printf("メモリを確保できませんでした。\n");
return 1;
}
products[0] = (Product){ 101, "りんご", 120 };
products[1] = (Product){ 102, "みかん", 100 };
// 3件目を追加するため、新しい件数を決めます。
size_t new_product_count = product_count + 1;
// 一時的なポインタで realloc の結果を受け取ります。
Product *resized_products = realloc(
products,
sizeof *products * new_product_count);
if (resized_products == NULL)
{
// この時点では products は元の領域を指しているため、解放できます。
free(products);
products = NULL;
printf("メモリを広げられませんでした。\n");
return 1;
}
// サイズ変更に成功したため、ポインタと件数を更新します。
products = resized_products;
product_count = new_product_count;
// 追加した3件目の商品を設定します。
products[2] = (Product){ 103, "牛乳", 210 };
for (size_t index = 0; index < product_count; index++)
{
print_product(&products[index]);
}
free(products);
products = NULL;
return 0;
}
5. free で必ずメモリを解放する
malloc、calloc、realloc で確保したメモリは、使い終わったら free で解放します。
free(products);
products = NULL;
free を忘れることを、メモリリークと呼びます。
メモリを確保する
↓
使い終わる
↓
free しない
↓
プログラムが動いている間、不要なメモリを使い続ける
短時間で終わる小さなプログラムでは目立たなくても、長時間動くプログラムや繰り返し処理では問題になります。
確保した場所と解放する場所を意識してください。
malloc / calloc / realloc で確保した
↓
最後に free する責任がある
解放後のポインタは使わない
次のコードは危険です。
free(products);
printf("%d\n", products[0].id);
free の後、その領域は自分のものではありません。
解放後にアクセスすることを、解放後アクセスと呼びます。
free の直後に NULL を代入すると、解放済みであることが分かりやすくなります。
free(products);
products = NULL;
NULL のポインタを free しても問題ありません。
free(products);
そのため、後片付けの処理を書きやすくなります。
同じ領域を2回 free しない
次のように、同じ領域を2回解放してはいけません。
free(products);
free(products);
これを二重解放と呼びます。
free の後に products = NULL; としておけば、2回目は free(NULL) になるため、二重解放を避けやすくなります。
free(products);
products = NULL;
free(products);
ただし、別のポインタが同じ領域を指している場合は、NULL を代入しただけでは防げません。
「どの処理がメモリを解放する責任を持つか」を決めることが重要です。
6. memset はメモリをバイト単位で埋める関数
memset は、指定したメモリ領域を1バイト単位で同じ値に埋める関数です。
使うには string.h を読み込みます。
#include <string.h>
文字列用の配列を空に戻す例です。
#include <stdio.h>
#include <string.h>
int main(void)
{
char message[32] = "前のメッセージ";
printf("変更前: %s\n", message);
// message の全領域を 0 で埋めます。
memset(message, 0, sizeof message);
// 先頭が文字列の終わりを表す 0 になったため、空文字列です。
printf("変更後: %s\n", message);
return 0;
}
memset の形は次のとおりです。
memset(対象の場所, 埋める値, 埋めるバイト数);
memset(message, 0, sizeof message);
これは、message の領域全体をゼロで埋めます。
memset を初期化の万能手段にしない
memset は便利ですが、構造体やポインタを「何となく全部ゼロにする」ための万能な初期化関数ではありません。
特に、初期値に意味があるデータでは、値を明示的に設定する方が読みやすく安全です。
Product product = {
101,
"りんご",
120,
10
};
動的に確保した構造体の配列も、必要な値を設定してから使います。
products[0].id = 101;
products[0].unit_price = 120;
memset は、文字列用バッファや通信データのバッファなど、バイト列を扱う場面で意図を理解して使ってください。
7. 動的メモリ管理で守ること
動的メモリ管理では、次の3点を必ず守ってください。
確保に失敗していないか NULL を確認する
↓
確保したメモリは、使い終わったら free する
↓
free した後は、そのポインタを使わない
よくある問題を整理します。
| 問題 | 何が起きるか | 防ぎ方 |
|---|---|---|
| 確保失敗を確認しない |
NULL を使って異常終了する可能性がある |
malloc などの直後に確認する |
free し忘れ |
メモリリークになる | 確保した処理と解放する処理を対応させる |
| 二重解放 | 異常終了やデータ破壊につながる | 解放責任を決め、解放後に NULL を入れる |
| 解放後アクセス | 自分のものではない領域を使ってしまう |
free 後はアクセスしない |
realloc を直接代入 |
失敗時に元の領域を見失う | 一時ポインタで戻り値を受け取る |
8. 確認課題
課題1:点数の件数を入力して確保する
キーボードから点数の件数を入力し、その件数分の int 型配列を calloc で確保してください。
確保に成功したら、すべての点数へ 0 を入れて表示してください。
答えを見る
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int score_count = 0;
printf("点数の件数を入力してください: ");
if (scanf("%d", &score_count) != 1 || score_count <= 0)
{
printf("1以上の整数を入力してください。\n");
return 1;
}
int *scores = calloc(score_count, sizeof *scores);
if (scores == NULL)
{
printf("メモリを確保できませんでした。\n");
return 1;
}
for (int index = 0; index < score_count; index++)
{
printf("%d人目: %d点\n", index + 1, scores[index]);
}
free(scores);
scores = NULL;
return 0;
}
課題2:realloc で点数を1件追加する
課題1で確保した配列を、realloc を使って1件増やしてください。
realloc の戻り値は、元のポインタへ直接代入せず、一時的なポインタで受け取ります。
答えの例を見る
int new_score_count = score_count + 1;
int *temporary_scores = realloc(scores, sizeof *scores * new_score_count);
if (temporary_scores == NULL)
{
printf("メモリを拡張できませんでした。\n");
free(scores);
scores = NULL;
return 1;
}
scores = temporary_scores;
scores[score_count] = 0;
score_count = new_score_count;
realloc に失敗した場合でも、元の scores は解放されません。
そのため、失敗時は元の scores を使って free できます。
まとめ
動的メモリ管理では、実行時に必要な件数だけメモリを用意できます。
| 関数 | 役割 |
|---|---|
malloc |
指定したサイズのメモリを確保する。中身は未初期化 |
calloc |
指定した個数・サイズのメモリを確保し、ゼロのビット列で初期化する |
realloc |
確保済みメモリのサイズを変更する |
free |
確保したメモリを解放する |
memset |
指定した領域を1バイト単位で同じ値に埋める |
最も重要なのは、次の流れです。
確保する
↓
NULL を確認する
↓
必要な値を設定して使う
↓
使い終わったら free する
↓
解放後は使わない
次は、構造体で扱ったデータをファイルへ保存し、ヘッダーファイルと複数の .c ファイルへ分ける方法を学びます。
次の記事
次は、プログラムのデータをファイルへ保存し、後で読み込む方法を学びます。
あわせて、.h ファイルと複数の .c ファイルへ処理を分ける基本も確認します。