C言語のダイナミックメモリの詳細解説


序文

プログラム内で最もエラーが発生しやすいのはメモリ、特にこの章の動的メモリですが、C 言語の最大の欠点は、メモリ管理を自分で行う必要があることです。他の高級言語にはメモリの再利用メカニズムがないため、この章で学ぶ動的メモリでは、メモリに注意を払い、メモリ リークを避ける必要があります。

まず、なぜ動的メモリ割り当てが行われるのか

私たちが習得したメモリ開発方法の中には、変数と配列があります。

	int num = 10;//在栈空间上开辟4个字节
	int sum[10] = {
    
     0 };//在栈空间上开辟40个连续的字节

上記の空き領域では、コンパイル時に領域を確保できるように、空き領域のサイズが固定であるか、配列の宣言時に配列の長さを指定する必要がありますしかし、クラスに何人の生徒がいるか、後でクラスに新しい生徒が参加するかどうかなど、必要なスペースのサイズがわからない場合があります。静的配列によって開かれるスペースは非常に限られているため、スペースの無駄やスペース不足の原因となります。このとき、動的なメモリ割り当てが必要になります。
まず、図でメモリの分布を理解しましょう:
ここに画像の説明を挿入
前に作成した変数は通常スタック上に配置され、動的に開かれた領域はヒープ領域に配置されます。スタックは下方向に増加し、ヒープは上方向に増加する可能性があります。
1. スタック領域(スタック):関数実行時に関数内のローカル変数の記憶部をスタック上に作成でき、関数終了時に自動的に解放されます スタック領域には主にローカル変数が格納されます実行中の関数、関数パラメータ、戻りデータ、戻りアドレスなどによって割り当てられます。
2. ヒープ領域 (ヒープ):通常、プログラマによって割り当ておよび解放されますが、プログラマが解放しない場合は、プログラムの終了時に OS によって再利用される場合があります。
3. コードセグメント:関数本体のバイナリコードを格納します。

2. 動的なメモリ割り当て

動的メモリ割り当ての機能には、ヘッダー ファイル stdlib.h が含まれている必要があります。

1.malloc

関数プロトタイプ:

void* malloc(size_t size);

この関数は、メモリから連続的に使用可能なスペースを適用し、このスペースへのポインタを返すことができます
注:
割り当てが成功すると、このスペースへのポインタが返されます。
開発が失敗した場合は、NULL ポインタが返されます。malloc の戻り値をチェックする必要があります
サイズが 0 の場合、それは未定義の動作であり、コンパイラに依存します。
関数の戻り値は void* 型で、必要に応じて使用する型に変換できます。
コードは次のとおりです(例)。

int main()
{
    
    
	//向内存申请10个连续的整形空间
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
    
    
		printf("%s\n", strerror(errno));
		exit(1);
	}
	else
	{
    
    
		int i = 0;
		//为开辟的空间赋值
		for (i = 0; i < 10; i++)
		{
    
    
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
    
    
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

ここに画像の説明を挿入

2.無料

関数プロトタイプ:

void* free(void* ptr);

free 関数は、動的に割り当てられたメモリを解放するために使用されます。
パラメータ ptr が指すスペースが動的に開かれていない場合、free 関数の動作は未定義です
パラメータ ptr が NULL ポインタの場合、関数は何も行いません。

3.calloc

関数プロトタイプ:

void* calloc(size_t num,size_t size);

この関数の機能は、サイズ size の num 要素用のスペースを開き、スペースの各バイトを 0 に初期化することです。
malloc と比較すると、この関数はアドレスを返す前に要求された領域を初期化します
コードは次のとおりです(例)。

int main()
{
    
    
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
    
    
		printf("%s\n", strerror(errno));
		exit(1);
	}
	else
	{
    
    
		int i = 0;
		for (i = 0; i < 10; i++)
		{
    
    
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

ここに画像の説明を挿入

4.再割り当て

realloc 関数を使用すると、動的メモリ管理をより柔軟に行うことができます。
アプリケーションのスペースが小さすぎるか大きすぎる場合、メモリを合理的に使用するために、realloc を使用してサイズを調整できます。

void* realloc(void *ptr,size_t size);

ptr は調整するメモリアドレスです
サイズは調整後のサイズです
関数の戻り値は、調整されたメモリ開始位置です
realloc がメモリ空間を調整する状況は 2 つあります。
1. 元の空間の後に十分な空間がある場合、ptr の直後に空間を追加し、ptr に戻ります。
2. 元のスペースの後に十分なスペースがない場合。それから新しいスペースを見つけてください。そして、元のメモリのデータを新しい空間にコピーし、自動的に古い空間を解放し、新しい空間のアドレスを返します
コードは次のとおりです(例)。

int main()
{
    
    
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
    
    
		printf("%s\n", strerror(errno));
		exit(1);
	}
	else
	{
    
    
		int* ptr = realloc(p, 10 * sizeof(int));
		if (ptr != NULL)
		{
    
    
			p = ptr;
		}
		int i = 0;
		for (i = 0; i < 10; i++)
		{
    
    
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
    
    
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

ここに画像の説明を挿入
注:
realloc では、最初に受信するために新しいポインタを使用する必要があります。realloc が失敗し、元のポインタを使用して受信すると、元のポインタは失われます。

3. 一般的なメモリエラー

1. NULL ポインタの逆参照:この状況は通常、動的に割り当てられたメモリが、正常にオープンされたかどうかを判断せずに直接使用される場合に発生します。
2. 動的に割り当てられたスペースへの境界外アクセス:この状況は通常、割り当てられたスペースがアクセスされるスペースよりも小さい場合に発生します。
3. free を使用して、非動的にオープンされたスペースのメモリを解放します
。 4. free を使用して、動的にオープンされたメモリの一部を解放します。この状況は、一般に、オープンされたスペースのポインタが変更され、ポインタが指していない場合に発生します。開始位置、フリーリリースが実行されると一部がリリースされます。
5. 動的メモリの同じ部分の複数のリリース:この状況は、動的メモリの複数のリリースで一般的に発生します。解決策は、解放されたメモリのポインタを NULL に設定することです。変更されたメモリが複数のリリースがある場合でも、問題を引き起こさないでください。

4. 柔軟なアレイ

フレキシブル配列は構造体に存在します
フレキシブル配列の特徴:
1. 構造体のフレキシブル配列メンバの前に少なくとも 1 つの他のメンバが存在する必要があります。
2. sizeof によって返されるこの構造体のサイズには、フレキシブル配列のメモリは含まれません。
3. フレキシブル配列を含む構造体は、メモリ割り当てに malloc を使用します。割り当てられたメモリは、フレキシブル配列の予期されるサイズに対応するための構造体のサイズよりも大きくなります。
コードは次のとおりです(例)。

struct S
{
    
    
	int n;
	int arr[0];
};
int main()
{
    
    
	struct S* p = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	p->n = 10;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
    
    
		p->arr[i] = i;
	}
	for (i = 0; i < 5; i++)
	{
    
    
		printf("%d ", p->arr[i]);
	}
	free(p);
	p=NULL;
	return 0;
}

ここに画像の説明を挿入

struct S
{
    
    
	int n;
	int* arr;
};
int main()
{
    
    
	struct S* p = (struct S*)malloc(sizeof(struct S));
	p->arr = malloc(5*sizeof(int));
	int i = 0;
	for (i = 0; i < 5; i++)
	{
    
    
		p->arr[i] = i;
	}
	for (i = 0; i < 5; i++)
	{
    
    
		printf("%d ", p->arr[i]);
	}
	free(p->arr);
	p->arr = NULL;
	free(p);
	p = NULL;
	return 0;
}

ここに画像の説明を挿入
上記のいずれかの形式でも動的な開発を実現できますが、なぜ柔軟な配列を設定するのでしょうか?
柔軟な配列を使用することの 1 つ目の利点は、上記の 2 つのコードと比較して、メモリの解放が容易になることです。2 つ目の利点は、アクセス速度が向上することです。メモリの連続性は、アクセス速度の向上とメモリの断片化の軽減に役立ちます。

要約する

動的メモリ割り当てはプログラミングの柔軟性を大幅に向上させますが、C 言語でのエラーのほとんどはこれに由来しており、動的メモリ割り当てではメモリ エラーを減らすために多くのことに注意する必要があります。

おすすめ

転載: blog.csdn.net/2301_76986069/article/details/130744605