C言語教室 解答編 第16、17回 - と、データの構造化
今回は教室2回分の解答をまとめてやります。まだ、これらの課題に取り組んでいない方は。この先を見る前に、ぜひ挑戦してください。課題と演習は
C言語教室 第16回 - 構造体あれこれ
C言語教室 第17回 - 共用体とは何か
にあります。もし挑戦したら、ぜひ結果を見せてくださいね。読みに行けるようにコメントを頂ければ幸いです。
さて、うっかり解答を見ないための余談です。そろそろC言語教室も、だいぶ難しいと感じてしまう方も出てきている頃かと思います。変数の型であるとか配列くらいまでは何となくわかるような気がして来ても、構造体であるとかが出てくると「そんなものを使わなくてもコードは書けるよね」くらいに思うかもしれません。
ただ複数の変数をまとめて扱う構造体という仕組みは、非常に便利でこれを使わないと、いろいろな変数を束ねることができず見通しが悪くなりがちなんです。古くは FORTRAN の 名前付き COMMON 文で、関数と関数の間で、変数をまとめてやり取りする仕組みがありますし(FORTRAN77以降にはFORTRANにもSTRUCTUREは、ちゃんとあります)、この後、教室でとりあえげるつもりのリスト構造やツリー構造などを扱うには必須のものなんです。
FORTRANコモンブロックをCから使う
また C++ で登場するクラスも、その仕組としては構造体を拡張したものであるとも言えます。オブジェクト指向と呼ばれる考え方では、PASCALやCのようにコードだけを構造化するだけではなく、データも構造化できるようになっているのです。データ自身が、その操作についてのコードを持つという形にすることで、複雑なデータであっても簡潔な記述で、処理を書けるようにしようという考えなんですね。
考え方さえわかってしまえば、後はサンプルコードなどを見ながら「見よう見まね」で動かしてみるのが一番です。構造を持つデータをアクセスするのに、何だかいろいろな名前を記号で結ぶ必要があるのが面倒といえば面倒ですね。ただ複雑な構造を持つデータを扱うときに、この手順があるからこそ、そのための面倒なコードを書かなくても済んでいるのです。
そろそろ課題に進みますか。
まず16回の方です。
今回はちょっと「カンニング」?して、ChatGPTに解答を作ってもらいました。
#include <stdio.h>
// 商品情報を格納する構造体
typedef struct {
int code; // 商品コード
int price; // 価格
} Product;
// 商品情報を出力する関数
void printProducts(Product products[], int n) {
printf("product_code\tprice\n");
for (int i = 0; i < n; i++) {
printf("%d\t\t%d\n", products[i].code, products[i].price);
}
}
int main() {
// 商品情報を格納する構造体配列を作成し、適当な内容を設定する
Product products[] = {
{ 1001, 980 },
{ 1002, 1280 },
{ 1003, 580 },
{ 1004, 1980 },
{ 1005, 880 },
};
// 商品情報を出力する関数を呼び出す
printProducts(products, 5);
return 0;
}
このコードに添えて
と説明があり、以下のような結果をもらえました。実際にコードを走らせたところ、ちゃんと同じ結果でしたよ。
商品コード 価格
1001 980
1002 1280
1003 580
1004 1980
1005 880
printPruducts に構造体の配列で渡しているのが気になったので、ポインタを使うように直してみました(配列のサイズも定義しておきました)。
#include <stdio.h>
// 商品情報を格納する構造体
struct product{
int code; // 商品コード
int price; // 価格
};
// 商品情報を出力する関数
void print_products(struct product *p, int n) {
printf("商品コード\t価格\n");
for (int i = 0; i < n; i++) {
printf("%d\t\t%d\n", p->code, p->price);
p++;
}
}
#define ARRY_SIZE 5
void main() {
// 商品情報を格納する構造体配列を作成し、適当な内容を設定する
struct product products[ARRY_SIZE] = {
{1001, 980},
{ 1002, 1280 },
{ 1003, 580 },
{ 1004, 1980 },
{ 1005, 880 },
};
// 商品情報を出力する関数を呼び出す
print_products(&products[0], ARRY_SIZE);
}
どちらでも良いのですが、構造体へのポインタをインクリメントすると、構造体の大きさだけアドレスが増えるという使い方が出来るんだよ。と覚えておいてください。
次は17回の分です。
これは2バイトの例が教室の中で出ていたので、簡単だとは思います。
#include <stdio.h>
union byte_access {
int i;
unsigned char b[4];
};
void print_hex_bytes(int num) {
union byte_access access;
access.i = num;
for (int i = 0; i < 4; i++) {
printf("%02x ", access.b[i]);
}
printf("\n");
}
void main() {
int num = 0x12345678;
print_hex_bytes(num);
}
この結果は処理系のエンディアンによって異なるのですが、例えば以下のように出力されます。
78 56 34 12
このテクニックを応用すると、変数の値がどのようにメモリに格納されているのかをデバッガなどを使うこと無く知ることが出来るので、ややこしいトラブルが発生したときなどに使うことがあります。
今回も Akio van der Meer さんから回答をいただくことが出来ました。ありがとうございます。今後も新たに回答をいただけたら追記しますので、コメントなどでお知らせ頂ければと思います。
(答案提出)C言語教室 第16回 - 構造体あれこれ
ブラウザ版環境 では、配列であるとか構造体の初期化ができないですよね。仕方がないので、そこは普通に代入してください。商品コードは数値にしてしまいましたが、文字列でも大丈夫でしたね。もし構造体の中に文字へのポインタでなく文字自身を入れるのであれば、strcpyで初期化する必要がありそうです。
構造体の配列を関数に渡すやり方は、2つの解答例を参考にしてください。ポインタで渡す場合はは、先頭のポインタからインクリメントすることで次の要素を読み書きします。
(答案提出)C言語教室 第17回 - 共用体とは何か
元になるコードが教室で示されていたので、そのまま使われたようですね。サンプルコードでバイトアクセス用のメンバを char にしてしまいましたが、unsigned char にしてしまうのが楽だと思います。printf の "%x" は値を符号なしと仮定するので、手を抜いてしまいました。サンプルが悪くて申し訳ありません。
参照渡しバージョンも頂いたのですが、最初はどこが参照渡しになっているのか悩みました。なるほどなるほど、ポインタを共用体にする形ですね。充分に共用体の書き方を習得されたようです。
だんだんややこしいことが増えてきましたが、ボチボチとお付き合い頂ければ幸いです。解答まで少し時間がかかってしまい申し訳ありません。
2023.4.3 追記
AyumiKatayama さんから答案を頂きましたので、コメントを追記します。まず 16回の分は以下に頂きました、
C言語教室 第16回 - 構造体あれこれ(回答提出)
構造体という新しい概念に慣れていただくために、課題はシンプルにしたつもりなのですが、もしかしたら少々物足りなかったのかもしれませんね。
商品コードも価格も負になるケースはあまり考えなくて良いので、確かに符号はいらなかったかもしれません。本当は商品コードは「文字列だろ!」とは思っていたのですが、そうするとプログラムが読みにくくなるので、整数で済ませてしまいました。
価格についても小数になることは確かにあるのですが、お金をコンピュータが得意な浮動小数点で使うと、時折、合計が合わなくなるなどの事故があるので、出来れば使いたくないのが本音です。実際、マイクロソフト系の言語である VB とか C# には通貨型という型があって、欧米のように価格が整数にならないことが一般的な場合でも使いやすくなっています。この型を使わなくても、整数を使って入出力で2桁ずらすような処理を使うことを聞いたことがあります。
構造体を示す時に str という略し方はあまり賛成できません。どうしても文字列に見えてしまいます。略すとすると構造体は他の言語ではレコード型に相当するので、r であるとか rec を使うのが良さそうです。もっとも構造体の名前は、後に教室で説明した typedef を使って大文字で始まる名前にしてしまうことが多いですね。CとC++で構造体名の名前空間が異なるので、両方使う人は、同じルールにして被らないようにするのが吉です。
ちょうど良さそうな機会なので、ここで少しだけ printf の書式指定の追加の説明をしておきます。普通に整数を出すのであれば、"%d" なのですが、この d の前に数字を入れると、その桁で右揃えにしてくれます。"%5d" とすれば、10 を出力すると ▢▢▢10 と左に空白を3つ補ってくれます。ちなみに "%05d" とすると 00010 とゼロを補ってくれるんですよね。これで価格などを右揃えに出来るので表示が見やすくなるのですが、難点は5桁の指定をしている時に6桁の数値を出すと、そこだけ右にはみ出してしまうんです。まあ数字の一部が捨てられるよりはマシなんですが。
17回は、以下に頂きました。
C言語教室 第17回 - 共用体とは何か(回答提出)
頂いたコードに特にコメントするところも見当たりません。いやぁ共用体をお使いになったことが無いですかぁ。考えてみれば、わざわざ使う羽目にならなければ必要はないものかもしれません。教室の説明でも書いたように、FORTRAN であるとか PASCAL の経験と、ファイルに詰め込まれている謎の固定長ランダムファイルを解釈するようなことがなければ覚えることもなかったかもしれません。
実は一番多用したのは、まだ C++ は無かった時代にオブジェクトなプログラミングが要求されたことがあって、そこで似ているけど異なるオブジェクト(実は図形オブジェクトでした)をツリー構造でつなぐということをやりまして、こういう時には共用体が大活躍しました。C++であれば親クラスから派生させることで、こういう苦労はあまりしなくて済むのですけどね。
sizeof 演算子は最強ですね。ただ配列の大きさを求めてくれるかはコンパイラによって微妙なところがありますし(最近のは大丈夫)、型が[] である必要があって、* の時には当然ですがエラーになると思います。いずれにせよ静的なものなので実行時エラーにはならないのが良いところですね。
エンディアンの話も書かれていますが、リトルエンディアンは意外と多いです。異なる大きさのデータを扱う時にはリトルの方が何かと都合が良いですし。ビッグだとサイズが違う時にどうも面倒になるみたいです。もっともネットワーク情報は必ずビッグエンディアンと決められているので、リトルエンディアンのCPUでいつも変換する羽目にはなっています。それで共用体の例としても出したわけです。そういえばミドルエンディアンという変態CPUもありましたね。
そろそろ、どこかのタイミングで変数の初期化について触れる必要がありそうです。ただ変数の初期化を説明する前に、static を済ます必要があるので、タイミングをはかっているところです。
ようやく頂いた答案に追いついたかな。今後ともよろしくお願いします。他の方の答案もお待ちしています。
ヘッダ画像は、いらすとや さんより
https://www.irasutoya.com/2020/04/blog-post_243.html