見出し画像

C言語教室 番外編1 - 第4回の課題について

調子に乗ってC言語教室を進めていたら、Akio van der Meerさんに答案を頂いてしまいました。こういう形で記事にして頂くなんて恐縮です。

(答案提出)C言語教室 第4回 - 配列を関数に渡す

実は最初の答案には誤りがあって内心、これはうまいことネタになる、シメシメと思っていたのですが、ちゃんとご自身で気が付かれて訂正して頂いたようです。

私ももうかなり長いこと、C言語をまともに使うことは無くなっていて、今のPCにもC言語環境は入っていなかったので、思い出すために慌てて環境を入れた次第です。それなりに忘れていることもありますし、最新のC言語規格であるC11なんて全然追いついてはいないです。

C11 (C言語)


さてさて第4回の課題ですが、

配列のそれぞれの値の平均値を小数で返す関数を書きなさい。
配列のそれぞれの値の中でもっとも大きい値を返す関数を書きなさい。

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

というものでした。平均値から行きましょう。

#include <stdio.h>

double ave(int *x, int z) {
  int i;
  double s = 0.;

  for (i = 0; i < z; i++) {
    s += x[i];
  }
  return s / z;
}

void main() {
  int a[2];
  double v;
  int i;

  a[0] = 2;
  a[1] = 9;

  v = ave(a, 2);
  for ( i = 0; i < 2; i++) {
    printf("a[%d]=%d\n", i, a[i]);
  }

  printf("Ave=%f\n", v);
}

ちゃんと答えはでていますね。

a[0]=2
a[1]=9
Ave=5.500000

浮動小数点の値であることを明示するには小数点さえついていれば大丈夫なので、0.0と書いても0.と書いても同じです(0だけだと整数となるのでよろしくない、暗黙の型変換は後日やります)。+=演算子はもう大丈夫ですね?

ところで

C言語教室始まる

で紹介したブラウザ版のコンパイラではスルーしてしまうのですが、一般的なCコンパイラであるgccでは、&をつけた変数でaveを呼び出すと以下のようなメッセージが出ます。

.\ave.c: In function 'main':
.\ave.c:21:11: warning: passing argument 1 of 'ave' from incompatible pointer type [-Wincompatible-pointer-types]
21 |   v = ave(&a, 2);
   |           ^~
   |           |
   |           int (*)[2]
.\ave.c:3:17: note: expected 'int ' but argument is of type 'int ()[2]'
3 | double ave(int *x, int z) {
   |           ~~~~~^

C言語の配列とポインタの間には微妙な関係があって配列変数の変数名の型としてはint[]です。int[]の参照をとってもint*と等価ではあるのですが、参照を取らなくてもint[]とint*は同じなので、厳密には&ナシが正しいです(なのでエラーではなくて警告扱いです)。


次は最大値です。

#include <stdio.h>

int max(int *x, int z) {
  int i;
  int m;

  m = x[0];
  for (i = 1; i < z; i++) {
    if ( x[i] > m ) {
      m = x[i];
    }
  }
  return m;
}

void main() {
  int a[2];
  int m;
  int i;

  a[0] = 2;
  a[1] = 9;
  
  m = max(a, 2);

  for (i = 0; i < 2; i++) {
    printf("a[%d]=%d\n", i, a[i]);
  }
  printf("Max = %d\n", m);
}

さて最大値はいくつかな。

a[0]=2
a[1]=9
Max = 9

ポイントは最初の要素だけ比較をせずに初期値とするところですね。Cのfor文はループの条件が成立しない時には1度も実行されないので、もし配列の大きさが1であっても、この書き方で大丈夫です。

最初の要素だけ特別扱いするというのは、それなりに良くあるやり方なのですが、気持ち悪いなと思う人もいるかもしれません。そこで初期値として「最も小さい整数の値」がわかれば、これを使うことも出来ます。

この「最も小さい整数の値」は、limits.h の中で INT_MIN というマクロで定義されています。これを使って書き直してみます。ここから先は先のブラウザ環境だと"limits.h"が見つからないと怒られます。このヘッダファイルで使うのは以下の値の定義だけなので、#includeの代わりに、この行を追加して書いてしまってください。

#define INT_MIN (-2147483648)

limits.h
#include <stdio.h>
#include <limits.h>

int max(int *x, int z) {
  int i;
  int m;

  m = INT_MIN;

  for (i = 0; i < z; i++) {
    if ( x[i] > m ) {
      m = x[i];
    }
  }
  return m;
}

void main() {
  int a[2];
  int m;
  int i;

  a[0] = 2;
  a[1] = 9;

  m = max(a, 2);

  for (i = 0; i < 2; i++) {
    printf("a[%d]=%d\n", i, a[i]);
  }
  printf("Max = %d\n", m);
}

さてC言語には三項間演算子という見慣れないものがあります(他の言語でも使えるものがあります)、

式 ? 式が真の時の値 : 式が偽の時の値

という書き方をします。何に使うのかというと条件によって値が決まる時に if を使うこと無く式の中で書けてしまうのが便利なわけです。これを使って書き直してみましょう。m の初期値設定も宣言のところで一緒にしてしまいましょうね。

#include <stdio.h>
#define INT_MIN (-2147483648)

int max(int *x, int z) {
  int i;
  int m = INT_MIN;

  for (i = 0; i < z; i++) {
    m = (x[i] > m) ? x[i] : m;
  }
  return m;
}

void main() {
  int a[2];
  int m;
  int i;

  a[0] = 2;
  a[1] = 9;

  m = max(a, 2);

  for ( i = 0; i < 2; i++) {
    printf("a[%d]=%d\n", i, a[i]);
  }
  printf("Max = %d\n", m);
}

こうしてみると最大値であるか調べるところで、x[i]を2度評価しているのが気になります。配列の中身を取り出す時にポインタのままで使えるやり方も以前にやったと思うので、その形にしてみます。

#include <stdio.h>
#define INT_MIN (-2147483648)

int max(int *x, int z) {
  int i;
  int m = INT_MIN;
  int *p = x;

  for (i = 0; i < z; i++ ) {
    m = (*p > m) ? *p : m;
    p++;
  }
  return m;
}

void main() {
  int a[2];
  int m;
  int i;

  a[0] = 2;
  a[1] = 9;
  
  m = max(a, 2);

  for (i = 0; i < 2; i++) {
    printf("a[%d]=%d\n", i, a[i]);
  }
  printf("Max = %d\n", m);
}

これについてはコンパイラの最適化で配列のままであっても、このコードと同等のものを吐いてくれることもありそうですが、他の人のコードを見る時に、こういう形になっているものを見ることもあるかと思います。ここまで来ると、あと一歩短くしてみたくなります。

#include <stdio.h>
#include <limits.h>

int max(int *x, int z) {
  int m= INT_MIN; 
  int i;
  int *p;

  for (i = 0, p = x; i < z; i++, p++ ) m = (*p > m) ? *p : m;
  return m;
}

void main() {
  int a[2];
  int m;
  int i;

  a[0] = 2;
  a[1] = 9;

  m = max(a, 2);

  for ( i = 0; i < 2; i++) {
    printf("a[%d]=%d\n", i, a[i]);
  }
  printf("Max = %d\n", m);
}

う~ん厳しいな。ブラウザ版のCだとfor文の中のカンマを許してくれません。gccではちゃんと動きますので、こういう書き方があるとだけ見ておいてください。

最大値を探すfor文がだんだんパズルのようになってきましたね。複数の式はカンマ演算子を使って続けて書くことができるので、for文の初期化と継続の部分に2つの式を書いてしまうこともできます。またfor文は中身がひとつの文しかないのであれば{}で括る必要もありません。ただ今のコンパイラはここまでやってもやらなくても殆ど影響が無いですし、後でforの中身を追加した時に{}をつけ忘れたり、行を変えておかないとデバッガでブレークポイントを仕掛けられなくなったりするので、自分の流儀に合わせれば充分だと思います。但し、こういう書き方をする人もいるので、見かけた時にビックリしなければ大丈夫です。

課題はあえて、引っ掛かり易いところや、少し調べないとならない内容を混ぜ込んでいるつもりなので、わからないところが残っても、結果が出れば良しと思ってくださいませ。

(2022-11-20 追記)

上記の最大値の例として「最小の整数の値」という定数をシステムから得て使うようなコードを書きましたが、最初の要素で初期化してループの回数をひとつ減らす書き方をしても全く問題ありません。この手の処理は大体において最初と最後に例外的な処理が必要になることは多いのですが、自分にとってわかりやすいと判断したロジックを使えば充分です。最近のコンパイラは、この手の前処理などのコードを上手にループに畳み込んでくれることもあります(逆にループ内での例外的な処理だけをループから追い出しているケースも見つけたことがあります)。そもそも最大値を求めるロジックは配列のサイズに対して線形(リニアまたは1次元)の処理量であることが変わらなければ大勢に影響はありません。それでもパフォーマンスが気になる時は、あれこれ考えるよりもコードに対して、何らかの測定を行って判断したほうが賢明です。例えば今流のCPUであれば配列のサイズが大きければ配列を分割し、その部分配列ごとにスレッドを起して並列動作させ、最後にスレッドごとの最大値から全体の最大値を得たほうが結果が早く出るかもしれません。この時はスレッドのオーバーヘッドとのトレードオフになるので、測ってみないことにはしきい値は見えてこないです。細かい効率化はコンパイラに任せ、プログラムを書く人は適切なアルゴリズムを選択することに専念したほうが良さそうです。


余談

Cパズルブック 単行本

最初の邦訳は1985年にASCIIから出たものだったと思うのですが、当時はこの本で知識を競った記憶があります。2000年に出たものは読んで確認していないのですが、おそらくその後のC言語の改訂に合わせたものになっているのではないかと思います。とはいえ、そこからでも20年以上が経過しているので、最新のC言語には合わない部分があるとは思います。C言語に自信が無い方が読むと、C言語が嫌いになることは間違いないので、くれぐれもある程度使えるようになってからチャレンジしてください。


ヘッダ画像は、以下のところのものを使わせていただきました。
https://photosku.com/photo/2615/

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