C言語ポインタ応用編!基本から一歩進んだ使い方を学ぼう

2025年4月14日月曜日

C言語

この記事では、動的なメモリ確保や、配列・構造体との連携、はたまた関数を指し示すなんていう面白い使い方まで、ポインタの応用テクニックを分かりやすく、具体例たっぷりで解説していきます。

さあ、ポインタマスターへの道、ここから一緒に進みましょう!

この記事で学べること

  • ポインタの基本(サクッとおさらい)
  • 動的メモリ確保(mallocやfreeの使い方と注意点)
  • 配列とポインタのちょっと深い関係
  • 関数ポインタ(関数だって指し示せる!)
  • ポインタのポインタ(ダブルポインタって何?)
  • ポインタ応用で失敗しないための注意点

C言語ポインタの基本をおさらい

応用編に入る前に、ほんの少しだけ基本を振り返っておきましょう。そもそもポインタって何?って方はC言語のポインタの使い方をまず見ておきましょう。

ポインタは「メモリアドレスを格納するための変数」でしたね。変数や関数のメモリ上の住所を示すものです。

&演算子で変数のアドレスを取得し、ポインタ変数に代入。
*演算子で、ポインタが指し示すアドレスにある実際の値を取り出す(間接参照とかデリファレンスって言います)。

これだけ覚えていれば、今日の応用編はバッチリついてこれます!

#include <stdio.h>

int main(void) {
  int number = 100;
  int *p_number; // ポインタ変数の宣言

  p_number = &number; // numberのアドレスをp_numberに代入

  printf("変数numberの値: %d\n", number);
  printf("変数numberのアドレス: %p\n", &number);
  printf("ポインタp_numberが指すアドレス: %p\n", p_number);
  printf("ポインタp_numberを使って値を取得: %d\n", *p_number); // *で値を取得

  // ポインタ経由で値を変更もできる!
  *p_number = 200;
  printf("ポインタ経由で変更後のnumberの値: %d\n", number); // numberの値が200に変わる!

  return 0;
}

こんな感じでしたね。もう知ってるよ!という方も、ウォーミングアップということで!
さあ、ここからが本番の応用編ですよ。

C言語ポインタ応用(動的メモリ確保を理解する)

プログラムを書いていると、「実行してみるまで、どれくらいのデータ量が必要かわからない…」なんて場面、結構あります。

例えば、ユーザーが入力した人数分のデータを保存したい、とか。
そんな時に大活躍するのが「動的メモリ確保」です。
プログラムの実行中に、必要な分だけメモリ領域を確保したり、不要になったら解放したりできる仕組みです。

ポインタは、この確保したメモリ領域を指し示すために使われます。
代表的な関数が `malloc` (メモリ確保) と `free` (メモリ解放) で、これらは `stdlib.h` というヘッダファイルで定義されています。

動的メモリ確保の使い方(malloc, free)とサンプル

実際にどう使うのか、見てみましょう。

`malloc` 関数は、引数で指定したバイト数分のメモリを確保し、その先頭アドレスを返してくれます。戻り値は `void*` 型なので、使う型に合わせてキャスト(型変換)するのが一般的です。

使い終わったら、必ず `free` 関数で確保したメモリを解放します。解放しないと、メモリリークという、使わないメモリが残り続けてしまう問題が起きる可能性があるのです。

【書き方】

#include <stdio.h>
#include <stdlib.h> // malloc, free を使うために必要

int main(void) {
  int *p_array;
  int num_elements = 5; // 例えば5個分の整数型配列を確保したい

  // ① 必要なサイズを計算 (int型 * 要素数)
  size_t size_to_allocate = sizeof(int) * num_elements;
  printf("確保するメモリサイズ: %zu バイト\n", size_to_allocate);

  // ② mallocでメモリを確保し、int*型にキャスト
  p_array = (int *)malloc(size_to_allocate);

  // ③ 確保できたかNULLチェック (超重要!)
  if (p_array == NULL) {
    printf("メモリ確保に失敗しました。\n");
    return 1; //異常終了
  }
  printf("メモリ確保成功!アドレス: %p\n", p_array);

  // ④ ポインタを使って確保した領域にアクセス (配列のように使える!)
  for (int i = 0; i < num_elements; i++) {
    p_array[i] = i * 10; // 値を代入
    printf("p_array[%d] = %d\n", i, p_array[i]);
  }

  // ⑤ 使い終わったら必ずfreeで解放!
  free(p_array);
  printf("メモリを解放しました。\n");
  p_array = NULL; // 解放後はNULLを入れておくと安全(ダングリングポインタ対策)

  return 0;
}

【実行結果】(環境によってアドレスは異なります)

確保するメモリサイズ: 20 バイト
メモリ確保成功!アドレス: 0x12a9c20
p_array[0] = 0
p_array[1] = 10
p_array[2] = 20
p_array[3] = 30
p_array[4] = 40
メモリを解放しました。

こんな風に、実行時にサイズを決めてメモリを確保できるのが、動的メモリ確保の便利なところですね!

動的メモリ確保の注意点(NULLチェックとメモリ解放忘れ)

動的メモリ確保は便利ですが、いくつか落とし穴もあります。

まず、`malloc` はメモリ確保に失敗することがあります。例えば、システムのメモリが足りない時などですね。

失敗した場合、`malloc` は `NULL` (ヌルポインタ、無効なアドレスを示す特別な値) を返します。

もし `NULL` が返ってきたのに、それに気づかずアクセスしようとすると…プログラムは即座にクラッシュ!なんてことになりかねません。

だから、`malloc` の後は必ず `NULL` かどうかチェックする習慣をつけましょう!

p_data = (int *)malloc(sizeof(int) * 10);
if (p_data == NULL) {
  // メモリ確保失敗時の処理 (エラーメッセージ表示、プログラム終了など)
  perror("メモリ確保失敗"); // エラー理由を表示する関数
  exit(EXIT_FAILURE); // プログラムを異常終了させる
}
// ここに来る = メモリ確保成功!安心して使える

もう一つの超重要な注意点は、確保したメモリは必ず `free` で解放することです。

解放を忘れると、そのメモリ領域はプログラムが終了するまで(あるいはもっと長く)誰にも使われないまま占有され続けます。これが「メモリリーク」。

小さなプログラムなら問題にならないかもしれませんが、長時間動くプログラムや、何度もメモリ確保・解放を繰り返すプログラムでは、じわじわとメモリを食いつぶし、最終的にはシステム全体の動作を不安定にさせる原因になります。

「借りたら返す!」これはメモリ管理の鉄則です!
ちなみに、`free` した後のポインタに再度 `free` をかける「二重解放」も、プログラムがクラッシュする原因になるので気をつけましょう。

C言語のポインタ応用(配列とポインタの関係を深掘り)

C言語では、配列とポインタは非常に密接な関係にあります。

実は、配列の名前は、多くの場合、その配列の先頭要素のアドレスを指すポインタ定数として扱われるんです。

例えば、`int array[10];` と宣言した場合、`array` という名前は `&array[0]` と同じアドレスを示すことになります。(一部例外はありますが、基本はこの理解でOK!)
この性質を利用すると、ポインタを使って配列の要素にアクセスできます。

int array[5] = {10, 20, 30, 40, 50};
int *p = array; // 配列名をポインタに代入 ( &array[0] を代入するのと同じ )

printf("array[0] = %d\n", array[0]);
printf("*(p + 0) = %d\n", *(p + 0)); // p[0] と同じ

printf("array[1] = %d\n", array[1]);
printf("*(p + 1) = %d\n", *(p + 1)); // p[1] と同じ

printf("array[2] = %d\n", array[2]);
printf("*(p + 2) = %d\n", *(p + 2)); // p[2] と同じ

`*(p + i)` という書き方は、ポインタ `p` が指すアドレスから `i` 要素分だけ進んだアドレスの値を取り出す、という意味です。

面白いのは、`p + i` の計算は、ちゃんと `p` の型(この場合は `int` 型)のサイズを考慮してくれる点です。`int` が4バイトなら、`p + 1` はアドレスを4バイト分進めてくれる、というわけです。

ポインタ演算による配列アクセス実践

このポインタ演算を使うと、ループで配列を処理するコードを、添え字を使わずに書くこともできます。
ポインタ変数自体をインクリメント (`p++`) していく方法がよく使われます。

【書き方】

#include <stdio.h>

int main(void) {
  int numbers[5] = {11, 22, 33, 44, 55};
  int *p_num;
  int i;

  // ポインタp_numを配列の先頭アドレスで初期化
  p_num = numbers; // p_num = &numbers[0]; と同じ

  printf("ポインタ演算でアクセス:\n");
  for (i = 0; i < 5; i++) {
    // p_numが指すアドレスの値を表示し、その後p_numのアドレスを次に進める
    printf("要素%d: 値 = %d, アドレス = %p\n", i, *p_num, p_num);
    p_num++; // ポインタを次の要素へ進める!
  }

  // 参考:添え字でアクセスする場合
  printf("\n添え字でアクセス:\n");
  for (i = 0; i < 5; i++) {
    printf("要素%d: 値 = %d, アドレス = %p\n", i, numbers[i], &numbers[i]);
  }

  // p_numはループ後、配列の範囲外を指しているので注意!
  // 再度使う場合は、 p_num = numbers; のように初期化し直す必要がある

  return 0;
}

【実行結果】(アドレスは環境によって異なります)

ポインタ演算でアクセス:
要素0: 値 = 11, アドレス = 0x7ffeeabc1a40
要素1: 値 = 22, アドレス = 0x7ffeeabc1a44
要素2: 値 = 33, アドレス = 0x7ffeeabc1a48
要素3: 値 = 44, アドレス = 0x7ffeeabc1a4c
要素4: 値 = 55, アドレス = 0x7ffeeabc1a50

添え字でアクセス:
要素0: 値 = 11, アドレス = 0x7ffeeabc1a40
要素1: 値 = 22, アドレス = 0x7ffeeabc1a44
要素2: 値 = 33, アドレス = 0x7ffeeabc1a48
要素3: 値 = 44, アドレス = 0x7ffeeabc1a4c
要素4: 値 = 55, アドレス = 0x7ffeeabc1a50

ポインタ `p_num` がインクリメントされるたびに、アドレスが `int` 型のサイズ(この環境では4バイト)ずつ増えているのが分かりますね。

添え字を使う方法と結果は同じですが、ポインタ演算の方が、よりメモリ上の動きを意識したコードになります。

C言語のポインタ応用(関数ポインタを使いこなす)

ポインタは変数のアドレスだけでなく、なんと関数のアドレスも格納できるんです!
これを「関数ポインタ」と呼びます。
関数ポインタを使うと、まるで変数のように関数を扱えるようになります。
どんな時に便利かというと、

  • 状況に応じて呼び出す関数を切り替えたい時
  • ある関数(例:ソート関数)の動作の一部を、別の関数(例:比較関数)でカスタマイズしたい時(コールバック関数)
  • 関数をデータ構造の一部として保持したい時

など、プログラムの柔軟性を高めたい場合に役立ちます。
ちょっと難しそうに聞こえるかもしれませんが、使えるようになるとプログラミングの幅がグッと広がります。

関数ポインタの宣言と使い方(サンプル付き)

関数ポインタの宣言は、ちょっとクセがあります。
`戻り値の型 (*ポインタ変数名)(引数の型リスト);` という形になります。
カッコの付け方がポイントです。

【書き方】簡単な四則演算を関数ポインタで切り替える例

#include <stdio.h>

// 関数のプロトタイプ宣言
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);

int main(void) {
  // 関数ポインタの宣言 (int型の引数を2つ取り、int型の戻り値を返す関数へのポインタ)
  int (*func_ptr)(int, int);
  int result;
  int x = 10, y = 5;

  // ① 関数ポインタにadd関数のアドレスを代入
  func_ptr = add; // &add と書いてもOK
  result = func_ptr(x, y); // 関数ポインタ経由でadd関数を呼び出す
  printf("add(%d, %d) = %d\n", x, y, result);

  // ② 関数ポインタにsubtract関数のアドレスを代入
  func_ptr = subtract;
  result = func_ptr(x, y); // 今度はsubtract関数が呼び出される
  printf("subtract(%d, %d) = %d\n", x, y, result);

  // ③ 関数ポインタにmultiply関数のアドレスを代入
  func_ptr = multiply;
  result = func_ptr(x, y); // 今度はmultiply関数が呼び出される
  printf("multiply(%d, %d) = %d\n", x, y, result);

  // 同じ func_ptr(x, y) という呼び出し方で、中身の関数を切り替えられた!
  return 0;
}

// 関数の定義
int add(int a, int b) {
  return a + b;
}

int subtract(int a, int b) {
  return a - b;
}

int multiply(int a, int b) {
  return a * b;
}

【実行結果】

add(10, 5) = 15
subtract(10, 5) = 5
multiply(10, 5) = 50

このように、同じ `func_ptr(x, y)` という呼び出し方なのに、`func_ptr` に代入されている関数のアドレスによって、実際に実行される処理が変わっていますね。
これが関数ポインタの面白いところです。

C言語のポインタ応用(ポインタのポインタ(ダブルポインタ))

さあ、ラスボス感のある「ポインタのポインタ」です!

これは、その名の通り「ポインタ変数のアドレス」を格納するためのポインタです。
`int` 型の変数を指すポインタが `int *` なら、`int *` 型の変数を指すポインタは `int **` と、`*` が一つ増えます。

どんな時に使うのでしょうか? 代表的なのは、

  • 関数の中で、呼び出し元で使われているポインタ変数が指すアドレス自体を変更したい時
  • 文字列の配列(実体は `char *` の配列)を扱いたい時

などです。

ポインタのポインタの具体例:関数でポインタを変更

よくある使い方が、「関数内でメモリを動的に確保して、そのアドレスを呼び出し元のポインタに設定する」というパターンです。

普通のポインタ(シングルポインタ)を関数の引数に渡しても、関数内でそのポインタに別のアドレスを代入しただけでは、呼び出し元のポインタ変数は変わりません(値渡しのため)。

呼び出し元のポインタ変数自体を変更するには、そのポインタ変数のアドレス、つまりダブルポインタを渡す必要があります。

【書き方】

#include <stdio.h>
#include <stdlib.h>

// ダブルポインタを使って、関数内でメモリ確保し、そのアドレスを返す関数
// 引数: int **ptr (呼び出し元の int* 型変数のアドレス)
//       int value (確保したメモリに入れる値)
// 戻り値: 成功したら0, 失敗したら-1
int allocateMemory(int **ptr, int value) {
  // int型のサイズ分メモリを確保し、呼び出し元のポインタ変数にアドレスを設定
  *ptr = (int *)malloc(sizeof(int)); // *ptr で呼び出し元のポインタ変数にアクセス!

  if (*ptr == NULL) { // メモリ確保失敗チェック
    perror("メモリ確保失敗 in allocateMemory");
    return -1;
  }

  // 確保したメモリ領域に値を設定
  **ptr = value; // **ptr で確保したメモリ領域の値にアクセス!

  return 0; // 成功
}

int main(void) {
  int *p_data = NULL; // 最初はNULLで初期化しておくのが安全

  printf("allocateMemory呼び出し前: p_data = %p\n", p_data);

  // allocateMemory関数に p_data のアドレス (&p_data) を渡す
  if (allocateMemory(&p_data, 123) == 0) {
    // 成功した場合
    printf("allocateMemory呼び出し後: p_data = %p\n", p_data);
    if (p_data != NULL) {
      printf("p_dataが指す値: %d\n", *p_data);
      // 忘れずに解放!
      free(p_data);
      p_data = NULL;
    }
  } else {
    printf("メモリ確保処理中にエラーが発生しました。\n");
  }

  return 0;
}

【実行結果】(アドレスは環境によって異なります)

allocateMemory呼び出し前: p_data = (nil)
allocateMemory呼び出し後: p_data = 0x13a9c20
p_dataが指す値: 123

`allocateMemory` 関数に `p_data` のアドレス `&p_data` を渡しています。関数内では、引数 `ptr` は `&p_data` を受け取ります。
`*ptr` は、`ptr` が指すアドレスにあるもの、つまり `main` 関数内の `p_data` 変数そのものを意味します。

`*ptr = malloc(...)` とすることで、`main` 関数内の `p_data` に確保したメモリアドレスが代入されるわけです。

そして `**ptr` は、`p_data` が指す先のメモリアドレスの値、つまり確保した領域そのものにアクセスすることになります。
ややこしいですが、図を描きながら考えると理解が進むはずです!

C言語ポインタ応用における重要注意点まとめ

ポインタは強力ですが、使い方を間違えると途端に危険なバグを生み出します。
これまでも触れてきましたが、応用的な使い方をする上で特に注意したい点をまとめておきます。

  • NULLポインタチェックは絶対!
    • `malloc` の戻り値や、ポインタを使う前には `NULL` でないか確認する癖をつけましょう。`NULL` を参照すると高確率でクラッシュします。
  • メモリ解放 (`free`) は確実に!
    • `malloc` などで確保したメモリは、使い終わったら必ず `free` しましょう。メモリリークは後々大きな問題になります。
  • 二重解放はダメ!
    • 一度 `free` したポインタを再度 `free` しないように気をつけましょう。クラッシュの原因です。解放後に `NULL` を代入しておくと、間違って再解放するリスクを減らせます。
  • ダングリングポインタに注意!
    • 解放済みのメモリ領域を指したままのポインタ(ダングリングポインタ)を使わないようにしましょう。予期せぬ動作やクラッシュの原因です。解放後は `NULL` 代入が有効です。
  • 未初期化ポインタは危険!
    • 宣言しただけのポインタは、どこを指しているか分かりません(不定値)。いきなり `*p` などでアクセスするのは絶対にやめましょう。必ず有効なアドレスを代入するか、`NULL` で初期化します。
  • ポインタ演算の範囲超過に注意!
    • 配列などをポインタで操作する際、確保したメモリ範囲を超えてアクセスしないように細心の注意を払いましょう。他のデータを壊したり、クラッシュしたりします。

これらの注意点を守ることで、ポインタを安全かつ有効に活用できます。

【まとめ】C言語ポインタ応用をマスターして可能性を広げよう

お疲れさまでした!

今回は、C言語のポインタ応用編として、基本的な使い方から一歩進んだテクニックを見てきました。

  • 必要な時にメモリを確保する「動的メモリ確保」 (`malloc`, `free`)
  • 配列とポインタの深い関係と「ポインタ演算」
  • 関数を変数のように扱う「関数ポインタ」
  • ポインタのアドレスを扱う「ポインタのポインタ」

これらの応用テクニックをマスターすれば、C言語でできることの幅が格段に広がります。
最初は少し難しく感じるかもしれませんが、サンプルコードを自分で打ち込んで動かしてみたり、値を色々変えて試してみたりすることで、理解が深まっていくはずです。

ポインタは、C言語の面白さであり、強力さの源泉です。

ぜひ、恐れずにどんどん使ってみて、ポインタと仲良くなってくださいね!

【関連記事】

>> C言語とは?特徴・できること・学ぶメリット

このブログを検索

  • ()

自己紹介

自分の写真
リモートワークでエンジニア兼Webディレクターとして活動しています。プログラミングやAIなど、日々の業務や学びの中で得た知識や気づきをわかりやすく発信し、これからITスキルを身につけたい人にも役立つ情報をお届けします。 note → https://note.com/yurufuri X → https://x.com/mnao111

QooQ