はじめに
今回は第3回に引き続き、ポインタの応用について学習していく。
まずは、メモリについて理解しよう。
1. メモリ領域について
Cプログラムが実行されると、メモリは以下の主要な4つの領域に分けられる。後のメモリ操作関数を使いこなす上で重要なのでそれぞれの領域について理解しておこう。
テキスト領域
実行可能なコードが格納される。読み取り専用で、OSによって書き込み禁止にされている。
データ領域
初期化された静的変数やグローバル変数が格納される。
スタック領域
ローカル変数や関数の呼び出しに関する情報が格納される。スタックはLIFO(Last In First Out)方式で管理され、関数を呼び出した際にメモリが割り当てられ、関数終了時に解放される。
ヒープ領域
mallocやcallo、realloc(後述)によって動的に確保されたメモリが格納される。プログラム実行中にサイズが伸縮する。自由に確保・開放することができる。
2. 関数ポインタ
関数のアドレスを格納するために使われる関数ポインタも存在する。これにより、関数を変数として扱うことができ、関数を引数として他の関数に渡したり、戻り値として返すことが可能になる。
関数ポインタの宣言は以下のようになる。
戻り値の型 (*ポインタ名)(引数の型);
例として、整数を2つ受け取り、整数を返すadd関数のaddptrポインタは以下のように宣言する。
int (*addptr)(int, int);
add関数に対して、関数ポインタは*addとは定義できないことに注意!
さらに次のプログラムを動作させて、関数ポインタがどのように使われるかを確認しよう。
#include <stdio.h>
int add(int x, int y) {
return x + y;
}
int main() {
// 関数ポインタの宣言
int (*funcPtr)(int, int);
// 関数ポインタに add 関数のアドレスを代入
funcPtr = add;
// 関数ポインタを使って関数を呼び出し
int result = funcPtr(10, 20);
printf("Result: %d\n", result);
return 0;
}
3. メモリ操作
これから挙げる関数は、メモリ管理やデータ処理の基本的なツールとして、C言語では広く使用されている。特に、リソースが限られた環境などで重要となる。
これらはヒープメモリを操作する関数です。
malloc
ヒープ領域から動的にメモリを割り当てる。
確保したメモリの先頭アドレスを示すポインタを返す。
void* malloc(size_t size);
int型10個分のメモリを確保する例を次に示す。
int *ptr = (int *)malloc(sizeof(int) * 10); // int型10個分のメモリを確保
if (ptr == NULL) {
// メモリ確保失敗
}
C言語におけるキャストの必要性
C言語では、キャストなしでmallocを使用することができる。上の例ではptrをint *にキャストしているが、C言語ではvoid *から他の型へのポインタへのキャストは暗黙的に行われるため、キャストは必須ではない。
C++ではvoid *から他の方への暗黙のキャストが許可されていないため、CのコードをC++コンパイラでコンパイルする場合、明示的なキャストが必要となる。
キャストを明示することでC++との互換性が保たれる。
calloc
ヒープ領域から動的にメモリを割り当てる。
指定されたバイト数のメモリを割り当て、そのメモリへのポインタを返す。メモリの初期化は行われない。
void* calloc(size_t num, size_t size);
realloc
割り当てられたメモリブロックのサイズを変更する。新しいサイズが元のサイズより大きい場合、追加された部分は初期化されない。
void* realloc(void* ptr, size_t size);
free
割り当てられたメモリを解放する。malloc、calloc、reallocによって割り当てられたメモリブロックを解放する。
void free(void* ptr);
memcpy
メモリブロックの内容を別のメモリブロックをコピーする。
void* memcpy(void* dest, const void* src, size_t n);
srcからdestにnバイトコピーするが、オーバーラップするメモリブロック間でのコピーには適していない。
memmove
メモリブロックの内容を別のメモリブロックにコピーするが、オーバーラップに対応している。
void* memmove(void* dest, const void* src, size_t n);
memset
メモリブロックを特定の値で初期化する。ptrが示すメモリブロックの先頭からnumバイトをvalueで設定する。valueはunsigned charに変換されて使用される。
void* memset(void* ptr, int value, size_t num);
memcmp
メモリブロックを比較する。ptr1とptr2が示すメモリブロックの先頭numバイトを比較し、異なる場合は差異の符号付き結果を返す。
int memcmp(const void* ptr1, const void* ptr2, size_t num);
sizeof演算子
sizeofまたはsizeof型でその型がメモリ上で何バイト消費するかを求めることができる。
試しに、次のプログラムを実行して基本データ型(int、float、double、char)のサイズを取得して表示してみよう。
#include <stdio.h>
int main() {
printf("Size of int: %zu bytes\n", sizeof(int));
printf("Size of float: %zu bytes\n", sizeof(float));
printf("Size of double: %zu bytes\n", sizeof(double));
printf("Size of char: %zu bytes\n", sizeof(char));
return 0;
}
次に、配列全体のサイズと、配列内の要素数を計算して表示するプログラムを見てみよう。
#include <stdio.h>
int main() {
int array[10];
printf("Size of array: %zu bytes\n", sizeof(array));
printf("Number of elements in array: %zu\n", sizeof(array) / sizeof(array[0]));
return 0;
}
4. メモリ管理の注意点
メモリを管理する際は、以下のようなプログラムを書かないように注意しよう。
メモリリーク
mallocやcallocで確保したメモリはfreeで開放しないとメモリリークが発生する。メモリリークはメモリ不足を引き起こし、パフォーマンスを悪化させるので、不必要になったメモリはfreeで開放する必要がある。
二重開放
同じメモリを2回以上解放する「二重解放」は未定義動作となるので注意が必要。
5. ポインタ演算
ポインタ演算を使うことで、配列の要素に効率的にアクセスしたり、メモリ上でのデータ構造を柔軟に操作することが可能。
次のプログラムで確認しよう。
#include <stdio.h>
int main() {
int array[5] = {10, 20, 30, 40, 50};
int* ptr = array; // ptrはarrayの最初の要素を指す
// ポインタを使って配列の3番目の要素にアクセス
printf("%d\n", *(ptr + 2)); // 出力: 30
// ポインタ間の減算
int* ptr2 = &array[4]; // ptr2はarrayの最後の要素を指す
printf("%ld\n", ptr2 - ptr); // 出力: 4
// ポインタの比較
if (ptr < ptr2) {
printf("ptrはptr2より先にある\n");
}
}