プログラミングなどなど

プログラミングに関係しそうな内容を

C言語:和を計算する高階関数で関数ポインタを使ってみる

関数ポインタとは

関数ポインタとは関数を指すポインタのことです。

私がC言語を学び始めた頃はポインタについてよく理解していないこともあり、関数ポインタが何の役に立つのかわかりませんでした。
そんな中、関数ポインタを理解するきっかけになったのは、「計算機プログラムの構造と解釈」の第1章「手続きによる抽象の構築」でした。
(注:この本ではSchemeという言語が使われていますのでC言語ではありません。)

この本で学んだことを練習がてらC言語で紹介したいと思います。
流れとしては以下のようになります。

  1. 和を計算する関数をいくつか定義する。
  2. 共通部分を抜き出した関数を定義する。(関数ポインタを引数にとる関数)
  3. 共通部分以外を関数として定義する。

和について

共通部分を抜き出す元ネタとして最初に次の関数を作成してみましょう。

sumId
1からnまでの整数の和を求める関数


sumId(n) = \displaystyle \sum_{k=1}^{n} k

sumSquare
1からnまでの整数を二乗した値の和を求める関数


sumSquare(n) = \displaystyle \sum_{k=1}^{n} k^{2}

和を計算する関数

sumId01.c
#include "util.h"

int sumId(int n)
{
	int acc = 0;

	int i;
	for (i = 1; i <= n; i++) {
		acc += i;
	}
	return acc;
}
sumSquare01.c
#include "util.h"

int sumSquare(int n)
{
	int acc = 0;

	int i;
	for (i = 1; i <= n; i++) {
		acc += i * i;
	}
	return acc;
}
util.h
#ifndef __UTIL_H__
#define __UTIL_H__

int sumId(int);
int sumSquare(int);

#endif
main01.c
#include <stdio.h>
#include "util.h"

int main(void)
{
	int n = 10;
	printf("sumId : %d %d\n", n, sumId(n));
	printf("sumSquare : %d %d\n", n, sumSquare(n));

	return 0;
}
コンパイルと実行
$ gcc -Wall sumId01.c sumSquare01.c main01.c
$ ./a.out
sumId : 10 55
sumSquare : 10 385

和を計算する関数の比較

sumIdとsumSquareをsdiffで比較すると以下のようになります。

和の関数定義の比較
$ sdiff -t sumId01.c sumSquare01.c
#include "util.h"                                  #include "util.h"

int sumId(int n)                                |  int sumSquare(int n)
{                                                  {
        int acc = 0;                                       int acc = 0;

        int i;                                             int i;
        for (i = 1; i <= n; i++) {                         for (i = 1; i <= n; i++) {
                acc += i;                       |                  acc += i * i;
        }                                                  }
        return acc;                                        return acc;
}                                                  }

違いは関数名と変数accにどのような値を足しこむかを決める部分です。そのまま足す(acc += i) と二乗して足す(acc += i * i)の部分です。

通化の準備

関数の役割の一つに処理の共通化があります。これから同じ部分を共通化した関数を書くわけですが、その前にプログラムをもう少し変更しておきます。

sumId02.c
#include "util.h"

int sumId(int n)
{
	int acc = 0;

	int i;
	for (i = 1; i <= n; i++) {
		acc += id(i);
	}
	return acc;
}
sumSquare02.c
#include "util.h"

int sumSquare(int n)
{
	int acc = 0;

	int i;
	for (i = 1; i <= n; i++) {
		acc += square(i);
	}
	return acc;
}
util.h
#ifndef __UTIL_H__
#define __UTIL_H__

int sumId(int);
int sumSquare(int);

int id(int);
int square(int);

#endif
util.c
#include "util.h"

int id(int x) { return x; }
int square(int x) { return x * x; }

コンパイルと実行結果

$ gcc -Wall sumId02.c sumSquare02.c util.c main01.c
$ ./a.out
sumId : 10 55
sumSquare : 10 385
和の関数定義の比較②

僅かな変更ですが違いが関数idとsquareの呼び出しだけとなり見やすなったと思います。

$ sdiff -t sumId02.c sumSquare02.c
#include "util.h"                                  #include "util.h"

int sumId(int n)                                |  int sumSquare(int n)
{                                                  {
        int acc = 0;                                       int acc = 0;

        int i;                                             int i;
        for (i = 1; i <= n; i++) {                         for (i = 1; i <= n; i++) {
                acc += id(i);                   |                  acc += square(i);
        }                                                  }
        return acc;                                        return acc;
}                                                  }

共通部分は値を足しこみ、戻り値accを返す部分になります。個別の部分はどのように値を足しこむかを決める関数idやsquareの部分です。
個別の部分は関数の引数として渡すようにしたいので、ここで関数ポインタを使用します。

和を計算する関数(足し込み部分一般化)

sumf.c
#include "util.h"

int sumf(int n, int (*f)(int))
{
	int acc = 0;

	int i;
	for (i = 1; i <= n; i++) {
		acc += f(i);
	}
	return acc;
}

sumfは渡す関数fによってどのような和を求めるかを決めることができます。引数int (*f)(int) が関数ポインタとなります。どのようなポインタかというと、int型の引数を一つ取りint型を戻り値に持つ関数へのポインタということになります。関数を引数に取るsumfのような関数は高階関数と呼ばれます。
sumfは以下のような計算を行うイメージです。


sumf(n, f) = \displaystyle \sum_{k=1}^{n} f(k)

util.h
#ifndef __UTIL_H__
#define __UTIL_H__

int sumId(int);
int sumSquare(int);

int sumf(int, int (*)(int));

int id(int);
int square(int);

#endif
main02.c
#include <stdio.h>
#include "util.h"

int main(void)
{
	int n = 10;
	printf("sumId : %d %d\n", n, sumId(n));
	printf("sumSquare : %d %d\n", n, sumSquare(n));

	printf("sumf id : %d %d\n", n, sumf(n, id));
	printf("sumf square : %d %d\n", n, sumf(n, square));

	return 0;
}

コンパイルと実行

$ gcc -Wall sumId02.c sumSquare02.c sumf.c util.c main02.c
$ ./a.out
sumId : 10 55
sumSquare : 10 385
sumf id : 10 55
sumf square : 10 385

おわりに

関数ポインタを理解する手助けになれば幸いです。