C言語でのメモリ管理

導入

C 言語のメモリ管理は 2 つの部分に分かれています。 1 つの部分はシステムによって管理され、もう 1 つの部分はユーザーによって手動で管理されます。

システムが管理するメモリは主に関数内の変数(ローカル変数)です。これらの変数は、関数の実行中にメモリに入力され、関数の終了後にメモリから自動的にアンロードされます。これらの変数を格納する領域を「スタック」と呼び、「スタック」が配置されるメモリはシステムによって自動的に管理されます。

ユーザーが手動で管理するメモリは主に、プログラムの実行プロセス全体にわたって存在する変数 (グローバル変数) であり、これらの変数はユーザーが手動でメモリから解放する必要があります。使用後に解放し忘れると、プログラムが終了するまでメモリを占有し続ける状態を「メモリリーク」(メモリリーク)といいます。これらの変数が配置されているメモリは「ヒープ」と呼ばれ、「ヒープ」が配置されているメモリはユーザーによって手動で管理されます。

ボイドポインタ

前の章で述べたように、各メモリにはアドレスがあり、指定されたアドレスにあるメモリ ブロックはポインタ変数を通じて取得できます。ポインタ変数には型がなければなりません。型がないと、コンパイラはメモリ ブロックに格納されているバイナリ データを解釈する方法を知ることができません。ただし、システムにメモリを要求する場合、どのような種類のデータがメモリに書き込まれるかがわからない場合があります。最初にメモリ ブロックを取得してから、書き込むデータの種類を決定する必要があります。

このニーズを満たすために、C 言語では void ポインタと呼ばれる不定型のポインタが提供されています。メモリ ブロックのアドレス情報のみがあり、型情報はありません。メモリ ブロックが使用される場合、内部のデータ型に関する追加情報がコンパイラに提供されます。

一方、void ポインターは型なしポインターと同等であり、任意の型のデータを指すことができますが、データを解釈することはできません。 void ポインターと他のすべての型のポインターの間には相互変換関係があり、任意の型のポインターは void ポインターに変換でき、void ポインターも任意の型のポインターに変換できます。

int x = 10;

void* p = &x; // 整数ポインタを void ポインタに変換します。
int* q = p; // void ポインタを整数ポインタに変換します。

上の例は、整数ポインターと無効ポインターを相互に変換する方法を示しています。 &x は整数ポインタ、p は void ポインタであり、&x のアドレスは代入時に自動的に void 型として解釈されます。同様に、「p」を整数ポインタ「q」に代入すると、「p」のアドレスは自動的に整数ポインタとして解釈されます。

void ポインターが指す値のタイプがわからないため、* 演算子を使用してそれが指す値を取得することはできないことに注意してください。

文字 a = 'X';
void* p = &a;

printf("%c\n", *p); // エラーレポート

上の例では、p は void ポインターであるため、*p を使用してポインターが指す値を取得することはできません。

void ポインタで重要なのは、多くのメモリ関連関数の戻り値がメモリ ブロックのアドレス情報のみを与える void ポインタであるため、これを先頭に導入していることです。

malloc()

malloc() 関数はメモリを割り当てるために使用されます。この関数はシステムからメモリの一部を必要とし、システムは「ヒープ」内のそれに連続メモリ ブロックを割り当てます。そのプロトタイプはヘッダー ファイル stdlib.h で定義されます。

void* malloc(size_t サイズ)

割り当てられるメモリ バイト数を示す非負の整数をパラメータとして受け取り、割り当てられたメモリ ブロックを指す void ポインタを返します。これは非常に合理的です。なぜなら、「malloc()」関数は、このメモリ ブロックにどのようなデータが格納されるのかを知らないため、型なしの void ポインタしか返せないからです。

malloc() を使用して、あらゆるタイプのデータにメモリを割り当てることができます。一般的なアプローチは、まず sizeof() 関数を使用して特定のデータ型に必要なバイト長を計算し、次にこの長さを に渡すことです。 malloc()

int* p = malloc(sizeof(int));

*p = 12;
printf("%d\n", *p);

上記の例では、整数型にメモリが割り当てられ、整数「12」がこのメモリに配置されます。 C 言語は整数 (この場合は 12) 用のメモリを自動的に提供するため、実際にはこの例では malloc() を使用する必要はありません。

コードの可読性を高めるために、malloc() によって返されたポインタに対して強制的な型変換を実行することがあります。

int* p = (int*) malloc(sizeof(int));

上記のコードは、malloc() によって返された void ポインターを整数ポインターにキャストします。

sizeof() のパラメータは変数にできるので、上記の例は次のように書くこともできます。

int* p = (int*) malloc(sizeof(*p));

malloc() はメモリの割り当てに失敗する可能性があり、その場合は定数 NULL が返されます。 NULL の値は 0 で、読み取りも書き込みもできないメモリ アドレスです。これは、どこを指さないポインタとして理解できます。 stdlib.hを含む複数のヘッダファイルに定義されているため、malloc()が使用できる限り、NULLを使用することができます。割り当てに失敗する可能性があるため、malloc() を使用して割り当てが成功したかどうかを確認するのが最善です。

int* p = malloc(sizeof(int));

if (p == NULL) {
  //メモリ割り当てに失敗しました
}

// または
if (!p) {
  //...
}

上記の例では、返されたポインタ p が NULL かどうかを判断することで、malloc() が正しく割り当てられたかどうかを判断します。

malloc() の最も一般的な使用例は、配列およびカスタム データ構造にメモリを割り当てることです。

int* p = (int*) malloc(sizeof(int) * 10);

for (int i = 0; i < 10; i++)
  p[i] = i * 5;

上の例では、「p」は整数ポインタで、10 個の整数を保持できるメモリを指すため、配列として使用できます。

malloc() は配列の作成に使用されます。利点の 1 つは、動的配列を作成できることです。つまり、メンバーの数に応じて異なる長さの配列を作成できることです。

int* p = (int*) malloc(n * sizeof(int));

上の例では、malloc() は変数 n に基づいて異なるサイズを配列に動的に割り当てることができます。

malloc() は割り当てられたメモリを初期化せず、元の値がまだメモリに格納されていることに注意してください。初期化されていない場合、このメモリが使用され、以前の値がそこから読み取られる可能性があります。たとえば、文字列の初期化には strcpy() 関数を使用できます。

char* p = malloc(4);
strcpy(p, "abc");

上の例では、文字ポインタ p は 4 バイトのメモリを指し、strcpy() は文字列 "abc" をこのメモリにコピーし、このメモリの初期化を完了します。

無料()

free() は、malloc() 関数によって割り当てられたメモリを解放し、このメモリをシステムに返して再利用するために使用されます。それ以外の場合、このメモリ ブロックはプログラムの終了まで占有されます。この関数のプロトタイプはヘッダー ファイル stdlib.h で定義されます。

ボイドフリー(ボイド*ブロック)

上記のコードでは、free() のパラメータは、malloc() によって返されるメモリ アドレスです。以下に使用例を示します。

int* p = (int*) malloc(sizeof(int));

*p = 12;
無料(p);

割り当てられたメモリブロックが解放された後は、解放されたアドレスを再度操作したり、「free()」を使用してアドレスを再度解放したりしないでください。

非常によくある間違いは、関数内でメモリが割り当てられているのに、関数呼び出しの最後に free() を使用してメモリが解放されていないことです。

void gobble(double arr[], int n) {
  double* temp = (double*) malloc(n * sizeof(double));
  // ...
}

上記の例では、関数 gobble() は内部でメモリを割り当てますが、free(temp) は書き込みません。これにより、関数終了後も占有されたメモリ ブロックが残ります。gobble() が複数回呼び出された場合、複数のメモリ ブロックが残ります。また、ポインタ「temp」が消失しているため、これらのメモリブロックに再度アクセスして使用することはできません。

calloc()

関数 calloc()malloc() に似ており、同様にメモリ ブロックを割り当てます。この関数のプロトタイプはヘッダー ファイル stdlib.h で定義されます。

両者には主に次の 2 つの違いがあります。

(1) calloc() は 2 つのパラメータを受け取ります。最初のパラメータは特定のデータ型の値の数であり、2 番目のパラメータはデータ型の単位バイト長です。

void* calloc(size_t n, size_t size);

calloc() の戻り値も void ポインタです。割り当てに失敗した場合は NULL が返されます。

(2) calloc() は、割り当てられたすべてのメモリを 0 に初期化します。 malloc() はメモリを初期化しません。メモリを 0 に初期化したい場合は、さらに memset() 関数を呼び出す必要があります。

int* p = calloc(10, sizeof(int));

// と同等
int* p = malloc(sizeof(int) * 10);
memset(p, 0, sizeof(int) * 10);

上記の例では、calloc()malloc() + memset() と同等です。

calloc() によって割り当てられたメモリ ブロックも、free() を使用して解放する必要があります。

realloc()

「realloc()」関数は、割り当てられたメモリ ブロックのサイズを変更するために使用され、拡大または縮小することができ、新しいメモリ ブロックへのポインタを返します。割り当てに失敗した場合は、NULL が返されます。この関数のプロトタイプはヘッダー ファイル stdlib.h で定義されます。

void* realloc(void* ブロック, size_t サイズ)

2 つのパラメータを受け入れます。

  • block: すでに割り当てられているメモリ ブロック ポインター (malloc() または calloc() または realloc() によって生成される)。
  • size: このメモリ ブロックの新しいサイズ (バイト単位)。

realloc() は、まったく新しいアドレス (データはそこに自動的にコピーされます) を返す場合もあれば、元のアドレスと同じアドレスを返す場合もあります。 realloc() は元のメモリブロックを減らすことを優先し、データの移動を行わないようにするため、通常は元のアドレスを返します。新しいメモリ ブロックが元のサイズより小さい場合、余分な部分は破棄され、元のサイズより大きい場合、新しい部分は初期化されません (プログラマは自動的に memset() を呼び出すことができます)。

以下は例です。「b」は配列ポインタで、「realloc()」はそのサイズを動的に調整します。

int* b;

b = malloc(sizeof(int) * 10);
b = realloc(b, sizeof(int) * 2000);

上記の例では、ポインタ b は元々 10 メンバの整数配列を指していましたが、realloc() を使用して 2000 メンバの配列に調整されました。これは、配列メモリを手動で割り当てることの利点であり、実行時にいつでも配列の長さを調整できます。

realloc() の最初のパラメータは NULL にすることができます。これは新しいポインタを作成するのと同じです。

char* p = realloc(NULL, 3490);
// と同等
char* p = malloc(3490);

realloc() の第 2 パラメータが 0 の場合、メモリ ブロックは解放されます。

割り当てに失敗する可能性があるため、realloc() を呼び出した後、戻り値が NULL かどうかを確認するのが最善です。割り当てに失敗した場合、元のメモリ ブロック内のデータは変更されません。

float* new_p = realloc(p, sizeof(*p * 40));

if (new_p == NULL) {
  printf("再割り当てエラー\n");
  1を返します。
}

realloc() はメモリ ブロックを初期化しないことに注意してください。

制限指定子

ポインタ変数を宣言するときに、「restrict」指定子を使用して、このメモリ領域のブロックには現在のポインタによってのみアクセスでき、他のポインタはこのメモリ ブロックの読み書きができないことをコンパイラに伝えることができます。この種のポインタは「制限付きポインタ」と呼ばれます。

int* 制限 p;
p = malloc(sizeof(int));

上記の例では、ポインター変数 p を宣言するときに、restrict 指定子が追加され、p が制限付きポインターになります。その後、pmalloc() 関数によって返されたメモリ領域を指す場合、この領域には p を介してのみアクセスでき、他のアクセス方法はないことを意味します。

int* 制限 p;
p = malloc(sizeof(int));

int* q = p;
*q = 0; // 未定義の動作

上の例では、別のポインタ q と制限されたポインタ p が同じメモリを指しており、メモリには 2 つのアクセス方法 pq があります。これはコンパイラへの約束に違反し、その後 *q を介してこのメ​​モリ領域に割り当てられると、未定義の動作が発生します。

memcpy()

memcpy() は、あるメモリを別のメモリにコピーするために使用されます。この関数のプロトタイプはヘッダー ファイル string.h で定義されます。

void* memcpy(
  void* 制限宛先、
  void* 制限ソース、
  サイズ_t n
);

上記のコードでは、「dest」は宛先アドレス、「source」は送信元アドレス、3 番目のパラメータ「n」はコピーされるバイト数「n」です。 10 個の double 配列メンバーをコピーする場合、n10 ではなく、10 * sizeof(double) に等しくなります。この関数は、source から始まる n バイトを dest にコピーします。

destsource は両方とも void ポインターです。これは、ポインターの型に制限がなく、すべての型のメモリ データをコピーできることを意味します。両方とも、restrict キーワードを持っています。これは、2 つのメモリ ブロックに重複領域があってはいけないことを意味します。

memcpy() の戻り値は最初のパラメータであり、ターゲット アドレスへのポインタです。

memcpy() はあるメモリの値を別のメモリにコピーするだけなので、メモリ内のデータのタイプを知る必要はありません。以下は文字列をコピーする例です。

#include <stdio.h>
#include <文字列.h>

int main(void) {
  char s[] = "ヤギ!";
  文字 t[100];

  memcpy(t, s, sizeof(s)); //ターミネータを含む 7 バイトをコピーします

  printf("%s\n", t); // "ヤギ!"

  0を返します。
}

上記の例では、文字列 's' が配置されているメモリが、文字配列 't' が配置されているメモリにコピーされます。

memcpy() は文字列コピー用の strcpy() を置き換えることができ、より安全であるだけでなく、文字列の末尾の \0 文字をチェックしません。

char* s = "ハローワールド";

size_t len = strlen(s) + 1;
char *c = malloc(len);

if (c) {
  // strcpy()の書き方
  strcpy(c, s);

  // memcpy()の書き方
  memcpy(c, s, len);
}

上記の例では、2 つの書き方は全く同じ効果を持ちますが、memcpy() の書き方の方が strcpy() よりも優れています。

void ポインターを使用すると、メモリをコピーする関数をカスタマイズすることもできます。

void* my_memcpy(void* dest, void* src, int byte_count) {
  char* s = ソース;
  char* d = 宛先;

  while (byte_count--) {
    *d++ = *s++;
  }

  戻り先;

}

上記の例では、渡された destsrc がどのような種類のポインタであっても、バイト単位でコピーできるように 1 バイトの Char ポインタに再定義されます。 *d++ = *s++ ステートメントは、最初に *d = *s を実行し (ソース バイトの値が宛先バイトにコピーされ)、次にそれぞれが次のバイトに移動するのと同じです。最後に、コピーされた dest ポインタが後で使用するために返されます。

memmove()

memmove() 関数は、あるメモリ データを別のメモリにコピーするために使用されます。 memcpy() との主な違いは、ターゲット領域をソース領域とオーバーラップできることです。オーバーラップが発生した場合はソース領域の内容が変更され、オーバーラップがなかった場合は memcpy() と同じように動作します。

この関数のプロトタイプはヘッダー ファイル string.h で定義されます。

void* memmove(
  void* 宛先、
  void* ソース、
  サイズ_t n
);

上記のコードでは、「dest」は宛先アドレス、「source」は送信元アドレス、「n」は移動されるバイト数です。 destsource は両方とも void ポインターであり、あらゆる種類のメモリ データを移動でき、2 つのメモリ領域が重複できることを示します。

memmove() の戻り値は最初のパラメータであり、ターゲット アドレスへのポインタです。

int a[100];
// ...

memmove(&a[0], &a[1], 99 * sizeof(int));

上記の例では、配列メンバー 'a[1]' から始まる 99 個のメンバーが 1 つ前に移動されます。

別の例を示します。

char x[] = "ホーム スイート ホーム";

// スイートホームホームを出力
printf("%s\n", (char *) memmove(x, &x[5], 10));

上の例では、文字列 x の位置 5 から始まる 10 バイトは「Sweet Home」です。memmove() はそれを位置 0 に移動するため、x は「Sweet Home Home」になります。

memcmp()

memcmp() 関数は 2 つのメモリ領域を比較するために使用されます。そのプロトタイプは string.h で定義されます。

int memcmp(
  const void* s1、
  const void* s2、
  サイズ_t n
);

これは 3 つのパラメータを受け入れます。最初の 2 つのパラメータは比較に使用されるポインタで、3 番目のパラメータは比較するバイト数を指定します。

戻り値は整数です。 2 つのメモリ領域の各バイトは文字形式で解釈され、辞書順に比較されます。 2 つが同じ場合は 0 が返され、s1 が s2 より大きい場合は 0 より大きい整数が返されます。 s1s2 より小さい場合は、0 未満の整数を返します。

char* s1 = "abc";
char* s2 = "acd";
int r = memcmp(s1, s2, 3); // 0 未満

上の例では、「s1」と「s2」の最初の 3 バイトを比較します。「s1」は「s2」より小さいため、「r」は 0 未満の整数で、通常は -1 です。

別の例を示します。

char s1[] = {'b', 'i', 'g', '\0', 'c', 'a', 'r'};
char s2[] = {'b', 'i', 'g', '\0', 'c', 'a', 't'};

if (memcmp(s1, s2, 3) == 0) // true
if (memcmp(s1, s2, 4) == 0) // true
if (memcmp(s1, s2, 7) == 0) // false

上の例は、memcmp() がメモリ領域を内部文字列終端文字 \0 と比較できることを示しています。


作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0