目次
0 はじめに
実際の開発 (C 言語) では、配列の要素はメモリ内の連続した場所に格納されます。ただし、配列を使用してデータを格納する場合、プログラムを実行する前にそのサイズを知る必要があるという欠点があり、実際の開発では、適用する必要があるメモリを常に正確に把握できるとは限りません。他の手段を講じれば開発が可能になります。職員は悲惨な状況にあります。
C 言語の兄貴分である C++ は、明らかに落ち着いてリラックスでき、多くの複雑なシナリオに直面した場合でも、多くの場合、コンテナーを使用して落ち着いて簡単に対処できます。詳細については、リンクを参照してください: C++ Common Containers Catch All
このような問題に対する C 言語独自の対応があり、この行き詰まりを打開するために、今日の主人公が優しく軽やかにやってくるのが、動的メモリ割り当てです(C++ も同様)。
この記事の内容の概要:
1 動的メモリ割り当てを使用する理由
上で述べたように、データを保存するためにどのくらいのメモリを申請する必要があるかわからないことがよくありますが、大きすぎる場合はスペースを無駄にし、小さすぎる場合は十分ではありません。
2 malloc と無料
Malloc と free は兄弟であり、前者はメモリの適用を担当し、後者はメモリの解放を担当します。明確な分業、シンプルかつ効率的。これら 2 つの関数のプロトタイプは次のとおりです。
void *malloc(size_t size);
void free(void *pointer);
Malloc は連続したメモリを割り当てます。同時に、実際に割り当てられるメモリは申請した量よりわずかに多くなる可能性があり、具体的なサイズはコンパイラによって異なります。
メモリ プールが空の場合、または利用可能なメモリが要求を満たすことができない場合、malloc
関数はオペレーティング システムに追加のメモリを要求し、この新しいメモリに対して割り当てタスクを実行します。オペレーティング システムがmalloc
これ以上のメモリを提供できない場合は、NULL
ポインタが返されます。
free
引数 への値は、または から以前に返された値NULL
である必要があります。1 を渡しても効果はありません。malloc
calloc
realloc
free
NULL
具体的なケースについては後続のコンテンツで説明します。
3 コールロックとリアロック
calloc
2 つのメモリ割り当て関数ともありますrealloc
。彼らのプロトタイプは次のようになります。
void *calloc(size_t num_elements, size_t element_size);
void *realloc(void *ptr, size_t new_size);
calloc
両者には次の 2 つの違いがありますmalloc
。
- 形式的には、
malloc
渡されるのは合計バイト数であり、calloc
渡されるのは要素の数と各要素が占めるバイト数です。 - 機能的な観点から見ると、
malloc
メモリ空間の適用のみを担当し、calloc
メモリ空間の適用だけでなく初期化も行います0
。
これは、 calloc = clear + mallocという名前からもわかります。これは、メモリのクリアと適用を意味します。
realloc はメモリの一部を変更/再適用します。
- p が指すスペースの後に追加するのに十分なスペースがある場合は、直接追加され、
p
元の開始アドレスが返されます。 - p が指すスペースの後に追加する十分なスペースがない
realloc
場合、関数は新しいメモリ領域を見つけ、new_size
動的メモリ領域を 1 つずつ再度開き、元のメモリ領域のデータをコピーして古いメモリ領域を解放します。それをオペレーティング システムに返し、最後に新しく開かれたメモリ空間の開始アドレスを返します。
最初のケースを次の図に示します。
2 番目のケースを次の図に示します。
これは、名前realloc = re + mallocからもわかります。これは、メモリを再適用することを意味します。
4 動的に割り当てられたメモリの使用
本書には次のような例があります。
int *pi;
pi = malloc(100);
if (pi == NULL)
{
printf("Out of memory!\n");
exit(1);
}
この例はわかりやすいです。100バイトのメモリを割り当てます。割り当てに失敗した場合は、エラーを出力して、現在実行中のプログラムを終了します。
もちろん、次のように簡単なプログラムを自分で書くこともできます。
#include<stdio.h>
#include<stdlib.h>
//定义返回值类型
typedef enum res
{
FASLE,
TRUE
}res_bool;
//分配内存并初始化、打印输出
res_bool fun_malloc(int const size)
{
int *p;
p = malloc(sizeof(int) * 25);
if(p == NULL)
return FASLE;
else
{
for (int i = 0; i < size; i++)
p[i] = i;
}
for (int i = 0; i < size; i++)
printf("%d\t", p[i]);
free(p);
p = NULL;
return TRUE;
}
int main()
{
if (fun_malloc(25) == TRUE)
{
printf("内存分配成功!");
}
else
{
printf("内存分配失败!");
}
system("pause");
return 0;
}
これは比較的完全なケースで、メモリの割り当て、初期化、メモリ割り当てが成功したかどうかの検証を行います。
5 つの一般的な動的メモリ エラー
動的メモリ エラーには、一般的に次の 2 つのタイプがあります。
- 1 つは、メモリ適用が成功したかどうかを判断せずにそのまま使用すると、予期せぬ問題が発生する可能性があります。
- 1 つは、割り当てられたメモリの境界を超える操作が行われ、予期しない問題が発生する可能性があることです。
メモリ エラーの具体的なケースを記述するのは困難ですが、日常のプログラミングで注意するだけで十分です。
6 メモリ割り当ての例
6.1 整数値のリストのソート
ソート アルゴリズムは、エンジニアリング開発において最も一般的で古典的なアルゴリズムです。一般的なソート アルゴリズムは 10 個あります。興味がある場合は、「トップ 10 の古典的なソート アルゴリズム (C 言語で実装)」を参照してください。以下の例は、書籍に記載されてい
ます
。 . 使い方 ソート用のライブラリ関数でqsort
、最下層はクイックソートのアルゴリズムを使っているそうです。
#include <stdlib.h>
#include <stdio.h>
//该函数由qsort调用,用于比较整型值
int compare_integers(void const *a, void const *b)
{
register int const *pa = a;
register int const *pb = b;
return *pa > *pb ? 1 : *pa < *pb ? -1 : 0;
}
//主函数
int main()
{
int *array;
int n_values;
int i;
//观察共有多少个值
printf("How many values are there?");
if (scanf_s("%d", &n_values) != 1 || n_values <= 0)
{
printf("Illegal number of values.\n");
exit(EXIT_FAILURE);
}
//分配内存,用于存储这些值
array = malloc(n_values * sizeof(int));
if (array == NULL)
{
printf("Can't get memory for that many values.\n");
exit(EXIT_FAILURE);
}
//读取这些值
for (i = 0; i < n_values; i += 1)
{
printf("?");
if (scanf_s("%d", array + i) != 1)
{
printf("error.\n");
free(array);
exit(EXIT_FAILURE);
}
}
//对这些值排序
qsort(array, n_values, sizeof(int), compare_integers);
//打印这些值
for (i = 0; i < n_values; i += 1)
printf("%d\n",array[i]);
//释放内存并推出
free(array);
system("pause");
return EXIT_SUCCESS;
}
実行して出力を印刷:
基本的には難しいことはありません。唯一の難点は、compare_integers
関数の戻り値がネストされた条件式を使用していることです。条件式は条件文の簡略化されたバージョンです (すべてのケースを「簡略化」できるわけではありません)。条件式については、「C とポインタ」の読書メモ (第 5 章 演算子と式) のセクション 2.1.8を参照してください。
6.2 文字列のコピー
文字列のコピーに使用できる既製のライブラリ関数もありますが、この本の例では新しい文字列用のスペースが開くだけで、それ以上は何もありません (わずかに変更されています)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *my_strdup(char const *string)
{
char *new_string;
new_string = (char *)malloc(strlen(string) + 1);
if (new_string != NULL)
strcpy(new_string, string);
return new_string;
}
int main()
{
char *new_p;
char base_char[] = "Hello World!";
//复制字符串
new_p = my_strdup(base_char);
//检查是否顺利复制
if (new_p == NULL)
{
printf("error.\n");
free(new_p);
exit(EXIT_FAILURE);
}
//检查复制结果
for (int i = 0; i < (int)(strlen(base_char)); i++)
{
if (new_p[i] != base_char[i])
{
printf("new_p[%d] != base_char[%d]", i, i);
free(new_p);
exit(EXIT_FAILURE);
}
}
printf("success.\n");
free(new_p);
return 0;
}
実行して出力を印刷します。
文字列が正常にコピーされたことがわかります。この例から、開発における動的メモリ割り当ての利便性もわかります。
6.3 バリアントレコードの作成と破棄
最後の例では、動的メモリ割り当てを使用して、バリアント レコードの使用によって生じる無駄なメモリ領域を排除する方法を示します。プログラムでは構造体と共用体の知識が使用されますので、関連する知識を知りたい場合は「Cとポインタ」読書メモ(第10章 構造体と共用体)を参照してください。
まずヘッダー ファイルを作成して、使用する必要がある構造を定義します。
#pragma once
//包含零件专用信息的结构
typedef struct {
int cost;
int supplier;
}Partinfo;
//存储配件专用信息的结构
typedef struct {
int n_parts;
struct SUBASSYPART{
char partno[10];
short quan;
} *part;
}Subassyinfo;
//存货记录结构,一个变体记录
typedef struct {
char partno[10];
int quan;
enum {
PART, SUBASSY} type;
union {
Partinfo *part;
Subassyinfo *subassy;
}info;
}Invrec;
次に、バリアント レコードを作成するための関連手順を記述します。
#include <stdio.h>
#include <stdlib.h>
#include "inventor.h"
Invrec *creat_subassy_record(int n_parts)
{
Invrec *new_rec;
//试图为Inverc部分分配内存
new_rec = malloc(sizeof(Invrec));
if (new_rec != NULL)
{
//内存分配成功,现在存储SUBASSYPART部分
new_rec->info.subassy = malloc(sizeof(Subassyinfo));
if (new_rec->info.subassy != NULL)
{
//为零件获取一个足够大的数组
new_rec->info.subassy->part = malloc(n_parts * sizeof(struct SUBASSYPART));
if (new_rec->info.subassy->part != NULL)
{
//获取内存,填充我们已知道的字段,然后返回
new_rec->type = SUBASSY;
new_rec->info.subassy->n_parts = n_parts;
return new_rec;
}
//内存已用完,释放我们原先分配的内存
free(new_rec->info.subassy);
}
free(new_rec);
}
return NULL;
}
バリアント レコードの破棄に関連する手順もあります。
#include <stdlib.h>
#include "inventor.h"
void discard_inventory_record(Invrec *record)
{
//删除记录中的变体部分
switch (record->type)
{
case SUBASSY:
free(record->info.subassy->part);
free(record->info.subassy);
break;
case PART:
free(record->info.part);
break;
}
//删除记录的主体部分
free(record);
}
この例は、入れ子構造が含まれているため、比較的複雑です。層ごとにメモリを適用し、層ごとに解放する必要があります。
7 まとめ
この章の内容はそれほど多くありませんが、非常に実践的です。配列が宣言される場合、その長さはコンパイル時にわかっている必要があります。動的メモリ割り当てにより、プログラムは実行時にのみ長さがわかる配列にメモリ空間を割り当てることができます。