これまでに、int float double などのメモリ開発のいくつかの方法や、さまざまな種類の配列を学習しました。これらによりメモリ領域が解放される可能性があります。しかし、開いたスペースはすべてデッドになっており、開いた後に自由に変更できないため、非常に不便です。今日は、記憶を開くための新しい方法、つまり動的記憶開発を学びます。
目次
3.4 free を使用して動的に割り当てられたメモリの一部を解放します
3.6 メモリを動的に開いて解放し忘れる (メモリ リーク)
1. 動的メモリ割り当てはなぜ存在するのでしょうか?
私たちが習得した記憶力開発方法は次のとおりです。
int val = 20;//スタックスペースで 4 バイトを許可します
char arr[10] = {0};//スタック領域に 10 バイトの連続領域を空ける
ただし、上記のスペースを開く方法には 2 つの特徴があります。
1. スペース割り当てサイズは固定です。
2. 配列を宣言するときは、配列の長さを指定する必要があります。必要なメモリはコンパイル時に割り当てられます。
しかし、スペースの需要は上記の状況だけではありません。必要なスペースのサイズは、プログラムの実行中にのみ知ることができ、配列のコンパイル時にスペースを作成する方法だけでは不十分な場合があります。現時点では、動的ストレージと開発のみを試すことができます。開発し放題なのが人気のポイント!!!
2. ダイナミックメモリ関数の紹介
mallocと無料
C 言語には動的メモリ割り当て関数が用意されています。まず、関数のプロトタイプを見てみましょう。
この関数は、メモリ内の連続した利用可能な領域に適用され、この領域へのポインタを返します。
割り当てが成功すると、割り当てられた領域へのポインタが返されます。
オープンに失敗した場合は NULL ポインタが返されるため、malloc の戻り値を確認する必要があります。
戻り値の型は void* なので、malloc 関数は開いている空間の型を知りません。使用する際はユーザーが自分で決めることができます。
パラメータのサイズが 0 の場合、malloc の動作は標準では定義されておらず、コンパイラに依存します。
C 言語には、動的メモリの解放と再利用に特に使用される別の関数が無料で提供されており、関数のプロトタイプは次のとおりです。
free 関数は、動的に割り当てられたメモリを解放するために使用されます。
パラメータ ptr が指す空間が動的に開かれていない場合、free 関数の動作は未定義です。
パラメータ ptr が NULL ポインタの場合、関数は何も行いません。
したがって、ptr が指す空間は、通常、動的メモリ関数によって開かれたポインタになります。
int main()
{
int a = 10;
int* p = &a;
free(p);
return 0;
}
上記は間違ったコードです。安易に試さないでください。プログラムがクラッシュする可能性があります。!!
malloc と free は両方とも stdlib.h ヘッダー ファイルで宣言されます。
手順を実行してみましょう。
int main()
{
int arr[10];
int* p = (int*)malloc(40);
return 0;
}
整数配列を適用したい場合、通常は int arr[10]; を使用して定義しますが、malloc 関数を使用して 40 バイトのメモリを開くこともできます。適用するバイト サイズをパラメータに指定するだけです。必要な malloc 関数は配列のように一度に 4 バイトにアクセスする必要があり、malloc によって返されたアドレスを int* p ポインターに割り当てることができますが、malloc は void* 型のポインターを返すため、それを int * にキャストする必要があります。
malloc を使用してアプリケーションの配列を模倣します。
int main()
{
int num = 0;
scanf("%d", &num);
int* ptr = NULL;
ptr = (int*)malloc(num * sizeof(int));
if (NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for (i = 0; i < num; i++)
{
printf("%d ", *(ptr + i));
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//是否有必要?
return 0;
}
malloc 関数を使用して領域を開くときは、ポインタが null ポインタであるかどうかを判断する必要があります。malloc は領域を開くことができない可能性があるためです。
動的スペースの使用が終了したら、free 関数を使用してそれを解放する必要があります。そうしないと、コンピューターの電源がオフになるかプログラムが終了しない限り、このメモリは常に存在します。!!
スペースを解放すると、ptr ポインタは役に立たなくなり、ワイルド ポインタになります。したがって、最後にポインタを初期化する必要があります。
(これらの内容はcalloc関数やrealloc関数でも実用的なものなので注意が必要です)
malloc が正常に開かれた場合は、ポインターを使用してスペース内にあるものを出力できます。
20 個のスペースを開けると、5 つの要素のコンテンツにアクセスできます。すべてランダムな内容であり、malloc 関数によって開かれた領域が初期化されていないことを証明できます。
開発に失敗した場合、perror 関数を使用して失敗の原因を出力することで、問題がどこにあるのかをより深く理解することができ、ポインタが空であると判断したモジュールを次のように変更できます。
int main()
{
int* ptr = NULL;
ptr = (int*)malloc(INT_MAX);
if (ptr == NULL)
{
perror("malloc");
return 1;
}
else if (NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for (i = 0; i < INT_MAX; i++)
{
printf("%d ", *(ptr + i));
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//是否有必要?
return 0;
}
作成バイトを INT_MAX に書き込むと、開くのに十分なメモリがなく、NULL ポインタが返されます。
calloc関数
C 言語には calloc と呼ばれる関数も用意されており、これも動的メモリ割り当てに使用されます。プロトタイプは次のとおりです。
この関数の機能は、サイズが size である num 個の要素用のスペースを開き、スペースの各バイトを 0 に初期化することです。
関数 malloc との唯一の違いは、calloc はアドレスを返す前に、要求された空間の各バイトをすべて 0 に初期化することです。
例を挙げてみましょう:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p)
{
//使用空间
}
free(p);
p = NULL;
return 0;
}
プログラムをデバッグすると、すべて 0 に初期化されていることがわかります。これが malloc 関数との違いです。
再割り当て関数
メモリ割り当てを柔軟にするという先ほど述べた内容は、上記の 2 つの関数には反映されていませんが、realloc 関数には完全に反映されます。
realloc 関数の登場により、動的メモリ管理がより柔軟になります。
過去に申請したスペースが小さすぎると感じることもあれば、申請したスペースが大きすぎると感じることもあり、適正なメモリにするためには、メモリのサイズを柔軟に調整する必要があります。次に、realloc 関数を使用して、動的に割り当てられたメモリのサイズを調整できます。
関数のプロトタイプは次のとおりです。
ptr は調整するメモリアドレスです
サイズ 調整後の新しいサイズ
戻り値は調整されたメモリ開始位置です。
この関数は、元のメモリ空間のサイズを調整することに基づいて、元のメモリ内のデータも新しい空間に移動します。
realloc がメモリ空間を調整する場合は 2 つの状況があります。
ケース 1: 元のスペースの後に十分なスペースがある
ケース 2: 元のスペースの後に十分なスペースがない
ケース 1: ケース 1 の場合、メモリを拡張したい場合は、元のメモリの後にスペースを直接追加できます。元のスペースのデータは変更されません。
ケース 2: ケース 2 では、元のスペースの後に十分なスペースがない場合、拡張方法は次のとおりです。ヒープ スペース上で適切なサイズの別の連続したスペースを見つけて使用します。この関数は新しいメモリ アドレスを返します。上記 2 つの状況のため、realloc 関数の使用には注意が必要です。realloc が新しいスペースを開くと、古いスペースのデータが新しいスペースにコピーされ、古いスペースが解放され、新しいスペースのアドレスが返されます。
例えば:
#include <stdio.h>
int main()
{
int *ptr = (int*)malloc(100);
if(ptr != NULL)
{
//业务处理
}
else
{
exit(EXIT_FAILURE);
}
//扩展容量
//代码1
ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
//代码2
int*p = NULL;
p = realloc(ptr, 1000);
if(p != NULL)
{
ptr = p;
}
//业务处理
free(ptr);
return 0;
}
malloc を使用してスペースを空けるのに十分ではない場合、realloc を使用してスペースを増やします。これは 2 つの場合に見られます。元のスペースが十分に大きい場合は、後ろに余分なスペースを直接追加して元のアドレスに戻ることができます。スペースが十分でない場合は、十分な大きさの新しいスペースが開かれ、新しいスペースのアドレスが返されます。コード 1 を使用して操作し、常に同じポインタ変数を使用して維持する場合、オープンが成功したかどうかは問題ではありませんが、オープンに失敗した場合、realloc は ptr に NULL を返し、以前に malloc で作成した変数はポインタを見つけることができないため、データが失われ、malloc によって開かれたスペースを解放する方法がありません。深刻な結果を引き起こすことになります。!!したがって、realloc (コード 2 で記述) によって返されるポインターを受け取る新しいポインター変数を作成する必要があります。
上記3つの機能を使用する際は、バグを防ぐために今強調した点に注意する必要があります。
3. 一般的な動的メモリエラー
3.1 NULL ポインタの逆参照操作
最初のタイプの問題は、動的メモリ関数を使用するときに最もよくある間違いです。つまり、関数の戻り値 (NULL かどうか) を判断せず、受信ポインターを直接逆参照してしまうことです。問題は、null ポインターを逆参照するときに発生します。
void test()
{
int *p = (int *)malloc(LLONG_MAX);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
上記のコードは、十分なスペースがないために確実に開くことができず、p=NULL が与えられるため、p を逆参照するとエラーが発生するため、動的メモリ開発を使用する場合は、受け取ったポインタを判断する必要があります。!!
3.2 動的に割り当てられたスペースへの境界外アクセス
この問題を理解するために次のプログラムを使用します。
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
malloc を使用して 40 バイトのスペースを開く場合、for ループを使用してスペース内の値にアクセスする場合、ループ i=10 の場合、アクセスは範囲外になります。i=10 でアクセスすると、空き領域を超えた 44 バイトの領域にアクセスしたことになります。これは配列に似ており、範囲外のアクセスによってヒープ領域の他の内容が変更される可能性がある場合、これは非常に危険です。!!
3.3 free を使用して非動的メモリを解放する
これについては、動的に割り当てられたメモリを解放するために malloc モジュールですでに述べましたが、これによりプログラムが直接クラッシュします。
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
3.4 free を使用して動的に割り当てられたメモリの一部を解放します
ダイナミックメモリ関数から返されるポインタを使用する場合、ポインタを移動すると、使用後にポインタが指す位置が開いたメモリの先頭アドレスではなくなりますので、このポインタを直接解放に使用すると、解放されます。それは動的記憶発達の一部です。
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
このとき、プログラムがクラッシュしてしまうため、このポインタを使用する場合は、このエラーを回避するために別のポインタ変数を作成して動作させるのが最善です。
3.5 同じ動的メモリの複数のリリース
この問題は通常、プログラマが混乱し、空き領域の同じ部分を複数回解放した場合にのみ発生します。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
free(p);
free(p);
return 0;
}
このプログラムもクラッシュします! !!
3.6 メモリを動的に開いて解放し忘れる (メモリ リーク)
動的に割り当てられたメモリ領域を解放するのを忘れると、メモリ リークが発生します。!!
以下に、より極端なプログラムを示します。
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
return 0;
}
再度関数を呼び出すときは、malloc関数を使ってメモリ空間を解放し、pポインタを使って戻り値を受け取りますが、使用後解放し忘れると、関数終了後にポインタpが破棄されてしまいます。ただし、malloc によって作成されたメモリ ブロックはそうではありません。この機能が終了すると、この記憶の断片を指すポインタはなくなり、二度と見つかることはありません(警察映画の潜入警官のように、チェン卿だけが知っていますが、ある日チェン卿が亡くなると、誰も知ることはありません)覆面警官の身元はもうわかりません)、そして while(1) を使用して無限ループに入ると、プログラムは決して終了せず、このメモリがリークします。!!
動的に割り当てられた、使用されなくなった領域を解放し忘れると、メモリ リークが発生する可能性があります。
覚えておいてください: 動的に割り当てられたスペースは、正しく解放されなければなりません。
4. C/C++プログラムのメモリ割り当て
これは皆さんのための図です。メモリ割り当てがはっきりとわかります。
C/C++ プログラムのメモリ割り当てのいくつかの領域:
1. スタック領域(スタック):関数実行時にスタック上に関数内のローカル変数の記憶部を作成でき、関数の実行終了時にこの記憶部は自動的に解放されます。スタック メモリ割り当て操作はプロセッサの命令セットに組み込まれており、非常に効率的ですが、割り当てられるメモリ容量には制限があります。スタック領域には主に、関数の実行により割り当てられるローカル変数、関数のパラメータ、戻りデータ、戻りアドレスなどが格納されます。
2. ヒープ領域(ヒープ):通常、プログラマが確保、解放しますが、プログラマが解放しない場合、プログラム終了時にOSが再利用する場合があります。割り当て方法はリンク リストに似ています。
3. データセグメント(静的領域)(static)には、グローバル変数と静的データが格納されます。番組終了後にシステムにより解放されます。
4. コードセグメント:関数本体(クラスメンバ関数およびグローバル関数)のバイナリコードを格納します。
5. 柔軟なアレイ
フレキシブル配列の概念について聞いたことがないかもしれませんが、フレキシブル配列は存在します。C99 では、構造体の最後の要素はサイズが不明な配列であることが許可されており、これはフレキシブル配列メンバーと呼ばれます。
例えば:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
一部のコンパイラはコンパイルできないエラーを報告しますが、これは次のように変更できます。
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
配列の要素数は 0 として指定することも、指定しないこともできます。
5.1 フレキシブル アレイの特徴:
構造体のフレキシブル配列メンバーの前には、少なくとも 1 つの他のメンバーがなければなりません。
sizeof によって返されるこのような構造体のサイズには、フレキシブル配列のメモリは含まれません。
フレキシブル配列メンバーを含む構造体は、メモリの動的な割り当てに malloc() 関数を使用します。割り当てられるメモリは、フレキシブル配列の予想されるサイズに対応できるように、構造体のサイズより大きくする必要があります。
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4
この構造体のサイズを計算してみると、メンバは整変数iとint型の配列を持っていますが、その配列はフレキシブル配列(要素数が0)であるため、サイズがないことが分かります。したがって、この構造体のサイズは 4 です。
5.2 フレキシブルアレイの使用
//代码1
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
sizeof(type_a) は非柔軟な配列のサイズしか見つけることができず、開きたい配列のサイズは malloc で開いたサイズなので、柔軟な配列を持つ構造体用のスペースを動的に開きます。構造体ポインタを受け取って、必要な空間メモリを取得できます。このように、柔軟な配列メンバー a は、100 個の整数要素の連続空間を取得するのと等価です。
5.3 フレキシブルアレイの利点
//代码2
typedef struct st_type
{
int i;
int *p_a;
}type_a;
type_a *p = (type_a *)malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int *)malloc(p->i*sizeof(int));
//业务处理
for(i=0; i<100; i++)
{
p->p_a[i] = i;
}
//释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
上記のコード 1 とコード 2 は同じ機能を実現できますが、方法 1 の実装には 2 つの利点があります。
最初の利点は、メモリの解放が簡単であることです。
私たちのコードが他の関数の中にある場合は、その中で二次メモリの割り当てを行い、構造全体をユーザーに返します。ユーザーは free を呼び出すことで構造体を解放できますが、ユーザーはこの構造体のメンバーも解放する必要があることを知らないため、ユーザーがこれを見つけることは期待できません。そこで、構造体のメモリとそのメンバが必要とするメモリを一度に割り当て、構造体へのポインタをユーザに返すと、ユーザは一度フリーにするだけですべてのメモリを解放することができます。
2 番目の利点は、アクセス速度が向上することです。
連続したメモリは、アクセス速度を向上させ、メモリの断片化を軽減するのに役立ちます。
上記の 2 つのコードは、差異を減らし、より類似した目的を達成するために、割り当てられたすべてのメモリをヒープ領域に配置します。実際、その必要はありません。コード 2 では、構造体内にポインター オブジェクト用のスペースを空けるだけです。
以上が今回の内容のすべてです。私の欠点をコメント欄でご指摘いただければ幸いです。謙虚に学ぶことが私の信念です。!!