見出し画像

C言語教室 解答編 第14、15回 - と、余りの符号

今回は教室2回分の解答をまとめてやります。まだ、これらの課題に取り組んでいない方は。この先を見る前に、ぜひ挑戦してください。課題と演習は

C言語教室 第14回 - 変数型のサイズ

C言語教室 第15回 - 構造体の基本

にあります。もし挑戦したら、ぜひ結果を見せてくださいね。読みに行けるようにコメントを頂ければ幸いです。


恒例の「うっかり回答を見ないため」の閑話ですが、今回は割り算の余りの符号です。

型変換とも関連して符号があるのかないのかが、結構、トラブルの元になるケースを見てきましたが、割り算の余りを求める %演算子 についても注意点があります。

内部処理の都合もあって、古くからある処理系ではだいたいにおいて、マイナスの値の余りを取るとマイナスになるのですが、そもそも余りに負はありえないよね。ということで、プラスの余りを返す処理系もあったりします。python は後者なので、Cと異なる結果が得られてビックリしないようにしてください。python から Cのライブラリを呼び出すと、そちらでは C の計算になるので、余りを取る処理があれば関数の仕様をよく確認したほうがよさそうです。割り算って、うっかり0で割ると致命的なエラーにもなるので、前もって確認が必要なことがいろいろありますね。

その昔、割る数が2の累乗だとシフト演算に最適化される処理系で、このときに限って符号の扱いにバグがあって、ひどい目にあった覚えがあります。最適化されたときだけにでるバグって、それなりにあってリリースビルドでちゃんとテストを済ませないといけないのですが、これが理由でリリースビルドの最適化レベルを下げなければならないときは、ちょっと悲しくなります。この段階だと最適化にしくった原因をきちんと調べて対応するのが難しいことが多いんですよね。

【C言語】負の値に対する剰余演算の結果まとめ

『負数』の『除算・剰余』と『プログラミング』


では、解答に進みましょう。まず 14回の分からです。

課題
long 型配列に格納されているランダムな数値を short 型配列にコピーしなさい。short に格納できない値が含まれているときには、short で表現できる最大値または最小値にすること。

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

配列にある値に対する処理って、それなりに良くあるので、これを使って型の勉強をしてみようと考えました。単に範囲チェックなので、素直に書けますね。

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

#define LENGTH 10

void copy_array(long *src, short *dst, int length) {
  for (int i = 0; i < length; i++) {
    if (src[i] > SHRT_MAX) {
      dst[i] = SHRT_MAX;
    } else if (src[i] < SHRT_MIN) {
      dst[i] = SHRT_MIN;
    } else {
      dst[i] = (short) src[i];
    }
  }
}

void main(void) {
  long src[LENGTH];
  short dst[LENGTH];

  for (int i = 0; i < LENGTH; i++) {
    src[i] = rand() % (65536 * 2) - 65536;
  }

  copy_array(src, dst, LENGTH);

  printf("Source Array:\n");
  for (int i = 0; i < LENGTH; i++) {
    printf("%ld ", src[i]);
  }
  printf("\nDestination Array:\n");
  for (int i = 0; i < LENGTH; i++) {
    printf("%hd ", dst[i]);
  }
  printf("\n");
}

ブラウザ環境では、相変わらず limits.h を読み込めませんので、この行は削除してください。その場合に SHRT_MAX や SHRT_MIN が未定義だと怒られたら、以下の定義を追加してください。

#define SHRT_MIN -32768
#define SHRT_MAX 32767

乱数の発生に関しては、工夫していません。乱数列を変化させたければ、今までの教室の内容を踏まえて適宜、修正してみてください。同じ数値が出ないかもしれませんが、以下のような結果となりました。

Source Array:
43701 46417 -5033 38919 17730 16102 -49244 56171 28993 -61791 
Destination Array:
32767 32767 -5033 32767 17730 16102 -32768 32767 28993 -32768 

乱数を作るときに、負の数が出ること、範囲内の値も一定の割合で含まれるように、ちょっと工夫してあります。これで条件分岐のいずれのコードも走るようになっています。値の範囲に工夫しないと、走らないコードが出てしまい確認になりません。

演習の方も済ませましょう。

演習
short、unsigned short、long、unsigned long 型の数値を printf で出力する時に使う書式指定子を調べなさい。

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

short (や char)に対しては、printfが int に変換してしまうので、気が付かないかもしれないのですが、ちゃんと書式指定子があります。long や long long の場合は、ちゃんと書式指定子を書かないと不思議な結果がでてしまうかもしれません。long long も載せておきますね。

short %h
unsigned short %hu
long %l
unsigned long %lu
long long %ll
unsigned long long %llu

%d を付けなくても値を10進数で表示しますが、%xであれば16進、%oは8進で表示します。但し %x と %o は符号なし型を表示するので、符号あり型を渡すと暗黙の型変換が行われるので気をつけてください。なお %i でも %d と同じです。

最近は %lc でワイド文字が使えたり、%j(intmax_t) であるとか %z(size_t)や %t(ptrdiff_t)なんかが追加されていたり、%Xを使うと16進に含まれるアルファベットが大文字になったり、細かなバリエーションがあるようです。

C言語 printfのフォーマット指定子

書式指定構文: printf および wprintf 関数 - vc は、いろいろ独自拡張があるなぁ

printf の書式指定子は、まだいろいろありすぎるので、いずれ取り上げます。


次は 15回の分です。

課題
立体空間における座標を表す構造体を定義してから、それを使って立体空間の2点を結ぶ直線を表す構造体を定義した上で、適当な座標を設定して、その直線の長さを求めなさい。なお、直線の長さは √((x2-x1)^2+(y2-y1)^2+(z2-z1)^2)で求められる。※^は累乗演算子だと解釈してください。右上に小さく書くやつです。

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

久しぶりに数学にしてみました。複素数とか持ち出すと「習っていないぞ」と言われたら困るので、立体空間にしてみました。ここまでは習ってますよねぇ?

#include <math.h>
#include <stdio.h>

struct coordinate {
  double x;
  double y;
  double z;
};

struct line {
  struct coordinate start;
  struct coordinate end;
};

double distance(struct coordinate a, struct coordinate b) {
  return sqrt(pow(b.x - a.x, 2) + pow(b.y - a.y, 2) + pow(b.z - a.z, 2));
}

void main() {
  struct coordinate a;
  struct coordinate b;
  struct line line;

  a.x = 1;
  a.y = 2;
  a.z = 3;
  b.x = 4;
  b.y = 5;
  b.z = 6;
  line.start = a;
  line.end = b;
  double length = distance(line.start, line.end);
  printf("The length of the line is: %lf\n", length);
}

構造体には初期化構文もあるのですが、ブラウザ環境では動かないようなのと、まだ説明していないので、普通に適当な値を代入してあります。

The length of the line is: 5.196152

構造体の中に構造体があっても、このようにただ繋げて行けば良いだけです。カーニハン時代のC言語は、関数に構造体の値渡しが出来なかったのですが、最近の処理系は問題ないみたいです。ただ構造体はサイズが大きくなりがちなので、値渡しはコピーになるので乱用しないほうが良さそうです。


さて 14回、15回共に、Akio van der Meer さんより答案を頂きました。ご帰国で忙しいところをありがとうございます。まずは14回から。

(答案提出) C言語教室 第14回 - 変数型のサイズ

ちょっと苦労されたようですが、かなり丁寧に書かれていますね。OKだと思います。

locale ですが、まずC言語というよりも、OSがきちんと locale に対応していないと機能しないようです。私も Windows 環境では動作させられませんでした(エラーにはならないのですが切り替えられない)。Ubuntu で確認したところ

#include <stdio.h>
#include <locale.h>

void main() {
  setlocale(LC_NUMERIC, “ja_JP.utf8”;
  printf(“%’d\n”, 100000000);
}

100,000,000

と、ちゃんと3桁区切りに表示されました。

locale 処理はある意味正しいのですが、locale に応じて表示される場合はともかく、これをファイルに書き込まれてしまったりすると、読み出す時に同じ locale とは限らないので、ちょっと心配になります。リダイレクトされない状況なら安心なんですが。

仕様として3桁区切りだよというのであれば(そういう指定があることもありました)、自力で区切り処理を書いてしまうのも悪くはないと思います。最初に桁数を割り出して、その長さ(と符号の分)でバッファを用意して目的の値を桁ごとに順にはめ込んでいく感じかな?最下位から10で割った余りを入れて、元の数を10で割っていけばよさそうですが、先に符号の処理が必要なのと、文字列のお尻から埋めていくのは、癖のあるコードになるかもしれません。

15回の分は、こちらですね。

(答案提出) C言語教室 第15回 - 構造体の基本

構造体の使い方で苦労されたようですが、まずは「見よう見まね」で良いと思います。実は構造体の定義と変数の宣言には結構いろいろな流儀があって、自分で最もシックリくるパターンを見出してください。構造体の名前は struct とセットなので、構造体名と同じ名前の変数も宣言できるのですが、何かと混乱のもとになるかもしれません。またプログラムが複数のファイルに分割された場合に、それぞれで同じ構造体の宣言を使う必要があり、宣言はヘッダファイル側に置くことが多いのですが、そうすると同じ名前は一度しか宣言できないので、ひと工夫必要になったりします。このあたりの話は、構造体の話の最後あたりでまとめたいと思っています。

さて、頂いた答案ですが「それを使って立体空間の2点を結ぶ直線を表す構造体を定義した上で」としたので、出来れば配列ではなくて構造体を使ってほしかったのですが、配列であっても良い練習にはなっていると思います。コードを見て、一瞬どこで原点を代入しているのかと思いましたが、0 を掛ければ 0 ですものね。うまいこと処理されたようです。

今回はブラウザ環境を使われたようですが、gcc でやると math.h を使った場合、コンパイル時に -lm を付けないとエラーになるので、だんだん本質的ではないけれど実際に使うには必要な面倒な話も出てきます。実は sqrt を使わなくても pow(x, 0.5) で平方根を求められるのですが、気が付いた人はどれくらい居るのでしょうかね。


2023.4.3 追記

しばらくお忙しいようだった AyumiKatayama さんからも答案を頂いていたのですが、今度は私が年度末のドタバタでなかなか答案を見ることができず、お待たせしてしまいました。まずは 14回の分です。

【C言語】型と型変換 C言語教室 第14回 - 変数型のサイズ(回答提出)

課題の方からいきます。いわゆる「危険な型変換」と呼ばれる範囲の大きな型から小さな型への変換を実感してもらおうとして考えた課題なのですが、私の意図を察して頂いた解答に感激しております。ランダムにするのは忘れても構いません^^;。

C言語で「危険な型変換(元の値が保証されない変換)」が発生すると、範囲を超えた部分が単に捨てられて収まる範囲「のみ」が代入されるんですね。そうなると元の値の大小とも関係なく、符号すら変わってしまうということを示して頂きました。

このような時にエラーとして処理を中断してしまうのが良い場合も、もちろんあるのですが、既に出来上がったデータを取り扱うときなどはエラーにしても困るだけなので、次善の策として収まる範囲に「丸める」処理を課題としたわけです。pythonでは数値型は自動的に収まるように何とかしてくれるのは確かなのですが、それはそれで結構、複雑な内部処理が走っていて、それを外部に出す時にはまた範囲がどうなっているか苦労するのも確かなので、C言語より人にやさしいのは間違いないのですが、所詮pythonもCで書かれているので、苦労のなすり合いという気がしなくもありません。

演習の方は、printf のよろしくない仕様を知ってもらおうと考えたのですが、変数の型と書式指定子を揃えるのはプログラマの責任ということになっていて、知らない間に暗黙の型変換が発生しているのに気が付かず、妙な値を出力してしまう事故があることを理解して頂ければと思います。プログラムはあっているのに出力する方法を間違えて、バグがあるとコードを弄るという経験がきっとあるのではないかと思います。基本は int に向かって型変換されるので、int よりもサイズの大きな型に注意すれば大丈夫ではあります。ひとつでも unsigned があるとこちらが優先されるのは、まだ良いのですが、%x は unsigned を仮定しているのをお忘れなく。

15回は以下に頂きました。

C言語教室 第15回 - 構造体の基本 (回答提出)

座標が整数で距離だけ小数になっているのは、本質的ではないので構わないです。自乗するので絶対値を取る必要はないかもしれませんが、そもそもコストの低い関数なので気になりません。

今回のポイントとして、引き数に構造体そのものを渡すのか、構造体へのポインタを渡すのかなのですが、どうも昔のコンパイラは構造体のまま渡すことが出来なかった習慣が抜けきれていないのかもしれません。ただ構造体は大きくなりがちなので、やはり参照渡し(ポインタ渡し)がオススメです。もちろん const は付けたほうが良いのですが、ブラウザ環境くんが思ったように解釈してくれないので端折っています。どこかのタイミングで、ちゃんとしたコンパイラを基準にする時期がくるような気もしています。

残りの答案も順に見させて頂きます。常連さん以外の答案もお待ちしています。


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

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