【初心者から中級者向け】C言語のポインタ変数を完全攻略!図解・コード例付きでやさしく解説

目次

1. はじめに

C言語を学ぶうえで避けて通れないのが「ポインタ変数」の理解です。初心者にとっては、「アドレス」や「間接参照」といった概念が難解に感じられるかもしれませんが、ポインタはC言語の根幹をなす重要な要素であり、使いこなせるようになることで、より高度なプログラミングが可能になります。

本記事では、「ポインタ変数とは何か?」という基礎から、実践的なコード例、さらには配列や関数との関係、応用的な使い方まで、段階的に丁寧に解説していきます。専門用語の意味や動作のイメージをつかみやすいよう、図解やサンプルコードも交えながら進めていきますので、C言語にまだ慣れていない方でも安心して読み進めていただけます。

ポインタは、慣れてしまえば非常に強力で便利な機能です。動的なメモリ操作や、関数への値の受け渡しなど、プログラムの幅を大きく広げてくれます。この記事を通じて、ポインタ変数に対する理解を深め、C言語での開発スキルを一段階高めていただければ幸いです。

2. ポインタ変数とは何か?

ポインタ変数の基本概念

C言語におけるポインタ変数とは、「メモリ上のアドレスを格納する変数」のことです。通常の変数がデータそのものを格納するのに対し、ポインタは別の変数が格納されている場所(アドレス)を記憶します

たとえば、次のようなコードを考えてみましょう。

int a = 10;
int *p = &a;

この場合、変数aには値10が格納され、pにはaのアドレスが格納されます。*pと書くことで、pが指すアドレスにある値(この場合は10)を間接的に参照できます。

アドレスとメモリの関係

すべてのデータはコンピュータのメモリ上に格納されています。そして、メモリには1バイトごとにアドレスが振られています。ポインタは、このアドレス情報を使って「どのメモリ位置を操作するか」を指定するための手段です。

ポインタを使うことで、以下のようなことが可能になります。

  • 関数間で変数の中身を直接書き換える
  • 配列の要素を柔軟に操作する
  • ヒープメモリを用いた動的メモリ管理

つまり、ポインタはC言語の柔軟性と低レベル制御力を支える重要な仕組みなのです。

ポインタ変数の宣言と初期化

ポインタ変数は、対象となるデータ型にアスタリスク(*)を付けて宣言します。

int *p;   // int型の変数を指すポインタ
char *c;  // char型の変数を指すポインタ

そして、通常は&演算子を使って、他の変数のアドレスを代入します。

int a = 5;
int *p = &a;  // aのアドレスをpに格納

ここで重要なのは、「ポインタの型」と「ポインタが指す値の型」は一致している必要があるという点です。int型を指すポインタにchar型のアドレスを格納すると、動作が保証されない場合があります。

侍エンジニア塾

3. ポインタの基本操作

ポインタ変数の基本を理解したら、次は「どうやって使うのか?」を具体的に見ていきましょう。ここでは、ポインタ操作に欠かせない演算子や、値の読み書き、ポインタ同士の演算といった基本的な操作方法を紹介します。

アドレス演算子(&)と間接演算子(*)

アドレス演算子(&)

&は「アドレス演算子」と呼ばれ、変数のメモリ上のアドレスを取得するために使います。

int a = 10;
int *p = &a;

この例では、変数aのアドレスをpに格納しています。pには「aが格納されている場所」が保存されるわけです。

間接演算子(*)

*は「間接演算子」または「参照演算子」と呼ばれ、ポインタが指しているアドレスの中身を参照または変更するときに使います。

int a = 10;
int *p = &a;

printf("%d
", *p);  // 結果: 10

このように、*pと書くことで、aの値(中身)を間接的に取得できます。逆に、次のように書けば値の変更も可能です。

*p = 20;
printf("%d
", a);  // 結果: 20

値の取得と書き換え:ポインタの活用例

ポインタを使うことで、別の関数から変数の値を直接変更することができます。以下はその基本例です。

void updateValue(int *p) {
    *p = 100;
}

int main() {
    int num = 50;
    updateValue(&num);
    printf("%d
", num);  // 結果: 100
    return 0;
}

このように、関数内で値を更新する際にもポインタは活躍します。C言語では、関数に値を渡すときは基本的にコピー(値渡し)となりますが、ポインタを使えば元の値自体を操作することが可能になります。

ポインタの加算と減算

ポインタは、加算・減算が可能です。これは配列や連続するメモリ領域を扱う際に非常に便利です。

int arr[3] = {10, 20, 30};
int *p = arr;

printf("%d
", *p);     // 10
p++;
printf("%d
", *p);     // 20

ここで重要なのは、「p++」とすると、次のint型変数のアドレスへ移動するという点です。int型が4バイトなら、p++はアドレス的には「+4」されます。

このように、ポインタの基本操作を理解することは、C言語におけるメモリ操作の基盤を築く第一歩です。次章では、ポインタと配列の関係についてさらに詳しく見ていきましょう。

4. 配列とポインタの関係

C言語において、配列とポインタは非常に密接な関係にあります。初心者にとっては混乱しやすいポイントでもありますが、この関係を理解することで、より柔軟かつ効率的な配列操作が可能になります。

配列名はポインタのように扱える

C言語では、配列名は先頭要素のアドレスを指すポインタとして扱われます。たとえば次のようなコードを見てみましょう。

int arr[3] = {10, 20, 30};
printf("%d
", *arr);     // 結果: 10

このとき、arr&arr[0]と同じアドレスを指しており、*arrは配列の最初の要素(arr[0])を意味します。

ポインタを使った配列のアクセス

配列はインデックスでアクセスできますが、ポインタを使っても同様の操作が可能です。

int arr[3] = {10, 20, 30};
int *p = arr;

printf("%d
", p[1]);     // 結果: 20

ここでは、p[1]*(p + 1)と同じ意味になります。つまり、ポインタを使えば次のようにも書けます。

printf("%d
", *(arr + 2));   // 結果: 30

このように、インデックス表記とポインタ演算は本質的に同じことをしています

ポインタ演算と配列インデックスの違い

配列とポインタは似ていますが、完全に同じではないことにも注意が必要です。

1. サイズの取得

配列名を使うと、そのサイズをsizeofで取得できますが、ポインタに代入した時点でサイズは失われます。

int arr[5];
int *p = arr;

printf("%zu
", sizeof(arr)); // 結果: 20(5×4バイト)
printf("%zu
", sizeof(p));   // 結果: 8(64bit環境ではポインタは8バイト)

2. 書き換えの可否

配列名は定数ポインタのようなものであり、代入による変更はできません

int arr[3];
int *p = arr;
p = p + 1;     // OK
arr = arr + 1; // エラー(配列名は再代入不可)

ポインタと配列を使いこなすメリット

  • ポインタを使えば、メモリ空間を柔軟に操作できる
  • 配列処理が高速・効率的になる(インデックスよりもポインタ演算の方がわずかに速い場合も)
  • 関数に配列を渡す際、実態ではなく先頭アドレスのみ渡されるので、ポインタとしての知識は必須

5. 関数とポインタ

C言語では、関数に変数を渡す際、値渡し(call by value)が基本です。そのため、関数内で引数を変更しても、元の変数には影響しません。しかし、ポインタを使うことで関数から元の変数の値を直接操作することが可能になります。

この章では、関数とポインタの関係、値の書き換え方法、関数ポインタの基本など、関数におけるポインタの使い方を解説します。

ポインタを使った値の書き換え

まず、関数内から変数の値を変更したい場合、ポインタを使う必要があります。

例:ポインタなしの場合

void update(int x) {
    x = 100;
}

int main() {
    int a = 10;
    update(a);
    printf("%d
", a); // 結果: 10(変更されない)
    return 0;
}

この場合、関数にはaのコピーが渡されているため、a自体は変更されません。

例:ポインタを使う場合

void update(int *x) {
    *x = 100;
}

int main() {
    int a = 10;
    update(&a);
    printf("%d
", a); // 結果: 100(変更される)
    return 0;
}

このように、変数のアドレスを関数に渡すことで、元の値を変更することができます。これは「参照渡し(call by reference)」のテクニックとして広く使われています。

配列と関数の関係

C言語では、配列を関数に渡すと自動的にポインタとして扱われます。つまり、配列の先頭要素のアドレスが関数に渡されるため、関数内で内容の変更が可能です。

void setValues(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] = i * 10;
    }
}

int main() {
    int nums[3];
    setValues(nums, 3);
    printf("%d
", nums[1]); // 結果: 10
    return 0;
}

この例のように、配列名をそのまま渡すだけで関数内から内容を変更できます。

関数ポインタの基本

C言語では、関数のアドレスを変数に格納して呼び出すこともできます。これが関数ポインタです。

宣言と使用例:

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*funcPtr)(int, int); // int型を返し、int型を2つ引数に取る関数ポインタ
    funcPtr = add;

    int result = funcPtr(3, 4);
    printf("%d
", result); // 結果: 7
    return 0;
}

関数ポインタは、動的に関数を選んで実行したいときや、コールバック関数の実装などに使われます。関数名だけでも関数のアドレスを取得できるため、柔軟なプログラミングが可能です。

実践的な使用場面

  • 配列の並べ替え(ソート)において、比較関数をポインタで渡す
  • メニュー選択型プログラムで、選択肢ごとに実行する関数を関数ポインタで管理
  • イベント駆動型処理やコールバック関数の実装(GUI、組み込み、ゲーム開発など)

6. ポインタの応用例

ポインタの基本的な使い方を理解したら、次は応用的な活用方法を学んでいきましょう。C言語のポインタは、動的なメモリ操作や高度なデータ構造の実装に不可欠です。この章では、実務でも頻繁に使われる応用的なテクニックを3つ紹介します。

動的メモリ確保(mallocfree

C言語では、実行時に必要なメモリを動的に確保することができます。これを可能にするのが、標準ライブラリのmalloc関数です。確保されたメモリはポインタで参照され、使い終わったらfreeで解放する必要があります。

例:整数を動的に確保する

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

int main() {
    int *p = (int *)malloc(sizeof(int));  // int型1つ分のメモリを確保
    if (p == NULL) {
        printf("メモリ確保に失敗しました
");
        return 1;
    }

    *p = 123;
    printf("%d
", *p);  // 結果: 123

    free(p);  // メモリを解放
    return 0;
}

注意点:

  • mallocは確保されたメモリの先頭アドレスを返す
  • 戻り値は必ずNULLチェック
  • メモリは使い終わったらfreeで必ず解放

このように、必要なタイミングで必要なだけメモリを確保できるのが、ポインタを使った動的メモリ管理の魅力です。

ポインタのポインタ(二重ポインタ)

ポインタのアドレスを保持するポインタ、つまり「ポインタのポインタ」もC言語ではよく使われます。特に、関数内でポインタを変更したい場合や、2次元配列の操作などで活躍します。

例:関数内でポインタを初期化する

void allocate(int **pp) {
    *pp = (int *)malloc(sizeof(int));
    if (*pp != NULL) {
        **pp = 42;
    }
}

int main() {
    int *p = NULL;
    allocate(&p);
    printf("%d
", *p);  // 結果: 42
    free(p);
    return 0;
}

よく使われる場面:

  • 配列の配列(2次元配列)
  • 構造体の中で可変数の配列を持つとき
  • 複数の文字列を扱う(char *argv など)

関数ポインタと配列の組み合わせ

関数ポインタを配列として保持し、状況に応じて動的に関数を切り替えて呼び出すといった高度なテクニックも、C言語では一般的です。

例:簡単なメニュー選択

#include <stdio.h>

void hello() { printf("Hello
"); }
void bye()   { printf("Goodbye
"); }

int main() {
    void (*funcs[2])() = {hello, bye};

    int choice = 0;
    printf("0: hello, 1: bye > ");
    scanf("%d", &choice);

    if (choice >= 0 && choice < 2) {
        funcs[choice]();  // 関数呼び出し
    }
    return 0;
}

このような設計は、状態管理やイベント駆動型プログラムでも役立ちます。

ポインタを使った応用的なテクニックは、単なる記述力以上に、メモリや実行制御を意識した設計力が求められます。しかし、使いこなせるようになれば、C言語の力を最大限に引き出すことができるようになります。

7. よくあるエラーとその対処法

ポインタは非常に強力な機能ですが、扱いを間違えるとバグやセキュリティホールの原因にもなります。この章では、C言語におけるポインタの使用時によく発生するエラーと、それを防ぐための対策について解説します。

未初期化ポインタの使用

最も基本的でありながら危険なのが、未初期化のポインタを使用するケースです。ポインタは宣言しただけでは有効なアドレスを指していません。

悪い例:

int *p;
*p = 10;  // 未定義動作!pはどこも指していない

対策:

  • ポインタは必ず初期化してから使う
  • 使用前にNULLチェックを行う
int *p = NULL;
// 使用前にメモリ確保または有効なアドレスを代入

NULLポインタの逆参照

ポインタがNULLを指している状態で*pを行うと、プログラムはクラッシュします。これは非常に一般的なバグです。

例:

int *p = NULL;
printf("%d
", *p);  // 実行時エラー(セグメンテーションフォールトなど)

対策:

  • ポインタがNULLでないか確認してから使用
if (p != NULL) {
    printf("%d
", *p);
}

メモリリーク

動的に確保したメモリをfreeし忘れると、メモリが解放されずに蓄積される「メモリリーク」が発生します。長時間動作するプログラムや埋め込み系では致命的です。

例:

int *p = (int *)malloc(sizeof(int));
// 処理を終えてもfreeしない → メモリリーク

対策:

  • 使用が終わったら必ずfreeする
  • mallocfreeの対応を意識する
  • 開発中はメモリリーク検出ツール(例:Valgrind)を活用

ダングリングポインタ

freeした後のメモリを指しているポインタは“ダングリングポインタ”と呼ばれ、再利用すると未定義動作を引き起こします。

例:

int *p = (int *)malloc(sizeof(int));
free(p);
*p = 123;  // エラー!すでに解放されたメモリにアクセス

対策:

  • freeの後は必ずNULLを代入して無効化する
free(p);
p = NULL;

配列外アクセス

ポインタを使ったインデックス演算で、意図せず配列の境界を超えてしまうことがあります。これも非常に危険で、バグや脆弱性の原因となります。

例:

int arr[3] = {1, 2, 3};
printf("%d
", *(arr + 3));  // 未定義動作(arr[3]は存在しない)

対策:

  • 常に有効な範囲内でアクセスしているかを確認
  • ループ処理では「境界チェック」を徹底する

同じポインタを二重に解放する

同じメモリアドレスをfreeする操作を2回行うと、プログラムがクラッシュする危険があります。

対策:

  • free後のポインタをNULLにすることで、二重解放を防止
free(p);
p = NULL;

これらのエラーは、基本を守って丁寧にコーディングすることで防止可能です。特に初心者のうちは、「初期化」「NULLチェック」「freeの徹底」をルールとして守ることが、バグのないコードにつながります。

8. まとめ

C言語において、ポインタ変数は最も基本でありながら奥が深い重要な要素です。本記事では、「ポインタとは何か?」という基礎から、配列・関数・メモリ管理・関数ポインタといった応用例までを段階的に解説してきました。

学んだポイントの振り返り

  • ポインタ変数は、データのアドレスを格納する変数であり、*(間接演算子)と&(アドレス演算子)によって操作される
  • 配列とポインタは密接に関連しており、配列名は先頭アドレスを示すポインタとして扱える
  • 関数とポインタを組み合わせれば、関数内で変数を直接操作する「参照渡し」が可能になり、関数ポインタを使うことで柔軟な関数呼び出しも実現できる
  • 動的メモリ管理(malloc/free)二重ポインタといったテクニックは、より実践的で柔軟なプログラム設計を支える
  • 一方で、未初期化ポインタ・NULL参照・メモリリーク・ダングリングポインタなど、ポインタ特有のエラーも多く、慎重な扱いが求められる

初心者へのアドバイス

ポインタは「難しい」「怖い」といった印象を持たれがちですが、それはブラックボックスのまま使ってしまっているからです。アドレスの意味やメモリの仕組みをしっかり理解することで、不安は自信へと変わります

以下のような手順で学びを定着させるとよいでしょう:

  • サンプルコードを紙と図で手書きで追ってみる
  • printfでアドレスや値を可視化して確認する
  • Valgrindなどのメモリチェックツールを活用する
  • 小さなポインタ操作の練習プログラムを複数書いてみる

次のステップへ

本記事で紹介した内容は、C言語のポインタに関する基礎から中級レベルまでをカバーしています。今後さらに理解を深めるためには、以下のようなテーマに進むとよいでしょう。

  • 構造体とポインタ
  • ポインタを使った文字列操作
  • ファイル入出力とポインタ
  • 多次元配列の操作
  • 関数ポインタを用いたコールバック設計

ポインタを理解することで、C言語の本当の面白さや力強さを実感できるようになります。最初は戸惑うこともあるかもしれませんが、少しずつ確実に理解を積み重ねていきましょう。この記事がその一助となれば幸いです。

1. はじめに

C言語を学ぶうえで避けて通れないのが「ポインタ変数」の理解です。初心者にとっては、「アドレス」や「間接参照」といった概念が難解に感じられるかもしれませんが、ポインタはC言語の根幹をなす重要な要素であり、使いこなせるようになることで、より高度なプログラミングが可能になります。

本記事では、「ポインタ変数とは何か?」という基礎から、実践的なコード例、さらには配列や関数との関係、応用的な使い方まで、段階的に丁寧に解説していきます。専門用語の意味や動作のイメージをつかみやすいよう、図解やサンプルコードも交えながら進めていきますので、C言語にまだ慣れていない方でも安心して読み進めていただけます。

ポインタは、慣れてしまえば非常に強力で便利な機能です。動的なメモリ操作や、関数への値の受け渡しなど、プログラムの幅を大きく広げてくれます。この記事を通じて、ポインタ変数に対する理解を深め、C言語での開発スキルを一段階高めていただければ幸いです。

2. ポインタ変数とは何か?

ポインタ変数の基本概念

C言語におけるポインタ変数とは、「メモリ上のアドレスを格納する変数」のことです。通常の変数がデータそのものを格納するのに対し、ポインタは別の変数が格納されている場所(アドレス)を記憶します

たとえば、次のようなコードを考えてみましょう。

int a = 10;
int *p = &a;

この場合、変数aには値10が格納され、pにはaのアドレスが格納されます。*pと書くことで、pが指すアドレスにある値(この場合は10)を間接的に参照できます。

アドレスとメモリの関係

すべてのデータはコンピュータのメモリ上に格納されています。そして、メモリには1バイトごとにアドレスが振られています。ポインタは、このアドレス情報を使って「どのメモリ位置を操作するか」を指定するための手段です。

ポインタを使うことで、以下のようなことが可能になります。

  • 関数間で変数の中身を直接書き換える
  • 配列の要素を柔軟に操作する
  • ヒープメモリを用いた動的メモリ管理

つまり、ポインタはC言語の柔軟性と低レベル制御力を支える重要な仕組みなのです。

ポインタ変数の宣言と初期化

ポインタ変数は、対象となるデータ型にアスタリスク(*)を付けて宣言します。

int *p;   // int型の変数を指すポインタ
char *c;  // char型の変数を指すポインタ

そして、通常は&演算子を使って、他の変数のアドレスを代入します。

int a = 5;
int *p = &a;  // aのアドレスをpに格納

ここで重要なのは、「ポインタの型」と「ポインタが指す値の型」は一致している必要があるという点です。int型を指すポインタにchar型のアドレスを格納すると、動作が保証されない場合があります。

3. ポインタの基本操作

ポインタ変数の基本を理解したら、次は「どうやって使うのか?」を具体的に見ていきましょう。ここでは、ポインタ操作に欠かせない演算子や、値の読み書き、ポインタ同士の演算といった基本的な操作方法を紹介します。

アドレス演算子(&)と間接演算子(*)

アドレス演算子(&)

&は「アドレス演算子」と呼ばれ、変数のメモリ上のアドレスを取得するために使います。

int a = 10;
int *p = &a;

この例では、変数aのアドレスをpに格納しています。pには「aが格納されている場所」が保存されるわけです。

間接演算子(*)

*は「間接演算子」または「参照演算子」と呼ばれ、ポインタが指しているアドレスの中身を参照または変更するときに使います。

int a = 10;
int *p = &a;

printf("%d
", *p);  // 結果: 10

このように、*pと書くことで、aの値(中身)を間接的に取得できます。逆に、次のように書けば値の変更も可能です。

*p = 20;
printf("%d
", a);  // 結果: 20

値の取得と書き換え:ポインタの活用例

ポインタを使うことで、別の関数から変数の値を直接変更することができます。以下はその基本例です。

void updateValue(int *p) {
    *p = 100;
}

int main() {
    int num = 50;
    updateValue(&num);
    printf("%d
", num);  // 結果: 100
    return 0;
}

このように、関数内で値を更新する際にもポインタは活躍します。C言語では、関数に値を渡すときは基本的にコピー(値渡し)となりますが、ポインタを使えば元の値自体を操作することが可能になります。

ポインタの加算と減算

ポインタは、加算・減算が可能です。これは配列や連続するメモリ領域を扱う際に非常に便利です。

int arr[3] = {10, 20, 30};
int *p = arr;

printf("%d
", *p);     // 10
p++;
printf("%d
", *p);     // 20

ここで重要なのは、「p++」とすると、次のint型変数のアドレスへ移動するという点です。int型が4バイトなら、p++はアドレス的には「+4」されます。

このように、ポインタの基本操作を理解することは、C言語におけるメモリ操作の基盤を築く第一歩です。次章では、ポインタと配列の関係についてさらに詳しく見ていきましょう。

4. 配列とポインタの関係

C言語において、配列とポインタは非常に密接な関係にあります。初心者にとっては混乱しやすいポイントでもありますが、この関係を理解することで、より柔軟かつ効率的な配列操作が可能になります。

配列名はポインタのように扱える

C言語では、配列名は先頭要素のアドレスを指すポインタとして扱われます。たとえば次のようなコードを見てみましょう。

int arr[3] = {10, 20, 30};
printf("%d
", *arr);     // 結果: 10

このとき、arr&arr[0]と同じアドレスを指しており、*arrは配列の最初の要素(arr[0])を意味します。

ポインタを使った配列のアクセス

配列はインデックスでアクセスできますが、ポインタを使っても同様の操作が可能です。

int arr[3] = {10, 20, 30};
int *p = arr;

printf("%d
", p[1]);     // 結果: 20

ここでは、p[1]*(p + 1)と同じ意味になります。つまり、ポインタを使えば次のようにも書けます。

printf("%d
", *(arr + 2));   // 結果: 30

このように、インデックス表記とポインタ演算は本質的に同じことをしています

ポインタ演算と配列インデックスの違い

配列とポインタは似ていますが、完全に同じではないことにも注意が必要です。

1. サイズの取得

配列名を使うと、そのサイズをsizeofで取得できますが、ポインタに代入した時点でサイズは失われます。

int arr[5];
int *p = arr;

printf("%zu
", sizeof(arr)); // 結果: 20(5×4バイト)
printf("%zu
", sizeof(p));   // 結果: 8(64bit環境ではポインタは8バイト)

2. 書き換えの可否

配列名は定数ポインタのようなものであり、代入による変更はできません

int arr[3];
int *p = arr;
p = p + 1;     // OK
arr = arr + 1; // エラー(配列名は再代入不可)

ポインタと配列を使いこなすメリット

  • ポインタを使えば、メモリ空間を柔軟に操作できる
  • 配列処理が高速・効率的になる(インデックスよりもポインタ演算の方がわずかに速い場合も)
  • 関数に配列を渡す際、実態ではなく先頭アドレスのみ渡されるので、ポインタとしての知識は必須

5. 関数とポインタ

C言語では、関数に変数を渡す際、値渡し(call by value)が基本です。そのため、関数内で引数を変更しても、元の変数には影響しません。しかし、ポインタを使うことで関数から元の変数の値を直接操作することが可能になります。

この章では、関数とポインタの関係、値の書き換え方法、関数ポインタの基本など、関数におけるポインタの使い方を解説します。

ポインタを使った値の書き換え

まず、関数内から変数の値を変更したい場合、ポインタを使う必要があります。

例:ポインタなしの場合

void update(int x) {
    x = 100;
}

int main() {
    int a = 10;
    update(a);
    printf("%d
", a); // 結果: 10(変更されない)
    return 0;
}

この場合、関数にはaのコピーが渡されているため、a自体は変更されません。

例:ポインタを使う場合

void update(int *x) {
    *x = 100;
}

int main() {
    int a = 10;
    update(&a);
    printf("%d
", a); // 結果: 100(変更される)
    return 0;
}

このように、変数のアドレスを関数に渡すことで、元の値を変更することができます。これは「参照渡し(call by reference)」のテクニックとして広く使われています。

配列と関数の関係

C言語では、配列を関数に渡すと自動的にポインタとして扱われます。つまり、配列の先頭要素のアドレスが関数に渡されるため、関数内で内容の変更が可能です。

void setValues(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] = i * 10;
    }
}

int main() {
    int nums[3];
    setValues(nums, 3);
    printf("%d
", nums[1]); // 結果: 10
    return 0;
}

この例のように、配列名をそのまま渡すだけで関数内から内容を変更できます。

関数ポインタの基本

C言語では、関数のアドレスを変数に格納して呼び出すこともできます。これが関数ポインタです。

宣言と使用例:

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*funcPtr)(int, int); // int型を返し、int型を2つ引数に取る関数ポインタ
    funcPtr = add;

    int result = funcPtr(3, 4);
    printf("%d
", result); // 結果: 7
    return 0;
}

関数ポインタは、動的に関数を選んで実行したいときや、コールバック関数の実装などに使われます。関数名だけでも関数のアドレスを取得できるため、柔軟なプログラミングが可能です。

実践的な使用場面

  • 配列の並べ替え(ソート)において、比較関数をポインタで渡す
  • メニュー選択型プログラムで、選択肢ごとに実行する関数を関数ポインタで管理
  • イベント駆動型処理やコールバック関数の実装(GUI、組み込み、ゲーム開発など)

6. ポインタの応用例

ポインタの基本的な使い方を理解したら、次は応用的な活用方法を学んでいきましょう。C言語のポインタは、動的なメモリ操作や高度なデータ構造の実装に不可欠です。この章では、実務でも頻繁に使われる応用的なテクニックを3つ紹介します。

動的メモリ確保(mallocfree

C言語では、実行時に必要なメモリを動的に確保することができます。これを可能にするのが、標準ライブラリのmalloc関数です。確保されたメモリはポインタで参照され、使い終わったらfreeで解放する必要があります。

例:整数を動的に確保する

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

int main() {
    int *p = (int *)malloc(sizeof(int));  // int型1つ分のメモリを確保
    if (p == NULL) {
        printf("メモリ確保に失敗しました
");
        return 1;
    }

    *p = 123;
    printf("%d
", *p);  // 結果: 123

    free(p);  // メモリを解放
    return 0;
}

注意点:

  • mallocは確保されたメモリの先頭アドレスを返す
  • 戻り値は必ずNULLチェック
  • メモリは使い終わったらfreeで必ず解放

このように、必要なタイミングで必要なだけメモリを確保できるのが、ポインタを使った動的メモリ管理の魅力です。

ポインタのポインタ(二重ポインタ)

ポインタのアドレスを保持するポインタ、つまり「ポインタのポインタ」もC言語ではよく使われます。特に、関数内でポインタを変更したい場合や、2次元配列の操作などで活躍します。

例:関数内でポインタを初期化する

void allocate(int **pp) {
    *pp = (int *)malloc(sizeof(int));
    if (*pp != NULL) {
        **pp = 42;
    }
}

int main() {
    int *p = NULL;
    allocate(&p);
    printf("%d
", *p);  // 結果: 42
    free(p);
    return 0;
}

よく使われる場面:

  • 配列の配列(2次元配列)
  • 構造体の中で可変数の配列を持つとき
  • 複数の文字列を扱う(char *argv など)

関数ポインタと配列の組み合わせ

関数ポインタを配列として保持し、状況に応じて動的に関数を切り替えて呼び出すといった高度なテクニックも、C言語では一般的です。

例:簡単なメニュー選択

#include <stdio.h>

void hello() { printf("Hello
"); }
void bye()   { printf("Goodbye
"); }

int main() {
    void (*funcs[2])() = {hello, bye};

    int choice = 0;
    printf("0: hello, 1: bye > ");
    scanf("%d", &choice);

    if (choice >= 0 && choice < 2) {
        funcs[choice]();  // 関数呼び出し
    }
    return 0;
}

このような設計は、状態管理やイベント駆動型プログラムでも役立ちます。

ポインタを使った応用的なテクニックは、単なる記述力以上に、メモリや実行制御を意識した設計力が求められます。しかし、使いこなせるようになれば、C言語の力を最大限に引き出すことができるようになります。

7. よくあるエラーとその対処法

ポインタは非常に強力な機能ですが、扱いを間違えるとバグやセキュリティホールの原因にもなります。この章では、C言語におけるポインタの使用時によく発生するエラーと、それを防ぐための対策について解説します。

未初期化ポインタの使用

最も基本的でありながら危険なのが、未初期化のポインタを使用するケースです。ポインタは宣言しただけでは有効なアドレスを指していません。

悪い例:

int *p;
*p = 10;  // 未定義動作!pはどこも指していない

対策:

  • ポインタは必ず初期化してから使う
  • 使用前にNULLチェックを行う
int *p = NULL;
// 使用前にメモリ確保または有効なアドレスを代入

NULLポインタの逆参照

ポインタがNULLを指している状態で*pを行うと、プログラムはクラッシュします。これは非常に一般的なバグです。

例:

int *p = NULL;
printf("%d
", *p);  // 実行時エラー(セグメンテーションフォールトなど)

対策:

  • ポインタがNULLでないか確認してから使用
if (p != NULL) {
    printf("%d
", *p);
}

メモリリーク

動的に確保したメモリをfreeし忘れると、メモリが解放されずに蓄積される「メモリリーク」が発生します。長時間動作するプログラムや埋め込み系では致命的です。

例:

int *p = (int *)malloc(sizeof(int));
// 処理を終えてもfreeしない → メモリリーク

対策:

  • 使用が終わったら必ずfreeする
  • mallocfreeの対応を意識する
  • 開発中はメモリリーク検出ツール(例:Valgrind)を活用

ダングリングポインタ

freeした後のメモリを指しているポインタは“ダングリングポインタ”と呼ばれ、再利用すると未定義動作を引き起こします。

例:

int *p = (int *)malloc(sizeof(int));
free(p);
*p = 123;  // エラー!すでに解放されたメモリにアクセス

対策:

  • freeの後は必ずNULLを代入して無効化する
free(p);
p = NULL;

配列外アクセス

ポインタを使ったインデックス演算で、意図せず配列の境界を超えてしまうことがあります。これも非常に危険で、バグや脆弱性の原因となります。

例:

int arr[3] = {1, 2, 3};
printf("%d
", *(arr + 3));  // 未定義動作(arr[3]は存在しない)

対策:

  • 常に有効な範囲内でアクセスしているかを確認
  • ループ処理では「境界チェック」を徹底する

同じポインタを二重に解放する

同じメモリアドレスをfreeする操作を2回行うと、プログラムがクラッシュする危険があります。

対策:

  • free後のポインタをNULLにすることで、二重解放を防止
free(p);
p = NULL;

これらのエラーは、基本を守って丁寧にコーディングすることで防止可能です。特に初心者のうちは、「初期化」「NULLチェック」「freeの徹底」をルールとして守ることが、バグのないコードにつながります。