見出し画像

C言語教室 解答編 第24回 - と、オブジェクト指向の入り口

今回は、

C言語教室 第24回 - 関数ポインタとは

の解答です。関数ポインタなんて聞いたことも無いかもしれないですし、聞いたことがあっても使ったことがない、そもそも、それは美味しいの?と思う人のほうが多いかもしれないなとは思います。そこで課題は易しめにしたつもりなんですが、やはり対象が理解できないと難しいかもしれません。少なくとも書き方はちょっと面倒です。まだ課題をやっていないという方は、是非、挑戦をしてできれば、結果をお知らせいただけたら幸いです。


という事で、まだ課題を解いていないという方が、うっかり解答を見ないための閑話です。

プログラムと言うかコードを、そのポインタであれ変数に入れておいて呼び出せるというのは、アセンブラの世界では、コードも変数もメモリにある数字に過ぎないので特別なことではないのですが、高級言語ではやっかいなことが殆どです。

いわゆるテーブルジャンプと呼ばれるアドレスを配列のような一覧にしておいて、特定の要素にあたるアドレスに飛ぶような方法は、FORTRANやBASICの場合は教室で説明したような特別な構文を使いますし、C言語のswitch/caseをコンパイルすると、このテーブルジャンプのアセンブラコードが出力されます。

それ以外の関数ポインタの使い方は、昔はそれほど多くは無かったのですが、割り込み処理や並行処理をOSが扱うようになり、次回でやるコールバック関数という使い方が便利だということになり、この関数ポインタが使われることが増えてきました。

さらに GUI の登場により、オブジェクト指向という考えが広まり、対象となるデータごとに処理を逐一書いていくのではなく、データの方に必要な処理を書くとコードがとてもシンプルになり再利用しやすいということが理解されるようになりました。これが何を言っているかがわかるためには、もう少し説明が必要だとは思いますが、C++のクラスというものは、この関数ポインタをとても上手に使って継承であるとか、メソッドを管理しています。

もっともオブジェクト指向のコードはデータによって実行される内容が決まるので、処理の手順がいわゆる動的なものとなり、テストが難しくなりますし、そもそも関数ポインタの機能は強力すぎるので、スタックにコードを置いて、それを実行するなんていうウィルスばりのコードも書くことが出来るので、システムのセキュリティに関する知識を持っていないと、予想外のことが起こる場所でもあります(無理やり動的にパッチをあてるのに使ったことがありますが)。


さて、そろそろ課題に入ります。今回の課題は

課題
1. まず整数の引き数を2つ持ち、この引き数の和を返す関数と、差を返す関数を書きなさい。
2. 整数を2つと先に作った和と差を求める関数のポインタ型(整数の引き数が2つ、戻り値が整数の関数)を持つ構造体を定義し、この構造体の配列を作って、それぞれの要素の2つの整数型のメンバ変数には適当な値を入れてください。
3. 構造体配列の最初の整数型のメンバ変数の値が奇数だった場合は、構造体の関数ポインタ型のメンバ変数に和を求める関数、偶数だった場合は、差を求める関数を代入してください。
4. 構造体配列の要素を順に取り出し、整数型の2つのメンバ変数を引数として、関数ポインタ型のメンバ変数の関数を呼び出し、引き数と戻り値を表示してください。

https://note.com/kazushinakamura/n/nc41b1dc6d3d3

でした。関数ポインタをどう扱っていいかを課題にするのは難しそうだったので、課題にある順にコードを書いていって書き方を理解してもらえればと思います。

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

/* 1 */
int add(int i, int j) {
  return i + j;
}

int sub(int i, int j) {
  return i - j;
}

/* 2 */
typedef int(*FUNC)(int, int);
typedef struct datatbl {
  int i1;
  int i2;
  FUNC f;
} DATATBL;

void init_data(DATATBL *dt, unsigned int sz) {
  srand(0);
  for (int i = 0; i < sz; i++) {
    dt[i].i1 = rand();
    dt[i].i2 = rand();
  }
}

/* 3 */
void assign_func(DATATBL *dt, unsigned int sz) {
  for (int i = 0; i < sz; i++) {
    if ((dt[i].i1 % 2) == 0)
      dt[i].f = add;
    else
      dt[i].f = sub;
  }
}

/* 4 */
void operation(DATATBL *dt, unsigned int sz) {
  for (int i = 0; i < sz; i++) {
    printf("[%d]%d,%d->%d\n", i, dt[i].i1, dt[i].i2,
      (*dt[i].f)(dt[i].i1, dt[i].i2));
  }
}

/* Main */

#define DATA_LENGTH 3

void main() {
  DATATBL* dt = (DATATBL*)malloc(sizeof(DATATBL) * DATA_LENGTH);
  if (dt != NULL) {
    /* Set Data */
    init_data(dt, DATA_LENGTH);
    /* Assign Function */
    assign_func(dt, DATA_LENGTH);
    /* Execute */
    operation(dt, DATA_LENGTH);
  }
  free(dt);
}

構造体配列の押さえ方は、グローバルで書いても構いません。ポイントになるのが、関数ポインタ変数の型宣言と、関数ポインタを使っての関数呼び出しの部分です。値は乱数で生成したのですが乱数系列の初期化はさぼったので、毎回同じ値が出るはずです。もちろん、固定値で与えても、乱数に凝っていただいても構わないですよ。今回はたまたま以下の結果となりました。

[0]38,7719->7757
[1]21238,2437->23675
[2]8855,11797->-2942

課題にあるような値と結果しか出力しませんでしたので、正しい結果であるかは目で追ってください。足しているか引いているかだけなので、そんなに難しいことは無いはずです。


さて、今回も AyumiKatayama さんからの回答を頂きました。いつもありがとうございます。

C言語教室 第24回 - 関数ポインタとは(回答提出)

既に関数ポインタの使い方をご理解いただけているので、課題に悩むところもないですし、サラッと書かれていますが、関数ポインタで関数を呼び出すのにも、実はいろいろ苦労がありますよね。戻り値と引き数の数と型が完全に一致したときのみ、同じ型になるので、ここにポインタの型キャストが必要になったりすると、結構何が何だかわからなくなることがあります。

その昔、XWindow が関わる仕事をしたことがあるのですが、まだC++が使えずにCでオブジェクト操作をしていたので、この手のコードだらけで気が遠くなった覚えがあります。逆に言うとC++になった時に、あまり苦労することもなく、いやぁC++は便利だなと思ったものです(MFCが出来る前のWindowsSDK時代も同様)。


次回は、コールバック関数を取り上げます。

ヘッダ画像は、いらすとや さんより
https://www.irasutoya.com/2020/04/blog-post_243.html

#C言語 #答え合わせ #プログラミング講座 #関数ポインタ #オブジェクト指向 #テーブルジャンプ


この記事が気に入ったらサポートをしてみませんか?