関数ポインタを使ってみる

突然C言語のはなし。突然なのはいつものことだが。今日は、授業でちらっとだけ話題に出て、おもしろそうなので試しに関数ポインタを利用するようなコードをかいてみた。

まずは、関数ポインタを引数にとるような関数print_decoratedを定義する。

#include <stdio.h>
#include <string.h>
#include <ctype.h> 
const int MAX_BUF_LEN = 255;

void print_decorated(char *msg, void (*decorate)(char *)) {
    char copy_msg[MAX_BUF_LEN];
    strncpy(copy_msg, msg, MAX_BUF_LEN);

    decorate(copy_msg);
    printf("%s\n", copy_msg);
}

この関数は、受け取った文字列msgのコピーを関数decorateで変更してから表示する。

関数定義部分のいかにも怪しい、void (*decorate)(char *)が関数ポインタの仮引数になっている。この怪しい表記にびびってこれまで関数ポインタを使ってみなかったとも言える。しかし、一見、普通の仮引数と違っていても、関数定義のようなものだと思えばそんなに複雑ではないといことがわかった。単に、返り値(void)、ポインタ名(*decorate)、引数(char *)を順に並べただけ。関数の中でも単に呼び出せば良いだけなので、使い方も簡単ぽいぞ。

だいたい様子がわかったので、研究室の友達といっしょに、print_decoratedに渡せる、void (*decorate)(char *)に当てはまるような関数をいくつか定義してみた。

void upper(char *msg) {
    int i = 0;
    for (i = 0; i < strlen(msg); i++) {
        msg[i] = toupper(msg[i]);
    }
}

void lower(char *msg) {
    int i = 0;
    for (i = 0; i < strlen(msg); i++) {
        msg[i] = tolower(msg[i]);
    }
}

void reverse(char *msg) {
    int i = 0;
    int j = strlen(msg) - 1;
    char copy_msg[MAX_BUF_LEN];
    strncpy(copy_msg, msg, MAX_BUF_LEN);
    for (i = 0; i < strlen(msg); i++) {
        msg[i] = copy_msg[j];
        j--;
    }
}

void caesar(char *msg) {
    int i = 0;
    for (i = 0; i < strlen(msg); i++) {
        msg[i] = msg[i] + 3;
    }
}

これらの関数は全部、void (*decorate)(char *)に合致してるのでprint_decorated関数の引数に渡すことができる。実際に渡してみたコードを次のように書いてみた。

int main(int argc, char *argv[]) {
    char msg[MAX_BUF_LEN];
    strncpy(msg, "Hello, function pointer", MAX_BUF_LEN);

    printf("%s\n", msg);

    print_decorated(msg, upper);
    print_decorated(msg, lower);
    print_decorated(msg, reverse);
    print_decorated(msg, caesar);

    return 0;
}

実行結果:

$ gcc -o func_ptr func_ptr.c
$ ./func_ptr
Hello, function pointer
HELLO, FUNCTION POINTER
hello, function pointer
retniop noitcnuf ,olleH
Khoor/#ixqfwlrq#srlqwhu

おお、たしかに引数で渡した関数が呼び出されているみたい。外部から関数の振る舞いをいろいろと変更することができた。個人的C言語七不思議の一つである関数ポインタをなんとか攻略した気分。

しかしこれ、使い方の感覚としてはPerlとかスクリプト言語のクロージャっぽいな。ただ、関数ポインタによる呼び出しは単にその関数へとんでいるだけのようだ。クロージャって言うともっと環境ごとまるまるひっぱり混んでいる印象だよな。うーん、ちょっとこの流れで、Perlのクロージャ周りも少し調べてみるか。