6日でできる 新C言語入門|関数ポインタの基本と使い方

関数を変数のように選んで呼び出す。
関数ポインタを理解すると、処理の切り替えやコールバックの仕組みが見えてきます。

C言語では、変数のアドレスをポインタに保存できるように、関数の呼び出し先もポインタとして扱えます。この仕組みを関数ポインタと呼びます。

通常のポインタは、int型やchar型などのデータが置かれている場所を保存します。一方、関数ポインタは、関数が実行される場所、つまり関数のアドレスを保存します。

関数ポインタを使うと、実行する関数をあとから切り替えたり、関数を別の関数へ引数として渡したり、複数の関数を配列で管理したりできます。

最初は宣言の形が少し読みづらく感じるかもしれません。しかし、戻り値の型、ポインタ変数名、引数の型を分けて見ると、考え方はかなり整理できます。

この記事では、関数ポインタの基本、宣言の読み方、関数の切り替え、引数や戻り値がある関数の扱い、コールバックの考え方、関数ポインタ配列による管理まで、サンプルプログラムを使ってやさしく解説していきます。

関数ポインタとは何か

関数ポインタは、関数のアドレスを保存するためのポインタ変数です。

通常のポインタがデータの場所を指すのに対して、関数ポインタは関数の場所を指します。

種類指すもの
通常のポインタ変数や配列要素のアドレスint型変数の場所、配列の先頭
関数ポインタ関数のアドレス表示関数、計算関数、判定関数

たとえば、画面にメッセージを表示する関数が複数ある場合、関数ポインタに呼び出したい関数を代入しておけば、同じ呼び出し方で違う関数を実行できます。

これは、処理の内容を状況に応じて切り替えたいときにとても便利です。

関数ポインタの宣言の形

関数ポインタの基本的な宣言は、次の形です。

戻り値の型 (*変数名)(引数の型リスト);

たとえば、引数がなく、戻り値もない関数を指す関数ポインタは、次のように宣言できます。

void (*funcp)(void);

この宣言は、次のように分けて読むと分かりやすくなります。

部分意味
void指す関数の戻り値がない
(*funcp)funcpは関数を指すポインタ変数
(void)指す関数に引数がない

ここで大切なのは、*funcpを丸かっこで囲むことです。

void (*funcp)(void);

丸かっこがないと、意味が変わってしまいます。

void *funcp(void);

これは、関数ポインタ変数ではなく、void*を返す関数の宣言として読まれます。

書き方意味
void (*funcp)(void)引数なし、戻り値voidの関数を指すポインタ
void *funcp(void)引数なし、戻り値void*の関数

関数ポインタでは、丸かっこの位置がとても重要です。

図:通常のポインタと関数ポインタの違い

この図から分かること

この図から分かるのは、通常のポインタと関数ポインタでは、指している対象が違うということです。

通常のポインタは変数や配列などのデータを指します。一方、関数ポインタは関数の呼び出し先を指します。関数ポインタに別の関数を代入すると、同じ呼び出し方でも実行される処理を切り替えられます。

関数ポインタで関数を切り替えて呼び出す

まずは、引数も戻り値もない関数を関数ポインタで呼び出す例を見てみましょう。

このプログラムでは、開始メッセージを表示する関数と、終了メッセージを表示する関数を用意し、関数ポインタの指す先を切り替えて呼び出します。

プロジェクト/ファイル名: Lesson60_1/main.c

#include <stdio.h>

// 表示用の関数
void show_start(void);
void show_finish(void);

int main(void) {
    void (*messageFunc)(void) = NULL;

    // 開始メッセージを表示する関数を指す
    messageFunc = show_start;
    messageFunc();

    // 終了メッセージを表示する関数を指す
    messageFunc = show_finish;
    messageFunc();

    return 0;
}

void show_start(void) {
    printf("処理を開始します。\n");
}

void show_finish(void) {
    printf("処理を終了します。\n");
}

実行結果

処理を開始します。
処理を終了します。

このプログラムでは、messageFuncが関数ポインタです。

void (*messageFunc)(void) = NULL;

これは、引数なし、戻り値voidの関数を指せる関数ポインタです。

最初にmessageFuncへshow_startを代入しています。

messageFunc = show_start;

関数名show_startは、関数のアドレスとして扱えます。そのため、関数ポインタに代入できます。

その後、次のように呼び出します。

messageFunc();

この時点ではmessageFuncがshow_startを指しているため、show_start関数が実行されます。

次に、messageFuncへshow_finishを代入すると、呼び出し先が切り替わります。

messageFunc = show_finish;
messageFunc();

同じmessageFunc()という呼び出しでも、指している関数が変わるため、実行される処理も変わります。

関数名は関数のアドレスとして扱える

関数ポインタに関数を代入するときは、関数名を書きます。

messageFunc = show_start;

このとき、show_start()とは書きません。

show_startと書くと、関数のアドレスを表します。
show_start()と書くと、その場で関数を実行する意味になります。

書き方意味
show_start関数のアドレスとして扱う
show_start()関数を実行する
messageFunc = show_start関数ポインタに関数のアドレスを代入する
messageFunc()関数ポインタが指す関数を実行する

この違いはとても大切です。

関数ポインタへ代入したいときは、関数名だけを書きます。
関数を実行したいときは、丸かっこを付けます。

引数や戻り値がある関数ポインタ

関数ポインタは、引数や戻り値がある関数にも使えます。

ただし、関数ポインタの型は、指したい関数の戻り値の型と引数の型に合わせる必要があります。

たとえば、int型の引数を2つ受け取り、int型の値を返す関数を指す関数ポインタは、次のように宣言します。

int (*calc)(int, int);

この宣言は、次のように読めます。

部分意味
int指す関数の戻り値がint型
(*calc)calcは関数を指すポインタ変数
(int, int)指す関数はint型の引数を2つ受け取る

この型に合う関数であれば、calcに代入して呼び出せます。

計算関数を切り替えて呼び出す

次のプログラムでは、2つの整数を使った計算関数を関数ポインタで切り替えます。

プロジェクト/ファイル名: Lesson60_2/main.c

#include <stdio.h>

// 2つの整数を合計する関数
int add_score(int a, int b);

// 2つの整数の差を求める関数
int diff_score(int a, int b);

int main(void) {
    int left = 18;
    int right = 7;
    int result;
    int (*calc)(int, int) = NULL;

    // 合計を求める関数を呼び出す
    calc = add_score;
    result = calc(left, right);
    printf("%d と %d の合計は %d です。\n", left, right, result);

    // 差を求める関数へ切り替える
    calc = diff_score;
    result = calc(left, right);
    printf("%d と %d の差は %d です。\n", left, right, result);

    return 0;
}

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

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

実行結果

18 と 7 の合計は 25 です。
18 と 7 の差は 11 です。

このプログラムでは、calcが関数ポインタです。

int (*calc)(int, int) = NULL;

calcは、int型の引数を2つ受け取り、int型を返す関数を指せます。

add_scoreもdiff_scoreも、どちらも次の形に合っています。

int 関数名(int, int);

そのため、calcに代入できます。

calc = add_score;
calc = diff_score;

calc(left, right)と呼び出すと、その時点でcalcが指している関数が実行されます。

図:関数ポインタで処理を切り替える

この図から分かること

この図から分かるのは、関数ポインタを使うと、同じ呼び出し方で異なる関数を実行できるということです。

calcがadd_scoreを指していれば合計を計算し、diff_scoreを指していれば差を計算します。関数ポインタは、処理の切り替えを変数の代入のように扱えるところが大きな特徴です。

関数ポインタでは型を合わせることが大切

関数ポインタを使うときは、戻り値の型と引数の型を合わせる必要があります。

たとえば、次の関数ポインタは、int型の引数を2つ受け取り、int型を返す関数だけを指す想定です。

int (*calc)(int, int);

この場合、次のような関数は代入できます。

int add_score(int a, int b);
int diff_score(int a, int b);

しかし、戻り値がvoidだったり、引数の数が違ったりする関数は、この関数ポインタには合いません。

関数ポインタの型代入できる関数合わない関数の例
void (*fp)(void)void show_start(void)int add_score(int, int)
int (*calc)(int, int)int add_score(int, int)void show_start(void)
int (*op)(int)int double_value(int)int add_score(int, int)

関数ポインタは、ただ関数なら何でも代入できるわけではありません。
戻り値の型、引数の数、引数の型をそろえることが大切です。

型が合わない関数を無理に扱うと、コンパイル時に警告やエラーになったり、実行時に不安定な動作につながったりします。

関数ポインタを引数として渡す

関数ポインタは、別の関数の引数として渡すこともできます。

これは、コールバックと呼ばれる考え方につながります。

コールバックとは、ある関数に処理内容を渡しておき、必要なタイミングでその処理を呼び出してもらう仕組みです。

たとえば、値を表示する処理は共通にしておき、値を2倍にするのか、3倍にするのかだけを関数ポインタで切り替えることができます。

関数ポインタを引数にして処理を切り替える

次のプログラムでは、数値を変換する関数を関数ポインタとして渡し、print_result関数の中で呼び出します。

プロジェクト/ファイル名: Lesson60_3/main.c

#include <stdio.h>

// 値を2倍にする関数
int double_value(int n);

// 値を3倍にする関数
int triple_value(int n);

// 関数ポインタを受け取り、計算結果を表示する関数
void print_result(int (*operation)(int), int value);

int main(void) {
    int number = 10;

    // 2倍にする処理を渡す
    print_result(double_value, number);

    // 3倍にする処理を渡す
    print_result(triple_value, number);

    return 0;
}

int double_value(int n) {
    return n * 2;
}

int triple_value(int n) {
    return n * 3;
}

void print_result(int (*operation)(int), int value) {
    int result;

    // 渡された関数を呼び出して結果を受け取る
    result = operation(value);

    printf("計算結果:%d\n", result);
}

実行結果

計算結果:20
計算結果:30

このプログラムでは、print_result関数の第一引数が関数ポインタです。

void print_result(int (*operation)(int), int value)

operationは、int型の引数を1つ受け取り、int型を返す関数を指せます。

main関数では、double_valueとtriple_valueを渡しています。

print_result(double_value, number);
print_result(triple_value, number);

print_result関数の中では、受け取ったoperationを呼び出しています。

result = operation(value);

これにより、print_result関数は、どの変換処理を行うかを自分で固定せず、呼び出し側から渡された関数に任せることができます。

コールバックの考え方

関数ポインタを引数として渡すと、処理の一部を外から差し替えられます。

これがコールバックの基本的な考え方です。

役割内容
呼び出し側実行したい関数を選んで渡す
受け取り側渡された関数ポインタを必要なタイミングで呼び出す
関数ポインタ処理の差し替え口になる

たとえば、print_result関数は、値を表示する流れを担当しています。
しかし、値をどのように変換するかは、渡された関数によって変わります。

このように、共通処理の中に一部だけ変更可能な処理を組み込めるのが、関数ポインタの便利なところです。

関数ポインタ配列で複数の関数を管理する

関数ポインタは配列にすることもできます。

関数ポインタ配列を使うと、複数の関数をまとめて管理し、番号や添え字で呼び出し先を選べます。

たとえば、メニュー処理やコマンド処理のように、選択番号ごとに実行する関数が決まっている場面で便利です。

関数ポインタ配列で処理を順番に呼び出す

次のプログラムでは、3つの画面表示関数を関数ポインタ配列に入れて、順番に呼び出します。

プロジェクト/ファイル名: Lesson60_4/main.c

#include <stdio.h>

void show_menu(void);
void show_help(void);
void show_status(void);

int main(void) {
    void (*actions[3])(void) = { show_menu, show_help, show_status };
    int i;

    // 関数ポインタ配列に登録された関数を順番に呼び出す
    for (i = 0; i < 3; i++) {
        actions[i]();
    }

    return 0;
}

void show_menu(void) {
    printf("メニューを表示します。\n");
}

void show_help(void) {
    printf("ヘルプを表示します。\n");
}

void show_status(void) {
    printf("状態を表示します。\n");
}

実行結果

メニューを表示します。
ヘルプを表示します。
状態を表示します。

このプログラムでは、actionsが関数ポインタ配列です。

void (*actions[3])(void) = { show_menu, show_help, show_status };

これは、引数なし、戻り値voidの関数を3つ格納できる配列です。

添え字格納されている関数実行される処理
actions[0]show_menuメニューを表示する
actions[1]show_helpヘルプを表示する
actions[2]show_status状態を表示する

for文でactionsiを実行すると、配列に登録された関数が順番に呼び出されます。

actions[i]();

関数ポインタ配列を使うと、if文やswitch文で何度も分岐を書く代わりに、番号に対応する関数を呼び出す設計にできます。

図:関数ポインタを引数や配列で活用する

この図から分かること

この図から分かるのは、関数ポインタが単なる関数呼び出しの代替ではなく、処理を柔軟に切り替えるための仕組みとして使えるということです。

関数ポインタを引数にすれば、呼び出し側から処理内容を渡せます。関数ポインタ配列を使えば、複数の関数をまとめて管理し、添え字で選んで呼び出せます。これにより、プログラムの構造を整理しやすくなります。

関数ポインタを使うときの注意点

関数ポインタは便利ですが、使うときにはいくつか注意点があります。

NULLのまま呼び出さない

関数ポインタをNULLで初期化した場合、関数を代入する前に呼び出してはいけません。

void (*funcp)(void) = NULL;

/* funcp(); は危険 */

関数ポインタが有効な関数を指しているか確認してから呼び出しましょう。

if (funcp != NULL) {
    funcp();
}

型を必ず合わせる

関数ポインタの型と、代入する関数の型はそろえる必要があります。

戻り値の型、引数の数、引数の型が合っているかを確認しましょう。

宣言の丸かっこを忘れない

関数ポインタの宣言では、変数名と*を丸かっこで囲む必要があります。

int (*calc)(int, int);

この丸かっこがないと、意味が変わってしまいます。

読みにくい場合はtypedefも検討する

関数ポインタの宣言は、複雑になると読みづらくなります。

そのような場合は、typedefを使って型に名前を付ける方法もあります。

typedef int (*CalcFunc)(int, int);

このように書いておけば、次のように関数ポインタ変数を宣言できます。

CalcFunc calc = add_score;

typedefを使うと、関数ポインタを引数にする関数や、関数ポインタ配列が読みやすくなります。

関数ポインタが役立つ場面

関数ポインタは、次のような場面で役立ちます。

活用場面内容
処理の切り替え条件に応じて呼び出す関数を変更する
コールバック関数へ別の関数を渡して、あとから呼び出してもらう
関数テーブル複数の関数を配列で管理する
メニュー処理選択番号に応じて実行する関数を変える
汎用処理共通処理の一部だけを関数として差し替える

関数ポインタを使うと、処理を固定せず、呼び出し側で選べるようになります。

これにより、同じ流れの中で一部の処理だけを変えたい場合や、機能を追加しやすい設計にしたい場合に便利です。

関数ポインタの読み方に慣れるコツ

関数ポインタは、最初に宣言を見たときに難しく感じやすいです。

しかし、次の順番で読むと整理しやすくなります。

見る場所確認すること
戻り値の型その関数が何を返すか
(*変数名)関数ポインタ変数の名前
引数の型リストその関数が何を受け取るか

たとえば、次の宣言を見てみます。

int (*operation)(int);

これは、次の意味です。

部分意味
int戻り値はint型
(*operation)operationは関数ポインタ
(int)int型の引数を1つ受け取る

つまり、operationは、int型を1つ受け取り、int型を返す関数を指せます。

次の宣言も見てみましょう。

void (*actions[3])(void);

これは、次の意味です。

部分意味
void戻り値はvoid
(*actions[3])actionsは関数ポインタの配列
(void)引数なしの関数を指す

このように、複雑に見える宣言も、分解すると理解しやすくなります。

関数ポインタを理解すると設計の幅が広がる

関数ポインタは、関数のアドレスを保存し、必要なタイミングで呼び出すための仕組みです。

最初は宣言の形に戸惑いやすいですが、戻り値の型、ポインタ変数名、引数の型を分けて考えると、読み方が見えてきます。

関数ポインタを使うと、同じ呼び出し方で処理を切り替えたり、関数を引数として渡したり、複数の関数を配列で管理したりできます。

Lesson60_1では、引数なし、戻り値なしの関数を切り替えて呼び出しました。
Lesson60_2では、引数と戻り値を持つ計算関数を切り替えました。
Lesson60_3では、関数ポインタを別の関数へ渡し、コールバックの考え方を確認しました。
Lesson60_4では、関数ポインタ配列を使って複数の関数をまとめて管理しました。

関数ポインタは、C言語の中でも少し発展的なテーマです。しかし、使い方を理解すると、処理を柔軟に切り替えられるようになり、プログラムの設計にも広がりが出てきます。関数名は呼び出し先を表せること、関数ポインタはその呼び出し先を保存できること、この2つを意識しながら練習していきましょう。