見出し画像

C言語教室 解答編 第25回 - と、記号の話

今回の教室では関数ポインタの使い方として、コールバック関数を取り上げました。この解答を説明します。

C言語教室 第25回 - コールバック関数

コールバック関数という名前ですが、別にコールバックしてもらうとは限らず、引き数に関数ポインタを渡すだけなんですが、これが実に便利で特に何らかの並行処理を行う時には必須のテクニックです。

さて、まだ課題をやっていないという方は、是非、挑戦をしてできれば、結果をお知らせいただけたら幸いです。期限は設けていないので、いつでもOKですよ。


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

C言語って、記号を上手に使っていて少ない文字数で多くの表現ができるのが素敵ではあるのですが、前後関係で同じ記号に異なる意味を持たせているところがあって混乱します。似たようなことは予約語にもあって static なんて何が静的なんだよと思うこともシバシバです。

記号の一覧 | Programming Place Plus C言語編

他の言語では、ブロックの開始と終了を begin/end と書く必要があったり、多くの演算子も and とか not のように書かなければなりません。.net の世界でもC系列であるC#とVB.NETを比べると、VBでは何かと長く文字列を連ねる必要があって辟易します。

ただC言語では言語仕様で、ASCIIのうち国際的に担保されていない記号を使ってしまっているために、一部の記号が使えないケースも有り、トライグラフという仕組みで代替手段が用意されています。これ、入出力のエスケープシーケンスとはまったく異なる仕組みなので注意してください。と言っても使っているのを見たことは無いですね(どうやら廃止の方向にあるようです)。

トライグラフ

また、C++では演算子の一部を記号ではなくキーワードで表現することも出来るのですが、これも見た覚えはないです。

CとC++の演算子 - C++の演算子の代替表現 を見てください。

コードを書く人は記号のもつ意味を誤解しないように暗黙に使い方によって空白の置き方を変えている人も多いのですが、そこに規格は無いので、先入観を持たずコンパイラの気持ちになって読む必要があります。


そろそろ課題にいきましょう。今回の課題は

課題
CTRL-Cが2度続けて押された時にプログラムを中断するようなシグナルハンドラ関数を書きなさい。

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

でした。signal関数のプロトタイプ宣言は signal.h にありますが、

void (*signal(int sig, void (*func)(int)))(int);

signal.h

戻り値voidで整数の引き数をひとつ持つ関数を用意すれば良いわけです。signal関数自身は、この関数の名前を2番めの引き数に書けば良いだけです。最初の引き数はこのハンドルするシグナルの種類で、定数マクロもsignal.hで宣言されています。しかし、なかなか読むのが難しい宣言ですね。

シグナルの種類はOSによって異なりますが、SIGINT が CTRL-C で、他には SIGHUP がよく使われます。

SIGNAL

Linuxのシグナルまとめ

シグナルの中にはシステムがいろいろな理由で発生させるものが多いのですが、コマンドラインから kill コマンドでプロセスに送る方法も良く使います。

ここまでわかればハンドラ関数を書くことは難しくありません。

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

int is_interrupted = 0;

void signal_handler(int signum) {
  if (signum == SIGINT) {
    if (is_interrupted) {
      printf("Exiting program...\n");
      exit(0);
    } else {
      printf("CTRL-C detected. Press again to exit.\n");
       is_interrupted = 1;
    }
  }
}

void main() {
  signal(SIGINT, signal_handler);
  while (1) {
    if (is_interrupted) {
      printf("Interrupting...\n");
      is_interrupted = 0;
    }
    printf("Doing some work...\n");
    sleep(5);
  }
}

このプログラムを走らせて CTRL-C を続けて打ち込むと

Doing some work...
CTRL-C detected. Press again to exit.
Interrupting...
Doing some work...

こんな出力が出てプログラムが終わります。続けてと書きましたが、一度セットした変数を戻すところがちょっと雑なので、やや不思議な動作になったらごめんなさい。


今回も AyumiKatayama さんからの答案を頂くことが出来ました。

C言語教室 第25回 - コールバック関数(回答提出)

シグナル処理もスレッドを使っていないものの、並行処理ではあるので、いろいろと気をつけることがありますね。ハンドラで指定した関数は、他の関数を実行中に一時的に割り込まれて実行され、終了後は元の関数の実行が継続されることが殆どなので、そもそもあまり複雑な処理を書くところではありません。

大抵はフラグとなっているグローバル変数の値を変更するくらいの処理に留め、メインとなっている処理の中で、時々このフラグをチェックするという書き方が基本です。そもそもハンドラ関数で使って良い関数は制限されています。

安全なシグナルハンドラを実装するには

確かにこの中には printf はもちろん入出力が出来るような関数がありません。入出力はそれ自身、並行処理を使っていることが普通なので、無理もないのですが、これではデバッグするにも四苦八苦です。printf のソースは見ていないのですが、一般論として読み出し専用の静的領域を使うだけ(固定的な文字列)であれば、まあ大丈夫だとは思います。スタックの状態を当てに出来ないので、ローカル変数を使うのはちょっと怖いですし、静的な変数であっても同時にハンドラが呼ばれる可能性が無くないので、リエントラントにする必要はあります。ですのでハンドラ中での入出力は少なくともリリースビルドでは諦めて、状態だけ保存してメインループでの処理で読み出して使うのが間違いないでしょう。

シグナルハンドラにprintf()を書いてはいけない

こういう並行処理のノウハウは、少しばかり敷居が高すぎるのですが、非同期ファイル処理のアタリで触れようかとは思っています。

今後も鋭いツッコミをお願いします。


という事で、次回はソートを取り上げる予定です。

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

#C言語 #答え合わせ #プログラミング講座 #関数ポインタ #記号 #演算子 #トライグラフ

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